Skip to content

Commit

Permalink
Add autonaming overlay (#1828)
Browse files Browse the repository at this point in the history
Most resources have some limits on what the resource name can be.
Unfortunately a lot of those limits are not currently stored in the
CloudFormation schema.

This PR introduces a new schema overlay where we can manually store
min/max length constraints for resource names.

This is the first step in addressing #1816. I will follow this up with
another PR to trim names to fit within the constraints.

re #1816, re pulumi/pulumi-cdk#62
  • Loading branch information
corymhall authored Nov 14, 2024
1 parent 4872ad6 commit 4111ee4
Show file tree
Hide file tree
Showing 11 changed files with 3,981 additions and 9 deletions.
34 changes: 34 additions & 0 deletions aws-cfn-autoname-overlay.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"Resources": {
"AWS::IAM::Role": {
"RoleName": {
"minLength": 1,
"maxLength": 64
}
},
"AWS::Lambda::Function": {
"FunctionName": {
"minLength": 1,
"maxLength": 64
}
},
"AWS::Lambda::Alias": {
"Name": {
"minLength": 1,
"maxLength": 128
}
},
"AWS::ElasticLoadBalancingV2::TargetGroup": {
"Name": {
"minLength": 1,
"maxLength": 32
}
},
"AWS::ElasticLoadBalancingV2::LoadBalancer": {
"Name": {
"minLength": 1,
"maxLength": 32
}
}
}
}
2 changes: 2 additions & 0 deletions examples/autonaming-overlay/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin/
/node_modules/
3 changes: 3 additions & 0 deletions examples/autonaming-overlay/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: aws-native-autonaming-overlay
runtime: nodejs
description: An example program to test autoname overlay
16 changes: 16 additions & 0 deletions examples/autonaming-overlay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as aws from "@pulumi/aws-native";

new aws.iam.Role("myRole".repeat(11), {
assumeRolePolicyDocument: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: {
Service: "lambda.amazonaws.com",
},
Action: "sts:AssumeRole",
},
],
},
});
3,725 changes: 3,725 additions & 0 deletions examples/autonaming-overlay/package-lock.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions examples/autonaming-overlay/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "aws-native-naming-conventions",
"main": "index.ts",
"devDependencies": {
"@types/node": "^16"
},
"dependencies": {
"@pulumi/pulumi": "^3.74.0",
"@pulumi/aws-native": "^0.8.0"
}
}
18 changes: 18 additions & 0 deletions examples/autonaming-overlay/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"strict": true,
"outDir": "bin",
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"files": [
"index.ts"
]
}
16 changes: 16 additions & 0 deletions examples/examples_nodejs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package examples

import (
"bytes"
"path/filepath"
"testing"

Expand Down Expand Up @@ -141,6 +142,21 @@ func TestNamingConventions(t *testing.T) {
integration.ProgramTest(t, &test)
}

func TestAutoNamingOverlay(t *testing.T) {
var buf bytes.Buffer
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Dir: filepath.Join(getCwd(t), "autonaming-overlay"),
Stderr: &buf,
ExpectFailure: true,
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.Contains(t, buf.String(), "is too large to fix max length constraint of 64")
},
})

integration.ProgramTest(t, &test)
}

