//go:build func_test package cfn import ( "errors" "fmt" "time" "github.com/google/uuid" "gopkg.in/yaml.v3" "github.com/aws-cloudformation/rain/cft" "github.com/aws-cloudformation/rain/cft/format" "github.com/aws-cloudformation/rain/internal/aws" "github.com/aws-cloudformation/rain/internal/dc" "github.com/aws/aws-sdk-go-v2/service/cloudformation" "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" "github.com/aws/smithy-go/ptr" ) const WAIT_PERIOD_IN_SECONDS = 2 type mockStack struct { name string template cft.Template stack types.Stack resources []types.StackResource } type mockChangeSet struct { template cft.Template params []types.Parameter tags map[string]string stackName string roleArn string } type regionConfig struct { stacks map[string]*mockStack changeSets map[string]*mockChangeSet } var regions = map[string]regionConfig{ "mock-region-1": { stacks: make(map[string]*mockStack), changeSets: make(map[string]*mockChangeSet), }, "mock-region-2": { stacks: make(map[string]*mockStack), changeSets: make(map[string]*mockChangeSet), }, "mock-region-3": { stacks: make(map[string]*mockStack), changeSets: make(map[string]*mockChangeSet), }, } func region() regionConfig { return regions[aws.Config().Region] } var errNoStack = errors.New("no such mock stack") var errNoChangeSet = errors.New("no such mock change set") var errWrongChangeSet = errors.New("mock change set does not match stack name") var now = time.Date(2010, time.September, 9, 0, 0, 0, 0, time.UTC) // GetStackTemplate returns the template used to launch the named stack func GetStackTemplate(stackName string, processed bool) (string, error) { if s, ok := region().stacks[stackName]; ok { return format.String(s.template, format.Options{}), nil } return "", errNoStack } // StackExists checks whether the named stack currently exists func StackExists(stackName string) (bool, error) { _, ok := region().stacks[stackName] return ok, nil } // ListStacks returns a list of all existing stacks func ListStacks() ([]types.StackSummary, error) { out := make([]types.StackSummary, 0) for _, s := range region().stacks { if s.stack.StackStatus != types.StackStatusCreateFailed && s.stack.StackStatus != types.StackStatusDeleteComplete { out = append(out, types.StackSummary{ CreationTime: s.stack.CreationTime, StackName: s.stack.StackName, StackStatus: s.stack.StackStatus, DeletionTime: s.stack.DeletionTime, LastUpdatedTime: s.stack.LastUpdatedTime, ParentId: s.stack.ParentId, RootId: s.stack.RootId, StackId: s.stack.StackId, StackStatusReason: s.stack.StackStatusReason, TemplateDescription: s.stack.Description, }) } } return out, nil } // DeleteStack deletes a stack func DeleteStack(stackName string, roleArn string) error { if s, ok := region().stacks[stackName]; ok { s.stack.StackStatus = types.StackStatusDeleteComplete return nil } return errNoStack } // SetTerminationProtection enables or disables termination protection for a stack func SetTerminationProtection(stackName string, protectionEnabled bool) error { if s, ok := region().stacks[stackName]; ok { s.stack.EnableTerminationProtection = ptr.Bool(true) return nil } return errNoStack } // GetStack returns a cloudformation.Stack representing the named stack func GetStack(stackName string) (types.Stack, error) { if s, ok := region().stacks[stackName]; ok { return s.stack, nil } return types.Stack{}, errNoStack } // GetStackResources returns a list of the resources in the named stack func GetStackResources(stackName string) ([]types.StackResource, error) { if s, ok := region().stacks[stackName]; ok { return s.resources, nil } return nil, errNoStack } func GetStackResource(stackName string, logicalId string) (*types.StackResourceDetail, error) { // TODO return nil, nil } // GetStackEvents returns all events associated with the named stack func GetStackEvents(stackName string) ([]types.StackEvent, error) { return []types.StackEvent{ { EventId: ptr.String("mock event id"), StackId: ptr.String(stackName), StackName: ptr.String(stackName), Timestamp: &now, ClientRequestToken: ptr.String("mock event token"), LogicalResourceId: ptr.String("MockResourceId"), PhysicalResourceId: ptr.String("MockPhysicalId"), ResourceProperties: ptr.String("mock resource properties"), ResourceStatus: types.ResourceStatusCreateInProgress, ResourceStatusReason: ptr.String("mock status reason"), ResourceType: ptr.String("Mock::Resource::Type"), }, }, nil } // CreateChangeSet creates a changeset func CreateChangeSet(template cft.Template, params []types.Parameter, tags map[string]string, stackName string, roleArn string) (string, error) { name := uuid.New().String() region().changeSets[name] = &mockChangeSet{ template: template, params: params, tags: tags, stackName: stackName, roleArn: roleArn, } if stackName == "emptychangeset" { //lint:ignore ST1005 we want to create errors with upper case and punctuation for mock return name, fmt.Errorf("No updates are to be performed.") } return name, nil } // GetChangeSet returns the named changeset func GetChangeSet(stackName, changeSetName string) (*cloudformation.DescribeChangeSetOutput, error) { c, ok := region().changeSets[changeSetName] if !ok { return nil, errNoChangeSet } if c.stackName != stackName { return nil, fmt.Errorf("mock change set's stack name is not '%s'", stackName) } return &cloudformation.DescribeChangeSetOutput{ Capabilities: []types.Capability{}, ChangeSetId: ptr.String(changeSetName), ChangeSetName: ptr.String(changeSetName), Changes: []types.Change{ { ResourceChange: &types.ResourceChange{ Action: types.ChangeActionAdd, Details: []types.ResourceChangeDetail{ { CausingEntity: ptr.String("mock entity"), ChangeSource: types.ChangeSourceResourceAttribute, Evaluation: types.EvaluationTypeDynamic, Target: &types.ResourceTargetDefinition{ Attribute: types.ResourceAttributeProperties, Name: ptr.String("mock attribute"), RequiresRecreation: types.RequiresRecreationNever, }, }, }, LogicalResourceId: ptr.String("MockResourceId"), PhysicalResourceId: ptr.String("MockPhysicalId"), Replacement: types.ReplacementFalse, ResourceType: ptr.String("Mock::Resource::Type"), Scope: []types.ResourceAttribute{}, }, Type: types.ChangeTypeResource, }, }, CreationTime: &now, Description: ptr.String("Mock change set"), ExecutionStatus: types.ExecutionStatusAvailable, NextToken: nil, NotificationARNs: []string{}, Parameters: c.params, RollbackConfiguration: &types.RollbackConfiguration{}, StackId: ptr.String(stackName), StackName: ptr.String(stackName), Status: types.ChangeSetStatusCreateComplete, StatusReason: ptr.String("Mock status reason"), Tags: dc.MakeTags(c.tags), }, nil } // ExecuteChangeSet executes the named changeset func ExecuteChangeSet(stackName, changeSetName string, disableRollback bool) error { c, ok := region().changeSets[changeSetName] if !ok { return errNoChangeSet } if c.stackName != stackName { return errWrongChangeSet } s, ok := region().stacks[stackName] if !ok { s = &mockStack{ name: stackName, template: c.template, } region().stacks[stackName] = s } s.stack = types.Stack{ CreationTime: &now, StackName: ptr.String(stackName), StackStatus: types.StackStatusCreateComplete, Capabilities: []types.Capability{}, ChangeSetId: ptr.String(changeSetName), Description: ptr.String("Mock stack description"), DisableRollback: ptr.Bool(false), EnableTerminationProtection: ptr.Bool(false), Outputs: []types.Output{ { Description: ptr.String("Mock output description"), ExportName: ptr.String("MockExport"), OutputKey: ptr.String("MockKey"), OutputValue: ptr.String("Mock value"), }, }, Parameters: []types.Parameter{ { ParameterKey: ptr.String("MockKey"), ParameterValue: ptr.String("Mock value"), }, }, StackId: ptr.String(stackName), StackStatusReason: ptr.String("Mock status reason"), Tags: dc.MakeTags(c.tags), } s.resources = []types.StackResource{ { LogicalResourceId: ptr.String("MockResourceId"), ResourceStatus: types.ResourceStatusCreateComplete, ResourceType: ptr.String("Mock::Resource::Type"), Timestamp: &now, Description: ptr.String("Mock resource description"), PhysicalResourceId: ptr.String("MockPhysicalId"), ResourceStatusReason: ptr.String("Mock status reason"), StackId: ptr.String(stackName), StackName: ptr.String(stackName), }, } return nil } // DeleteChangeSet deletes the named changeset func DeleteChangeSet(stackName, changeSetName string) error { c, ok := region().changeSets[changeSetName] if !ok { return errNoChangeSet } if c.stackName != stackName { return errWrongChangeSet } delete(region().changeSets, changeSetName) return nil } // WaitUntilStackExists pauses execution until the named stack exists func WaitUntilStackExists(stackName string) error { if _, ok := region().stacks[stackName]; !ok { return errNoStack } return nil } // WaitUntilStackCreateComplete pauses execution until the stack is completed (or fails) func WaitUntilStackCreateComplete(stackName string) error { if _, ok := region().stacks[stackName]; !ok { return errNoStack } return nil } func ResourceAlreadyExists( typeName string, resource *yaml.Node, stackExists bool, template *yaml.Node, dc *dc.DeployConfig) bool { return true } func GetTypeSchema(name string) (string, error) { return "", nil } func GetTypePermissions(name string, handlerVerb string) ([]string, error) { return make([]string, 0), nil } func GetTypeIdentifier(name string) ([]string, error) { return make([]string, 0), nil } func GetPrimaryIdentifierValues(primaryIdentifier []string, resource map[string]interface{}) []string { return make([]string, 0) } // TODO - Fill out the mocks for stacksets func GetStackSet(stackSetName string) (*types.StackSet, error) { return nil, nil } func ListStackSetInstances(stackSetName string) ([]types.StackInstanceSummary, error) { return nil, nil } func CreateStackSet(conf StackSetConfig) (*string, error) { return nil, nil } func UpdateStackSet(conf StackSetConfig, instanceConf StackSetInstancesConfig, wait bool) error { return nil } func ListLast10StackSetOperations(stackSetName string) ([]types.StackSetOperationSummary, error) { return nil, nil } func DeleteStackSet(stackSetName string) error { return nil } func DeleteAllStackSetInstances(stackSetName string, wait bool, retainStacks bool) error { return nil } func CreateStackSetInstances(conf StackSetInstancesConfig, wait bool) error { return nil } func AddStackSetInstances(conf StackSetConfig, instanceConf StackSetInstancesConfig, wait bool) error { return nil } func ListStackSets() ([]types.StackSetSummary, error) { return nil, nil } func GetStackSetOperationsResult(stackSetName *string, operationId *string) (*types.StackSetOperationResultSummary, error) { return nil, nil } func DeleteStackSetInstances(stackSetName string, accounts []string, regions []string, wait bool, retainStacks bool) error { return nil }