├── .dockerignore ├── .github ├── cloudformation │ ├── integ-test-authentication.yaml │ └── publisher-authentication.yaml ├── dependabot.yml └── workflows │ ├── integ-tests-pr.yml │ ├── integ-tests.yml │ ├── lint-pr.yml │ ├── release.yml │ └── unit-tests.yml ├── .gitignore ├── .mergify.yml ├── .versionrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── THIRD-PARTY-LICENSES.txt ├── VERSION ├── api ├── go.mod ├── go.sum └── v1alpha1 │ ├── cloudformation_stack_types.go │ ├── doc.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go ├── config ├── crd │ ├── bases │ │ └── cloudformation.contrib.fluxcd.io_cloudformationstacks.yaml │ └── kustomization.yaml ├── default │ └── kustomization.yaml ├── manager │ ├── deployment.yaml │ ├── kustomization.yaml │ └── overlays │ │ ├── aws-creds-from-env-vars │ │ ├── env-patch.yaml │ │ └── kustomization.yaml │ │ ├── aws-creds-from-mounted-file │ │ ├── env-patch.yaml │ │ └── kustomization.yaml │ │ ├── base │ │ ├── env-patch.yaml │ │ └── kustomization.yaml │ │ └── dev │ │ └── kustomization.yaml └── rbac │ ├── cfnstack_editor_role.yaml │ ├── cfnstack_viewer_role.yaml │ ├── cluster_role.yaml │ ├── cluster_role_binding.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── overlays │ └── eks-irsa │ │ ├── eks-irsa-patch.yaml │ │ └── kustomization.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── ct.yaml ├── demo.sh ├── docs ├── api │ └── cloudformationstack.md ├── demo.gif ├── design.md ├── developing.md ├── diagrams │ ├── data-flow-create.png │ ├── data-flow-delete.png │ ├── data-flow-update.png │ ├── data-flow.drawio │ ├── reconciliation-loop-deletion.png │ ├── reconciliation-loop.drawio │ └── reconciliation-loop.png └── install.md ├── examples ├── my-cloudformation-templates │ ├── another-template.yaml │ ├── template-with-parameters.yaml │ ├── template.yaml │ └── yet-another-template.yaml ├── my-flux-configuration │ ├── my-cloudformation-stack.yaml │ ├── my-cloudformation-templates-repo.yaml │ ├── my-other-cloudformation-stack.yaml │ └── yet-another-cloudformation-stack.yaml └── resources.yaml ├── go.mod ├── go.sum ├── hack ├── api-docs │ ├── config.json │ └── template │ │ ├── members.tpl │ │ ├── pkg.tpl │ │ └── type.tpl └── boilerplate.go.txt ├── internal ├── clients │ ├── clients.go │ ├── cloudformation │ │ ├── changeset.go │ │ ├── cloudformation.go │ │ ├── cloudformation_test.go │ │ ├── errors.go │ │ ├── mocks │ │ │ └── mock_sdk.go │ │ └── sdk_interfaces.go │ ├── mocks │ │ └── mock_clients.go │ ├── s3 │ │ ├── mocks │ │ │ └── mock_sdk.go │ │ ├── s3.go │ │ ├── s3_test.go │ │ └── sdk_interfaces.go │ └── types │ │ ├── changeset.go │ │ └── stack.go ├── controllers │ ├── cloudformationstack_controller.go │ ├── cloudformationstack_controller_test.go │ ├── source_predicate.go │ └── util.go ├── integtests │ ├── cfn_controller_integ_test.go │ ├── cfn_controller_test_scenario.go │ ├── cfn_controller_test_suite.go │ ├── command_runner.go │ ├── features │ │ └── cfn_controller.feature │ └── git.go └── mocks │ ├── mock_event_recorder.go │ └── mock_kubernetes_client.go ├── lintconf.yaml ├── local-dev ├── bootstrap-local-kind-cluster.sh ├── kind-cluster-config.yaml └── local-flux-dev-config.patch └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries and configuration. 3 | bin/ 4 | testbin/ 5 | testdata/ 6 | internal/integtests/ 7 | cover.out 8 | *.yaml 9 | config/ 10 | Makefile 11 | PROJECT 12 | 13 | # Ignore git 14 | **/.git 15 | .gitignore 16 | 17 | # Ignore docs 18 | **/*.md 19 | docs/ 20 | examples/ 21 | demo.sh -------------------------------------------------------------------------------- /.github/cloudformation/integ-test-authentication.yaml: -------------------------------------------------------------------------------- 1 | # This CloudFormation template configures an IAM identity provider that uses GitHub's OIDC, 2 | # enabling GitHub Actions to run the controller integration tests against the AWS account 3 | # where this template is deployed. 4 | # aws cloudformation deploy \ 5 | # --template-file .github/cloudformation/integ-test-authentication.yaml \ 6 | # --stack-name github-integ-test-identity-provider \ 7 | # --parameter-overrides GitHubOrg=awslabs RepositoryName=aws-cloudformation-controller-for-flux \ 8 | # --capabilities CAPABILITY_IAM \ 9 | # --region us-west-2 \ 10 | # --profile flux-cfn-integ-test-account 11 | 12 | Parameters: 13 | GitHubOrg: 14 | Description: Name of GitHub organization/user (case sensitive) 15 | Type: String 16 | RepositoryName: 17 | Description: Name of GitHub repository (case sensitive) 18 | Type: String 19 | OIDCProviderArn: 20 | Description: Arn for the GitHub OIDC Provider. 21 | Default: "" 22 | Type: String 23 | OIDCAudience: 24 | Description: Audience supplied to configure-aws-credentials. 25 | Default: "sts.amazonaws.com" 26 | Type: String 27 | 28 | Conditions: 29 | CreateOIDCProvider: !Equals 30 | - !Ref OIDCProviderArn 31 | - "" 32 | 33 | Resources: 34 | Role: 35 | Type: AWS::IAM::Role 36 | Properties: 37 | AssumeRolePolicyDocument: 38 | Statement: 39 | - Effect: Allow 40 | Action: sts:AssumeRoleWithWebIdentity 41 | Principal: 42 | Federated: !If 43 | - CreateOIDCProvider 44 | - !Ref GithubOidc 45 | - !Ref OIDCProviderArn 46 | Condition: 47 | StringEquals: 48 | token.actions.githubusercontent.com:aud: !Ref OIDCAudience 49 | StringLike: 50 | token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/${RepositoryName}:* 51 | 52 | ClusterBootstrapPolicy: 53 | Type: 'AWS::IAM::Policy' 54 | Properties: 55 | PolicyName: flux-cfn-local-cluster-bootstrap 56 | PolicyDocument: 57 | Version: "2012-10-17" 58 | Statement: 59 | - Effect: Allow 60 | Action: 61 | - 'secretsmanager:GetSecretValue' 62 | Resource: 63 | - !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:flux-git-credentials-*' 64 | - Effect: Allow 65 | Action: 66 | - 'codecommit:GetRepository' 67 | Resource: 68 | - !Sub 'arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:my-cloudformation-templates' 69 | Roles: 70 | - !Ref Role 71 | 72 | IntegrationTestPolicy: 73 | Type: 'AWS::IAM::Policy' 74 | Properties: 75 | PolicyName: flux-cfn-integration-test-runner 76 | PolicyDocument: 77 | Version: "2012-10-17" 78 | Statement: 79 | - Effect: Allow 80 | Action: 81 | - 'secretsmanager:GetSecretValue' 82 | Resource: 83 | - !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:flux-git-credentials-*' 84 | - Effect: Allow 85 | Action: 86 | - 'cloudformation:DescribeStacks' 87 | - 'cloudformation:DeleteStack' 88 | Resource: 89 | - !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/cfn-flux-controller-integ-test-*' 90 | Roles: 91 | - !Ref Role 92 | 93 | CfnControllerPolicy: 94 | Type: 'AWS::IAM::Policy' 95 | Properties: 96 | PolicyName: flux-cfn-controller 97 | PolicyDocument: 98 | Version: "2012-10-17" 99 | Statement: 100 | # Allow controller to manage CloudFormation stacks 101 | - Effect: Allow 102 | Action: 103 | - 'cloudformation:ContinueUpdateRollback' 104 | - 'cloudformation:CreateChangeSet' 105 | - 'cloudformation:DeleteChangeSet' 106 | - 'cloudformation:DeleteStack' 107 | - 'cloudformation:DescribeChangeSet' 108 | - 'cloudformation:DescribeStacks' 109 | - 'cloudformation:ExecuteChangeSet' 110 | Resource: 111 | - !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/cfn-flux-controller-integ-test-*' 112 | # Allow controller to upload templates to the template bucket 113 | - Effect: Allow 114 | Action: 115 | - 's3:PutObject' 116 | - 's3:AbortMultipartUpload' 117 | Resource: 118 | - !Sub 'arn:aws:s3:::flux-cfn-templates-${AWS::AccountId}-${AWS::Region}/flux-*.template' 119 | # Allow CloudFormation to provision the resources defined in the sample CloudFormation templates 120 | - Effect: Allow 121 | Action: 122 | - 'ssm:PutParameter' 123 | - 'ssm:GetParameters' 124 | - 'ssm:DeleteParameter' 125 | - 'ssm:DescribeParameters' 126 | - 'ssm:AddTagsToResource' 127 | - 'ssm:RemoveTagsFromResource' 128 | Resource: '*' 129 | Condition: 130 | "ForAnyValue:StringEquals": 131 | "aws:CalledVia": 132 | - cloudformation.amazonaws.com 133 | # Allow CloudFormation to download templates from the template bucket 134 | - Effect: Allow 135 | Action: 136 | - 's3:GetObject' 137 | Resource: 138 | - !Sub 'arn:aws:s3:::flux-cfn-templates-${AWS::AccountId}-${AWS::Region}/flux-*.template' 139 | Condition: 140 | "ForAnyValue:StringEquals": 141 | "aws:CalledVia": 142 | - cloudformation.amazonaws.com 143 | Roles: 144 | - !Ref Role 145 | 146 | GithubOidc: 147 | Type: AWS::IAM::OIDCProvider 148 | Condition: CreateOIDCProvider 149 | Properties: 150 | Url: https://token.actions.githubusercontent.com 151 | ClientIdList: 152 | - sts.amazonaws.com 153 | ThumbprintList: 154 | - 6938fd4d98bab03faadb97b34396831e3780aea1 155 | 156 | Outputs: 157 | Role: 158 | Value: !GetAtt Role.Arn -------------------------------------------------------------------------------- /.github/cloudformation/publisher-authentication.yaml: -------------------------------------------------------------------------------- 1 | # This CloudFormation template configures an IAM identity provider that uses GitHub's OIDC, 2 | # enabling GitHub Actions to publish the controller Docker image in the AWS account where 3 | # this template is deployed. 4 | # aws cloudformation deploy \ 5 | # --template-file .github/cloudformation/publisher-authentication.yaml \ 6 | # --stack-name github-identity-provider \ 7 | # --parameter-overrides GitHubOrg=awslabs RepositoryName=aws-cloudformation-controller-for-flux \ 8 | # --capabilities CAPABILITY_NAMED_IAM \ 9 | # --region us-west-2 \ 10 | # --profile flux-cfn-publisher-account 11 | 12 | Parameters: 13 | GitHubOrg: 14 | Description: Name of GitHub organization/user (case sensitive) 15 | Type: String 16 | RepositoryName: 17 | Description: Name of GitHub repository (case sensitive) 18 | Type: String 19 | OIDCProviderArn: 20 | Description: Arn for the GitHub OIDC Provider. 21 | Default: "" 22 | Type: String 23 | OIDCAudience: 24 | Description: Audience supplied to configure-aws-credentials. 25 | Default: "sts.amazonaws.com" 26 | Type: String 27 | 28 | Conditions: 29 | CreateOIDCProvider: !Equals 30 | - !Ref OIDCProviderArn 31 | - "" 32 | 33 | Resources: 34 | Role: 35 | Type: AWS::IAM::Role 36 | Properties: 37 | RoleName: GitHubEcrPublisherRole 38 | ManagedPolicyArns: 39 | - arn:aws:iam::aws:policy/AmazonElasticContainerRegistryPublicPowerUser 40 | AssumeRolePolicyDocument: 41 | Statement: 42 | - Effect: Allow 43 | Action: sts:AssumeRoleWithWebIdentity 44 | Principal: 45 | Federated: !If 46 | - CreateOIDCProvider 47 | - !Ref GithubOidc 48 | - !Ref OIDCProviderArn 49 | Condition: 50 | StringEquals: 51 | token.actions.githubusercontent.com:aud: !Ref OIDCAudience 52 | StringLike: 53 | token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/${RepositoryName}:* 54 | 55 | GithubOidc: 56 | Type: AWS::IAM::OIDCProvider 57 | Condition: CreateOIDCProvider 58 | Properties: 59 | Url: https://token.actions.githubusercontent.com 60 | ClientIdList: 61 | - sts.amazonaws.com 62 | ThumbprintList: 63 | - 6938fd4d98bab03faadb97b34396831e3780aea1 64 | 65 | Outputs: 66 | Role: 67 | Value: !GetAtt Role.Arn -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: monthly 8 | open-pull-requests-limit: 10 9 | commit-message: 10 | prefix: "chore(deps)" 11 | - package-ecosystem: gomod 12 | directory: "/api" 13 | schedule: 14 | interval: monthly 15 | open-pull-requests-limit: 10 16 | commit-message: 17 | prefix: "chore(api-deps)" 18 | -------------------------------------------------------------------------------- /.github/workflows/integ-tests-pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Integ Tests 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, labeled] 6 | branches: [main] 7 | workflow_dispatch: {} 8 | 9 | jobs: 10 | integ_tests: 11 | name: Run Pull Request Integration Tests 12 | runs-on: ubuntu-latest 13 | if: contains(github.event.pull_request.labels.*.name, 'safe-to-test') 14 | permissions: 15 | id-token: write 16 | contents: read 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | ref: ${{ github.event.pull_request.head.sha }} 21 | - name: Configure git 22 | run: | 23 | git config --global user.name "github-actions[bot]" 24 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 25 | - name: Restore Go cache 26 | uses: actions/cache@v3 27 | with: 28 | path: ~/go/pkg/mod 29 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 30 | restore-keys: | 31 | ${{ runner.os }}-go- 32 | - name: Setup Go 33 | uses: actions/setup-go@v3 34 | with: 35 | go-version: 1.20.x 36 | - name: Setup Kind 37 | uses: engineerd/setup-kind@v0.5.0 38 | with: 39 | version: v0.17.0 40 | image: kindest/node:v1.28.0 41 | - name: Setup Kustomize 42 | uses: fluxcd/pkg/actions/kustomize@main 43 | - name: Setup Kubectl 44 | uses: fluxcd/pkg/actions/kubectl@main 45 | - name: Setup Flux CLI 46 | uses: fluxcd/flux2/action@main 47 | - name: Install tools 48 | run: make install-tools 49 | - name: Configure AWS Credentials 50 | uses: aws-actions/configure-aws-credentials@v2 51 | with: 52 | role-to-assume: ${{ secrets.INTEG_TEST_ROLE_ARN }} 53 | aws-region: ${{ secrets.INTEG_TEST_REGION }} 54 | - name: Bootstrap local test cluster 55 | run: make bootstrap-local-cluster 56 | - name: Deploy into local test cluster 57 | run: make deploy-local 58 | - name: Run tests 59 | run: make integ-test 60 | - name: Debug failure 61 | if: failure() 62 | run: | 63 | kubectl get all -n flux-system 64 | kubectl describe pods -l app=cfn-controller -n flux-system || true 65 | kubectl describe cfnstack -n flux-system || true 66 | kubectl logs deployment/cfn-controller -n flux-system || true 67 | -------------------------------------------------------------------------------- /.github/workflows/integ-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integ Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: {} 7 | 8 | jobs: 9 | integ_tests: 10 | name: Run Integration Tests 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | contents: read 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Configure git 18 | run: | 19 | git config --global user.name "github-actions[bot]" 20 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 21 | - name: Restore Go cache 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/go/pkg/mod 25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 26 | restore-keys: | 27 | ${{ runner.os }}-go- 28 | - name: Setup Go 29 | uses: actions/setup-go@v3 30 | with: 31 | go-version: 1.20.x 32 | - name: Setup Kind 33 | uses: engineerd/setup-kind@v0.5.0 34 | with: 35 | version: v0.17.0 36 | image: kindest/node:v1.28.0 37 | - name: Setup Kustomize 38 | uses: fluxcd/pkg/actions/kustomize@main 39 | - name: Setup Kubectl 40 | uses: fluxcd/pkg/actions/kubectl@main 41 | - name: Setup Flux CLI 42 | uses: fluxcd/flux2/action@main 43 | - name: Install tools 44 | run: make install-tools 45 | - name: Configure AWS Credentials 46 | uses: aws-actions/configure-aws-credentials@v2 47 | with: 48 | role-to-assume: ${{ secrets.INTEG_TEST_ROLE_ARN }} 49 | aws-region: ${{ secrets.INTEG_TEST_REGION }} 50 | - name: Bootstrap local test cluster 51 | run: make bootstrap-local-cluster 52 | - name: Deploy into local test cluster 53 | run: make deploy-local 54 | - name: Run tests 55 | run: make integ-test 56 | - name: Debug failure 57 | if: failure() 58 | run: | 59 | kubectl get all -n flux-system 60 | kubectl describe pods -l app=cfn-controller -n flux-system || true 61 | kubectl describe cfnstack -n flux-system || true 62 | kubectl logs deployment/cfn-controller -n flux-system || true 63 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | semanticpr: 12 | name: Semantic Pull Request 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release CFN Controller 2 | 3 | on: 4 | workflow_dispatch: {} 5 | schedule: 6 | - cron: '0 18 * * 2' # Tuesdays at 10 am PST, 11 am PDT 7 | 8 | jobs: 9 | stage_release: 10 | name: "Stage a new release" 11 | runs-on: ubuntu-latest 12 | outputs: 13 | staged_version: ${{ steps.versiondetails.outputs.stagedversion }} 14 | staged_version_available: ${{ steps.versiondetails.outputs.stagedversionavailable }} 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Setup Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 16 23 | - name: Install standard-version 24 | run: | 25 | npm install -g standard-version@^9.5.0 26 | 27 | - name: Configure git 28 | run: | 29 | git config user.name "github-actions[bot]" 30 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 31 | 32 | - name: Check for new commits to release 33 | run: | 34 | CURRENT_VERSION=$(cat VERSION) 35 | COMMITS_TO_RELEASE=$(git log --pretty=oneline v$CURRENT_VERSION..HEAD | wc -l) 36 | 37 | echo Current version: v$CURRENT_VERSION 38 | echo Commits to release: $COMMITS_TO_RELEASE 39 | 40 | echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV 41 | echo "COMMITS_TO_RELEASE=${COMMITS_TO_RELEASE}" >> $GITHUB_ENV 42 | 43 | - name: Check if no release needed 44 | if: ${{ env.COMMITS_TO_RELEASE == 0 }} 45 | run: | 46 | echo No changes to release! 47 | echo Current release: $CURRENT_VERSION 48 | 49 | - name: Stage new version 50 | if: ${{ env.COMMITS_TO_RELEASE != 0 }} 51 | run: | 52 | standard-version 53 | 54 | NEW_VERSION=$(cat VERSION) 55 | RELEASE_COMMIT_ID=$(git rev-parse HEAD) 56 | 57 | echo "NEW_VERSION=${NEW_VERSION}" >> $GITHUB_ENV 58 | echo "RELEASE_COMMIT_ID=${RELEASE_COMMIT_ID}" >> $GITHUB_ENV 59 | 60 | - name: Check if version was bumped 61 | if: ${{ env.COMMITS_TO_RELEASE != 0 && env.NEW_VERSION == env.CURRENT_VERSION }} 62 | run: | 63 | echo No changes to release! 64 | echo Current release: $CURRENT_VERSION 65 | 66 | - name: 'Show staged version details' 67 | if: ${{ env.COMMITS_TO_RELEASE != 0 && env.NEW_VERSION != env.CURRENT_VERSION }} 68 | id: versiondetails 69 | shell: bash 70 | run: | 71 | echo New version: v$NEW_VERSION 72 | echo Commit ID: $RELEASE_COMMIT_ID 73 | echo Previous version: v$CURRENT_VERSION 74 | echo Changes to be released: 75 | git log --pretty=oneline v$CURRENT_VERSION..v$NEW_VERSION 76 | 77 | echo "stagedversion=${NEW_VERSION}" >> $GITHUB_OUTPUT 78 | echo "stagedversionavailable=true" >> $GITHUB_OUTPUT 79 | 80 | run_unit_tests: 81 | name: "Run unit tests" 82 | runs-on: ubuntu-latest 83 | needs: stage_release 84 | if: needs.stage_release.outputs.staged_version_available == 'true' 85 | steps: 86 | - uses: actions/checkout@v3 87 | - name: Restore Go cache 88 | uses: actions/cache@v3 89 | with: 90 | path: ~/go/pkg/mod 91 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 92 | restore-keys: | 93 | ${{ runner.os }}-go- 94 | - name: Setup Go 95 | uses: actions/setup-go@v3 96 | with: 97 | go-version: 1.20.x 98 | - run: make test 99 | - name: Check if working tree is dirty 100 | run: | 101 | if [[ $(git diff --stat) != '' ]]; then 102 | git --no-pager diff 103 | echo 'run make test and commit changes' 104 | exit 1 105 | fi 106 | 107 | run_integration_tests: 108 | name: "Run integration tests" 109 | runs-on: ubuntu-latest 110 | needs: stage_release 111 | if: needs.stage_release.outputs.staged_version_available == 'true' 112 | permissions: 113 | id-token: write 114 | contents: read 115 | steps: 116 | - uses: actions/checkout@v3 117 | - name: Configure git 118 | run: | 119 | git config --global user.name "github-actions[bot]" 120 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 121 | - name: Restore Go cache 122 | uses: actions/cache@v3 123 | with: 124 | path: ~/go/pkg/mod 125 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 126 | restore-keys: | 127 | ${{ runner.os }}-go- 128 | - name: Setup Go 129 | uses: actions/setup-go@v3 130 | with: 131 | go-version: 1.20.x 132 | - name: Setup Kind 133 | uses: engineerd/setup-kind@v0.5.0 134 | with: 135 | version: v0.17.0 136 | image: kindest/node:v1.28.0 137 | - name: Setup Kustomize 138 | uses: fluxcd/pkg/actions/kustomize@main 139 | - name: Setup Kubectl 140 | uses: fluxcd/pkg/actions/kubectl@main 141 | - name: Setup Flux CLI 142 | uses: fluxcd/flux2/action@main 143 | - name: Install tools 144 | run: make install-tools 145 | - name: Configure AWS Credentials 146 | uses: aws-actions/configure-aws-credentials@v2 147 | with: 148 | role-to-assume: ${{ secrets.INTEG_TEST_ROLE_ARN }} 149 | aws-region: ${{ secrets.INTEG_TEST_REGION }} 150 | - name: Bootstrap local test cluster 151 | run: make bootstrap-local-cluster 152 | - name: Deploy into local test cluster 153 | run: make deploy-local 154 | - name: Run tests 155 | run: make integ-test 156 | - name: Debug failure 157 | if: failure() 158 | run: | 159 | kubectl get all -n flux-system 160 | kubectl describe pods -l app=cfn-controller -n flux-system || true 161 | kubectl describe cfnstack -n flux-system || true 162 | kubectl logs deployment/cfn-controller -n flux-system || true 163 | 164 | release_new_version: 165 | name: "Release the new version" 166 | needs: [stage_release, run_unit_tests, run_integration_tests] 167 | if: needs.stage_release.outputs.staged_version_available == 'true' 168 | runs-on: ubuntu-latest 169 | permissions: 170 | contents: write 171 | id-token: write 172 | steps: 173 | - uses: actions/checkout@v3 174 | with: 175 | fetch-depth: 0 176 | - name: Setup Node 177 | uses: actions/setup-node@v3 178 | with: 179 | node-version: 16 180 | - name: Install release tools 181 | run: | 182 | npm install -g standard-version@^9.5.0 183 | - name: Setup Kustomize 184 | uses: fluxcd/pkg/actions/kustomize@main 185 | - name: Setup QEMU 186 | uses: docker/setup-qemu-action@v2 187 | - name: Setup Docker Buildx 188 | id: buildx 189 | uses: docker/setup-buildx-action@v2 190 | - name: Configure git 191 | run: | 192 | git config user.name "github-actions[bot]" 193 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 194 | 195 | - name: Update Kubernetes configuration with new version 196 | env: 197 | NEW_VERSION: ${{needs.stage_release.outputs.staged_version}} 198 | run: | 199 | cd config/manager 200 | kustomize edit set image public.ecr.aws/aws-cloudformation/aws-cloudformation-controller-for-flux=public.ecr.aws/aws-cloudformation/aws-cloudformation-controller-for-flux:v$NEW_VERSION 201 | cd ../.. 202 | git add config/manager/kustomization.yaml 203 | git commit -m "chore(release): Bump controller image version to v$NEW_VERSION" 204 | echo "STAGED_VERSION=${NEW_VERSION}" >> $GITHUB_ENV 205 | 206 | - name: Tag new version and update changelog 207 | run: | 208 | standard-version 209 | 210 | NEW_VERSION=$(cat VERSION) 211 | RELEASE_COMMIT_ID=$(git rev-parse HEAD) 212 | 213 | echo "NEW_VERSION=${NEW_VERSION}" >> $GITHUB_ENV 214 | echo "RELEASE_COMMIT_ID=${RELEASE_COMMIT_ID}" >> $GITHUB_ENV 215 | 216 | - name: Confirm version number 217 | if: ${{ env.STAGED_VERSION != env.NEW_VERSION }} 218 | run: | 219 | echo Staged release and actual release numbers do not match 220 | echo Staged release: $STAGED_VERSION 221 | echo Actual release: $NEW_VERSION 222 | exit 1 223 | 224 | - name: Configure AWS credentials 225 | uses: aws-actions/configure-aws-credentials@v2 226 | with: 227 | role-to-assume: ${{ secrets.PUBLISHER_ROLE_ARN }} 228 | aws-region: ${{ secrets.PUBLISHER_REGION }} 229 | 230 | - name: Login to Amazon ECR Public 231 | id: login-ecr-public 232 | uses: aws-actions/amazon-ecr-login@v1 233 | with: 234 | registry-type: public 235 | 236 | - name: Configure Docker image tags 237 | id: docker-image-tags 238 | env: 239 | REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} 240 | REGISTRY_ALIAS: ${{ secrets.PUBLISHER_REGISTRY_ALIAS }} 241 | REPOSITORY: aws-cloudformation-controller-for-flux 242 | run: | 243 | echo "versioned_image=$REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:v$NEW_VERSION" >> "$GITHUB_OUTPUT" 244 | echo "latest_image=$REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:latest" >> "$GITHUB_OUTPUT" 245 | 246 | - name: Build and push controller Docker image to ECR Public 247 | uses: docker/build-push-action@v4 248 | with: 249 | push: true 250 | no-cache: true 251 | builder: ${{ steps.buildx.outputs.name }} 252 | context: . 253 | file: ./Dockerfile 254 | build-args: | 255 | BUILD_SHA=${{ github.sha }} 256 | BUILD_VERSION=v${{ env.NEW_VERSION }} 257 | platforms: linux/amd64,linux/arm64 258 | tags: | 259 | ${{ steps.docker-image-tags.outputs.versioned_image }} 260 | ${{ steps.docker-image-tags.outputs.latest_image }} 261 | 262 | - name: Push new version to GitHub 263 | run: | 264 | git push origin HEAD:main 265 | git push origin v$NEW_VERSION 266 | 267 | - name: Create GitHub release 268 | uses: softprops/action-gh-release@v1 269 | with: 270 | name: v${{ env.NEW_VERSION }} 271 | tag_name: v${{ env.NEW_VERSION }} 272 | target_commitish: ${{ env.RELEASE_COMMIT_ID }} 273 | body: See the [changelog](CHANGELOG.md) for details about the changes included in this release. 274 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: {} 9 | 10 | jobs: 11 | unit_tests: 12 | name: Run Unit Tests 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Restore Go cache 19 | uses: actions/cache@v3 20 | with: 21 | path: ~/go/pkg/mod 22 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 23 | restore-keys: | 24 | ${{ runner.os }}-go- 25 | - name: Setup Go 26 | uses: actions/setup-go@v3 27 | with: 28 | go-version: 1.20.x 29 | - run: make test 30 | - name: Check if working tree is dirty 31 | run: | 32 | if [[ $(git diff --stat) != '' ]]; then 33 | git --no-pager diff 34 | echo 'run make test and commit changes' 35 | exit 1 36 | fi 37 | - name: Security Scan 38 | uses: securego/gosec@master 39 | with: 40 | args: ./... 41 | 42 | build_docker_image: 43 | name: Build Docker Image 44 | runs-on: ubuntu-latest 45 | permissions: 46 | contents: read 47 | steps: 48 | - uses: actions/checkout@v3 49 | - name: Setup QEMU 50 | uses: docker/setup-qemu-action@v2 51 | - name: Setup Docker Buildx 52 | id: buildx 53 | uses: docker/setup-buildx-action@v2 54 | - name: Cache Docker layers 55 | uses: actions/cache@v3 56 | with: 57 | path: /tmp/.buildx-cache 58 | key: ${{ runner.os }}-buildx-${{ github.sha }} 59 | restore-keys: | 60 | ${{ runner.os }}-buildx- 61 | - name: Build controller Docker image 62 | uses: docker/build-push-action@v4 63 | with: 64 | push: false 65 | builder: ${{ steps.buildx.outputs.name }} 66 | context: . 67 | file: ./Dockerfile 68 | cache-from: type=local,src=/tmp/.buildx-cache 69 | cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max 70 | build-args: | 71 | BUILD_SHA=${{ github.sha }} 72 | BUILD_VERSION=test-build 73 | platforms: linux/amd64,linux/arm64 74 | tags: | 75 | cfn-flux-controller:test-build 76 | - name: Move cache 77 | run: | 78 | rm -rf /tmp/.buildx-cache 79 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin 10 | testdata 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Generated files - skip generated files, except for vendored files 19 | config/dev 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | .vscode 28 | *.drawio.bkp 29 | 30 | # OS files 31 | **/.DS_Store 32 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | # Conditions to get out of the queue (= merged) 5 | - status-success=Run Unit Tests 6 | - status-success=Build Docker Image 7 | - status-success=Semantic Pull Request 8 | 9 | pull_request_rules: 10 | - name: Automatically merge on CI success and review approval 11 | conditions: 12 | - base=main 13 | - '#approved-reviews-by>=1' 14 | - approved-reviews-by=@awslabs/aws-proton 15 | - -approved-reviews-by~=author 16 | - status-success=Run Unit Tests 17 | - status-success=Build Docker Image 18 | - status-success=Run Pull Request Integration Tests 19 | - status-success=Semantic Pull Request 20 | - label!=work-in-progress 21 | - label=safe-to-test 22 | - -title~=(WIP|wip) 23 | - -merged 24 | - -closed 25 | - author!=dependabot[bot] 26 | actions: 27 | queue: 28 | method: squash 29 | name: default 30 | 31 | - name: Automatically approve and merge Dependabot PRs, skip integ tests 32 | conditions: 33 | - base=main 34 | - author=dependabot[bot] 35 | - status-success=Run Unit Tests 36 | - status-success=Build Docker Image 37 | - status-success=Semantic Pull Request 38 | - -title~=(WIP|wip) 39 | - -label~=(blocked|do-not-merge) 40 | - -merged 41 | - -closed 42 | actions: 43 | review: 44 | type: APPROVE 45 | queue: 46 | method: squash 47 | name: default 48 | -------------------------------------------------------------------------------- /.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "packageFiles": [ 3 | { 4 | "filename": "VERSION", 5 | "type": "plain-text" 6 | } 7 | ], 8 | "bumpFiles": [ 9 | { 10 | "filename": "VERSION", 11 | "type": "plain-text" 12 | } 13 | ], 14 | "tagPrefix": "v" 15 | } 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.2.23](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.22...v0.2.23) (2024-07-16) 6 | 7 | ### [0.2.22](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.21...v0.2.22) (2024-06-18) 8 | 9 | ### [0.2.21](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.20...v0.2.21) (2024-05-07) 10 | 11 | ### [0.2.20](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.19...v0.2.20) (2024-04-30) 12 | 13 | ### [0.2.19](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.18...v0.2.19) (2024-04-23) 14 | 15 | ### [0.2.18](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.17...v0.2.18) (2024-04-02) 16 | 17 | ### [0.2.17](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.16...v0.2.17) (2024-03-19) 18 | 19 | ### [0.2.16](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.15...v0.2.16) (2024-03-05) 20 | 21 | ### [0.2.15](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.14...v0.2.15) (2024-02-06) 22 | 23 | ### [0.2.14](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.13...v0.2.14) (2024-01-09) 24 | 25 | ### [0.2.13](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.12...v0.2.13) (2024-01-03) 26 | 27 | ### [0.2.12](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.11...v0.2.12) (2023-11-07) 28 | 29 | ### [0.2.11](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.10...v0.2.11) (2023-10-17) 30 | 31 | ### [0.2.10](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.9...v0.2.10) (2023-10-03) 32 | 33 | 34 | ### Features 35 | 36 | * Upgrade to Flux source controller v1 ([4053f0b](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/4053f0bc352269d9c9f5f6c8cfafbf16941b4f71)) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * Re-add v1beta2 source controller API to scheme ([2a758a2](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/2a758a23597a481802b8bfc3aaabf0a0d4d26875)) 42 | 43 | ### [0.2.9](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.8...v0.2.9) (2023-09-12) 44 | 45 | ### [0.2.8](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.7...v0.2.8) (2023-09-05) 46 | 47 | ### [0.2.7](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.6...v0.2.7) (2023-07-11) 48 | 49 | ### [0.2.6](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.5...v0.2.6) (2023-06-06) 50 | 51 | ### [0.2.5](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.4...v0.2.5) (2023-05-30) 52 | 53 | ### [0.2.4](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.3...v0.2.4) (2023-05-16) 54 | 55 | ### [0.2.3](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.2...v0.2.3) (2023-05-04) 56 | 57 | 58 | ### Features 59 | 60 | * Publish ARM Docker images for the controller ([e1e296d](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/e1e296d5fed6d472699706468afce5a25eed0eec)), closes [#32](https://github.com/awslabs/aws-cloudformation-controller-for-flux/issues/32) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * Pull target arch from Docker ([eed3351](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/eed3351463555bd70ac2d2c374a80d28b111c469)) 66 | * Remove ARM v7 support (not supported by base image) ([2d2034d](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/2d2034daa5b84a8befcf5d37e0eea4d7cd0268a7)) 67 | 68 | ### [0.2.2](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.1...v0.2.2) (2023-04-25) 69 | 70 | ### [0.2.1](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.2.0...v0.2.1) (2023-04-18) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * Add amd64 nodeSelector ([74dfa5b](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/74dfa5bc9611ba8ec700fd01f5092ea827c54170)) 76 | 77 | ## [0.2.0](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.1.1...v0.2.0) (2023-04-17) 78 | 79 | 80 | ### ⚠ BREAKING CHANGES 81 | 82 | * Switch to publishing to production ECR public repo 83 | 84 | ### Features 85 | 86 | * Apply default stack tags to all stacks (cfn-flux-controller/version, cfn-flux-controller/name, cfn-flux-controller/namespace) ([e50fb10](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/e50fb1083e60a2cec4885123b70615e9928f3685)) 87 | * New aws-region controller flag ([b656ab2](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/b656ab2a9bfaabd326df802407b8fa67cd7d2098)) 88 | * New stack-tags controller flag for specifying default stack tags ([b6942a3](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/b6942a3bbe6cdaf1a035dd0c521e62655dbc29bd)) 89 | * New template-bucket controller flag for specifying the S3 bucket to use for storing CloudFormation templates prior to stack deployment ([271da08](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/271da08bff27d68a97482fb246235b25c55176f0)) 90 | * Specify stack tags in the CloudFormationStack object ([e71683f](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/e71683f9002e84192803fb1565865702e426c731)) 91 | * Switch to publishing to production ECR public repo ([e0e4c2e](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/e0e4c2ea97202cea415017fcca5302e12169f89b)) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * Fill in default version if unknown during build ([ccb1940](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/ccb19408f964dfcdbc708f245cde5bb9273ddfe6)) 97 | 98 | ### [0.1.1](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.1.0...v0.1.1) (2023-04-11) 99 | 100 | ## [0.1.0](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.0.3...v0.1.0) (2023-04-07) 101 | 102 | ### Features 103 | 104 | * Add support for blocking cross-namespace source references ([3feb7f0](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/3feb7f0c7ea93498091f9f7df434a577b0abe081)) 105 | * Add support for sharding controllers based on labels ([3feb7f0](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/3feb7f0c7ea93498091f9f7df434a577b0abe081)) 106 | * Add support for the newer digest field for source artifacts ([3feb7f0](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/3feb7f0c7ea93498091f9f7df434a577b0abe081)) 107 | 108 | ### Bug Fixes 109 | 110 | * Various security fixes found by gosec ([008fe32](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/008fe322137090a50d7c1f9cd0f930c7052bda4e)) 111 | 112 | ### [0.0.3](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.0.2...v0.0.3) (2023-04-05) 113 | 114 | 115 | ### Features 116 | 117 | * Add support for declaring stack dependencies ([a2ffd37](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/a2ffd37bf0c3ac45760f33018e0977fe3aa62965)) 118 | * Add support for stack parameters ([465b1c8](https://github.com/awslabs/aws-cloudformation-controller-for-flux/commit/465b1c8933304a2a74471062a7ccd7a82c3cee5e)) 119 | 120 | ### [0.0.2](https://github.com/awslabs/aws-cloudformation-controller-for-flux/compare/v0.0.1...v0.0.2) (2023-04-04) 121 | 122 | Working CloudFormation controller with support for declaring a CloudFormationStack object with a stack name and 123 | a Flux source reference for the stack's CloudFormation template. 124 | 125 | ### 0.0.1 (2023-03-03) 126 | 127 | Beginning of this project 128 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the controller binary 2 | FROM public.ecr.aws/docker/library/golang:1.20 as builder 3 | 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | 8 | ENV GOPROXY=direct 9 | ENV GO111MODULE=on 10 | ENV GOARCH=$TARGETARCH 11 | ENV GOOS=linux 12 | ENV CGO_ENABLED=0 13 | 14 | # Copy license and attributions 15 | COPY LICENSE LICENSE 16 | COPY THIRD-PARTY-LICENSES.txt THIRD-PARTY-LICENSES.txt 17 | 18 | # Copy API submodule and module manifests 19 | COPY api/ api/ 20 | COPY go.mod go.mod 21 | COPY go.sum go.sum 22 | 23 | # Cache deps 24 | RUN go mod download 25 | 26 | # Copy controller source code 27 | COPY main.go main.go 28 | COPY internal/ internal/ 29 | 30 | # Build 31 | ARG BUILD_SHA 32 | ARG BUILD_VERSION 33 | 34 | RUN go build \ 35 | -ldflags "-X main.BuildSHA=$BUILD_SHA -X main.BuildVersion=$BUILD_VERSION" \ 36 | -a -o bin/cfn-controller main.go 37 | 38 | # Build the controller image 39 | FROM public.ecr.aws/eks-distro-build-tooling/eks-distro-minimal-base-nonroot:2021-12-01-1638322424 40 | 41 | COPY --from=builder /workspace/bin/cfn-controller /workspace/LICENSE /workspace/THIRD-PARTY-LICENSES.txt /bin/ 42 | 43 | USER 1000 44 | 45 | ENTRYPOINT ["/bin/cfn-controller"] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | BUILD_SHA ?= $(shell git rev-parse --short HEAD) 3 | BUILD_VERSION ?= $(shell git describe --tags $$(git rev-list --tags --max-count=1)) 4 | 5 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 6 | GOBIN=$(shell pwd)/bin 7 | 8 | SHELL = /usr/bin/env bash -o pipefail 9 | .SHELLFLAGS = -ec 10 | 11 | # Allows for defining additional Docker buildx arguments, e.g. '--push'. 12 | BUILD_ARGS ?= 13 | 14 | AWS_ACCOUNT_ID="$(shell aws sts get-caller-identity --query 'Account' --output text)" 15 | AWS_REGION=us-west-2 16 | 17 | all: build 18 | 19 | ##### Generate CRDs ##### 20 | 21 | # Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 22 | generate: controller-gen 23 | cd api; $(CONTROLLER_GEN) object:headerFile="../hack/boilerplate.go.txt" paths="./..." 24 | 25 | # Generate manifests e.g. CRD, RBAC, etc. 26 | manifests: controller-gen 27 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config="config/crd/bases" 28 | cd api; $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config="../config/crd/bases" 29 | 30 | # Generate API reference documentation 31 | api-docs: gen-crd-api-reference-docs 32 | $(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1alpha1 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/cloudformationstack.md 33 | 34 | ##### Clean up code ##### 35 | 36 | tidy: 37 | cd api; rm -f go.sum; go mod tidy -compat=1.20 38 | rm -f go.sum; go mod tidy -compat=1.20 39 | 40 | fmt: 41 | go fmt ./... 42 | 43 | vet: 44 | go vet ./... 45 | 46 | clean: 47 | go clean ./... 48 | rm -rf config/crd/bases 49 | 50 | ##### Build and test ##### 51 | 52 | PHONY: gen-mocks 53 | gen-mocks: mockgen 54 | ${MOCKGEN} -package=mocks -destination=./internal/clients/cloudformation/mocks/mock_sdk.go -source=./internal/clients/cloudformation/sdk_interfaces.go 55 | ${MOCKGEN} -package=mocks -destination=./internal/clients/s3/mocks/mock_sdk.go -source=./internal/clients/s3/sdk_interfaces.go 56 | ${MOCKGEN} -package=mocks -destination=./internal/clients/mocks/mock_clients.go -source=./internal/clients/clients.go 57 | ${MOCKGEN} -package=mocks -destination=./internal/mocks/mock_event_recorder.go k8s.io/client-go/tools/record EventRecorder 58 | ${MOCKGEN} -package=mocks -destination=./internal/mocks/mock_kubernetes_client.go sigs.k8s.io/controller-runtime/pkg/client Client,StatusWriter 59 | 60 | test: tidy generate gen-mocks fmt vet manifests api-docs 61 | go test ./... -coverprofile cover.out 62 | cd api; go test ./... -coverprofile cover.out 63 | 64 | scan: 65 | ${GOSEC} ./... 66 | 67 | view-test-coverage: 68 | go tool cover -html=cover.out 69 | 70 | build: generate gen-mocks fmt vet manifests api-docs 71 | go build -o bin/manager \ 72 | -ldflags "-X main.BuildSHA=$(BUILD_SHA) -X main.BuildVersion=$(BUILD_VERSION)" \ 73 | main.go 74 | 75 | build-docker-image: 76 | docker build \ 77 | -t "aws-cloudformation-controller-for-flux:latest" \ 78 | -f "./Dockerfile" \ 79 | --build-arg BUILD_SHA="$(BUILD_SHA)" \ 80 | --build-arg BUILD_VERSION="$(BUILD_VERSION)" \ 81 | "." 82 | 83 | push-docker-image-to-ecr: 84 | aws ecr get-login-password --region $(AWS_REGION) | docker login --username AWS --password-stdin $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com 85 | docker tag aws-cloudformation-controller-for-flux:latest $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/aws-cloudformation-controller-for-flux:latest 86 | docker push $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/aws-cloudformation-controller-for-flux:latest 87 | 88 | ##### Run locally ##### 89 | 90 | # Run a controller from your host. 91 | run: generate fmt vet install 92 | SOURCE_CONTROLLER_LOCALHOST=localhost:30000 AWS_REGION=$(AWS_REGION) TEMPLATE_BUCKET=flux-cfn-templates-$(AWS_ACCOUNT_ID)-$(AWS_REGION) go run ./main.go 93 | 94 | # Install CRDs into a cluster 95 | install: manifests 96 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 97 | 98 | # Uninstall CRDs from a cluster 99 | uninstall: manifests 100 | $(KUSTOMIZE) build config/crd | kubectl delete -f - 101 | 102 | # Deploy into a local kind cluster - the cluster must already be bootstrapped 103 | deploy-local: install build-docker-image 104 | docker tag aws-cloudformation-controller-for-flux:latest aws-cloudformation-controller-for-flux:local 105 | kind load docker-image aws-cloudformation-controller-for-flux:local 106 | mkdir -p config/dev && cp -r config/default config/crd config/manager config/rbac config/dev/ 107 | cp config/manager/overlays/dev/kustomization.yaml config/dev/manager 108 | ifdef AWS_ACCESS_KEY_ID 109 | cat config/manager/overlays/aws-creds-from-env-vars/env-patch.yaml | AWS_REGION=$(AWS_REGION) TEMPLATE_BUCKET=flux-cfn-templates-$(AWS_ACCOUNT_ID)-$(AWS_REGION) envsubst > config/dev/manager/env-patch.yaml 110 | else 111 | cat config/manager/overlays/aws-creds-from-mounted-file/env-patch.yaml | AWS_REGION=$(AWS_REGION) TEMPLATE_BUCKET=flux-cfn-templates-$(AWS_ACCOUNT_ID)-$(AWS_REGION) envsubst > config/dev/manager/env-patch.yaml 112 | endif 113 | cd config/dev/default && $(KUSTOMIZE) edit set image public.ecr.aws/aws-cloudformation/aws-cloudformation-controller-for-flux=aws-cloudformation-controller-for-flux:local 114 | $(KUSTOMIZE) build config/dev/default | kubectl apply -f - 115 | kubectl rollout restart deployment cfn-controller --namespace=flux-system 116 | kubectl rollout status deployment/cfn-controller --namespace=flux-system --timeout=30s 117 | kubectl logs deployment/cfn-controller --namespace flux-system 118 | rm -rf config/dev 119 | 120 | bootstrap-local-cluster: 121 | $(shell pwd)/local-dev/bootstrap-local-kind-cluster.sh 122 | 123 | integ-test: generate fmt vet manifests 124 | go test -v -tags=integration ./internal/integtests/ 125 | 126 | ##### Install dev tools ##### 127 | 128 | .PHONY: install-tools 129 | install-tools: kustomize controller-gen gen-crd-api-reference-docs gosec 130 | 131 | KUSTOMIZE = $(shell pwd)/bin/kustomize 132 | .PHONY: kustomize 133 | kustomize: ## Download kustomize locally if necessary. 134 | $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v4@v4.5.7) 135 | 136 | CONTROLLER_GEN = $(GOBIN)/controller-gen 137 | .PHONY: controller-gen 138 | controller-gen: ## Download controller-gen locally if necessary. 139 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2) 140 | 141 | MOCKGEN = $(GOBIN)/mockgen 142 | .PHONY: mockgen 143 | mockgen: ## Download mockgen locally if necessary. 144 | $(call go-install-tool,$(MOCKGEN),github.com/golang/mock/mockgen@v1.6.0) 145 | 146 | GOSEC = $(GOBIN)/gosec 147 | .PHONY: gosec 148 | gosec: ## Download gosec locally if necessary. 149 | $(call go-install-tool,$(GOSEC),github.com/securego/gosec/v2/cmd/gosec@v2.15.0) 150 | 151 | GEN_CRD_API_REFERENCE_DOCS = $(GOBIN)/gen-crd-api-reference-docs 152 | .PHONY: gen-crd-api-reference-docs 153 | gen-crd-api-reference-docs: 154 | $(call go-install-tool,$(GEN_CRD_API_REFERENCE_DOCS),github.com/ahmetb/gen-crd-api-reference-docs@v0.3.0) 155 | 156 | # go-install-tool will 'go install' any package $2 and install it to $1. 157 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 158 | define go-install-tool 159 | @[ -f $(1) ] || { \ 160 | set -e ;\ 161 | TMP_DIR=$$(mktemp -d) ;\ 162 | cd $$TMP_DIR ;\ 163 | go mod init tmp ;\ 164 | echo "Downloading $(2)" ;\ 165 | GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ 166 | rm -rf $$TMP_DIR ;\ 167 | } 168 | endef 169 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: contrib.fluxcd.io 2 | layout: 3 | - go.kubebuilder.io/v3 4 | projectName: aws-cloudformation-controller-for-flux 5 | repo: github.com/awslabs/aws-cloudformation-controller-for-flux 6 | resources: 7 | - api: 8 | crdVersion: v1 9 | namespaced: true 10 | controller: true 11 | domain: cloudformation.contrib.fluxcd.io 12 | group: infra 13 | kind: CloudFormationStack 14 | path: github.com/awslabs/aws-cloudformation-controller-for-flux/api/v1alpha1 15 | version: v1alpha1 16 | version: "3" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Note: this project is now archived!** AWS CloudFormation now natively supports syncing stacks with source code stored in a Git repository. 2 | https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/git-sync.html 3 | 4 | # AWS CloudFormation Template Sync Controller for Flux 5 | 6 | [![Unit tests](https://github.com/awslabs/aws-cloudformation-controller-for-flux/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/awslabs/aws-cloudformation-controller-for-flux/actions/workflows/unit-tests.yml) 7 | [![Integration tests](https://github.com/awslabs/aws-cloudformation-controller-for-flux/actions/workflows/integ-tests.yml/badge.svg?branch=main)](https://github.com/awslabs/aws-cloudformation-controller-for-flux/actions/workflows/integ-tests.yml) 8 | 9 | The AWS CloudFormation Template Sync Controller for Flux helps you to store CloudFormation templates in a git repository 10 | and automatically sync template changes to CloudFormation stacks in your AWS account with Flux. 11 | 12 | [Flux CD](https://fluxcd.io/) is an open source, Cloud Native Computing Foundation (CNCF) graduated project that keeps 13 | Kubernetes clusters in sync with sources of configuration including Git repositories, S3 buckets, and Open Container 14 | Initiative (OCI) compatible repositories (such as Amazon ECR). 15 | 16 | The AWS CloudFormation Template Sync Controller for Flux is an extension to Flux that lets you store your CloudFormation 17 | templates in a Git repository and automatically deploy them as CloudFormation stacks in your AWS account. After installing 18 | the CloudFormation Template Sync controller into your Kubernetes cluster, you can configure Flux to monitor your Git repository 19 | for changes to CloudFormation template files. When a CloudFormation template file is updated in a Git commit, the CloudFormation 20 | controller is designed to automatically deploy the latest template changes to your CloudFormation stack. The CloudFormation 21 | controller is also designed to continuously sync the latest template from the Git repository into your stack by re-deploying 22 | the template on a regular interval. 23 | 24 | ## Demo 25 | 26 | ![Demo](/docs/demo.gif 'Demo') 27 | 28 | ## Example 29 | 30 | Connect a git repository to Flux - this git repository will store your CloudFormation templates: 31 | 32 | ```yaml 33 | apiVersion: source.toolkit.fluxcd.io/v1 34 | kind: GitRepository 35 | metadata: 36 | name: my-cfn-templates-repo 37 | namespace: flux-system 38 | spec: 39 | url: https://git-codecommit.us-west-2.amazonaws.com/v1/repos/my-cfn-templates-repo 40 | ref: 41 | branch: main 42 | interval: 5m 43 | secretRef: 44 | name: my-cfn-templates-repo-auth 45 | ``` 46 | 47 | In your git repository, add a CloudFormation template file for each stack that you want automatically deployed by Flux: 48 | 49 | ``` 50 | project-a/stack-template.yaml 51 | project-b/template.yaml 52 | project-c/my-stack-template.yaml 53 | README.md 54 | ``` 55 | 56 | Register each CloudFormation template file in your git repository with Flux as a separate CloudFormation stack object: 57 | 58 | ```yaml 59 | apiVersion: cloudformation.contrib.fluxcd.io/v1alpha1 60 | kind: CloudFormationStack 61 | metadata: 62 | name: hello-world-stack 63 | namespace: flux-system 64 | spec: 65 | stackName: flux-hello-world 66 | templatePath: ./project-a/stack-template.yaml 67 | sourceRef: 68 | kind: GitRepository 69 | name: my-cfn-templates-repo 70 | interval: 1h 71 | retryInterval: 5m 72 | ``` 73 | 74 | See the [CloudFormationStack API reference](./docs/api/cloudformationstack.md) for the full set of configuration options 75 | supported by the CloudFormation controller for CloudFormation stacks. 76 | 77 | When either the stack template file in the git repo OR the stack object in Flux is created or updated, you will see the CloudFormation stack created/updated in your AWS account: 78 | 79 | ```yaml 80 | $ kubectl describe cfnstack hello-world-stack --namespace flux-system 81 | Name: hello-world-stack 82 | Namespace: flux-system 83 | ... 84 | Status: 85 | Conditions: 86 | Last Transition Time: 2023-02-28T19:56:58Z 87 | Message: deployed stack 'flux-hello-world' 88 | Observed Generation: 1 89 | Reason: Succeeded 90 | Status: True 91 | Type: Ready 92 | ``` 93 | 94 | ## Installation 95 | 96 | See the [AWS CloudFormation Template Sync Controller for Flux installation guide](./docs/install.md). 97 | 98 | ## API reference 99 | 100 | See the [CloudFormationStack API reference](./docs/api/cloudformationstack.md) for the full set of configuration options 101 | supported by the CloudFormation controller for CloudFormation stacks. 102 | 103 | ## Development 104 | 105 | For information about developing the CloudFormation controller locally, see [Developing the AWS CloudFormation Template Sync Controller for Flux](./docs/developing.md). 106 | 107 | For information about the design of the CloudFormation controller, see [AWS CloudFormation Template Sync Controller for Flux Design](./docs/design.md). 108 | 109 | ## Security 110 | 111 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information on reporting security issues. 112 | 113 | ## License 114 | 115 | This library is licensed under the MIT-0 License. See the LICENSE file. 116 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.23 -------------------------------------------------------------------------------- /api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/aws-cloudformation-controller-for-flux/api 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/fluxcd/pkg/apis/meta v1.3.0 7 | k8s.io/apimachinery v0.28.6 8 | sigs.k8s.io/controller-runtime v0.16.3 9 | ) 10 | 11 | require ( 12 | github.com/go-logr/logr v1.2.4 // indirect 13 | github.com/gogo/protobuf v1.3.2 // indirect 14 | github.com/google/gofuzz v1.2.0 // indirect 15 | github.com/json-iterator/go v1.1.12 // indirect 16 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 17 | github.com/modern-go/reflect2 v1.0.2 // indirect 18 | golang.org/x/net v0.23.0 // indirect 19 | golang.org/x/text v0.14.0 // indirect 20 | gopkg.in/inf.v0 v0.9.1 // indirect 21 | gopkg.in/yaml.v2 v2.4.0 // indirect 22 | k8s.io/klog/v2 v2.100.1 // indirect 23 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect 24 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 25 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /api/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fluxcd/pkg/apis/meta v1.3.0 h1:KxeEc6olmSZvQ5pBONPE4IKxyoWQbqTJF1X6K5nIXpU= 5 | github.com/fluxcd/pkg/apis/meta v1.3.0/go.mod h1:3Ui8xFkoU4sYehqmscjpq7NjqH2YN1A2iX2okbO3/yA= 6 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 7 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 8 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 9 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 10 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 11 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 12 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 13 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 14 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 15 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 16 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 17 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 18 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 19 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 20 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 21 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 23 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 24 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 25 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 26 | github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= 27 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 33 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 34 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 35 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 37 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 38 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 39 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 40 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 44 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 45 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 46 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 47 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 51 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 54 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 55 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 56 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 57 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 58 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 59 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 60 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 61 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 62 | golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= 63 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 64 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 65 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 66 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 67 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 70 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 71 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 72 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 73 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 74 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 75 | k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= 76 | k8s.io/apimachinery v0.28.6 h1:RsTeR4z6S07srPg6XYrwXpTJVMXsjPXn0ODakMytSW0= 77 | k8s.io/apimachinery v0.28.6/go.mod h1:QFNX/kCl/EMT2WTSz8k4WLCv2XnkOLMaL8GAVRMdpsA= 78 | k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= 79 | k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 80 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= 81 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 82 | sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= 83 | sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= 84 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 85 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 86 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 87 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 88 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 89 | -------------------------------------------------------------------------------- /api/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Package v1alpha1 contains API Schema definitions for the CloudFormation v1alpha1 API group 5 | // +kubebuilder:object:generate=true 6 | // +groupName=cloudformation.contrib.fluxcd.io 7 | package v1alpha1 8 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Package v1alpha1 contains API Schema definitions for the CloudFormation v1alpha1 API group 5 | // +kubebuilder:object:generate=true 6 | // +groupName=cloudformation.contrib.fluxcd.io 7 | package v1alpha1 8 | 9 | import ( 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "sigs.k8s.io/controller-runtime/pkg/scheme" 12 | ) 13 | 14 | var ( 15 | // GroupVersion is group version used to register these objects 16 | GroupVersion = schema.GroupVersion{Group: "cloudformation.contrib.fluxcd.io", Version: "v1alpha1"} 17 | 18 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 19 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 20 | 21 | // AddToScheme adds the types in this group-version to the given scheme. 22 | AddToScheme = SchemeBuilder.AddToScheme 23 | ) 24 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | // SPDX-License-Identifier: MIT-0 6 | 7 | // Code generated by controller-gen. DO NOT EDIT. 8 | 9 | package v1alpha1 10 | 11 | import ( 12 | "github.com/fluxcd/pkg/apis/meta" 13 | "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | runtime "k8s.io/apimachinery/pkg/runtime" 15 | ) 16 | 17 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 18 | func (in *CloudFormationStack) DeepCopyInto(out *CloudFormationStack) { 19 | *out = *in 20 | out.TypeMeta = in.TypeMeta 21 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 22 | in.Spec.DeepCopyInto(&out.Spec) 23 | in.Status.DeepCopyInto(&out.Status) 24 | } 25 | 26 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudFormationStack. 27 | func (in *CloudFormationStack) DeepCopy() *CloudFormationStack { 28 | if in == nil { 29 | return nil 30 | } 31 | out := new(CloudFormationStack) 32 | in.DeepCopyInto(out) 33 | return out 34 | } 35 | 36 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 37 | func (in *CloudFormationStack) DeepCopyObject() runtime.Object { 38 | if c := in.DeepCopy(); c != nil { 39 | return c 40 | } 41 | return nil 42 | } 43 | 44 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 45 | func (in *CloudFormationStackList) DeepCopyInto(out *CloudFormationStackList) { 46 | *out = *in 47 | out.TypeMeta = in.TypeMeta 48 | in.ListMeta.DeepCopyInto(&out.ListMeta) 49 | if in.Items != nil { 50 | in, out := &in.Items, &out.Items 51 | *out = make([]CloudFormationStack, len(*in)) 52 | for i := range *in { 53 | (*in)[i].DeepCopyInto(&(*out)[i]) 54 | } 55 | } 56 | } 57 | 58 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudFormationStackList. 59 | func (in *CloudFormationStackList) DeepCopy() *CloudFormationStackList { 60 | if in == nil { 61 | return nil 62 | } 63 | out := new(CloudFormationStackList) 64 | in.DeepCopyInto(out) 65 | return out 66 | } 67 | 68 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 69 | func (in *CloudFormationStackList) DeepCopyObject() runtime.Object { 70 | if c := in.DeepCopy(); c != nil { 71 | return c 72 | } 73 | return nil 74 | } 75 | 76 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 77 | func (in *CloudFormationStackSpec) DeepCopyInto(out *CloudFormationStackSpec) { 78 | *out = *in 79 | out.SourceRef = in.SourceRef 80 | out.Interval = in.Interval 81 | out.PollInterval = in.PollInterval 82 | if in.RetryInterval != nil { 83 | in, out := &in.RetryInterval, &out.RetryInterval 84 | *out = new(v1.Duration) 85 | **out = **in 86 | } 87 | if in.StackParameters != nil { 88 | in, out := &in.StackParameters, &out.StackParameters 89 | *out = make([]StackParameter, len(*in)) 90 | copy(*out, *in) 91 | } 92 | if in.StackTags != nil { 93 | in, out := &in.StackTags, &out.StackTags 94 | *out = make([]StackTag, len(*in)) 95 | copy(*out, *in) 96 | } 97 | if in.DependsOn != nil { 98 | in, out := &in.DependsOn, &out.DependsOn 99 | *out = make([]meta.NamespacedObjectReference, len(*in)) 100 | copy(*out, *in) 101 | } 102 | } 103 | 104 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudFormationStackSpec. 105 | func (in *CloudFormationStackSpec) DeepCopy() *CloudFormationStackSpec { 106 | if in == nil { 107 | return nil 108 | } 109 | out := new(CloudFormationStackSpec) 110 | in.DeepCopyInto(out) 111 | return out 112 | } 113 | 114 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 115 | func (in *CloudFormationStackStatus) DeepCopyInto(out *CloudFormationStackStatus) { 116 | *out = *in 117 | out.ReconcileRequestStatus = in.ReconcileRequestStatus 118 | if in.Conditions != nil { 119 | in, out := &in.Conditions, &out.Conditions 120 | *out = make([]v1.Condition, len(*in)) 121 | for i := range *in { 122 | (*in)[i].DeepCopyInto(&(*out)[i]) 123 | } 124 | } 125 | } 126 | 127 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudFormationStackStatus. 128 | func (in *CloudFormationStackStatus) DeepCopy() *CloudFormationStackStatus { 129 | if in == nil { 130 | return nil 131 | } 132 | out := new(CloudFormationStackStatus) 133 | in.DeepCopyInto(out) 134 | return out 135 | } 136 | 137 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 138 | func (in *ReadinessUpdate) DeepCopyInto(out *ReadinessUpdate) { 139 | *out = *in 140 | } 141 | 142 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReadinessUpdate. 143 | func (in *ReadinessUpdate) DeepCopy() *ReadinessUpdate { 144 | if in == nil { 145 | return nil 146 | } 147 | out := new(ReadinessUpdate) 148 | in.DeepCopyInto(out) 149 | return out 150 | } 151 | 152 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 153 | func (in *SourceReference) DeepCopyInto(out *SourceReference) { 154 | *out = *in 155 | } 156 | 157 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceReference. 158 | func (in *SourceReference) DeepCopy() *SourceReference { 159 | if in == nil { 160 | return nil 161 | } 162 | out := new(SourceReference) 163 | in.DeepCopyInto(out) 164 | return out 165 | } 166 | 167 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 168 | func (in *StackParameter) DeepCopyInto(out *StackParameter) { 169 | *out = *in 170 | } 171 | 172 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackParameter. 173 | func (in *StackParameter) DeepCopy() *StackParameter { 174 | if in == nil { 175 | return nil 176 | } 177 | out := new(StackParameter) 178 | in.DeepCopyInto(out) 179 | return out 180 | } 181 | 182 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 183 | func (in *StackTag) DeepCopyInto(out *StackTag) { 184 | *out = *in 185 | } 186 | 187 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackTag. 188 | func (in *StackTag) DeepCopy() *StackTag { 189 | if in == nil { 190 | return nil 191 | } 192 | out := new(StackTag) 193 | in.DeepCopyInto(out) 194 | return out 195 | } 196 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - bases/cloudformation.contrib.fluxcd.io_cloudformationstacks.yaml 5 | # +kubebuilder:scaffold:crdkustomizeresource 6 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: flux-system 4 | resources: 5 | - ../crd 6 | - ../rbac 7 | - ../manager 8 | -------------------------------------------------------------------------------- /config/manager/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: cfn-controller 5 | labels: 6 | control-plane: controller 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: cfn-controller 11 | replicas: 1 12 | template: 13 | metadata: 14 | labels: 15 | app: cfn-controller 16 | annotations: 17 | prometheus.io/scrape: "true" 18 | prometheus.io/port: "8080" 19 | spec: 20 | terminationGracePeriodSeconds: 600 21 | serviceAccountName: cfn-controller 22 | securityContext: 23 | # Required for AWS IAM Role bindings 24 | # https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html 25 | fsGroup: 1337 26 | containers: 27 | - name: manager 28 | image: public.ecr.aws/aws-cloudformation/aws-cloudformation-controller-for-flux 29 | imagePullPolicy: IfNotPresent 30 | securityContext: 31 | allowPrivilegeEscalation: false 32 | readOnlyRootFilesystem: true 33 | runAsNonRoot: true 34 | capabilities: 35 | drop: ["ALL"] 36 | seccompProfile: 37 | type: RuntimeDefault 38 | ports: 39 | - containerPort: 8080 40 | name: http-prom 41 | protocol: TCP 42 | - containerPort: 9440 43 | name: healthz 44 | protocol: TCP 45 | env: 46 | - name: RUNTIME_NAMESPACE 47 | valueFrom: 48 | fieldRef: 49 | fieldPath: metadata.namespace 50 | args: 51 | - --watch-all-namespaces 52 | - --log-level=info 53 | - --log-encoding=json 54 | - --enable-leader-election 55 | readinessProbe: 56 | httpGet: 57 | path: /readyz 58 | port: healthz 59 | livenessProbe: 60 | httpGet: 61 | path: /healthz 62 | port: healthz 63 | resources: 64 | limits: 65 | cpu: 1000m 66 | memory: 1Gi 67 | requests: 68 | cpu: 100m 69 | memory: 64Mi 70 | volumeMounts: 71 | - name: temp 72 | mountPath: /tmp 73 | volumes: 74 | - name: temp 75 | emptyDir: {} 76 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | images: 6 | - name: public.ecr.aws/aws-cloudformation/aws-cloudformation-controller-for-flux 7 | newName: public.ecr.aws/aws-cloudformation/aws-cloudformation-controller-for-flux 8 | newTag: v0.2.23 9 | -------------------------------------------------------------------------------- /config/manager/overlays/aws-creds-from-env-vars/env-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: cfn-controller 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | env: 11 | - name: AWS_REGION 12 | value: "${AWS_REGION}" 13 | - name: TEMPLATE_BUCKET 14 | value: "${TEMPLATE_BUCKET}" 15 | - name: AWS_ACCESS_KEY_ID 16 | valueFrom: 17 | secretKeyRef: 18 | name: aws-creds 19 | key: AWS_ACCESS_KEY_ID 20 | - name: AWS_SECRET_ACCESS_KEY 21 | valueFrom: 22 | secretKeyRef: 23 | name: aws-creds 24 | key: AWS_SECRET_ACCESS_KEY 25 | - name: AWS_SESSION_TOKEN 26 | valueFrom: 27 | secretKeyRef: 28 | name: aws-creds 29 | key: AWS_SESSION_TOKEN 30 | optional: true 31 | -------------------------------------------------------------------------------- /config/manager/overlays/aws-creds-from-env-vars/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | bases: 5 | - ../../ 6 | 7 | patchesStrategicMerge: 8 | - path: env-patch.yaml 9 | -------------------------------------------------------------------------------- /config/manager/overlays/aws-creds-from-mounted-file/env-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: cfn-controller 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | env: 11 | - name: AWS_REGION 12 | value: "${AWS_REGION}" 13 | - name: TEMPLATE_BUCKET 14 | value: "${TEMPLATE_BUCKET}" 15 | volumeMounts: 16 | - name: aws-creds 17 | mountPath: "/.aws" 18 | readOnly: true 19 | volumes: 20 | - name: aws-creds 21 | secret: 22 | secretName: aws-creds 23 | -------------------------------------------------------------------------------- /config/manager/overlays/aws-creds-from-mounted-file/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | bases: 5 | - ../../ 6 | 7 | patchesStrategicMerge: 8 | - path: env-patch.yaml 9 | -------------------------------------------------------------------------------- /config/manager/overlays/base/env-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: cfn-controller 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | env: 11 | - name: AWS_REGION 12 | value: "us-west-2" 13 | - name: TEMPLATE_BUCKET 14 | value: "" 15 | -------------------------------------------------------------------------------- /config/manager/overlays/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | bases: 5 | - ../../ 6 | 7 | patchesStrategicMerge: 8 | - path: env-patch.yaml 9 | -------------------------------------------------------------------------------- /config/manager/overlays/dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | images: 6 | - name: public.ecr.aws/aws-cloudformation/aws-cloudformation-controller-for-flux 7 | newName: public.ecr.aws/aws-cloudformation/aws-cloudformation-controller-for-flux 8 | newTag: v0.0.1 9 | patchesStrategicMerge: 10 | - env-patch.yaml -------------------------------------------------------------------------------- /config/rbac/cfnstack_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit cfnstacks. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: cfnstack-editor-role 6 | rules: 7 | - apiGroups: 8 | - cloudformation.contrib.fluxcd.io 9 | resources: 10 | - cfnstacks 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - cloudformation.contrib.fluxcd.io 21 | resources: 22 | - cfnstacks/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/cfnstack_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view cfnstacks. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: cfnstack-viewer-role 6 | rules: 7 | - apiGroups: 8 | - cloudformation.contrib.fluxcd.io 9 | resources: 10 | - cfnstacks 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - cloudformation.contrib.fluxcd.io 17 | resources: 18 | - cfnstacks/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/cluster_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: reconciler-role 5 | rules: 6 | - apiGroups: ['*'] 7 | resources: ['*'] 8 | verbs: ['*'] 9 | - nonResourceURLs: ['*'] 10 | verbs: ['*'] 11 | -------------------------------------------------------------------------------- /config/rbac/cluster_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: reconciler-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: reconciler-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: flux-system 4 | resources: 5 | - role.yaml 6 | - role_binding.yaml 7 | - leader_election_role.yaml 8 | - leader_election_role_binding.yaml 9 | - cluster_role.yaml 10 | - cluster_role_binding.yaml 11 | - service_account.yaml 12 | namePrefix: cfn- 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | - apiGroups: 34 | - "coordination.k8s.io" 35 | resources: 36 | - leases 37 | verbs: 38 | - get 39 | - list 40 | - watch 41 | - create 42 | - update 43 | - patch 44 | - delete -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/overlays/eks-irsa/eks-irsa-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: cfn-controller 5 | namespace: system 6 | annotations: 7 | eks.amazonaws.com/role-arn: arn:aws:iam::${AWS_ACCOUNT_ID}:role/AWSCloudFormationControllerFluxIRSARole 8 | -------------------------------------------------------------------------------- /config/rbac/overlays/eks-irsa/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | bases: 5 | - ../../ 6 | 7 | patchesStrategicMerge: 8 | - path: eks-irsa-patch.yaml 9 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: manager-role 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - events 12 | verbs: 13 | - create 14 | - patch 15 | - apiGroups: 16 | - cloudformation.contrib.fluxcd.io 17 | resources: 18 | - cloudformationstacks 19 | verbs: 20 | - create 21 | - delete 22 | - get 23 | - list 24 | - patch 25 | - update 26 | - watch 27 | - apiGroups: 28 | - cloudformation.contrib.fluxcd.io 29 | resources: 30 | - cloudformationstacks/finalizers 31 | verbs: 32 | - create 33 | - delete 34 | - get 35 | - patch 36 | - update 37 | - apiGroups: 38 | - cloudformation.contrib.fluxcd.io 39 | resources: 40 | - cloudformationstacks/status 41 | verbs: 42 | - get 43 | - patch 44 | - update 45 | - apiGroups: 46 | - source.toolkit.fluxcd.io 47 | resources: 48 | - buckets 49 | - gitrepositories 50 | - ocirepositories 51 | verbs: 52 | - get 53 | - list 54 | - watch 55 | - apiGroups: 56 | - source.toolkit.fluxcd.io 57 | resources: 58 | - buckets/status 59 | - gitrepositories/status 60 | - ocirepositories/status 61 | verbs: 62 | - get 63 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller 5 | namespace: system 6 | -------------------------------------------------------------------------------- /ct.yaml: -------------------------------------------------------------------------------- 1 | # See https://github.com/helm/chart-testing#configuration 2 | remote: origin 3 | target-branch: main 4 | validate-maintainers: false 5 | validate-chart-schema: false 6 | -------------------------------------------------------------------------------- /demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Reset for demo: 4 | # * Tear down the kind cluster 5 | # kind delete cluster 6 | # * Delete the CloudFormation stacks from my AWS account 7 | # aws cloudformation delete-stack --stack-name my-cfn-stack-deployed-by-flux 8 | # aws cloudformation delete-stack --stack-name my-other-cfn-stack-deployed-by-flux 9 | # aws cloudformation delete-stack --stack-name yet-another-cfn-stack-deployed-by-flux 10 | # aws cloudformation wait stack-delete-complete --stack-name my-cfn-stack-deployed-by-flux 11 | # aws cloudformation wait stack-delete-complete --stack-name my-other-cfn-stack-deployed-by-flux 12 | # aws cloudformation wait stack-delete-complete --stack-name yet-another-cfn-stack-deployed-by-flux 13 | # * Delete the stack files from testdata/my-flux-configuration (git push) 14 | # * Re-copy the examples into testdata 15 | # cp -rf examples/my-cloudformation-templates/* testdata/my-cloudformation-templates/ (git push) 16 | # cp -rf examples/my-flux-configuration/* testdata/my-flux-configuration/ 17 | # * Re-create the kind cluster 18 | # make bootstrap-local-cluster 19 | # * Start up local controller: 20 | # make run 21 | 22 | export PATH="$PATH:$PWD/bin/local" 23 | PS1="$ " 24 | 25 | # Ensure demo-magic is cloned here 26 | # https://github.com/paxtonhare/demo-magic 27 | . ../demo-magic/demo-magic.sh 28 | 29 | clear 30 | 31 | # Look and feel 32 | 33 | TYPE_SPEED=20 34 | DEMO_COMMENT_COLOR=$CYAN 35 | NO_WAIT=false 36 | 37 | # Start the demo 38 | 39 | # Press enter to continue 40 | PROMPT_TIMEOUT=0 41 | p "# Welcome to the AWS CloudFormation Template Sync Controller for Flux!" 42 | PROMPT_TIMEOUT=1 43 | 44 | NO_WAIT=true 45 | p "#" 46 | p "# Flux is a GitOps tool that runs on Kubernetes. Out of the box, Flux automates syncing" 47 | p "# Kubernetes configuration from source locations like git repositories into your Kubernetes" 48 | p "# cluster." 49 | p "#" 50 | p "# The CloudFormation controller for Flux automates syncing CloudFormation templates from source" 51 | p "# locations like git repositories into CloudFormation stacks in your AWS account." 52 | p "#" 53 | p "# Let's walk through an example!" 54 | NO_WAIT=false 55 | 56 | pe "cd examples/" 57 | 58 | p "# I have 2 git repositories here:" 59 | 60 | pe "ls -1" 61 | 62 | # Highlight repos, press enter to continue 63 | 64 | PROMPT_TIMEOUT=0 65 | p "# First, I have a repository that stores the CloudFormation templates that I need to deploy." 66 | PROMPT_TIMEOUT=1 67 | 68 | pe "ls -1 my-cloudformation-templates" 69 | 70 | # Highlight template files, press enter to continue 71 | 72 | PROMPT_TIMEOUT=0 73 | p "# I have three CloudFormation template files that will be deployed to three stacks." 74 | 75 | NO_WAIT=true 76 | p "# I also have a git repository that stores the configuration for Flux running in my Kubernetes" 77 | p "# cluster." 78 | NO_WAIT=false 79 | 80 | pe "cd my-flux-configuration" 81 | pe "ls -1" 82 | 83 | # Highlight the template git repo file, press enter to continue 84 | 85 | PROMPT_TIMEOUT=0 86 | p "# I first hooked up my CloudFormation template git repository to Flux." 87 | PROMPT_TIMEOUT=1 88 | 89 | NO_WAIT=true 90 | p "# Flux polls my git repository every five minutes to check for new commits to my CloudFormation" 91 | p "# templates." 92 | NO_WAIT=false 93 | 94 | pe "cat my-cloudformation-templates-repo.yaml" 95 | 96 | # Highlight repo configuration, press enter to continue 97 | 98 | PROMPT_TIMEOUT=0 99 | p "# In my Kubernetes cluster, I can see that Flux has the latest commits for my git repositories." 100 | PROMPT_TIMEOUT=1 101 | 102 | pe "flux get sources git" 103 | 104 | # Highlight git sources, press enter to continue 105 | 106 | PROMPT_TIMEOUT=0 107 | p "# In my Flux configuration, I have three Flux CloudFormationStack objects defined." 108 | PROMPT_TIMEOUT=1 109 | 110 | pe "ls -1 *-stack.yaml" 111 | 112 | NO_WAIT=true 113 | p "# For each CloudFormationStack object, the CloudFormation controller for Flux will create and" 114 | p "# update a CloudFormation stack in my AWS account." 115 | p "#" 116 | p "# The CloudFormationStack configuration specifies which source code repository and file contain" 117 | p "# the template for the stack, and how often to re-sync the latest template into the stack." 118 | NO_WAIT=false 119 | 120 | pe "cat my-cloudformation-stack.yaml" 121 | 122 | # Highlight stack configuration, press enter to continue 123 | 124 | PROMPT_TIMEOUT=0 125 | p "# Let's push this configuration to Flux and watch it create the CloudFormation stacks!" 126 | PROMPT_TIMEOUT=1 127 | 128 | cd ../../testdata/my-flux-configuration 129 | 130 | pe "git add *-stack.yaml" 131 | pe "git commit -m 'Add CFN stacks'" 132 | pe "git push -q" 133 | pe "flux reconcile source git flux-system" 134 | pe "flux get sources git" 135 | 136 | pe "kubectl get cfnstack -A --watch" 137 | 138 | pe "kubectl get cfnstack -A" 139 | 140 | p "# The stacks are now created in my AWS account!" 141 | 142 | # Highlight succeeded reconciliation, press enter to continue 143 | 144 | PROMPT_TIMEOUT=0 145 | pe "aws cloudformation describe-stacks --stack-name my-cfn-stack-deployed-by-flux" 146 | 147 | # Highlight stack status, press enter to continue 148 | 149 | p "# Let's now update a template file and watch Flux automatically deploy the change." 150 | PROMPT_TIMEOUT=1 151 | 152 | pe "cd ../my-cloudformation-templates" 153 | pe "sed -i 's/Hello World/Hey there/g' template.yaml" 154 | pe "git diff" 155 | pe "git add template.yaml" 156 | pe "git commit -m 'Update template file'" 157 | pe "git push -q" 158 | pe "flux reconcile source git my-cfn-templates-repo" 159 | pe "flux get sources git" 160 | 161 | pe "kubectl get cfnstack -A --watch" 162 | 163 | pe "kubectl get cfnstack -A" 164 | 165 | p "# The stack is now updated in my AWS account with the latest template file!" 166 | 167 | # Highlight stack status, press enter to continue 168 | 169 | PROMPT_TIMEOUT=0 170 | pe "aws cloudformation describe-stacks --stack-name my-cfn-stack-deployed-by-flux" 171 | 172 | p "# Enjoy continuous delivery of your CloudFormation stacks with Flux!" 173 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cloudformation-controller-for-flux/6c936890f96e5e5ddbe5a1bf0576cc2e0d0c78b6/docs/demo.gif -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # AWS CloudFormation Template Sync Controller for Flux Design 2 | 3 | 4 | 5 | 1. [Overview](#overview) 6 | 1. [Sequence of events for CloudFormation stack creation](#sequence-of-events-for-cloudformation-stack-creation) 7 | 1. [Sequence of events for CloudFormation stack update](#sequence-of-events-for-cloudformation-stack-update) 8 | 1. [Sequence of events for CloudFormation stack deletion](#sequence-of-events-for-cloudformation-stack-deletion) 9 | 1. [Reconciliation logic](#reconciliation-logic) 10 | 11 | 12 | ## Overview 13 | 14 | The CloudFormation controller registers a `CloudFormationStack` custom resource type into the Kubernetes API server 15 | (see the [CloudFormationStack API reference](./api/cloudformationstack.md)). 16 | The CloudFormationStack resource type describes the specifications for a CloudFormation stack that the CloudFormation 17 | controller should manage, including the location of a CloudFormation template file in a source code repository. Users 18 | create CloudFormation stack objects in their Kubernetes cluster using kubectl to register CloudFormation stacks that 19 | they want Flux to manage. They can also describe these objects using kubectl to get the latest status of the stack, 20 | like whether the latest source code changes have been synced to the stack by Flux. The CloudFormation controller 21 | listens for changes to objects of this type (like creations or updates) from the API server, syncs templates from 22 | source code into the desired CloudFormation stacks, and stores the latest status back into the CloudFormationStack 23 | objects by sending updates to the API server. 24 | 25 | ## Sequence of events for CloudFormation stack creation 26 | 27 | ![Create flow](./diagrams/data-flow-create.png 'Create flow') 28 | 29 | 1. The Kubernetes user registers a git repository with Flux by creating a GitRepository object through the Kubernetes API. 30 | 2. The Flux source controller watches the Kubernetes API for GitRepository object changes, and is notified that a new GitRepository object was created. 31 | 3. The Flux source controller begins polling the git repository for new git commits. 32 | 4. The git user pushes a CloudFormation template file to the git repository. 33 | 5. The Flux source controller detects the new git commit containing the CloudFormation template file during the next poll of the git repository. It clones the git repository to its local disk and creates a tarball artifact of the repository contents. 34 | 6. The Flux source controller updates the GitRepository object through the Kubernetes API with the git commit ID it cloned and information about the artifact it created from the repository contents. 35 | 7. The Kubernetes user creates a CloudFormationStack object through the Kubernetes API. 36 | 8. The CloudFormation controller watches the Kubernetes API for changes to CloudFormationStack objects, and is notified that a new CloudFormationStack object was created. 37 | 9. The CloudFormation controller retrieves the GitRepository object from the Kubernetes API that the CloudFormationStack object refers to as the source of the stack template file. 38 | 10. The CloudFormation controller downloads the contents of the source code repository from the Flux source controller using the artifact URL in the retrieved GitRepository object. 39 | 11. The CloudFormation controller uploads the template file from the source code repository to S3. 40 | 12. The CloudFormation controller deploys the template file from S3 to a CloudFormation stack. 41 | 13. During the stack deployment, the CloudFormation controller updates the status fields in the CloudFormationStack object through the Kubernetes API, eventually marking the stack as successfully deployed. 42 | 43 | ## Sequence of events for CloudFormation stack update 44 | 45 | ![Update flow](./diagrams/data-flow-update.png 'Update flow') 46 | 47 | 1. The git user pushes an update to the CloudFormation template file to the git repository. 48 | 2. The Flux source controller detects the new git commit during the next poll of the git repository. It clones the git repository to its local disk and creates a tarball artifact of the repository contents. 49 | 3. The Flux source controller updates the GitRepository object through the Kubernetes API with the git commit ID it cloned and information about the artifact it created from the repository contents. 50 | 4. The CloudFormation controller watches the Kubernetes API for changes to the GitRepository object referred to by the CloudFormationStack object, and is notified that the GitRepository object was updated. 51 | 5. The CloudFormation controller retrieves the CloudFormationStack object that refers to the updated GitRepository object from the Kubernetes API. 52 | 6. The CloudFormation controller downloads the new contents of the source code repository from the Flux source controller using the artifact URL in the retrieved GitRepository object. 53 | 7. The CloudFormation controller uploads the updated template file from the source code repository to S3. 54 | 8. The CloudFormation controller deploys the template file from S3 to the CloudFormation stack. 55 | 9. During the stack deployment, the CloudFormation controller updates the status fields in the CloudFormationStack object through the Kubernetes API, eventually marking the stack as successfully updated. 56 | 57 | ## Sequence of events for CloudFormation stack deletion 58 | 59 | ![Delete flow](./diagrams/data-flow-delete.png 'Delete flow') 60 | 61 | 1. The Kubernetes user marks the CloudFormationStack object for deletion through the Kubernetes API. 62 | 2. The CloudFormation controller watches the Kubernetes API for changes to CloudFormationStack objects, and is notified that a new CloudFormationStack object was marked for deletion. 63 | 3. The CloudFormation controller deletes the CloudFormation stack. 64 | 4. During stack deletion, the CloudFormation controller updates the status fields in the CloudFormationStack object through the Kubernetes API, eventually marking the stack as successfully deleted. When the CloudFormationStack object status field shows successful stack deletion, the Kubernetes API server fully deletes the CloudFormationStack object. 65 | 66 | ## Reconciliation logic 67 | 68 | The CloudFormation controller continuously reconciles the CloudFormationStack objects with the real CloudFormation 69 | stacks in your AWS account. On a regular interval defined by the user, the CloudFormation controller takes actions to 70 | move the real CloudFormation stack to match the desired state (i.e. the template) defined by the CloudFormationStack 71 | object in Kubernetes. For example, a reconciliation loop may create a new stack or execute a stack change set in your 72 | AWS account to ensure the stack is deployed with the latest template. When the CloudFormation controller starts up, it 73 | lists all CloudFormationStack objects in the Kubernetes cluster, and runs a reconciliation loop on each object. The 74 | controller then adds the stack objects to an internal in-memory queue with a delay defined by each stack’s 75 | reconciliation interval. 76 | 77 | Stack objects can define faster reconciliation intervals for certain cases. They can define a "polling" interval to run 78 | a reconciliation loop more often when a stack action is in progress and needs to be checked on frequently, for example 79 | stack creation or change set creation. They can also define a "retry" interval to quickly retry failed reconciliation 80 | loops, for example if an API call to CloudFormation failed or if a change set failed to execute. 81 | 82 | The following diagram shows the logic of a single reconciliation loop, starting from describing the current state of 83 | the stack to creating or executing change sets to apply the latest template to the stack. 84 | 85 | ![Reconciliation loop](./diagrams/reconciliation-loop.png 'Reconciliation loop') 86 | 87 | When the CloudFormationStack object has been marked for deletion from the Kubernetes API server by a user, the 88 | CloudFormation controller follows different reconciliation logic to delete the real CloudFormation stack in your AWS 89 | account. 90 | 91 | ![Deletion reconciliation loop](./diagrams/reconciliation-loop-deletion.png 'Deletion reconciliation loop') 92 | -------------------------------------------------------------------------------- /docs/developing.md: -------------------------------------------------------------------------------- 1 | # Developing the AWS CloudFormation Template Sync Controller for Flux 2 | 3 | Follow these instructions for setting a local development environment for the AWS CloudFormation Template Sync Controller for Flux. 4 | 5 | ## Install required tools 6 | 7 | 1. Install go 1.20+ 8 | 9 | 2. Run `make install-tools` 10 | 11 | 3. Install kind and create a kind cluster: 12 | 13 | https://kind.sigs.k8s.io/docs/user/quick-start 14 | 15 | 4. Install the Flux CLI: 16 | ``` 17 | $ curl -s https://fluxcd.io/install.sh | sudo bash 18 | ``` 19 | 20 | ## Useful commands 21 | 22 | | | Command | 23 | | ------ | ----------- | 24 | | Generate CRDs | `make generate` | 25 | | Build | `make build` | 26 | | Test | `make test` | 27 | | Integration test | `make integ-test` | 28 | | See CloudFormation stacks | `kubectl describe cfnstack -A` | 29 | | View logs | `kubectl logs deployment/cfn-controller --namespace flux-system` | 30 | | Clean up | `make clean` | 31 | 32 | ## Run the CloudFormation controller on a local kind cluster 33 | 34 | 1. Bootstrap a local kind cluster that runs Flux: 35 | ``` 36 | $ make bootstrap-local-cluster 37 | ``` 38 | 39 | 2. Clone your git repository created by the previous step: 40 | ``` 41 | $ git clone https://git-codecommit.us-west-2.amazonaws.com/v1/repos/my-cloudformation-templates 42 | $ cd my-cloudformation-templates 43 | ``` 44 | 45 | 3. Copy the example CloudFormation templates found in examples/my-cloudformation-templates/ into your CFN template git repository. Then, push the sample template files to the repo: 46 | ``` 47 | $ git add -A 48 | $ git commit -m "Sample templates" 49 | $ git push --set-upstream origin main 50 | ``` 51 | 52 | 4. Clone your Flux configuration repository created by the bootstrap step: 53 | ``` 54 | $ git clone https://git-codecommit.us-west-2.amazonaws.com/v1/repos/my-flux-configuration 55 | $ cd my-flux-configuration 56 | ``` 57 | 58 | 5. Copy the example Flux configuration file `examples/my-flux-configuration/my-cloudformation-templates-repo.yaml` into your Flux config git repository. Then, push the config file to the repo: 59 | ``` 60 | $ git add my-cloudformation-templates-repo.yaml 61 | $ git commit -m "Register CFN templates repo with Flux" 62 | $ git push --set-upstream origin main 63 | $ flux reconcile source git flux-system 64 | $ flux get sources git 65 | ``` 66 | 67 | 6. Copy one of the example CloudFormationStack configuration files `examples/my-flux-configuration/my-cloudformation-stack.yaml` into your Flux config git repository: 68 | ``` 69 | $ git add my-cloudformation-stack.yaml 70 | $ git commit -m "Register a CFN stack with Flux" 71 | $ git push 72 | $ flux reconcile source git flux-system 73 | $ kubectl describe cfnstack -A 74 | ``` 75 | 76 | 7. Build the CloudFormation controller into a Docker image, then deploy it to your local cluster: 77 | ``` 78 | $ make deploy-local 79 | ``` 80 | 81 | ## Run the CloudFormation controller outside of your local kind cluster 82 | 83 | For development purposes, it can be slow to build your controller source code into a Docker image 84 | and deploy the new image to your cluster. Instead, you can run the controller outside 85 | of a container and have it connect to your local cluster. 86 | 87 | Follow the previous section's steps to set up Flux on a local kind cluster. 88 | Instead of running `make deploy-local` at the end, run: 89 | ``` 90 | make run 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/diagrams/data-flow-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cloudformation-controller-for-flux/6c936890f96e5e5ddbe5a1bf0576cc2e0d0c78b6/docs/diagrams/data-flow-create.png -------------------------------------------------------------------------------- /docs/diagrams/data-flow-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cloudformation-controller-for-flux/6c936890f96e5e5ddbe5a1bf0576cc2e0d0c78b6/docs/diagrams/data-flow-delete.png -------------------------------------------------------------------------------- /docs/diagrams/data-flow-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cloudformation-controller-for-flux/6c936890f96e5e5ddbe5a1bf0576cc2e0d0c78b6/docs/diagrams/data-flow-update.png -------------------------------------------------------------------------------- /docs/diagrams/reconciliation-loop-deletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cloudformation-controller-for-flux/6c936890f96e5e5ddbe5a1bf0576cc2e0d0c78b6/docs/diagrams/reconciliation-loop-deletion.png -------------------------------------------------------------------------------- /docs/diagrams/reconciliation-loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cloudformation-controller-for-flux/6c936890f96e5e5ddbe5a1bf0576cc2e0d0c78b6/docs/diagrams/reconciliation-loop.png -------------------------------------------------------------------------------- /examples/my-cloudformation-templates/another-template.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | SampleResource: 3 | Type: AWS::SSM::Parameter 4 | Properties: 5 | Type: String 6 | Value: "ABC 123" 7 | -------------------------------------------------------------------------------- /examples/my-cloudformation-templates/template-with-parameters.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | Param1: 3 | Type: String 4 | Param2: 5 | Type: String 6 | 7 | Resources: 8 | SampleResource: 9 | Type: AWS::SSM::Parameter 10 | Properties: 11 | Type: String 12 | Value: !Sub "${Param1} ${Param2}" 13 | -------------------------------------------------------------------------------- /examples/my-cloudformation-templates/template.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | SampleResource: 3 | Type: AWS::SSM::Parameter 4 | Properties: 5 | Type: String 6 | Value: "Hello World" 7 | -------------------------------------------------------------------------------- /examples/my-cloudformation-templates/yet-another-template.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | SampleResource: 3 | Type: AWS::SSM::Parameter 4 | Properties: 5 | Type: String 6 | Value: "How are you" 7 | -------------------------------------------------------------------------------- /examples/my-flux-configuration/my-cloudformation-stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cloudformation.contrib.fluxcd.io/v1alpha1 2 | kind: CloudFormationStack 3 | metadata: 4 | name: my-cfn-stack 5 | namespace: flux-system 6 | spec: 7 | stackName: my-cfn-stack-deployed-by-flux 8 | templatePath: ./template.yaml 9 | sourceRef: 10 | kind: GitRepository 11 | name: my-cfn-templates-repo 12 | interval: 1h 13 | retryInterval: 5m 14 | destroyStackOnDeletion: true 15 | -------------------------------------------------------------------------------- /examples/my-flux-configuration/my-cloudformation-templates-repo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1 2 | kind: GitRepository 3 | metadata: 4 | name: my-cfn-templates-repo 5 | namespace: flux-system 6 | spec: 7 | url: https://git-codecommit.us-west-2.amazonaws.com/v1/repos/my-cloudformation-templates 8 | interval: 5m 9 | ref: 10 | branch: main 11 | secretRef: 12 | name: cfn-template-repo-auth 13 | -------------------------------------------------------------------------------- /examples/my-flux-configuration/my-other-cloudformation-stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cloudformation.contrib.fluxcd.io/v1alpha1 2 | kind: CloudFormationStack 3 | metadata: 4 | name: my-other-cfn-stack 5 | namespace: flux-system 6 | spec: 7 | stackName: my-other-cfn-stack-deployed-by-flux 8 | templatePath: ./another-template.yaml 9 | sourceRef: 10 | kind: GitRepository 11 | name: my-cfn-templates-repo 12 | interval: 1h 13 | retryInterval: 5m 14 | destroyStackOnDeletion: true 15 | -------------------------------------------------------------------------------- /examples/my-flux-configuration/yet-another-cloudformation-stack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cloudformation.contrib.fluxcd.io/v1alpha1 2 | kind: CloudFormationStack 3 | metadata: 4 | name: yet-another-cfn-stack 5 | namespace: flux-system 6 | spec: 7 | stackName: yet-another-cfn-stack-deployed-by-flux 8 | templatePath: ./yet-another-template.yaml 9 | sourceRef: 10 | kind: GitRepository 11 | name: my-cfn-templates-repo 12 | interval: 1h 13 | retryInterval: 5m 14 | destroyStackOnDeletion: true 15 | -------------------------------------------------------------------------------- /examples/resources.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Description: AWS resources needed to use the CloudFormation controller for Flux 4 | 5 | Parameters: 6 | EnableTemplateBucketAccessLogging: 7 | Description: Enable or disable configuring bucket access logging on the S3 bucket that stores your CloudFormation templates. 8 | Default: "false" 9 | Type: String 10 | AllowedValues: [true, false] 11 | 12 | Conditions: 13 | TemplateBucketAccessLoggingCondition: 14 | !Equals 15 | - "true" 16 | - !Ref EnableTemplateBucketAccessLogging 17 | 18 | Resources: 19 | 20 | # Bucket that the controller will use to upload CFN template files prior to syncing them to their CFN stack 21 | TemplateBucket: 22 | Type: AWS::S3::Bucket 23 | Properties: 24 | BucketName: !Sub flux-cfn-templates-${AWS::AccountId}-${AWS::Region} 25 | LifecycleConfiguration: 26 | Rules: 27 | - ExpirationInDays : 1 28 | NoncurrentVersionExpiration: 29 | NewerNoncurrentVersions: 5 30 | NoncurrentDays: 1 31 | AbortIncompleteMultipartUpload: 32 | DaysAfterInitiation: 1 33 | Status: Enabled 34 | VersioningConfiguration: 35 | Status: Enabled 36 | LoggingConfiguration: 37 | !If 38 | - TemplateBucketAccessLoggingCondition 39 | - { DestinationBucketName: !Ref TemplateAccessLoggingBucket } 40 | - !Ref "AWS::NoValue" 41 | PublicAccessBlockConfiguration: 42 | BlockPublicAcls: true 43 | BlockPublicPolicy: true 44 | IgnorePublicAcls: true 45 | RestrictPublicBuckets: true 46 | DeletionPolicy: Delete 47 | 48 | # Enforce HTTPS only on the template bucket 49 | TemplateBucketPolicy: 50 | Type: AWS::S3::BucketPolicy 51 | Properties: 52 | Bucket: !Ref TemplateBucket 53 | PolicyDocument: 54 | Version: 2012-10-17 55 | Statement: 56 | - Action: 's3:*' 57 | Effect: Deny 58 | Principal: 59 | AWS: '*' 60 | Resource: 61 | - !Sub arn:aws:s3:::${TemplateBucket}/* 62 | - !Sub arn:aws:s3:::${TemplateBucket} 63 | Condition: 64 | Bool: 65 | 'aws:SecureTransport': false 66 | 67 | # Send access logs from the template bucket to this bucket 68 | TemplateAccessLoggingBucket: 69 | Type: AWS::S3::Bucket 70 | Condition: TemplateBucketAccessLoggingCondition 71 | Properties: 72 | AccessControl: LogDeliveryWrite 73 | LifecycleConfiguration: 74 | Rules: 75 | - ExpirationInDays : 365 76 | NoncurrentVersionExpiration: 77 | NewerNoncurrentVersions: 5 78 | NoncurrentDays: 365 79 | AbortIncompleteMultipartUpload: 80 | DaysAfterInitiation: 1 81 | Status: Enabled 82 | VersioningConfiguration: 83 | Status: Enabled 84 | PublicAccessBlockConfiguration: 85 | BlockPublicAcls: true 86 | BlockPublicPolicy: true 87 | IgnorePublicAcls: true 88 | RestrictPublicBuckets: true 89 | DeletionPolicy: Delete 90 | 91 | TemplateAccessLoggingBucketPolicy: 92 | Type: AWS::S3::BucketPolicy 93 | Condition: TemplateBucketAccessLoggingCondition 94 | Properties: 95 | Bucket: !Ref TemplateAccessLoggingBucket 96 | PolicyDocument: 97 | Version: 2012-10-17 98 | Statement: 99 | - Action: 's3:*' 100 | Effect: Deny 101 | Principal: 102 | AWS: '*' 103 | Resource: 104 | - !Sub arn:aws:s3:::${TemplateAccessLoggingBucket}/* 105 | - !Sub arn:aws:s3:::${TemplateAccessLoggingBucket} 106 | Condition: 107 | Bool: 108 | 'aws:SecureTransport': false 109 | - Action: 's3:PutObject' 110 | Effect: Allow 111 | Principal: 112 | Service: 'logging.s3.amazonaws.com' 113 | Resource: 114 | - !Sub arn:aws:s3:::${TemplateAccessLoggingBucket}/* 115 | 116 | # Repository for storing CloudFormation templates 117 | TemplateRepo: 118 | Type: AWS::CodeCommit::Repository 119 | Properties: 120 | RepositoryName: my-cloudformation-templates 121 | 122 | # Repository for storing Flux configuration 123 | FluxRepo: 124 | Type: AWS::CodeCommit::Repository 125 | Properties: 126 | RepositoryName: my-flux-configuration 127 | 128 | # User for Flux to use when interacting with CodeCommit repos 129 | GitUser: 130 | Type: AWS::IAM::User 131 | Properties: 132 | UserName: 'flux-git' 133 | ManagedPolicyArns: 134 | - "arn:aws:iam::aws:policy/AWSCodeCommitPowerUser" 135 | 136 | GitCredentials: 137 | Type: AWS::SecretsManager::Secret 138 | Properties: 139 | Name: flux-git-credentials 140 | # CloudFormation does not yet support creating service-specific credentials, 141 | # so this secret is a placeholder until the credentials are created manually. 142 | SecretString: | 143 | { 144 | "ServiceUserName":"TO-FILL-IN", 145 | "ServicePassword":"TO-FILL-IN" 146 | } 147 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/aws-cloudformation-controller-for-flux 2 | 3 | go 1.20 4 | 5 | replace github.com/awslabs/aws-cloudformation-controller-for-flux/api => ./api 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go-v2 v1.30.3 9 | github.com/aws/aws-sdk-go-v2/config v1.27.27 10 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9 11 | github.com/aws/aws-sdk-go-v2/service/cloudformation v1.53.3 12 | github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 13 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4 14 | github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 15 | github.com/aws/smithy-go v1.20.3 16 | github.com/awslabs/aws-cloudformation-controller-for-flux/api v0.0.0-00010101000000-000000000000 17 | github.com/cucumber/godog v0.14.1 18 | github.com/cyphar/filepath-securejoin v0.2.5 19 | github.com/fluxcd/pkg/apis/event v0.7.0 20 | github.com/fluxcd/pkg/apis/meta v1.3.0 21 | github.com/fluxcd/pkg/runtime v0.44.1 22 | github.com/fluxcd/pkg/untar v0.3.0 23 | github.com/fluxcd/source-controller/api v1.2.5 24 | github.com/go-git/go-git/v5 v5.12.0 25 | github.com/golang/mock v1.6.0 26 | github.com/google/uuid v1.6.0 27 | github.com/hashicorp/go-retryablehttp v0.7.7 28 | github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98 29 | github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98 30 | github.com/spf13/pflag v1.0.5 31 | github.com/stretchr/testify v1.9.0 32 | k8s.io/apimachinery v0.28.6 33 | k8s.io/client-go v0.28.6 34 | k8s.io/utils v0.0.0-20231127182322-b307cd553661 35 | sigs.k8s.io/controller-runtime v0.16.3 36 | ) 37 | 38 | require ( 39 | dario.cat/mergo v1.0.0 // indirect 40 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 41 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 42 | github.com/Microsoft/go-winio v0.6.1 // indirect 43 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 44 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect 45 | github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect 46 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect 47 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect 48 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect 49 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 50 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect 51 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect 52 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect 53 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect 54 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect 55 | github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect 56 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect 57 | github.com/beorn7/perks v1.0.1 // indirect 58 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 59 | github.com/chai2010/gettext-go v1.0.2 // indirect 60 | github.com/cloudflare/circl v1.3.7 // indirect 61 | github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect 62 | github.com/cucumber/messages/go/v21 v21.0.1 // indirect 63 | github.com/davecgh/go-spew v1.1.1 // indirect 64 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 65 | github.com/emirpasic/gods v1.18.1 // indirect 66 | github.com/evanphx/json-patch v5.7.0+incompatible // indirect 67 | github.com/evanphx/json-patch/v5 v5.7.0 // indirect 68 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect 69 | github.com/fluxcd/cli-utils v0.36.0-flux.3 // indirect 70 | github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect 71 | github.com/fluxcd/pkg/tar v0.2.0 // indirect 72 | github.com/fsnotify/fsnotify v1.7.0 // indirect 73 | github.com/go-errors/errors v1.5.1 // indirect 74 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 75 | github.com/go-git/go-billy/v5 v5.5.0 // indirect 76 | github.com/go-logr/logr v1.3.0 // indirect 77 | github.com/go-logr/zapr v1.3.0 // indirect 78 | github.com/go-openapi/jsonpointer v0.20.0 // indirect 79 | github.com/go-openapi/jsonreference v0.20.2 // indirect 80 | github.com/go-openapi/swag v0.22.4 // indirect 81 | github.com/gofrs/uuid v4.3.1+incompatible // indirect 82 | github.com/gogo/protobuf v1.3.2 // indirect 83 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 84 | github.com/golang/protobuf v1.5.3 // indirect 85 | github.com/google/btree v1.1.2 // indirect 86 | github.com/google/gnostic-models v0.6.8 // indirect 87 | github.com/google/go-cmp v0.6.0 // indirect 88 | github.com/google/gofuzz v1.2.0 // indirect 89 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 90 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 91 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 92 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 93 | github.com/hashicorp/go-memdb v1.3.4 // indirect 94 | github.com/hashicorp/golang-lru v0.5.4 // indirect 95 | github.com/imdario/mergo v0.3.16 // indirect 96 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 97 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 98 | github.com/jmespath/go-jmespath v0.4.0 // indirect 99 | github.com/josharian/intern v1.0.0 // indirect 100 | github.com/json-iterator/go v1.1.12 // indirect 101 | github.com/kevinburke/ssh_config v1.2.0 // indirect 102 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 103 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 104 | github.com/mailru/easyjson v0.7.7 // indirect 105 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 106 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 107 | github.com/moby/spdystream v0.2.0 // indirect 108 | github.com/moby/term v0.5.0 // indirect 109 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 110 | github.com/modern-go/reflect2 v1.0.2 // indirect 111 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 112 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 113 | github.com/onsi/gomega v1.31.1 // indirect 114 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 115 | github.com/pjbgf/sha1cd v0.3.0 // indirect 116 | github.com/pkg/errors v0.9.1 // indirect 117 | github.com/pmezard/go-difflib v1.0.0 // indirect 118 | github.com/prometheus/client_golang v1.18.0 // indirect 119 | github.com/prometheus/client_model v0.5.0 // indirect 120 | github.com/prometheus/common v0.45.0 // indirect 121 | github.com/prometheus/procfs v0.12.0 // indirect 122 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 123 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 124 | github.com/skeema/knownhosts v1.2.2 // indirect 125 | github.com/spf13/cobra v1.8.0 // indirect 126 | github.com/xanzy/ssh-agent v0.3.3 // indirect 127 | github.com/xlab/treeprint v1.2.0 // indirect 128 | github.com/zeebo/blake3 v0.2.3 // indirect 129 | go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect 130 | go.uber.org/multierr v1.11.0 // indirect 131 | go.uber.org/zap v1.26.0 // indirect 132 | golang.org/x/crypto v0.21.0 // indirect 133 | golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect 134 | golang.org/x/mod v0.14.0 // indirect 135 | golang.org/x/net v0.23.0 // indirect 136 | golang.org/x/oauth2 v0.16.0 // indirect 137 | golang.org/x/sync v0.6.0 // indirect 138 | golang.org/x/sys v0.20.0 // indirect 139 | golang.org/x/term v0.18.0 // indirect 140 | golang.org/x/text v0.14.0 // indirect 141 | golang.org/x/time v0.5.0 // indirect 142 | golang.org/x/tools v0.17.0 // indirect 143 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 144 | google.golang.org/appengine v1.6.8 // indirect 145 | google.golang.org/protobuf v1.33.0 // indirect 146 | gopkg.in/evanphx/json-patch.v5 v5.7.0 // indirect 147 | gopkg.in/inf.v0 v0.9.1 // indirect 148 | gopkg.in/warnings.v0 v0.1.2 // indirect 149 | gopkg.in/yaml.v2 v2.4.0 // indirect 150 | gopkg.in/yaml.v3 v3.0.1 // indirect 151 | k8s.io/api v0.28.6 // indirect 152 | k8s.io/apiextensions-apiserver v0.28.6 // indirect 153 | k8s.io/cli-runtime v0.28.6 // indirect 154 | k8s.io/component-base v0.28.6 // indirect 155 | k8s.io/klog/v2 v2.110.1 // indirect 156 | k8s.io/kube-openapi v0.0.0-20231206194836-bf4651e18aa8 // indirect 157 | k8s.io/kubectl v0.28.6 // indirect 158 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 159 | sigs.k8s.io/kustomize/api v0.16.0 // indirect 160 | sigs.k8s.io/kustomize/kyaml v0.16.0 // indirect 161 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 162 | sigs.k8s.io/yaml v1.4.0 // indirect 163 | ) 164 | 165 | // Replace digest lib to master to gather access to BLAKE3. 166 | // xref: https://github.com/opencontainers/go-digest/pull/66 167 | replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98 168 | 169 | // Pin kustomize to v5.3.0 170 | replace ( 171 | sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.16.0 172 | sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.16.0 173 | ) 174 | -------------------------------------------------------------------------------- /hack/api-docs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hideMemberFields": [ 3 | "TypeMeta" 4 | ], 5 | "hideTypePatterns": [ 6 | "ParseError$", 7 | "List$" 8 | ], 9 | "externalPackages": [ 10 | { 11 | "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/apis/meta/v1\\.Duration$", 12 | "docsURLTemplate": "https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration" 13 | }, 14 | { 15 | "typeMatchPrefix": "^k8s\\.io/apiextensions-apiserver/pkg/apis/apiextensions/v1\\.JSON$", 16 | "docsURLTemplate": "https://pkg.go.dev/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1?tab=doc#JSON" 17 | }, 18 | { 19 | "typeMatchPrefix": "^k8s\\.io/(api|apimachinery/pkg/apis)/", 20 | "docsURLTemplate": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#{{lower .TypeIdentifier}}-{{arrIndex .PackageSegments -1}}-{{arrIndex .PackageSegments -2}}" 21 | }, 22 | { 23 | "typeMatchPrefix": "^github.com/fluxcd/pkg/runtime/dependency\\.CrossNamespaceDependencyReference$", 24 | "docsURLTemplate": "https://godoc.org/github.com/fluxcd/pkg/runtime/dependency#CrossNamespaceDependencyReference" 25 | }, 26 | { 27 | "typeMatchPrefix": "^github.com/fluxcd/pkg/apis/kustomize", 28 | "docsURLTemplate": "https://godoc.org/github.com/fluxcd/pkg/apis/kustomize#{{ .TypeIdentifier }}" 29 | }, 30 | { 31 | "typeMatchPrefix": "^github.com/fluxcd/pkg/apis/meta", 32 | "docsURLTemplate": "https://godoc.org/github.com/fluxcd/pkg/apis/meta#{{ .TypeIdentifier }}" 33 | }, 34 | { 35 | "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/apis/meta/v1\\.Condition$", 36 | "docsURLTemplate": "https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Condition" 37 | } 38 | ], 39 | "typeDisplayNamePrefixOverrides": { 40 | "k8s.io/api/": "Kubernetes ", 41 | "k8s.io/apimachinery/pkg/apis/": "Kubernetes ", 42 | "k8s.io/apiextensions-apiserver/": "Kubernetes ", 43 | "github.com/fluxcd/pkg/runtime/": "Runtime ", 44 | "github.com/fluxcd/pkg/apis/kustomize/": "Kustomize ", 45 | "github.com/fluxcd/pkg/apis/meta/": "Meta " 46 | }, 47 | "markdownDisabled": false 48 | } 49 | -------------------------------------------------------------------------------- /hack/api-docs/template/members.tpl: -------------------------------------------------------------------------------- 1 | {{ define "members" }} 2 | {{ range .Members }} 3 | {{ if not (hiddenMember .)}} 4 | 5 | 6 | {{ fieldName . }}
7 | 8 | {{ if linkForType .Type }} 9 | 10 | {{ typeDisplayName .Type }} 11 | 12 | {{ else }} 13 | {{ typeDisplayName .Type }} 14 | {{ end }} 15 | 16 | 17 | 18 | {{ if fieldEmbedded . }} 19 |

