From 9cd1514d87b85d306f428f31ad7d0fd0f365a0dd Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Thu, 9 Jan 2025 14:53:49 -0800 Subject: [PATCH] bicep: Support `@sealed()` decorator on user defined types When the `@sealed()` decorator is applied on user defined types, it causes an `additionalProperties: false` property to be added to the type defintition in the generated arm deployment. Our unmarshalling logic did not understand this pattern and so it would cause an error when trying to unmarshall this, since it expected if `additionalProperties` was set, it would be to set to an object that described information about the additional properties. I've updated the marshalling code to support `false` as a valid value for the additional properties node when parsing an ARM template. Since Go doesn't have great support for modeling descrimited unions, the new `ArmTemplateParameterAdditionalPropertiesValue` type is used to provide the logcal descrimited union of `false` or a object like value. Fixes #4659 --- cli/azd/pkg/azure/arm_template.go | 58 +++++++++++++++---- .../provisioning/bicep/bicep_provider_test.go | 35 ++++++++++- 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/cli/azd/pkg/azure/arm_template.go b/cli/azd/pkg/azure/arm_template.go index 006fcde4848..8890fe73b0f 100644 --- a/cli/azd/pkg/azure/arm_template.go +++ b/cli/azd/pkg/azure/arm_template.go @@ -57,7 +57,7 @@ type ArmTemplateParameterDefinitions map[string]ArmTemplateParameterDefinition type ArmTemplateOutputs map[string]ArmTemplateOutput -type ArmTemplateParameterAdditionalProperties struct { +type ArmTemplateParameterAdditionalPropertiesProperties struct { Type string `json:"type"` MinValue *int `json:"minValue,omitempty"` MaxValue *int `json:"maxValue,omitempty"` @@ -66,18 +66,52 @@ type ArmTemplateParameterAdditionalProperties struct { Metadata map[string]json.RawMessage `json:"metadata"` } +type ArmTemplateParameterAdditionalPropertiesValue struct { + props *ArmTemplateParameterAdditionalPropertiesProperties +} + +func (v ArmTemplateParameterAdditionalPropertiesValue) HasAdditionalProperties() bool { + return v.props != nil +} + +func (v ArmTemplateParameterAdditionalPropertiesValue) Properties() ArmTemplateParameterAdditionalPropertiesProperties { + return *v.props +} + +func (v *ArmTemplateParameterAdditionalPropertiesValue) UnmarshalJSON(data []byte) error { + if string(data) == "false" { + return nil + } + + var props ArmTemplateParameterAdditionalPropertiesProperties + if err := json.Unmarshal(data, &props); err != nil { + return err + } + + v.props = &props + return nil +} + +func (v *ArmTemplateParameterAdditionalPropertiesValue) MarshalJSON() ([]byte, error) { + if v.props == nil { + return []byte("false"), nil + } + + return json.Marshal(v.props) +} + type ArmTemplateParameterDefinition struct { - Type string `json:"type"` - DefaultValue any `json:"defaultValue"` - AllowedValues *[]any `json:"allowedValues,omitempty"` - MinValue *int `json:"minValue,omitempty"` - MaxValue *int `json:"maxValue,omitempty"` - MinLength *int `json:"minLength,omitempty"` - MaxLength *int `json:"maxLength,omitempty"` - Metadata map[string]json.RawMessage `json:"metadata"` - Ref string `json:"$ref"` - Properties ArmTemplateParameterDefinitions `json:"properties,omitempty"` - AdditionalProperties ArmTemplateParameterAdditionalProperties `json:"additionalProperties,omitempty"` + Type string `json:"type"` + DefaultValue any `json:"defaultValue"` + AllowedValues *[]any `json:"allowedValues,omitempty"` + MinValue *int `json:"minValue,omitempty"` + MaxValue *int `json:"maxValue,omitempty"` + MinLength *int `json:"minLength,omitempty"` + MaxLength *int `json:"maxLength,omitempty"` + Metadata map[string]json.RawMessage `json:"metadata"` + Ref string `json:"$ref"` + Properties ArmTemplateParameterDefinitions `json:"properties,omitempty"` + AdditionalProperties *ArmTemplateParameterAdditionalPropertiesValue `json:"additionalProperties,omitempty"` } func (d *ArmTemplateParameterDefinition) Secure() bool { diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go index 0c0d4cff1f6..08928cf3fdb 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go @@ -1032,9 +1032,10 @@ func TestUserDefinedTypes(t *testing.T) { }, objectParam.Properties) require.NotNil(t, objectParam.AdditionalProperties) + require.True(t, objectParam.AdditionalProperties.HasAdditionalProperties()) require.Equal( t, - azure.ArmTemplateParameterAdditionalProperties{ + azure.ArmTemplateParameterAdditionalPropertiesProperties{ Type: "string", MinLength: to.Ptr(10), Metadata: map[string]json.RawMessage{ @@ -1042,7 +1043,7 @@ func TestUserDefinedTypes(t *testing.T) { "fromDefinitionBar": []byte(`"bar"`), }, }, - objectParam.AdditionalProperties) + objectParam.AdditionalProperties.Properties()) require.NotNil(t, objectParam.Metadata) require.Equal( t, @@ -1056,6 +1057,21 @@ func TestUserDefinedTypes(t *testing.T) { }, objectParam.Metadata) + sealedObjectParam, exists := template.Parameters["sealedObjectParam"] + require.True(t, exists) + require.Equal(t, "object", sealedObjectParam.Type) + require.Nil(t, sealedObjectParam.AllowedValues) + require.NotNil(t, sealedObjectParam.Properties) + require.Equal( + t, + azure.ArmTemplateParameterDefinitions{ + "name": {Type: "string"}, + "sku": {Type: "string"}, + }, + sealedObjectParam.Properties) + require.NotNil(t, sealedObjectParam.AdditionalProperties) + require.False(t, sealedObjectParam.AdditionalProperties.HasAdditionalProperties()) + // output resolves just the type. Value and Metadata should persist customOutput, exists := template.Outputs["customOutput"] require.True(t, exists) @@ -1211,6 +1227,18 @@ const userDefinedParamsSample = `{ "fromDefinitionFoo": "foo", "fromDefinitionBar": "bar" } + }, + "sealedObjectType": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sku": { + "type": "string" + } + }, + "additionalProperties": false } }, "parameters": { @@ -1242,6 +1270,9 @@ const userDefinedParamsSample = `{ "fromDefinitionBar": "override", "fromParameter": "parameter" } + }, + "sealedObjectParam": { + "$ref": "#/definitions/sealedObjectType" } }, "resources": {},