Skip to content

Commit

Permalink
Add resource types in outputs and parameters
Browse files Browse the repository at this point in the history
Fixes: #2246

This change implements functionality for declaring strongly type
parameters and outputs using resource types.

Example:

```
param storage resource 'Microsoft.Storage/storageAccounts@2020-01-01'
```

This declares a parameter that can be interacted with as-if it were an
'existing' resource type declaration of the provided type.

In addition you can do the same with outputs:

```
output out resource 'Microsoft.Storage/storageAccounts@2020-01-01' = foo
```

or using type inference (outputs):

```
output out resource = foo
```

These features together allow you to pass resources across module
boundaries, and as command line parameters (using the resource ID).

---

This PR implements #2246 with a few caveats that I discussed offline
with one of the maintainers.

1. It does not include the 'any resource type' that's outlined in the
   proposal. It's not clear how that part of the proposal will work
   in the future with extensibility.
2. It does not support extensibility resources currently. To do this we
   need to define the semantics of how an extensibility resource can
   be serialized (what's the equivalent of `.id`). This is explicitly
   blocked in the first cut and can be added as extensibility is
   designed.
3. Parameter resources cannot be used with the `.parent` or `.scope`
   properties because it would allow you to bypass scope validation
   easily. The set of cases that we could actually provide validation
   for these use cases are really limited.
  • Loading branch information
rynowak committed Oct 23, 2021
1 parent 5b9c07f commit ad7d66d
Show file tree
Hide file tree
Showing 94 changed files with 2,165 additions and 542 deletions.
9 changes: 6 additions & 3 deletions docs/grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ targetScopeDecl -> "targetScope" "=" expression
importDecl -> decorator* "import" IDENTIFIER(aliasName) "from" IDENTIFIER(providerName) object? NL
parameterDecl -> decorator* "parameter" IDENTIFIER(name) IDENTIFIER(type) parameterDefaultValue? NL
parameterDecl ->
decorator* "parameter" IDENTIFIER(name) IDENTIFIER(type) parameterDefaultValue? NL |
decorator* "parameter" IDENTIFIER(name) "resource" interpString(type) parameterDefaultValue? NL |
parameterDefaultValue -> "=" expression
variableDecl -> decorator* "variable" IDENTIFIER(name) "=" expression NL
Expand All @@ -25,8 +27,9 @@ resourceDecl -> decorator* "resource" IDENTIFIER(name) interpString(type) "exist
moduleDecl -> decorator* "module" IDENTIFIER(name) interpString(type) "=" (ifCondition | object | forExpression) NL
outputDecl -> decorator* "output" IDENTIFIER(name) IDENTIFIER(type) "=" expression NL
outputDecl ->
decorator* "output" IDENTIFIER(name) IDENTIFIER(type) "=" expression NL
decorator* "output" IDENTIFIER(name) "resource" interpString(type) "=" expression NL
NL -> ("\n" | "\r")+
decorator -> "@" decoratorExpression NL
Expand Down
84 changes: 84 additions & 0 deletions src/Bicep.Core.IntegrationTests/ModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,90 @@ public void External_module_reference_with_oci_scheme_should_be_rejected_if_regi
});
}