func TestParallelTs(t *testing.T) {
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Expand Down
66 changes: 62 additions & 4 deletions provider/cmd/pulumi-gen-aws-native/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ import (

type schemaUrls []string

// CloudFormationAutoNameOverlay represents the overlay file `aws-cfn-autoname-overlay.json`.
// Some resources have name requirements that are not defined yet in the CloudFormation schema.
// This file allows us to define those requirements as an overlay. It will never overwrite existing
// values in the schema, so as they add these requirements to the schema, it will overwrite what we have.
type CloudFormationAutoNameOverlay struct {
// Resources is a map of CloudFormation resource types to their auto-naming properties.
Resources map[string]AutoNamingOverlay `json:"Resources"`
}

// AutoNamingOverlay is a map of property names to their auto-naming properties.
type AutoNamingOverlay map[string]AutoNamingProps

// AutoNamingProps is the auto-naming properties for a property.
// These properties will overwrite the auto-naming properties in the schema only
// if the schema property does not already have a value set.
type AutoNamingProps struct {
MinLength int `json:"minLength"`
MaxLength int `json:"maxLength"`
}

func (s *schemaUrls) String() string {
return fmt.Sprint(*s)
}
Expand Down Expand Up @@ -77,6 +97,12 @@ func main() {

genDir := filepath.Join(".", "provider", "cmd", "pulumi-gen-aws-native")
semanticsDir := filepath.Join(".", "provider", "cmd", "pulumi-resource-aws-native")
autonameOverlay := filepath.Join(".", "aws-cfn-autoname-overlay.json")
autoNameOverlay, err := readAutonamingOverlay(autonameOverlay)
if err != nil {
fmt.Println("Error reading autonaming overlay file: ", err)
return
}

switch operation {
case "docs":
Expand Down Expand Up @@ -106,7 +132,7 @@ func main() {

case "schema":
supportedTypes := readSupportedResourceTypes(genDir)
jsonSchemas := readJsonSchemas(schemaFolder)
jsonSchemas := readJsonSchemas(schemaFolder, autoNameOverlay)
docsTypes, err := schema.ReadCloudFormationDocsFile(filepath.Join(".", "aws-cloudformation-docs", "CloudFormationDocumentation.json"))
if err != nil {
fatalf("error reading CloudFormation docs file: %v", err)
Expand Down Expand Up @@ -172,7 +198,7 @@ func fatalf(format string, a ...any) {
log.Fatalf(fmt.Sprintf("schema generation failed\n%s\n%s\n%s\n", barrier, fmt.Sprintf(format, a...), barrier))
}

func readJsonSchemas(schemaDir string) (res []*jsschema.Schema) {
func readJsonSchemas(schemaDir string, overlay map[string]AutoNamingOverlay) (res []*jsschema.Schema) {
var fileNames []string
root := filepath.Join(".", schemaDir)
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
Expand All @@ -187,20 +213,38 @@ func readJsonSchemas(schemaDir string) (res []*jsschema.Schema) {

sort.Strings(fileNames)
for _, fileName := range fileNames {
res = append(res, readJsonSchema(fileName))
res = append(res, readJsonSchema(fileName, overlay))
}
return
}

func readJsonSchema(schemaPath string) *jsschema.Schema {
func readJsonSchema(schemaPath string, overlay map[string]AutoNamingOverlay) *jsschema.Schema {
s, err := jsschema.ReadFile(schemaPath)
if err != nil {
fatalf("%v", errors.Wrapf(err, schemaPath))
}
mergeAutoNaming(s, overlay)

return s
}

func mergeAutoNaming(schema *jsschema.Schema, overlay map[string]AutoNamingOverlay) {
if cfTypeName, ok := schema.Extras["typeName"].(string); ok {
if typeOverlay, ok := overlay[cfTypeName]; ok {
for propertyName, autoName := range typeOverlay {
if p, ok := schema.Properties[propertyName]; ok {
if p.MaxLength.Val == 0 {
p.MaxLength.Val = autoName.MaxLength
}
if p.MinLength.Val == 0 {
p.MinLength.Val = autoName.MinLength
}
}
}
}
}
}

const regionsFile = "regions.json"

func readRegions(metadataFolder string) ([]schema.RegionInfo, error) {
Expand Down Expand Up @@ -230,6 +274,20 @@ func readSupportedResourceTypes(outDir string) []string {
return strings.Split(string(bytes), "\n")
}

func readAutonamingOverlay(file string) (map[string]AutoNamingOverlay, error) {
bytes, err := os.ReadFile(file)
if err != nil {
return nil, err
}

var overlay CloudFormationAutoNameOverlay
err = json.Unmarshal(bytes, &overlay)
if err != nil {
return nil, err
}
return overlay.Resources, nil
}

func writeSupportedResourceTypes(outDir string) error {
cfg, err := config.LoadDefaultConfig(ctx.Background())
cfg.Region = "us-west-2"
Expand Down
80 changes: 80 additions & 0 deletions provider/cmd/pulumi-gen-aws-native/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"os"
"testing"

jsschema "github.com/pulumi/jsschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -53,6 +54,85 @@ func TestCleanDir(t *testing.T) {
}
}

func Test_mergeAutoNaming(t *testing.T) {
t.Run("overlay overwrites", func(t *testing.T) {
schema := &jsschema.Schema{
Extras: map[string]interface{}{
"typeName": "AWS::IAM::Role",
},
Properties: map[string]*jsschema.Schema{
"RoleName": {},
},
}

overlay := map[string]AutoNamingOverlay{
"AWS::IAM::Role": {
"RoleName": {
MaxLength: 64,
MinLength: 1,
},
},
}

mergeAutoNaming(schema, overlay)

assert.Equal(t, &jsschema.Schema{
MaxLength: jsschema.Integer{Val: 64},
MinLength: jsschema.Integer{Val: 1},
}, schema.Properties["RoleName"])
})

t.Run("overlay does not overwrite", func(t *testing.T) {
schema := &jsschema.Schema{
Extras: map[string]interface{}{
"typeName": "AWS::IAM::Role",
},
Properties: map[string]*jsschema.Schema{
"RoleName": {
MaxLength: jsschema.Integer{Val: 100},
MinLength: jsschema.Integer{Val: 4},
},
},
}

overlay := map[string]AutoNamingOverlay{
"AWS::IAM::Role": {
"RoleName": {
MaxLength: 64,
MinLength: 1,
},
},
}

mergeAutoNaming(schema, overlay)

assert.Equal(t, &jsschema.Schema{
MaxLength: jsschema.Integer{Val: 100},
MinLength: jsschema.Integer{Val: 4},
}, schema.Properties["RoleName"])
})

t.Run("overlay not found", func(t *testing.T) {
schema := &jsschema.Schema{
Extras: map[string]interface{}{
"typeName": "AWS::IAM::Role",
},
Properties: map[string]*jsschema.Schema{
"RoleName": {},
},
}

overlay := map[string]AutoNamingOverlay{}

mergeAutoNaming(schema, overlay)

assert.Equal(t, &jsschema.Schema{
MaxLength: jsschema.Integer{Val: 0},
MinLength: jsschema.Integer{Val: 0},
}, schema.Properties["RoleName"])
})
}

func assertExistsAndEmpty(t *testing.T, path string) bool {
assert := assert.New(t)

Expand Down
19 changes: 14 additions & 5 deletions provider/cmd/pulumi-resource-aws-native/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -40333,7 +40333,9 @@
}
},
"autoNamingSpec": {
"sdkName": "name"
"sdkName": "name",
"minLength": 1,
"maxLength": 32
},
"createOnly": [
"name",
Expand Down Expand Up @@ -40550,7 +40552,9 @@
}
},
"autoNamingSpec": {
"sdkName": "name"
"sdkName": "name",
"minLength": 1,
"maxLength": 32
},
"createOnly": [
"ipAddressType",
Expand Down Expand Up @@ -48671,7 +48675,9 @@
}
},
"autoNamingSpec": {
"sdkName": "roleName"
"sdkName": "roleName",
"minLength": 1,
"maxLength": 64
},
"required": [
"assumeRolePolicyDocument"
Expand Down Expand Up @@ -58167,7 +58173,9 @@
}
},
"autoNamingSpec": {
"sdkName": "name"
"sdkName": "name",
"minLength": 1,
"maxLength": 128
},
"required": [
"functionName",
Expand Down Expand Up @@ -58809,7 +58817,8 @@
},
"autoNamingSpec": {
"sdkName": "functionName",
"minLength": 1
"minLength": 1,
"maxLength": 64
},
"required": [
"code",
Expand Down

0 comments on commit 4111ee4

Please sign in to comment.