Skip to content

Commit 92ebf45

Browse files
authored
Block the existing keyword for extension resources that have no readable scopes (#18260)
## Description Since some extension resources do not have a concept of existing resources, it is a requirement to block the existing keyword when authoring these extension resources. ## Example Usage ### Write-Only Resource **Scenario**: A resource type with no readable scopes but has writable scopes cannot be used with the `existing` keyword. ```bicep // This will produce error BCP441 resource writeOnlyResource 'Microsoft.Example/writeOnlyType@2024-01-01' existing = { name: 'myResource' } ``` **Error Message** - Resource type "Microsoft.Example/writeOnlyType@2024-01-01" cannot be used with the 'existing' keyword. ## Checklist - [x] I have read and adhere to the [contribution guide](https://github.com/Azure/bicep/blob/main/CONTRIBUTING.md). ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/18260)
1 parent c88949d commit 92ebf45

File tree

5 files changed

+103
-2
lines changed

5 files changed

+103
-2
lines changed

src/Bicep.Core.IntegrationTests/ExtensibilityTests.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,60 @@ extension kubernetes with {
271271
});
272272
}
273273

274+
[TestMethod]
275+
public async Task Extension_import_existing_blocked_for_writeonly_resource()
276+
{
277+
// Create a mock extension with a write only resource (no readable scopes, has writable scopes)
278+
var services = CreateServiceBuilder();
279+
await ExtensionTestHelper.AddMockExtensions(services, TestContext, CreateMockExtWithWriteOnlyResource());
280+
281+
var mainUri = new Uri("file:///main.bicep");
282+
var files = new Dictionary<Uri, string>
283+
{
284+
[mainUri] = """
285+
extension 'br:mcr.microsoft.com/bicep/extensions/writeonly/v1:1.2.3'
286+
287+
resource myResource 'writeOnlyType@v1' existing = {
288+
identifier: 'test-resource'
289+
}
290+
"""
291+
};
292+
293+
var compilation = await services.BuildCompilationWithRestore(files, mainUri);
294+
295+
compilation.Should().HaveDiagnostics(new[] {
296+
("no-unused-existing-resources", DiagnosticLevel.Warning, "Existing resource \"myResource\" is declared but never used."),
297+
("BCP441", DiagnosticLevel.Error, "Resource type \"writeOnlyType@v1\" cannot be used with the 'existing' keyword."),
298+
});
299+
}
300+
301+
[TestMethod]
302+
public async Task Extension_writeonly_resource_can_be_deployed_normally()
303+
{
304+
// Create a mock extension with a write only resource
305+
var services = CreateServiceBuilder();
306+
await ExtensionTestHelper.AddMockExtensions(services, TestContext, CreateMockExtWithWriteOnlyResource());
307+
308+
var mainUri = new Uri("file:///main.bicep");
309+
var files = new Dictionary<Uri, string>
310+
{
311+
[mainUri] = """
312+
extension 'br:mcr.microsoft.com/bicep/extensions/writeonly/v1:1.2.3'
313+
314+
// Write-only resources should work fine for normal (non-existing) deployment
315+
resource myResource 'writeOnlyType@v1' = {
316+
identifier: 'test-resource'
317+
}
318+
319+
output resourceId string = myResource.identifier
320+
"""
321+
};
322+
323+
var compilation = await services.BuildCompilationWithRestore(files, mainUri);
324+
325+
compilation.Should().NotHaveAnyDiagnostics();
326+
}
327+
274328
[TestMethod]
275329
public void Kubernetes_competing_imports_are_blocked()
276330
{
@@ -1121,6 +1175,32 @@ private static MockExtensionData CreateMockExtWithDiscriminatedConfigType(string
11211175
}))
11221176
});
11231177

