Skip to content

Commit 3f2dad6

Browse files
authored
Add & handle readableScopes + writableScopes properties (#17849)
## Description The current _bicep-types_ ResourceType scope **legacy properties** are prone to contradiction. In _bicep-types_ they have now been replaced with **modern properties** which erases any confusion. This PR has changes to handle the newest _bicep.types_ `0.6.12` which contains these new modern properties and a few other changes: - Added new modern properties to ResourceType called `readableScopes` and `writableScopes` - Deserialization will normalize legacy properties: `scopeType`, `flags` & `readOnly` to the modern properties - Introduced a constant called `All` which has all bits set to 1 - Renamed `ScopeType.Unknown` to `ScopeType.None` - Behaviorally deprecated `ScopeType.Unknown` is equivalent to the new `ScopeType.All` ## Example Usage Let’s say a resource type has the following: ``` { "scopes": ["resourceGroup", "subscription"], "readOnlyScopes": ["tenant"], "flags": "ReadOnly" } ``` This is translated to: - scopes: Says the resource can be read from and written to resourceGroup and subscription levels - readOnlyScopes: Says the resource can only be read at tenant level - flags: ReadOnly: Says the resource type is read only The contradiction: - resourceGroup: can read (from scopes), can write (from scopes) - subscription: can read (from scopes), can write (from scopes) - tenant: can read (from readOnlyScopes) But the flag says `ReadOnly`, which means the resource type can only be referenced not written to. But scopes says otherwise. With the changes in this PR we will go from this:  ``` {   "scopes": ["subscription", "resourceGroup"],   "readOnlyScopes": ["tenant", "managementGroup"] } ``` To this:  ``` {   "writableScopes": ["subscription", "resourceGroup"],   "readableScopes": ["subscription", "resourceGroup", "tenant", "managementGroup"] } ``` With this change, we can also support any resource types that may be write only. ## 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/17849)
1 parent 3b0de47 commit 3f2dad6

File tree

14 files changed

+213
-68
lines changed

14 files changed

+213
-68
lines changed

src/Bicep.Core.IntegrationTests/Files/ExtensionRegistryTests/http/types/index.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"name": "http",
1010
"version": "1.2.3",
1111
"isSingleton": false,
12+
"isPreview": false,
13+
"isDeprecated": false,
1214
"configurationType": null
1315
},
1416
"fallbackResourceType": null

src/Bicep.Core.IntegrationTests/Files/ExtensionRegistryTests/http/types/v1/types.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,14 @@
7979
{
8080
"$type": "ResourceType",
8181
"name": "request@v1",
82-
"scopeType": 0,
83-
"readOnlyScopes": null,
8482
"body": {
8583
"$ref": "#/6"
8684
},
87-
"flags": 0,
88-
"functions": null
85+
"functions": null,
86+
"readableScopes": 31,
87+
"writableScopes": 31,
88+
"scopeType": null,
89+
"readOnlyScopes": null,
90+
"flags": null
8991
}
90-
]
92+
]

