Skip to content

Commit

Permalink
Merge pull request #511 from ericzbeard/webapp
Browse files Browse the repository at this point in the history
Asset deployment and a sample web application
  • Loading branch information
ericzbeard authored Oct 2, 2024
2 parents ea98e43 + 563fef2 commit 4398aa0
Show file tree
Hide file tree
Showing 130 changed files with 8,999 additions and 179 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 14 additions & 3 deletions cft/cft.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
10 changes: 4 additions & 6 deletions cft/format/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions cft/parse/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
61 changes: 45 additions & 16 deletions cft/pkg/directives.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")
}
}
}
Expand Down
Loading

0 comments on commit 4398aa0

Please sign in to comment.