1178+
private static MockExtensionData CreateMockExtWithWriteOnlyResource(string extName = "writeonly") =>
1179+
ExtensionTestHelper.CreateMockExtensionMockData(
1180+
extName, "1.2.3", "v1", new CustomExtensionTypeFactoryDelegates
1181+
{
1182+
CreateResourceTypes = (ctx, tf) =>
1183+
{
1184+
var bodyType = tf.Create(() => new ObjectType(
1185+
"writeOnlyBody",
1186+
new Dictionary<string, ObjectTypeProperty>
1187+
{
1188+
["identifier"] = new(ctx.CoreStringTypeRef, ObjectTypePropertyFlags.Required | ObjectTypePropertyFlags.Identifier, "The resource identifier")
1189+
},
1190+
null));
1191+
1192+
// Create a write only resource: no readable scopes, but has writable scopes
1193+
var writeOnlyType = tf.Create(() => new ResourceType(
1194+
"writeOnlyType@v1",
1195+
tf.GetReference(bodyType),
1196+
null,
1197+
writableScopes_in: ScopeType.All,
1198+
readableScopes_in: (ScopeType)0)); // No readable scopes makes it write only
1199+
1200+
return [writeOnlyType];
1201+
}
1202+
});
1203+
11241204
#endregion
11251205

11261206
}

src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,10 @@ public Diagnostic LambdaVariablesInInlineFunctionUnsupported(string functionName
12451245
$"Using lambda variables inside the \"{functionName}\" function is not currently supported."
12461246
+ $" Found the following lambda variable(s) being accessed: {ToQuotedString(variableNames)}.");
12471247

1248+
public Diagnostic CannotUseExistingWithWriteOnlyResource(ResourceTypeReference resourceTypeReference) => CoreError(
1249+
"BCP441",
1250+
$"Resource type \"{resourceTypeReference.FormatName()}\" cannot be used with the 'existing' keyword.");
1251+
12481252
public Diagnostic ExpectedLoopVariableBlockWith2Elements(int actualCount) => CoreError(
12491253
"BCP249",
12501254
$"Expected loop variable block to consist of exactly 2 elements (item variable and index variable), but found {actualCount}.");

src/Bicep.Core/TypeSystem/Providers/Extensibility/ExtensionResourceTypeFactory.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,19 @@ private static ResourceFlags ToResourceFlags(Azure.Bicep.Types.Concrete.Resource
239239
var output = ResourceFlags.None;
240240
var (readableScopes, writableScopes) = GetScopeInfo(input);
241241

242-
// Resource is ReadOnly if there are no writable scopes (matches legacy behavior)
243-
if (writableScopes == ResourceScope.None)
242+
// Resource is ReadOnly if there are no writable scopes but has readable scopes
243+
if (writableScopes == ResourceScope.None && readableScopes != ResourceScope.None)
244244
{
245245
output |= ResourceFlags.ReadOnly;
246246
}
247247

248+
// Resource is WriteOnly if there are no readable scopes but has writable scopes
249+
// This is specifically for extensions which cannot be referenced as existing
250+
if (readableScopes == ResourceScope.None && writableScopes != ResourceScope.None)
251+
{
252+
output |= ResourceFlags.WriteOnly;
253+
}
254+
248255
return output;
249256
}
250257

src/Bicep.Core/TypeSystem/ResourceFlags.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@ public enum ResourceFlags
1414
/// The resource must be used with the 'existing' keyword.
1515
/// </summary>
1616
ReadOnly = 1 << 0,
17+
18+
/// <summary>
19+
/// The resource cannot be used with the 'existing' keyword.
20+
/// </summary>
21+
WriteOnly = 1 << 1,
1722
}
1823
}

src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax sy
326326
{
327327
diagnostics.Write(syntax.Type, x => x.ResourceTypeIsReadonly(resourceType.TypeReference));
328328
}
329+
330+
if (syntax.IsExistingResource() && resourceType.Flags.HasFlag(ResourceFlags.WriteOnly))
331+
{
332+
diagnostics.Write(syntax.Type, x => x.CannotUseExistingWithWriteOnlyResource(resourceType.TypeReference));
333+
}
329334
}
330335

331336
return TypeValidator.NarrowTypeAndCollectDiagnostics(typeManager, binder, this.parsingErrorLookup, diagnostics, syntax.Value, declaredType, true);

0 commit comments

Comments
 (0)