[TestMethod]
public void Module_can_pass_correct_resource_type_as_parameter()
{
var result = CompilationHelper.Compile(
("main.bicep", @"
resource resource 'Microsoft.Storage/storageAccounts@2019-06-01' existing = {
name: 'test'
}
module mod './module.bicep' = {
name: 'test'
params: {
p: resource
}
}
"),
("module.bicep", @"
param p resource 'Microsoft.Storage/storageAccounts@2019-06-01'
output out string = p.properties.accessTier
"));
result.Should().NotHaveAnyDiagnostics();

var model = result.Compilation.GetEntrypointSemanticModel();
result.Template.Should().HaveValueAtPath("$.resources[0].properties.parameters.p.value", "[resourceId('Microsoft.Storage/storageAccounts', 'test')]");
}

[TestMethod]
public void Module_can_pass_correct_resource_type_with_different_api_version_as_parameter()
{
var result = CompilationHelper.Compile(
("main.bicep", @"
resource resource 'Microsoft.Storage/storageAccounts@2019-06-01' existing = {
name: 'test'
}
module mod './module.bicep' = {
name: 'test'
params: {
p: resource
}
}
"),
("module.bicep", @"
param p resource 'Microsoft.Storage/storageAccounts@2021-04-01'
output out string = p.properties.accessTier
"));
result.Should().NotHaveAnyDiagnostics();

var model = result.Compilation.GetEntrypointSemanticModel();
result.Template.Should().HaveValueAtPath("$.resources[0].properties.parameters.p.value", "[resourceId('Microsoft.Storage/storageAccounts', 'test')]");
}

[TestMethod]
public void Module_cannot_pass_incorrect_resource_type_as_parameter()
{
var result = CompilationHelper.Compile(
("main.bicep", @"
resource resource 'Microsoft.Storage/storageAccounts@2019-06-01' existing = {
name: 'test'
}
module mod './module.bicep' = {
name: 'test'
params: {
p: resource
}
}
"),
("module.bicep", @"
param p resource 'Microsoft.Sql/servers@2021-02-01-preview'
output out string = p.properties.minimalTlsVersion
"));
result.Should().HaveDiagnostics(new []
{
("BCP036", DiagnosticLevel.Error, "The property \"p\" expected a value of type \"Microsoft.Sql/servers\" but the provided value is of type \"Microsoft.Storage/storageAccounts@2019-06-01\"."),
});
}

private static string GetTemplate(Compilation compilation)
{
var stringBuilder = new StringBuilder();
Expand Down
29 changes: 15 additions & 14 deletions src/Bicep.Core.IntegrationTests/NestedResourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Bicep.Core.Emit;
using Bicep.Core.FileSystem;
using Bicep.Core.Semantics;
using Bicep.Core.Semantics.Metadata;
using Bicep.Core.TypeSystem;
using Bicep.Core.UnitTests;
using Bicep.Core.UnitTests.Assertions;
Expand Down Expand Up @@ -65,7 +66,7 @@ public void NestedResources_symbols_are_bound()
new { name = "sibling", type = "My.RP/parentType/childType@2020-01-02", },
};

model.AllResources.Select(x => x.Symbol)
model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol)
.Select(s => new { name = s.Name, type = (s.Type as ResourceType)?.TypeReference.FormatName(), })
.OrderBy(n => n.name)
.Should().BeEquivalentTo(expected);
Expand Down Expand Up @@ -104,7 +105,7 @@ public void NestedResources_resource_can_contain_property_called_resource()
new { name = "parent", type = "My.RP/parentType@2020-01-01", },
};

model.AllResources.Select(x => x.Symbol)
model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol)
.Select(s => new { name = s.Name, type = (s.Type as ResourceType)?.TypeReference.FormatName(), })
.OrderBy(n => n.name)
.Should().BeEquivalentTo(expected);
Expand Down Expand Up @@ -154,19 +155,19 @@ public void NestedResources_valid_resource_references()

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

var parent = model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "parent");
var parent = model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "parent");
var references = model.FindReferences(parent);
references.Should().HaveCount(6);

var child = model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "child");
var child = model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "child");
references = model.FindReferences(child);
references.Should().HaveCount(6);

var grandchild = model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "grandchild");
var grandchild = model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "grandchild");
references = model.FindReferences(grandchild);
references.Should().HaveCount(4);

var sibling = model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "sibling");
var sibling = model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "sibling");
references = model.FindReferences(sibling);
references.Should().HaveCount(1);

Expand Down Expand Up @@ -417,13 +418,13 @@ public void NestedResources_ancestors_are_detected()
var model = compilation.GetEntrypointSemanticModel();
model.GetAllDiagnostics().ExcludingMissingTypes().Should().BeEmpty();

var parent = model.ResourceMetadata.TryLookup(model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "parent").DeclaringSyntax)!;
var parent = (DeclaredResourceMetadata)model.ResourceMetadata.TryLookup(model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "parent").DeclaringSyntax)!;
model.ResourceAncestors.GetAncestors(parent).Should().BeEmpty();

var child = model.ResourceMetadata.TryLookup(model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "child").DeclaringSyntax)!;
var child = (DeclaredResourceMetadata)model.ResourceMetadata.TryLookup(model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "child").DeclaringSyntax)!;
model.ResourceAncestors.GetAncestors(child).Select(x => x.Resource).Should().Equal(new[] { parent, });

var grandchild = model.ResourceMetadata.TryLookup(model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "grandchild").DeclaringSyntax)!;
var grandchild = (DeclaredResourceMetadata)model.ResourceMetadata.TryLookup(model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "grandchild").DeclaringSyntax)!;
model.ResourceAncestors.GetAncestors(grandchild).Select(x => x.Resource).Should().Equal(new[] { parent, child, }); // order matters
}

Expand Down Expand Up @@ -466,19 +467,19 @@ public void NestedResources_scopes_isolate_names()
var model = compilation.GetEntrypointSemanticModel();
model.GetAllDiagnostics().ExcludingMissingTypes().Should().BeEmpty();

var parent = model.ResourceMetadata.TryLookup(model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "parent").DeclaringSyntax)!;
var parent = (DeclaredResourceMetadata)model.ResourceMetadata.TryLookup(model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "parent").DeclaringSyntax)!;
model.ResourceAncestors.GetAncestors(parent).Should().BeEmpty();

var child = model.ResourceMetadata.TryLookup(model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "child").DeclaringSyntax)!;
var child = (DeclaredResourceMetadata)model.ResourceMetadata.TryLookup(model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "child").DeclaringSyntax)!;
model.ResourceAncestors.GetAncestors(child).Select(x => x.Resource).Should().Equal(new[] { parent, });

var childGrandChild = model.ResourceMetadata.TryLookup(child.Symbol.DeclaringResource.GetBody().Resources.Single())!;
var childGrandChild = (DeclaredResourceMetadata)model.ResourceMetadata.TryLookup(child.Symbol.DeclaringResource.GetBody().Resources.Single())!;
model.ResourceAncestors.GetAncestors(childGrandChild).Select(x => x.Resource).Should().Equal(new[] { parent, child, });