20 | (Members of {{ fieldName . }} are embedded into this type.) 21 |

22 | {{ end}} 23 | 24 | {{ if isOptionalMember .}} 25 | (Optional) 26 | {{ end }} 27 | 28 | {{ safe (renderComments .CommentLines) }} 29 | 30 | {{ if and (eq (.Type.Name.Name) "ObjectMeta") }} 31 | Refer to the Kubernetes API documentation for the fields of the 32 | metadata field. 33 | {{ end }} 34 | 35 | {{ if or (eq (fieldName .) "spec") }} 36 |
37 |
38 | 39 | {{ template "members" .Type }} 40 |
41 | {{ end }} 42 | 43 | 44 | {{ end }} 45 | {{ end }} 46 | {{ end }} 47 | -------------------------------------------------------------------------------- /hack/api-docs/template/pkg.tpl: -------------------------------------------------------------------------------- 1 | {{ define "packages" }} 2 |

CloudFormationStack API reference

3 | 4 | {{ with .packages}} 5 |

Packages:

6 | 13 | {{ end}} 14 | 15 | {{ range .packages }} 16 |

17 | {{- packageDisplayName . -}} 18 |

19 | 20 | {{ with (index .GoPackages 0 )}} 21 | {{ with .DocComments }} 22 | {{ safe (renderComments .) }} 23 | {{ end }} 24 | {{ end }} 25 | 26 | Resource Types: 27 | 28 | 37 | 38 | {{ range (visibleTypes (sortedTypes .Types))}} 39 | {{ template "type" . }} 40 | {{ end }} 41 | {{ end }} 42 | 43 |
44 |

