Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions compat/aws/cloudformation_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func TestAWSCloudFormationCompat(t *testing.T) {
})

changeSetOps(ctx, sess, client)
exportOps(ctx, sess, client)

sess.Op(svc, "DeleteStack", func() error {
_, err := client.DeleteStack(ctx, &awscfn.DeleteStackInput{StackName: aws.String(stack)})
Expand Down Expand Up @@ -206,6 +207,100 @@ func changeSetOps(ctx context.Context, sess *compat.AWSSession, client *awscfn.C
})
}

// exportOps exports a bucket name from one stack, imports it into another,
// then protects the importer and checks the account operations.
func exportOps(ctx context.Context, sess *compat.AWSSession, client *awscfn.Client) {
const svc = "cloudformation"

sess.Op(svc, "ListExports", func() error {
if _, err := client.CreateStack(ctx, &awscfn.CreateStackInput{
StackName: aws.String("compat-exporter"), TemplateBody: aws.String(exporterTemplate),
}); err != nil {
return err
}

out, err := client.ListExports(ctx, &awscfn.ListExportsInput{})
if err != nil {
return err
}

if len(out.Exports) != 1 || aws.ToString(out.Exports[0].Value) != "compat-exported" {
return errCompat("expected 1 export")
}

return nil
})

sess.Op(svc, "ListImports", func() error {
if _, err := client.CreateStack(ctx, &awscfn.CreateStackInput{
StackName: aws.String("compat-importer"), TemplateBody: aws.String(importerTemplate),
}); err != nil {
return err
}

out, err := client.ListImports(ctx, &awscfn.ListImportsInput{ExportName: aws.String("compat-export")})
if err != nil {
return err
}

if len(out.Imports) != 1 || out.Imports[0] != "compat-importer" {
return errCompat("expected 1 import")
}

return nil
})

sess.Op(svc, "UpdateTerminationProtection", func() error {
_, err := client.UpdateTerminationProtection(ctx, &awscfn.UpdateTerminationProtectionInput{
StackName: aws.String("compat-importer"), EnableTerminationProtection: aws.Bool(true),
})
if err != nil {
return err
}

if _, err = client.DeleteStack(ctx, &awscfn.DeleteStackInput{StackName: aws.String("compat-importer")}); err == nil {
return errCompat("protected stack deleted")
}

return nil
})

sess.Op(svc, "DescribeAccountLimits", func() error {
out, err := client.DescribeAccountLimits(ctx, &awscfn.DescribeAccountLimitsInput{})
if err != nil {
return err
}

if len(out.AccountLimits) != 3 {
return errCompat("expected 3 limits")
}

return nil
})

sess.Op(svc, "EstimateTemplateCost", func() error {
out, err := client.EstimateTemplateCost(ctx, &awscfn.EstimateTemplateCostInput{TemplateBody: aws.String(exporterTemplate)})
if err != nil {
return err
}

if aws.ToString(out.Url) == "" {
return errCompat("empty url")
}

return nil
})
}

const exporterTemplate = `{
"Resources":{"Bucket":{"Type":"AWS::S3::Bucket","Properties":{"BucketName":"compat-exported"}}},
"Outputs":{"Name":{"Value":{"Ref":"Bucket"},"Export":{"Name":"compat-export"}}}
}`

const importerTemplate = `{"Resources":{"Bucket":{"Type":"AWS::S3::Bucket","Properties":{
"BucketName":{"Fn::Join":["-",[{"Fn::ImportValue":"compat-export"},"copy"]]}
}}}}`

const changeSetTemplate = `{"Resources":{"Bucket":{"Type":"AWS::S3::Bucket","Properties":{"BucketName":"compat-cs-bucket"}}}}`

// continueUpdateRollback drives a stack into UPDATE_ROLLBACK_FAILED, where
Expand Down
7 changes: 6 additions & 1 deletion docs/compat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,26 @@ Each service's summary breaks the verified counts out per SDK/language. Only lan
| CreateStack | ✅ |
| DeleteChangeSet | ✅ |
| DeleteStack | ✅ |
| DescribeAccountLimits | ✅ |
| DescribeChangeSet | ✅ |
| DescribeStackEvents | ✅ |
| DescribeStackResources | ✅ |
| DescribeStacks | ✅ |
| EstimateTemplateCost | ✅ |
| ExecuteChangeSet | ✅ |
| GetTemplate | ✅ |
| GetTemplateSummary | ✅ |
| ListChangeSets | ✅ |
| ListExports | ✅ |
| ListImports | ✅ |
| ListStackResources | ✅ |
| ListStacks | ✅ |
| UpdateStack | ✅ |
| UpdateTerminationProtection | ✅ |
| ValidateTemplate | ✅ |

