Skip to content

Commit

Permalink
Lexically scoped child resources
Browse files Browse the repository at this point in the history
In progress for Azure#1363

This change add support for lexical scoping/nesting of child resources.
That it, for a resource type: `My.RP/someType@2020-01-01` there's now a
simplified syntax for including a resource:
`My.RP/someType/childType@2020-01-01` inside the body of the parent.

This syntax also allows simplication of the type name of the child,
limits its scope/visibility and implies an automatic `dependsOn` to the
parent resource. The goal is to be the most idiomatic way to declare
multiple resources to be deployed with a parent/child/grandchild/etc
relationship.

This is a mostly feature complete implementation of the proposal at
issue Azure#1363.
  • Loading branch information
rynowak committed Feb 16, 2021
1 parent 3350d6b commit 2188cce
Show file tree
Hide file tree
Showing 41 changed files with 1,337 additions and 179 deletions.
270 changes: 270 additions & 0 deletions src/Bicep.Core.IntegrationTests/NestedResourceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Linq;
using Bicep.Core.Diagnostics;
using Bicep.Core.Semantics;
using Bicep.Core.TypeSystem;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Bicep.Core.IntegrationTests
{
[TestClass]
public class NestedResourceTests
{
[TestMethod]
public void NestedResources_symbols_are_bound()
{
var program = @"
resource parent 'My.RP/parentType@2020-01-01' = {
name: 'parent'
properties: {
size: 'large'
}
resource child 'childType' = {
name: 'child'
properties: {
style: 'very cool'
}
}
resource sibling 'childType@2020-01-02' = {
name: 'sibling'
properties: {
style: child.properties.style
size: parent.properties.size
}
}
}
";

var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program));
var model = compilation.GetEntrypointSemanticModel();

model.GetAllDiagnostics().Should().BeEmpty();

var expected = new []
{
new { name = "child", type = "My.RP/parentType/childType@2020-01-01", },
new { name = "parent", type = "My.RP/parentType@2020-01-01", },
new { name = "sibling", type = "My.RP/parentType/childType@2020-01-02", },
};

model.Root.GetAllResourceDeclarations()
.Select(s => new { name = s.Name, type = (s.Type as ResourceType)?.TypeReference.FormatName(), })
.OrderBy(n => n.name)
.Should().BeEquivalentTo(expected);
}

[TestMethod]
public void NestedResources_child_cannot_be_referenced_outside_of_scope()
{
var program = @"
resource parent 'My.RP/parentType@2020-01-01' = {
name: 'parent'
properties: {
}
resource child 'childType' = {
name: 'child'
properties: {
style: 'very cool'
}
}
}
resource other 'My.RP/parentType@2020-01-01' = {
name: 'other'
properties: {
style: child.properties.style
}
}
";

var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program));
var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics();
diagnostics.Should().HaveDiagnostics(new[] {
("BCP057", DiagnosticLevel.Error, "The name \"child\" does not exist in the current context."),
});
}

[TestMethod]
public void NestedResources_child_cannot_specify_qualified_type()
{
var program = @"
resource parent 'My.RP/parentType@2020-01-01' = {
name: 'parent'
properties: {
}
resource child 'My.RP/parentType/childType@2020-01-01' = {
name: 'child'
properties: {
style: 'very cool'
}
}
}
";

var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program));
var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics();
diagnostics.Should().HaveDiagnostics(new[] {
("BCP139", DiagnosticLevel.Error, "The resource type part \"My.RP/parentType/childType@2020-01-01\" is invalid. Nested resources must specify a single type, and optionally can specify a version using the format \"<type>@<apiVersion>\"."),
});
}

[TestMethod]
public void NestedResources_error_in_base_type()
{
var program = @"
resource parent 'My.RP/parentType@invalid-version' = {
name: 'parent'
properties: {
}
resource child 'My.RP/parentType/childType@2020-01-01' = {
name: 'child'
properties: {
style: 'very cool'
}
}
}
";

var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program));
var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics();
diagnostics.Should().HaveDiagnostics(new[] {
("BCP029", DiagnosticLevel.Error, "The resource type is not valid. Specify a valid resource type of format \"<provider>/<types>@<apiVersion>\"."),
("BCP140", DiagnosticLevel.Error, "The resource type cannot be determined due an error in containing resource \"parent\"."),
});
}

[TestMethod]
public void NestedResources_error_in_parent_type()
{
var program = @"
resource parent 'My.RP/parentType@2020-01-01' = {
name: 'parent'
properties: {
}
// Error here
resource child 'My.RP/parentType/childType@2020-01-01' = {
name: 'child'
properties: {
}
resource grandchild 'granchildType' = {
name: 'grandchild'
properties: {
}
}
}
}
";

var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program));
var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics();
diagnostics.Should().HaveDiagnostics(new[] {
("BCP139", DiagnosticLevel.Error, "The resource type part \"My.RP/parentType/childType@2020-01-01\" is invalid. Nested resources must specify a single type, and optionally can specify a version using the format \"<type>@<apiVersion>\"."),
("BCP140", DiagnosticLevel.Error, "The resource type cannot be determined due an error in containing resource \"child\"."),
});
}

[TestMethod]
public void NestedResources_child_cycle_is_detected_correctly()
{
var program = @"
resource parent 'My.RP/parentType@2020-01-01' = {
name: 'parent'
properties: {
style: child.properties.style
}
resource child 'childType' = {
name: 'child'
properties: {
style: 'very cool'
}
}
}
";

var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program));
compilation.GetEntrypointSemanticModel().GetAllDiagnostics().Should().HaveDiagnostics(new[] {
("BCP080", DiagnosticLevel.Error, "The expression is involved in a cycle (\"child\" -> \"parent\")."),
});
}

