// Package build contains functionality to generate a cft.Template // from specification data in cft.spec package build import ( "fmt" "strings" "github.com/aws-cloudformation/rain/cft" "github.com/aws-cloudformation/rain/cft/spec" ) const ( policyDocument = "PolicyDocument" assumeRolePolicyDocument = "AssumeRolePolicyDocument" optionalTag = "Optional" changeMeTag = "CHANGEME" ) type tracker struct { done map[string]bool last []string } func newTracker() *tracker { return &tracker{done: make(map[string]bool), last: make([]string, 0)} } func (t *tracker) push(s string) bool { if _, ok := t.done[s]; ok { return false } t.last = append(t.last, s) t.done[s] = true return true } func (t *tracker) pop() { delete(t.done, t.last[len(t.last)-1]) t.last = t.last[:len(t.last)-1] } // builder generates a template from its Spec type builder struct { Spec spec.Spec IncludeOptionalProperties bool BuildIamPolicies bool tracker *tracker } var iam iamBuilder var emptyProp = spec.Property{} func init() { iam = newIamBuilder() } func (b builder) newResource(resourceType string) (map[string]interface{}, []*cft.Comment) { defer func() { if r := recover(); r != nil { panic(fmt.Errorf("error building resource type '%s': %v", resourceType, r)) } }() rSpec, ok := b.Spec.ResourceTypes[resourceType] if !ok { panic(fmt.Errorf("no such resource type '%s'", resourceType)) } // fmt.Printf("%#v\n", rSpec) b.tracker = newTracker() // Generate properties properties := make(map[string]interface{}) comments := make([]*cft.Comment, 0) for name, pSpec := range rSpec.Properties { // fmt.Printf("Generating prop %v, pSpec: %#v \n", name, pSpec) if b.IncludeOptionalProperties || pSpec.Required { var p interface{} var cs []*cft.Comment if b.BuildIamPolicies && (name == policyDocument || name == assumeRolePolicyDocument) { p, cs = iam.Policy() } else { p, cs = b.newProperty(resourceType, name, pSpec) } properties[name] = p for _, c := range cs { c.Path = append([]interface{}{"Properties", name}, c.Path...) } comments = append(comments, cs...) } } resource := map[string]interface{}{ "Type": resourceType, "Properties": properties, } if len(properties) == 0 { delete(resource, "Properties") } return resource, comments } func (b builder) newProperty(resourceType, propertyName string, pSpec *spec.Property) (interface{}, []*cft.Comment) { if !b.tracker.push(fmt.Sprintf("%s/%s", resourceType, propertyName)) { return nil, nil } defer b.tracker.pop() defer func() { if r := recover(); r != nil { panic(fmt.Errorf("error building property %s.%s: %v", resourceType, propertyName, r)) } }() // Correct badly-formed entries if pSpec.PrimitiveType == spec.TypeMap { pSpec.PrimitiveType = spec.TypeEmpty pSpec.Type = spec.TypeMap } // Attempt to fix failures do to lack of types in some properties // TODO - This is a hack, figure out why they are missing /* Example from cfn.go "DialerConfig": &Property{ Documentation: "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-connectcampaigns-campaign.html#cfn-connectcampaigns-campaign-dialerconfig", Required: true, UpdateType: "Mutable", }, */ if pSpec.PrimitiveType == spec.TypeEmpty && pSpec.Type == spec.TypeEmpty { pSpec.PrimitiveType = "String" } // Primitive types if pSpec.PrimitiveType != spec.TypeEmpty { if pSpec.Required { return b.newPrimitive(pSpec.PrimitiveType), make([]*cft.Comment, 0) } return b.newPrimitive(pSpec.PrimitiveType), []*cft.Comment{{ Path: []interface{}{}, Value: optionalTag, }} } if pSpec.Type == spec.TypeList || pSpec.Type == spec.TypeMap { var value interface{} var subComments []*cft.Comment // Calculate a single item example if pSpec.PrimitiveItemType != spec.TypeEmpty { value = b.newPrimitive(pSpec.PrimitiveItemType) } else if pSpec.ItemType != spec.TypeEmpty { value, subComments = b.newPropertyType(resourceType, pSpec.ItemType) } else { value = changeMeTag } if pSpec.Type == spec.TypeList { // Returning a list - append a zero to comment paths for _, c := range subComments { c.Path = append([]interface{}{0}, c.Path...) } return []interface{}{value}, subComments } // Returning a map - append changemetag to comment paths for _, c := range subComments { c.Path = append([]interface{}{changeMeTag}, c.Path...) } return map[string]interface{}{changeMeTag: value}, subComments } // Fall through to property types b.tracker.pop() defer b.tracker.push(fmt.Sprintf("%s/%s", resourceType, propertyName)) output, comments := b.newPropertyType(resourceType, pSpec.Type) if !pSpec.Required { comments = append(comments, &cft.Comment{ Path: []interface{}{}, Value: optionalTag, }) } return output, comments } func (b builder) newPrimitive(primitiveType string) interface{} { switch primitiveType { case "String": return changeMeTag case "Integer": return 0 case "Double": return 0.0 case "Long": return 0.0 case "Boolean": return false case "Timestamp": return "1970-01-01 00:00:00" case "Json": return "{\"JSON\": \"CHANGEME\"}" default: panic(fmt.Errorf("unimplemented primitive type '%s'", primitiveType)) } } func (b builder) newPropertyType(resourceType, propertyType string) (interface{}, []*cft.Comment) { if !b.tracker.push(fmt.Sprintf("%s/%s", resourceType, propertyType)) { return nil, nil } defer b.tracker.pop() defer func() { if r := recover(); r != nil { panic(fmt.Errorf("error building property type '%s.%s': %v", resourceType, propertyType, r)) } }() var ptSpec *spec.PropertyType var ok bool // If we've used a property from another resource type // switch to that resource type for now parts := strings.Split(propertyType, ".") if len(parts) == 2 { resourceType = parts[0] } ptSpec, ok = b.Spec.PropertyTypes[propertyType] if !ok { ptSpec, ok = b.Spec.PropertyTypes[resourceType+"."+propertyType] } if !ok { // TODO - Why is this failing during tests? // fmt.Println("About to fail? on", propertyType) // propertyType is "" panic(fmt.Errorf("unimplemented property type '%s.%s'", resourceType, propertyType)) } // Deal with the case that a property type is directly a plain property // for example AWS::Glue::SecurityConfiguration.S3Encryptions if ptSpec.Property != emptyProp { return b.newProperty(resourceType, propertyType, &ptSpec.Property) } comments := make([]*cft.Comment, 0) // Generate properties properties := make(map[string]interface{}) for name, pSpec := range ptSpec.Properties { if b.IncludeOptionalProperties || pSpec.Required { if !pSpec.Required { comments = append(comments, &cft.Comment{ Path: []interface{}{name}, Value: optionalTag, }) } var p interface{} var cs []*cft.Comment if b.BuildIamPolicies && (name == policyDocument || name == assumeRolePolicyDocument) { p, cs = iam.Policy() } else if pSpec.Type == propertyType || pSpec.ItemType == propertyType { p = make(map[string]interface{}) cs = make([]*cft.Comment, 0) } else { p, cs = b.newProperty(resourceType, name, pSpec) } properties[name] = p for _, c := range cs { c.Path = append([]interface{}{name}, c.Path...) } comments = append(comments, cs...) } } return properties, comments }