This page was automatically generated with gen-crd-api-reference-docs

45 |
46 | {{ end }} 47 | -------------------------------------------------------------------------------- /hack/api-docs/template/type.tpl: -------------------------------------------------------------------------------- 1 | {{ define "type" }} 2 |

3 | {{- .Name.Name }} 4 | {{ if eq .Kind "Alias" }}({{.Underlying}} alias){{ end -}} 5 |

6 | 7 | {{ with (typeReferences .) }} 8 |

9 | (Appears on: 10 | {{- $prev := "" -}} 11 | {{- range . -}} 12 | {{- if $prev -}}, {{ end -}} 13 | {{ $prev = . }} 14 | {{ typeDisplayName . }} 15 | {{- end -}} 16 | ) 17 |

18 | {{ end }} 19 | 20 | {{ with .CommentLines }} 21 | {{ safe (renderComments .) }} 22 | {{ end }} 23 | 24 | {{ if .Members }} 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {{ if isExportedType . }} 36 | 37 | 40 | 43 | 44 | 45 | 49 | 52 | 53 | {{ end }} 54 | {{ template "members" . }} 55 | 56 |
FieldDescription
38 | apiVersion
39 | string
41 | {{ apiGroup . }} 42 |
46 | kind
47 | string 48 |
50 | {{ .Name.Name }} 51 |
57 |
58 |
59 | {{ end }} 60 | {{ end }} 61 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | -------------------------------------------------------------------------------- /internal/clients/clients.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | package clients 5 | 6 | import ( 7 | "io" 8 | 9 | "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/clients/types" 10 | ) 11 | 12 | type CloudFormationClient interface { 13 | // Stack methods 14 | CreateStack(stack *types.Stack) (changeSetArn string, err error) 15 | UpdateStack(stack *types.Stack) (changeSetArn string, err error) 16 | DescribeStack(stack *types.Stack) (*types.StackDescription, error) 17 | DeleteStack(stack *types.Stack) error 18 | ContinueStackRollback(stack *types.Stack) error 19 | 20 | // Change set methods 21 | ExecuteChangeSet(stack *types.Stack) error 22 | DeleteChangeSet(stack *types.Stack) error 23 | DescribeChangeSet(stack *types.Stack) (*types.ChangeSetDescription, error) 24 | } 25 | 26 | type S3Client interface { 27 | UploadTemplate(bucket, region, key string, data io.Reader) (string, error) 28 | } 29 | -------------------------------------------------------------------------------- /internal/clients/cloudformation/changeset.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | package cloudformation 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 14 | sdktypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 15 | "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/clients/types" 16 | ) 17 | 18 | const ( 19 | // The change set name will be formatted as "flux-". 20 | fmtChangeSetName = "flux-%d-%s" 21 | maxLengthChangeSetName = 128 22 | ) 23 | 24 | var ( 25 | // all except alphanumeric characters 26 | changeSetNameSpecialChars, _ = regexp.Compile("[^a-zA-Z0-9]+") 27 | ) 28 | 29 | type changeSet struct { 30 | name string 31 | stackName string 32 | region string 33 | csType sdktypes.ChangeSetType 34 | client changeSetAPI 35 | ctx context.Context 36 | } 37 | 38 | // GetChangeSetName generates a unique change set name using the generation number 39 | // (a specific version of the CloudFormationStack Spec contents) and the source 40 | // revision (such as the branch and commit ID for git sources). 41 | // 42 | // Examples: 43 | // 44 | // Git repository: main@sha1:132f4e719209eb10b9485302f8593fc0e680f4fc 45 | // Bucket: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 46 | // OCI repository: latest@sha256:3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de 47 | func GetChangeSetName(generation int64, sourceRevision string) string { 48 | name := fmt.Sprintf(fmtChangeSetName, generation, sourceRevision) 49 | name = changeSetNameSpecialChars.ReplaceAllString(name, "-") 50 | if len(name) <= maxLengthChangeSetName { 51 | return name 52 | } 53 | return name[:maxLengthChangeSetName] 54 | } 55 | 56 | // ExtractChangeSetName extracts the name of the change set from the change set ARN 57 | // Example: 58 | // arn:aws:cloudformation:us-west-2:123456789012:changeSet// -> name 59 | func ExtractChangeSetName(arn string) string { 60 | arnParts := strings.Split(arn, "/") 61 | if len(arnParts) < 2 { 62 | return "" 63 | } 64 | return arnParts[1] 65 | } 66 | 67 | func newCreateChangeSet(ctx context.Context, cfnClient changeSetAPI, region string, stackName string, generation int64, sourceRevision string) *changeSet { 68 | return &changeSet{ 69 | name: GetChangeSetName(generation, sourceRevision), 70 | stackName: stackName, 71 | region: region, 72 | csType: sdktypes.ChangeSetTypeCreate, 73 | client: cfnClient, 74 | ctx: ctx, 75 | } 76 | } 77 | 78 | func newUpdateChangeSet(ctx context.Context, cfnClient changeSetAPI, region string, stackName string, generation int64, sourceRevision string) *changeSet { 79 | return &changeSet{ 80 | name: GetChangeSetName(generation, sourceRevision), 81 | stackName: stackName, 82 | region: region, 83 | csType: sdktypes.ChangeSetTypeUpdate, 84 | client: cfnClient, 85 | ctx: ctx, 86 | } 87 | } 88 | 89 | func (cs *changeSet) String() string { 90 | return fmt.Sprintf("change set %s for stack %s", cs.name, cs.stackName) 91 | } 92 | 93 | // create creates a ChangeSet, waits until it's created, and returns the change set ARN on success. 94 | func (cs *changeSet) create(conf *types.StackConfig) (string, error) { 95 | input := &cloudformation.CreateChangeSetInput{ 96 | ChangeSetName: aws.String(cs.name), 97 | StackName: aws.String(cs.stackName), 98 | Description: aws.String("Managed by Flux"), 99 | ChangeSetType: cs.csType, 100 | TemplateURL: aws.String(conf.TemplateURL), 101 | Parameters: conf.Parameters, 102 | Tags: conf.Tags, 103 | IncludeNestedStacks: aws.Bool(true), 104 | Capabilities: []sdktypes.Capability{ 105 | sdktypes.CapabilityCapabilityIam, 106 | sdktypes.CapabilityCapabilityNamedIam, 107 | sdktypes.CapabilityCapabilityAutoExpand, 108 | }, 109 | } 110 | 111 | opts := func(opts *cloudformation.Options) { 112 | if cs.region != "" { 113 | opts.Region = cs.region 114 | } 115 | } 116 | 117 | out, err := cs.client.CreateChangeSet(cs.ctx, input, opts) 118 | if err != nil { 119 | return "", fmt.Errorf("create %s: %w", cs, err) 120 | } 121 | return *out.Id, nil 122 | } 123 | 124 | // describe collects all the changes and statuses that the change set will apply and returns them. 125 | func (cs *changeSet) describe() (*types.ChangeSetDescription, error) { 126 | var arn string 127 | var status sdktypes.ChangeSetStatus 128 | var executionStatus sdktypes.ExecutionStatus 129 | var statusReason string 130 | var changes []sdktypes.Change 131 | var nextToken *string 132 | for { 133 | out, err := cs.client.DescribeChangeSet(cs.ctx, &cloudformation.DescribeChangeSetInput{ 134 | ChangeSetName: aws.String(cs.name), 135 | StackName: aws.String(cs.stackName), 136 | NextToken: nextToken, 137 | }, func(opts *cloudformation.Options) { 138 | if cs.region != "" { 139 | opts.Region = cs.region 140 | } 141 | }) 142 | if err != nil { 143 | return nil, fmt.Errorf("describe %s: %w", cs, err) 144 | } 145 | arn = *out.ChangeSetId 146 | status = out.Status 147 | executionStatus = out.ExecutionStatus 148 | if out.StatusReason != nil { 149 | statusReason = *out.StatusReason 150 | } 151 | changes = append(changes, out.Changes...) 152 | nextToken = out.NextToken 153 | 154 | if nextToken == nil { // no more results left 155 | break 156 | } 157 | } 158 | return &types.ChangeSetDescription{ 159 | Arn: arn, 160 | Status: status, 161 | ExecutionStatus: executionStatus, 162 | StatusReason: statusReason, 163 | Changes: changes, 164 | }, nil 165 | } 166 | 167 | // execute executes a created change set. 168 | func (cs *changeSet) execute() error { 169 | _, err := cs.client.ExecuteChangeSet(cs.ctx, &cloudformation.ExecuteChangeSetInput{ 170 | ChangeSetName: aws.String(cs.name), 171 | StackName: aws.String(cs.stackName), 172 | }, func(opts *cloudformation.Options) { 173 | if cs.region != "" { 174 | opts.Region = cs.region 175 | } 176 | }) 177 | if err != nil { 178 | return fmt.Errorf("execute %s: %w", cs, err) 179 | } 180 | return nil 181 | } 182 | 183 | // delete removes the change set. 184 | func (cs *changeSet) delete() error { 185 | _, err := cs.client.DeleteChangeSet(cs.ctx, &cloudformation.DeleteChangeSetInput{ 186 | ChangeSetName: aws.String(cs.name), 187 | StackName: aws.String(cs.stackName), 188 | }, func(opts *cloudformation.Options) { 189 | if cs.region != "" { 190 | opts.Region = cs.region 191 | } 192 | }) 193 | if err != nil { 194 | return fmt.Errorf("delete %s: %w", cs, err) 195 | } 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /internal/clients/cloudformation/cloudformation.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Package cloudformation provides a client to make API requests to AWS CloudFormation. 5 | package cloudformation 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" 13 | "github.com/aws/aws-sdk-go-v2/config" 14 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 15 | sdktypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 16 | "github.com/aws/smithy-go/middleware" 17 | "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/clients" 18 | "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/clients/types" 19 | ) 20 | 21 | // CloudFormation represents a client to make requests to AWS CloudFormation. 22 | type CloudFormation struct { 23 | client 24 | ctx context.Context 25 | } 26 | 27 | // New creates a new CloudFormation client. 28 | func New(ctx context.Context, region string) (clients.CloudFormationClient, error) { 29 | cfg, err := config.LoadDefaultConfig( 30 | ctx, 31 | config.WithAPIOptions([]func(*middleware.Stack) error{ 32 | awsmiddleware.AddUserAgentKey("cfn-flux-controller"), 33 | }), 34 | config.WithRegion(region), 35 | ) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return &CloudFormation{ 41 | client: cloudformation.NewFromConfig(cfg), 42 | ctx: ctx, 43 | }, nil 44 | } 45 | 46 | // For passing a mock client in tests 47 | func NewWithClient(ctx context.Context, client client) *CloudFormation { 48 | return &CloudFormation{ 49 | client: client, 50 | ctx: ctx, 51 | } 52 | } 53 | 54 | // Describe returns a description of an existing stack. 55 | // If the stack does not exist, returns ErrStackNotFound. 56 | func (c *CloudFormation) DescribeStack(stack *types.Stack) (*types.StackDescription, error) { 57 | out, err := c.client.DescribeStacks(c.ctx, &cloudformation.DescribeStacksInput{ 58 | StackName: aws.String(stack.Name), 59 | }, func(opts *cloudformation.Options) { 60 | if stack.Region != "" { 61 | opts.Region = stack.Region 62 | } 63 | }) 64 | if err != nil { 65 | if stackDoesNotExist(err) { 66 | return nil, &ErrStackNotFound{name: stack.Name} 67 | } 68 | return nil, fmt.Errorf("describe stack %s: %w", stack.Name, err) 69 | } 70 | if len(out.Stacks) == 0 { 71 | return nil, &ErrStackNotFound{name: stack.Name} 72 | } 73 | if out.Stacks[0].StackStatus == sdktypes.StackStatusReviewInProgress { 74 | // there is a creation change set for the stack, but it has not been executed, 75 | // so the stack has not been created yet 76 | return nil, &ErrStackNotFound{name: stack.Name} 77 | } 78 | if out.Stacks[0].StackStatus == sdktypes.StackStatusDeleteComplete { 79 | // the stack was previously successfully deleted 80 | return nil, &ErrStackNotFound{name: stack.Name} 81 | } 82 | descr := types.StackDescription(out.Stacks[0]) 83 | return &descr, nil 84 | } 85 | 86 | // DescribeChangeSet gathers and returns all changes for the stack's current change set. 87 | // If the stack or changeset does not exist, returns ErrChangeSetNotFound. 88 | func (c *CloudFormation) DescribeChangeSet(stack *types.Stack) (*types.ChangeSetDescription, error) { 89 | var changeSetName string 90 | if stack.ChangeSetArn != "" { 91 | changeSetName = stack.ChangeSetArn 92 | } else { 93 | changeSetName = GetChangeSetName(stack.Generation, stack.SourceRevision) 94 | } 95 | cs := &changeSet{name: changeSetName, stackName: stack.Name, region: stack.Region, client: c.client, ctx: c.ctx} 96 | 97 | out, err := cs.describe() 98 | if err != nil { 99 | if changeSetDoesNotExist(err) || stackDoesNotExist(err) { 100 | return nil, &ErrChangeSetNotFound{name: changeSetName, stackName: stack.Name} 101 | } 102 | return nil, err 103 | } 104 | 105 | stack.ChangeSetArn = out.Arn 106 | 107 | if out.IsDeleted() { 108 | return nil, &ErrChangeSetNotFound{name: changeSetName, stackName: stack.Name} 109 | } 110 | 111 | // The change set was empty. The status reason will be like 112 | // "The submitted information didn't contain changes. Submit different information to create a change set." 113 | if out.IsEmpty() { 114 | return nil, &ErrChangeSetEmpty{name: changeSetName, stackName: stack.Name, Arn: out.Arn} 115 | } 116 | 117 | return out, nil 118 | } 119 | 120 | // CreateStack begins the process of deploying a new CloudFormation stack by creating a change set. 121 | // The change set must be executed when it is successfully created. 122 | func (c *CloudFormation) CreateStack(stack *types.Stack) (changeSetArn string, err error) { 123 | cs := newCreateChangeSet(c.ctx, c.client, stack.Region, stack.Name, stack.Generation, stack.SourceRevision) 124 | arn, err := cs.create(stack.StackConfig) 125 | if err != nil { 126 | return "", err 127 | } 128 | stack.ChangeSetArn = arn 129 | return arn, nil 130 | } 131 | 132 | // UpdateStack begins the process of updating an existing CloudFormation stack with new configuration 133 | // by creating a change set. 134 | // The change set must be executed when it is successfully created. 135 | func (c *CloudFormation) UpdateStack(stack *types.Stack) (changeSetArn string, err error) { 136 | cs := newUpdateChangeSet(c.ctx, c.client, stack.Region, stack.Name, stack.Generation, stack.SourceRevision) 137 | arn, err := cs.create(stack.StackConfig) 138 | if err != nil { 139 | return "", err 140 | } 141 | stack.ChangeSetArn = arn 142 | return arn, nil 143 | } 144 | 145 | // ExecutChangeSet starts the execution of the stack's current change set. 146 | // If the stack or changeset does not exist, returns ErrChangeSetNotFound. 147 | func (c *CloudFormation) ExecuteChangeSet(stack *types.Stack) error { 148 | cs := &changeSet{name: stack.ChangeSetArn, stackName: stack.Name, region: stack.Region, client: c.client, ctx: c.ctx} 149 | return cs.execute() 150 | } 151 | 152 | // Delete removes an existing CloudFormation stack. 153 | // If the stack doesn't exist then do nothing. 154 | func (c *CloudFormation) DeleteStack(stack *types.Stack) error { 155 | _, err := c.client.DeleteStack(c.ctx, &cloudformation.DeleteStackInput{ 156 | StackName: aws.String(stack.Name), 157 | }, func(opts *cloudformation.Options) { 158 | if stack.Region != "" { 159 | opts.Region = stack.Region 160 | } 161 | }) 162 | if err != nil { 163 | if !stackDoesNotExist(err) { 164 | return fmt.Errorf("delete stack %s: %w", stack.Name, err) 165 | } 166 | // Move on if stack is already deleted. 167 | } 168 | return nil 169 | } 170 | 171 | // Delete removes an existing CloudFormation change set. 172 | // If the change set doesn't exist then do nothing. 173 | func (c *CloudFormation) DeleteChangeSet(stack *types.Stack) error { 174 | cs := &changeSet{name: stack.ChangeSetArn, stackName: stack.Name, region: stack.Region, client: c.client, ctx: c.ctx} 175 | if err := cs.delete(); err != nil { 176 | if !changeSetDoesNotExist(err) && !stackDoesNotExist(err) { 177 | return err 178 | } 179 | // Move on if change set is already deleted. 180 | } 181 | return nil 182 | } 183 | 184 | // ContinueStackRollback attempts to continue an Update rollback for an existing CloudFormation stack. 185 | func (c *CloudFormation) ContinueStackRollback(stack *types.Stack) error { 186 | _, err := c.client.ContinueUpdateRollback(c.ctx, &cloudformation.ContinueUpdateRollbackInput{ 187 | StackName: aws.String(stack.Name), 188 | }, func(opts *cloudformation.Options) { 189 | if stack.Region != "" { 190 | opts.Region = stack.Region 191 | } 192 | }) 193 | return err 194 | } 195 | -------------------------------------------------------------------------------- /internal/clients/cloudformation/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | package cloudformation 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/aws/smithy-go" 12 | ) 13 | 14 | // ErrChangeSetEmpty occurs when the change set does not contain any new or updated resources. 15 | type ErrChangeSetEmpty struct { 16 | name string 17 | stackName string 18 | Arn string 19 | } 20 | 21 | func (e *ErrChangeSetEmpty) Error() string { 22 | return fmt.Sprintf("change set with name %s for stack %s has no changes", e.name, e.stackName) 23 | } 24 | 25 | // ErrStackNotFound occurs when a CloudFormation stack does not exist. 26 | type ErrStackNotFound struct { 27 | name string 28 | } 29 | 30 | func (e *ErrStackNotFound) Error() string { 31 | return fmt.Sprintf("stack named %s cannot be found", e.name) 32 | } 33 | 34 | // ErrChangeSetNotFound occurs when a CloudFormation changeset does not exist. 35 | type ErrChangeSetNotFound struct { 36 | name string 37 | stackName string 38 | } 39 | 40 | func (e *ErrChangeSetNotFound) Error() string { 41 | return fmt.Sprintf("change set with name %s for stack %s cannot be found", e.name, e.stackName) 42 | } 43 | 44 | // stackDoesNotExist returns true if the underlying error is a stack doesn't exist. 45 | func stackDoesNotExist(err error) bool { 46 | var ae smithy.APIError 47 | if errors.As(err, &ae) { 48 | switch ae.ErrorCode() { 49 | case "ValidationError": 50 | // A ValidationError occurs if we describe a stack which doesn't exist. 51 | if strings.Contains(ae.ErrorMessage(), "does not exist") { 52 | return true 53 | } 54 | } 55 | } 56 | return false 57 | } 58 | 59 | // changeSetDoesNotExist returns true if the underlying error is a change set doesn't exist. 60 | func changeSetDoesNotExist(err error) bool { 61 | var ae smithy.APIError 62 | if errors.As(err, &ae) { 63 | switch ae.ErrorCode() { 64 | case "ChangeSetNotFound": 65 | return true 66 | } 67 | } 68 | return false 69 | } 70 | -------------------------------------------------------------------------------- /internal/clients/cloudformation/sdk_interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | package cloudformation 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 10 | ) 11 | 12 | type changeSetAPI interface { 13 | CreateChangeSet(ctx context.Context, params *cloudformation.CreateChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.CreateChangeSetOutput, error) 14 | DescribeChangeSet(ctx context.Context, params *cloudformation.DescribeChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeChangeSetOutput, error) 15 | ExecuteChangeSet(ctx context.Context, params *cloudformation.ExecuteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ExecuteChangeSetOutput, error) 16 | DeleteChangeSet(ctx context.Context, params *cloudformation.DeleteChangeSetInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteChangeSetOutput, error) 17 | } 18 | 19 | type client interface { 20 | changeSetAPI 21 | 22 | DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) 23 | DeleteStack(ctx context.Context, params *cloudformation.DeleteStackInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) 24 | ContinueUpdateRollback(ctx context.Context, params *cloudformation.ContinueUpdateRollbackInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ContinueUpdateRollbackOutput, error) 25 | } 26 | -------------------------------------------------------------------------------- /internal/clients/mocks/mock_clients.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./internal/clients/clients.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | io "io" 9 | reflect "reflect" 10 | 11 | types "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/clients/types" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockCloudFormationClient is a mock of CloudFormationClient interface. 16 | type MockCloudFormationClient struct { 17 | ctrl *gomock.Controller 18 | recorder *MockCloudFormationClientMockRecorder 19 | } 20 | 21 | // MockCloudFormationClientMockRecorder is the mock recorder for MockCloudFormationClient. 22 | type MockCloudFormationClientMockRecorder struct { 23 | mock *MockCloudFormationClient 24 | } 25 | 26 | // NewMockCloudFormationClient creates a new mock instance. 27 | func NewMockCloudFormationClient(ctrl *gomock.Controller) *MockCloudFormationClient { 28 | mock := &MockCloudFormationClient{ctrl: ctrl} 29 | mock.recorder = &MockCloudFormationClientMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockCloudFormationClient) EXPECT() *MockCloudFormationClientMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // ContinueStackRollback mocks base method. 39 | func (m *MockCloudFormationClient) ContinueStackRollback(stack *types.Stack) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "ContinueStackRollback", stack) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // ContinueStackRollback indicates an expected call of ContinueStackRollback. 47 | func (mr *MockCloudFormationClientMockRecorder) ContinueStackRollback(stack interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContinueStackRollback", reflect.TypeOf((*MockCloudFormationClient)(nil).ContinueStackRollback), stack) 50 | } 51 | 52 | // CreateStack mocks base method. 53 | func (m *MockCloudFormationClient) CreateStack(stack *types.Stack) (string, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "CreateStack", stack) 56 | ret0, _ := ret[0].(string) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // CreateStack indicates an expected call of CreateStack. 62 | func (mr *MockCloudFormationClientMockRecorder) CreateStack(stack interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockCloudFormationClient)(nil).CreateStack), stack) 65 | } 66 | 67 | // DeleteChangeSet mocks base method. 68 | func (m *MockCloudFormationClient) DeleteChangeSet(stack *types.Stack) error { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "DeleteChangeSet", stack) 71 | ret0, _ := ret[0].(error) 72 | return ret0 73 | } 74 | 75 | // DeleteChangeSet indicates an expected call of DeleteChangeSet. 76 | func (mr *MockCloudFormationClientMockRecorder) DeleteChangeSet(stack interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChangeSet", reflect.TypeOf((*MockCloudFormationClient)(nil).DeleteChangeSet), stack) 79 | } 80 | 81 | // DeleteStack mocks base method. 82 | func (m *MockCloudFormationClient) DeleteStack(stack *types.Stack) error { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "DeleteStack", stack) 85 | ret0, _ := ret[0].(error) 86 | return ret0 87 | } 88 | 89 | // DeleteStack indicates an expected call of DeleteStack. 90 | func (mr *MockCloudFormationClientMockRecorder) DeleteStack(stack interface{}) *gomock.Call { 91 | mr.mock.ctrl.T.Helper() 92 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockCloudFormationClient)(nil).DeleteStack), stack) 93 | } 94 | 95 | // DescribeChangeSet mocks base method. 96 | func (m *MockCloudFormationClient) DescribeChangeSet(stack *types.Stack) (*types.ChangeSetDescription, error) { 97 | m.ctrl.T.Helper() 98 | ret := m.ctrl.Call(m, "DescribeChangeSet", stack) 99 | ret0, _ := ret[0].(*types.ChangeSetDescription) 100 | ret1, _ := ret[1].(error) 101 | return ret0, ret1 102 | } 103 | 104 | // DescribeChangeSet indicates an expected call of DescribeChangeSet. 105 | func (mr *MockCloudFormationClientMockRecorder) DescribeChangeSet(stack interface{}) *gomock.Call { 106 | mr.mock.ctrl.T.Helper() 107 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeChangeSet", reflect.TypeOf((*MockCloudFormationClient)(nil).DescribeChangeSet), stack) 108 | } 109 | 110 | // DescribeStack mocks base method. 111 | func (m *MockCloudFormationClient) DescribeStack(stack *types.Stack) (*types.StackDescription, error) { 112 | m.ctrl.T.Helper() 113 | ret := m.ctrl.Call(m, "DescribeStack", stack) 114 | ret0, _ := ret[0].(*types.StackDescription) 115 | ret1, _ := ret[1].(error) 116 | return ret0, ret1 117 | } 118 | 119 | // DescribeStack indicates an expected call of DescribeStack. 120 | func (mr *MockCloudFormationClientMockRecorder) DescribeStack(stack interface{}) *gomock.Call { 121 | mr.mock.ctrl.T.Helper() 122 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStack", reflect.TypeOf((*MockCloudFormationClient)(nil).DescribeStack), stack) 123 | } 124 | 125 | // ExecuteChangeSet mocks base method. 126 | func (m *MockCloudFormationClient) ExecuteChangeSet(stack *types.Stack) error { 127 | m.ctrl.T.Helper() 128 | ret := m.ctrl.Call(m, "ExecuteChangeSet", stack) 129 | ret0, _ := ret[0].(error) 130 | return ret0 131 | } 132 | 133 | // ExecuteChangeSet indicates an expected call of ExecuteChangeSet. 134 | func (mr *MockCloudFormationClientMockRecorder) ExecuteChangeSet(stack interface{}) *gomock.Call { 135 | mr.mock.ctrl.T.Helper() 136 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteChangeSet", reflect.TypeOf((*MockCloudFormationClient)(nil).ExecuteChangeSet), stack) 137 | } 138 | 139 | // UpdateStack mocks base method. 140 | func (m *MockCloudFormationClient) UpdateStack(stack *types.Stack) (string, error) { 141 | m.ctrl.T.Helper() 142 | ret := m.ctrl.Call(m, "UpdateStack", stack) 143 | ret0, _ := ret[0].(string) 144 | ret1, _ := ret[1].(error) 145 | return ret0, ret1 146 | } 147 | 148 | // UpdateStack indicates an expected call of UpdateStack. 149 | func (mr *MockCloudFormationClientMockRecorder) UpdateStack(stack interface{}) *gomock.Call { 150 | mr.mock.ctrl.T.Helper() 151 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStack", reflect.TypeOf((*MockCloudFormationClient)(nil).UpdateStack), stack) 152 | } 153 | 154 | // MockS3Client is a mock of S3Client interface. 155 | type MockS3Client struct { 156 | ctrl *gomock.Controller 157 | recorder *MockS3ClientMockRecorder 158 | } 159 | 160 | // MockS3ClientMockRecorder is the mock recorder for MockS3Client. 161 | type MockS3ClientMockRecorder struct { 162 | mock *MockS3Client 163 | } 164 | 165 | // NewMockS3Client creates a new mock instance. 166 | func NewMockS3Client(ctrl *gomock.Controller) *MockS3Client { 167 | mock := &MockS3Client{ctrl: ctrl} 168 | mock.recorder = &MockS3ClientMockRecorder{mock} 169 | return mock 170 | } 171 | 172 | // EXPECT returns an object that allows the caller to indicate expected use. 173 | func (m *MockS3Client) EXPECT() *MockS3ClientMockRecorder { 174 | return m.recorder 175 | } 176 | 177 | // UploadTemplate mocks base method. 178 | func (m *MockS3Client) UploadTemplate(bucket, region, key string, data io.Reader) (string, error) { 179 | m.ctrl.T.Helper() 180 | ret := m.ctrl.Call(m, "UploadTemplate", bucket, region, key, data) 181 | ret0, _ := ret[0].(string) 182 | ret1, _ := ret[1].(error) 183 | return ret0, ret1 184 | } 185 | 186 | // UploadTemplate indicates an expected call of UploadTemplate. 187 | func (mr *MockS3ClientMockRecorder) UploadTemplate(bucket, region, key, data interface{}) *gomock.Call { 188 | mr.mock.ctrl.T.Helper() 189 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadTemplate", reflect.TypeOf((*MockS3Client)(nil).UploadTemplate), bucket, region, key, data) 190 | } 191 | -------------------------------------------------------------------------------- /internal/clients/s3/mocks/mock_sdk.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./internal/clients/s3/sdk_interfaces.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 12 | s3 "github.com/aws/aws-sdk-go-v2/service/s3" 13 | sts "github.com/aws/aws-sdk-go-v2/service/sts" 14 | gomock "github.com/golang/mock/gomock" 15 | ) 16 | 17 | // Mocks3ManagerAPI is a mock of s3ManagerAPI interface. 18 | type Mocks3ManagerAPI struct { 19 | ctrl *gomock.Controller 20 | recorder *Mocks3ManagerAPIMockRecorder 21 | } 22 | 23 | // Mocks3ManagerAPIMockRecorder is the mock recorder for Mocks3ManagerAPI. 24 | type Mocks3ManagerAPIMockRecorder struct { 25 | mock *Mocks3ManagerAPI 26 | } 27 | 28 | // NewMocks3ManagerAPI creates a new mock instance. 29 | func NewMocks3ManagerAPI(ctrl *gomock.Controller) *Mocks3ManagerAPI { 30 | mock := &Mocks3ManagerAPI{ctrl: ctrl} 31 | mock.recorder = &Mocks3ManagerAPIMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *Mocks3ManagerAPI) EXPECT() *Mocks3ManagerAPIMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // Upload mocks base method. 41 | func (m *Mocks3ManagerAPI) Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) { 42 | m.ctrl.T.Helper() 43 | varargs := []interface{}{ctx, input} 44 | for _, a := range opts { 45 | varargs = append(varargs, a) 46 | } 47 | ret := m.ctrl.Call(m, "Upload", varargs...) 48 | ret0, _ := ret[0].(*manager.UploadOutput) 49 | ret1, _ := ret[1].(error) 50 | return ret0, ret1 51 | } 52 | 53 | // Upload indicates an expected call of Upload. 54 | func (mr *Mocks3ManagerAPIMockRecorder) Upload(ctx, input interface{}, opts ...interface{}) *gomock.Call { 55 | mr.mock.ctrl.T.Helper() 56 | varargs := append([]interface{}{ctx, input}, opts...) 57 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*Mocks3ManagerAPI)(nil).Upload), varargs...) 58 | } 59 | 60 | // Mocks3API is a mock of s3API interface. 61 | type Mocks3API struct { 62 | ctrl *gomock.Controller 63 | recorder *Mocks3APIMockRecorder 64 | } 65 | 66 | // Mocks3APIMockRecorder is the mock recorder for Mocks3API. 67 | type Mocks3APIMockRecorder struct { 68 | mock *Mocks3API 69 | } 70 | 71 | // NewMocks3API creates a new mock instance. 72 | func NewMocks3API(ctrl *gomock.Controller) *Mocks3API { 73 | mock := &Mocks3API{ctrl: ctrl} 74 | mock.recorder = &Mocks3APIMockRecorder{mock} 75 | return mock 76 | } 77 | 78 | // EXPECT returns an object that allows the caller to indicate expected use. 79 | func (m *Mocks3API) EXPECT() *Mocks3APIMockRecorder { 80 | return m.recorder 81 | } 82 | 83 | // MockstsAPI is a mock of stsAPI interface. 84 | type MockstsAPI struct { 85 | ctrl *gomock.Controller 86 | recorder *MockstsAPIMockRecorder 87 | } 88 | 89 | // MockstsAPIMockRecorder is the mock recorder for MockstsAPI. 90 | type MockstsAPIMockRecorder struct { 91 | mock *MockstsAPI 92 | } 93 | 94 | // NewMockstsAPI creates a new mock instance. 95 | func NewMockstsAPI(ctrl *gomock.Controller) *MockstsAPI { 96 | mock := &MockstsAPI{ctrl: ctrl} 97 | mock.recorder = &MockstsAPIMockRecorder{mock} 98 | return mock 99 | } 100 | 101 | // EXPECT returns an object that allows the caller to indicate expected use. 102 | func (m *MockstsAPI) EXPECT() *MockstsAPIMockRecorder { 103 | return m.recorder 104 | } 105 | 106 | // GetCallerIdentity mocks base method. 107 | func (m *MockstsAPI) GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { 108 | m.ctrl.T.Helper() 109 | varargs := []interface{}{ctx, params} 110 | for _, a := range optFns { 111 | varargs = append(varargs, a) 112 | } 113 | ret := m.ctrl.Call(m, "GetCallerIdentity", varargs...) 114 | ret0, _ := ret[0].(*sts.GetCallerIdentityOutput) 115 | ret1, _ := ret[1].(error) 116 | return ret0, ret1 117 | } 118 | 119 | // GetCallerIdentity indicates an expected call of GetCallerIdentity. 120 | func (mr *MockstsAPIMockRecorder) GetCallerIdentity(ctx, params interface{}, optFns ...interface{}) *gomock.Call { 121 | mr.mock.ctrl.T.Helper() 122 | varargs := append([]interface{}{ctx, params}, optFns...) 123 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerIdentity", reflect.TypeOf((*MockstsAPI)(nil).GetCallerIdentity), varargs...) 124 | } 125 | -------------------------------------------------------------------------------- /internal/clients/s3/s3.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Package s3 provides a client to make API requests to Amazon S3. 5 | package s3 6 | 7 | import ( 8 | "context" 9 | "io" 10 | 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" 13 | "github.com/aws/aws-sdk-go-v2/config" 14 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 15 | "github.com/aws/aws-sdk-go-v2/service/s3" 16 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 17 | "github.com/aws/aws-sdk-go-v2/service/sts" 18 | "github.com/aws/smithy-go/middleware" 19 | "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/clients" 20 | ) 21 | 22 | const ( 23 | // Error codes. 24 | errCodeNotFound = "NotFound" 25 | ) 26 | 27 | // S3 wraps an Amazon S3 client. 28 | type S3 struct { 29 | manager s3ManagerAPI 30 | client s3API 31 | stsClient stsAPI 32 | ctx context.Context 33 | } 34 | 35 | // New returns an S3 client. 36 | func New(ctx context.Context, region string) (clients.S3Client, error) { 37 | cfg, err := config.LoadDefaultConfig( 38 | ctx, 39 | config.WithAPIOptions([]func(*middleware.Stack) error{ 40 | awsmiddleware.AddUserAgentKey("cfn-flux-controller"), 41 | }), 42 | config.WithRegion(region), 43 | ) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | client := s3.NewFromConfig(cfg) 49 | stsClient := sts.NewFromConfig(cfg) 50 | 51 | return &S3{ 52 | client: client, 53 | manager: manager.NewUploader(client), 54 | stsClient: stsClient, 55 | ctx: ctx, 56 | }, nil 57 | } 58 | 59 | // Upload uploads a template file to an S3 bucket under the specified key. 60 | // Returns an object URL that can be passed directly to CloudFormation 61 | func (s *S3) UploadTemplate(bucket, region, key string, data io.Reader) (string, error) { 62 | url, err := s.upload(bucket, region, key, data) 63 | if err != nil { 64 | return "", err 65 | } 66 | return url, nil 67 | } 68 | 69 | func (s *S3) upload(bucket, region, key string, buf io.Reader) (string, error) { 70 | // The expected bucket owner is the current caller 71 | identityResp, err := s.stsClient.GetCallerIdentity(s.ctx, &sts.GetCallerIdentityInput{}, func(opts *sts.Options) { 72 | if region != "" { 73 | opts.Region = region 74 | } 75 | }) 76 | if err != nil { 77 | return "", err 78 | } 79 | expectedBucketOwner := identityResp.Account 80 | 81 | in := &s3.PutObjectInput{ 82 | Body: buf, 83 | Bucket: aws.String(bucket), 84 | Key: aws.String(key), 85 | // Per s3's recommendation, the bucket owner, in addition to the 86 | // object owner, is granted full control. 87 | // https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html 88 | ACL: types.ObjectCannedACLBucketOwnerFullControl, 89 | ExpectedBucketOwner: expectedBucketOwner, 90 | } 91 | var opts []func(*manager.Uploader) 92 | if region != "" { 93 | opts = append(opts, manager.WithUploaderRequestOptions( 94 | func(opts *s3.Options) { 95 | if region != "" { 96 | opts.Region = region 97 | } 98 | }, 99 | )) 100 | } 101 | resp, err := s.manager.Upload(s.ctx, in, opts...) 102 | if err != nil { 103 | return "", err 104 | } 105 | return resp.Location, nil 106 | } 107 | -------------------------------------------------------------------------------- /internal/clients/s3/s3_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | package s3 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/aws/aws-sdk-go-v2/aws" 14 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 15 | "github.com/aws/aws-sdk-go-v2/service/s3" 16 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 17 | "github.com/aws/aws-sdk-go-v2/service/sts" 18 | "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/clients/s3/mocks" 19 | "github.com/golang/mock/gomock" 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | const ( 24 | awsAccountId = "123456789012" 25 | templateBody = "hello" 26 | mockBucket = "mockBucket" 27 | mockRegion = "mockRegion" 28 | mockObjectKey = "mockFileName" 29 | ) 30 | 31 | func TestS3_Upload(t *testing.T) { 32 | testCases := map[string]struct { 33 | mockS3ManagerClient func(m *mocks.Mocks3ManagerAPI) 34 | mockStsClient func(m *mocks.MockstsAPI) 35 | 36 | wantedURL string 37 | wantError error 38 | }{ 39 | "return error if STS call fails": { 40 | mockStsClient: func(m *mocks.MockstsAPI) { 41 | m.EXPECT().GetCallerIdentity( 42 | gomock.Any(), 43 | gomock.Any(), 44 | gomock.Any(), 45 | ).Return(nil, errors.New("some STS error")) 46 | }, 47 | mockS3ManagerClient: func(m *mocks.Mocks3ManagerAPI) { 48 | m.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Times(0) 49 | }, 50 | wantError: fmt.Errorf("some STS error"), 51 | }, 52 | "return error if upload fails": { 53 | mockS3ManagerClient: func(m *mocks.Mocks3ManagerAPI) { 54 | expectedIn := &s3.PutObjectInput{ 55 | Body: strings.NewReader(templateBody), 56 | Bucket: aws.String(mockBucket), 57 | Key: aws.String(mockObjectKey), 58 | ACL: types.ObjectCannedACLBucketOwnerFullControl, 59 | ExpectedBucketOwner: aws.String(awsAccountId), 60 | } 61 | m.EXPECT().Upload( 62 | gomock.Any(), 63 | gomock.Eq(expectedIn), 64 | gomock.Any(), 65 | ).Return(nil, errors.New("some upload error")) 66 | }, 67 | mockStsClient: func(m *mocks.MockstsAPI) { 68 | m.EXPECT().GetCallerIdentity( 69 | gomock.Any(), 70 | gomock.Any(), 71 | gomock.Any(), 72 | ).Return(&sts.GetCallerIdentityOutput{ 73 | Account: aws.String(awsAccountId), 74 | }, nil) 75 | }, 76 | wantError: fmt.Errorf("some upload error"), 77 | }, 78 | "should upload to the s3 bucket": { 79 | mockS3ManagerClient: func(m *mocks.Mocks3ManagerAPI) { 80 | expectedIn := &s3.PutObjectInput{ 81 | Body: strings.NewReader(templateBody), 82 | Bucket: aws.String(mockBucket), 83 | Key: aws.String(mockObjectKey), 84 | ACL: types.ObjectCannedACLBucketOwnerFullControl, 85 | ExpectedBucketOwner: aws.String(awsAccountId), 86 | } 87 | m.EXPECT().Upload( 88 | gomock.Any(), 89 | gomock.Eq(expectedIn), 90 | gomock.Any(), 91 | ).Return(&manager.UploadOutput{ 92 | Location: "mockURL", 93 | }, nil) 94 | }, 95 | mockStsClient: func(m *mocks.MockstsAPI) { 96 | m.EXPECT().GetCallerIdentity( 97 | gomock.Any(), 98 | gomock.Any(), 99 | gomock.Any(), 100 | ).Return(&sts.GetCallerIdentityOutput{ 101 | Account: aws.String(awsAccountId), 102 | }, nil) 103 | }, 104 | wantedURL: "mockURL", 105 | }, 106 | } 107 | 108 | for name, tc := range testCases { 109 | t.Run(name, func(t *testing.T) { 110 | ctrl, ctx := gomock.WithContext(context.Background(), t) 111 | defer ctrl.Finish() 112 | 113 | mockS3ManagerClient := mocks.NewMocks3ManagerAPI(ctrl) 114 | tc.mockS3ManagerClient(mockS3ManagerClient) 115 | 116 | mockStsClient := mocks.NewMockstsAPI(ctrl) 117 | tc.mockStsClient(mockStsClient) 118 | 119 | service := S3{ 120 | manager: mockS3ManagerClient, 121 | stsClient: mockStsClient, 122 | ctx: ctx, 123 | } 124 | 125 | gotURL, gotErr := service.UploadTemplate(mockBucket, mockRegion, mockObjectKey, strings.NewReader(templateBody)) 126 | 127 | if gotErr != nil { 128 | require.EqualError(t, gotErr, tc.wantError.Error()) 129 | } else { 130 | require.Equal(t, gotErr, nil) 131 | require.Equal(t, gotURL, tc.wantedURL) 132 | } 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /internal/clients/s3/sdk_interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | package s3 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 10 | "github.com/aws/aws-sdk-go-v2/service/s3" 11 | "github.com/aws/aws-sdk-go-v2/service/sts" 12 | ) 13 | 14 | type s3ManagerAPI interface { 15 | Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) 16 | } 17 | 18 | type s3API interface { 19 | } 20 | 21 | type stsAPI interface { 22 | GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) 23 | } 24 | -------------------------------------------------------------------------------- /internal/clients/types/changeset.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | package types 5 | 6 | import ( 7 | "strings" 8 | 9 | sdktypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 10 | ) 11 | 12 | // ChangeSetDescription is the output of the DescribeChangeSet action. 13 | type ChangeSetDescription struct { 14 | Arn string 15 | Status sdktypes.ChangeSetStatus 16 | ExecutionStatus sdktypes.ExecutionStatus 17 | StatusReason string 18 | Changes []sdktypes.Change 19 | } 20 | 21 | const ( 22 | // Status reasons that can occur if the change set execution status is "FAILED". 23 | noChangesReason = "NO_CHANGES_REASON" 24 | noUpdatesReason = "NO_UPDATES_REASON" 25 | ) 26 | 27 | var ( 28 | inProgressChangeSetStatuses = []sdktypes.ChangeSetStatus{ 29 | sdktypes.ChangeSetStatusCreateInProgress, 30 | sdktypes.ChangeSetStatusCreatePending, 31 | sdktypes.ChangeSetStatusDeleteInProgress, 32 | sdktypes.ChangeSetStatusDeletePending, 33 | } 34 | 35 | failedChangeSetStatuses = []sdktypes.ChangeSetStatus{ 36 | sdktypes.ChangeSetStatusDeleteFailed, 37 | sdktypes.ChangeSetStatusFailed, 38 | } 39 | 40 | inProgressChangeSetExecutionStatuses = []sdktypes.ExecutionStatus{ 41 | sdktypes.ExecutionStatusExecuteInProgress, 42 | sdktypes.ExecutionStatusUnavailable, 43 | } 44 | 45 | failedChangeSetExecutionStatuses = []sdktypes.ExecutionStatus{ 46 | sdktypes.ExecutionStatusExecuteFailed, 47 | sdktypes.ExecutionStatusObsolete, 48 | } 49 | ) 50 | 51 | func (d *ChangeSetDescription) IsEmpty() bool { 52 | return (len(d.Changes) == 0 && strings.Contains(d.StatusReason, "didn't contain changes")) || 53 | d.StatusReason == noChangesReason || 54 | d.StatusReason == noUpdatesReason 55 | } 56 | 57 | func (d *ChangeSetDescription) IsDeleted() bool { 58 | return d.Status == sdktypes.ChangeSetStatusDeleteComplete 59 | } 60 | 61 | func (d *ChangeSetDescription) IsCreated() bool { 62 | return d.Status == sdktypes.ChangeSetStatusCreateComplete 63 | } 64 | 65 | func (d *ChangeSetDescription) InProgress() bool { 66 | return changesetStatusListContains(d.Status, inProgressChangeSetStatuses) || 67 | changesetExecutionStatusListContains(d.ExecutionStatus, inProgressChangeSetExecutionStatuses) 68 | } 69 | 70 | func (d *ChangeSetDescription) IsFailed() bool { 71 | return changesetStatusListContains(d.Status, failedChangeSetStatuses) || 72 | changesetExecutionStatusListContains(d.ExecutionStatus, failedChangeSetExecutionStatuses) 73 | } 74 | 75 | func (d *ChangeSetDescription) IsSuccess() bool { 76 | return d.IsCreated() && d.ExecutionStatus == sdktypes.ExecutionStatusExecuteComplete 77 | } 78 | 79 | func (d *ChangeSetDescription) ReadyForExecution() bool { 80 | return d.IsCreated() && d.ExecutionStatus == sdktypes.ExecutionStatusAvailable 81 | } 82 | 83 | func changesetStatusListContains(element sdktypes.ChangeSetStatus, statusList []sdktypes.ChangeSetStatus) bool { 84 | for _, status := range statusList { 85 | if element == status { 86 | return true 87 | } 88 | } 89 | return false 90 | } 91 | 92 | func changesetExecutionStatusListContains(element sdktypes.ExecutionStatus, statusList []sdktypes.ExecutionStatus) bool { 93 | for _, status := range statusList { 94 | if element == status { 95 | return true 96 | } 97 | } 98 | return false 99 | } 100 | -------------------------------------------------------------------------------- /internal/clients/types/stack.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | package types 5 | 6 | import ( 7 | sdktypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 8 | ) 9 | 10 | var ( 11 | successfulDeploymentStackStatuses = []sdktypes.StackStatus{ 12 | sdktypes.StackStatusCreateComplete, 13 | sdktypes.StackStatusUpdateComplete, 14 | sdktypes.StackStatusImportComplete, 15 | } 16 | 17 | inProgressStackStatuses = []sdktypes.StackStatus{ 18 | sdktypes.StackStatusCreateInProgress, 19 | sdktypes.StackStatusDeleteInProgress, 20 | sdktypes.StackStatusRollbackInProgress, 21 | sdktypes.StackStatusUpdateCompleteCleanupInProgress, 22 | sdktypes.StackStatusUpdateInProgress, 23 | sdktypes.StackStatusUpdateRollbackCompleteCleanupInProgress, 24 | sdktypes.StackStatusUpdateRollbackInProgress, 25 | sdktypes.StackStatusImportInProgress, 26 | sdktypes.StackStatusImportRollbackInProgress, 27 | } 28 | 29 | unrecoverableFailureStackStatuses = []sdktypes.StackStatus{ 30 | sdktypes.StackStatusCreateFailed, 31 | sdktypes.StackStatusDeleteFailed, 32 | sdktypes.StackStatusRollbackComplete, 33 | sdktypes.StackStatusRollbackFailed, 34 | } 35 | 36 | recoverableFailureStackStatuses = []sdktypes.StackStatus{ 37 | sdktypes.StackStatusUpdateFailed, 38 | sdktypes.StackStatusUpdateRollbackComplete, 39 | sdktypes.StackStatusImportRollbackComplete, 40 | sdktypes.StackStatusImportRollbackFailed, 41 | } 42 | ) 43 | 44 | // Stack represents a AWS CloudFormation stack. 45 | type Stack struct { 46 | Name string 47 | Region string 48 | Generation int64 49 | SourceRevision string 50 | ChangeSetArn string 51 | *StackConfig 52 | } 53 | 54 | type StackConfig struct { 55 | TemplateBucket string 56 | TemplateBody string 57 | TemplateURL string 58 | Parameters []sdktypes.Parameter 59 | Tags []sdktypes.Tag 60 | } 61 | 62 | // StackEvent is an alias the SDK's StackEvent type. 63 | type StackEvent sdktypes.StackEvent 64 | 65 | // StackDescription is an alias the SDK's Stack type. 66 | type StackDescription sdktypes.Stack 67 | 68 | // StackResource is an alias the SDK's StackResource type. 69 | type StackResource sdktypes.StackResource 70 | 71 | // SDK returns the underlying struct from the AWS SDK. 72 | func (d *StackDescription) SDK() *sdktypes.Stack { 73 | raw := sdktypes.Stack(*d) 74 | return &raw 75 | } 76 | 77 | // RequiresCleanup returns true if the stack was created or deleted, but the action failed and the stack should be deleted. 78 | func (d *StackDescription) RequiresCleanup() bool { 79 | return stackStatusListContains(d.StackStatus, unrecoverableFailureStackStatuses) 80 | } 81 | 82 | // ReadyForStackCleanup returns true if the stack is in a state where it can be deleted. 83 | func (d *StackDescription) ReadyForCleanup() bool { 84 | return !d.InProgress() 85 | } 86 | 87 | // RequiresRollbackContinuation returns true if the stack failed an update, and the rollback failed. 88 | // The only valid actions for the stack in this state are the ContinueUpdateRollback or DeleteStack operations 89 | func (d *StackDescription) RequiresRollbackContinuation() bool { 90 | return sdktypes.StackStatusUpdateRollbackFailed == d.StackStatus 91 | } 92 | 93 | // InProgress returns true if the stack is currently being deployed. 94 | func (d *StackDescription) InProgress() bool { 95 | return stackStatusListContains(d.StackStatus, inProgressStackStatuses) 96 | } 97 | 98 | // IsSuccess returns true if the stack mutated successfully. 99 | func (d *StackDescription) IsSuccess() bool { 100 | return stackStatusListContains(d.StackStatus, successfulDeploymentStackStatuses) 101 | } 102 | 103 | // IsRecoverableFailure returns true if the stack failed to mutate, but can be further updated. 104 | func (d *StackDescription) IsRecoverableFailure() bool { 105 | return stackStatusListContains(d.StackStatus, recoverableFailureStackStatuses) 106 | } 107 | 108 | // DeleteFailed returns true if the stack is in DELETE_FAILED state 109 | func (d *StackDescription) DeleteFailed() bool { 110 | return sdktypes.StackStatusDeleteFailed == d.StackStatus 111 | } 112 | 113 | func stackStatusListContains(element sdktypes.StackStatus, statusList []sdktypes.StackStatus) bool { 114 | for _, status := range statusList { 115 | if element == status { 116 | return true 117 | } 118 | } 119 | return false 120 | } 121 | -------------------------------------------------------------------------------- /internal/controllers/source_predicate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Flux authors 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | // These methods were originally from 15 | // https://github.com/fluxcd/helm-controller/blob/main/controllers/source_predicate.go 16 | 17 | package controllers 18 | 19 | import ( 20 | "sigs.k8s.io/controller-runtime/pkg/event" 21 | "sigs.k8s.io/controller-runtime/pkg/predicate" 22 | 23 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 24 | ) 25 | 26 | type SourceRevisionChangePredicate struct { 27 | predicate.Funcs 28 | } 29 | 30 | func (SourceRevisionChangePredicate) Update(e event.UpdateEvent) bool { 31 | if e.ObjectOld == nil || e.ObjectNew == nil { 32 | return false 33 | } 34 | 35 | oldSource, ok := e.ObjectOld.(sourcev1.Source) 36 | if !ok { 37 | return false 38 | } 39 | 40 | newSource, ok := e.ObjectNew.(sourcev1.Source) 41 | if !ok { 42 | return false 43 | } 44 | 45 | if oldSource.GetArtifact() == nil && newSource.GetArtifact() != nil { 46 | return true 47 | } 48 | 49 | if oldSource.GetArtifact() != nil && newSource.GetArtifact() != nil && 50 | !oldSource.GetArtifact().HasRevision(newSource.GetArtifact().Revision) { 51 | return true 52 | } 53 | 54 | return false 55 | } 56 | 57 | func (SourceRevisionChangePredicate) Create(e event.CreateEvent) bool { 58 | return false 59 | } 60 | 61 | func (SourceRevisionChangePredicate) Delete(e event.DeleteEvent) bool { 62 | return false 63 | } 64 | -------------------------------------------------------------------------------- /internal/integtests/cfn_controller_integ_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: MIT-0 5 | 6 | package integtests 7 | 8 | import ( 9 | "context" 10 | "flag" 11 | "os" 12 | "testing" 13 | 14 | "github.com/aws/aws-sdk-go-v2/config" 15 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 16 | "github.com/aws/aws-sdk-go-v2/service/secretsmanager" 17 | "github.com/cucumber/godog" 18 | "github.com/cucumber/godog/colors" 19 | ) 20 | 21 | const ( 22 | Region = "us-west-2" 23 | ) 24 | 25 | var opts = godog.Options{Output: colors.Colored(os.Stdout)} 26 | 27 | func init() { 28 | godog.BindFlags("godog.", flag.CommandLine, &opts) 29 | } 30 | 31 | func TestCloudFormationController(t *testing.T) { 32 | cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(Region)) 33 | if err != nil { 34 | t.Error(err) 35 | t.FailNow() 36 | } 37 | 38 | testSuite := &cfnControllerTestSuite{ 39 | testingT: t, 40 | cmdRunner: &cfnControllerTestCommandRunner{ 41 | testingT: t, 42 | stdLogger: &cfnControllerTestStdLogger{testingT: t}, 43 | errLogger: &cfnControllerTestErrLogger{testingT: t}, 44 | }, 45 | cfnClient: cloudformation.NewFromConfig(cfg), 46 | secretsManagerClient: secretsmanager.NewFromConfig(cfg), 47 | } 48 | 49 | o := opts 50 | o.TestingT = t 51 | 52 | status := godog.TestSuite{ 53 | Name: "flux", 54 | Options: &o, 55 | TestSuiteInitializer: testSuite.InitializeTestSuite, 56 | ScenarioInitializer: testSuite.InitializeScenario, 57 | }.Run() 58 | 59 | if status == 2 { 60 | t.SkipNow() 61 | } 62 | 63 | if status != 0 { 64 | t.Fatalf("zero status code expected, %d received", status) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/integtests/cfn_controller_test_suite.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: MIT-0 5 | 6 | package integtests 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "errors" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/aws/aws-sdk-go-v2/aws" 16 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 17 | "github.com/aws/aws-sdk-go-v2/service/secretsmanager" 18 | "github.com/cucumber/godog" 19 | http "github.com/go-git/go-git/v5/plumbing/transport/http" 20 | ) 21 | 22 | const ( 23 | GitCredentialsSecretName = "flux-git-credentials" 24 | GitCredentialsUserNameKey = "ServiceUserName" 25 | GitCredentialsPasswordKey = "ServicePassword" 26 | 27 | FluxConfigRepoName = "my-flux-configuration" 28 | CfnTemplatesRepoConfigFile = "examples/my-flux-configuration/my-cloudformation-templates-repo.yaml" 29 | CfnTemplateRepoName = "my-cloudformation-templates" 30 | ) 31 | 32 | type cfnControllerTestSuite struct { 33 | testingT *testing.T 34 | cmdRunner *cfnControllerTestCommandRunner 35 | secretsManagerClient *secretsmanager.Client 36 | cfnClient *cloudformation.Client 37 | gitCredentials *http.BasicAuth 38 | } 39 | 40 | func (t *cfnControllerTestSuite) InitializeTestSuite(ctx *godog.TestSuiteContext) { 41 | // Before starting the test suite: 42 | // 1. Bootstrap and validate the local Kubernetes cluster 43 | // 2. Clone the Flux config git repo and CloudFormation template git repo locally 44 | // 3. Register the CFN template git repo with Flux 45 | ctx.BeforeSuite(func() { 46 | resp, err := t.secretsManagerClient.GetSecretValue(context.TODO(), &secretsmanager.GetSecretValueInput{ 47 | SecretId: aws.String(GitCredentialsSecretName), 48 | }) 49 | if err != nil { 50 | t.testingT.Error(err) 51 | t.testingT.FailNow() 52 | } 53 | 54 | creds := map[string]string{} 55 | json.Unmarshal([]byte(*resp.SecretString), &creds) 56 | 57 | auth := &http.BasicAuth{ 58 | Username: creds[GitCredentialsUserNameKey], 59 | Password: creds[GitCredentialsPasswordKey], 60 | } 61 | t.gitCredentials = auth 62 | 63 | if err = t.checkKubernetesCluster(); err != nil { 64 | t.testingT.Error(err) 65 | t.testingT.FailNow() 66 | } 67 | if err = t.checkTemplateGitRepository(context.TODO()); err != nil { 68 | t.testingT.Error(err) 69 | t.testingT.FailNow() 70 | } 71 | 72 | // TODO clear out any old CFN stacks from previous integ tests 73 | }) 74 | } 75 | 76 | func (t *cfnControllerTestSuite) InitializeScenario(ctx *godog.ScenarioContext) { 77 | scenario := &cfnControllerScenario{ 78 | suite: t, 79 | } 80 | 81 | ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) { 82 | cleanupErr := scenario.cleanup(ctx) 83 | return ctx, cleanupErr 84 | }) 85 | 86 | ctx.Step(`^I apply the following CloudFormationStack configuration to my Kubernetes cluster$`, scenario.applyCfnStackConfiguration) 87 | ctx.Step(`^I mark the CloudFormationStack for deletion$`, scenario.deleteCfnStackObject) 88 | ctx.Step(`^I push a valid CloudFormation template to my git repository$`, scenario.createCfnTemplateFile) 89 | ctx.Step(`^I push a valid CloudFormation template with parameters to my git repository$`, scenario.createCfnTemplateFileWithParameters) 90 | ctx.Step(`^I push an update for my CloudFormation template to my git repository$`, scenario.updateCfnTemplateFile) 91 | ctx.Step(`^I push another valid CloudFormation template to my git repository$`, scenario.createSecondCfnTemplateFile) 92 | ctx.Step(`^I trigger Flux to reconcile my git repository$`, t.reconcileTemplateGitRepository) 93 | ctx.Step(`^the CloudFormation stack in my AWS account should be deleted$`, scenario.realCfnStackShouldBeDeleted) 94 | ctx.Step(`^the CloudFormation stack in my AWS account should be in "([^"]*)" state$`, scenario.realCfnStackShouldBeInState) 95 | ctx.Step(`^the CloudFormationStack should eventually be deleted$`, scenario.cfnStackObjectShouldBeDeleted) 96 | ctx.Step(`^the CloudFormationStack\'s Ready condition should eventually have "([^"]*)" status$`, scenario.cfnStackObjectShouldHaveStatus) 97 | ctx.Step(`^the other CloudFormationStack\'s Ready condition should eventually have "([^"]*)" status$`, scenario.otherCfnStackObjectShouldHaveStatus) 98 | } 99 | 100 | func (s *cfnControllerTestSuite) checkTemplateGitRepository(ctx context.Context) error { 101 | // Add the template git repository configuration to the Flux config repo 102 | _, err := copyFileToGitRepository(ctx, FluxConfigRepoName, s.gitCredentials, CfnTemplatesRepoConfigFile, "my-cloudformation-templates-repo.yaml") 103 | if err != nil { 104 | return err 105 | } 106 | 107 | // Validate that Flux can pull from the Flux config repo 108 | if err = s.reconcileFluxConfigGitRepository(); err != nil { 109 | return err 110 | } 111 | 112 | // Validate that Flux can pull from the CFN templates repo 113 | if err = s.reconcileTemplateGitRepository(); err != nil { 114 | return err 115 | } 116 | 117 | // TODO clear out any old integ test templates from the template git repository 118 | 119 | return nil 120 | } 121 | 122 | func (s *cfnControllerTestSuite) checkKubernetesCluster() error { 123 | if err := s.cmdRunner.run("kubectl", "version"); err != nil { 124 | return err 125 | } 126 | if err := s.cmdRunner.run("flux", "check"); err != nil { 127 | return err 128 | } 129 | out, err := s.cmdRunner.getOutput("kubectl", "get", "deployment", "cfn-controller", "--namespace", "flux-system", "-o", "jsonpath=\"{.status.conditions[?(@.type == 'Available')].status}\"") 130 | if err != nil { 131 | return err 132 | } 133 | out = strings.Trim(out, "\"") 134 | 135 | if out != "True" { 136 | return errors.New("CloudFormation controller is not available in the Kubernetes cluster, current Available status is " + out) 137 | } 138 | return nil 139 | } 140 | 141 | func (s *cfnControllerTestSuite) reconcileFluxConfigGitRepository() error { 142 | if err := s.cmdRunner.run("flux", "reconcile", "source", "git", "flux-system"); err != nil { 143 | return err 144 | } 145 | out, err := s.cmdRunner.getOutput("flux", "get", "sources", "git", "flux-system", "--status-selector", "ready=true", "--no-header") 146 | if err != nil { 147 | return err 148 | } 149 | if out == "" { 150 | output, err := s.cmdRunner.getOutput("flux", "get", "sources", "git", "flux-system") 151 | if err == nil { 152 | s.testingT.Error(output) 153 | } 154 | return errors.New("CloudFormation template file repository could not be reconciled by Flux") 155 | } 156 | return nil 157 | } 158 | 159 | func (s *cfnControllerTestSuite) reconcileTemplateGitRepository() error { 160 | if err := s.cmdRunner.run("flux", "reconcile", "source", "git", "my-cfn-templates-repo"); err != nil { 161 | return err 162 | } 163 | out, err := s.cmdRunner.getOutput("flux", "get", "sources", "git", "my-cfn-templates-repo", "--status-selector", "ready=true", "--no-header") 164 | if err != nil { 165 | return err 166 | } 167 | if out == "" { 168 | output, err := s.cmdRunner.getOutput("flux", "get", "sources", "git", "my-cfn-templates-repo") 169 | if err == nil { 170 | s.testingT.Error(output) 171 | } 172 | return errors.New("CloudFormation template file repository could not be reconciled by Flux") 173 | } 174 | return nil 175 | } 176 | -------------------------------------------------------------------------------- /internal/integtests/command_runner.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: MIT-0 5 | 6 | package integtests 7 | 8 | import ( 9 | "fmt" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | type cfnControllerTestCommandRunner struct { 17 | testingT *testing.T 18 | stdLogger *cfnControllerTestStdLogger 19 | errLogger *cfnControllerTestErrLogger 20 | } 21 | 22 | type cfnControllerTestStdLogger struct { 23 | testingT *testing.T 24 | } 25 | 26 | // Use custom writers so that we can pipe command output to the testing framework's logger 27 | func (l *cfnControllerTestStdLogger) Write(data []byte) (n int, err error) { 28 | l.testingT.Log(string(data)) 29 | return len(data), err 30 | } 31 | 32 | type cfnControllerTestErrLogger struct { 33 | testingT *testing.T 34 | } 35 | 36 | func (l *cfnControllerTestErrLogger) Write(data []byte) (n int, err error) { 37 | l.testingT.Error(string(data)) 38 | return len(data), err 39 | } 40 | 41 | func (t *cfnControllerTestCommandRunner) runExitOnFail(command string, arg ...string) { 42 | // current working directory is expected to be /internal/integtests 43 | rootDir, err := filepath.Abs("../..") 44 | if err != nil { 45 | t.testingT.Error(err) 46 | t.testingT.FailNow() 47 | } 48 | 49 | cmd := exec.Command(command, arg...) 50 | cmd.Dir = rootDir 51 | cmd.Stdout = t.stdLogger 52 | cmd.Stderr = t.errLogger 53 | t.testingT.Log(fmt.Sprintf("Running command %s %s", command, strings.Join(arg, " "))) 54 | if err := cmd.Run(); err != nil { 55 | t.testingT.Error(err) 56 | t.testingT.FailNow() 57 | } 58 | } 59 | 60 | func (t *cfnControllerTestCommandRunner) run(command string, arg ...string) error { 61 | // current working directory is expected to be /internal/integtests 62 | rootDir, err := filepath.Abs("../..") 63 | if err != nil { 64 | t.testingT.Error(err) 65 | return err 66 | } 67 | 68 | cmd := exec.Command(command, arg...) 69 | cmd.Dir = rootDir 70 | output, err := cmd.Output() 71 | if err != nil { 72 | t.testingT.Error(fmt.Sprintf("Command failed: %s %s", command, strings.Join(arg, " "))) 73 | t.testingT.Error(output) 74 | if ee, ok := err.(*exec.ExitError); ok { 75 | t.testingT.Error(string(ee.Stderr)) 76 | } 77 | t.testingT.Error(err) 78 | return err 79 | } 80 | return nil 81 | } 82 | 83 | func (t *cfnControllerTestCommandRunner) runWithStdIn(stdinContent string, command string, arg ...string) error { 84 | // current working directory is expected to be /internal/integtests 85 | rootDir, err := filepath.Abs("../..") 86 | if err != nil { 87 | t.testingT.Error(err) 88 | return err 89 | } 90 | 91 | cmd := exec.Command(command, arg...) 92 | cmd.Dir = rootDir 93 | cmd.Stdin = strings.NewReader(stdinContent) 94 | output, err := cmd.Output() 95 | if err != nil { 96 | t.testingT.Error(fmt.Sprintf("Command failed: %s %s", command, strings.Join(arg, " "))) 97 | t.testingT.Error(stdinContent) 98 | t.testingT.Error(output) 99 | if ee, ok := err.(*exec.ExitError); ok { 100 | t.testingT.Error(string(ee.Stderr)) 101 | } 102 | t.testingT.Error(err) 103 | return err 104 | } 105 | return nil 106 | } 107 | 108 | func (t *cfnControllerTestCommandRunner) getOutput(command string, arg ...string) (string, error) { 109 | // current working directory is expected to be /internal/integtests 110 | rootDir, err := filepath.Abs("../..") 111 | if err != nil { 112 | t.testingT.Error(err) 113 | return "", err 114 | } 115 | 116 | cmd := exec.Command(command, arg...) 117 | cmd.Dir = rootDir 118 | output, err := cmd.Output() 119 | if err != nil { 120 | t.testingT.Error(fmt.Sprintf("Command failed: %s %s", command, strings.Join(arg, " "))) 121 | t.testingT.Error(output) 122 | if ee, ok := err.(*exec.ExitError); ok { 123 | t.testingT.Error(string(ee.Stderr)) 124 | } 125 | t.testingT.Error(err) 126 | return "", err 127 | } 128 | 129 | return string(output), nil 130 | } 131 | -------------------------------------------------------------------------------- /internal/integtests/git.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: MIT-0 5 | 6 | package integtests 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "os" 12 | "path/filepath" 13 | "time" 14 | 15 | git "github.com/go-git/go-git/v5" 16 | http "github.com/go-git/go-git/v5/plumbing/transport/http" 17 | ) 18 | 19 | const ( 20 | CodeCommitRegion = "us-west-2" 21 | 22 | GitEventuallyMaxAttempts = 5 23 | GitEventuallyRetryDelay = "2s" 24 | ) 25 | 26 | // Clones a repository from CodeCommit. 27 | // Returns the temporary directory where the git repository is located 28 | func cloneGitRepository(ctx context.Context, repositoryName string, gitCredentials *http.BasicAuth) (*git.Repository, string, error) { 29 | tmpDir, err := os.MkdirTemp("", repositoryName) 30 | if err != nil { 31 | return nil, tmpDir, fmt.Errorf("unable to create temp dir for repository %s, error: %w", repositoryName, err) 32 | } 33 | 34 | repositoryUrl := fmt.Sprintf("https://git-codecommit.%s.amazonaws.com/v1/repos/%s", CodeCommitRegion, repositoryName) 35 | r, err := git.PlainClone(tmpDir, false, &git.CloneOptions{ 36 | URL: repositoryUrl, 37 | Auth: gitCredentials, 38 | }) 39 | if err != nil { 40 | return r, tmpDir, err 41 | } 42 | return r, tmpDir, nil 43 | } 44 | 45 | // Copies the given file into the git repository. 46 | // If the destination file name is not specified, the method will create a file using a new unique file name 47 | func copyFileToGitRepository(ctx context.Context, repositoryName string, gitCredentials *http.BasicAuth, originalFile string, destFile string) (string, error) { 48 | content, err := os.ReadFile("../../" + originalFile) 49 | if err != nil { 50 | return "", err 51 | } 52 | return addFileToGitRepository(ctx, repositoryName, gitCredentials, string(content), destFile) 53 | } 54 | 55 | // Adds a file with the given content into the git repository. 56 | // If the destination file name is not specified, the method will create a file using a new unique file name 57 | func addFileToGitRepository(ctx context.Context, repositoryName string, gitCredentials *http.BasicAuth, content string, destFile string) (string, error) { 58 | var newFileRelativePath string 59 | 60 | err := gitEventually(func() error { 61 | // Clone the repo fresh 62 | repo, dir, err := cloneGitRepository(ctx, repositoryName, gitCredentials) 63 | if err != nil { 64 | return err 65 | } 66 | defer os.RemoveAll(dir) 67 | w, err := repo.Worktree() 68 | if err != nil { 69 | return err 70 | } 71 | 72 | // Write the file into the git repo on disk 73 | var newFile *os.File 74 | if destFile == "" { 75 | newFile, err = os.CreateTemp(dir, "integ-test.*.yaml") 76 | if err != nil { 77 | return err 78 | } 79 | } else { 80 | newFile, err = os.OpenFile(dir+"/"+destFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 81 | if err != nil { 82 | return err 83 | } 84 | } 85 | newFilePath := newFile.Name() 86 | newFileRelativePath, err = filepath.Rel(dir, newFilePath) 87 | if err != nil { 88 | return err 89 | } 90 | if _, err = newFile.Write([]byte(content)); err != nil { 91 | return err 92 | } 93 | if err = newFile.Close(); err != nil { 94 | return err 95 | } 96 | 97 | // Add the file to git 98 | if err = w.AddWithOptions(&git.AddOptions{All: true}); err != nil { 99 | return err 100 | } 101 | if _, err = w.Commit("Add file for integ test", &git.CommitOptions{AllowEmptyCommits: false}); err != nil { 102 | if err == git.ErrEmptyCommit { 103 | // Nothing to do 104 | return nil 105 | } 106 | return err 107 | } 108 | if err = repo.Push(&git.PushOptions{RemoteName: "origin", Auth: gitCredentials}); err != nil { 109 | return err 110 | } 111 | return nil 112 | }) 113 | 114 | return newFileRelativePath, err 115 | } 116 | 117 | // Deletes files from the git repository 118 | func deleteFilesFromGitRepository(ctx context.Context, repositoryName string, gitCredentials *http.BasicAuth, filePaths ...string) error { 119 | return gitEventually(func() error { 120 | // Clone the repo fresh 121 | repo, dir, err := cloneGitRepository(ctx, repositoryName, gitCredentials) 122 | if err != nil { 123 | return err 124 | } 125 | defer os.RemoveAll(dir) 126 | w, err := repo.Worktree() 127 | if err != nil { 128 | return err 129 | } 130 | 131 | // Delete the files from disk 132 | for _, filePath := range filePaths { 133 | if filePath != "" { 134 | if err = os.Remove(dir + "/" + filePath); err != nil { 135 | return err 136 | } 137 | } 138 | } 139 | if _, err = w.Commit("Delete integ test files", &git.CommitOptions{All: true, AllowEmptyCommits: false}); err != nil { 140 | if err == git.ErrEmptyCommit { 141 | // Nothing to do 142 | return nil 143 | } 144 | return err 145 | } 146 | 147 | // Delete the files on the remote 148 | if err = repo.Push(&git.PushOptions{RemoteName: "origin", Auth: gitCredentials}); err != nil { 149 | return err 150 | } 151 | return nil 152 | }) 153 | } 154 | 155 | func gitEventually(f func() error) (err error) { 156 | delay, durationErr := time.ParseDuration(GitEventuallyRetryDelay) 157 | if err != nil { 158 | return durationErr 159 | } 160 | 161 | for i := 0; i < GitEventuallyMaxAttempts; i++ { 162 | err = f() 163 | if err != nil { 164 | time.Sleep(delay) 165 | continue 166 | } 167 | break 168 | } 169 | return err 170 | } 171 | -------------------------------------------------------------------------------- /internal/mocks/mock_event_recorder.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: k8s.io/client-go/tools/record (interfaces: EventRecorder) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | runtime "k8s.io/apimachinery/pkg/runtime" 12 | ) 13 | 14 | // MockEventRecorder is a mock of EventRecorder interface. 15 | type MockEventRecorder struct { 16 | ctrl *gomock.Controller 17 | recorder *MockEventRecorderMockRecorder 18 | } 19 | 20 | // MockEventRecorderMockRecorder is the mock recorder for MockEventRecorder. 21 | type MockEventRecorderMockRecorder struct { 22 | mock *MockEventRecorder 23 | } 24 | 25 | // NewMockEventRecorder creates a new mock instance. 26 | func NewMockEventRecorder(ctrl *gomock.Controller) *MockEventRecorder { 27 | mock := &MockEventRecorder{ctrl: ctrl} 28 | mock.recorder = &MockEventRecorderMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockEventRecorder) EXPECT() *MockEventRecorderMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // AnnotatedEventf mocks base method. 38 | func (m *MockEventRecorder) AnnotatedEventf(arg0 runtime.Object, arg1 map[string]string, arg2, arg3, arg4 string, arg5 ...interface{}) { 39 | m.ctrl.T.Helper() 40 | varargs := []interface{}{arg0, arg1, arg2, arg3, arg4} 41 | for _, a := range arg5 { 42 | varargs = append(varargs, a) 43 | } 44 | m.ctrl.Call(m, "AnnotatedEventf", varargs...) 45 | } 46 | 47 | // AnnotatedEventf indicates an expected call of AnnotatedEventf. 48 | func (mr *MockEventRecorderMockRecorder) AnnotatedEventf(arg0, arg1, arg2, arg3, arg4 interface{}, arg5 ...interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | varargs := append([]interface{}{arg0, arg1, arg2, arg3, arg4}, arg5...) 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnnotatedEventf", reflect.TypeOf((*MockEventRecorder)(nil).AnnotatedEventf), varargs...) 52 | } 53 | 54 | // Event mocks base method. 55 | func (m *MockEventRecorder) Event(arg0 runtime.Object, arg1, arg2, arg3 string) { 56 | m.ctrl.T.Helper() 57 | m.ctrl.Call(m, "Event", arg0, arg1, arg2, arg3) 58 | } 59 | 60 | // Event indicates an expected call of Event. 61 | func (mr *MockEventRecorderMockRecorder) Event(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Event", reflect.TypeOf((*MockEventRecorder)(nil).Event), arg0, arg1, arg2, arg3) 64 | } 65 | 66 | // Eventf mocks base method. 67 | func (m *MockEventRecorder) Eventf(arg0 runtime.Object, arg1, arg2, arg3 string, arg4 ...interface{}) { 68 | m.ctrl.T.Helper() 69 | varargs := []interface{}{arg0, arg1, arg2, arg3} 70 | for _, a := range arg4 { 71 | varargs = append(varargs, a) 72 | } 73 | m.ctrl.Call(m, "Eventf", varargs...) 74 | } 75 | 76 | // Eventf indicates an expected call of Eventf. 77 | func (mr *MockEventRecorderMockRecorder) Eventf(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...) 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eventf", reflect.TypeOf((*MockEventRecorder)(nil).Eventf), varargs...) 81 | } 82 | -------------------------------------------------------------------------------- /lintconf.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | rules: 4 | braces: 5 | min-spaces-inside: 0 6 | max-spaces-inside: 0 7 | min-spaces-inside-empty: -1 8 | max-spaces-inside-empty: -1 9 | brackets: 10 | min-spaces-inside: 0 11 | max-spaces-inside: 0 12 | min-spaces-inside-empty: -1 13 | max-spaces-inside-empty: -1 14 | colons: 15 | max-spaces-before: 0 16 | max-spaces-after: 1 17 | commas: 18 | max-spaces-before: 0 19 | min-spaces-after: 1 20 | max-spaces-after: 1 21 | comments: 22 | require-starting-space: true 23 | min-spaces-from-content: 1 24 | document-end: disable 25 | document-start: disable # No --- to start a file 26 | empty-lines: 27 | max: 2 28 | max-start: 0 29 | max-end: 0 30 | hyphens: 31 | max-spaces-after: 1 32 | indentation: 33 | spaces: consistent 34 | indent-sequences: whatever # - list indentation will handle both indentation and without 35 | check-multi-line-strings: false 36 | key-duplicates: enable 37 | line-length: disable # Lines can be any length 38 | new-line-at-end-of-file: enable 39 | new-lines: 40 | type: unix 41 | trailing-spaces: enable 42 | truthy: 43 | level: warning -------------------------------------------------------------------------------- /local-dev/bootstrap-local-kind-cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script creates a new local kind cluster, and installs Flux and the CFN controller onto it. 4 | # The cluster is created with temporary ECR credentials that expire after 12 hours. 5 | 6 | set -e 7 | 8 | export AWS_REGION=us-west-2 9 | export AWS_ACCOUNT_ID=`aws sts get-caller-identity --query 'Account' --output text` 10 | 11 | # Create required AWS resources 12 | 13 | if [[ ! -v CI ]]; then 14 | echo Deploying CloudFormation stack with prerequisite resources 15 | 16 | aws cloudformation deploy --stack-name flux-cfn-controller-resources --region $AWS_REGION --template-file examples/resources.yaml --capabilities CAPABILITY_NAMED_IAM 17 | 18 | existing_creds=`aws iam list-service-specific-credentials --user-name flux-git --service-name codecommit.amazonaws.com --query 'ServiceSpecificCredentials'` 19 | empty_creds="[]" 20 | 21 | if [ "$existing_creds" = "$empty_creds" ]; then 22 | new_creds=`aws iam create-service-specific-credential --user-name flux-git --service-name codecommit.amazonaws.com --query 'ServiceSpecificCredential' --output json` 23 | aws secretsmanager put-secret-value --region $AWS_REGION --secret-string "$new_creds" --secret-id flux-git-credentials 24 | fi 25 | fi 26 | 27 | echo Setting up git repository for CloudFormation templates 28 | 29 | creds=`aws secretsmanager get-secret-value --region $AWS_REGION --secret-id flux-git-credentials --query 'SecretString' --output text` 30 | 31 | export CODECOMMIT_USERNAME=`echo $creds | jq -r '.ServiceUserName'` 32 | export CODECOMMIT_PASSWORD=`echo $creds | jq -r '.ServicePassword'` 33 | 34 | default_branch=`aws codecommit get-repository --repository-name my-cloudformation-templates --query 'repositoryMetadata.defaultBranch' --output text` 35 | no_default_branch="None" 36 | 37 | if [ "$default_branch" = "$no_default_branch" ]; then 38 | rm -rf init-cfn-template-repo 39 | mkdir init-cfn-template-repo 40 | cd init-cfn-template-repo 41 | git clone https://$CODECOMMIT_USERNAME:$CODECOMMIT_PASSWORD@git-codecommit.$AWS_REGION.amazonaws.com/v1/repos/my-cloudformation-templates 42 | cd my-cloudformation-templates 43 | git checkout --orphan main 44 | echo My CloudFormation templates > README.md 45 | git add README.md 46 | git commit -m "Initial commit" 47 | git push --set-upstream origin main 48 | cd ../.. 49 | rm -rf init-cfn-template-repo 50 | fi 51 | 52 | # Set up the kind cluster 53 | 54 | echo Creating the kind cluster 55 | 56 | kind delete cluster 57 | 58 | kind create cluster --config=local-dev/kind-cluster-config.yaml --image=kindest/node:v1.28.0 59 | 60 | # Install Flux 61 | 62 | echo Installing flux into the kind cluster 63 | 64 | flux check --pre 65 | 66 | flux bootstrap git \ 67 | --url=https://git-codecommit.$AWS_REGION.amazonaws.com/v1/repos/my-flux-configuration \ 68 | --branch=main \ 69 | --token-auth=true \ 70 | --username=$CODECOMMIT_USERNAME \ 71 | --password=$CODECOMMIT_PASSWORD \ 72 | 73 | flux create secret git cfn-template-repo-auth \ 74 | --url=https://git-codecommit.$AWS_REGION.amazonaws.com/v1/repos/my-cloudformation-templates \ 75 | --username=$CODECOMMIT_USERNAME \ 76 | --password=$CODECOMMIT_PASSWORD 77 | 78 | if [[ ! -v CI ]]; then 79 | rm -rf patch-local-cluster 80 | mkdir patch-local-cluster 81 | cd patch-local-cluster 82 | git clone https://$CODECOMMIT_USERNAME:$CODECOMMIT_PASSWORD@git-codecommit.$AWS_REGION.amazonaws.com/v1/repos/my-flux-configuration 83 | cd my-flux-configuration 84 | git apply ../../local-dev/local-flux-dev-config.patch 85 | git add flux-system 86 | git commit -m "Expose source controller locally" 87 | git push 88 | cd ../.. 89 | rm -rf patch-local-cluster 90 | flux reconcile source git flux-system 91 | fi 92 | 93 | # Install CFN controller types 94 | 95 | echo Installing CloudFormation controller resource types into the kind cluster 96 | 97 | make install 98 | 99 | flux reconcile kustomization flux-system 100 | 101 | flux reconcile source git flux-system 102 | 103 | kubectl get all --namespace flux-system 104 | 105 | flux get all 106 | 107 | # Install secrets into the local cluster 108 | 109 | echo Installing credentials into the kind cluster 110 | 111 | kubectl delete secret aws-creds -n flux-system --ignore-not-found 112 | if [[ -v AWS_ACCESS_KEY_ID ]]; then 113 | echo Creating Kubernetes secret from AWS credentials in environment variables 114 | 115 | cat < /tmp/aws-creds.yaml 116 | apiVersion: v1 117 | kind: Secret 118 | metadata: 119 | name: aws-creds 120 | type: Opaque 121 | data: 122 | EOT 123 | 124 | accessKeyId=`echo -n $AWS_ACCESS_KEY_ID | base64 -w 0` 125 | secretAccessKey=`echo -n $AWS_SECRET_ACCESS_KEY | base64 -w 0` 126 | echo " AWS_ACCESS_KEY_ID: $accessKeyId" >> /tmp/aws-creds.yaml 127 | echo " AWS_SECRET_ACCESS_KEY: $secretAccessKey" >> /tmp/aws-creds.yaml 128 | 129 | if [[ -v AWS_SESSION_TOKEN ]]; then 130 | sessionToken=`echo -n $AWS_SESSION_TOKEN | base64 -w 0` 131 | echo " AWS_SESSION_TOKEN: $sessionToken" >> /tmp/aws-creds.yaml 132 | fi 133 | 134 | kubectl -n flux-system apply -f /tmp/aws-creds.yaml 135 | rm /tmp/aws-creds.yaml 136 | elif [[ ! -v CI ]]; then 137 | echo Creating Kubernetes secret from AWS credentials in credentials file 138 | kubectl create secret generic aws-creds -n flux-system --from-file ~/.aws/credentials 139 | fi 140 | -------------------------------------------------------------------------------- /local-dev/kind-cluster-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kind.x-k8s.io/v1alpha4 2 | kind: Cluster 3 | nodes: 4 | - role: control-plane 5 | extraPortMappings: 6 | - containerPort: 30000 7 | hostPort: 30000 8 | -------------------------------------------------------------------------------- /local-dev/local-flux-dev-config.patch: -------------------------------------------------------------------------------- 1 | diff --git a/flux-system/gotk-components.yaml b/flux-system/gotk-components.yaml 2 | index 1d5c44b..d3cf93b 100644 3 | --- a/flux-system/gotk-components.yaml 4 | +++ b/flux-system/gotk-components.yaml 5 | @@ -3517,9 +3517,10 @@ spec: 6 | port: 80 7 | protocol: TCP 8 | targetPort: http 9 | + nodePort: 30000 10 | selector: 11 | app: source-controller 12 | - type: ClusterIP 13 | + type: NodePort 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "time" 10 | 11 | flag "github.com/spf13/pflag" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 14 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 15 | "k8s.io/utils/ptr" 16 | ctrl "sigs.k8s.io/controller-runtime" 17 | ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" 18 | ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 19 | ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config" 20 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 21 | 22 | "github.com/fluxcd/pkg/runtime/acl" 23 | "github.com/fluxcd/pkg/runtime/client" 24 | helper "github.com/fluxcd/pkg/runtime/controller" 25 | "github.com/fluxcd/pkg/runtime/events" 26 | "github.com/fluxcd/pkg/runtime/leaderelection" 27 | "github.com/fluxcd/pkg/runtime/logger" 28 | "github.com/fluxcd/pkg/runtime/metrics" 29 | "github.com/fluxcd/pkg/runtime/pprof" 30 | "github.com/fluxcd/pkg/runtime/probes" 31 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 32 | sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" 33 | 34 | "github.com/awslabs/aws-cloudformation-controller-for-flux/api/v1alpha1" 35 | cfnv1 "github.com/awslabs/aws-cloudformation-controller-for-flux/api/v1alpha1" 36 | "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/clients/cloudformation" 37 | "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/clients/s3" 38 | "github.com/awslabs/aws-cloudformation-controller-for-flux/internal/controllers" 39 | // +kubebuilder:scaffold:imports 40 | ) 41 | 42 | const controllerName = "cfn-flux-controller" 43 | 44 | var ( 45 | scheme = runtime.NewScheme() 46 | setupLog = ctrl.Log.WithName("setup") 47 | ) 48 | 49 | var ( 50 | // BuildSHA is the controller version 51 | BuildSHA string 52 | 53 | // BuildVersion is the controller build version 54 | BuildVersion string 55 | ) 56 | 57 | func init() { 58 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 59 | 60 | utilruntime.Must(sourcev1.AddToScheme(scheme)) 61 | utilruntime.Must(sourcev1b2.AddToScheme(scheme)) 62 | utilruntime.Must(cfnv1.AddToScheme(scheme)) 63 | // +kubebuilder:scaffold:scheme 64 | } 65 | 66 | func main() { 67 | var ( 68 | metricsAddr string 69 | eventsAddr string 70 | healthAddr string 71 | concurrent int 72 | requeueDependency time.Duration 73 | gracefulShutdownTimeout time.Duration 74 | clientOptions client.Options 75 | logOptions logger.Options 76 | aclOptions acl.Options 77 | leaderElectionOptions leaderelection.Options 78 | watchOptions helper.WatchOptions 79 | httpRetry int 80 | awsRegion string 81 | templateBucket string 82 | stackTags map[string]string 83 | ) 84 | 85 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 86 | flag.StringVar(&eventsAddr, "events-addr", "", "The address of the events receiver.") 87 | flag.StringVar(&healthAddr, "health-addr", ":9440", "The address the health endpoint binds to.") 88 | flag.IntVar(&concurrent, "concurrent", 4, "The number of concurrent CloudFormationStack reconciles.") 89 | flag.DurationVar(&requeueDependency, "requeue-dependency", 30*time.Second, "The interval at which failing dependencies are reevaluated.") 90 | flag.DurationVar(&gracefulShutdownTimeout, "graceful-shutdown-timeout", 600*time.Second, 91 | "The duration given to the reconciler to finish before forcibly stopping.") 92 | flag.IntVar(&httpRetry, "http-retry", 9, "The maximum number of retries when failing to fetch artifacts over HTTP.") 93 | flag.StringVar(&awsRegion, "aws-region", "", 94 | "The AWS region where CloudFormation stacks should be deployed. Will default to the AWS_REGION environment variable.") 95 | flag.StringVar(&templateBucket, "template-bucket", "", 96 | "The S3 bucket where the controller should upload CloudFormation templates for deployment. Will default to the TEMPLATE_BUCKET environment variable.") 97 | flag.StringToStringVar(&stackTags, "stack-tags", map[string]string{}, 98 | "Tag key and value pairs to apply to all CloudFormation stacks, in addition to the default tags added by the controller "+ 99 | "(cfn-flux-controller/version, cfn-flux-controller/name, cfn-flux-controller/namespace). "+ 100 | "Example: default-tag-name=default-tag-value,another-tag-name=another-tag-value.") 101 | 102 | clientOptions.BindFlags(flag.CommandLine) 103 | logOptions.BindFlags(flag.CommandLine) 104 | aclOptions.BindFlags(flag.CommandLine) 105 | leaderElectionOptions.BindFlags(flag.CommandLine) 106 | watchOptions.BindFlags(flag.CommandLine) 107 | flag.Parse() 108 | 109 | ctrl.SetLogger(logger.NewLogger(logOptions)) 110 | setupLog.Info("Configuring manager", "version", BuildVersion, "sha", BuildSHA) 111 | 112 | watchNamespace := "" 113 | if !watchOptions.AllNamespaces { 114 | watchNamespace = os.Getenv("RUNTIME_NAMESPACE") 115 | } 116 | 117 | watchSelector, err := helper.GetWatchSelector(watchOptions) 118 | if err != nil { 119 | setupLog.Error(err, "unable to configure watch label selector for manager") 120 | os.Exit(1) 121 | } 122 | 123 | leaderElectionId := fmt.Sprintf("%s-%s", controllerName, "leader-election") 124 | if watchOptions.LabelSelector != "" { 125 | leaderElectionId = leaderelection.GenerateID(leaderElectionId, watchOptions.LabelSelector) 126 | } 127 | 128 | restConfig := client.GetConfigOrDie(clientOptions) 129 | mgrConfig := ctrl.Options{ 130 | Scheme: scheme, 131 | HealthProbeBindAddress: healthAddr, 132 | LeaderElection: leaderElectionOptions.Enable, 133 | LeaderElectionReleaseOnCancel: leaderElectionOptions.ReleaseOnCancel, 134 | LeaseDuration: &leaderElectionOptions.LeaseDuration, 135 | RenewDeadline: &leaderElectionOptions.RenewDeadline, 136 | RetryPeriod: &leaderElectionOptions.RetryPeriod, 137 | GracefulShutdownTimeout: &gracefulShutdownTimeout, 138 | LeaderElectionID: leaderElectionId, 139 | Logger: ctrl.Log, 140 | Client: ctrlclient.Options{ 141 | Cache: &ctrlclient.CacheOptions{}, 142 | }, 143 | Cache: ctrlcache.Options{ 144 | ByObject: map[ctrlclient.Object]ctrlcache.ByObject{ 145 | &v1alpha1.CloudFormationStack{}: {Label: watchSelector}, 146 | }, 147 | }, 148 | Controller: ctrlcfg.Controller{ 149 | RecoverPanic: ptr.To(true), 150 | MaxConcurrentReconciles: concurrent, 151 | }, 152 | Metrics: metricsserver.Options{ 153 | BindAddress: metricsAddr, 154 | ExtraHandlers: pprof.GetHandlers(), 155 | }, 156 | } 157 | 158 | if watchNamespace != "" { 159 | mgrConfig.Cache.DefaultNamespaces = map[string]ctrlcache.Config{ 160 | watchNamespace: ctrlcache.Config{}, 161 | } 162 | } 163 | 164 | mgr, err := ctrl.NewManager(restConfig, mgrConfig) 165 | if err != nil { 166 | setupLog.Error(err, "unable to start manager") 167 | os.Exit(1) 168 | } 169 | 170 | probes.SetupChecks(mgr, setupLog) 171 | 172 | metricsH := helper.NewMetrics(mgr, metrics.MustMakeRecorder(), v1alpha1.CloudFormationStackFinalizer) 173 | var eventRecorder *events.Recorder 174 | if eventRecorder, err = events.NewRecorder(mgr, ctrl.Log, eventsAddr, controllerName); err != nil { 175 | setupLog.Error(err, "unable to create event recorder") 176 | os.Exit(1) 177 | } 178 | 179 | signalHandlerContext := ctrl.SetupSignalHandler() 180 | 181 | cfnClient, err := cloudformation.New(signalHandlerContext, awsRegion) 182 | if err != nil { 183 | setupLog.Error(err, "unable to create CloudFormation client") 184 | os.Exit(1) 185 | } 186 | 187 | s3Client, err := s3.New(signalHandlerContext, awsRegion) 188 | if err != nil { 189 | setupLog.Error(err, "unable to create S3 client") 190 | os.Exit(1) 191 | } 192 | 193 | if templateBucket == "" { 194 | templateBucket = os.Getenv("TEMPLATE_BUCKET") 195 | } 196 | 197 | controllerVersion := BuildVersion 198 | if controllerVersion == "" { 199 | controllerVersion = "unknown-version" 200 | } 201 | 202 | reconciler := &controllers.CloudFormationStackReconciler{ 203 | Client: mgr.GetClient(), 204 | Scheme: mgr.GetScheme(), 205 | EventRecorder: eventRecorder, 206 | Metrics: metricsH, 207 | NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs, 208 | CfnClient: cfnClient, 209 | S3Client: s3Client, 210 | TemplateBucket: templateBucket, 211 | StackTags: stackTags, 212 | ControllerName: controllerName, 213 | ControllerVersion: controllerVersion, 214 | } 215 | 216 | reconcilerOpts := controllers.CloudFormationStackReconcilerOptions{ 217 | HTTPRetry: httpRetry, 218 | DependencyRequeueInterval: requeueDependency, 219 | } 220 | 221 | if err = reconciler.SetupWithManager(signalHandlerContext, mgr, reconcilerOpts); err != nil { 222 | setupLog.Error(err, "unable to create controller", "controller", cfnv1.CloudFormationStackKind) 223 | os.Exit(1) 224 | } 225 | //+kubebuilder:scaffold:builder 226 | 227 | setupLog.Info("Starting manager", "version", BuildVersion, "sha", BuildSHA) 228 | 229 | if err := mgr.Start(signalHandlerContext); err != nil { 230 | setupLog.Error(err, "problem running manager") 231 | os.Exit(1) 232 | } 233 | } 234 | --------------------------------------------------------------------------------