Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

Commit

Permalink
feat: Specify stack tags in the CloudFormationStack object
Browse files Browse the repository at this point in the history
  • Loading branch information
clareliguori committed Apr 11, 2023
1 parent b656ab2 commit e71683f
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 0 deletions.
15 changes: 15 additions & 0 deletions api/v1alpha1/cloudformation_stack_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ type CloudFormationStackSpec struct {
// +optional
StackParameters []StackParameter `json:"stackParameters,omitempty"`

// The tag keys and values to set on the stack
// +optional
StackTags []StackTag `json:"stackTags,omitempty"`

// Suspend tells the controller to suspend reconciliation for this CloudFormation stack,
// it does not apply to already started reconciliations. Defaults to false.
// +optional
Expand Down Expand Up @@ -91,6 +95,17 @@ type StackParameter struct {
Value string `json:"value"`
}

// Key and value for a CloudFormation stack tag.
type StackTag struct {
// Name of the stack tag.
// +required
Key string `json:"key"`

// Value of the stack tag.
// +required
Value string `json:"value"`
}

// Reference to a Flux source object.
type SourceReference struct {
// API version of the source object.
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,22 @@ spec:
- value
type: object
type: array
stackTags:
description: The tag keys and values to set on the stack
items:
description: Key and value for a CloudFormation stack tag.
properties:
key:
description: Name of the stack tag.
type: string
value:
description: Value of the stack tag.
type: string
required:
- key
- value
type: object
type: array
suspend:
default: false
description: Suspend tells the controller to suspend reconciliation
Expand Down
71 changes: 71 additions & 0 deletions docs/api/cloudformationstack.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,20 @@ value to retry failures.</p>
</tr>
<tr>
<td>
<code>stackTags</code><br>
<em>
<a href="#cloudformation.contrib.fluxcd.io/v1alpha1.StackTag">
[]StackTag
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>The tag keys and values to set on the stack</p>
</td>
</tr>
<tr>
<td>
<code>suspend</code><br>
<em>
bool
Expand Down Expand Up @@ -325,6 +339,20 @@ value to retry failures.</p>
</tr>
<tr>
<td>
<code>stackTags</code><br>
<em>
<a href="#cloudformation.contrib.fluxcd.io/v1alpha1.StackTag">
[]StackTag
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>The tag keys and values to set on the stack</p>
</td>
</tr>
<tr>
<td>
<code>suspend</code><br>
<em>
bool
Expand Down Expand Up @@ -662,6 +690,49 @@ string
</table>
</div>
</div>
<h3 id="cloudformation.contrib.fluxcd.io/v1alpha1.StackTag">StackTag
</h3>
<p>
(<em>Appears on:</em>
<a href="#cloudformation.contrib.fluxcd.io/v1alpha1.CloudFormationStackSpec">CloudFormationStackSpec</a>)
</p>
<p>Key and value for a CloudFormation stack tag.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>key</code><br>
<em>
string
</em>
</td>
<td>
<p>Name of the stack tag.</p>
</td>
</tr>
<tr>
<td>
<code>value</code><br>
<em>
string
</em>
</td>
<td>
<p>Value of the stack tag.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="admonition note">
<p class="last">This page was automatically generated with <code>gen-crd-api-reference-docs</code></p>
</div>
11 changes: 11 additions & 0 deletions internal/controllers/cloudformationstack_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,17 @@ func (r *CloudFormationStackReconciler) reconcileStack(ctx context.Context, cfnS
clientStack.StackConfig.Parameters = params
}

if len(cfnStack.Spec.StackTags) > 0 {
var tags []sdktypes.Tag
for _, tag := range cfnStack.Spec.StackTags {
tags = append(tags, sdktypes.Tag{
Key: aws.String(tag.Key),
Value: aws.String(tag.Value),
})
}
clientStack.StackConfig.Tags = tags
}

// Check if we need to generate a new change set or describe the current one
desiredChangeSetName := cloudformation.GetChangeSetName(cfnStack.Generation, revision)
lastAttemptedChangeSetName := cloudformation.ExtractChangeSetName(cfnStack.Status.LastAttemptedChangeSet)
Expand Down
135 changes: 135 additions & 0 deletions internal/controllers/cloudformationstack_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,141 @@ func TestCfnController_ReconcileStack(t *testing.T) {
cfnClient.EXPECT().UpdateStack(expectedUpdateStackIn).Return(mockChangeSetArnNewGeneration, nil)
},
},
"create stack with tags": {
wantedEvents: []*expectedEvent{{
eventType: "Normal",
severity: "info",
message: fmt.Sprintf("Creation of stack 'mock-real-stack' in progress (change set %s)", mockChangeSetArn),
}},
wantedRequeueDelay: mockPollIntervalDuration,
wantedStackStatus: &cfnv1.CloudFormationStackStatus{
ObservedGeneration: mockGenerationId,
StackName: mockRealStackName,
LastAttemptedRevision: mockSourceRevision,
LastAttemptedChangeSet: mockChangeSetArn,
Conditions: []metav1.Condition{
{
Type: "Ready",
Status: "Unknown",
ObservedGeneration: mockGenerationId,
Reason: "Progressing",
Message: fmt.Sprintf("Creation of stack 'mock-real-stack' in progress (change set %s)", mockChangeSetArn),
},
},
},
markStackAsInProgress: true,
fillInSource: generateMockGitRepoSource,
mockS3ClientCalls: mockS3ClientUpload,
fillInInitialCfnStack: func(cfnStack *cfnv1.CloudFormationStack) {
cfnStack.Name = mockStackName
cfnStack.Namespace = mockNamespace
cfnStack.Generation = mockGenerationId
cfnStack.Spec = generateMockCfnStackSpec()
cfnStack.Spec.StackTags = []cfnv1.StackTag{
{
Key: "TagKey",
Value: "TagValue",
},
}
},
mockCfnClientCalls: func(cfnClient *clientmocks.MockCloudFormationClient) {
expectedDescribeStackIn := generateStackInput(mockGenerationId, mockSourceRevision, "")
expectedDescribeStackIn.Tags = []sdktypes.Tag{
{
Key: aws.String("TagKey"),
Value: aws.String("TagValue"),
},
}
cfnClient.EXPECT().DescribeStack(expectedDescribeStackIn).Return(nil, &cloudformation.ErrStackNotFound{})

expectedDescribeChangeSetIn := generateStackInput(mockGenerationId, mockSourceRevision, "")
expectedDescribeChangeSetIn.Tags = expectedDescribeStackIn.Tags
cfnClient.EXPECT().DescribeChangeSet(expectedDescribeChangeSetIn).Return(nil, &cloudformation.ErrChangeSetNotFound{})

expectedCreateStackIn := generateStackInputWithTemplateUrl(mockGenerationId, mockSourceRevision)
expectedCreateStackIn.Tags = expectedDescribeStackIn.Tags
cfnClient.EXPECT().CreateStack(expectedCreateStackIn).Return(mockChangeSetArn, nil)
},
},
"update stack with tags": {
wantedEvents: []*expectedEvent{{
eventType: "Normal",
severity: "info",
message: fmt.Sprintf("Update of stack 'mock-real-stack' in progress (change set %s)", mockChangeSetArnNewGeneration),
}},
wantedRequeueDelay: mockPollIntervalDuration,
wantedStackStatus: &cfnv1.CloudFormationStackStatus{
ObservedGeneration: mockGenerationId2,
StackName: mockRealStackName,
LastAttemptedRevision: mockSourceRevision,
LastAppliedRevision: mockSourceRevision,
LastAttemptedChangeSet: mockChangeSetArnNewGeneration,
LastAppliedChangeSet: mockChangeSetArn,
Conditions: []metav1.Condition{
{
Type: "Ready",
Status: "Unknown",
ObservedGeneration: mockGenerationId2,
Reason: "Progressing",
Message: fmt.Sprintf("Update of stack 'mock-real-stack' in progress (change set %s)", mockChangeSetArnNewGeneration),
},
},
},
markStackAsInProgress: true,
fillInSource: generateMockGitRepoSource,
mockS3ClientCalls: mockS3ClientUpload,
fillInInitialCfnStack: func(cfnStack *cfnv1.CloudFormationStack) {
cfnStack.Name = mockStackName
cfnStack.Namespace = mockNamespace
cfnStack.Generation = mockGenerationId2
cfnStack.Spec = generateMockCfnStackSpec()
cfnStack.Spec.StackTags = []cfnv1.StackTag{
{
Key: "TagKey",
Value: "TagValue",
},
}
cfnStack.Status = cfnv1.CloudFormationStackStatus{
ObservedGeneration: mockGenerationId,
StackName: mockRealStackName,
LastAttemptedRevision: mockSourceRevision,
LastAppliedRevision: mockSourceRevision,
LastAttemptedChangeSet: mockChangeSetArn,
LastAppliedChangeSet: mockChangeSetArn,
Conditions: []metav1.Condition{
{
Type: "Ready",
Status: "True",
ObservedGeneration: mockGenerationId,
Reason: "Hello",
Message: "World",
},
},
}
},
mockCfnClientCalls: func(cfnClient *clientmocks.MockCloudFormationClient) {
expectedDescribeStackIn := generateStackInput(mockGenerationId2, mockSourceRevision, "")
expectedDescribeStackIn.Tags = []sdktypes.Tag{
{
Key: aws.String("TagKey"),
Value: aws.String("TagValue"),
},
}
cfnClient.EXPECT().DescribeStack(expectedDescribeStackIn).Return(&clienttypes.StackDescription{
StackName: aws.String(mockRealStackName),
StackStatus: sdktypes.StackStatusCreateComplete,
StackStatusReason: aws.String("hello world"),
}, nil)

expectedDescribeChangeSetIn := generateStackInput(mockGenerationId2, mockSourceRevision, "")
expectedDescribeChangeSetIn.Tags = expectedDescribeStackIn.Tags
cfnClient.EXPECT().DescribeChangeSet(expectedDescribeChangeSetIn).Return(nil, &cloudformation.ErrChangeSetNotFound{})

expectedUpdateStackIn := generateStackInputWithTemplateUrl(mockGenerationId2, mockSourceRevision)
expectedUpdateStackIn.Tags = expectedDescribeStackIn.Tags
cfnClient.EXPECT().UpdateStack(expectedUpdateStackIn).Return(mockChangeSetArnNewGeneration, nil)
},
},
"continue stack rollback if the real stack has UPDATE_ROLLBACK_FAILED status": {
wantedEvents: []*expectedEvent{{
eventType: "Warning",
Expand Down
53 changes: 53 additions & 0 deletions internal/integtests/features/cfn_controller.feature
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,59 @@ Feature: CloudFormation controller for Flux
And the CloudFormationStack's Ready condition should eventually have "True" status
And the CloudFormation stack in my AWS account should be in "UPDATE_COMPLETE" state

Scenario: Update an existing CloudFormation stack by changing the stack tags
Given I push a valid CloudFormation template with parameters to my git repository
And I trigger Flux to reconcile my git repository
And I apply the following CloudFormationStack configuration to my Kubernetes cluster
"""
apiVersion: cloudformation.contrib.fluxcd.io/v1alpha1
kind: CloudFormationStack
metadata:
name: {stack_object_name}
namespace: flux-system
spec:
stackName: {stack_name}
templatePath: {template_path}
sourceRef:
kind: GitRepository
name: my-cfn-templates-repo
interval: 1h
retryInterval: 5s
stackTags:
- key: Tag1
value: Hello
- key: Tag2
value: world
"""
And the CloudFormationStack's Ready condition should eventually have "True" status
And the CloudFormation stack in my AWS account should be in "CREATE_COMPLETE" state

When I apply the following CloudFormationStack configuration to my Kubernetes cluster
"""
apiVersion: cloudformation.contrib.fluxcd.io/v1alpha1
kind: CloudFormationStack
metadata:
name: {stack_object_name}
namespace: flux-system
spec:
stackName: {stack_name}
templatePath: {template_path}
sourceRef:
kind: GitRepository
name: my-cfn-templates-repo
interval: 1h
retryInterval: 5s
stackTags:
- key: Tag1
value: Hi
- key: Tag2
value: everyone
"""

Then the CloudFormationStack's Ready condition should eventually have "Unknown" status
And the CloudFormationStack's Ready condition should eventually have "True" status
And the CloudFormation stack in my AWS account should be in "UPDATE_COMPLETE" state

Scenario: Update an existing CloudFormation stack by pushing a template file change
Given I push a valid CloudFormation template to my git repository
And I trigger Flux to reconcile my git repository
Expand Down

0 comments on commit e71683f

Please sign in to comment.