src/Bicep.Core.Samples/Files/baselines/Extensions_CRLF/main.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"_generator": {
1111
"name": "bicep",
1212
"version": "dev",
13-
"templateHash": "14300936452791949961"
13+
"templateHash": "12920343705566664077"
1414
}
1515
},
1616
"parameters": {
@@ -30,7 +30,7 @@
3030
"extensions": {
3131
"az": {
3232
"name": "AzureResourceManager",
33-
"version": "0.2.789"
33+
"version": "0.2.802"
3434
},
3535
"k8s": {
3636
"name": "Kubernetes",

src/Bicep.Core.Samples/Files/baselines/Extensions_CRLF/main.sourcemap.bicep

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ var strVar1 = 'strVar1Value'
3030
extension az
3131
//@ "az": {
3232
//@ "name": "AzureResourceManager",
33-
//@ "version": "0.2.789"
33+
//@ "version": "0.2.802"
3434
//@ },
3535
extension kubernetes as k8s
3636
//@ "k8s": {

src/Bicep.Core.Samples/Files/baselines/Extensions_CRLF/main.symbolicnames.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"_generator": {
1111
"name": "bicep",
1212
"version": "dev",
13-
"templateHash": "14300936452791949961"
13+
"templateHash": "12920343705566664077"
1414
}
1515
},
1616
"parameters": {
@@ -30,7 +30,7 @@
3030
"extensions": {
3131
"az": {
3232
"name": "AzureResourceManager",
33-
"version": "0.2.789"
33+
"version": "0.2.802"
3434
},
3535
"k8s": {
3636
"name": "Kubernetes",

src/Bicep.Core.UnitTests/TypeSystem/Az/AzResourceTypeProviderTests.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
using Bicep.Core.Semantics;
99
using Bicep.Core.TypeSystem;
1010
using Bicep.Core.TypeSystem.Providers.Az;
11+
using Bicep.Core.TypeSystem.Providers.Extensibility;
1112
using Bicep.Core.TypeSystem.Types;
1213
using Bicep.Core.UnitTests.Assertions;
1314
using Bicep.Core.UnitTests.Utils;
1415
using FluentAssertions;
1516
using Microsoft.VisualStudio.TestTools.UnitTesting;
17+
using AzTypes = Azure.Bicep.Types;
18+
using AzConcreteTypes = Azure.Bicep.Types.Concrete;
1619

1720
namespace Bicep.Core.UnitTests.TypeSystem.Az
1821
{
@@ -258,6 +261,111 @@ Compilation createCompilation(string program) => Services
258261
_ => []
259262
};
260263

264+
265+
[TestMethod]
266+
public void AzResourceTypeFactory_ScopeTypeAll_ShouldReturnAllScopes()
267+
{
268+
// Test the ScopeType.All handling
269+
var factory = new AzResourceTypeFactory();
270+
var resourceType = CreateMockResourceType(
271+
readableScopes: AzConcreteTypes.ScopeType.All,
272+
writableScopes: AzConcreteTypes.ScopeType.All);
273+
274+
var result = factory.GetResourceType(resourceType, []);
275+
276+
// ScopeType.All should map to all deployment scopes including Resource/Extension
277+
result.ValidParentScopes.Should().Be(
278+
ResourceScope.Tenant | ResourceScope.ManagementGroup |
279+
ResourceScope.Subscription | ResourceScope.ResourceGroup | ResourceScope.Resource);
280+
}
281+
282+
[TestMethod]
283+
public void AzResourceTypeFactory_DifferentReadableAndwritableScopes_ShouldNotSetReadOnlyFlag()
284+
{
285+
// Test that ReadOnly flag is NOT set when there are some writable scopes
286+
var factory = new AzResourceTypeFactory();
287+
var resourceType = CreateMockResourceType(
288+
readableScopes: AzConcreteTypes.ScopeType.Tenant | AzConcreteTypes.ScopeType.Subscription | AzConcreteTypes.ScopeType.ResourceGroup,
289+
writableScopes: AzConcreteTypes.ScopeType.Subscription | AzConcreteTypes.ScopeType.ResourceGroup);
290+
291+
var result = factory.GetResourceType(resourceType, []);
292+
293+
// Should NOT be marked as ReadOnly because there are writable scopes available
294+
result.Flags.Should().NotHaveFlag(ResourceFlags.ReadOnly);
295+
// ReadOnlyScopes should be readable scopes minus writable scopes (tenant only)
296+
result.ReadOnlyScopes.Should().Be(ResourceScope.Tenant);
297+
// ValidParentScopes should be the writable scopes
298+
result.ValidParentScopes.Should().Be(ResourceScope.Subscription | ResourceScope.ResourceGroup);
299+
}
300+
301+
[TestMethod]
302+
public void AzResourceTypeFactory_SameReadableAndwritableScopes_ShouldNotSetReadOnlyFlag()
303+
{
304+
// Test when readable and writable scopes are identical
305+
var factory = new AzResourceTypeFactory();
306+
var resourceType = CreateMockResourceType(
307+
readableScopes: AzConcreteTypes.ScopeType.Subscription | AzConcreteTypes.ScopeType.ResourceGroup,
308+
writableScopes: AzConcreteTypes.ScopeType.Subscription | AzConcreteTypes.ScopeType.ResourceGroup);
309+
310+
var result = factory.GetResourceType(resourceType, []);
311+
312+
// Should NOT be marked as ReadOnly when readable and writable scopes are the same
313+
result.Flags.Should().NotHaveFlag(ResourceFlags.ReadOnly);
314+
result.ReadOnlyScopes.Should().Be(ResourceScope.None);
315+
}
316+
317+
[TestMethod]
318+
public void AzResourceTypeFactory_NowritableScopes_WithReadableScopes_ShouldSetReadOnlyFlag()
319+
{
320+
// Test resources that have readable scopes but no writable scopes (fully read-only)
321+
var factory = new AzResourceTypeFactory();
322+
var resourceType = CreateMockResourceType(
323+
readableScopes: AzConcreteTypes.ScopeType.Tenant | AzConcreteTypes.ScopeType.Subscription,
324+
writableScopes: (AzConcreteTypes.ScopeType)0);
325+
326+
var result = factory.GetResourceType(resourceType, []);
327+
328+
// Should be marked as ReadOnly because there are no writable scopes
329+
result.Flags.Should().HaveFlag(ResourceFlags.ReadOnly);
330+
result.ReadOnlyScopes.Should().Be(ResourceScope.Tenant | ResourceScope.Subscription);
331+
result.ValidParentScopes.Should().Be(ResourceScope.None); // No writable scopes
332+
}
333+
334+
[TestMethod]
335+
public void AzResourceTypeFactory_ReadableNone_WithSpecificWritableScopes_ShouldUseWritableScopes()
336+
{
337+
var factory = new AzResourceTypeFactory();
338+
var resourceType = CreateMockResourceType(
339+
readableScopes: AzConcreteTypes.ScopeType.None,
340+
writableScopes: AzConcreteTypes.ScopeType.Tenant | AzConcreteTypes.ScopeType.ManagementGroup |
341+
AzConcreteTypes.ScopeType.Subscription | AzConcreteTypes.ScopeType.ResourceGroup);
342+
343+
var result = factory.GetResourceType(resourceType, []);
344+
345+
// Should NOT be marked as ReadOnly because there are writable scopes
346+
result.Flags.Should().NotHaveFlag(ResourceFlags.ReadOnly);
347+
result.ValidParentScopes.Should().Be(
348+
ResourceScope.Tenant | ResourceScope.ManagementGroup |
349+
ResourceScope.Subscription | ResourceScope.ResourceGroup);
350+
result.ReadOnlyScopes.Should().Be(ResourceScope.None);
351+
}
352+
353+
private static AzConcreteTypes.ResourceType CreateMockResourceType(
354+
AzConcreteTypes.ScopeType readableScopes,
355+
AzConcreteTypes.ScopeType writableScopes,
356+
string name = "Test.Provider/testResource@2021-01-01")
357+
{
358+
var factory = new AzConcreteTypes.TypeFactory([]);
359+
var bodyType = factory.Create(() => new AzConcreteTypes.ObjectType("body", new Dictionary<string, AzConcreteTypes.ObjectTypeProperty>(), null));
360+
361+
return factory.Create(() => new AzConcreteTypes.ResourceType(
362+
name,
363+
factory.GetReference(bodyType),
364+
null,
365+
writableScopes_in: writableScopes,
366+
readableScopes_in: readableScopes));
367+
}
368+
261369
private static void VisitAllReachableTypes(TypeSymbol typeSymbol, HashSet<TypeSymbol> visited)
262370
{
263371
if (visited.Contains(typeSymbol))

src/Bicep.Core.UnitTests/Utils/ExtensionResourceTypeHelper.cs

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,10 @@ public static IReadOnlyDictionary<string, string> GetHttpExtensionTypes()
3838

3939
var requestType = factory.Create(() => new ResourceType(
4040
"request@v1",
41-
ScopeType.Unknown,
42-
null,
4341
factory.GetReference(requestBodyType),
44-
ResourceFlags.None,
45-
null));
42+
null,
43+
writableScopes_in: ScopeType.All,
44+
readableScopes_in: ScopeType.All));
4645

4746
var settings = new TypeSettings(
4847
name: "http",
@@ -99,14 +98,13 @@ public static BinaryData GetTestTypesTgz()
9998

10099
var fooType = factory.Create(() => new ResourceType(
101100
"fooType@v1",
102-
ScopeType.Unknown,
103-
null,
104101
factory.GetReference(fooBodyType),
105-
ResourceFlags.None,
106102
new Dictionary<string, ResourceTypeFunction>
107103
{
108104
["convertBarToBaz"] = new(factory.GetReference(barFunctionType), "Converts a bar into a baz!")
109-
}));
105+
},
106+
writableScopes_in: ScopeType.All,
107+
readableScopes_in: ScopeType.All));
110108

111109
var settings = new TypeSettings(name: "ThirdPartyExtension", version: "1.0.0", isSingleton: false, configurationType: null!);
112110

@@ -142,14 +140,13 @@ public static BinaryData GetTestTypesTgzWithFallbackAndConfiguration(bool allCon
142140

143141
var fooType = factory.Create(() => new ResourceType(
144142
"fooType@v1",
145-
ScopeType.Unknown,
146-
null,
147143
factory.GetReference(fooBodyType),
148-
ResourceFlags.None,
149144
new Dictionary<string, ResourceTypeFunction>
150145
{
151146
["convertBarToBaz"] = new(factory.GetReference(barFunctionType), "Converts a bar into a baz!")
152-
}));
147+
},
148+
writableScopes_in: ScopeType.All,
149+
readableScopes_in: ScopeType.All));
153150