[TestMethod] // With more than one level of nesting the name just isn't visible.
public void NestedResources_grandchild_cycle_results_in_binding_failure()
{
var program = @"
resource parent 'My.RP/parentType@2020-01-01' = {
name: 'parent'
properties: {
style: grandchild.properties.style
}
resource child 'childType' = {
name: 'child'
properties: {
}
resource grandchild 'grandchildType' = {
name: 'grandchild'
properties: {
style: 'very cool'
}
}
}
}
";

var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program));
compilation.GetEntrypointSemanticModel().GetAllDiagnostics().Should().HaveDiagnostics(new[] {
("BCP057", DiagnosticLevel.Error, "The name \"grandchild\" does not exist in the current context."),
});
}

[TestMethod]
public void NestedResources_ancestors_are_detected()
{
var program = @"
resource parent 'My.RP/parentType@2020-01-01' = {
name: 'parent'
properties: {
}
resource child 'childType' = {
name: 'child'
properties: {
}
resource grandchild 'grandchildType' = {
name: 'grandchild'
properties: {
}
}
}
}
";

var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program));
var model = compilation.GetEntrypointSemanticModel();
model.GetAllDiagnostics().Should().BeEmpty();

var parent = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "parent");
model.ResourceAncestors.GetAncestors(parent).Should().BeEmpty();

var child = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "child");
model.ResourceAncestors.GetAncestors(child).Should().Equal(new []{ parent, });

var grandchild = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "grandchild");
model.ResourceAncestors.GetAncestors(grandchild).Should().Equal(new []{ parent, child, }); // order matters
}
}
}
19 changes: 19 additions & 0 deletions src/Bicep.Core.Samples/Files/Resources_CRLF/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,23 @@ resource existing2 'Mock.Rp/existingExtensionResource@2020-01-01' existing = {
resource extension3 'My.Rp/extensionResource@2020-12-01' = {
name: 'extension3'
scope: existing1
}

param shouldDeployChildAndGrandChild bool = true
resource nestedA 'My.Rp/nestedResource@2020-01-01' existing = {
name: 'nestedA'

resource nestedB 'childType' = if (shouldDeployChildAndGrandChild) {
name: 'nestedB'

resource nestedC 'grandchildType' = {
name: 'nestedC'
properties: {
style: nestedA.properties.style
}
}

properties: {
}
}
}
22 changes: 22 additions & 0 deletions src/Bicep.Core.Samples/Files/Resources_CRLF/main.diagnostics.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,25 @@ resource extension3 'My.Rp/extensionResource@2020-12-01' = {
name: 'extension3'
scope: existing1
}

param shouldDeployChildAndGrandChild bool = true
resource nestedA 'My.Rp/nestedResource@2020-01-01' existing = {
//@[17:50) [BCP081 (Warning)] Resource type "My.Rp/nestedResource@2020-01-01" does not have types available. |'My.Rp/nestedResource@2020-01-01'|
name: 'nestedA'

resource nestedB 'childType' = if (shouldDeployChildAndGrandChild) {
//@[19:30) [BCP081 (Warning)] Resource type "My.Rp/nestedResource/childType@2020-01-01" does not have types available. |'childType'|
name: 'nestedB'

resource nestedC 'grandchildType' = {
//@[21:37) [BCP081 (Warning)] Resource type "My.Rp/nestedResource/childType/grandchildType@2020-01-01" does not have types available. |'grandchildType'|
name: 'nestedC'
properties: {
style: nestedA.properties.style
}
}

properties: {
}
}
}
18 changes: 18 additions & 0 deletions src/Bicep.Core.Samples/Files/Resources_CRLF/main.formatted.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,21 @@ resource extension3 'My.Rp/extensionResource@2020-12-01' = {
name: 'extension3'
scope: existing1
}

param shouldDeployChildAndGrandChild bool = true
resource nestedA 'My.Rp/nestedResource@2020-01-01' existing = {
name: 'nestedA'

resource nestedB 'childType' = if (shouldDeployChildAndGrandChild) {
name: 'nestedB'

resource nestedC 'grandchildType' = {
name: 'nestedC'
properties: {
style: nestedA.properties.style
}
}

properties: {}
}
}
24 changes: 24 additions & 0 deletions src/Bicep.Core.Samples/Files/Resources_CRLF/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"shouldDeployVm": {
"type": "bool",
"defaultValue": true
},
"shouldDeployChildAndGrandChild": {
"type": "bool",
"defaultValue": true
}
},
"functions": [],
Expand All @@ -37,6 +41,26 @@
"myInterpKey": "abc"
},
"resources": [
{
"condition": "[parameters('shouldDeployChildAndGrandChild')]",
"type": "My.Rp/nestedResource/childType/grandchildType",
"apiVersion": "2020-01-01",
"name": "[format('{0}/{1}/{2}', 'nestedA', 'nestedB', 'nestedC')]",
"properties": {
"style": "[reference(resourceId('My.Rp/nestedResource', 'nestedA'), '2020-01-01').style]"
},
"dependsOn": [
"[resourceId('My.Rp/nestedResource/childType', split(format('{0}/{1}', 'nestedA', 'nestedB'), '/')[0], split(format('{0}/{1}', 'nestedA', 'nestedB'), '/')[1])]"
]
},
{
"condition": "[parameters('shouldDeployChildAndGrandChild')]",
"type": "My.Rp/nestedResource/childType",
"apiVersion": "2020-01-01",
"name": "[format('{0}/{1}', 'nestedA', 'nestedB')]",
"properties": {},
"dependsOn": []
},
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-06-01",
Expand Down
Loading

0 comments on commit 2188cce

Please sign in to comment.