├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .tool-versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── bundle.go ├── main_test.go ├── new.go ├── new_test.go ├── protonize.go ├── protonize_test.go ├── publish.go ├── root.go ├── templates │ ├── infrastructure │ │ ├── awsmanaged │ │ │ ├── cloudformation.env.yaml.jinja │ │ │ ├── cloudformation.svc.yaml.jinja │ │ │ └── manifest.yaml │ │ └── codebuild │ │ │ └── terraform │ │ │ ├── install-terraform.sh │ │ │ ├── main.env.tf.go.tpl │ │ │ ├── main.env.tf.new.go.tpl │ │ │ ├── main.svc.tf.go.tpl │ │ │ ├── main.svc.tf.new.go.tpl │ │ │ ├── manifest.yaml.go.tpl │ │ │ ├── output.sh │ │ │ ├── outputs.tf.go.tpl │ │ │ ├── variables.env.tf │ │ │ └── variables.svc.tf │ ├── readme │ │ ├── env.cfn.md │ │ ├── env.tf.md │ │ ├── svc.cfn.md │ │ └── svc.tf.md │ └── schema │ │ ├── schema.env.yaml.go.tpl │ │ └── schema.svc.yaml.go.tpl ├── test │ ├── main.tf │ └── outputs.tf └── utils.go ├── go.mod ├── go.sum └── main.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: [main] 4 | types: [opened, reopened, synchronize, edited] 5 | 6 | jobs: 7 | ci: 8 | name: test and build source 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Install pre-commit 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.10' 19 | 20 | - name: Run pre-commit hooks 21 | run: | 22 | python -m pip install pre-commit 23 | make precommit 24 | 25 | - name: Test 26 | run: make test 27 | 28 | - name: Build 29 | run: make build 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | releases-matrix: 7 | name: Release Go Binary 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | goos: [linux, windows, darwin] 12 | goarch: ["386", amd64, arm64] 13 | exclude: 14 | - goarch: "386" 15 | goos: darwin 16 | - goarch: arm64 17 | goos: windows 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set APP_VERSION 22 | run: echo APP_VERSION=$(basename ${GITHUB_REF}) >> ${GITHUB_ENV} 23 | 24 | - name: Set BUILD_TIME 25 | run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV} 26 | 27 | - uses: wangyoucao577/go-release-action@v1 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | goos: ${{ matrix.goos }} 31 | goarch: ${{ matrix.goarch }} 32 | goversion: "1.20" 33 | ldflags: -X "main.version=${{ env.APP_VERSION }} built on ${{ env.BUILD_TIME }}, commit ${{ github.sha }}" 34 | overwrite: true 35 | pre_command: export CGO_ENABLED=0 && export GODEBUG=http2client=0 36 | retry: 10 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app 2 | *.swp 3 | *.test 4 | *.out 5 | out 6 | dist 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | 10 | - repo: https://github.com/Bahjat/pre-commit-golang 11 | rev: v1.0.2 12 | hooks: 13 | - id: go-fmt-import 14 | - id: go-vet 15 | - id: go-lint 16 | - id: go-unit-tests 17 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.20.5 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/protonizer/0e6e027dd93fe55d2e55fa04caf6a9d6f5c9b94f/CHANGELOG.md -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGES := $(shell go list ./...) 2 | BUILD_VERSION := $(shell git describe --tags) 3 | 4 | all: help 5 | 6 | .PHONY: help 7 | help: Makefile 8 | @echo 9 | @echo " Choose a make command to run" 10 | @echo 11 | @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' 12 | @echo 13 | 14 | ## precommit: run all pre-commit hooks 15 | .PHONY: precommit 16 | precommit: 17 | pre-commit run --all-files 18 | 19 | ## vet: vet code 20 | .PHONY: vet 21 | vet: 22 | go vet $(PACKAGES) 23 | 24 | ## test: run unit tests 25 | .PHONY: test 26 | test: vet 27 | go test -race -cover $(PACKAGES) 28 | 29 | ## build: build a binary 30 | .PHONY: build 31 | build: 32 | echo building ${BUILD_VERSION} 33 | go build -ldflags "-X main.version=${BUILD_VERSION}" -o ./app -v 34 | 35 | ## autobuild: auto build when source files change 36 | .PHONY: autobuild 37 | autobuild: 38 | # curl -sf https://gobinaries.com/cespare/reflex | sh 39 | reflex -g '*.go' -- sh -c 'echo "\n\n\n\n\n\n" && make build' 40 | 41 | ## start: build and run local project 42 | .PHONY: start 43 | start: build 44 | clear 45 | @echo "" 46 | ./app 47 | 48 | ## xplat: multiplatform build 49 | .PHONY: xplat 50 | xplat: build 51 | GOOS=darwin GOARCH=amd64 go build -v -o ./dist/protonizer-darwin-amd64 -ldflags "-X main.version=${BUILD_VERSION}" 52 | GOOS=darwin GOARCH=arm64 go build -v -o ./dist/protonizer-darwin-arm64 -ldflags "-X main.version=${BUILD_VERSION}" 53 | GOOS=windows GOARCH=amd64 go build -v -o ./dist/protonizer-windows-amd64 -ldflags "-X main.version=${BUILD_VERSION}" 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # protonizer 2 | 3 | A CLI tool for working with IaC in [AWS Proton](https://aws.amazon.com/proton/). 4 | 5 | AWS Proton provides a self-service deployment service with versioning and traceability for your IaC templates. The Protonizer CLI tool lets you scaffold out new Proton templates from scratch as well as allows you take your existing IaC (infrastructure as code) templates and modules and bring them into [AWS Proton](https://aws.amazon.com/proton/) to scale them out across your organization. 6 | 7 | Note that this is an experimental project and currently supports generating Proton templates based on existing [Terraform](https://www.terraform.io/) and [CodeBuild provisioning](https://docs.aws.amazon.com/proton/latest/userguide/ag-works-prov-methods.html). The tool also currently only supports primitive [HCL data types](https://developer.hashicorp.com/terraform/language/expressions/types#types) such as `strings`, `numbers`, `bools`, and `lists` of primitive types. This is currently aligned with the Proton schema types that are supported by the Proton console. 8 | 9 | 10 | ## Install 11 | 12 | To install the `protonizer` CLI tool, you can download the latest binary [release](https://github.com/awslabs/protonizer/releases) for your platform and architecture. 13 | 14 | 15 | ## How it works 16 | 17 | Protonizer can scaffold out all of the files you need to build a Proton template. It can them register and publish the template using the `publish` command. 18 | 19 | Protonizer can also parse your existing Terraform modules and generates Proton [templates](https://docs.aws.amazon.com/proton/latest/userguide/ag-template-authoring.html) with [schemas](https://docs.aws.amazon.com/proton/latest/userguide/ag-schema.html) based on your input and output variables. It also outputs [manifest.yml](https://docs.aws.amazon.com/proton/latest/userguide/ag-wrap-up.html) files that will run `terraform apply` within a Proton-managed environment. 20 | 21 | 22 | ## Usage 23 | 24 | ### new 25 | 26 | The `new` command scaffolds out new Proton templates from scratch. 27 | 28 | **Terraform** 29 | 30 | ``` 31 | protonizer new \ 32 | --name my_template \ 33 | --type service \ 34 | --provisioning codebuild --tool terraform \ 35 | --terraform-remote-state-bucket my-s3-bucket \ 36 | --publish-bucket my-s3-bucket \ 37 | --out ~/proton/templates 38 | ``` 39 | 40 | ``` 41 | tree 42 | . 43 | | |____my_template 44 | | | |____v1 45 | | | | |____proton.yaml 46 | | | | |____schema 47 | | | | | |____schema.yaml 48 | | | | |____instance_infrastructure 49 | | | | | |____outputs.tf 50 | | | | | |____main.tf 51 | | | | | |____output.sh 52 | | | | | |____manifest.yaml 53 | | | | | |____install-terraform.sh 54 | | | | | |____variables.tf 55 | ``` 56 | 57 | **CloudFormation** 58 | 59 | ``` 60 | protonizer new \ 61 | --name my-template \ 62 | --type environment \ 63 | --provisioning awsmanaged \ 64 | --out ~/proton/templates \ 65 | --publish-bucket my-s3-bucket 66 | ``` 67 | 68 | ``` 69 | tree 70 | . 71 | | |____my-template 72 | | | |____v1 73 | | | | |____proton.yaml 74 | | | | |____schema 75 | | | | | |____schema.yaml 76 | | | | |____infrastructure 77 | | | | | |____cloudformation.yaml 78 | | | | | |____manifest.yaml 79 | ``` 80 | 81 | 82 | ### protonize 83 | 84 | The `protonize` command can generate and publish a [CodeBuild provisioning](https://docs.aws.amazon.com/proton/latest/userguide/ag-works-prov-methods.html) template based on an existing Terraform module. 85 | 86 | #### Generate a Proton environment 87 | 88 | ``` 89 | protonizer protonize \ 90 | --name my_template \ 91 | --type environment \ 92 | --provisioning codebuild --tool terraform \ 93 | --terraform-remote-state-bucket my-s3-bucket \ 94 | --dir ~/my-existing-tf-module \ 95 | --out ~/proton/templates \ 96 | 97 | template source outputted to ~/proton/templates/my_template 98 | done 99 | ``` 100 | 101 | #### Generate a Proton service (and publish inline) 102 | 103 | ``` 104 | protonizer protonize \ 105 | --name my_template \ 106 | --type service \ 107 | --compatible-env env1:1 --compatible-env env2:1 \ 108 | --provisioning codebuild --tool terraform \ 109 | --terraform-remote-state-bucket my-s3-bucket \ 110 | --dir ~/my-existing-tf-module \ 111 | --out ~/proton/templates \ 112 | --publish-bucket my-s3-bucket \ 113 | --publish 114 | 115 | template source outputted to ~/proton/templates/my_template 116 | published my_template:1.0 117 | https://us-east-1.console.aws.amazon.com/proton/home?region=us-east-1#/templates/services/detail/my_template 118 | done 119 | ``` 120 | 121 | ### publish 122 | 123 | The `publish` command registers and publishes a template with AWS Proton. Just add a `proton.yaml` file to your project and run `protonizer publish`. This is alternative to Proton's [Template sync](https://docs.aws.amazon.com/proton/latest/userguide/ag-template-sync-configs.html) feature, useful for local development or for Git providers that aren't supported. 124 | 125 | #### Publish an environment template 126 | 127 | proton.yaml 128 | 129 | ```yaml 130 | name: my_template 131 | type: environment 132 | displayName: My Template 133 | description: "This is my template" 134 | publishBucket: my-s3-bucket 135 | ``` 136 | 137 | publish using yaml file 138 | 139 | ``` 140 | protonizer publish 141 | published my_template:1.0 142 | https://us-east-1.console.aws.amazon.com/proton/home?region=us-east-1#/templates/environments/detail/my_template 143 | ``` 144 | 145 | #### Publish a service template 146 | 147 | proton.yaml 148 | 149 | ```yaml 150 | name: my_template 151 | type: service 152 | displayName: My Template 153 | description: "This is my template" 154 | compatibleEnvironments: 155 | - env1:3 156 | - env2:4 157 | publishBucket: my-s3-bucket 158 | ``` 159 | 160 | publish using yaml file 161 | 162 | ``` 163 | protonizer publish 164 | published my_template:1.0 165 | https://us-east-1.console.aws.amazon.com/proton/home?region=us-east-1#/templates/services/detail/my_template 166 | ``` 167 | 168 | or specify file name 169 | 170 | ``` 171 | protonizer publish -f file.yml 172 | published my_template:1.0 173 | https://us-east-1.console.aws.amazon.com/proton/home?region=us-east-1#/templates/environments/detail/my_template 174 | ``` 175 | 176 | Note that this can also be done inline with the `protonize --publish` command. 177 | 178 | 179 | ### Terraform variable mapping 180 | 181 | To avoid conflicts, if you have variables in your source templates with reserved names in Proton (i.e., `name` and `environment`), they will be removed as template input variables and instead be sourced from proton metadata. 182 | 183 | 184 | #### Environment templates 185 | 186 | If the source terraform module has an input variable named `name`, it will be supplied by the name of the proton environment rather than by template specific input. 187 | 188 | 189 | #### Service templates 190 | 191 | If the source terraform module has a variable named `name`, it will be set to the name of the service and the service instance with a `-` (dash) in between. If the source terraform module has a variable named `environment`, it will be set to the service instance's environment name. 192 | 193 | For example, when creating a service named `sales-api` and a service instance named `dev` associated with a proton environment named `dev`, the Terraform module will get passed the following values: 194 | 195 | ```hcl 196 | name = "sales-api-dev" 197 | environment = "dev" 198 | ``` 199 | 200 | 201 | ### Development 202 | 203 | #### Setup 204 | 205 | - Go 1.20 206 | - Install [pre-commit](https://pre-commit.com/) 207 | - Run `pre-commit install` to setup git hooks 208 | 209 | #### Commands 210 | 211 | ``` 212 | Choose a make command to run 213 | 214 | vet vet code 215 | test run unit tests 216 | build build a binary 217 | autobuild auto build when source files change 218 | dockerbuild build project into a docker container image 219 | start build and run local project 220 | deploy build code into a container and deploy it to the cloud dev environment 221 | xplat multiplatform build 222 | ``` 223 | -------------------------------------------------------------------------------- /cmd/bundle.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | func createTarGZFile(source, target string) error { 13 | 14 | f, err := os.Create(target) 15 | if err != nil { 16 | return err 17 | } 18 | defer f.Close() 19 | 20 | gw := gzip.NewWriter(f) 21 | defer gw.Close() 22 | tw := tar.NewWriter(gw) 23 | defer tw.Close() 24 | 25 | return filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 26 | if err != nil { 27 | return err 28 | } 29 | 30 | if info.IsDir() { 31 | return nil 32 | } 33 | 34 | //ignore the zip itself 35 | if filepath.Base(path) == filepath.Base(target) { 36 | return nil 37 | } 38 | 39 | //ignore .git 40 | if strings.Contains(path, ".git") { 41 | return nil 42 | } 43 | 44 | header, err := tar.FileInfoHeader(info, info.Name()) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | header.Name = strings.TrimPrefix(path, source) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | if info.IsDir() { 55 | header.Name += "/" 56 | } 57 | 58 | err = tw.WriteHeader(header) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | f, err := os.Open(path) 64 | if err != nil { 65 | return err 66 | } 67 | defer f.Close() 68 | 69 | _, err = io.Copy(tw, f) 70 | return err 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /cmd/main_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestMain(m *testing.M) { 9 | verbose = true 10 | os.Exit(m.Run()) 11 | } 12 | -------------------------------------------------------------------------------- /cmd/new.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "path" 7 | "path/filepath" 8 | 9 | hackpados "github.com/hack-pad/hackpadfs/os" 10 | "github.com/jritsema/scaffolder" 11 | "github.com/spf13/cobra" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | // upCmd represents the up command 16 | var newCmd = &cobra.Command{ 17 | Use: "new", 18 | Short: "new scaffolds out a new proton template", 19 | Long: `new scaffolds out a new proton template`, 20 | Run: doNew, 21 | Example: ` 22 | # Create a new environment template using AWS-Managed CloudFormation 23 | protonizer new --name my-env-template --provisioning awsmanaged 24 | 25 | # Create a new service template using AWS-Managed CloudFormation 26 | protonizer new --name my-template \ 27 | --provisioning awsmanaged \ 28 | --type service \ 29 | --compatible-env my-env-template:1 30 | 31 | # Create a new environment template using CodeBuild provisioning with Terraform 32 | protonizer new \ 33 | --name my_template \ 34 | --provisioning codebuild --tool terraform \ 35 | --terraform-remote-state-bucket my-s3-bucket 36 | 37 | # Create a new service template using CodeBuild provisioning with Terraform 38 | protonizer new \ 39 | --name my_template \ 40 | --type service \ 41 | --provisioning codebuild --tool terraform \ 42 | --terraform-remote-state-bucket my-s3-bucket \ 43 | --publish-bucket my-s3-bucket \ 44 | --compatible-env my-env-template:1 \ 45 | --out ~/proton/templates 46 | 47 | # If you would like to use protonizer to publish this template, 48 | then you can include an S3 bucket that you have write access to 49 | protonizer new --name my-template --publish-bucket my-s3-bucket 50 | `, 51 | } 52 | 53 | var ( 54 | flagNewTemplateName string 55 | flagNewTemplateType string 56 | flagNewOutDir string 57 | flagNewProvisoning string 58 | flagNewTool string 59 | flagNewPublishBucket string 60 | flagNewTerraformRemoteStateBucket string 61 | flagNewCompatibleEnvs []string 62 | ) 63 | 64 | func init() { 65 | newCmd.Flags().StringVarP(&flagNewTemplateName, "name", "n", "", "The name of the template") 66 | newCmd.MarkFlagRequired("name") 67 | 68 | newCmd.Flags().StringVarP(&flagNewTemplateType, "type", "t", "environment", "Template type: environment or service") 69 | 70 | newCmd.Flags().StringVarP(&flagNewOutDir, "out", "o", ".", "The directory to output the protonized template. Defaults to current directory.") 71 | 72 | newCmd.Flags().StringVarP(&flagNewProvisoning, "provisioning", "p", provisioningTypeCodeBuild, "The provisioning mode to use") 73 | 74 | newCmd.Flags().StringVar(&flagNewTool, "tool", toolTerraform, "The tool to use. Currently Terraform is supported") 75 | 76 | newCmd.Flags().StringVarP(&flagNewPublishBucket, "publish-bucket", "b", "", 77 | "The S3 bucket to use for template publishing. This is optional if not using the publish command.") 78 | 79 | newCmd.Flags().StringVar(&flagNewTerraformRemoteStateBucket, "terraform-remote-state-bucket", "", 80 | "The S3 bucket to use for storing Terraform remote state. This is required for --provisioning codebuild and --tool terraform") 81 | 82 | newCmd.Flags().StringArrayVar(&flagNewCompatibleEnvs, "compatible-env", []string{}, 83 | `Proton environments (name:majorversion) that the service template is compatible with. 84 | You may specify any number of environments by repeating --compatible-env before each one`) 85 | 86 | rootCmd.AddCommand(newCmd) 87 | } 88 | 89 | type scaffoldInputData struct { 90 | Contents *scaffolder.FSContents 91 | Name string 92 | Type string 93 | Shorthand string 94 | RootDir string 95 | InfraDir string 96 | Vars []schemaVariable 97 | TerraformS3StateBucket string 98 | } 99 | 100 | func doNew(cmd *cobra.Command, args []string) { 101 | 102 | //check required args 103 | 104 | if !(flagNewTemplateType == "environment" || flagNewTemplateType == "service") { 105 | errorExit(fmt.Sprintf("template type: %s is invalid. only environment and service are supported", 106 | flagProtonizeTemplateType)) 107 | } 108 | 109 | if flagNewTool != toolTerraform { 110 | errorExit("currently the only provisioning type supported is", toolTerraform) 111 | } 112 | 113 | if flagNewTemplateType == "service" && len(flagNewCompatibleEnvs) == 0 { 114 | errorExit("--compatible-env is required for service templates") 115 | } 116 | 117 | //create a file system rooted at output path 118 | //the scaffold function will write to this file system 119 | out, err := filepath.Abs(flagNewOutDir) 120 | handleError("getting absolute path of out dir", err) 121 | osfs := hackpados.NewFS() 122 | fsPath, err := osfs.FromOSPath(out) 123 | m := "creating out file system: " + fsPath 124 | debug(m) 125 | handleError("FromOSPath", err) 126 | outFS, err := osfs.Sub(fsPath) 127 | handleError(fsPath, err) 128 | 129 | scaffoldProton( 130 | flagNewTemplateName, 131 | flagNewTemplateType, 132 | flagNewProvisoning, 133 | flagNewTool, 134 | flagNewPublishBucket, 135 | flagNewTerraformRemoteStateBucket, 136 | flagNewCompatibleEnvs, 137 | outFS, 138 | ) 139 | 140 | fmt.Println("template source outputted to", path.Join(out, flagNewTemplateName)) 141 | fmt.Println("done") 142 | } 143 | 144 | func scaffoldProton( 145 | name, 146 | templateType, 147 | provisioning, 148 | tool, 149 | s3Bucket, 150 | terraformRemoteStateBucket string, 151 | compatibleEnvironments []string, 152 | outFS fs.FS) { 153 | 154 | m := "generating proton config" 155 | debug(m) 156 | protonData := protonConfigData{ 157 | Name: name, 158 | Type: templateType, 159 | DisplayName: name, 160 | Description: fmt.Sprintf("A %s template scaffolded by the Protonizer CLI tool", templateType), 161 | PublishBucket: s3Bucket, 162 | CompatibleEnvironments: compatibleEnvironments, 163 | } 164 | protonConfig, err := yaml.Marshal(protonData) 165 | handleError(m, err) 166 | 167 | //schema variables 168 | //proton seems to require at least one input variable 169 | schemaVars := []schemaVariable{ 170 | { 171 | Name: "example_input", 172 | Type: "string", 173 | Title: "Example Input", 174 | Description: "This is an example string input", 175 | Default: "default", 176 | }, 177 | } 178 | 179 | tType := getTemplateTypeShorthand(templateType) 180 | root := path.Join(name, "v1") 181 | infraDir := path.Join(root, getInfrastructureDirectory(templateType)) 182 | 183 | //scaffold common files 184 | contents := scaffolder.FSContents{ 185 | path.Join(root, "proton.yaml"): protonConfig, 186 | path.Join(root, "schema", "schema.yaml"): render("schema/schema.%s.yaml.go.tpl", schemaVars, tType), 187 | } 188 | 189 | //add proton template-specific content 190 | in := scaffoldInputData{ 191 | Contents: &contents, 192 | Name: name, 193 | Type: templateType, 194 | Shorthand: getTemplateTypeShorthand(templateType), 195 | RootDir: root, 196 | InfraDir: infraDir, 197 | Vars: schemaVars, 198 | TerraformS3StateBucket: terraformRemoteStateBucket, 199 | } 200 | 201 | if provisioning == provisioningTypeAWSManaged { 202 | addAWSManagedTemplateContent(in) 203 | } 204 | if provisioning == provisioningTypeCodeBuild { 205 | if flagNewTool == toolTerraform { 206 | addCBPTerraformTemplateContent(in) 207 | } 208 | } 209 | 210 | //populate the file system with the generated contents 211 | m = "writing to file system" 212 | debug(m) 213 | err = scaffolder.PopulateFS(outFS, contents) 214 | handleError(m, err) 215 | } 216 | 217 | func addAWSManagedTemplateContent(in scaffoldInputData) { 218 | 219 | addContent(in.Contents, in.RootDir, "README.md", 220 | "readme/%s.cfn.md", in.Shorthand) 221 | 222 | addContent(in.Contents, in.InfraDir, "manifest.yaml", 223 | "infrastructure/awsmanaged/manifest.yaml") 224 | 225 | addContent(in.Contents, in.InfraDir, "cloudformation.yaml", 226 | "infrastructure/awsmanaged/cloudformation.%s.yaml.jinja", in.Shorthand) 227 | } 228 | 229 | func addCBPTerraformTemplateContent(in scaffoldInputData) { 230 | contents := *in.Contents 231 | 232 | manifestData := terraformManifest{ 233 | TemplateType: in.Type, 234 | TemplateName: in.Name, 235 | TerraformS3StateBucket: in.TerraformS3StateBucket, 236 | } 237 | manifest := render("infrastructure/codebuild/terraform/manifest.yaml.go.tpl", manifestData) 238 | contents[path.Join(in.InfraDir, "manifest.yaml")] = manifest 239 | 240 | mainData := terraformMain{ 241 | ModuleName: in.Name, 242 | Variables: in.Vars, 243 | } 244 | contents[path.Join(in.InfraDir, "main.tf")] = 245 | render("infrastructure/codebuild/terraform/main.%s.tf.new.go.tpl", 246 | mainData.Variables[0], in.Shorthand) 247 | 248 | contents[path.Join(in.InfraDir, "outputs.tf")] = 249 | render("infrastructure/codebuild/terraform/outputs.tf.go.tpl", 250 | outputData{ModuleName: in.Name}) 251 | 252 | addContent(in.Contents, in.RootDir, "README.md", 253 | "readme/%s.tf.md", in.Shorthand) 254 | 255 | addContent(in.Contents, in.InfraDir, "variables.tf", 256 | "infrastructure/codebuild/terraform/variables.%s.tf", in.Shorthand) 257 | 258 | addContent(in.Contents, in.InfraDir, "output.sh", 259 | "infrastructure/codebuild/terraform/output.sh") 260 | 261 | addContent(in.Contents, in.InfraDir, "install-terraform.sh", 262 | "infrastructure/codebuild/terraform/install-terraform.sh") 263 | } 264 | -------------------------------------------------------------------------------- /cmd/new_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "path" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/hack-pad/hackpadfs" 11 | "github.com/hack-pad/hackpadfs/mem" 12 | "github.com/jritsema/scaffolder" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | func TestNewEnvironmentTemplateCloudFormation(t *testing.T) { 17 | 18 | //create in-memory file system for testing 19 | destFS, err := mem.NewFS() 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | name := "my-template" 25 | 26 | scaffoldProton( 27 | name, 28 | "environment", 29 | "awsmanaged", 30 | "", //tool 31 | "my-s3-bucket", 32 | "my-s3-bucket", 33 | []string{}, 34 | destFS, 35 | ) 36 | 37 | pathsToCheck := getExpectedOutputFiles(name, "environment", "awsmanaged", "") 38 | internalCheckPaths(t, destFS, pathsToCheck) 39 | } 40 | 41 | func TestNewServiceTemplateCloudFormation(t *testing.T) { 42 | 43 | //create in-memory file system for testing 44 | destFS, err := mem.NewFS() 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | 49 | name := "my-template" 50 | 51 | scaffoldProton( 52 | name, 53 | "service", 54 | "awsmanaged", 55 | "", //tool 56 | "my-s3-bucket", 57 | "my-s3-bucket", 58 | []string{"my-template:1"}, 59 | destFS, 60 | ) 61 | 62 | pathsToCheck := getExpectedOutputFiles(name, "service", "awsmanaged", "") 63 | internalCheckPaths(t, destFS, pathsToCheck) 64 | } 65 | 66 | func TestNewEnvironmentTemplateCodeBuildTerraform(t *testing.T) { 67 | 68 | //create in-memory file system for testing 69 | destFS, err := mem.NewFS() 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | 74 | name := "my-template" 75 | 76 | scaffoldProton( 77 | name, 78 | "environment", 79 | "codebuild", 80 | "terraform", 81 | "my-publish-bucket", 82 | "my-remote-state-bucket", 83 | []string{}, 84 | destFS, 85 | ) 86 | 87 | pathsToCheck := getExpectedOutputFiles(name, "environment", "codebuild", "terraform") 88 | internalCheckPaths(t, destFS, pathsToCheck) 89 | } 90 | 91 | func TestNewServiceTemplateCodeBuildTerraform(t *testing.T) { 92 | 93 | //create in-memory file system for testing 94 | destFS, err := mem.NewFS() 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | 99 | name := "my-template" 100 | 101 | scaffoldProton( 102 | name, 103 | "service", 104 | "codebuild", 105 | "terraform", 106 | "my-publish-bucket", 107 | "my-remote-state-bucket", 108 | []string{"my-env:1"}, 109 | destFS, 110 | ) 111 | 112 | pathsToCheck := getExpectedOutputFiles(name, "service", "codebuild", "terraform") 113 | internalCheckPaths(t, destFS, pathsToCheck) 114 | } 115 | 116 | func internalCheckPaths(t *testing.T, destFS fs.FS, pathsToCheck []string) { 117 | scaffolder.InspectFS(destFS, t.Log, false) 118 | 119 | t.Log("pathsToCheck") 120 | t.Log(pathsToCheck) 121 | 122 | found := 0 123 | findings := []string{} 124 | err := hackpadfs.WalkDir(destFS, ".", func(path string, d fs.DirEntry, err error) error { 125 | if err != nil { 126 | return err 127 | } 128 | if !d.IsDir() { 129 | findings = append(findings, path) 130 | } 131 | t.Log("looking for path in destFS", path) 132 | if SliceContains(&pathsToCheck, path, false) { 133 | found++ 134 | t.Log("found", path) 135 | } 136 | 137 | //test that any generated yaml is valid 138 | if (strings.HasSuffix(path, "yaml") || strings.HasSuffix(path, "yml")) && 139 | !strings.HasSuffix(path, "cloudformation.yaml") { 140 | t.Log("testing", path) 141 | contents, err := hackpadfs.ReadFile(destFS, path) 142 | t.Log(string(contents)) 143 | if err != nil { 144 | t.Error(err) 145 | } 146 | var data interface{} 147 | err = yaml.Unmarshal(contents, &data) 148 | if err != nil { 149 | t.Error("invalid generated yaml", err) 150 | } 151 | } 152 | return nil 153 | }) 154 | if err != nil { 155 | t.Error(err) 156 | } 157 | 158 | t.Log("expected paths:", len(pathsToCheck)) 159 | t.Log("actual paths:", len(findings)) 160 | 161 | if len(findings) != len(pathsToCheck) { 162 | t.Log() 163 | //show expected files that were not found 164 | for _, f := range pathsToCheck { 165 | if !SliceContains(&findings, f, false) { 166 | t.Log("missing", f) 167 | } 168 | } 169 | t.Log() 170 | //show found files that were not expected 171 | for _, f := range findings { 172 | if !SliceContains(&pathsToCheck, f, false) { 173 | t.Log("not expecting", f) 174 | } 175 | } 176 | t.Log() 177 | t.Error(errors.New("path counts don't match. did you add/remove something in the local templates directory?")) 178 | } 179 | } 180 | 181 | func getExpectedOutputFiles(name, templateType, provisioningMethod, tool string) []string { 182 | 183 | iDir := protonInfrastructureDirEnv 184 | if templateType == "service" { 185 | iDir = protonInfrastructureDirSvc 186 | } 187 | 188 | root := path.Join(name, "v1") 189 | schemaDir := path.Join(root, "schema") 190 | infraDir := path.Join(root, iDir) 191 | 192 | pathsToCheck := []string{ 193 | path.Join(root, "proton.yaml"), 194 | path.Join(root, "README.md"), 195 | path.Join(schemaDir, "schema.yaml"), 196 | path.Join(infraDir, "manifest.yaml"), 197 | } 198 | 199 | if provisioningMethod == provisioningTypeAWSManaged { 200 | pathsToCheck = append(pathsToCheck, path.Join(infraDir, "cloudformation.yaml")) 201 | 202 | } else if provisioningMethod == provisioningTypeCodeBuild && tool == "terraform" { 203 | pathsToCheck = append(pathsToCheck, path.Join(infraDir, "main.tf")) 204 | pathsToCheck = append(pathsToCheck, path.Join(infraDir, "variables.tf")) 205 | pathsToCheck = append(pathsToCheck, path.Join(infraDir, "outputs.tf")) 206 | pathsToCheck = append(pathsToCheck, path.Join(infraDir, "output.sh")) 207 | pathsToCheck = append(pathsToCheck, path.Join(infraDir, "install-terraform.sh")) 208 | } 209 | 210 | return pathsToCheck 211 | } 212 | -------------------------------------------------------------------------------- /cmd/protonize.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/hack-pad/hackpadfs" 10 | hackpados "github.com/hack-pad/hackpadfs/os" 11 | "github.com/hashicorp/terraform-config-inspect/tfconfig" 12 | "github.com/jritsema/scaffolder" 13 | "github.com/spf13/cobra" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | const ( 18 | protonInfrastructureDirEnv = "infrastructure" 19 | protonInfrastructureDirSvc = "instance_infrastructure" 20 | protonPipelineDirSvc = "pipeline_infrastructure" 21 | protonTFSrc = "src" 22 | provisioningTypeCodeBuild = "codebuild" 23 | toolTerraform = "terraform" 24 | provisioningTypeAWSManaged = "awsmanaged" 25 | ) 26 | 27 | var ( 28 | 29 | //cli flags 30 | flagProtonizeName string 31 | flagProtonizeSrcDir string 32 | flagProtonizeOutDir string 33 | flagProtonizeProvisoning string 34 | flagProtonizeTool string 35 | flagProtonizeTemplateType string 36 | flagProtonizePublish bool 37 | flagProtonizeTerraformRemoteStateBucket string 38 | flagProtonizePublishBucket string 39 | flagProtonizeCompatibleEnvs []string 40 | 41 | tfEnvInfraSrcDir string 42 | tfSvcInfraSrcDir string 43 | ) 44 | 45 | // upCmd represents the up command 46 | var templateProtonizeCmd = &cobra.Command{ 47 | Use: "protonize", 48 | Short: "Protonize converts existing IaC to Proton", 49 | Long: `Protonize converts existing IaC to Proton's format so that it can be published. 50 | Currently only supports Terraform using CodeBuild provisioning.`, 51 | Run: doTemplateProtonize, 52 | Example: ` 53 | # Convert existing Terraform into a Proton environment template 54 | protonizer protonize \ 55 | --name my_template \ 56 | --type environment \ 57 | --dir ~/my-existing-tf-module 58 | 59 | # Convert existing Terraform into a Proton service template and publish it 60 | protonizer protonize \ 61 | --name my_template \ 62 | --type service \ 63 | --compatible-env env1:1 --compatible-env env2:1 \ 64 | --provisioning codebuild --tool terraform \ 65 | --dir ~/my-existing-tf-module \ 66 | --bucket my-s3-bucket \ 67 | --publish`, 68 | } 69 | 70 | type generateInput struct { 71 | name string 72 | templateType string 73 | srcDir string 74 | srcFS hackpadfs.FS 75 | destFS hackpadfs.FS 76 | publishBucket string 77 | terraformRemoteStateBucket string 78 | compatibleEnvironments []string 79 | } 80 | 81 | type schemaVariable struct { 82 | Name string 83 | Title string 84 | Type string 85 | Description string 86 | Default interface{} 87 | Required bool 88 | ArrayType string 89 | } 90 | 91 | type outputData struct { 92 | ModuleName string 93 | Outputs []tfconfig.Output 94 | } 95 | 96 | type terraformManifest struct { 97 | TemplateName string 98 | TemplateType string 99 | TerraformS3StateBucket string 100 | } 101 | 102 | type terraformMain struct { 103 | ModuleName string 104 | Variables []schemaVariable 105 | } 106 | 107 | func init() { 108 | templateProtonizeCmd.Flags().StringVarP(&flagProtonizeName, "name", "n", "", "The name of the template") 109 | templateProtonizeCmd.MarkFlagRequired("name") 110 | 111 | templateProtonizeCmd.Flags().StringVarP(&flagProtonizeTemplateType, "type", "t", "environment", 112 | "Template type: environment or service") 113 | 114 | templateProtonizeCmd.Flags().StringVarP(&flagProtonizeSrcDir, "dir", "s", "", 115 | "The source directory of the template to parse") 116 | templateProtonizeCmd.MarkFlagRequired("dir") 117 | 118 | templateProtonizeCmd.Flags().StringVarP(&flagProtonizeOutDir, "out", "o", ".", 119 | "The directory to output the protonized template. Defaults to the current directory") 120 | 121 | templateProtonizeCmd.Flags().StringVarP(&flagProtonizeProvisoning, "provisioning", "p", 122 | provisioningTypeCodeBuild, "The provisioning mode to use") 123 | 124 | templateProtonizeCmd.Flags().StringVar(&flagProtonizeTool, "tool", toolTerraform, 125 | "The tool to use. Currently, only Terraform is supported") 126 | 127 | templateProtonizeCmd.Flags().BoolVar(&flagProtonizePublish, "publish", false, 128 | "Whether or not to publish the protonized template") 129 | 130 | templateProtonizeCmd.Flags().StringVarP(&flagProtonizePublishBucket, "publish-bucket", "b", "", 131 | "The S3 bucket to use for template publishing. This is optional if not using the publish command.") 132 | 133 | templateProtonizeCmd.Flags().StringVar(&flagProtonizeTerraformRemoteStateBucket, "terraform-remote-state-bucket", "", 134 | "The S3 bucket to use for storing Terraform remote state. This is required for --provisioning codebuild and --tool terraform") 135 | 136 | templateProtonizeCmd.Flags().StringArrayVar(&flagProtonizeCompatibleEnvs, "compatible-env", []string{}, 137 | `Proton environments (name:majorversion) that the service template is compatible with. 138 | You may specify any number of environments by repeating --compatible-env before each one`) 139 | 140 | rootCmd.AddCommand(templateProtonizeCmd) 141 | 142 | //env and svc specific TF src directories 143 | tfEnvInfraSrcDir = path.Join(protonInfrastructureDirEnv, protonTFSrc) 144 | tfSvcInfraSrcDir = path.Join(protonInfrastructureDirSvc, protonTFSrc) 145 | } 146 | 147 | func doTemplateProtonize(cmd *cobra.Command, args []string) { 148 | 149 | //check required args 150 | 151 | if !(flagProtonizeTemplateType == "environment" || flagProtonizeTemplateType == "service") { 152 | errorExit(fmt.Sprintf("template type: %s is invalid. only environment and service are supported", 153 | flagProtonizeTemplateType)) 154 | } 155 | 156 | if flagProtonizeTemplateType == "service" && len(flagProtonizeCompatibleEnvs) == 0 { 157 | errorExit("--compatible-env is required for service templates") 158 | } 159 | 160 | if flagProtonizeProvisoning != provisioningTypeCodeBuild { 161 | errorExit("currently the only provisioning type supported is", provisioningTypeCodeBuild) 162 | } 163 | 164 | if flagProtonizeTool != toolTerraform { 165 | errorExit("currently the only provisioning type supported is", toolTerraform) 166 | } 167 | 168 | if flagProtonizeProvisoning == "CodeBuild" && flagProtonizeTool == toolTerraform && 169 | flagProtonizeTerraformRemoteStateBucket == "" { 170 | errorExit("--terraform-remote-state-bucket is required for --provisioning codebuild and --tool terraform") 171 | } 172 | 173 | //create an os file system rooted at output path 174 | //the scaffold function will write to this file system 175 | osfs := hackpados.NewFS() 176 | 177 | //we will output to this file system 178 | out, err := filepath.Abs(flagProtonizeOutDir) 179 | handleError("getting absolute path of out dir", err) 180 | fsPath, err := osfs.FromOSPath(out) 181 | handleError("FromOSPath", err) 182 | m := "creating out file system: " + fsPath 183 | debug(m) 184 | outFS, err := osfs.Sub(fsPath) 185 | handleError(m, err) 186 | 187 | //we will copy the user's terraform source into this file system 188 | sDir, err := filepath.Abs(flagProtonizeSrcDir) 189 | handleError("getting absolute path of src dir", err) 190 | fsPath, err = osfs.FromOSPath(sDir) 191 | handleError("FromOSPath", err) 192 | m = "creating src file system: " + fsPath 193 | debug(m) 194 | srcFS, err := osfs.Sub(fsPath) 195 | handleError(m, err) 196 | 197 | //generate proton template 198 | input := generateInput{ 199 | name: flagProtonizeName, 200 | templateType: flagProtonizeTemplateType, 201 | srcDir: sDir, 202 | srcFS: srcFS, 203 | destFS: outFS, 204 | publishBucket: flagProtonizePublishBucket, 205 | terraformRemoteStateBucket: flagProtonizeTerraformRemoteStateBucket, 206 | compatibleEnvironments: flagProtonizeCompatibleEnvs, 207 | } 208 | err = generateCodeBuildTerraformTemplate(input) 209 | handleError("generating template", err) 210 | 211 | templateDir := path.Join(out, flagProtonizeName) 212 | fmt.Println("template source outputted to", templateDir) 213 | 214 | if flagProtonizePublish { 215 | publishTemplate(path.Join(templateDir, "v1", "proton.yaml")) 216 | } 217 | 218 | fmt.Println("done") 219 | } 220 | 221 | // generates a proton template and returns the outputted template directory 222 | func generateCodeBuildTerraformTemplate(in generateInput) error { 223 | debug("name =", in.name) 224 | 225 | //create datasets that gets fed into templates 226 | 227 | //parse input/output variables 228 | vars, outputs := parseTerraformSource(in.name, in.srcDir) 229 | 230 | mainData := terraformMain{ 231 | ModuleName: in.name, 232 | Variables: vars, 233 | } 234 | 235 | manifestData := terraformManifest{ 236 | TemplateName: in.name, 237 | TerraformS3StateBucket: in.terraformRemoteStateBucket, 238 | TemplateType: string(in.templateType), 239 | } 240 | 241 | //codegen proton config 242 | protonData := protonConfigData{ 243 | Name: in.name, 244 | Type: string(in.templateType), 245 | DisplayName: in.name, 246 | Description: fmt.Sprintf("A %s template generated from %s", in.templateType, in.name), 247 | PublishBucket: flagProtonizePublishBucket, 248 | CompatibleEnvironments: in.compatibleEnvironments, 249 | } 250 | protonConfig, err := yaml.Marshal(protonData) 251 | handleError("marshalling proton config yaml", err) 252 | 253 | tType := getTemplateTypeShorthand(in.templateType) 254 | root := path.Join(in.name, "v1") 255 | infraDir := path.Join(root, getInfrastructureDirectory(string(in.templateType))) 256 | 257 | contents := scaffolder.FSContents{ 258 | path.Join(root, "README.md"): readTemplateFS("readme/%s.tf.md", tType), 259 | path.Join(root, "proton.yaml"): protonConfig, 260 | path.Join(root, "schema/schema.yaml"): render("schema/schema.%s.yaml.go.tpl", vars, tType), 261 | path.Join(infraDir, "manifest.yaml"): render("infrastructure/codebuild/terraform/manifest.yaml.go.tpl", manifestData), 262 | path.Join(infraDir, "main.tf"): render("infrastructure/codebuild/terraform/main.%s.tf.go.tpl", mainData, tType), 263 | path.Join(infraDir, "outputs.tf"): render("infrastructure/codebuild/terraform/outputs.tf.go.tpl", outputs), 264 | path.Join(infraDir, "output.sh"): readTemplateFS("infrastructure/codebuild/terraform/output.sh"), 265 | path.Join(infraDir, "variables.tf"): readTemplateFS("infrastructure/codebuild/terraform/variables.%s.tf", tType), 266 | path.Join(infraDir, "install-terraform.sh"): readTemplateFS("infrastructure/codebuild/terraform/install-terraform.sh"), 267 | } 268 | 269 | //populate the file system with the generated contents 270 | err = scaffolder.PopulateFS(in.destFS, contents) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | //copy terraform src filesystem to infrastructure/src 276 | outDir := path.Join(infraDir, protonTFSrc) 277 | destFS, err := hackpadfs.Sub(in.destFS, outDir) 278 | handleError("creating file system", err) 279 | m := "copying filesystem" 280 | debug(m) 281 | err = scaffolder.CopyFS(in.srcFS, destFS) 282 | handleError(m, err) 283 | 284 | return nil 285 | } 286 | 287 | // returns the name of the infrastructure directory based on the template type 288 | func getInfrastructureDirectory(templateType string) string { 289 | if templateType == "environment" { 290 | return protonInfrastructureDirEnv 291 | } 292 | return protonInfrastructureDirSvc 293 | } 294 | 295 | // returns the template type shorthand (env or svc) 296 | func getTemplateTypeShorthand(templateType string) string { 297 | if templateType == "environment" { 298 | return "env" 299 | } 300 | return "svc" 301 | } 302 | 303 | func parseTerraformSource(name, srcDir string) ([]schemaVariable, outputData) { 304 | 305 | m := "parsing terraform module: " + srcDir 306 | debug(m) 307 | module, diags := tfconfig.LoadModule(srcDir) 308 | if err := diags.Err(); err != nil { 309 | handleError(m, err) 310 | } 311 | debug("\n") 312 | debugFmt("found %v variables", len(module.Variables)) 313 | debug("\n") 314 | 315 | //sort variables by name 316 | inputVars := sortTFVariables(module) 317 | 318 | //map tf variables to openapi properties 319 | vars := []schemaVariable{} 320 | for _, v := range inputVars { 321 | debugFmt("%v (type: %v; default: %v) \n", v.Name, v.Type, v.Default) 322 | 323 | //escape quotes in descriptions 324 | desc := strings.Replace(v.Description, `"`, `\"`, -1) 325 | 326 | sv := schemaVariable{ 327 | Name: v.Name, 328 | Title: v.Name, 329 | Type: v.Type, 330 | Description: desc, 331 | Default: v.Default, 332 | Required: v.Required, 333 | } 334 | 335 | //default values 336 | if v.Default != nil { 337 | sv.Default = v.Default 338 | } 339 | 340 | if v.Type == "bool" { 341 | sv.Type = "boolean" 342 | } 343 | 344 | //list(x) -> array of x 345 | if strings.HasPrefix(sv.Type, "list(") { 346 | sv.Type = "array" 347 | sv.ArrayType = strings.Split(v.Type, "list(")[1] 348 | sv.ArrayType = sv.ArrayType[:len(sv.ArrayType)-1] 349 | sv.Default = nil 350 | } 351 | 352 | //output warning for unsupported types 353 | if strings.HasPrefix(sv.Type, "object(") || 354 | strings.HasPrefix(sv.Type, "any") || 355 | strings.HasPrefix(sv.Type, "map(") || 356 | strings.HasPrefix(sv.Type, "set(") { 357 | 358 | fmt.Println("WARNING: skipping unsupported input variable:") 359 | fmt.Println(v.Name) 360 | fmt.Println(v.Type) 361 | fmt.Println() 362 | continue 363 | } 364 | 365 | vars = append(vars, sv) 366 | } 367 | 368 | //debug 369 | if verbose { 370 | debug("\n") 371 | debugFmt("found %v outputs", len(module.Outputs)) 372 | debug("\n") 373 | for _, o := range module.Outputs { 374 | debugFmt("%v (description: %v) \n", o.Name, o.Description) 375 | } 376 | } 377 | 378 | //return output 379 | outputs := outputData{ModuleName: name} 380 | outputs.Outputs = sortTFOutputs(module) 381 | 382 | return vars, outputs 383 | } 384 | -------------------------------------------------------------------------------- /cmd/protonize_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/hack-pad/hackpadfs" 13 | "github.com/hack-pad/hackpadfs/mem" 14 | "github.com/jritsema/scaffolder" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | func TestGenerateEnvironmentTemplate(t *testing.T) { 19 | internalTestGenerateTemplate(t, "environment", protonInfrastructureDirEnv, tfEnvInfraSrcDir) 20 | } 21 | 22 | // tests that reserved variables are mapped properly 23 | func TestGenerateEnvironmentTemplate_ReservedVar(t *testing.T) { 24 | 25 | result := internalTestGenerateTemplate(t, "environment", protonInfrastructureDirEnv, tfEnvInfraSrcDir) 26 | 27 | f, err := result.Open("my_template/v1/infrastructure/main.tf") 28 | if err != nil { 29 | t.Error(err) 30 | } 31 | defer f.Close() 32 | b, err := ioutil.ReadAll(f) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | contents := string(b) 37 | t.Log(contents) 38 | if strings.Contains(contents, "name = var.environment.inputs.name") { 39 | t.Error("name variable should not be mapped to environment.inputs") 40 | } 41 | 42 | f, err = result.Open("my_template/v1/schema/schema.yaml") 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | defer f.Close() 47 | b, err = ioutil.ReadAll(f) 48 | if err != nil { 49 | t.Error(err) 50 | } 51 | contents = string(b) 52 | t.Log(contents) 53 | lines := strings.Split(contents, "\n") 54 | if SliceContains(&lines, "title: name", true) { 55 | t.Error("schema should not contain variable `name`") 56 | } 57 | } 58 | 59 | // tests that reserved variables are mapped properly 60 | func TestGenerateServiceTemplate_ReservedVar(t *testing.T) { 61 | 62 | result := internalTestGenerateTemplate(t, "service", protonInfrastructureDirSvc, tfSvcInfraSrcDir) 63 | 64 | f, err := result.Open("my_template/v1/instance_infrastructure/main.tf") 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | defer f.Close() 69 | b, err := ioutil.ReadAll(f) 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | contents := string(b) 74 | t.Log(contents) 75 | if strings.Contains(contents, "name = var.service_instance.inputs.name") { 76 | t.Error("name variable should not be mapped to service_instance.inputs") 77 | } 78 | if strings.Contains(contents, "environment = var.service_instance.inputs.environment") { 79 | t.Error("environment variable should not be mapped to service_instance.inputs") 80 | } 81 | 82 | f, err = result.Open("my_template/v1/schema/schema.yaml") 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | defer f.Close() 87 | b, err = ioutil.ReadAll(f) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | contents = string(b) 92 | t.Log(contents) 93 | lines := strings.Split(contents, "\n") 94 | if SliceContains(&lines, "title: name", true) { 95 | t.Error("schema should not contain variable `name`") 96 | } 97 | if SliceContains(&lines, "title: environment", true) { 98 | t.Error("schema should not contain variable `environment`") 99 | } 100 | } 101 | 102 | func TestGenerateServiceTemplate(t *testing.T) { 103 | internalTestGenerateTemplate(t, "service", protonInfrastructureDirSvc, tfSvcInfraSrcDir) 104 | } 105 | 106 | func internalTestGenerateTemplate(t *testing.T, templateType string, infraDir, infraSrcDir string) hackpadfs.FS { 107 | 108 | //create in memory file system for testing 109 | srcFS, err := mem.NewFS() 110 | if err != nil { 111 | t.Error(err) 112 | } 113 | 114 | //populate source fs with user content 115 | userFiles := scaffolder.FSContents{ 116 | "source1.tf": []byte(""), 117 | "dir1/source1.tf": []byte(""), 118 | "dir2/source1.tf": []byte(""), 119 | "dir2/source2.tf": []byte(""), 120 | } 121 | err = scaffolder.PopulateFS(srcFS, userFiles) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | 126 | //create destination file system (in-memory) 127 | destFS, err := mem.NewFS() 128 | if err != nil { 129 | t.Error(err) 130 | } 131 | workDir, _ := os.Getwd() 132 | 133 | //test generateTemplate (in-memory) 134 | name := "my_template" 135 | input := generateInput{ 136 | name: name, 137 | templateType: templateType, 138 | srcDir: path.Join(workDir, "test"), 139 | srcFS: srcFS, 140 | destFS: destFS, 141 | } 142 | err = generateCodeBuildTerraformTemplate(input) 143 | if err != nil { 144 | t.Error(err) 145 | } 146 | 147 | err = scaffolder.InspectFS(destFS, t.Log, false) 148 | if err != nil { 149 | t.Error(err) 150 | } 151 | 152 | pathsToCheck := getExpectedOutputFiles(name, templateType, "codebuild", "terraform") 153 | 154 | //add user files 155 | for file := range userFiles { 156 | pathsToCheck = append(pathsToCheck, path.Join(name, "v1", infraSrcDir, file)) 157 | } 158 | 159 | t.Log("pathsToCheck") 160 | t.Log(pathsToCheck) 161 | 162 | found := 0 163 | findings := []string{} 164 | err = hackpadfs.WalkDir(destFS, ".", func(path string, d fs.DirEntry, err error) error { 165 | if err != nil { 166 | return err 167 | } 168 | if !d.IsDir() { 169 | findings = append(findings, path) 170 | } 171 | t.Log("looking for path in destFS", path) 172 | if SliceContains(&pathsToCheck, path, false) { 173 | found++ 174 | t.Log("found", path) 175 | } 176 | 177 | //test that any generated yaml is valid 178 | if strings.HasSuffix(path, "yaml") || strings.HasSuffix(path, "yml") { 179 | t.Log("testing", path) 180 | contents, err := hackpadfs.ReadFile(destFS, path) 181 | t.Log(string(contents)) 182 | if err != nil { 183 | t.Error(err) 184 | } 185 | var data interface{} 186 | err = yaml.Unmarshal(contents, &data) 187 | if err != nil { 188 | t.Error("invalid generated yaml", err) 189 | } 190 | } 191 | return nil 192 | }) 193 | if err != nil { 194 | t.Error(err) 195 | } 196 | t.Log("expected paths:", len(pathsToCheck)) 197 | t.Log("actual paths:", len(findings)) 198 | 199 | if len(findings) != len(pathsToCheck) { 200 | t.Log() 201 | //show expected files that were not found 202 | for _, f := range pathsToCheck { 203 | if !SliceContains(&findings, f, false) { 204 | t.Log("missing", f) 205 | } 206 | } 207 | t.Log() 208 | //show found files that were not expected 209 | for _, f := range findings { 210 | if !SliceContains(&pathsToCheck, f, false) { 211 | t.Log("not expecting", f) 212 | } 213 | } 214 | t.Log() 215 | t.Error(errors.New("path counts don't match. did you add/remove something in the local templates directory?")) 216 | } 217 | 218 | return destFS 219 | } 220 | -------------------------------------------------------------------------------- /cmd/publish.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "strings" 12 | "time" 13 | 14 | "github.com/aws/aws-sdk-go-v2/aws" 15 | "github.com/aws/aws-sdk-go-v2/config" 16 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 17 | "github.com/aws/aws-sdk-go-v2/service/proton" 18 | "github.com/aws/aws-sdk-go-v2/service/proton/types" 19 | "github.com/aws/aws-sdk-go-v2/service/s3" 20 | "github.com/spf13/cobra" 21 | "gopkg.in/yaml.v3" 22 | ) 23 | 24 | var ( 25 | flagTemplatePublishFile string 26 | ) 27 | 28 | type protonConfigData struct { 29 | Name string `yaml:"name"` 30 | Type string `yaml:"type"` 31 | DisplayName string `yaml:"displayName"` 32 | Description string `yaml:"description"` 33 | 34 | //optional 35 | PublishBucket string `yaml:"publishBucket,omitempty"` 36 | CompatibleEnvironments []string `yaml:"compatibleEnvironments,omitempty"` 37 | } 38 | 39 | var templatePublishCmd = &cobra.Command{ 40 | Use: "publish", 41 | Short: "Publishes proton templates", 42 | Long: "Publishes proton templates", 43 | Run: doTemplatePublish, 44 | } 45 | 46 | func init() { 47 | log.SetFlags(log.LstdFlags | log.Lmicroseconds) 48 | templatePublishCmd.Flags().StringVarP(&flagTemplatePublishFile, "file", "f", "proton.yaml", "The proton yaml file to use") 49 | rootCmd.AddCommand(templatePublishCmd) 50 | } 51 | 52 | func getAWSConfig() aws.Config { 53 | ctx := context.Background() 54 | cfg, err := config.LoadDefaultConfig(ctx) 55 | handleError("aws config", err) 56 | return cfg 57 | } 58 | 59 | func doTemplatePublish(cmd *cobra.Command, args []string) { 60 | publishTemplate(flagTemplatePublishFile) 61 | } 62 | 63 | func publishTemplate(file string) { 64 | 65 | //parse proton.yaml 66 | protonConfig, err := readProtonYAMLFile(file) 67 | if err != nil { 68 | errorExit(fmt.Errorf("could not read proton.yaml: %w", err)) 69 | } 70 | 71 | if protonConfig.PublishBucket == "" { 72 | errorExit("The `publishBucket` key is not specified in proton.yaml. This setting is required for publishing.") 73 | } 74 | 75 | if region := os.Getenv("AWS_REGION"); region == "" { 76 | errorExit(`Please specify the AWS region by setting the "AWS_REGION" environment variable. 77 | 78 | For example: 79 | export AWS_REGION=us-east-1 80 | OR 81 | AWS_REGION=us-east-1 protonizer publish`) 82 | } 83 | 84 | //tar gz template 85 | //assume template bundle is in the same directory as the proton.yaml file 86 | dir := filepath.Dir(file) 87 | zipFileName := "bundle.tar.gz" 88 | zipPath := path.Join(dir, zipFileName) 89 | m := "creating template bundle: " + zipPath 90 | debug(m) 91 | err = createTarGZFile(dir, zipPath) 92 | handleError(m, err) 93 | 94 | cfg := getAWSConfig() 95 | ctx := context.Background() 96 | 97 | //upload to s3 98 | bucket := protonConfig.PublishBucket 99 | key := zipFileName 100 | 101 | s3Client := s3.NewFromConfig(cfg) 102 | 103 | m = fmt.Sprintf("uploading template bundle to s3://%s/%s", bucket, key) 104 | debug(m) 105 | f, err := os.Open(zipPath) 106 | handleError("opening zip", err) 107 | uploader := manager.NewUploader(s3Client) 108 | _, err = uploader.Upload(ctx, &s3.PutObjectInput{ 109 | Bucket: &bucket, 110 | Key: &key, 111 | Body: f, 112 | }) 113 | handleError(m, err) 114 | 115 | //delete local zip file 116 | err = os.Remove(zipPath) 117 | handleError("removing zip file", err) 118 | 119 | //publish 120 | var majorVersion, minorVersion string 121 | 122 | switch protonConfig.Type { 123 | 124 | case "environment": 125 | majorVersion, minorVersion = publishEnvironmentTemplate(cfg, protonConfig, key, ctx) 126 | 127 | case "service": 128 | majorVersion, minorVersion = publishServiceTemplate(cfg, protonConfig, key, ctx) 129 | } 130 | fmt.Printf("published %s:%s.%s \n", protonConfig.Name, majorVersion, minorVersion) 131 | 132 | //output console url of published template 133 | fmt.Printf("https://%s.console.aws.amazon.com/proton/home#/templates/%vs/detail/%s\n", 134 | cfg.Region, protonConfig.Type, protonConfig.Name) 135 | } 136 | 137 | func publishEnvironmentTemplate(cfg aws.Config, protonConfig *protonConfigData, s3Key string, ctx context.Context) (string, string) { 138 | 139 | //publish proton template and version 140 | protonClient := proton.NewFromConfig(cfg) 141 | 142 | reqTemplate := &proton.CreateEnvironmentTemplateInput{ 143 | Name: &protonConfig.Name, 144 | Description: &protonConfig.Description, 145 | DisplayName: &protonConfig.DisplayName, 146 | Tags: []types.Tag{ 147 | { 148 | Key: aws.String("creator"), 149 | Value: aws.String("protonizer-cli"), 150 | }, 151 | }, 152 | } 153 | m := "proton.CreateEnvironmentTemplate()" 154 | debug(m) 155 | _, err := protonClient.CreateEnvironmentTemplate(ctx, reqTemplate) 156 | handleError(m, err) 157 | 158 | //publish version 159 | majorVesion := "1" 160 | 161 | s3Source := types.TemplateVersionSourceInputMemberS3{ 162 | Value: types.S3ObjectSource{ 163 | Bucket: &protonConfig.PublishBucket, 164 | Key: &s3Key, 165 | }, 166 | } 167 | reqVersion := &proton.CreateEnvironmentTemplateVersionInput{ 168 | TemplateName: &protonConfig.Name, 169 | MajorVersion: &majorVesion, 170 | Source: &s3Source, 171 | } 172 | m = "proton.CreateEnvironmentTemplateVersion()" 173 | debug(m) 174 | templateVersion, err := protonClient.CreateEnvironmentTemplateVersion(ctx, reqVersion) 175 | handleError(m, err) 176 | 177 | if templateVersion.EnvironmentTemplateVersion.MinorVersion != nil { 178 | debug("minor version =", *templateVersion.EnvironmentTemplateVersion.MinorVersion) 179 | } 180 | debug(templateVersion.EnvironmentTemplateVersion.Status) 181 | if templateVersion.EnvironmentTemplateVersion.StatusMessage != nil { 182 | debug(*templateVersion.EnvironmentTemplateVersion.StatusMessage) 183 | } 184 | 185 | debug("waiting for registration to complete") 186 | 187 | //wait for version to be available then get the minor version 188 | minorVersion := "" 189 | for { 190 | m = "proton.GetEnvironmentTemplate()" 191 | debug(m) 192 | ver, err := protonClient.GetEnvironmentTemplateVersion(ctx, &proton.GetEnvironmentTemplateVersionInput{ 193 | TemplateName: &protonConfig.Name, 194 | MajorVersion: &majorVesion, 195 | MinorVersion: templateVersion.EnvironmentTemplateVersion.MinorVersion, 196 | }) 197 | handleError(m, err) 198 | debug(ver.EnvironmentTemplateVersion.Status) 199 | if ver.EnvironmentTemplateVersion.StatusMessage != nil { 200 | debug(*ver.EnvironmentTemplateVersion.StatusMessage) 201 | } 202 | 203 | if ver.EnvironmentTemplateVersion.Status == types.TemplateVersionStatusRegistrationFailed { 204 | errorExit(*ver.EnvironmentTemplateVersion.StatusMessage) 205 | } 206 | 207 | if ver.EnvironmentTemplateVersion.Status == types.TemplateVersionStatusDraft { 208 | minorVersion = *ver.EnvironmentTemplateVersion.MinorVersion 209 | debugFmt("template version %s.%s now in %s", majorVesion, minorVersion, ver.EnvironmentTemplateVersion.Status) 210 | break 211 | } 212 | time.Sleep(2 * time.Second) 213 | } 214 | 215 | desc := "published by proton cli" 216 | m = "proton.UpdateEnvironmentTemplateVersion" 217 | debug(m) 218 | _, err = protonClient.UpdateEnvironmentTemplateVersion(ctx, &proton.UpdateEnvironmentTemplateVersionInput{ 219 | TemplateName: &protonConfig.Name, 220 | MajorVersion: &majorVesion, 221 | MinorVersion: &minorVersion, 222 | Status: types.TemplateVersionStatusPublished, 223 | Description: &desc, 224 | }) 225 | handleError(m, err) 226 | 227 | return majorVesion, minorVersion 228 | } 229 | 230 | func publishServiceTemplate(cfg aws.Config, protonConfig *protonConfigData, s3Key string, ctx context.Context) (string, string) { 231 | 232 | //publish proton template and version 233 | protonClient := proton.NewFromConfig(cfg) 234 | 235 | reqTemplate := &proton.CreateServiceTemplateInput{ 236 | Name: &protonConfig.Name, 237 | Description: &protonConfig.Description, 238 | DisplayName: &protonConfig.DisplayName, 239 | PipelineProvisioning: types.ProvisioningCustomerManaged, 240 | Tags: []types.Tag{ 241 | { 242 | Key: aws.String("creator"), 243 | Value: aws.String("protonizer-cli"), 244 | }, 245 | }, 246 | } 247 | m := "proton.CreateServiceTemplate()" 248 | debug(m) 249 | _, err := protonClient.CreateServiceTemplate(ctx, reqTemplate) 250 | handleError(m, err) 251 | 252 | //publish version 253 | majorVesion := "1" 254 | 255 | s3Source := types.TemplateVersionSourceInputMemberS3{ 256 | Value: types.S3ObjectSource{ 257 | Bucket: &protonConfig.PublishBucket, 258 | Key: &s3Key, 259 | }, 260 | } 261 | reqVersion := &proton.CreateServiceTemplateVersionInput{ 262 | TemplateName: &protonConfig.Name, 263 | MajorVersion: &majorVesion, 264 | Source: &s3Source, 265 | CompatibleEnvironmentTemplates: []types.CompatibleEnvironmentTemplateInput{}, 266 | } 267 | 268 | for _, c := range protonConfig.CompatibleEnvironments { 269 | parts := strings.Split(c, ":") 270 | if len(parts) != 2 { 271 | errorExit("compatible environments must use the format: `name:version`") 272 | } 273 | reqVersion.CompatibleEnvironmentTemplates = append(reqVersion.CompatibleEnvironmentTemplates, types.CompatibleEnvironmentTemplateInput{ 274 | TemplateName: aws.String(parts[0]), 275 | MajorVersion: aws.String(parts[1]), 276 | }) 277 | } 278 | 279 | m = "proton.CreateServiceTemplateVersion()" 280 | debug(m) 281 | templateVersion, err := protonClient.CreateServiceTemplateVersion(ctx, reqVersion) 282 | if err != nil { 283 | if strings.Contains(err.Error(), "ValidationException") { 284 | errorExit(err) 285 | } else { 286 | handleError(m, err) 287 | } 288 | } 289 | 290 | if templateVersion.ServiceTemplateVersion.MinorVersion != nil { 291 | debug("minor version =", *templateVersion.ServiceTemplateVersion.MinorVersion) 292 | } 293 | debug(templateVersion.ServiceTemplateVersion.Status) 294 | if templateVersion.ServiceTemplateVersion.StatusMessage != nil { 295 | debug(*templateVersion.ServiceTemplateVersion.StatusMessage) 296 | } 297 | 298 | debug("waiting for registration to complete") 299 | 300 | //wait for version to be available then get the minor version 301 | minorVersion := "" 302 | for { 303 | m = "proton.GetServiceTemplate()" 304 | debug(m) 305 | ver, err := protonClient.GetServiceTemplateVersion(ctx, &proton.GetServiceTemplateVersionInput{ 306 | TemplateName: &protonConfig.Name, 307 | MajorVersion: &majorVesion, 308 | MinorVersion: templateVersion.ServiceTemplateVersion.MinorVersion, 309 | }) 310 | handleError(m, err) 311 | debug(ver.ServiceTemplateVersion.Status) 312 | if ver.ServiceTemplateVersion.StatusMessage != nil { 313 | debug(*ver.ServiceTemplateVersion.StatusMessage) 314 | } 315 | 316 | if ver.ServiceTemplateVersion.Status == types.TemplateVersionStatusRegistrationFailed { 317 | errorExit(*ver.ServiceTemplateVersion.StatusMessage) 318 | } 319 | 320 | if ver.ServiceTemplateVersion.Status == types.TemplateVersionStatusDraft { 321 | minorVersion = *ver.ServiceTemplateVersion.MinorVersion 322 | debugFmt("template version %s.%s now in %s", majorVesion, minorVersion, ver.ServiceTemplateVersion.Status) 323 | break 324 | } 325 | time.Sleep(2 * time.Second) 326 | } 327 | 328 | desc := "published by proton cli" 329 | m = "proton.UpdateEnvironmentTemplateVersion" 330 | debug(m) 331 | _, err = protonClient.UpdateServiceTemplateVersion(ctx, &proton.UpdateServiceTemplateVersionInput{ 332 | TemplateName: &protonConfig.Name, 333 | MajorVersion: &majorVesion, 334 | MinorVersion: &minorVersion, 335 | Status: types.TemplateVersionStatusPublished, 336 | Description: &desc, 337 | }) 338 | handleError(m, err) 339 | 340 | return majorVesion, minorVersion 341 | } 342 | 343 | func readProtonYAMLFile(fileName string) (*protonConfigData, error) { 344 | 345 | yamlFile, err := os.Open(fileName) 346 | if err != nil { 347 | return nil, fmt.Errorf("could not open file: %s: %w", fileName, err) 348 | } 349 | defer yamlFile.Close() 350 | b, err := io.ReadAll(yamlFile) 351 | if err != nil { 352 | return nil, fmt.Errorf("could not read file: %s : %w", fileName, err) 353 | } 354 | var result protonConfigData 355 | err = yaml.Unmarshal(b, &result) 356 | if err != nil { 357 | return nil, fmt.Errorf("unmarshaling file: %s : %w", fileName, err) 358 | } 359 | return &result, nil 360 | } 361 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "embed" 5 | "log" 6 | "os" 7 | "text/template" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | //go:embed templates/* 13 | var templateFS embed.FS 14 | 15 | // verbose logging enabled 16 | var verbose = false 17 | 18 | // parsed scaffold templates 19 | var scaffoldTemplates *template.Template 20 | 21 | // rootCmd represents the base command when called without any subcommands 22 | var rootCmd = &cobra.Command{ 23 | Use: "protonizer", 24 | Short: "A CLI tool for working with IaC in AWS Proton.", 25 | Long: "A CLI tool for working with IaC in AWS Proton.", 26 | } 27 | 28 | // Execute adds all child commands to the root command and sets flags appropriately. 29 | // This is called by main.main(). It only needs to happen once to the rootCmd. 30 | func Execute(version string) { 31 | rootCmd.Version = version 32 | err := rootCmd.Execute() 33 | if err != nil { 34 | os.Exit(1) 35 | } 36 | } 37 | 38 | func init() { 39 | log.SetFlags(log.LstdFlags | log.Lmicroseconds) 40 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") 41 | 42 | //parse go templates 43 | debug("parsing go templates") 44 | var err error 45 | scaffoldTemplates, err = templateParseFSRecursive(templateFS, ".tpl", nil) 46 | handleError("error parsing go templates", err) 47 | debugFmt("defined templates: %v", scaffoldTemplates.DefinedTemplates()) 48 | } 49 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/awsmanaged/cloudformation.env.yaml.jinja: -------------------------------------------------------------------------------- 1 | Resources: 2 | S3Bucket: 3 | Type: 'AWS::S3::Bucket' 4 | DeletionPolicy: Retain 5 | Properties: 6 | BucketName: {{ environment.inputs.example_input }} 7 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/awsmanaged/cloudformation.svc.yaml.jinja: -------------------------------------------------------------------------------- 1 | Resources: 2 | S3Bucket: 3 | Type: 'AWS::S3::Bucket' 4 | DeletionPolicy: Retain 5 | Properties: 6 | BucketName: {{ service_instance.inputs.example_input }} 7 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/awsmanaged/manifest.yaml: -------------------------------------------------------------------------------- 1 | infrastructure: 2 | templates: 3 | - file: "cloudformation.yaml" 4 | rendering_engine: jinja 5 | template_language: cloudformation 6 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/install-terraform.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | curl -Os https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip && \ 5 | curl -Os https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_SHA256SUMS && \ 6 | curl https://keybase.io/hashicorp/pgp_keys.asc | gpg --import && \ 7 | curl -Os https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_SHA256SUMS.sig && \ 8 | gpg --verify terraform_${TF_VERSION}_SHA256SUMS.sig terraform_${TF_VERSION}_SHA256SUMS && \ 9 | shasum -a 256 -c terraform_${TF_VERSION}_SHA256SUMS 2>&1 | grep "${TF_VERSION}_linux_amd64.zip:\sOK" && \ 10 | unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin && \ 11 | terraform --version 12 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/main.env.tf.go.tpl: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | 11 | backend "s3" {} 12 | } 13 | 14 | provider "aws" { 15 | default_tags { 16 | tags = { 17 | "proton:environment" = var.environment.name 18 | } 19 | } 20 | } 21 | 22 | module "{{ .ModuleName }}" { 23 | source = "./src" 24 | 25 | {{ range $v := .Variables }} 26 | {{ if eq $v.Name "name" }}name = var.environment.name{{ else }} 27 | {{ $v.Name }} = var.environment.inputs.{{ $v.Name }} 28 | {{ end }}{{ end }} 29 | } 30 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/main.env.tf.new.go.tpl: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | 11 | backend "s3" {} 12 | } 13 | 14 | provider "aws" { 15 | default_tags { 16 | tags = { 17 | "proton:environment" = var.environment.name 18 | } 19 | } 20 | } 21 | 22 | # TODO: this is just an example 23 | # replace this with your real template resources here 24 | resource "aws_cloudwatch_log_group" "example" { 25 | name = var.environment.inputs.{{.Name}} 26 | } 27 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/main.svc.tf.go.tpl: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | 11 | backend "s3" {} 12 | } 13 | 14 | provider "aws" { 15 | default_tags { 16 | tags = { 17 | "proton:environment" = var.environment.name 18 | "proton:service" = var.service.name, 19 | "proton:service_instance" = var.service_instance.name, 20 | } 21 | } 22 | } 23 | 24 | module "{{ .ModuleName }}" { 25 | source = "./src" 26 | 27 | {{ range $v := .Variables }} 28 | {{ if eq $v.Name "name" }}name = "${var.service.name}-${var.service_instance.name}"{{ else if eq $v.Name "environment" }}environment = var.environment.name{{ else }} 29 | {{ $v.Name }} = var.service_instance.inputs.{{ $v.Name }} 30 | {{ end }}{{ end }} 31 | } 32 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/main.svc.tf.new.go.tpl: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.0" 8 | } 9 | } 10 | 11 | backend "s3" {} 12 | } 13 | 14 | provider "aws" { 15 | default_tags { 16 | tags = { 17 | "proton:environment" = var.environment.name 18 | "proton:service" = var.service.name, 19 | "proton:service_instance" = var.service_instance.name, 20 | } 21 | } 22 | } 23 | 24 | # TODO: this is just an example resource 25 | # replace this with your real template resources here 26 | resource "aws_cloudwatch_log_group" "example" { 27 | name = var.service_instance.inputs.{{.Name}} 28 | } 29 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/manifest.yaml.go.tpl: -------------------------------------------------------------------------------- 1 | infrastructure: 2 | templates: 3 | - rendering_engine: codebuild 4 | settings: 5 | image: aws/codebuild/standard:6.0 6 | runtimes: 7 | golang: 1.18 # not needed, but required by proton (for now) 8 | env: 9 | variables: 10 | TF_VERSION: 1.4.5 11 | AWS_REGION: us-east-1 12 | TF_STATE_BUCKET: {{ .TerraformS3StateBucket }} 13 | 14 | provision: 15 | 16 | # get proton metadata from input file 17 | - export IN=$(cat proton-inputs.json) && echo ${IN} 18 | - export PROTON_ENV=$(echo $IN | jq '.environment.name' -r) 19 | {{ if eq .TemplateType "service" }} 20 | - export PROTON_SVC=$(echo $IN | jq '.service.name' -r) 21 | - export PROTON_SVC_INSTANCE=$(echo $IN | jq '.service_instance.name' -r) 22 | {{ end }} 23 | # set terraform remote state bucket key 24 | {{ if eq .TemplateType "service" }} 25 | - export KEY=svc.{{.TemplateName}}.${PROTON_ENV}.${PROTON_SVC}.${PROTON_SVC_INSTANCE} 26 | {{ else }} 27 | - export KEY=env.{{.TemplateName}}.${PROTON_ENV} 28 | {{ end }} 29 | - echo "remote state = ${TF_STATE_BUCKET}/${KEY}" 30 | 31 | # install terraform cli 32 | - echo "Installing Terraform CLI ${TF_VERSION}" 33 | - chmod +x ./install-terraform.sh && ./install-terraform.sh ${TF_VERSION} 34 | 35 | # provision, storing state in an s3 bucket 36 | - terraform init -backend-config="bucket=${TF_STATE_BUCKET}" -backend-config="key=${KEY}.tfstate" 37 | - terraform apply -var-file=proton-inputs.json -auto-approve 38 | 39 | # pass terraform output to proton 40 | - chmod +x ./output.sh && ./output.sh 41 | 42 | deprovision: 43 | 44 | # get proton metadata from input file 45 | - export IN=$(cat proton-inputs.json) && echo ${IN} 46 | - export PROTON_ENV=$(echo $IN | jq '.environment.name' -r) 47 | {{ if eq .TemplateType "service" }} 48 | - export PROTON_SVC=$(echo $IN | jq '.service.name' -r) 49 | - export PROTON_SVC_INSTANCE=$(echo $IN | jq '.service_instance.name' -r) 50 | {{ end }} 51 | # set terraform remote state bucket key 52 | {{ if eq .TemplateType "service" }} 53 | - export KEY=svc.{{.TemplateName}}.${PROTON_ENV}.${PROTON_SVC}.${PROTON_SVC_INSTANCE} 54 | {{ else }} 55 | - export KEY=env.{{.TemplateName}}.${PROTON_ENV} 56 | {{ end }} 57 | - echo "remote state = ${TF_STATE_BUCKET}/${KEY}" 58 | 59 | # install terraform cli 60 | - echo "Installing Terraform CLI ${TF_VERSION}" 61 | - chmod +x ./install-terraform.sh && ./install-terraform.sh ${TF_VERSION} 62 | 63 | # destroy environment 64 | - terraform init -backend-config="bucket=${TF_STATE_BUCKET}" -backend-config="key=${KEY}.tfstate" 65 | - terraform destroy -var-file=proton-inputs.json -auto-approve 66 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/output.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | terraform output -json | jq 'to_entries | map({key:.key, valueString:.value.value})' > output.json 4 | aws proton notify-resource-deployment-status-change --resource-arn ${RESOURCE_ARN} --status IN_PROGRESS --outputs file://./output.json 5 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/outputs.tf.go.tpl: -------------------------------------------------------------------------------- 1 | {{$moduleName := .ModuleName}} 2 | {{ range $o := .Outputs }} 3 | output "{{ $o.Name }}" { 4 | description = "{{ $o.Description }}" 5 | value = module.{{ $moduleName }}.{{ $o.Name }} 6 | } 7 | {{ end }} 8 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/variables.env.tf: -------------------------------------------------------------------------------- 1 | # required by proton 2 | variable "environment" { 3 | description = "The Proton Environment" 4 | type = object({ 5 | name = string 6 | inputs = any 7 | }) 8 | default = null 9 | } 10 | -------------------------------------------------------------------------------- /cmd/templates/infrastructure/codebuild/terraform/variables.svc.tf: -------------------------------------------------------------------------------- 1 | # required by proton 2 | 3 | variable "environment" { 4 | description = "proton environment" 5 | type = object({ 6 | name = string 7 | account_id = string 8 | outputs = map(string) 9 | }) 10 | } 11 | 12 | variable "service" { 13 | description = "proton service" 14 | type = object({ 15 | name = string 16 | repository_id = string 17 | repository_connection_arn = string 18 | branch_name = string 19 | }) 20 | } 21 | 22 | variable "service_instance" { 23 | description = "proton service instance" 24 | type = object({ 25 | name = string 26 | inputs = any 27 | }) 28 | } 29 | 30 | variable "region" { 31 | description = "aws region" 32 | type = string 33 | default = "us-east-1" 34 | } 35 | -------------------------------------------------------------------------------- /cmd/templates/readme/env.cfn.md: -------------------------------------------------------------------------------- 1 | ## Proton environment template 2 | 3 | This Proton environment template was scaffolded by the [Protonizer CLI tool](https://github.com/awslabs/protonizer). 4 | 5 | This environment template will be used to create shared infrastructure associated with one more many service templates. 6 | 7 | 8 | ### What's next? 9 | 10 | The next step is to design your template's interface. In other words, how will your consumers interact with your template? You do this by specifying input and output parameters. 11 | 12 | The `input` parameters are defined in your [schema.yaml file](./schema/schema.yaml) using the [standard Open API 3.0 schema specification](https://swagger.io/docs/specification/data-models/). 13 | 14 | ```yaml 15 | schema: 16 | format: 17 | openapi: "3.0.0" 18 | environment_input_type: environment 19 | types: 20 | environment: 21 | type: object 22 | description: Environment input properties 23 | properties: 24 | 25 | # define your input properties here 26 | example_input: 27 | title: Example Input 28 | type: string 29 | description: "This is an example string input" 30 | default: default 31 | ``` 32 | 33 | The `output` parameters are defined in your [CloudFormation IaC files](infrastructure/cloudformation.yaml) in the `Outputs:` block. 34 | 35 | The next step is to author your IaC code using the input parameters provided by Proton. Make changes in your `infrastructure/cloudformation.yaml` file. You'll use [jinja templating syntax]() to access template input parameters. The example below creates an S3 bucket using the proton input parameter `example_input` as the bucket name, and outputs a parameter `S3BucketArn` with the bucket ARN. 36 | 37 | ```yaml 38 | Resources: 39 | S3Bucket: 40 | Type: 'AWS::S3::Bucket' 41 | DeletionPolicy: Retain 42 | Properties: 43 | BucketName: {{ environment.inputs.example_input }} 44 | 45 | Outputs: 46 | S3BucketArn: 47 | Description: The ARN of the S3 bucket 48 | Value: !GetAtt S3Bucket.Arn 49 | ``` 50 | 51 | 52 | ### Publish your template 53 | 54 | Once you're happy with how your template looks, you'll need to publish the template to Proton before it can be used. To publish your template, you can run the following protonizer command. 55 | 56 | ``` 57 | cd my-template/v1 58 | protonizer publish 59 | 60 | published my-template:1.0 61 | https://us-east-1.console.aws.amazon.com/proton/home#/templates/environments/detail/my-template 62 | ``` 63 | 64 | Note that you'll need to ensure you've set the `publishBucket` key in your `proton.yaml` file. It should be there if you ran the `new` command using the `--public-bucket` CLI argument. 65 | 66 | ```yaml 67 | name: my-template 68 | type: environment 69 | displayName: my-template 70 | description: An environment template scaffolded by the Protonizer CLI tool 71 | publishBucket: my-s3-bucket 72 | ``` 73 | 74 | 75 | ### Consume your template 76 | 77 | Now that your template is published in Proton, you can start creating instances of the template called `environments`. There are a number of ways to do this. 78 | 79 | - [Use the GUI console](https://docs.aws.amazon.com/proton/latest/userguide/ag-create-env.html). Note that if using the approach, Proton can typically generate a custom GUI based on your template's input schema. 80 | 81 | - Use the Proton [API](https://docs.aws.amazon.com/proton/latest/APIReference/API_CreateEnvironment.html) (CLI or SDK). With this approach, you make imperative calls to create environments and services. For example `aws proton create-environment`. 82 | 83 | - [Use Proton service sync](https://docs.aws.amazon.com/proton/latest/userguide/ag-service-sync-configs.html) for a GitOps style workflow. With this approach, you specify your environments in a YAML file in a Git repo. You then provide Proton with access to the Git repo that it uses to watch the repo and listen for changes. When a change is made, Proton will automatically deploy the environments and services. 84 | 85 | 86 | ### Sample Templates 87 | 88 | You can find sample Proton templates here. 89 | 90 | - [AWS-Managed - CloudFormation](https://github.com/aws-samples/aws-proton-cloudformation-sample-templates) 91 | - [Codebuild - Terraform, CDK, Pulumi, etc.](https://github.com/aws-samples/aws-proton-terraform-sample-templates) 92 | -------------------------------------------------------------------------------- /cmd/templates/readme/env.tf.md: -------------------------------------------------------------------------------- 1 | ## Proton environment template 2 | 3 | This Proton environment template was scaffolded by the [Protonizer CLI tool](https://github.com/awslabs/protonizer). 4 | 5 | This environment template will be used to create shared infrastructure associated with one more many service templates. 6 | 7 | 8 | ### What's next? 9 | 10 | The next step is to design your template's interface. In other words, how will your consumers interact with your template? You do this by specifying input and output parameters. 11 | 12 | The `input` parameters are defined in your [schema.yaml file](./schema/schema.yaml) using the [standard Open API 3.0 schema specification](https://swagger.io/docs/specification/data-models/). 13 | 14 | ```yaml 15 | schema: 16 | format: 17 | openapi: "3.0.0" 18 | environment_input_type: environment 19 | types: 20 | environment: 21 | type: object 22 | description: Environment input properties 23 | properties: 24 | 25 | # define your input properties here 26 | example_input: 27 | title: Example Input 28 | type: string 29 | description: "This is an example string input" 30 | default: default 31 | ``` 32 | 33 | The `output` parameters are defined in the generated [infrastructure/outputs.tf](./infrastructure/outputs.tf) file. The generated [output.sh](./infrastructure/output.sh) script will read your Terraform output variables and send them to Proton as outputs. 34 | 35 | The next step is to author your IaC code using the input parameters provided by Proton. Make changes to the `.tf` files in the [infrastructure](./infrastructure) directory. The Proton input parameters are passed in to Terraform using standard input variables. The example below creates a CloudWatch log group bucket using the proton input parameter `example_input` as the name, and outputs a parameter `LogGroupName` with the log group ARN. 36 | 37 | main.tf 38 | ```hcl 39 | provider "aws" { 40 | default_tags { 41 | tags = { 42 | "proton:environment" = var.environment.name 43 | } 44 | } 45 | } 46 | 47 | resource "aws_cloudwatch_log_group" "example" { 48 | name = var.environment.inputs.example_input 49 | } 50 | ``` 51 | 52 | outputs.tf 53 | ```hcl 54 | output "LogGroupArn" { 55 | description = "the s3 bucket that was created" 56 | value = aws_cloudwatch_log_group.example.arn 57 | } 58 | ``` 59 | 60 | 61 | ### Publish your template 62 | 63 | Once you're happy with how your template looks, you'll need to publish the template to Proton before it can be used. To publish your template, you can run the following protonizer command. 64 | 65 | ``` 66 | cd my-template/v1 67 | protonizer publish 68 | 69 | published my-template:1.0 70 | https://us-east-1.console.aws.amazon.com/proton/home#/templates/environments/detail/my-template 71 | ``` 72 | 73 | Note that you'll need to ensure you've set the `publishBucket` key in your `proton.yaml` file. It should be there if you ran the `new` command using the `--public-bucket` CLI argument. 74 | 75 | ```yaml 76 | name: my-template 77 | type: environment 78 | displayName: my-template 79 | description: An environment template scaffolded by the Protonizer CLI tool 80 | publishBucket: my-s3-bucket 81 | ``` 82 | 83 | 84 | ### Consume your template 85 | 86 | Now that your template is published in Proton, you can start creating instances of the template called `environments`. There are a number of ways to do this. 87 | 88 | - [Use the GUI console](https://docs.aws.amazon.com/proton/latest/userguide/ag-create-env.html). Note that if using the approach, Proton can typically generate a custom GUI based on your template's input schema. 89 | 90 | - Use the Proton [API](https://docs.aws.amazon.com/proton/latest/APIReference/API_CreateEnvironment.html) (CLI or SDK). With this approach, you make imperative calls to create environments and services. For example `aws proton create-environment`. 91 | 92 | - [Use Proton service sync](https://docs.aws.amazon.com/proton/latest/userguide/ag-service-sync-configs.html) for a GitOps style workflow. With this approach, you specify your environments in a YAML file in a Git repo. You then provide Proton with access to the Git repo that it uses to watch the repo and listen for changes. When a change is made, Proton will automatically deploy the environments and services. 93 | 94 | 95 | ### Sample Templates 96 | 97 | You can find sample Proton templates here. 98 | 99 | - [AWS-Managed - CloudFormation](https://github.com/aws-samples/aws-proton-cloudformation-sample-templates) 100 | - [Codebuild - Terraform, CDK, Pulumi, etc.](https://github.com/aws-samples/aws-proton-terraform-sample-templates) 101 | -------------------------------------------------------------------------------- /cmd/templates/readme/svc.cfn.md: -------------------------------------------------------------------------------- 1 | ## Proton service template 2 | 3 | This Proton service template was scaffolded by the [Protonizer CLI tool](https://github.com/awslabs/protonizer). 4 | 5 | This service template will be used to create services that will be associated with a Proton environment. 6 | 7 | 8 | ### What's next? 9 | 10 | The next step is to design your template's interface. In other words, how will your consumers interact with your template? You do this by specifying input and output parameters. 11 | 12 | The `input` parameters are defined in your [schema.yaml file](./schema/schema.yaml) using the [standard Open API 3.0 schema specification](https://swagger.io/docs/specification/data-models/). 13 | 14 | ```yaml 15 | schema: 16 | format: 17 | openapi: "3.0.0" 18 | service_input_type: service 19 | types: 20 | service: 21 | type: object 22 | description: Service input properties 23 | properties: 24 | 25 | example_input: 26 | title: Example Input 27 | type: string 28 | 29 | description: "This is an example string input" 30 | default: default 31 | ``` 32 | 33 | The `output` parameters are defined in your [CloudFormation IaC files](infrastructure/cloudformation.yaml) in the `Outputs:` block. 34 | 35 | The next step is to author your IaC code using the input parameters provided by Proton. Make changes in your `instance_infrastructure/cloudformation.yaml` file. You'll use [jinja templating syntax](https://jinja.palletsprojects.com/en/3.1.x/) to access template input parameters. The example below creates an S3 bucket using the proton input parameter `example_input` as the bucket name, and outputs a parameter `S3BucketArn` with the bucket ARN. 36 | 37 | ```yaml 38 | Resources: 39 | S3Bucket: 40 | Type: 'AWS::S3::Bucket' 41 | DeletionPolicy: Retain 42 | Properties: 43 | BucketName: {{ service_instance.inputs.example_input }} 44 | 45 | Outputs: 46 | S3BucketArn: 47 | Description: The ARN of the S3 bucket 48 | Value: !GetAtt S3Bucket.Arn 49 | ``` 50 | 51 | 52 | ### Publish your template 53 | 54 | Once you're happy with how your template looks, you'll need to publish the template to Proton before it can be used. To publish your template, you can run the following protonizer command. 55 | 56 | ``` 57 | cd my-template/v1 58 | protonizer publish 59 | 60 | published my-template:1.0 61 | https://us-east-1.console.aws.amazon.com/proton/home#/templates/services/detail/my-template 62 | ``` 63 | 64 | Note that you'll need to ensure you've set the `publishBucket` key in your `proton.yaml` file. It should be there if you ran the `new` command using the `--public-bucket` CLI argument. 65 | 66 | ```yaml 67 | name: my-template 68 | type: service 69 | displayName: my-template 70 | description: A service template scaffolded by the Protonizer CLI tool 71 | publishBucket: my-s3-bucket 72 | compatibleEnvironments: 73 | - my-env-template:1 74 | ``` 75 | 76 | 77 | ### Consume your template 78 | 79 | Now that your template is published in Proton, you can start creating instances of the template called `services`. There are a number of ways to do this. 80 | 81 | - [Use the GUI console](https://docs.aws.amazon.com/proton/latest/userguide/ag-create-env.html). Note that if using the approach, Proton can typically generate a custom GUI based on your template's input schema. 82 | 83 | - Use the Proton [API](https://docs.aws.amazon.com/proton/latest/APIReference/API_CreateEnvironment.html) (CLI or SDK). With this approach, you make imperative calls to create environments and services. For example `aws proton create-environment`. 84 | 85 | - [Use Proton service sync](https://docs.aws.amazon.com/proton/latest/userguide/ag-service-sync-configs.html) for a GitOps style workflow. With this approach, you specify your environments in a YAML file in a Git repo. You then provide Proton with access to the Git repo that it uses to watch the repo and listen for changes. When a change is made, Proton will automatically deploy the environments and services. 86 | 87 | 88 | ### Sample Templates 89 | 90 | You can find sample Proton templates here. 91 | 92 | - [AWS-Managed - CloudFormation](https://github.com/aws-samples/aws-proton-cloudformation-sample-templates) 93 | - [Codebuild - Terraform, CDK, Pulumi, etc.](https://github.com/aws-samples/aws-proton-terraform-sample-templates) 94 | -------------------------------------------------------------------------------- /cmd/templates/readme/svc.tf.md: -------------------------------------------------------------------------------- 1 | ## Proton service template 2 | 3 | This Proton service template was scaffolded by the [Protonizer CLI tool](https://github.com/awslabs/protonizer). 4 | 5 | This service template will be used to create services that will be associated with a Proton environment. 6 | 7 | 8 | ### What's next? 9 | 10 | The next step is to design your template's interface. In other words, how will your consumers interact with your template? You do this by specifying input and output parameters. 11 | 12 | The `input` parameters are defined in your [schema.yaml file](./schema/schema.yaml) using the [standard Open API 3.0 schema specification](https://swagger.io/docs/specification/data-models/). 13 | 14 | ```yaml 15 | schema: 16 | format: 17 | openapi: "3.0.0" 18 | service_input_type: service 19 | types: 20 | service: 21 | type: object 22 | description: Service input properties 23 | properties: 24 | 25 | example_input: 26 | title: Example Input 27 | type: string 28 | 29 | description: "This is an example string input" 30 | default: default 31 | ``` 32 | 33 | The `output` parameters are defined in the generated [instance_infrastructure/outputs.tf](./instance_infrastructure/outputs.tf) file. The generated [output.sh](./instance_infrastructure/output.sh) script will read your Terraform output variables and send them to Proton as outputs. 34 | 35 | The next step is to author your IaC code using the input parameters provided by Proton. Make changes to the `.tf` files in the [instance_infrastructure](./instance_infrastructure) directory. The Proton input parameters are passed in to Terraform using standard input variables. The example below creates a CloudWatch log group bucket using the proton input parameter `example_input` as the name, and outputs a parameter `LogGroupName` with the log group ARN. 36 | 37 | main.tf 38 | ```hcl 39 | provider "aws" { 40 | default_tags { 41 | tags = { 42 | "proton:environment" = var.environment.name 43 | "proton:service" = var.service.name, 44 | "proton:service_instance" = var.service_instance.name, 45 | } 46 | } 47 | } 48 | 49 | resource "aws_cloudwatch_log_group" "example" { 50 | name = var.service_instance.inputs.example_input 51 | } 52 | ``` 53 | 54 | outputs.tf 55 | ```hcl 56 | output "LogGroupArn" { 57 | description = "the s3 bucket that was created" 58 | value = aws_cloudwatch_log_group.example.arn 59 | } 60 | ``` 61 | 62 | 63 | ### Publish your template 64 | 65 | Once you're happy with how your template looks, you'll need to publish the template to Proton before it can be used. To publish your template, you can run the following protonizer command. 66 | 67 | ``` 68 | cd my-template/v1 69 | protonizer publish 70 | 71 | published my-template:1.0 72 | https://us-east-1.console.aws.amazon.com/proton/home#/templates/services/detail/my-template 73 | ``` 74 | 75 | Note that you'll need to ensure you've set the `publishBucket` key in your `proton.yaml` file. It should be there if you ran the `new` command using the `--public-bucket` CLI argument. 76 | 77 | ```yaml 78 | name: my-template 79 | type: service 80 | displayName: my-template 81 | description: A service template scaffolded by the Protonizer CLI tool 82 | publishBucket: my-s3-bucket 83 | compatibleEnvironments: 84 | - my-env-template:1 85 | ``` 86 | 87 | 88 | ### Consume your template 89 | 90 | Now that your template is published in Proton, you can start creating instances of the template called `services`. There are a number of ways to do this. 91 | 92 | - [Use the GUI console](https://docs.aws.amazon.com/proton/latest/userguide/ag-create-env.html). Note that if using the approach, Proton can typically generate a custom GUI based on your template's input schema. 93 | 94 | - Use the Proton [API](https://docs.aws.amazon.com/proton/latest/APIReference/API_CreateEnvironment.html) (CLI or SDK). With this approach, you make imperative calls to create environments and services. For example `aws proton create-environment`. 95 | 96 | - [Use Proton service sync](https://docs.aws.amazon.com/proton/latest/userguide/ag-service-sync-configs.html) for a GitOps style workflow. With this approach, you specify your environments in a YAML file in a Git repo. You then provide Proton with access to the Git repo that it uses to watch the repo and listen for changes. When a change is made, Proton will automatically deploy the environments and services. 97 | 98 | 99 | ### Sample Templates 100 | 101 | You can find sample Proton templates here. 102 | 103 | - [AWS-Managed - CloudFormation](https://github.com/aws-samples/aws-proton-cloudformation-sample-templates) 104 | - [Codebuild - Terraform, CDK, Pulumi, etc.](https://github.com/aws-samples/aws-proton-terraform-sample-templates) 105 | -------------------------------------------------------------------------------- /cmd/templates/schema/schema.env.yaml.go.tpl: -------------------------------------------------------------------------------- 1 | schema: 2 | format: 3 | openapi: "3.0.0" 4 | environment_input_type: environment 5 | types: 6 | environment: 7 | type: object 8 | description: Environment input properties 9 | properties: 10 | {{ range $v := . }}{{ if ne $v.Name "name" }} 11 | {{ $v.Name }}: 12 | title: {{ $v.Title }} 13 | type: {{ $v.Type }} 14 | {{ if ne "" $v.ArrayType }}items: 15 | type: {{ $v.ArrayType }} 16 | {{ end }} 17 | description: "{{ $v.Description }}" 18 | {{ if ne nil $v.Default }}default: {{ $v.Default }}{{ end }} 19 | {{ end }}{{ end }} 20 | -------------------------------------------------------------------------------- /cmd/templates/schema/schema.svc.yaml.go.tpl: -------------------------------------------------------------------------------- 1 | schema: 2 | format: 3 | openapi: "3.0.0" 4 | service_input_type: service 5 | types: 6 | service: 7 | type: object 8 | description: Service input properties 9 | properties: 10 | {{ range $v := . }}{{ if and (ne $v.Name "name") (ne $v.Name "environment") }} 11 | {{ $v.Name }}: 12 | title: {{ $v.Title }} 13 | type: {{ $v.Type }} 14 | {{ if ne "" $v.ArrayType }}items: 15 | type: {{ $v.ArrayType }} 16 | {{ end }} 17 | description: "{{ $v.Description }}" 18 | {{ if ne nil $v.Default }}default: {{ $v.Default }}{{ end }} 19 | {{ end }}{{ end }} 20 | -------------------------------------------------------------------------------- /cmd/test/main.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "This should be mapped to proton metadata" 3 | type = string 4 | } 5 | 6 | variable "environment" { 7 | description = "This should be mapped to proton metadata for services" 8 | type = string 9 | } 10 | 11 | variable "vpc_cidr" { 12 | description = "The CIDR range for the VPC" 13 | type = string 14 | default = "10.0.0.0/16" 15 | } 16 | 17 | variable "private_subnet_one_cidr" { 18 | description = "The CIDR range for private subnet one" 19 | type = string 20 | default = "10.0.128.0/18" 21 | } 22 | 23 | variable "private_subnet_two_cidr" { 24 | description = "The CIDR range for private subnet two" 25 | type = string 26 | default = "10.0.192.0/18" 27 | } 28 | 29 | variable "public_subnet_one_cidr" { 30 | description = "The CIDR range for public subnet one" 31 | type = string 32 | default = "10.0.0.0/18" 33 | } 34 | 35 | variable "public_subnet_two_cidr" { 36 | description = "The CIDR range for public subnet two" 37 | type = string 38 | default = "10.0.64.0/18" 39 | } 40 | 41 | variable "quote_test" { 42 | description = "this variable is used to test \"quotes\" in descriptions" 43 | type = string 44 | } 45 | -------------------------------------------------------------------------------- /cmd/test/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cluster_name" { 2 | description = "this is cluster_name" 3 | value = "" 4 | } 5 | 6 | output "cluster_arn" { 7 | description = "this is cluster_arn" 8 | value = "" 9 | } 10 | 11 | output "service_taskdef_execution_role" { 12 | description = "this is service_taskdef_execution_role" 13 | value = "" 14 | } 15 | 16 | output "vpc_id" { 17 | description = "this is vpc_id" 18 | value = "" 19 | } 20 | 21 | output "public_subnet_one_id" { 22 | description = "this is public_subnet_one_id" 23 | value = "" 24 | } 25 | 26 | output "public_subnet_two_id" { 27 | description = "this is public_subnet_two_id" 28 | value = "" 29 | } 30 | 31 | output "private_subnet_one_id" { 32 | description = "this is private_subnet_one_id" 33 | value = "" 34 | } 35 | 36 | output "private_subnet_two_id" { 37 | description = "this is private_subnet_two_id" 38 | value = "" 39 | } 40 | -------------------------------------------------------------------------------- /cmd/utils.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/fs" 7 | "log" 8 | "os" 9 | "path" 10 | "sort" 11 | "strings" 12 | "text/template" 13 | 14 | "github.com/hashicorp/terraform-config-inspect/tfconfig" 15 | "github.com/jritsema/scaffolder" 16 | ) 17 | 18 | // handle errors 19 | func handle(e error) { 20 | if e != nil { 21 | 22 | //log error if verbose 23 | debug(e) 24 | 25 | //buld message to show user 26 | msg := `something went wrong 😞` 27 | if !verbose { 28 | msg += ` try -v for more info` 29 | } 30 | 31 | //show message 32 | errorExit(msg) 33 | } 34 | } 35 | 36 | // wraps an error with context and handles it 37 | func handleError(context string, err error) { 38 | if err != nil { 39 | handle(fmt.Errorf("%s: %w", context, err)) 40 | } 41 | } 42 | 43 | // prints message to user and exits 44 | func errorExit(a ...interface{}) { 45 | fmt.Println(a...) 46 | os.Exit(1) 47 | } 48 | 49 | // if -v, log detailed message 50 | func debug(a ...interface{}) { 51 | if verbose { 52 | log.Println(a...) 53 | } 54 | } 55 | 56 | // if -v, log detailed message 57 | func debugFmt(format string, a ...interface{}) { 58 | debug(fmt.Sprintf(format, a...)) 59 | } 60 | 61 | // SliceContains returns true if a slice contains a string 62 | func SliceContains(s *[]string, e string, trim bool) bool { 63 | for _, str := range *s { 64 | if trim { 65 | str = strings.TrimSpace(str) 66 | } 67 | if str == e { 68 | return true 69 | } 70 | } 71 | return false 72 | } 73 | 74 | // sorts a TF module's variables 75 | func sortTFVariables(module *tfconfig.Module) []tfconfig.Variable { 76 | result := []tfconfig.Variable{} 77 | for _, v := range module.Variables { 78 | result = append(result, *v) 79 | } 80 | sort.Slice(result, func(i, j int) bool { 81 | return result[i].Name < result[j].Name 82 | }) 83 | return result 84 | } 85 | 86 | func sortTFOutputs(module *tfconfig.Module) []tfconfig.Output { 87 | result := []tfconfig.Output{} 88 | for _, v := range module.Outputs { 89 | result = append(result, *v) 90 | } 91 | sort.Slice(result, func(i, j int) bool { 92 | return result[i].Name < result[j].Name 93 | }) 94 | return result 95 | } 96 | 97 | // recursively parses all templates in the FS with the given extension 98 | // filepaths used as template names to support duplicate file names 99 | func templateParseFSRecursive(templates fs.FS, ext string, funcMap template.FuncMap) (*template.Template, error) { 100 | root := template.New("fs") 101 | err := fs.WalkDir(templates, "templates", func(path string, d fs.DirEntry, err error) error { 102 | if !d.IsDir() && strings.HasSuffix(path, ext) { 103 | if err != nil { 104 | return err 105 | } 106 | b, err := fs.ReadFile(templates, path) 107 | if err != nil { 108 | return err 109 | } 110 | //name the template based on the file path (excluding the root) 111 | parts := strings.Split(path, string(os.PathSeparator)) 112 | name := strings.Join(parts[1:], string(os.PathSeparator)) 113 | t := root.New(name).Funcs(funcMap) 114 | _, err = t.Parse(string(b)) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | return nil 120 | }) 121 | return root, err 122 | } 123 | 124 | // reads a template 125 | func readTemplateFS(f string, a ...interface{}) []byte { 126 | result, err := fs.ReadFile(templateFS, path.Join("templates", fmt.Sprintf(f, a...))) 127 | handleError("reading template file", err) 128 | return result 129 | } 130 | 131 | // renders data into a template returning the result 132 | func render(template string, data interface{}, a ...interface{}) []byte { 133 | var buf bytes.Buffer 134 | err := scaffoldTemplates.ExecuteTemplate(&buf, fmt.Sprintf(template, a...), data) 135 | if err != nil { 136 | errorExit("error executing go template:", err) 137 | } 138 | return buf.Bytes() 139 | } 140 | 141 | // adds template content to the contents map 142 | func addContent(contents *scaffolder.FSContents, dir, file, template string, args ...interface{}) { 143 | c := *contents 144 | c[path.Join(dir, file)] = readTemplateFS(template, args...) 145 | } 146 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module protonizer 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.17.5 7 | github.com/aws/aws-sdk-go-v2/config v1.18.15 8 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55 9 | github.com/aws/aws-sdk-go-v2/service/proton v1.20.3 10 | github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5 11 | github.com/hack-pad/hackpadfs v0.2.1 12 | github.com/hashicorp/terraform-config-inspect v0.0.0-20230308124657-d7dec65d5f3a 13 | github.com/jritsema/scaffolder v0.1.0 14 | github.com/spf13/cobra v1.6.1 15 | gopkg.in/yaml.v3 v3.0.1 16 | ) 17 | 18 | require ( 19 | github.com/agext/levenshtein v1.2.2 // indirect 20 | github.com/apparentlymart/go-textseg v1.0.0 // indirect 21 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect 22 | github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect 23 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect 24 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect 26 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.21 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.24 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect 34 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 // indirect 35 | github.com/aws/smithy-go v1.13.5 // indirect 36 | github.com/google/go-cmp v0.5.8 // indirect 37 | github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f // indirect 38 | github.com/hashicorp/hcl/v2 v2.0.0 // indirect 39 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 40 | github.com/jmespath/go-jmespath v0.4.0 // indirect 41 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 42 | github.com/spf13/pflag v1.0.5 // indirect 43 | github.com/zclconf/go-cty v1.1.0 // indirect 44 | golang.org/x/text v0.3.8 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 2 | github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= 3 | github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 4 | github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 5 | github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= 6 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 7 | github.com/aws/aws-sdk-go-v2 v1.17.5 h1:TzCUW1Nq4H8Xscph5M/skINUitxM5UBAyvm2s7XBzL4= 8 | github.com/aws/aws-sdk-go-v2 v1.17.5/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 9 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= 10 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= 11 | github.com/aws/aws-sdk-go-v2/config v1.18.15 h1:509yMO0pJUGUugBP2H9FOFyV+7Mz7sRR+snfDN5W4NY= 12 | github.com/aws/aws-sdk-go-v2/config v1.18.15/go.mod h1:vS0tddZqpE8cD9CyW0/kITHF5Bq2QasW9Y1DFHD//O0= 13 | github.com/aws/aws-sdk-go-v2/credentials v1.13.15 h1:0rZQIi6deJFjOEgHI9HI2eZcLPPEGQPictX66oRFLL8= 14 | github.com/aws/aws-sdk-go-v2/credentials v1.13.15/go.mod h1:vRMLMD3/rXU+o6j2MW5YefrGMBmdTvkLLGqFwMLBHQc= 15 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc= 16 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo= 17 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55 h1:ClZKHmu2QIRQCEQ2Y2upfu4JPO0pG69Ce5eiq3PS2V4= 18 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55/go.mod h1:L/h5B6I7reig2QJXCGY0e0NVx4hYCcjETmsfR02hFng= 19 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE= 20 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg= 21 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 h1:b/Vn141DBuLVgXbhRWIrl9g+ww7G+ScV5SzniWR13jQ= 22 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23/go.mod h1:mr6c4cHC+S/MMkrjtSlG4QA36kOznDep+0fga5L/fGQ= 23 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 h1:IVx9L7YFhpPq0tTnGo8u8TpluFu7nAn9X3sUDMb11c0= 24 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30/go.mod h1:vsbq62AOBwQ1LJ/GWKFxX8beUEYeRp/Agitrxee2/qM= 25 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.21 h1:QdxdY43AiwsqG/VAqHA7bIVSm3rKr8/p9i05ydA0/RM= 26 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.21/go.mod h1:QtIEat7ksHH8nFItljyvMI0dGj8lipK2XZ4PhNihTEU= 27 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= 28 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= 29 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.24 h1:Qmm8klpAdkuN3/rPrIMa/hZQ1z93WMBPjOzdAsbSnlo= 30 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.24/go.mod h1:QelGeWBVRh9PbbXsfXKTFlU9FjT6W2yP+dW5jMQzOkg= 31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 h1:QoOybhwRfciWUBbZ0gp9S7XaDnCuSTeK/fySB99V1ls= 32 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23/go.mod h1:9uPh+Hrz2Vn6oMnQYiUi/zbh3ovbnQk19YKINkQny44= 33 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23 h1:qc+RW0WWZ2KApMnsu/EVCPqLTyIH55uc7YQq7mq4XqE= 34 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23/go.mod h1:FJhZWVWBCcgAF8jbep7pxQ1QUsjzTwa9tvEXGw2TDRo= 35 | github.com/aws/aws-sdk-go-v2/service/proton v1.20.3 h1:jcJeALhHrPufi1p3yJV0UhRcLFzIjwkl+5UaN3gbmI8= 36 | github.com/aws/aws-sdk-go-v2/service/proton v1.20.3/go.mod h1:o7oSUtJ+VE7glg1jyTwLLw0AjBdEW49/heU6QkwbOVI= 37 | github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5 h1:kFfb+NMap4R7nDvBYyABa/nw7KFMtAfygD1Hyoxh4uE= 38 | github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5/go.mod h1:Dze3kNt4T+Dgb8YCfuIFSBLmE6hadKNxqfdF0Xmqz1I= 39 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 h1:qJdM48OOLl1FBSzI7ZrA1ZfLwOyCYqkXV5lko1hYDBw= 40 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.4/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw= 41 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 h1:YRkWXQveFb0tFC0TLktmmhGsOcCgLwvq88MC2al47AA= 42 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc= 43 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 h1:L1600eLr0YvTT7gNh3Ni24yGI7NSHkq9Gp62vijPRCs= 44 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.5/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s= 45 | github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= 46 | github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 47 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 48 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 49 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 50 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 51 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 52 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 53 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 54 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 55 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 56 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 57 | github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMIxMw= 58 | github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU= 59 | github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws= 60 | github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= 61 | github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8= 62 | github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= 63 | github.com/hashicorp/terraform-config-inspect v0.0.0-20230308124657-d7dec65d5f3a h1:ioh/hkj66reatJ97/e+9ElFOBZtQNzmEjxxsuo+bv5Q= 64 | github.com/hashicorp/terraform-config-inspect v0.0.0-20230308124657-d7dec65d5f3a/go.mod h1:l8HcFPm9cQh6Q0KSWoYPiePqMvRFenybP1CH2MjKdlg= 65 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 66 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 67 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 68 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 69 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 70 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 71 | github.com/jritsema/scaffolder v0.1.0 h1:T208slBQodaPuVqvcrBV1woQ/+K1bsTr7Ydiz3Z+2AE= 72 | github.com/jritsema/scaffolder v0.1.0/go.mod h1:yEmP/A5AmzMid0pxpJqEPRyCuo5T2Aw+S7m+PPa3maQ= 73 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 74 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 75 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 76 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 77 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 78 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 79 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 80 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 81 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 82 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 83 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 84 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 85 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 86 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 87 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 88 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 89 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 90 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 91 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 92 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 93 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 94 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 95 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 96 | github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw= 97 | github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 100 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 101 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 102 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 103 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 104 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 105 | golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 107 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 108 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 109 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 110 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 112 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 113 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 114 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 115 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 116 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 117 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 118 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 119 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "protonizer/cmd" 4 | 5 | var version string 6 | 7 | func main() { 8 | cmd.Execute(version) 9 | } 10 | --------------------------------------------------------------------------------