**cloudformation verified per language:**
- Go: AWS 17/17
- Go: AWS 22/22

## compute

Expand Down
65 changes: 65 additions & 0 deletions docs/compat/compat.json
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,19 @@
},
"status": "green"
},
{
"service": "cloudformation",
"operation": "DescribeAccountLimits",
"providers": {
"aws": {
"native": "CloudFormation",
"clients": {
"sdkGo": "pass"
}
}
},
"status": "green"
},
{
"service": "cloudformation",
"operation": "DescribeChangeSet",
Expand Down Expand Up @@ -433,6 +446,19 @@
},
"status": "green"
},
{
"service": "cloudformation",
"operation": "EstimateTemplateCost",
"providers": {
"aws": {
"native": "CloudFormation",
"clients": {
"sdkGo": "pass"
}
}
},
"status": "green"
},
{
"service": "cloudformation",
"operation": "ExecuteChangeSet",
Expand Down Expand Up @@ -485,6 +511,32 @@
},
"status": "green"
},
{
"service": "cloudformation",
"operation": "ListExports",
"providers": {
"aws": {
"native": "CloudFormation",
"clients": {
"sdkGo": "pass"
}
}
},
"status": "green"
},
{
"service": "cloudformation",
"operation": "ListImports",
"providers": {
"aws": {
"native": "CloudFormation",
"clients": {
"sdkGo": "pass"
}
}
},
"status": "green"
},
{
"service": "cloudformation",
"operation": "ListStackResources",
Expand Down Expand Up @@ -524,6 +576,19 @@
},
"status": "green"
},
{
"service": "cloudformation",
"operation": "UpdateTerminationProtection",
"providers": {
"aws": {
"native": "CloudFormation",
"clients": {
"sdkGo": "pass"
}
}
},
"status": "green"
},
{
"service": "cloudformation",
"operation": "ValidateTemplate",
Expand Down
2 changes: 1 addition & 1 deletion docs/coverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ code does not implement. Machine-readable: [`coverage.json`](./coverage.json).
| `cloudasset` | - | - | [Cloudasset](./gcp/cloudasset.md) | - | 11 |
| `cloudbilling` | - | - | [Cloudbilling](./gcp/cloudbilling.md) | - | 14 |
| `clouddeploy` | - | - | [CloudDeploy](./gcp/clouddeploy.md) | - | 11 |
| `cloudformation` | [CloudFormation](./aws/cloudformation.md) | - | - | - | 17 |
| `cloudformation` | [CloudFormation](./aws/cloudformation.md) | - | - | - | 22 |
| `cloudfront` | [CloudFront](./aws/cloudfront.md) | - | - | - | 11 |
| `cloudids` | - | - | [CloudIDS](./gcp/cloudids.md) | - | 6 |
| `cloudrun` | - | - | [CloudRun](./gcp/cloudrun.md) | - | 18 |
Expand Down
2 changes: 1 addition & 1 deletion docs/coverage/aws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Services cloudemu emulates for AWS, by native name. Back to the [cross-provider
| [Bedrock](./bedrock.md) | `bedrock` | 65 |
| [BedrockAgent](./bedrockagent.md) | `bedrockagent` | 32 |
| [BedrockAgentRuntime](./bedrockagentruntime.md) | `bedrockagentruntime` | 3 |
| [CloudFormation](./cloudformation.md) | (provider-native) | 17 |
| [CloudFormation](./cloudformation.md) | (provider-native) | 22 |
| [CloudFront](./cloudfront.md) | `cloudfront` | 11 |
| [CloudTrail](./cloudtrail.md) | `cloudtrail` | 60 |
| [CloudWatch](./cloudwatch.md) | `monitoring` | 12 |
Expand Down
7 changes: 6 additions & 1 deletion docs/coverage/aws/cloudformation.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

provider-native `cloudformation` wire service (AWS-only) · no portable driver · [AWS index](./README.md)

## Operations (17)
## Operations (22)

| Operation | Description |
| --- | --- |
Expand All @@ -12,17 +12,22 @@ provider-native `cloudformation` wire service (AWS-only) · no portable driver
| `CreateStack` | |
| `DeleteChangeSet` | |
| `DeleteStack` | |
| `DescribeAccountLimits` | |
| `DescribeChangeSet` | |
| `DescribeStackEvents` | |
| `DescribeStackResources` | |
| `DescribeStacks` | |
| `EstimateTemplateCost` | |
| `ExecuteChangeSet` | |
| `GetTemplate` | |
| `GetTemplateSummary` | |
| `ListChangeSets` | |
| `ListExports` | |
| `ListImports` | |
| `ListStackResources` | |
| `ListStacks` | |
| `UpdateStack` | |
| `UpdateTerminationProtection` | |
| `ValidateTemplate` | |

## Not in scope
Expand Down
15 changes: 15 additions & 0 deletions docs/coverage/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -3149,6 +3149,9 @@
{
"name": "DeleteStack"
},
{
"name": "DescribeAccountLimits"
},
{
"name": "DescribeChangeSet"
},
Expand All @@ -3161,6 +3164,9 @@
{
"name": "DescribeStacks"
},
{
"name": "EstimateTemplateCost"
},
{
"name": "ExecuteChangeSet"
},
Expand All @@ -3173,6 +3179,12 @@
{
"name": "ListChangeSets"
},
{
"name": "ListExports"
},
{
"name": "ListImports"
},
{
"name": "ListStackResources"
},
Expand All @@ -3182,6 +3194,9 @@
{
"name": "UpdateStack"
},
{
"name": "UpdateTerminationProtection"
},
{
"name": "ValidateTemplate"
}
Expand Down
17 changes: 13 additions & 4 deletions providers/aws/cloudformation/changeset.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ func (m *Mock) planCreateChangeSet(ctx context.Context, in *cfn.CreateChangeSetI
return nil, nil, err
}

if lerr := m.checkStackLimit(); lerr != nil {
return nil, nil, lerr
}

sd = &stackData{
resolved: map[string]cfn.ResolvedResource{},
deleteIDs: map[string]string{},
Expand Down Expand Up @@ -604,7 +608,10 @@ func (m *Mock) ExecuteChangeSet(ctx context.Context, in *cfn.ExecuteChangeSetInp
return err
}

run := execution{rec: rec, onFailure: onFailure, token: in.ClientRequestToken}
run := execution{
rec: rec, onFailure: onFailure, token: in.ClientRequestToken,
retainExceptOnCreate: in.RetainExceptOnCreate != nil && *in.RetainExceptOnCreate,
}

if rec.ChangeSet.Type == cfn.ChangeSetTypeCreate {
return m.executeCreate(ctx, sd, &run)
Expand All @@ -615,9 +622,10 @@ func (m *Mock) ExecuteChangeSet(ctx context.Context, in *cfn.ExecuteChangeSetInp

// execution is one ExecuteChangeSet call.
type execution struct {
rec *changeSetRecord
onFailure string
token string
rec *changeSetRecord
onFailure string
token string
retainExceptOnCreate bool
}

// executable rejects a change set that cannot run. The caller holds the
Expand Down Expand Up @@ -741,6 +749,7 @@ func (m *Mock) beginExecute(

sd.stack.ChangeSetID = rec.ChangeSet.ID
sd.stack.DisableRollback = run.onFailure == cfn.OnStackFailureDoNothing
sd.stack.RetainExceptOnCreate = run.retainExceptOnCreate
sd.stack.Status = status
sd.stack.StatusReason = reasonUserInitiated
sd.stack.Events = append(sd.stack.Events,
Expand Down
4 changes: 2 additions & 2 deletions providers/aws/cloudformation/changeset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ func TestDoNothingKeepsReplacedResources(t *testing.T) {

restored := newParamMock(p)
requireNoError(t, restored.Restore(ctx, json.RawMessage(data)))
requireNoError(t, restored.DeleteStack(ctx, "s"))
requireNoError(t, restored.DeleteStack(ctx, &cfn.DeleteStackInput{StackName: "s"}))

for _, name := range []string{"/a", "/a2", "/b"} {
if _, ok := p.values[name]; ok {
Expand Down Expand Up @@ -686,7 +686,7 @@ func TestDeleteChangeSet(t *testing.T) {
assertEqual(t, again.StackID, st.ID, "same review stack")

// DeleteStack removes the review stack and its change sets.
requireNoError(t, m.DeleteStack(ctx, "s"))
requireNoError(t, m.DeleteStack(ctx, &cfn.DeleteStackInput{StackName: "s"}))

_, err = m.DescribeChangeSet(ctx, &cfn.DescribeChangeSetInput{ChangeSetName: again.ID})
assertException(t, err, cfn.ExceptionChangeSetNotFound, "")
Expand Down
Loading
Loading