From 178c614a7f70988e5b6115573cb9f20b8d808018 Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:04:37 -0500 Subject: [PATCH] Add configuration for controlling the autonaming behavior This PR adds some new functionality to control the auto naming behavior. The new behavior lives behind a provider config variable and must be explicitly enabled by the user. The existing behavior will remain the default behavior of the provider. **What's new** - `autoTrim`: When this is set to true the provider will automatically trim the generated name to fit within the `maxLength` requirement. - `randomSuffixMinLength`: Set this to control the minimum length of the random suffix that is generated. This is especially useful in combination with `autoTrim` to ensure that you still end up with unique names (e.g. a random suffix of 1 character is not very unique) closes #1816, re https://github.com/pulumi/pulumi-cdk/issues/62 --- provider/pkg/autonaming/application.go | 62 +++++++++- provider/pkg/autonaming/application_test.go | 124 ++++++++++++++++++- provider/pkg/provider/provider.go | 11 +- provider/pkg/resources/extension_resource.go | 2 +- provider/pkg/schema/config.go | 20 +++ provider/pkg/schema/gen.go | 12 ++ 6 files changed, 225 insertions(+), 6 deletions(-) diff --git a/provider/pkg/autonaming/application.go b/provider/pkg/autonaming/application.go index 36563b0692..d3ee735658 100644 --- a/provider/pkg/autonaming/application.go +++ b/provider/pkg/autonaming/application.go @@ -4,23 +4,30 @@ package autonaming import ( "fmt" + "math" "github.com/pulumi/pulumi-aws-native/provider/pkg/metadata" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" ) +type AutoNamingConfig struct { + AutoTrim bool `json:"autoTrim"` + RandomSuffixMinLength int `json:"randomSuffixMinLength"` +} + func ApplyAutoNaming( spec *metadata.AutoNamingSpec, urn resource.URN, randomSeed []byte, olds, news resource.PropertyMap, + config *AutoNamingConfig, ) error { if spec == nil { return nil } // Auto-name fields if not already specified - val, err := getDefaultName(randomSeed, urn, spec, olds, news) + val, err := getDefaultName(randomSeed, urn, spec, olds, news, config) if err != nil { return err } @@ -39,6 +46,7 @@ func getDefaultName( autoNamingSpec *metadata.AutoNamingSpec, olds, news resource.PropertyMap, + config *AutoNamingConfig, ) (resource.PropertyValue, error) { sdkName := autoNamingSpec.SdkName @@ -58,17 +66,43 @@ func getDefaultName( return resource.PropertyValue{}, err } + var autoTrim bool + // resource.NewUniqueName does not allow for a random suffix shorter than 1. + randomSuffixMinLength := 1 + if config != nil { + autoTrim = config.AutoTrim + if config.RandomSuffixMinLength != 0 { + randomSuffixMinLength = config.RandomSuffixMinLength + } + } + // Generate random name that fits the length constraints. name := urn.Name() prefix := name + "-" - randLength := 7 + randLength := 7 // Default value + if randomSuffixMinLength > randLength { + randLength = randomSuffixMinLength + } if len(prefix)+namingTrivia.Length()+randLength < autoNamingSpec.MinLength { randLength = autoNamingSpec.MinLength - len(prefix) - namingTrivia.Length() } maxLength := 0 if autoNamingSpec.MaxLength > 0 { - left := autoNamingSpec.MaxLength - len(prefix) - namingTrivia.Length() + left := autoNamingSpec.MaxLength - len(prefix) - namingTrivia.Length() - randomSuffixMinLength + + if left <= 0 && autoTrim { + autoTrimMaxLength := autoNamingSpec.MaxLength - namingTrivia.Length() - randomSuffixMinLength + if autoTrimMaxLength <= 0 { + return resource.PropertyValue{}, fmt.Errorf("failed to auto-generate value for %[1]q."+ + " Prefix: %[2]q is too large to fix max length constraint of %[3]d"+ + " with required suffix length %[4]d. Please provide a value for %[1]q", + sdkName, prefix, autoNamingSpec.MaxLength, randomSuffixMinLength) + } + prefix = trimName(prefix, autoTrimMaxLength) + randLength = randomSuffixMinLength + left = randomSuffixMinLength + } if left <= 0 { if namingTrivia.Length() > 0 { @@ -101,3 +135,25 @@ func getDefaultName( return resource.NewStringProperty(random), nil } + +// trimName will trim the prefix to fit within the max length constraint. +// It will cut out part of the middle, keeping the beginning and the end of the string. +// This is so that really long generated names can still be unique. For example, if the +// user creates a resource name by appending the parent onto the child, you could end up +// with names like: +// - "topParent-middleParent-bottonParent-child1" +// - "topParent-middleParent-bottonParent-child2" +// +// If the max length is 30, the trimmed generated name for both would be something like +// "topParent-middleParent-bottonP" and you would not be able to tell them apart. +// +// By trimming from the middle you would end up with something like this, preserving +// the uniqueness of the generated names: +// - "topParent-middlonParent-child1" +// - "topParent-middlonParent-child2" +func trimName(name string, maxLength int) string { + floorHalf := math.Floor(float64(maxLength) / 2) + half := int(floorHalf) + left := maxLength - half + return name[0:half] + name[len(name)-left:] +} diff --git a/provider/pkg/autonaming/application_test.go b/provider/pkg/autonaming/application_test.go index fb4be3adc4..96cdc05811 100644 --- a/provider/pkg/autonaming/application_test.go +++ b/provider/pkg/autonaming/application_test.go @@ -8,6 +8,7 @@ import ( "github.com/pulumi/pulumi-aws-native/provider/pkg/metadata" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -73,7 +74,123 @@ func Test_getDefaultName(t *testing.T) { MinLength: tt.minLength, MaxLength: tt.maxLength, } - got, err := getDefaultName(nil, urn, autoNamingSpec, tt.olds, tt.news) + got, err := getDefaultName(nil, urn, autoNamingSpec, tt.olds, tt.news, nil) + if tt.err != nil { + require.EqualError(t, err, tt.err.Error()) + return + } else { + require.NoError(t, err) + } + if !tt.comparison(got) { + t.Errorf("getDefaultName() = %v for spec: %+v", got, autoNamingSpec) + } + t.Logf("getDefaultName() = %v for spec: %+v", got, autoNamingSpec) + }) + } +} + +func Test_getDefaultName_withAutoNameConfig(t *testing.T) { + const sdkName = "autoName" + tests := []struct { + name string + resourceName string + autoNameConfig AutoNamingConfig + minLength int + maxLength int + olds resource.PropertyMap + news resource.PropertyMap + err error + comparison func(actual resource.PropertyValue) bool + }{ + { + name: "Name specified explicitly", + resourceName: "myName", + news: resource.PropertyMap{ + resource.PropertyKey(sdkName): resource.NewStringProperty("newName"), + }, + autoNameConfig: AutoNamingConfig{ + AutoTrim: true, + RandomSuffixMinLength: 1, + }, + maxLength: 2, + comparison: equals(resource.NewStringProperty("newName")), + }, + { + name: "Use old name", + resourceName: "myName", + olds: resource.PropertyMap{ + resource.PropertyKey(sdkName): resource.NewStringProperty("oldName"), + }, + autoNameConfig: AutoNamingConfig{ + AutoTrim: true, + RandomSuffixMinLength: 1, + }, + maxLength: 2, + comparison: equals(resource.NewStringProperty("oldName")), + }, + { + resourceName: "myReallyLongName", + name: "Autoname with constraints on max length", + maxLength: 12, + autoNameConfig: AutoNamingConfig{ + AutoTrim: true, + }, + comparison: within(12, 12), + }, + { + resourceName: "myReallyLongName", + name: "Autoname with odd constraints on max length", + maxLength: 13, + autoNameConfig: AutoNamingConfig{ + AutoTrim: true, + }, + comparison: within(13, 13), + }, + { + name: "Autoname with max length too small and no auto trim", + resourceName: "myName", + maxLength: 4, + autoNameConfig: AutoNamingConfig{ + RandomSuffixMinLength: 2, + }, + comparison: within(15, 15), + err: fmt.Errorf("failed to auto-generate value for %[1]q. Prefix: \"myName-\" is too large to fix max length constraint of 4. Please provide a value for %[1]q", sdkName), + }, + { + name: "Autoname with constraints on min and max length", + minLength: 13, + resourceName: "myReallyLongName", + autoNameConfig: AutoNamingConfig{ + RandomSuffixMinLength: 2, + AutoTrim: true, + }, + maxLength: 13, + comparison: within(13, 13), + }, + { + name: "Autoname with long random suffix", + resourceName: "myReallyLongName", + autoNameConfig: AutoNamingConfig{ + RandomSuffixMinLength: 14, + AutoTrim: true, + }, + maxLength: 13, + comparison: within(13, 13), + err: fmt.Errorf("failed to auto-generate value for %[1]q. Prefix: \"myReallyLongName-\" is too large to fix max length constraint of 13 with required suffix length 14. Please provide a value for %[1]q", sdkName), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + urn := resource.URN(fmt.Sprintf("urn:pulumi:dev::test::test-provider:testModule:TestResource::%s", tt.resourceName)) + autoNamingSpec := &metadata.AutoNamingSpec{ + SdkName: "autoName", + MinLength: tt.minLength, + MaxLength: tt.maxLength, + } + + autoNameConfig := tt.autoNameConfig + got, err := getDefaultName(nil, urn, autoNamingSpec, tt.olds, tt.news, &autoNameConfig) if tt.err != nil { require.EqualError(t, err, tt.err.Error()) return @@ -100,3 +217,8 @@ func within(min, max int) func(value resource.PropertyValue) bool { return min <= l && l <= max } } + +func Test_trimName(t *testing.T) { + assert.Equal(t, "mysupgname", trimName("mysuperlongname", 10)) + assert.Equal(t, "mysupernam", trimName("mysupernam", 10)) +} diff --git a/provider/pkg/provider/provider.go b/provider/pkg/provider/provider.go index c058c243f7..5f78c8db75 100644 --- a/provider/pkg/provider/provider.go +++ b/provider/pkg/provider/provider.go @@ -107,6 +107,7 @@ type cfnProvider struct { partition partition resourceMap *metadata.CloudAPIMetadata roleArn *string + autoNamingConfig *autonaming.AutoNamingConfig allowedAccountIds []string forbiddenAccountIds []string defaultTags map[string]string @@ -519,6 +520,14 @@ func (p *cfnProvider) Configure(ctx context.Context, req *pulumirpc.ConfigureReq metadata.CfnCustomResourceToken: resources.NewCfnCustomResource(p.name, s3Client, lambdaClient), } + if autoNaming, ok := vars["aws-native:config:autoNaming"]; ok { + var autoNamingConfig autonaming.AutoNamingConfig + if err := json.Unmarshal([]byte(autoNaming), &autoNamingConfig); err != nil { + return nil, fmt.Errorf("failed to unmarshal 'autoNaming' config: %w", err) + } + p.autoNamingConfig = &autoNamingConfig + } + p.configured = true return &pulumirpc.ConfigureResponse{ @@ -743,7 +752,7 @@ func (p *cfnProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) (* return nil, errors.Errorf("resource type %s not found", resourceToken) } - if err := autonaming.ApplyAutoNaming(spec.AutoNamingSpec, urn, req.RandomSeed, olds, newInputs); err != nil { + if err := autonaming.ApplyAutoNaming(spec.AutoNamingSpec, urn, req.RandomSeed, olds, newInputs, p.autoNamingConfig); err != nil { return nil, fmt.Errorf("failed to apply auto-naming: %w", err) } diff --git a/provider/pkg/resources/extension_resource.go b/provider/pkg/resources/extension_resource.go index bcaf7d5dd0..c3ce163cce 100644 --- a/provider/pkg/resources/extension_resource.go +++ b/provider/pkg/resources/extension_resource.go @@ -64,7 +64,7 @@ func (r *extensionResource) Check(ctx context.Context, urn resource.URN, randomS SdkName: typedInputs.AutoNaming.PropertyName, MinLength: typedInputs.AutoNaming.MinLength, MaxLength: typedInputs.AutoNaming.MaxLength, - }, urn, randomSeed, state, inputs); err != nil { + }, urn, randomSeed, state, inputs, nil); err != nil { return nil, nil, fmt.Errorf("failed to apply auto-naming: %w", err) } } diff --git a/provider/pkg/schema/config.go b/provider/pkg/schema/config.go index 6c9ce37910..fab8799576 100644 --- a/provider/pkg/schema/config.go +++ b/provider/pkg/schema/config.go @@ -139,6 +139,24 @@ var ignoreTags = pschema.ComplexTypeSpec{ }, } +var autoName = pschema.ComplexTypeSpec{ + ObjectTypeSpec: pschema.ObjectTypeSpec{ + Description: "The configuration for automatically naming resources.", + Properties: map[string]pschema.PropertySpec{ + "autoTrim": { + Description: "Automatically trim the auto-generated name to meet the maximum length constraint.", + TypeSpec: pschema.TypeSpec{Type: "boolean"}, + }, + "randomSuffixMinLength": { + Description: "The minimum length of the random suffix to append to the auto-generated name.", + Default: 1, + TypeSpec: pschema.TypeSpec{Type: "integer"}, + }, + }, + Type: "object", + }, +} + func generateRegionEnum(regions []RegionInfo) pschema.ComplexTypeSpec { enums := make([]pschema.EnumValueSpec, len(regions)) for i, region := range regions { @@ -178,4 +196,6 @@ var typeOverlays = map[string]pschema.ComplexTypeSpec{ "aws-native:index:ProviderEndpoint": configToProvider(endpoints), "aws-native:config:IgnoreTags": ignoreTags, "aws-native:index:ProviderIgnoreTags": configToProvider(ignoreTags), + "aws-native:config:AutoNaming": autoName, + "aws-native:index:ProviderAutoNaming": configToProvider(autoName), } diff --git a/provider/pkg/schema/gen.go b/provider/pkg/schema/gen.go index 6fddcd3b4c..d19a2792f8 100644 --- a/provider/pkg/schema/gen.go +++ b/provider/pkg/schema/gen.go @@ -177,6 +177,10 @@ func GatherPackage(supportedResourceTypes []string, jsonSchemas []*jsschema.Sche TypeSpec: pschema.TypeSpec{Type: "string"}, Secret: true, }, + "autoNaming": { + Description: "The configuration for automatically naming resources.", + TypeSpec: pschema.TypeSpec{Ref: "#/types/aws-native:config:AutoNaming"}, + }, }, Required: []string{ "region", @@ -261,6 +265,10 @@ func GatherPackage(supportedResourceTypes []string, jsonSchemas []*jsschema.Sche Description: "Skip requesting the account ID. Used for AWS API implementations that do not have IAM/STS API and/or metadata API.", TypeSpec: pschema.TypeSpec{Type: "boolean"}, }, + "autoNaming": { + Description: "The configuration for automatically naming resources.", + TypeSpec: pschema.TypeSpec{Ref: "#/types/aws-native:index:ProviderAutoNaming"}, + }, }, }, InputProperties: map[string]pschema.PropertySpec{ @@ -374,6 +382,10 @@ func GatherPackage(supportedResourceTypes []string, jsonSchemas []*jsschema.Sche Secret: true, TypeSpec: pschema.TypeSpec{Type: "string"}, }, + "autoNaming": { + Description: "The configuration for automatically naming resources.", + TypeSpec: pschema.TypeSpec{Ref: "#/types/aws-native:index:ProviderAutoNaming"}, + }, }, RequiredInputs: []string{ "region",