Skip to content

Commit

Permalink
Add configuration for controlling the autonaming behavior
Browse files Browse the repository at this point in the history
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 pulumi/pulumi-cdk#62
  • Loading branch information
corymhall committed Nov 14, 2024
1 parent 4111ee4 commit 178c614
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 6 deletions.
62 changes: 59 additions & 3 deletions provider/pkg/autonaming/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -39,6 +46,7 @@ func getDefaultName(
autoNamingSpec *metadata.AutoNamingSpec,
olds,
news resource.PropertyMap,
config *AutoNamingConfig,
) (resource.PropertyValue, error) {
sdkName := autoNamingSpec.SdkName

Expand All @@ -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 {
Expand Down Expand Up @@ -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:]
}
124 changes: 123 additions & 1 deletion provider/pkg/autonaming/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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))
}
11 changes: 10 additions & 1 deletion provider/pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion provider/pkg/resources/extension_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
20 changes: 20 additions & 0 deletions provider/pkg/schema/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
}
12 changes: 12 additions & 0 deletions provider/pkg/schema/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 178c614

Please sign in to comment.