154151
//setup fallback resource
155152
var fallbackBodyType = rootFactory.Create(() => new ObjectType("fallback body", new Dictionary<string, ObjectTypeProperty>
@@ -159,11 +156,10 @@ public static BinaryData GetTestTypesTgzWithFallbackAndConfiguration(bool allCon
159156

160157
var fallbackType = rootFactory.Create(() => new ResourceType(
161158
"fallback",
162-
ScopeType.Unknown,
163-
null,
164159
rootFactory.GetReference(fallbackBodyType),
165-
ResourceFlags.None,
166-
null));
160+
null,
161+
writableScopes_in: ScopeType.All,
162+
readableScopes_in: ScopeType.All));
167163

168164
var fallbackResource = new CrossFileTypeReference("types.json", rootFactory.GetIndex(fallbackType));
169165

@@ -210,11 +206,10 @@ public static BinaryData GetMockRadiusTypesTgz()
210206

211207
var awsBucketsType = factory.Create(() => new ResourceType(
212208
"AWS.S3/Bucket@default",
213-
ScopeType.Unknown,
214-
null,
215209
factory.GetReference(awsBucketsBodyType),
216-
ResourceFlags.None,
217-
null));
210+
null,
211+
writableScopes_in: ScopeType.All,
212+
readableScopes_in: ScopeType.All));
218213

