diff --git a/README.md b/README.md index a8f44d4f..91ba0ce7 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ cfn-lint, Guard and more: * **Modules** (EXPERIMENTAL): `rain pkg` supports client-side module development with the `!Rain::Module` directive. Rain modules are partial templates that are inserted into the parent template, with some extra functionality added to enable extending existing resource types. This feature integrates with CodeArtifact to enable package publish and install. +* **Content Deployment** (EXPERIMENTAL): `rain deploy` and `rain rm` support metadata commands that can upload static assets to a bucket and then delete those assets when the bucket is deleted. Rain can also run build scripts before and after stack deployment to prepare content like web sites and lambda functions before uploading to S3. + _Note that in order to use experimental commands, you have to add `--experimental` or `-x` as an argument._ ## Getting started diff --git a/cft/cft.go b/cft/cft.go index dba43c7f..c26096c4 100644 --- a/cft/cft.go +++ b/cft/cft.go @@ -8,6 +8,7 @@ import ( "fmt" "slices" + "github.com/aws-cloudformation/rain/internal/config" "github.com/aws-cloudformation/rain/internal/node" "github.com/aws-cloudformation/rain/internal/s11n" "gopkg.in/yaml.v3" @@ -118,6 +119,9 @@ func (t Template) AddMapSection(section Section) (*yaml.Node, error) { // GetSection returns the yaml node for the section func (t Template) GetSection(section Section) (*yaml.Node, error) { + if t.Node == nil { + return nil, fmt.Errorf("unable to get section because t.Node is nil") + } _, s, _ := s11n.GetMapValue(t.Node.Content[0], string(section)) if s == nil { return nil, fmt.Errorf("unable to locate the %s node", section) @@ -148,20 +152,27 @@ func (t Template) GetTypes() ([]string, error) { return retval, nil } -func (t Template) GetResourcesOfType(typeName string) []*yaml.Node { +type Resource struct { + LogicalId string + Node *yaml.Node +} + +func (t Template) GetResourcesOfType(typeName string) []*Resource { resources, err := t.GetSection(Resources) if err != nil { + config.Debugf("GetResourcesOfType error: %v", err) return nil } - retval := make([]*yaml.Node, 0) + retval := make([]*Resource, 0) for i := 0; i < len(resources.Content); i += 2 { + logicalId := resources.Content[i].Value resource := resources.Content[i+1] _, typ, _ := s11n.GetMapValue(resource, "Type") if typ == nil { continue } if typ.Value == typeName { - retval = append(retval, resource) + retval = append(retval, &Resource{LogicalId: logicalId, Node: resource}) } } return retval diff --git a/cft/format/transform.go b/cft/format/transform.go index f57da23f..79e30a90 100644 --- a/cft/format/transform.go +++ b/cft/format/transform.go @@ -34,12 +34,6 @@ func formatNode(n *yaml.Node) *yaml.Node { // Does it have just one key/value pair? if len(n.Content) == 2 { - if n.Content[1].Kind == yaml.ScalarNode { - if NodeStyle == "quotescalars" { - n.Content[1].Style = yaml.DoubleQuotedStyle - } - } - // Is the key relevant? for tag, funcName := range cft.Tags { if n.Content[0].Value == funcName { @@ -121,6 +115,10 @@ func formatNode(n *yaml.Node) *yaml.Node { n.Style = yaml.FlowStyle case "original": // Do nothing, leave it alone + case "quotescalars": + if n.Kind == yaml.ScalarNode { + n.Style = yaml.DoubleQuotedStyle + } case "": // Default style for consistent formatting n.Style = 0 diff --git a/cft/parse/parse_test.go b/cft/parse/parse_test.go index 3e1f0612..8bd2b8db 100644 --- a/cft/parse/parse_test.go +++ b/cft/parse/parse_test.go @@ -232,3 +232,23 @@ Resources: t.Error("Unexpected: resource is nil") } } + +func TestGetResourcesOfType(t *testing.T) { + + source := ` +Resources: + Bucket: + Type: AWS::S3::Bucket +` + + template, err := parse.String(source) + if err != nil { + t.Fatal(err) + } + + resources := template.GetResourcesOfType("AWS::S3::Bucket") + + if len(resources) != 1 { + t.Fatal("should have found 1 resource") + } +} diff --git a/cft/pkg/directives.go b/cft/pkg/directives.go index 2962c2a7..32dac897 100644 --- a/cft/pkg/directives.go +++ b/cft/pkg/directives.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "strings" @@ -32,6 +33,7 @@ type s3Options struct { KeyProperty string `yaml:"KeyProperty"` Zip bool `yaml:"Zip"` Format s3Format `yaml:"Format"` + Run string `yaml:"Run"` } type directiveContext struct { @@ -109,7 +111,6 @@ func includeLiteral(ctx *directiveContext) (bool, error) { } func includeEnv(ctx *directiveContext) (bool, error) { - config.Debugf("includeEnv n: %v", node.ToSJson(ctx.n)) name, err := expectString(ctx.n) if err != nil { return false, err @@ -131,6 +132,29 @@ func includeEnv(ctx *directiveContext) (bool, error) { } func handleS3(root string, options s3Options) (*yaml.Node, error) { + + // Check to see if we need to run a build command first + if options.Run != "" { + relativePath := filepath.Join(".", root, options.Run) + absPath, absErr := filepath.Abs(relativePath) + if absErr != nil { + config.Debugf("filepath.Abs failed? %s", absErr) + return nil, absErr + } + cmd := exec.Command(absPath) + var stdout strings.Builder + var stderr strings.Builder + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Dir = root + err := cmd.Run() + if err != nil { + config.Debugf("s3Option Run %s failed with %s: %s", + options.Run, err, stderr.String()) + return nil, err + } + } + s, err := upload(root, options.Path, options.Zip) if err != nil { return nil, err @@ -179,31 +203,36 @@ func handleS3(root string, options s3Options) (*yaml.Node, error) { } func includeS3Object(ctx *directiveContext) (bool, error) { + n := ctx.n parent := ctx.parent if n.Kind != yaml.MappingNode || len(n.Content) != 2 { return false, errors.New("expected a map") } - // Check to see if the Path is a Ref. + // Check to see if any of the properties is a Ref. // The only valid use case is if the !Rain::S3 directive is inside a module, // and the Ref points to one of the properties set in the parent template - _, pathOption, _ := s11n.GetMapValue(n.Content[1], "Path") - if pathOption != nil && pathOption.Kind == yaml.MappingNode { - if pathOption.Content[0].Value == "Ref" { - if parent.Parent != nil { - moduleParentMap := parent.Parent.Value - _, moduleParentProps, _ := s11n.GetMapValue(moduleParentMap, "Properties") - if moduleParentProps != nil { - _, pathProp, _ := s11n.GetMapValue(moduleParentProps, pathOption.Content[1].Value) - if pathProp != nil { - // Replace the Ref with the value - node.SetMapValue(n.Content[1], "Path", node.Clone(pathProp)) + for i := 0; i < len(n.Content[1].Content); i += 2 { + s3opt := n.Content[1].Content[i+1] + name := n.Content[1].Content[i].Value + if s3opt.Kind == yaml.MappingNode { + if s3opt.Content[0].Value == "Ref" { + if parent.Parent != nil { + moduleParentMap := parent.Parent.Value + _, moduleParentProps, _ := s11n.GetMapValue(moduleParentMap, "Properties") + if moduleParentProps != nil { + _, parentProp, _ := s11n.GetMapValue(moduleParentProps, s3opt.Content[1].Value) + if parentProp != nil { + // Replace the Ref with the value + node.SetMapValue(n.Content[1], name, node.Clone(parentProp)) + } else { + config.Debugf("expected Properties to have Path") + } } else { - config.Debugf("expected Properties to have Path") + config.Debugf("expected parent resource to have Properties") + config.Debugf("moduleParentMap: %s", node.ToSJson(moduleParentMap)) } - } else { - config.Debugf("expected parent resource to have Properties") } } } diff --git a/cft/pkg/module.go b/cft/pkg/module.go index a55f0b0d..97e235c0 100644 --- a/cft/pkg/module.go +++ b/cft/pkg/module.go @@ -17,6 +17,94 @@ import ( "gopkg.in/yaml.v3" ) +// mergeNodes merges two mapping nodes, replacing any values found in override +func MergeNodes(original *yaml.Node, override *yaml.Node) *yaml.Node { + + // If the nodes are not the same kind, just return the override + if override.Kind != original.Kind { + return override + } + + if override.Kind == yaml.ScalarNode { + return override + } + + retval := &yaml.Node{Kind: override.Kind, Content: make([]*yaml.Node, 0)} + overrideMap := make(map[string]bool) + + if override.Kind == yaml.SequenceNode { + retval.Content = append(retval.Content, override.Content...) + + for _, n := range original.Content { + already := false + for _, r := range retval.Content { + if r.Value == n.Value { + already = true + break + } + } + if !already { + retval.Content = append(retval.Content, n) + } + } + + return retval + } + + // else they are both Mapping nodes + + // Add everything in the override Mapping + for i, n := range override.Content { + retval.Content = append(retval.Content, n) + var name string + if i%2 == 0 { + // Remember what we added + name = n.Value + overrideMap[name] = true + } else { + name = retval.Content[i-1].Value + } + + /* + Original: + A: something + B: + foo: 1 + bar: 2 + + Override: + A: something else + B: + foo: 3 + baz: 6 + */ + + // Recurse if this is a mapping node + if i%2 == 1 && n.Kind == yaml.MappingNode { + // Find the matching node in original + for j, match := range original.Content { + if j%2 == 0 && match.Value == name { + n.Content[i] = MergeNodes(n.Content[i], original.Content[j]) + } + } + } + } + + // Only add things from the original if they weren't in original + for i := 0; i < len(original.Content); i++ { + n := original.Content[i] + if i%2 == 0 { + if _, exists := overrideMap[n.Value]; exists { + i = i + 1 // Skip the next node + continue + } + } + retval.Content = append(retval.Content, n) + } + + return retval +} + // Clone a property-like node from the module and replace any overridden values func cloneAndReplaceProps( n *yaml.Node, @@ -64,10 +152,34 @@ func cloneAndReplaceProps( for j, mprop := range props.Content { // Property names are even-indexed array elements if tprop.Value == mprop.Value && i%2 == 0 && j%2 == 0 { - // Is a clone good enough here? Could get weird. - // Maybe we just require that you replace the entire property if it's nested - // Otherwise we have to do a diff - props.Content[j+1] = node.Clone(templateProps.Content[i+1]) + // It's was not possible to override just one nested property + // + // Bucket: + // Metadata: + // Rain: + // Content: site + // DistributionLogicalId: AWS::NoValue + // + // Override: + // Bucket: + // Metadata: + // Rain: + // DistributionLogicalId: MyDistribution + // + // The result is that the Content node is removed + // + + // The old way + // props.Content[j+1] = node.Clone(templateProps.Content[i+1]) + + // The new way + clonedNode := node.Clone(templateProps.Content[i+1]) + // config.Debugf("original: %s", node.ToSJson(templateProps.Content[i+1])) + // config.Debugf("clonedNode: %s", node.ToSJson(clonedNode)) + merged := MergeNodes(props.Content[j+1], clonedNode) + // config.Debugf("merged: %s", node.ToSJson(merged)) + props.Content[j+1] = merged + found = true } } @@ -221,8 +333,20 @@ func resolveModuleRef(parentName string, prop *yaml.Node, sidx int, ctx *refctx) // Look for this property name in the parent template _, parentVal, _ := s11n.GetMapValue(templateProps, prop.Value) if parentVal == nil { - return fmt.Errorf("did not find %v in parent template Properties", - prop.Value) + // Check to see if there is a Default + _, mParam, _ := s11n.GetMapValue(moduleParams, prop.Value) + if mParam != nil { + _, defaultNode, _ := s11n.GetMapValue(mParam, "Default") + if defaultNode != nil { + parentVal = defaultNode + } + } + + // If we didn't find a parent template prop or a default, fail + if parentVal == nil { + return fmt.Errorf("did not find %v in parent template Properties", + prop.Value) + } } replaceProp(prop, parentName, parentVal, outNode, sidx) @@ -447,15 +571,18 @@ func resolveRefs(ctx *refctx) error { // Replace references to the module's parameters with the value supplied // by the parent template. Rename refs to other resources in the module. - - _, outNodeProps, _ := s11n.GetMapValue(outNode, "Properties") - if outNodeProps != nil { - for i, prop := range outNodeProps.Content { - if i%2 == 0 { - propName := prop.Value - err := renamePropRefs(propName, propName, outNodeProps.Content[i+1], -1, ctx) - if err != nil { - return fmt.Errorf("unable to resolve refs for %v: %v", propName, err) + propLikes := []string{"Properties", "Metadata"} + for _, propLike := range propLikes { + _, outNodeProps, _ := s11n.GetMapValue(outNode, propLike) + if outNodeProps != nil { + for i, prop := range outNodeProps.Content { + if i%2 == 0 { + propName := prop.Value + err := renamePropRefs(propName, propName, outNodeProps.Content[i+1], -1, ctx) + if err != nil { + return fmt.Errorf("unable to resolve refs for %s %v: %v", + propLike, propName, err) + } } } } @@ -520,6 +647,49 @@ func processModule( // Overrides have overridden values for module resources. Anything in a module can be overridden. _, overrides, _ := s11n.GetMapValue(templateResource, "Overrides") + // Validate that the overrides actually exist and error if not + if overrides != nil { + for i, override := range overrides.Content { + if i%2 != 0 { + continue + } + foundName := false + for i, moduleResource := range moduleResources.Content { + if moduleResource.Kind != yaml.MappingNode { + continue + } + name := moduleResources.Content[i-1].Value + if name == override.Value { + foundName = true + break + } + } + if !foundName { + return false, fmt.Errorf("override not found: %s", override.Value) + } + + // Make sure this Override name is not a module parameter. + // It is an error to try to override a property that shares + // a name with a module Parameter. + if moduleParams != nil { + _, overrideProps, _ := s11n.GetMapValue(overrides.Content[i+1], "Properties") + if overrideProps != nil { + for op, overrideProp := range overrideProps.Content { + if op%2 != 0 { + continue + } + _, mp, _ := s11n.GetMapValue(moduleParams, overrideProp.Value) + if mp != nil { + return false, + fmt.Errorf("cannot override module parameter %s", + overrideProp.Value) + } + } + } + } + } + } + fe, err := handleForEach(moduleResources, t, logicalId, outputNode, moduleParams, templateProps) if err != nil { @@ -734,8 +904,6 @@ func downloadModule(uri string) ([]byte, error) { // Type: !Rain::Module func module(ctx *directiveContext) (bool, error) { - config.Debugf("module directiveContext: %+v", ctx) - n := ctx.n root := ctx.rootDir t := ctx.t diff --git a/cft/pkg/module_test.go b/cft/pkg/module_test.go index 3f8036bb..240169b8 100644 --- a/cft/pkg/module_test.go +++ b/cft/pkg/module_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws-cloudformation/rain/cft/diff" "github.com/aws-cloudformation/rain/cft/parse" "github.com/aws-cloudformation/rain/cft/pkg" + "github.com/aws-cloudformation/rain/internal/node" "gopkg.in/yaml.v3" ) @@ -14,6 +15,14 @@ func TestModule(t *testing.T) { runTest("test", t) } +func TestBucket(t *testing.T) { + runTest("bucket", t) +} + +func TestApi(t *testing.T) { + runTest("api", t) +} + func TestSimple(t *testing.T) { runTest("simple", t) } @@ -30,10 +39,22 @@ func TestMany(t *testing.T) { runTest("many", t) } +func TestRef(t *testing.T) { + runTest("ref", t) +} + +func TestMeta(t *testing.T) { + runTest("meta", t) +} + func TestRefFalse(t *testing.T) { runTest("ref-false", t) } +func TestOverride(t *testing.T) { + runFailTest("override", t) +} + // TODO: This was broken in the refactor, come back to it later //func TestForeach(t *testing.T) { // runTest("foreach", t) @@ -71,6 +92,19 @@ func runTest(test string, t *testing.T) { } } +// runFailTest should fail to package +func runFailTest(test string, t *testing.T) { + + pkg.Experimental = true + + _, err := pkg.File(fmt.Sprintf("./tmpl/%v-template.yaml", test)) + if err == nil { + t.Errorf("did not fail: packaged %s", test) + return + } + +} + func TestCsvToSequence(t *testing.T) { csv := "A,B,C" seq := pkg.ConvertCsvToSequence(csv) @@ -83,3 +117,36 @@ func TestCsvToSequence(t *testing.T) { t.Errorf("Unexpected sequence") } } + +func TestMergeNodes(t *testing.T) { + original := &yaml.Node{Kind: yaml.MappingNode, Content: make([]*yaml.Node, 0)} + override := &yaml.Node{Kind: yaml.MappingNode, Content: make([]*yaml.Node, 0)} + expected := &yaml.Node{Kind: yaml.MappingNode, Content: make([]*yaml.Node, 0)} + + original.Content = append(original.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: "A"}) + original.Content = append(original.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: "foo"}) + + override.Content = append(override.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: "A"}) + override.Content = append(override.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: "bar"}) + + expected.Content = append(expected.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: "A"}) + expected.Content = append(expected.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: "bar"}) + + merged := pkg.MergeNodes(original, override) + + diff := node.Diff(merged, expected) + + if len(diff) > 0 { + for _, d := range diff { + fmt.Println(d) + } + t.Fatalf("nodes are not the same") + } + +} diff --git a/cft/pkg/pkg.go b/cft/pkg/pkg.go index 62a33076..2a627a2d 100644 --- a/cft/pkg/pkg.go +++ b/cft/pkg/pkg.go @@ -168,6 +168,7 @@ func Template(t cft.Template, rootDir string, fs *embed.FS) (cft.Template, error } // We lose the Document node here + // TODO: Actually we're ending up with 2 document nodes somehow... retval := cft.Template{} retval.Node = &yaml.Node{Kind: yaml.DocumentNode, Content: make([]*yaml.Node, 0)} retval.Node.Content = append(retval.Node.Content, templateNode) diff --git a/cft/pkg/tmpl/api-expect.yaml b/cft/pkg/tmpl/api-expect.yaml new file mode 100644 index 00000000..555783ff --- /dev/null +++ b/cft/pkg/tmpl/api-expect.yaml @@ -0,0 +1,14 @@ +Resources: + + MethGet: + Type: AWS::ApiGateway::Method + Properties: + HttpMethod: GET + ResourceId: !Ref Resource + RestApiId: !Ref RestApi + AuthorizationType: NONE + AuthorizerId: !Ref AuthorizerId + Integration: + IntegrationHttpMethod: POST + Type: AWS_PROXY + Uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Handler.Arn}/invocations" diff --git a/cft/pkg/tmpl/api-module.yaml b/cft/pkg/tmpl/api-module.yaml new file mode 100644 index 00000000..1c69f782 --- /dev/null +++ b/cft/pkg/tmpl/api-module.yaml @@ -0,0 +1,14 @@ +Resources: + + Get: + Type: AWS::ApiGateway::Method + Properties: + HttpMethod: GET + ResourceId: !Ref Resource + RestApiId: !Ref RestApi + AuthorizationType: COGNITO_USER_POOLS + AuthorizerId: !Ref AuthorizerId + Integration: + IntegrationHttpMethod: POST + Type: AWS_PROXY + Uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Handler.Arn}/invocations" diff --git a/cft/pkg/tmpl/api-template.yaml b/cft/pkg/tmpl/api-template.yaml new file mode 100644 index 00000000..b55c0ffd --- /dev/null +++ b/cft/pkg/tmpl/api-template.yaml @@ -0,0 +1,8 @@ +Resources: + + Meth: + Type: !Rain::Module "./api-module.yaml" + Overrides: + Get: + Properties: + AuthorizationType: NONE diff --git a/cft/pkg/tmpl/bucket-expect.yaml b/cft/pkg/tmpl/bucket-expect.yaml index 27b8501e..c31c1293 100644 --- a/cft/pkg/tmpl/bucket-expect.yaml +++ b/cft/pkg/tmpl/bucket-expect.yaml @@ -1,12 +1,9 @@ Resources: RainBucketBucket: - DeletionPolicy: !Ref RetentionPolicy - UpdateReplacePolicy: Delete Type: AWS::S3::Bucket - Metadata: {} Properties: LoggingConfiguration: - DestinationBucketName: !Ref ModuleExampleLogBucket + DestinationBucketName: !Ref RainBucketLogBucket BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: @@ -21,9 +18,10 @@ Resources: Value: test-value2 VersioningConfiguration: Status: Enabled + DeletionPolicy: !Ref RetentionPolicy + UpdateReplacePolicy: Delete RainBucketLogBucket: - DeletionPolicy: Retain Type: AWS::S3::Bucket Properties: BucketName: test-module-log-bucket @@ -34,8 +32,12 @@ Resources: VersioningConfiguration: Status: Enabled PublicAccessBlockConfiguration: - BlockPublicAcls: true + BlockPublicAcls: false + SomeNewThing: foo BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true + DeletionPolicy: Retain + Condition: HasLogBucket + diff --git a/cft/pkg/tmpl/bucket-module.yaml b/cft/pkg/tmpl/bucket-module.yaml index 835ffe59..fbb46fb1 100644 --- a/cft/pkg/tmpl/bucket-module.yaml +++ b/cft/pkg/tmpl/bucket-module.yaml @@ -9,6 +9,7 @@ Conditions: - true Resources: Bucket: + Type: AWS::S3::Bucket DeletionPolicy: !Ref RetentionPolicy Properties: LoggingConfiguration: diff --git a/cft/pkg/tmpl/bucket-template.yaml b/cft/pkg/tmpl/bucket-template.yaml index 369d3726..ffc3f2f9 100644 --- a/cft/pkg/tmpl/bucket-template.yaml +++ b/cft/pkg/tmpl/bucket-template.yaml @@ -15,5 +15,8 @@ Resources: LogBucket: Properties: BucketName: test-module-log-bucket + PublicAccessBlockConfiguration: + BlockPublicAcls: false + SomeNewThing: foo diff --git a/cft/pkg/tmpl/meta-expect.yaml b/cft/pkg/tmpl/meta-expect.yaml new file mode 100644 index 00000000..604a826b --- /dev/null +++ b/cft/pkg/tmpl/meta-expect.yaml @@ -0,0 +1,6 @@ +Resources: + BucketBucket: + Type: AWS::S3::Bucket + Metadata: + Rain: + Content: foo diff --git a/cft/pkg/tmpl/meta-module.yaml b/cft/pkg/tmpl/meta-module.yaml new file mode 100644 index 00000000..e3891cd7 --- /dev/null +++ b/cft/pkg/tmpl/meta-module.yaml @@ -0,0 +1,11 @@ +Parameters: + Content: + Type: String +Resources: + Bucket: + Type: AWS::S3::Bucket + Metadata: + Rain: + Content: !Ref Content + + diff --git a/cft/pkg/tmpl/meta-template.yaml b/cft/pkg/tmpl/meta-template.yaml new file mode 100644 index 00000000..a626d256 --- /dev/null +++ b/cft/pkg/tmpl/meta-template.yaml @@ -0,0 +1,6 @@ +Resources: + Bucket: + Type: !Rain::Module "./meta-module.yaml" + Properties: + Content: foo + diff --git a/cft/pkg/tmpl/override-expect.yaml b/cft/pkg/tmpl/override-expect.yaml new file mode 100644 index 00000000..f04aaa69 --- /dev/null +++ b/cft/pkg/tmpl/override-expect.yaml @@ -0,0 +1,9 @@ +Resources: + MyBucket1: + Type: AWS::S3::Bucket + Properties: + BucketName: foo + MyBucket2: + Type: AWS::S3::Bucket + Properties: + BucketName: bar diff --git a/cft/pkg/tmpl/override-module.yaml b/cft/pkg/tmpl/override-module.yaml new file mode 100644 index 00000000..27767d72 --- /dev/null +++ b/cft/pkg/tmpl/override-module.yaml @@ -0,0 +1,15 @@ +Parameters: + Name: + Type: String + BucketName: + Type: String +Resources: + Bucket1: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref Name + Bucket2: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref BucketName + diff --git a/cft/pkg/tmpl/override-template.yaml b/cft/pkg/tmpl/override-template.yaml new file mode 100644 index 00000000..0569e076 --- /dev/null +++ b/cft/pkg/tmpl/override-template.yaml @@ -0,0 +1,10 @@ +Resources: + My: + Type: !Rain::Module "./override-module.yaml" + Properties: + Name: foo + BucketName: bar + Overrides: + Bucket2: + Properties: + BucketName: baz diff --git a/cft/pkg/tmpl/ref-expect.yaml b/cft/pkg/tmpl/ref-expect.yaml new file mode 100644 index 00000000..f11524e5 --- /dev/null +++ b/cft/pkg/tmpl/ref-expect.yaml @@ -0,0 +1,6 @@ +Resources: + BucketBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: foo + diff --git a/cft/pkg/tmpl/ref-module.yaml b/cft/pkg/tmpl/ref-module.yaml new file mode 100644 index 00000000..7016c862 --- /dev/null +++ b/cft/pkg/tmpl/ref-module.yaml @@ -0,0 +1,10 @@ +Parameters: + AppName: + Type: String +Resources: + Bucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref AppName + + diff --git a/cft/pkg/tmpl/ref-template.yaml b/cft/pkg/tmpl/ref-template.yaml new file mode 100644 index 00000000..b90b8f5c --- /dev/null +++ b/cft/pkg/tmpl/ref-template.yaml @@ -0,0 +1,6 @@ +Resources: + Bucket: + Type: !Rain::Module "./ref-module.yaml" + Properties: + AppName: foo + diff --git a/cft/pkg/tmpl/test-template.yaml b/cft/pkg/tmpl/test-template.yaml index 7271223a..0ed59a1f 100644 --- a/cft/pkg/tmpl/test-template.yaml +++ b/cft/pkg/tmpl/test-template.yaml @@ -22,12 +22,9 @@ Resources: DependsOn: SecondResourceInOriginal UpdateReplacePolicy: Delete Properties: - LogBucketName: ezbeard-cep-test-module-log-bucket BucketName: ezbeard-cep-test-module-bucket - RetentionPolicy: !Ref BucketRetentionPolicy VersioningConfiguration: Status: Enabled - ConditionName: ConditionA Tags: - Key: test-tag Value: test-value2 diff --git a/docs/README.tmpl b/docs/README.tmpl index 348d2e3c..af40f2e1 100644 --- a/docs/README.tmpl +++ b/docs/README.tmpl @@ -37,6 +37,8 @@ cfn-lint, Guard and more: * **Modules** (EXPERIMENTAL): `rain pkg` supports client-side module development with the `!Rain::Module` directive. Rain modules are partial templates that are inserted into the parent template, with some extra functionality added to enable extending existing resource types. This feature integrates with CodeArtifact to enable package publish and install. +* **Content Deployment** (EXPERIMENTAL): `rain deploy` and `rain rm` support metadata commands that can upload static assets to a bucket and then delete those assets when the bucket is deleted. Rain can also run build scripts before and after stack deployment to prepare content like web sites and lambda functions before uploading to S3. + _Note that in order to use experimental commands, you have to add `--experimental` or `-x` as an argument._ ## Getting started diff --git a/docs/bash_completion.sh b/docs/bash_completion.sh index 5027aa55..502e4a9d 100644 --- a/docs/bash_completion.sh +++ b/docs/bash_completion.sh @@ -1710,6 +1710,8 @@ _rain_rm() flags+=("-d") local_nonpersistent_flags+=("--detach") local_nonpersistent_flags+=("-d") + flags+=("--experimental") + local_nonpersistent_flags+=("--experimental") flags+=("--help") flags+=("-h") local_nonpersistent_flags+=("--help") diff --git a/docs/index.md b/docs/index.md index ed82966d..6d03247b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,4 +36,4 @@ Rain is a command line tool for working with AWS CloudFormation templates and st * [rain tree](rain_tree.md) - Find dependencies of Resources and Outputs in a local template * [rain watch](rain_watch.md) - Display an updating view of a CloudFormation stack -###### Auto generated by spf13/cobra on 21-Aug-2024 +###### Auto generated by spf13/cobra on 2-Oct-2024 diff --git a/docs/rain_bootstrap.md b/docs/rain_bootstrap.md index dee4cb97..cb4c89f8 100644 --- a/docs/rain_bootstrap.md +++ b/docs/rain_bootstrap.md @@ -30,4 +30,4 @@ rain bootstrap * [rain](index.md) - -###### Auto generated by spf13/cobra on 21-Aug-2024 +###### Auto generated by spf13/cobra on 2-Oct-2024 diff --git a/docs/rain_build.md b/docs/rain_build.md index 50929baa..05230b7f 100644 --- a/docs/rain_build.md +++ b/docs/rain_build.md @@ -41,4 +41,4 @@ rain build [] or * [rain](index.md) - -###### Auto generated by spf13/cobra on 21-Aug-2024 +###### Auto generated by spf13/cobra on 2-Oct-2024 diff --git a/docs/rain_cat.md b/docs/rain_cat.md index 08c437aa..b7137c67 100644 --- a/docs/rain_cat.md +++ b/docs/rain_cat.md @@ -35,4 +35,4 @@ rain cat * [rain](index.md) - -###### Auto generated by spf13/cobra on 21-Aug-2024 +###### Auto generated by spf13/cobra on 2-Oct-2024 diff --git a/docs/rain_cc.md b/docs/rain_cc.md index 3d1ff904..e362fde2 100644 --- a/docs/rain_cc.md +++ b/docs/rain_cc.md @@ -32,4 +32,4 @@ You must pass the --experimental (-x) flag to use this command, to acknowledge t * [rain cc rm](rain_cc_rm.md) - Delete a deployment created by cc deploy (Experimental!) * [rain cc state](rain_cc_state.md) - Download the state file for a template deployed with cc deploy -###### Auto generated by spf13/cobra on 21-Aug-2024 +###### Auto generated by spf13/cobra on 2-Oct-2024 diff --git a/docs/rain_cc_deploy.md b/docs/rain_cc_deploy.md index a4b5bdc4..ef755ed9 100644 --- a/docs/rain_cc_deploy.md +++ b/docs/rain_cc_deploy.md @@ -40,4 +40,4 @@ rain cc deploy