├── .github └── workflows │ ├── dependabot.yaml │ └── release.yaml ├── .gitignore ├── .gitlab-ci.yml ├── .make-release-support ├── .release ├── LICENSE ├── Makefile ├── Makefile.mk ├── Pipfile ├── Pipfile.lock ├── README.md ├── setup.py └── src └── aws_ssm_copy ├── __init__.py ├── __main__.py └── ssm_copy.py /.github/workflows/dependabot.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Merge dependabot PRs 3 | 4 | on: pull_request_target 5 | 6 | permissions: 7 | pull-requests: write 8 | contents: write 9 | 10 | jobs: 11 | dependabot: 12 | runs-on: ubuntu-latest 13 | if: ${{ github.actor == 'dependabot[bot]' }} 14 | steps: 15 | - name: Dependabot metadata 16 | id: dependabot-metadata 17 | uses: dependabot/fetch-metadata@v1 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | - name: Approve a PR 21 | run: gh pr review --approve "$PR_URL" 22 | env: 23 | PR_URL: ${{ github.event.pull_request.html_url }} 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | - name: Enable auto-merge for Dependabot PRs 26 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 27 | run: gh pr merge --auto --squash "$PR_URL" 28 | env: 29 | PR_URL: ${{ github.event.pull_request.html_url }} 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | 'on': 4 | push: 5 | tags: 6 | - '*' 7 | jobs: 8 | build: 9 | name: snapshot 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.9 16 | - name: checkout 17 | run: git fetch --prune --unshallow 18 | - name: build 19 | run: | 20 | python setup.py check 21 | python setup.py build 22 | python setup.py sdist 23 | - name: distribute application 24 | uses: pypa/gh-action-pypi-publish@release/v1 25 | with: 26 | user: __token__ 27 | password: ${{secrets.twine_password }} 28 | packages_dir: dist/ 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stages: 3 | - build 4 | 5 | build: 6 | stage: build 7 | script: 8 | - make 9 | -------------------------------------------------------------------------------- /.make-release-support: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2015 Xebia Nederland B.V. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | function hasChanges() { 19 | test -n "$(git status -s .)" 20 | } 21 | 22 | function getRelease() { 23 | awk -F= '/^release=/{print $2}' .release 24 | } 25 | 26 | function getBaseTag() { 27 | sed -n -e "s/^tag=\(.*\)$(getRelease)\$/\1/p" .release 28 | } 29 | 30 | function getTag() { 31 | if [ -z "$1" ] ; then 32 | awk -F= '/^tag/{print $2}' .release 33 | else 34 | echo "$(getBaseTag)$1" 35 | fi 36 | } 37 | 38 | function setRelease() { 39 | if [ -n "$1" ] ; then 40 | sed -i.x -e "s/^tag=.*/tag=$(getTag $1)/" .release 41 | sed -i.x -e "s/^release=.*/release=$1/g" .release 42 | rm -f .release.x 43 | runPreTagCommand "$1" 44 | else 45 | echo "ERROR: missing release version parameter " >&2 46 | return 1 47 | fi 48 | } 49 | 50 | function runPreTagCommand() { 51 | if [ -n "$1" ] ; then 52 | COMMAND=$(sed -n -e "s/@@RELEASE@@/$1/g" -e 's/^pre_tag_command=\(.*\)/\1/p' .release) 53 | if [ -n "$COMMAND" ] ; then 54 | if ! OUTPUT=$(bash -c "$COMMAND" 2>&1) ; then echo $OUTPUT >&2 && exit 1 ; fi 55 | fi 56 | else 57 | echo "ERROR: missing release version parameter " >&2 58 | return 1 59 | fi 60 | } 61 | 62 | function tagExists() { 63 | tag=${1:-$(getTag)} 64 | test -n "$tag" && test -n "$(git tag | grep "^$tag\$")" 65 | } 66 | 67 | function differsFromRelease() { 68 | tag=$(getTag) 69 | ! tagExists $tag || test -n "$(git diff --shortstat -r $tag .)" 70 | } 71 | 72 | function getVersion() { 73 | result=$(getRelease) 74 | 75 | if differsFromRelease; then 76 | result="$result-$(git rev-parse --short HEAD)" 77 | fi 78 | 79 | if hasChanges ; then 80 | result="$result-dirty" 81 | fi 82 | echo $result 83 | } 84 | 85 | function nextPatchLevel() { 86 | version=${1:-$(getRelease)} 87 | major_and_minor=$(echo $version | cut -d. -f1,2) 88 | patch=$(echo $version | cut -d. -f3) 89 | version=$(printf "%s.%d" $major_and_minor $(($patch + 1))) 90 | echo $version 91 | } 92 | 93 | function nextMinorLevel() { 94 | version=${1:-$(getRelease)} 95 | major=$(echo $version | cut -d. -f1); 96 | minor=$(echo $version | cut -d. -f2); 97 | version=$(printf "%d.%d.0" $major $(($minor + 1))) ; 98 | echo $version 99 | } 100 | 101 | function nextMajorLevel() { 102 | version=${1:-$(getRelease)} 103 | major=$(echo $version | cut -d. -f1); 104 | version=$(printf "%d.0.0" $(($major + 1))) 105 | echo $version 106 | } 107 | -------------------------------------------------------------------------------- /.release: -------------------------------------------------------------------------------- 1 | release=0.5.3 2 | tag=v0.5.3 3 | pre_tag_command=sed -i "" -e 's/^version[ \t]*=.*/version = "@@RELEASE@@"/' setup.py 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include Makefile.mk 2 | USERNAME=mvanholsteijn 3 | NAME=aws-ssm-copy-parameters 4 | 5 | do-build: 6 | python setup.py check 7 | python setup.py build 8 | 9 | push: 10 | rm -rf dist/* 11 | python setup.py sdist 12 | twine upload dist/* 13 | 14 | clean: 15 | python setup.py clean 16 | rm -rf build/* dist/* 17 | 18 | autopep: 19 | autopep8 --experimental --in-place --max-line-length 132 $(shell find . -name \*.py) 20 | 21 | install: 22 | python setup.py install 23 | -------------------------------------------------------------------------------- /Makefile.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Xebia Nederland B.V. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | REGISTRY_HOST=docker.io 17 | USERNAME=$(USER) 18 | NAME=$(shell basename $(PWD)) 19 | 20 | RELEASE_SUPPORT := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))/.make-release-support 21 | IMAGE=$(REGISTRY_HOST)/$(USERNAME)/$(NAME) 22 | 23 | VERSION=$(shell . $(RELEASE_SUPPORT) ; getVersion) 24 | TAG=$(shell . $(RELEASE_SUPPORT); getTag) 25 | 26 | SHELL=/bin/bash 27 | 28 | .PHONY: pre-build do-build post-build build release patch-release minor-release major-release tag check-status check-release showver \ 29 | push do-push post-push 30 | 31 | build: pre-build do-build post-build 32 | 33 | pre-build: 34 | 35 | 36 | post-build: 37 | 38 | 39 | post-push: 40 | 41 | 42 | do-build: .release 43 | @if [[ -f Dockerfile ]]; then docker build -t $(IMAGE):$(VERSION) . ; else echo "INFO: No Dockerfile found." >/dev/null ; fi 44 | @if [[ -f Dockerfile ]]; then \ 45 | DOCKER_MAJOR=$(shell docker -v | sed -e 's/.*version //' -e 's/,.*//' | cut -d\. -f1) ; \ 46 | DOCKER_MINOR=$(shell docker -v | sed -e 's/.*version //' -e 's/,.*//' | cut -d\. -f2) ; \ 47 | if [ $$DOCKER_MAJOR -eq 1 ] && [ $$DOCKER_MINOR -lt 10 ] ; then \ 48 | echo docker tag -f $(IMAGE):$(VERSION) $(IMAGE):latest ;\ 49 | docker tag -f $(IMAGE):$(VERSION) $(IMAGE):latest ;\ 50 | else \ 51 | echo docker tag $(IMAGE):$(VERSION) $(IMAGE):latest ;\ 52 | docker tag $(IMAGE):$(VERSION) $(IMAGE):latest ; \ 53 | fi ; \ 54 | else \ 55 | echo 'No Dockerfile found.' > /dev/null ;\ 56 | fi 57 | 58 | .release: 59 | @echo "release=0.0.0" > .release 60 | @echo "tag=$(NAME)-0.0.0" >> .release 61 | @echo INFO: .release created 62 | @cat .release 63 | 64 | 65 | release: check-status check-release build push 66 | 67 | 68 | push: do-push post-push 69 | 70 | do-push: 71 | @if [[ -f Dockerfile ]]; then \ 72 | docker push $(IMAGE):$(VERSION) ; \ 73 | docker push $(IMAGE):latest ; \ 74 | else \ 75 | echo > /dev/null ; \ 76 | fi 77 | 78 | snapshot: build push 79 | 80 | showver: .release 81 | @. $(RELEASE_SUPPORT); getVersion 82 | 83 | tag-patch-release: VERSION := $(shell . $(RELEASE_SUPPORT); nextPatchLevel) 84 | tag-patch-release: .release tag 85 | 86 | tag-minor-release: VERSION := $(shell . $(RELEASE_SUPPORT); nextMinorLevel) 87 | tag-minor-release: .release tag 88 | 89 | tag-major-release: VERSION := $(shell . $(RELEASE_SUPPORT); nextMajorLevel) 90 | tag-major-release: .release tag 91 | 92 | patch-release: tag-patch-release release 93 | @echo $(VERSION) 94 | 95 | minor-release: tag-minor-release release 96 | @echo $(VERSION) 97 | 98 | major-release: tag-major-release release 99 | @echo $(VERSION) 100 | 101 | 102 | tag: TAG=$(shell . $(RELEASE_SUPPORT); getTag $(VERSION)) 103 | tag: check-status 104 | @. $(RELEASE_SUPPORT) ; ! tagExists $(TAG) || (echo "ERROR: tag $(TAG) for version $(VERSION) already tagged in git" >&2 && exit 1) ; 105 | @. $(RELEASE_SUPPORT) ; setRelease $(VERSION) 106 | git add . 107 | git commit -m "bumped to version $(VERSION)" ; 108 | git tag $(TAG) ; 109 | @[ -n "$(shell git remote -v)" ] && git push --tags 110 | 111 | check-status: 112 | @. $(RELEASE_SUPPORT) ; ! hasChanges || (echo "ERROR: there are still outstanding changes" >&2 && exit 1) ; 113 | 114 | check-release: .release 115 | @. $(RELEASE_SUPPORT) ; tagExists $(TAG) || (echo "ERROR: version not yet tagged in git. make [minor,major,patch]-release." >&2 && exit 1) ; 116 | @. $(RELEASE_SUPPORT) ; ! differsFromRelease $(TAG) || (echo "ERROR: current directory differs from tagged $(TAG). make [minor,major,patch]-release." ; exit 1) 117 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | twine = "*" 8 | autopep8 = "*" 9 | pylint = "*" 10 | git-release-tag = "*" 11 | 12 | [packages] 13 | aws-ssm-copy = {editable = true, path = "."} 14 | boto3 = "*" 15 | 16 | [requires] 17 | python_version = "3.9" 18 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "cae485b624b3209402b1fadd8c98867d30d0387e6df3003e8b06744ef4ddf462" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aws-ssm-copy": { 20 | "editable": true, 21 | "path": "." 22 | }, 23 | "boto3": { 24 | "hashes": [ 25 | "sha256:1de708665cbf156e76ca67e376d6cabc5f9a06d7213f925a510cb818a15340a6", 26 | "sha256:7574afd70c767fdbb19726565a67b47291e1e35ec792c9fbb8ee63cb3f630d45" 27 | ], 28 | "index": "pypi", 29 | "markers": "python_version >= '3.8'", 30 | "version": "==1.34.47" 31 | }, 32 | "botocore": { 33 | "hashes": [ 34 | "sha256:29f1d6659602c5d79749eca7430857f7a48dd02e597d0ea4a95a83c47847993e", 35 | "sha256:8f0c989d12cfceb06b005808492ec1ff6ae90fab1fc4bf7ac8e825ac86bc8a0b" 36 | ], 37 | "markers": "python_version >= '3.8'", 38 | "version": "==1.34.47" 39 | }, 40 | "jmespath": { 41 | "hashes": [ 42 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 43 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 44 | ], 45 | "markers": "python_version >= '3.7'", 46 | "version": "==1.0.1" 47 | }, 48 | "python-dateutil": { 49 | "hashes": [ 50 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 51 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 52 | ], 53 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 54 | "version": "==2.8.2" 55 | }, 56 | "s3transfer": { 57 | "hashes": [ 58 | "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e", 59 | "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b" 60 | ], 61 | "markers": "python_version >= '3.8'", 62 | "version": "==0.10.0" 63 | }, 64 | "six": { 65 | "hashes": [ 66 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 67 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 68 | ], 69 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 70 | "version": "==1.16.0" 71 | }, 72 | "urllib3": { 73 | "hashes": [ 74 | "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", 75 | "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" 76 | ], 77 | "markers": "python_version < '3.10'", 78 | "version": "==1.26.18" 79 | } 80 | }, 81 | "develop": { 82 | "astroid": { 83 | "hashes": [ 84 | "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93", 85 | "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17" 86 | ], 87 | "markers": "python_full_version >= '3.8.0'", 88 | "version": "==3.0.3" 89 | }, 90 | "autopep8": { 91 | "hashes": [ 92 | "sha256:067959ca4a07b24dbd5345efa8325f5f58da4298dab0dde0443d5ed765de80cb", 93 | "sha256:2913064abd97b3419d1cc83ea71f042cb821f87e45b9c88cad5ad3c4ea87fe0c" 94 | ], 95 | "index": "pypi", 96 | "markers": "python_version >= '3.6'", 97 | "version": "==2.0.4" 98 | }, 99 | "certifi": { 100 | "hashes": [ 101 | "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", 102 | "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" 103 | ], 104 | "markers": "python_version >= '3.6'", 105 | "version": "==2024.2.2" 106 | }, 107 | "charset-normalizer": { 108 | "hashes": [ 109 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 110 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 111 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 112 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 113 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 114 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 115 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 116 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 117 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 118 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 119 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 120 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 121 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 122 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 123 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 124 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 125 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 126 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 127 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 128 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 129 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 130 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 131 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 132 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 133 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 134 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 135 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 136 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 137 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 138 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 139 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 140 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 141 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 142 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 143 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 144 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 145 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 146 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 147 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 148 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 149 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 150 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 151 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 152 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 153 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 154 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 155 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 156 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 157 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 158 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 159 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 160 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 161 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 162 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 163 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 164 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 165 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 166 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 167 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 168 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 169 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 170 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 171 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 172 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 173 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 174 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 175 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 176 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 177 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 178 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 179 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 180 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 181 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 182 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 183 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 184 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 185 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 186 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 187 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 188 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 189 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 190 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 191 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 192 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 193 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 194 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 195 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 196 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 197 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 198 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 199 | ], 200 | "markers": "python_full_version >= '3.7.0'", 201 | "version": "==3.3.2" 202 | }, 203 | "click": { 204 | "hashes": [ 205 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 206 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 207 | ], 208 | "markers": "python_version >= '3.7'", 209 | "version": "==8.1.7" 210 | }, 211 | "dill": { 212 | "hashes": [ 213 | "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", 214 | "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7" 215 | ], 216 | "markers": "python_version < '3.11'", 217 | "version": "==0.3.8" 218 | }, 219 | "docutils": { 220 | "hashes": [ 221 | "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", 222 | "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b" 223 | ], 224 | "markers": "python_version >= '3.7'", 225 | "version": "==0.20.1" 226 | }, 227 | "git-release-tag": { 228 | "hashes": [ 229 | "sha256:9fc53a46778c99641f623a90baebecc7b65b0697091e034f95d11fac50af177f" 230 | ], 231 | "index": "pypi", 232 | "version": "==0.7.5" 233 | }, 234 | "idna": { 235 | "hashes": [ 236 | "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", 237 | "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" 238 | ], 239 | "markers": "python_version >= '3.5'", 240 | "version": "==3.6" 241 | }, 242 | "importlib-metadata": { 243 | "hashes": [ 244 | "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e", 245 | "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc" 246 | ], 247 | "markers": "python_version >= '3.8'", 248 | "version": "==7.0.1" 249 | }, 250 | "isort": { 251 | "hashes": [ 252 | "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", 253 | "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" 254 | ], 255 | "markers": "python_full_version >= '3.8.0'", 256 | "version": "==5.13.2" 257 | }, 258 | "jaraco.classes": { 259 | "hashes": [ 260 | "sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206", 261 | "sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30" 262 | ], 263 | "markers": "python_version >= '3.8'", 264 | "version": "==3.3.1" 265 | }, 266 | "keyring": { 267 | "hashes": [ 268 | "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836", 269 | "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25" 270 | ], 271 | "markers": "python_version >= '3.8'", 272 | "version": "==24.3.0" 273 | }, 274 | "markdown-it-py": { 275 | "hashes": [ 276 | "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", 277 | "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" 278 | ], 279 | "markers": "python_version >= '3.8'", 280 | "version": "==3.0.0" 281 | }, 282 | "mccabe": { 283 | "hashes": [ 284 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 285 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 286 | ], 287 | "markers": "python_version >= '3.6'", 288 | "version": "==0.7.0" 289 | }, 290 | "mdurl": { 291 | "hashes": [ 292 | "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", 293 | "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" 294 | ], 295 | "markers": "python_version >= '3.7'", 296 | "version": "==0.1.2" 297 | }, 298 | "more-itertools": { 299 | "hashes": [ 300 | "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", 301 | "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1" 302 | ], 303 | "markers": "python_version >= '3.8'", 304 | "version": "==10.2.0" 305 | }, 306 | "nh3": { 307 | "hashes": [ 308 | "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770", 309 | "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf", 310 | "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305", 311 | "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601", 312 | "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28", 313 | "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7", 314 | "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3", 315 | "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911", 316 | "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf", 317 | "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0", 318 | "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5", 319 | "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97", 320 | "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d", 321 | "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e", 322 | "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3", 323 | "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6" 324 | ], 325 | "version": "==0.2.15" 326 | }, 327 | "pkginfo": { 328 | "hashes": [ 329 | "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546", 330 | "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046" 331 | ], 332 | "markers": "python_version >= '3.6'", 333 | "version": "==1.9.6" 334 | }, 335 | "platformdirs": { 336 | "hashes": [ 337 | "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", 338 | "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" 339 | ], 340 | "markers": "python_version >= '3.8'", 341 | "version": "==4.2.0" 342 | }, 343 | "pycodestyle": { 344 | "hashes": [ 345 | "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", 346 | "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" 347 | ], 348 | "markers": "python_version >= '3.8'", 349 | "version": "==2.11.1" 350 | }, 351 | "pygments": { 352 | "hashes": [ 353 | "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", 354 | "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" 355 | ], 356 | "markers": "python_version >= '3.7'", 357 | "version": "==2.17.2" 358 | }, 359 | "pylint": { 360 | "hashes": [ 361 | "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b", 362 | "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810" 363 | ], 364 | "index": "pypi", 365 | "markers": "python_full_version >= '3.8.0'", 366 | "version": "==3.0.3" 367 | }, 368 | "readme-renderer": { 369 | "hashes": [ 370 | "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d", 371 | "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1" 372 | ], 373 | "markers": "python_version >= '3.8'", 374 | "version": "==42.0" 375 | }, 376 | "requests": { 377 | "hashes": [ 378 | "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", 379 | "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" 380 | ], 381 | "markers": "python_version >= '3.7'", 382 | "version": "==2.31.0" 383 | }, 384 | "requests-toolbelt": { 385 | "hashes": [ 386 | "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", 387 | "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" 388 | ], 389 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 390 | "version": "==1.0.0" 391 | }, 392 | "rfc3986": { 393 | "hashes": [ 394 | "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", 395 | "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" 396 | ], 397 | "markers": "python_version >= '3.7'", 398 | "version": "==2.0.0" 399 | }, 400 | "rich": { 401 | "hashes": [ 402 | "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", 403 | "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" 404 | ], 405 | "markers": "python_full_version >= '3.7.0'", 406 | "version": "==13.7.0" 407 | }, 408 | "tomli": { 409 | "hashes": [ 410 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 411 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 412 | ], 413 | "markers": "python_version < '3.11'", 414 | "version": "==2.0.1" 415 | }, 416 | "tomlkit": { 417 | "hashes": [ 418 | "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", 419 | "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" 420 | ], 421 | "markers": "python_version >= '3.7'", 422 | "version": "==0.12.3" 423 | }, 424 | "twine": { 425 | "hashes": [ 426 | "sha256:89b0cc7d370a4b66421cc6102f269aa910fe0f1861c124f573cf2ddedbc10cf4", 427 | "sha256:a262933de0b484c53408f9edae2e7821c1c45a3314ff2df9bdd343aa7ab8edc0" 428 | ], 429 | "index": "pypi", 430 | "markers": "python_version >= '3.8'", 431 | "version": "==5.0.0" 432 | }, 433 | "typing-extensions": { 434 | "hashes": [ 435 | "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", 436 | "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" 437 | ], 438 | "markers": "python_version < '3.10'", 439 | "version": "==4.9.0" 440 | }, 441 | "urllib3": { 442 | "hashes": [ 443 | "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", 444 | "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" 445 | ], 446 | "markers": "python_version < '3.10'", 447 | "version": "==1.26.18" 448 | }, 449 | "zipp": { 450 | "hashes": [ 451 | "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", 452 | "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0" 453 | ], 454 | "markers": "python_version >= '3.8'", 455 | "version": "==3.17.0" 456 | } 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-ssm-copy-parameters 2 | Copy parameters from a AWS parameter store to another 3 | 4 | ## Options 5 | ``` 6 | usage: aws-ssm-copy [options] PARAMETER [PARAMETER ...] 7 | ``` 8 | 9 | positional arguments: 10 | ``` 11 | PARAMETER source path 12 | ``` 13 | 14 | optional arguments: 15 | ``` 16 | -h, --help show this help message and exit 17 | --one-level, -1 one-level copy 18 | --recursive, -r recursive copy 19 | --overwrite, -f existing values 20 | --keep-going, -k as much as possible, even after an error 21 | --dry-run, -N only show what is to be copied 22 | --source-region REGION to get the parameters from 23 | --source-profile NAME to obtain the parameters from 24 | --region REGION to copy the parameters to 25 | --profile NAME to copy the parameters to 26 | --target-path NAME to copy the parameters to 27 | --key-id ID to use for parameter values in the destination 28 | --clear-key-id, -C clear the kms key id associated with the parameter 29 | --with-tags, -W copy the tags too, existing tags will be removed 30 | ``` 31 | 32 | 33 | ## Examples 34 | Copy all parameters under /dev to a new profile: 35 | ``` 36 | aws-ssm-copy --profile binx-io --recursive /dev 37 | ``` 38 | 39 | Copy all parameters under /dev to /production, with a dry run first: 40 | ``` 41 | aws-ssm-copy -r --dry-run --target-path /production /dev 42 | ``` 43 | 44 | Read more [about copying aws ssm parameters from one account to another](https://binx.io/blog/2020/12/21/how-to-copy-aws-ssm-parameters-from-one-account-to-another/). 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | '''setup.py: setuptools control.''' 6 | 7 | 8 | import re 9 | from setuptools import setup, find_packages 10 | from os import path 11 | 12 | here = path.abspath(path.dirname(__file__)) 13 | with open(path.join(here, 'README.md'), 'r') as f: 14 | long_description = f.read() 15 | 16 | version = "0.5.3" 17 | 18 | setup( 19 | name='aws-ssm-copy', 20 | package_dir={'': 'src'}, 21 | packages=find_packages(where='src'), 22 | entry_points={ 23 | 'console_scripts': ['aws-ssm-copy = aws_ssm_copy:main'] 24 | }, 25 | version=version, 26 | description='Copy AWS Parameter Store parameters', 27 | long_description=long_description, 28 | long_description_content_type="text/markdown", 29 | include_package_data=True, 30 | zip_safe=False, 31 | platforms='any', 32 | install_requires=['boto3'], 33 | author='Mark van Holsteijn', 34 | author_email='markvanholsteijn@binx.io', 35 | url='https://github.com/binxio/aws-ssm-copy', 36 | ) 37 | -------------------------------------------------------------------------------- /src/aws_ssm_copy/__init__.py: -------------------------------------------------------------------------------- 1 | from aws_ssm_copy.ssm_copy import main 2 | -------------------------------------------------------------------------------- /src/aws_ssm_copy/__main__.py: -------------------------------------------------------------------------------- 1 | from aws_ssm_copy.ssm_copy import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /src/aws_ssm_copy/ssm_copy.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import re 3 | import sys 4 | 5 | import boto3 6 | from botocore.exceptions import ClientError 7 | 8 | 9 | def rename_parameter(parameter, source_path, target_path): 10 | """ 11 | >>> rename_parameter({'Name':'/old-root/my-param'}, 'old-root', 'new-root') 12 | {'Name': '/new-root/my-param'} 13 | >>> rename_parameter({'Name':'/old-root/my-param'}, '/old-root', '/new-root') 14 | {'Name': '/new-root/my-param'} 15 | >>> rename_parameter({'Name':'old-root/my-param'}, '/old-root', '/new-root') 16 | {'Name': '/new-root/my-param'} 17 | >>> rename_parameter({'Name':'/old-root/my-param'}, '/invalid-root', '/new-root') 18 | {'Name': '/old-root/my-param'} 19 | >>> rename_parameter({'Name':'/old-root/my-param'}, '/old-root', None) 20 | {'Name': '/old-root/my-param'} 21 | >>> rename_parameter({'Name':'my-param'}, "/", "/new-root") 22 | {'Name': '/new-root/my-param'} 23 | >>> rename_parameter({'Name':'old-root/my-param'}, '/old-root', 'new-root') 24 | {'Name': '/new-root/my-param'} 25 | >>> rename_parameter({'Name':'/old-root-not/my-param'}, 'old-root', 'new-root') 26 | {'Name': '/old-root-not/my-param'} 27 | >>> rename_parameter({'Name':'/staging/mysql/MYSQL_OTHERS_PASSWORD'}, '/staging/mysql/MYSQL_OTHERS_PASSWORD', '/pepe/mysql') 28 | {'Name': '/pepe/mysql/MYSQL_OTHERS_PASSWORD'} 29 | >>> rename_parameter({'Name':'MYSQL_OTHERS_PASSWORD'}, 'MYSQL_OTHERS_PASSWORD', '/pepe/mysql') 30 | {'Name': '/pepe/mysql/MYSQL_OTHERS_PASSWORD'} 31 | >>> rename_parameter({'Name':'MYSQL_OTHERS_PASSWORD'}, 'MYSQL_OTHERS_PASSWORD', None) 32 | {'Name': 'MYSQL_OTHERS_PASSWORD'} 33 | 34 | """ 35 | result = parameter.copy() 36 | if not target_path: 37 | return result 38 | 39 | sp = source_path.strip("/") 40 | tp = target_path.strip("/") 41 | 42 | if sp == "": 43 | regex = r"^/?" 44 | elif parameter["Name"].strip("/") == sp: 45 | dir = "/".join(sp.split("/")[0:-1]) 46 | if dir: 47 | regex = r"^/?" + dir + "/" 48 | else: 49 | regex = r"^/?" 50 | else: 51 | regex = r"^/?" + sp + "/" 52 | 53 | result["Name"] = re.sub(regex, f"/{tp}/", parameter["Name"]) 54 | 55 | return result 56 | 57 | 58 | class ParameterCopier(object): 59 | def __init__(self): 60 | self.target_profile = None 61 | self.target_region = None 62 | self.source_profile = None 63 | self.source_region = None 64 | self.source_ssm = None 65 | self.target_ssm = None 66 | self.target_path = None 67 | self.dry_run = False 68 | 69 | @staticmethod 70 | def connect_to(profile, region): 71 | kwargs = {} 72 | if profile is not None: 73 | kwargs["profile_name"] = profile 74 | if region is not None: 75 | kwargs["region_name"] = region 76 | return boto3.Session(**kwargs) 77 | 78 | def connect_to_source(self, profile, region): 79 | self.source_ssm = self.connect_to(profile, region).client("ssm") 80 | 81 | def connect_to_target(self, profile, region): 82 | self.target_ssm = self.connect_to(profile, region).client("ssm") 83 | 84 | def load_source_parameters(self, arg, recursive, one_level): 85 | result = {} 86 | paginator = self.source_ssm.get_paginator("describe_parameters") 87 | kwargs = {} 88 | if recursive or one_level: 89 | option = "Recursive" if recursive else "OneLevel" 90 | kwargs["ParameterFilters"] = [ 91 | {"Key": "Path", "Option": option, "Values": [arg]} 92 | ] 93 | else: 94 | kwargs["ParameterFilters"] = [ 95 | {"Key": "Name", "Option": "Equals", "Values": [arg]} 96 | ] 97 | 98 | for page in paginator.paginate(**kwargs): 99 | for parameter in page["Parameters"]: 100 | result[parameter["Name"]] = parameter 101 | 102 | if len(result) == 0: 103 | sys.stderr.write("ERROR: {} not found.\n".format(arg)) 104 | sys.exit(1) 105 | return result 106 | 107 | def copy_tags(self, 108 | name: str, 109 | new_name: str 110 | ): 111 | """ 112 | copy the tags of the parameter `name` to `new_name`. Existing tags on `new_name` will be 113 | removed. 114 | """ 115 | source_tag_list = self.source_ssm.list_tags_for_resource( 116 | ResourceType='Parameter', 117 | ResourceId=name 118 | )['TagList'] 119 | source_tags = {tag['Key']: tag['Value'] for tag in source_tag_list} 120 | 121 | try: 122 | existing_tags_list = self.target_ssm.list_tags_for_resource( 123 | ResourceType='Parameter', 124 | ResourceId=new_name 125 | )['TagList'] 126 | target_tags = {tag['Key']: tag['Value'] for tag in existing_tags_list} 127 | except self.target_ssm.exceptions.InvalidResourceId: 128 | target_tags = {} 129 | 130 | to_remove = list(filter(lambda key: key not in source_tags, target_tags.keys())) 131 | to_add = list(filter(lambda key: key not in target_tags or source_tags[key] != target_tags[key], source_tags.keys())) 132 | if to_remove: 133 | 134 | if self.dry_run: 135 | sys.stdout.write(f"DRY-RUN: remove tags {','.join(to_remove)} from {new_name}\n") 136 | else: 137 | self.target_ssm.remove_tags_from_resource( 138 | ResourceType='Parameter', 139 | ResourceId=new_name, 140 | TagKeys=to_remove 141 | ) 142 | sys.stdout.write(f"INFO: removed tags {','.join(to_remove)} from {new_name}\n") 143 | 144 | if to_add: 145 | if self.dry_run: 146 | sys.stdout.write(f"DRY-RUN: adding tags {','.join(to_add)} from {new_name}\n") 147 | else: 148 | self.target_ssm.add_tags_to_resource( 149 | ResourceType='Parameter', 150 | ResourceId=new_name, 151 | Tags=[{'Key': key, 'Value': source_tags[key]} for key in to_add] 152 | ) 153 | sys.stdout.write(f"INFO: added tags {','.join(to_add)} to {new_name}\n") 154 | 155 | def copy( 156 | self, 157 | args, 158 | recursive, 159 | one_level, 160 | overwrite, 161 | key_id=None, 162 | clear_kms_key=False, 163 | keep_going=False, 164 | with_tags=False, 165 | ): 166 | for arg in args: 167 | parameters = self.load_source_parameters(arg, recursive, one_level) 168 | for name in parameters: 169 | value = self.source_ssm.get_parameter(Name=name, WithDecryption=True) 170 | 171 | parameter = parameters[name] 172 | parameter["Value"] = value["Parameter"]["Value"] 173 | 174 | if "KeyId" in parameter and key_id is not None: 175 | parameter["KeyId"] = key_id 176 | if "KeyId" in parameter and clear_kms_key: 177 | del parameter["KeyId"] 178 | for property_name in ["LastModifiedDate", "LastModifiedUser", "Version", "ARN"]: 179 | parameter.pop(property_name, None) 180 | 181 | if "Policies" in parameter: 182 | if not parameter["Policies"]: 183 | # an empty policies list causes an exception 184 | del parameter["Policies"] 185 | parameter["Overwrite"] = overwrite 186 | parameter = rename_parameter(parameter, arg, self.target_path) 187 | new_name = parameter["Name"] 188 | if self.dry_run: 189 | sys.stdout.write(f"DRY-RUN: copying {name} to {new_name}\n") 190 | self.copy_tags(name, new_name) 191 | else: 192 | try: 193 | self.target_ssm.put_parameter(**parameter) 194 | sys.stdout.write(f"INFO: copied {name} to {new_name}\n") 195 | if with_tags: 196 | self.copy_tags(name, new_name) 197 | except self.target_ssm.exceptions.ParameterAlreadyExists as e: 198 | if not keep_going: 199 | sys.stderr.write( 200 | f"ERROR: failed to copy {name} to {new_name} as it already exists: specify --overwrite or --keep-going\n" 201 | ) 202 | exit(1) 203 | else: 204 | sys.stderr.write( 205 | f"WARN: skipping copy {name} as {new_name} already exists\n" 206 | ) 207 | except ClientError as e: 208 | msg = e.response["Error"]["Message"] 209 | sys.stderr.write( 210 | f"ERROR: failed to copy {name} to {new_name}, {msg}\n" 211 | ) 212 | if not keep_going: 213 | exit(1) 214 | 215 | def main(self): 216 | parser = argparse.ArgumentParser(description="copy parameter store ") 217 | parser.add_argument( 218 | "--one-level", 219 | "-1", 220 | dest="one_level", 221 | action="store_true", 222 | help="one-level copy", 223 | ) 224 | parser.add_argument( 225 | "--recursive", 226 | "-r", 227 | dest="recursive", 228 | action="store_true", 229 | help="recursive copy", 230 | ) 231 | group = parser.add_mutually_exclusive_group() 232 | group.add_argument( 233 | "--overwrite", 234 | "-f", 235 | dest="overwrite", 236 | action="store_true", 237 | help="existing values", 238 | ) 239 | group.add_argument( 240 | "--keep-going", 241 | "-k", 242 | dest="keep_going", 243 | action="store_true", 244 | help="as much as possible after an error", 245 | ) 246 | 247 | parser.add_argument( 248 | "--dry-run", 249 | "-N", 250 | dest="dry_run", 251 | action="store_true", 252 | help="only show what is to be copied", 253 | ) 254 | parser.add_argument( 255 | "--source-region", 256 | dest="source_region", 257 | help="to get the parameters from ", 258 | metavar="AWS::Region", 259 | ) 260 | parser.add_argument( 261 | "--source-profile", 262 | dest="source_profile", 263 | help="to obtain the parameters from", 264 | metavar="NAME", 265 | ) 266 | parser.add_argument( 267 | "--region", 268 | dest="target_region", 269 | help="to copy the parameters to ", 270 | metavar="AWS::Region", 271 | ) 272 | parser.add_argument( 273 | "--profile", 274 | dest="target_profile", 275 | help="to copy the parameters to", 276 | metavar="NAME", 277 | ) 278 | parser.add_argument( 279 | "--target-path", 280 | dest="target_path", 281 | help="to copy the parameters to", 282 | metavar="NAME", 283 | ) 284 | key_group = parser.add_mutually_exclusive_group() 285 | key_group.add_argument( 286 | "--key-id", 287 | dest="key_id", 288 | help="to use for parameter values in the destination", 289 | metavar="ID", 290 | ) 291 | key_group.add_argument( 292 | "--clear-key-id", 293 | "-C", 294 | dest="clear_key_id", 295 | action="store_true", 296 | help="clear the KMS key id associated with the parameter", 297 | ) 298 | key_group.add_argument( 299 | "--with-tags", 300 | "-W", 301 | dest="with_tags", 302 | action="store_true", 303 | help="copy the tags of the parameters too", 304 | ) 305 | 306 | parser.add_argument( 307 | "parameters", metavar="PARAMETER", type=str, nargs="+", help="source path" 308 | ) 309 | options = parser.parse_args() 310 | 311 | try: 312 | self.connect_to_source(options.source_profile, options.source_region) 313 | self.connect_to_target(options.target_profile, options.target_region) 314 | self.target_path = options.target_path 315 | self.dry_run = options.dry_run 316 | self.copy( 317 | options.parameters, 318 | options.recursive, 319 | options.one_level, 320 | options.overwrite, 321 | options.key_id, 322 | options.clear_key_id, 323 | options.keep_going, 324 | options.with_tags 325 | ) 326 | except ClientError as e: 327 | sys.stderr.write("ERROR: {}\n".format(e)) 328 | sys.exit(1) 329 | 330 | 331 | def main(): 332 | cp = ParameterCopier() 333 | cp.main() 334 | 335 | 336 | if __name__ == "__main__": 337 | main() 338 | --------------------------------------------------------------------------------