219214
var environmentsBodyType = factory.Create(() => new ObjectType("body", new Dictionary<string, ObjectTypeProperty>
220215
{
@@ -224,11 +219,10 @@ public static BinaryData GetMockRadiusTypesTgz()
224219

225220
var environmentsType = factory.Create(() => new ResourceType(
226221
"Applications.Core/environments@2023-10-01-preview",
227-
ScopeType.Unknown,
228-
null,
229222
factory.GetReference(environmentsBodyType),
230-
ResourceFlags.None,
231-
null));
223+
null,
224+
writableScopes_in: ScopeType.All,
225+
readableScopes_in: ScopeType.All));
232226

233227
var applicationsBodyType = factory.Create(() => new ObjectType("body", new Dictionary<string, ObjectTypeProperty>
234228
{
@@ -238,11 +232,10 @@ public static BinaryData GetMockRadiusTypesTgz()
238232

239233
var applicationsType = factory.Create(() => new ResourceType(
240234
"Applications.Core/applications@2023-10-01-preview",
241-
ScopeType.Unknown,
242-
null,
243235
factory.GetReference(applicationsBodyType),
244-
ResourceFlags.None,
245-
null));
236+
null,
237+
writableScopes_in: ScopeType.All,
238+
readableScopes_in: ScopeType.All));
246239

247240
var recipeType = factory.Create(() => new ObjectType("recipe", new Dictionary<string, ObjectTypeProperty>
248241
{
@@ -264,11 +257,10 @@ public static BinaryData GetMockRadiusTypesTgz()
264257

265258
var extendersType = factory.Create(() => new ResourceType(
266259
"Applications.Core/extenders@2023-10-01-preview",
267-
ScopeType.Unknown,
268-
null,
269260
factory.GetReference(extendersBodyType),
270-
ResourceFlags.None,
271-
null));
261+
null,
262+
writableScopes_in: ScopeType.All,
263+
readableScopes_in: ScopeType.All));
272264

273265
var settings = new TypeSettings(name: "Radius", version: "1.0.0", isSingleton: false, configurationType: null!);
274266

src/Bicep.Core/TypeSystem/Providers/Az/AzResourceTypeFactory.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ public ResourceTypeComponents GetResourceType(Azure.Bicep.Types.Concrete.Resourc
3535
}
3636
}
3737

38-
return new ResourceTypeComponents(resourceTypeReference, ToResourceScope(resourceType.ScopeType), ToResourceScope(resourceType.ReadOnlyScopes), ToResourceFlags(resourceType.Flags), bodyType);
38+
var (readableScopes, writableScopes) = GetScopeInfo(resourceType);
39+
var readOnlyScopes = readableScopes & ~writableScopes;
40+
41+
return new ResourceTypeComponents(resourceTypeReference, writableScopes, readOnlyScopes, ToResourceFlags(resourceType), bodyType);
3942
}
4043

4144
public IEnumerable<FunctionOverload> GetResourceFunctionOverloads(Azure.Bicep.Types.Concrete.ResourceFunctionType resourceFunctionType)
@@ -234,20 +237,35 @@ private static TypeSymbolValidationFlags GetValidationFlags(bool isResourceBodyT
234237
return flags;
235238
}
236239

237-
private static ResourceFlags ToResourceFlags(Azure.Bicep.Types.Concrete.ResourceFlags input)
240+
private static ResourceFlags ToResourceFlags(Azure.Bicep.Types.Concrete.ResourceType input)
238241
{
239242
var output = ResourceFlags.None;
240-
if (input.HasFlag(Azure.Bicep.Types.Concrete.ResourceFlags.ReadOnly))
243+
244+
// Resource is ReadOnly if there are no writable scopes
245+
if (input.WritableScopes == Azure.Bicep.Types.Concrete.ScopeType.None)
241246
{
242247
output |= ResourceFlags.ReadOnly;
243248
}
244249

245250
return output;
246251
}
247252

253+
private static (ResourceScope readableScopes, ResourceScope writableScopes) GetScopeInfo(Azure.Bicep.Types.Concrete.ResourceType resourceType)
254+
{
255+
var readableScopes = ToResourceScope(resourceType.ReadableScopes);
256+
var writableScopes = ToResourceScope(resourceType.WritableScopes);
257+
return (readableScopes, writableScopes);
258+
}
259+
248260
private static ResourceScope ToResourceScope(Azure.Bicep.Types.Concrete.ScopeType input)
249261
{
250-
if (input == Azure.Bicep.Types.Concrete.ScopeType.Unknown)
262+
if (input == Azure.Bicep.Types.Concrete.ScopeType.None)
263+
{
264+
// ScopeType.None means no valid scopes
265+
return ResourceScope.None;
266+
}
267+
268+
if (input == Azure.Bicep.Types.Concrete.ScopeType.All)
251269
{
252270
return ResourceScope.Tenant | ResourceScope.ManagementGroup | ResourceScope.Subscription | ResourceScope.ResourceGroup | ResourceScope.Resource;
253271
}
@@ -260,8 +278,5 @@ private static ResourceScope ToResourceScope(Azure.Bicep.Types.Concrete.ScopeTyp
260278
output |= input.HasFlag(Azure.Bicep.Types.Concrete.ScopeType.ResourceGroup) ? ResourceScope.ResourceGroup : ResourceScope.None;
261279
return output;
262280
}
263-
264-
private static ResourceScope ToResourceScope(Azure.Bicep.Types.Concrete.ScopeType? input)
265-
=> input.HasValue ? ToResourceScope(input.Value) : ResourceScope.None;
266281
}
267282
}

0 commit comments

Comments
 (0)