var sibling = model.ResourceMetadata.TryLookup(model.AllResources.Select(x => x.Symbol).Single(r => r.Name == "sibling").DeclaringSyntax)!;
var sibling = (DeclaredResourceMetadata)model.ResourceMetadata.TryLookup(model.AllResources.OfType<DeclaredResourceMetadata>().Select(x => x.Symbol).Single(r => r.Name == "sibling").DeclaringSyntax)!;
model.ResourceAncestors.GetAncestors(child).Select(x => x.Resource).Should().Equal(new[] { parent, });

var siblingGrandChild = model.ResourceMetadata.TryLookup(sibling.Symbol.DeclaringResource.GetBody().Resources.Single())!;
var siblingGrandChild = (DeclaredResourceMetadata)model.ResourceMetadata.TryLookup(sibling.Symbol.DeclaringResource.GetBody().Resources.Single())!;
model.ResourceAncestors.GetAncestors(siblingGrandChild).Select(x => x.Resource).Should().Equal(new[] { parent, sibling, });
}

Expand Down
129 changes: 129 additions & 0 deletions src/Bicep.Core.IntegrationTests/OutputsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Diagnostics.CodeAnalysis;
using Bicep.Core.Diagnostics;
using Bicep.Core.IntegrationTests.Extensibility;
using Bicep.Core.TypeSystem;
using Bicep.Core.UnitTests;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;

namespace Bicep.Core.IntegrationTests
{
[TestClass]
public class OutputsTests
{
[NotNull]
public TestContext? TestContext { get; set; }

private CompilationHelper.CompilationHelperContext GetExtensibilityCompilationContext()
{
var features = BicepTestConstants.CreateFeaturesProvider(TestContext, importsEnabled: true);
var resourceTypeLoader = BicepTestConstants.AzResourceTypeLoader;
var namespaceProvider = new ExtensibilityNamespaceProvider(resourceTypeLoader, features);

return new(
AzResourceTypeLoader: resourceTypeLoader,
Features: features,
NamespaceProvider: namespaceProvider);
}

[TestMethod]
public void Output_can_have_inferred_resource_type()
{
var result = CompilationHelper.Compile(@"
resource resource 'Microsoft.Storage/storageAccounts@2019-06-01' = {
name: 'test'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
identity: {
type: 'SystemAssigned'
}
properties:{
accessTier: 'Cool'
}
}
output out resource = resource
");
result.Should().NotHaveAnyDiagnostics();

var model = result.Compilation.GetEntrypointSemanticModel();
var @out = model.Root.OutputDeclarations.Should().ContainSingle().Subject;
var typeInfo = model.GetTypeInfo(@out.DeclaringSyntax);
typeInfo.Should().BeOfType<ResourceType>().Which.TypeReference.FormatName().Should().BeEquivalentTo("Microsoft.Storage/storageAccounts@2019-06-01");

result.Template.Should().HaveValueAtPath("$.outputs.out", new JObject()
{
["type"] = "string",
["value"] = "[resourceId('Microsoft.Storage/storageAccounts', 'test')]",
["metadata"] = new JObject()
{
["resourceType"] = new JValue("Microsoft.Storage/storageAccounts@2019-06-01"),
},
});
}

[TestMethod]
public void Output_can_have_specified_resource_type()
{
var result = CompilationHelper.Compile(@"
resource resource 'Microsoft.Storage/storageAccounts@2019-06-01' = {
name: 'test'
location: 'eastus'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
identity: {
type: 'SystemAssigned'
}
properties:{
accessTier: 'Cool'
}
}
output out resource 'Microsoft.Storage/storageAccounts@2019-06-01' = resource
");
result.Should().NotHaveAnyDiagnostics();

var model = result.Compilation.GetEntrypointSemanticModel();
var @out = model.Root.OutputDeclarations.Should().ContainSingle().Subject;
var typeInfo = model.GetTypeInfo(@out.DeclaringSyntax);
typeInfo.Should().BeOfType<ResourceType>().Which.TypeReference.FormatName().Should().BeEquivalentTo("Microsoft.Storage/storageAccounts@2019-06-01");

result.Template.Should().HaveValueAtPath("$.outputs.out", new JObject()
{
["type"] = "string",
["value"] = "[resourceId('Microsoft.Storage/storageAccounts', 'test')]",
["metadata"] = new JObject()
{
["resourceType"] = new JValue("Microsoft.Storage/storageAccounts@2019-06-01"),
},
});
}

[TestMethod]
public void Output_cannot_use_extensibility_resource_type()
{
var result = CompilationHelper.Compile(GetExtensibilityCompilationContext(), @"
import stg from storage {
connectionString: 'asdf'
}
resource container 'AzureStorage/containers@2020-01-01' = {
name: 'myblob'
}
output out resource = container
");
result.Should().HaveDiagnostics(new []
{
("BCP225", DiagnosticLevel.Error, "The type \"AzureStorage/containers@2020-01-01\" cannot be used as an output type."),
});
}
}
}
Loading

0 comments on commit ad7d66d

Please sign in to comment.