├── .github ├── dependabot.yml └── workflows │ ├── check.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo.gif ├── go.mod ├── go.sum ├── main.go ├── mover.go ├── mover_test.go ├── planner.go ├── planner_test.go ├── prompt.go ├── prompt_test.go └── terraform.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | on: [push, pull_request] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | 11 | - name: Set up Go 12 | uses: actions/setup-go@v3 13 | with: 14 | go-version: 1.19 15 | id: go 16 | 17 | - name: Install terraform 18 | run: wget https://releases.hashicorp.com/terraform/1.2.2/terraform_1.2.2_linux_amd64.zip -O /tmp/terraform.zip && sudo unzip -o -d /usr/local/bin/ /tmp/terraform.zip 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v3 22 | 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v3.2.0 25 | with: 26 | version: v1.50 27 | 28 | - name: Get dependencies 29 | run: go get -v -t -d ./... 30 | 31 | - name: Test 32 | run: go test -v -cover -coverprofile=coverage.txt ./... 33 | 34 | - name: Coverage 35 | uses: codecov/codecov-action@v3 36 | with: 37 | files: ./coverage.txt 38 | 39 | - name: Build a snapshot with GoReleaser 40 | uses: goreleaser/goreleaser-action@v3 41 | with: 42 | args: build --snapshot 43 | 44 | pre012: 45 | name: pre012 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | 50 | - name: Set up Go 51 | uses: actions/setup-go@v3 52 | with: 53 | go-version: 1.19 54 | id: go 55 | 56 | - name: Install terraform 57 | run: wget https://releases.hashicorp.com/terraform/0.11.15/terraform_0.11.15_linux_amd64.zip -O /tmp/terraform.zip && sudo unzip -o -d /usr/local/bin/ /tmp/terraform.zip 58 | 59 | - name: Check out code into the Go module directory 60 | uses: actions/checkout@v3 61 | 62 | - name: Get dependencies 63 | run: go get -v -t -d ./... 64 | 65 | - name: Test 66 | run: go test -v -cover -coverprofile=coverage.txt ./... 67 | 68 | - name: Coverage 69 | uses: codecov/codecov-action@v3 70 | with: 71 | files: ./coverage.txt 72 | 73 | terraform_version_compatibility: 74 | name: Terraform compatibility 75 | runs-on: ubuntu-latest 76 | strategy: 77 | matrix: 78 | terraform: 79 | - 0.10.1 80 | - 0.10.2 81 | - 0.10.3 82 | - 0.10.4 83 | - 0.10.5 84 | - 0.10.6 85 | - 0.10.7 86 | - 0.10.8 87 | - 0.11.0 88 | - 0.11.1 89 | - 0.11.2 90 | - 0.11.3 91 | - 0.11.4 92 | - 0.11.5 93 | - 0.11.6 94 | - 0.11.7 95 | - 0.11.8 96 | - 0.11.9 97 | - 0.11.10 98 | - 0.11.11 99 | - 0.11.12 100 | - 0.11.13 101 | - 0.11.14 102 | - 0.11.15 103 | - 0.12.0 104 | - 0.12.1 105 | - 0.12.2 106 | - 0.12.3 107 | - 0.12.4 108 | - 0.12.5 109 | - 0.12.6 110 | - 0.12.7 111 | - 0.12.8 112 | - 0.12.9 113 | - 0.12.10 114 | - 0.12.11 115 | - 0.12.12 116 | - 0.12.13 117 | - 0.12.14 118 | - 0.12.15 119 | - 0.12.16 120 | - 0.12.17 121 | - 0.12.18 122 | - 0.12.19 123 | - 0.12.10 124 | - 0.12.21 125 | - 0.12.22 126 | - 0.12.23 127 | - 0.12.24 128 | - 0.12.25 129 | - 0.12.26 130 | - 0.12.27 131 | - 0.12.28 132 | - 0.12.29 133 | - 0.12.30 134 | - 0.12.31 135 | - 0.13.0 136 | - 0.13.1 137 | - 0.13.2 138 | - 0.13.3 139 | - 0.13.4 140 | - 0.13.5 141 | - 0.13.6 142 | - 0.13.7 143 | - 0.14.0 144 | - 0.14.1 145 | - 0.14.2 146 | - 0.14.3 147 | - 0.14.4 148 | - 0.14.5 149 | - 0.14.6 150 | - 0.14.7 151 | - 0.14.8 152 | - 0.14.9 153 | - 0.14.10 154 | - 0.14.11 155 | - 0.15.0 156 | - 0.15.1 157 | - 0.15.2 158 | - 0.15.3 159 | - 0.15.4 160 | - 0.15.5 161 | - 1.0.0 162 | - 1.0.1 163 | - 1.0.2 164 | - 1.0.3 165 | - 1.0.4 166 | - 1.0.5 167 | - 1.0.6 168 | - 1.0.7 169 | - 1.0.8 170 | - 1.0.9 171 | - 1.0.10 172 | - 1.0.11 173 | - 1.1.0 174 | - 1.1.1 175 | - 1.1.2 176 | - 1.1.3 177 | - 1.1.4 178 | - 1.1.5 179 | - 1.1.6 180 | - 1.1.7 181 | - 1.1.8 182 | - 1.1.9 183 | - 1.2.0 184 | - 1.2.1 185 | - 1.2.2 186 | 187 | steps: 188 | 189 | - name: Set up Go 190 | uses: actions/setup-go@v3 191 | with: 192 | go-version: 1.19 193 | id: go 194 | 195 | - name: Install terraform 196 | run: wget https://releases.hashicorp.com/terraform/${{ matrix.terraform }}/terraform_${{ matrix.terraform }}_linux_amd64.zip -O /tmp/terraform.zip && sudo unzip -o -d /usr/local/bin/ /tmp/terraform.zip 197 | 198 | - name: Check out code into the Go module directory 199 | uses: actions/checkout@v3 200 | 201 | - name: Get dependencies 202 | run: go get -v -t -d ./... 203 | 204 | # Making a special case for old versions of terraform, to ignore a revoked 205 | # GPG key from Hashicorp that is trusted by old versions of terraform. 206 | # https://discuss.hashicorp.com/t/terraform-updates-for-hcsec-2021-12/23570 207 | - name: Test 208 | run: | 209 | case ${{ matrix.terraform }} in 210 | 211 | # For the old versions of terraform that are patched, run the tests as normal. 212 | 0.11.15 | 0.12.31 | 0.13.7 ) 213 | go test -v ./... 214 | ;; 215 | 216 | # For the old unpatched versions, skip verifying plugins in the init step. 217 | 0.10.* | 0.11.* | 0.12.* | 0.13.* ) 218 | TF_CLI_ARGS_init="-verify-plugins=false" go test -v ./... 219 | ;; 220 | 221 | # For all other versions, run the tests as normal. 222 | * ) 223 | go test -v ./... 224 | ;; 225 | 226 | esac 227 | 228 | - name: Build 229 | run: go build -v . 230 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: 1.19 21 | 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v3 24 | with: 25 | version: latest 26 | args: release --rm-dist 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.PAT }} 29 | 30 | - name: Update Go report card 31 | uses: creekorful/goreportcard-action@v1.0 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | goos: 8 | - linux 9 | - windows 10 | - darwin 11 | archives: 12 | - replacements: 13 | darwin: Darwin 14 | linux: Linux 15 | windows: Windows 16 | 386: i386 17 | amd64: x86_64 18 | checksum: 19 | name_template: 'checksums.txt' 20 | snapshot: 21 | name_template: "{{ .Tag }}-next" 22 | brews: 23 | - tap: 24 | owner: mbode 25 | name: homebrew-tap 26 | homepage: "https://github.com/mbode/terraform-state-mover" 27 | description: "Refactoring Terraform code has never been easier" 28 | folder: Formula 29 | test: | 30 | system "#{bin}/terraform-state-mover", "--version" 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - Upgrade Go from 1.17 to 1.19 10 | ### Changed 11 | ### Deprecated 12 | ### Removed 13 | ### Fixed 14 | ### Security 15 | 16 | 17 | ## [0.5.0] - 2021-12-05 18 | ### Added 19 | - Reintroduce possibility of partial moves by selecting _Finished_ as source 20 | - Add builds for `darwin/arm64` and `windows/arm64` by upgrading Go from 1.15 to 1.17 21 | ### Fixed 22 | - Fix passing of extra arguments to `terraform plan` call 23 | 24 | ## [0.4.2] - 2021-12-05 25 | ### Fixed 26 | - Fix deprecation warning about `bottle :unneeded` in homebrew tap 27 | 28 | ## [0.4.1] - 2021-07-16 29 | ### Fixed 30 | - Fix `brew style` issue 31 | 32 | ## [0.4.0] - 2021-07-16 33 | ### Added 34 | - Search in resource addresses ([#20](https://github.com/mbode/terraform-state-mover/pull/20), contributed by [@pascal-hofmann](https://github.com/pascal-hofmann)) 35 | - Print terraform state mv commands on interrupt ([#22](https://github.com/mbode/terraform-state-mover/pull/22), contributed by [@pascal-hofmann](https://github.com/pascal-hofmann)) 36 | 37 | ## [0.3.0] - 2020-10-04 38 | ### Added 39 | - Delay flag to prevent hitting rate limits with remote state ([#8](https://github.com/mbode/terraform-state-mover/pull/8), contributed by [@xanonid](https://github.com/xanonid)) 40 | - Flags for verbose output and dry-run mode ([#8](https://github.com/mbode/terraform-state-mover/pull/8), contributed by [@xanonid](https://github.com/xanonid)) 41 | 42 | ## [0.2.0] - 2020-09-30 43 | ### Added 44 | - Publish a homebrew formula 45 | 46 | ## [0.1.0] - 2020-09-04 47 | ### Added 48 | - Initial release 49 | 50 | [Unreleased]: https://github.com/mbode/terraform-state-mover/compare/0.5.0...HEAD 51 | [0.5.0]: https://github.com/mbode/terraform-state-mover/compare/0.4.2...0.5.0 52 | [0.4.2]: https://github.com/mbode/terraform-state-mover/compare/0.4.1...0.4.2 53 | [0.4.1]: https://github.com/mbode/terraform-state-mover/compare/0.4.0...0.4.1 54 | [0.4.0]: https://github.com/mbode/terraform-state-mover/compare/0.3.0...0.4.0 55 | [0.3.0]: https://github.com/mbode/terraform-state-mover/compare/0.2.0...0.3.0 56 | [0.2.0]: https://github.com/mbode/terraform-state-mover/compare/0.1.0...0.2.0 57 | [0.1.0]: https://github.com/mbode/terraform-state-mover/releases/tag/0.1.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maximilian Bode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/mbode/terraform-state-mover)](https://goreportcard.com/report/github.com/mbode/terraform-state-mover) 2 | [![Actions Status](https://github.com/mbode/terraform-state-mover/workflows/Check/badge.svg)](https://github.com/mbode/terraform-state-mover/actions) 3 | [![codecov](https://codecov.io/gh/mbode/terraform-state-mover/branch/master/graph/badge.svg)](https://codecov.io/gh/mbode/terraform-state-mover) 4 | [![License](https://img.shields.io/github/license/mbode/terraform-state-mover)](https://github.com/mbode/terraform-state-mover/blob/master/LICENSE) 5 | [![Release](https://img.shields.io/github/v/release/mbode/terraform-state-mover)](https://github.com/mbode/terraform-state-mover/releases/latest) 6 | 7 | # Terraform State Mover 8 | 9 | Helps refactoring terraform code by offering an interactive prompt for the [`terraform state mv`](https://www.terraform.io/docs/commands/state/mv.html) command. 10 | 11 | ## Installation 12 | 13 | Using [homebrew](https://brew.sh/): 14 | ```bash 15 | brew install mbode/tap/terraform-state-mover 16 | ``` 17 | 18 | Alternatively, get a pre-built binary from the [latest release](https://github.com/mbode/terraform-state-mover/releases/latest) or build it yourself using 19 | 20 | ```bash 21 | go get github.com/mbode/terraform-state-mover 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```bash 27 | terraform-state-mover # inside a Terraform root directory 28 | ``` 29 | 30 | Extra arguments after a `--` are passed to the `terraform plan` call. This makes the following possible: 31 | ```bash 32 | terraform-state-mover -- -var key=value # set variables 33 | terraform-state-mover -- -var-file=variables.tfvars # use variable files 34 | terraform-state-mover -- -refresh=false # skip state refresh 35 | terraform-state-mover -- -parallelism=50 # speed up plan by using more concurrent operations 36 | ``` 37 | 38 | *Hint:* 39 | If terraform-state-mover is used with the [Google Cloud Platform provider](https://www.terraform.io/docs/providers/google/index.html) and remote state, it is recommended to use `--delay=2s`, otherwise API rate limits error might occur. 40 | 41 | ## Key mapping 42 | | Key | Action | 43 | |-----|-----------| 44 | | ⏎ | Select | 45 | | ↓ | Next | 46 | | ↑ | Previous | 47 | | → | Page Down | 48 | | ← | Page Up | 49 | 50 | ## Demo 51 | 52 | ![](demo.gif) 53 | 54 | ## Terraform version compatibility 55 | 56 | | < 0.10.1 | 0.10.1…8 | 0.11.0…15 | 0.12.0…31 | 0.13.0…7 | 0.14.0…11 | 0.15.0…5 | 1.0.0…11 | 1.1.0…9 | 1.2.0…2 | 57 | |:--------:|:--------:|:---------:|:---------:|:--------:|:---------:|:--------:|:--------:|:-------:|:-------:| 58 | | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 59 | 60 | ## Contributing 61 | Pull requests are welcome. Please make sure to update tests as appropriate. 62 | 63 | ## License 64 | [MIT](https://choosealicense.com/licenses/mit/) 65 | 66 | ## Similar tools 67 | - [tfautomv](https://github.com/busser/tfautomv) - Generate Terraform `moved` blocks automatically for painless refactoring 68 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbode/terraform-state-mover/91c573ee3494e7181796d961e72c456fe6851342/demo.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mbode/terraform-state-mover 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/agnivade/levenshtein v1.1.1 7 | github.com/hashicorp/go-version v1.3.0 8 | github.com/manifoldco/promptui v0.9.0 9 | github.com/urfave/cli/v2 v2.3.0 10 | ) 11 | 12 | require ( 13 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 14 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect 15 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 16 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 17 | golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= 3 | github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 4 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= 5 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 6 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 7 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 8 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 9 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 10 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 11 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= 14 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 15 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= 16 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 17 | github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= 18 | github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 19 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 20 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 24 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 25 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 26 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 27 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 28 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 29 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c h1:/h0vtH0PyU0xAoZJVcRw1k0Ng+U0JAy3QDiFmppIlIE= 31 | golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Terraform-state-mover helps refactoring terraform code by offering an interactive prompt for the `terraform state mv` command. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "github.com/manifoldco/promptui" 7 | "github.com/urfave/cli/v2" 8 | "log" 9 | "os" 10 | "reflect" 11 | "time" 12 | ) 13 | 14 | var ( 15 | version = "dev" 16 | ) 17 | 18 | type config struct { 19 | delay time.Duration 20 | verbose bool 21 | dryrun bool 22 | } 23 | 24 | func main() { 25 | 26 | // do not use "-v" to print the version 27 | cli.VersionFlag = &cli.BoolFlag{ 28 | Name: "version", Aliases: []string{}, 29 | Usage: "print the version only", 30 | } 31 | 32 | app := &cli.App{ 33 | Name: "terraform-state-mover", 34 | Usage: "refactoring Terraform code has never been easier", 35 | Authors: []*cli.Author{{Name: "Maximilian Bode", Email: "maxbode@gmail.com"}}, 36 | Action: action, 37 | Flags: []cli.Flag{ 38 | &cli.DurationFlag{ 39 | Name: "delay", Aliases: []string{"d"}, 40 | Usage: "Delay between terraform state mv calls. Helps to avoid rate-limits.", 41 | Value: time.Second * 0, 42 | }, 43 | &cli.BoolFlag{ 44 | Name: "verbose", Aliases: []string{"v"}, 45 | Usage: "Be more verbose - prints e.g. terraform mv calls", 46 | Value: false, 47 | }, 48 | &cli.BoolFlag{ 49 | Name: "dry-run", Aliases: []string{"n"}, 50 | Usage: "Do not actually move state, enables -v", 51 | Value: false, 52 | }, 53 | }, 54 | UsageText: "terraform-state-mover [-v] [-d delay] [-n] [-- ]", 55 | Version: version, 56 | } 57 | if err := app.Run(os.Args); err != nil { 58 | log.Fatal(err) 59 | } 60 | } 61 | 62 | func action(ctx *cli.Context) error { 63 | var planArgs []string 64 | for i, elem := range os.Args { 65 | if "--" == elem { 66 | planArgs = os.Args[i+1:] 67 | } 68 | } 69 | cfg := readConfig(ctx) 70 | 71 | changes, err := changes(cfg, planArgs) 72 | if err != nil { 73 | return err 74 | } 75 | dests := filterByAction(changes, create) 76 | srcs := filterByDestinationResourceTypes(filterByAction(changes, del), dests) 77 | 78 | moves := make(map[Resource]Resource) 79 | for len(srcs) > 0 && len(dests) > 0 { 80 | src, dest, err := prompt(srcs, dests) 81 | if err != nil { 82 | if err == promptui.ErrInterrupt && len(moves) > 0 { 83 | fmt.Println("Interrupted. These moves would have been executed based on your selections:") 84 | for src, dest := range moves { 85 | fmt.Printf(" terraform state mv '%s' '%s'\n", src.Address, dest.Address) 86 | } 87 | } 88 | return err 89 | } 90 | if reflect.DeepEqual(src, Resource{}) { 91 | break 92 | } 93 | moves[src] = dest 94 | delete(srcs, src) 95 | delete(dests, dest) 96 | } 97 | 98 | if len(moves) == 0 { 99 | fmt.Println("Nothing to do.") 100 | } 101 | 102 | var firstEntry = true 103 | for src, dest := range moves { 104 | if firstEntry { 105 | firstEntry = false 106 | } else { 107 | wait(cfg) 108 | } 109 | if err := move(cfg, src, dest); err != nil { 110 | return err 111 | } 112 | } 113 | return nil 114 | } 115 | 116 | func readConfig(ctx *cli.Context) config { 117 | return config{ 118 | delay: ctx.Duration("delay"), 119 | verbose: ctx.Bool("verbose") || ctx.Bool("dry-run"), 120 | dryrun: ctx.Bool("dry-run"), 121 | } 122 | } 123 | 124 | func wait(cfg config) { 125 | if cfg.verbose && cfg.delay > 0 { 126 | fmt.Println("Waiting", cfg.delay, "...") 127 | } 128 | time.Sleep(cfg.delay) 129 | } 130 | -------------------------------------------------------------------------------- /mover.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func move(cfg config, from Resource, to Resource) error { 4 | return terraformExec(cfg, false, []string{}, "state", "mv", from.Address, to.Address) 5 | } 6 | -------------------------------------------------------------------------------- /mover_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestMove(t *testing.T) { 10 | dir := createDir(t) 11 | defer os.RemoveAll(dir) 12 | 13 | oldTf := `resource "null_resource" "old" {} 14 | resource "null_resource" "second" {}` 15 | prepareState(dir, oldTf, t) 16 | 17 | newTf := `resource "null_resource" "new" {} 18 | resource "null_resource" "second" {}` 19 | if err := os.WriteFile(dir+"/main.tf", []byte(newTf), 0644); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if err := move(config{}, Resource{"null_resource.old", "null_resource"}, Resource{"null_resource.new", "null_resource"}); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | var want []ResChange 28 | isPre012, err := isPre012() 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | if !isPre012 { 33 | want = []ResChange{ 34 | {"null_resource.new", "null_resource", Change{[]changeAction{noOp}}}, 35 | {"null_resource.second", "null_resource", Change{[]changeAction{noOp}}}, 36 | } 37 | } 38 | 39 | if got, err := changes(config{}, []string{}); err != nil && !reflect.DeepEqual(got, want) { 40 | t.Errorf("changes() = %q, want %q", got, want) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /planner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "os" 7 | "os/exec" 8 | "reflect" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | func changes(cfg config, planArgs []string) ([]ResChange, error) { 14 | tfPlan, err := os.CreateTemp("", "tfplan") 15 | if err != nil { 16 | return nil, err 17 | } 18 | tfPlanName := tfPlan.Name() 19 | defer os.Remove(tfPlanName) 20 | 21 | if err := terraformExec(cfg, true, planArgs, "plan", "-out="+tfPlanName); err != nil { 22 | return nil, err 23 | } 24 | 25 | isPre012, err := isPre012() 26 | if err != nil { 27 | return nil, err 28 | } 29 | if isPre012 { 30 | cmd := exec.Command("terraform", "show", "-no-color", tfPlanName) 31 | var stdout bytes.Buffer 32 | cmd.Stdout = &stdout 33 | cmd.Stderr = os.Stderr 34 | if err = cmd.Run(); err != nil { 35 | return nil, err 36 | } 37 | stdoutBytes := stdout.Bytes() 38 | var changes []ResChange 39 | for _, res := range regexp.MustCompile(`(?m)\+ (.*)$`).FindAllSubmatch(stdoutBytes, -1) { 40 | address := string(res[1]) 41 | parts := strings.Split(address, ".") 42 | changes = append(changes, ResChange{address, parts[len(parts)-2], Change{[]changeAction{create}}}) 43 | } 44 | for _, res := range regexp.MustCompile(`(?m)- (.*)$`).FindAllSubmatch(stdoutBytes, -1) { 45 | address := string(res[1]) 46 | parts := strings.Split(address, ".") 47 | changes = append(changes, ResChange{address, parts[len(parts)-2], Change{[]changeAction{del}}}) 48 | } 49 | return changes, nil 50 | } 51 | cmd := exec.Command("terraform", "show", "-json", tfPlanName) 52 | var stdout bytes.Buffer 53 | cmd.Stdout = &stdout 54 | cmd.Stderr = os.Stderr 55 | if err = cmd.Run(); err != nil { 56 | return nil, err 57 | } 58 | 59 | changes := resChanges{} 60 | 61 | if err := json.Unmarshal(stdout.Bytes(), &changes); err != nil { 62 | return nil, err 63 | } 64 | return changes.ResChanges, nil 65 | } 66 | 67 | func filterByAction(resources []ResChange, action changeAction) map[Resource]bool { 68 | set := make(map[Resource]bool) 69 | for _, res := range resources { 70 | if reflect.DeepEqual(res.Change.Actions, []changeAction{action}) { 71 | set[Resource{res.Address, res.Type}] = true 72 | } 73 | } 74 | return set 75 | } 76 | 77 | func filterByDestinationResourceTypes(sourceResources map[Resource]bool, destResources map[Resource]bool) map[Resource]bool { 78 | set := make(map[Resource]bool) 79 | types := make(map[string]bool) 80 | for res := range destResources { 81 | types[res.Type] = true 82 | } 83 | 84 | for res := range sourceResources { 85 | if types[res.Type] || res.Address == FinishedAddress { 86 | set[Resource{res.Address, res.Type}] = true 87 | } 88 | } 89 | return set 90 | } 91 | -------------------------------------------------------------------------------- /planner_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestCreate(t *testing.T) { 10 | dir := createDir(t) 11 | defer os.RemoveAll(dir) 12 | 13 | content := `resource "null_resource" "first" {} 14 | resource "null_resource" "second" {}` 15 | if err := os.WriteFile(dir+"/main.tf", []byte(content), 0644); err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | if err := terraformExec(config{}, true, []string{}, "init"); err != nil { 20 | t.Fatalf("terraform init failed with %s\n", err) 21 | } 22 | 23 | want := []ResChange{ 24 | {"null_resource.first", "null_resource", Change{[]changeAction{create}}}, 25 | {"null_resource.second", "null_resource", Change{[]changeAction{create}}}, 26 | } 27 | got, err := changes(config{}, []string{}) 28 | if err != nil { 29 | t.Fatalf("failed computing changes") 30 | } 31 | if !reflect.DeepEqual(got, want) { 32 | t.Errorf("changes() = %q, want %q", got, want) 33 | } 34 | } 35 | 36 | func TestDelete(t *testing.T) { 37 | dir := createDir(t) 38 | defer os.RemoveAll(dir) 39 | 40 | content := `resource "null_resource" "first" {} 41 | resource "null_resource" "second" {}` 42 | prepareState(dir, content, t) 43 | 44 | if err := os.WriteFile(dir+"/main.tf", []byte("\n"), 0644); err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | want := []ResChange{ 49 | {"null_resource.first", "null_resource", Change{[]changeAction{del}}}, 50 | {"null_resource.second", "null_resource", Change{[]changeAction{del}}}, 51 | } 52 | got, err := changes(config{}, []string{}) 53 | if err != nil { 54 | t.Fatalf("failed computing changes") 55 | } 56 | if !reflect.DeepEqual(got, want) { 57 | t.Errorf("changes() = %q, want %q", got, want) 58 | } 59 | } 60 | 61 | func TestNoOp(t *testing.T) { 62 | dir := createDir(t) 63 | defer os.RemoveAll(dir) 64 | 65 | content := `resource "null_resource" "first" {} 66 | resource "null_resource" "second" {}` 67 | prepareState(dir, content, t) 68 | 69 | if err := os.WriteFile(dir+"/main.tf", []byte(content), 0644); err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | var want []ResChange 74 | isPre012, err := isPre012() 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | if !isPre012 { 79 | want = []ResChange{ 80 | {"null_resource.first", "null_resource", Change{[]changeAction{noOp}}}, 81 | {"null_resource.second", "null_resource", Change{[]changeAction{noOp}}}, 82 | } 83 | } 84 | 85 | got, err := changes(config{}, []string{}) 86 | if err != nil { 87 | t.Fatalf("failed computing changes") 88 | } 89 | if !reflect.DeepEqual(got, want) { 90 | t.Errorf("changes() = %q, want %q", got, want) 91 | } 92 | } 93 | 94 | func TestFilter(t *testing.T) { 95 | resources := []ResChange{ 96 | {"null_resource.create", "null_resource", Change{[]changeAction{create}}}, 97 | {"null_resource.delete", "null_resource", Change{[]changeAction{del}}}, 98 | {"null_resource.noop", "null_resource", Change{[]changeAction{noOp}}}, 99 | {"null_resource.update", "null_resource", Change{[]changeAction{update}}}, 100 | } 101 | 102 | want := make(map[Resource]bool) 103 | want[Resource{"null_resource.create", "null_resource"}] = true 104 | 105 | if got := filterByAction(resources, create); !reflect.DeepEqual(got, want) { 106 | t.Errorf("changes() = %v, want %v", got, want) 107 | } 108 | 109 | want = make(map[Resource]bool) 110 | want[Resource{"null_resource.delete", "null_resource"}] = true 111 | if got := filterByAction(resources, del); !reflect.DeepEqual(got, want) { 112 | t.Errorf("changes() = %v, want %v", got, want) 113 | } 114 | } 115 | 116 | func createDir(t *testing.T) string { 117 | dir, err := os.MkdirTemp("", t.Name()) 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | err = os.Chdir(dir) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | return dir 126 | } 127 | 128 | func prepareState(dir string, content string, t *testing.T) { 129 | if err := os.WriteFile(dir+"/main.tf", []byte(content), 0644); err != nil { 130 | t.Fatal(err) 131 | } 132 | if err := terraformExec(config{}, true, []string{}, "init"); err != nil { 133 | t.Fatal(err) 134 | } 135 | if err := terraformExec(config{}, true, []string{}, "apply", "-auto-approve"); err != nil { 136 | t.Fatal(err) 137 | } 138 | } 139 | 140 | func TestFilterByDestinationResourceTypes(t *testing.T) { 141 | resSrc1 := Resource{Address: "null_resource.resource_alpha", Type: "null_resource"} 142 | resSrc2 := Resource{Address: "null_resource.resource_beta", Type: "another_type"} 143 | resDest1 := Resource{Address: "null_resource.resource_gamma", Type: "null_resource"} 144 | resDest2 := Resource{Address: "null_resource.resource_delta", Type: "new_type"} 145 | 146 | sourceResources := make(map[Resource]bool) 147 | sourceResources[resSrc1] = true 148 | sourceResources[resSrc2] = true 149 | destResources := make(map[Resource]bool) 150 | destResources[resDest1] = true 151 | destResources[resDest2] = true 152 | 153 | want := make(map[Resource]bool) 154 | want[resSrc1] = true 155 | 156 | if got := filterByDestinationResourceTypes(sourceResources, destResources); !reflect.DeepEqual(got, want) { 157 | t.Errorf("filterByDestinationResourceTypes() = %v, want %v", got, want) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /prompt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/agnivade/levenshtein" 6 | "github.com/manifoldco/promptui" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | const FinishedAddress = "Finished" 12 | 13 | func prompt(sources map[Resource]bool, destinations map[Resource]bool) (Resource, Resource, error) { 14 | srcTempl := &promptui.SelectTemplates{ 15 | Label: "{{ . }}", 16 | Active: "\U0000261E {{ .Address | cyan | underline }} ({{ .Type | red }})", 17 | Inactive: " {{ .Address | cyan }} ({{ .Type | red }})", 18 | Selected: "{{ .Address }}", 19 | } 20 | srcs := append(toSlice(sources), Resource{FinishedAddress, "no more resources to move"}) 21 | srcSearcher := func(input string, index int) bool { 22 | if index >= len(srcs) { 23 | return false 24 | } 25 | 26 | resource := srcs[index] 27 | return strings.Contains(resource.Address, input) 28 | } 29 | 30 | prompt := promptui.Select{Label: "Select Source", Items: srcs, Templates: srcTempl, Searcher: srcSearcher, StartInSearchMode: true} 31 | i, _, err := prompt.Run() 32 | var empty Resource 33 | if err != nil { 34 | return empty, empty, err 35 | } 36 | if i == len(srcs)-1 { 37 | return empty, empty, nil 38 | } 39 | src := srcs[i] 40 | 41 | dests := toSlice(destinations) 42 | var compatDests []Resource 43 | for _, r := range dests { 44 | if r.Type == src.Type { 45 | compatDests = append(compatDests, r) 46 | } 47 | } 48 | destSearcher := func(input string, index int) bool { 49 | resource := compatDests[index] 50 | return strings.Contains(resource.Address, input) 51 | } 52 | sortByLevenshteinDistance(compatDests, src) 53 | 54 | fmt.Println(strings.Repeat(" ", len(src.Address)), "↘") 55 | 56 | spaces := strings.Repeat(" ", len(src.Address)+3) 57 | destTempl := &promptui.SelectTemplates{ 58 | Label: spaces + "{{ . }}", 59 | Active: spaces + "\U0000261E {{ .Address | cyan | underline }} ({{ .Type | red }})", 60 | Inactive: spaces + " {{ .Address | cyan }} ({{ .Type | red }})", 61 | Selected: spaces + "{{ .Address }}", 62 | } 63 | prompt = promptui.Select{Label: "Select Destination", Items: compatDests, Templates: destTempl, Searcher: destSearcher, StartInSearchMode: true} 64 | j, _, err := prompt.Run() 65 | if err != nil { 66 | return Resource{}, Resource{}, err 67 | } 68 | return src, compatDests[j], nil 69 | } 70 | 71 | func sortByLevenshteinDistance(dests []Resource, src Resource) { 72 | sort.Slice(dests, func(i, j int) bool { 73 | distanceToItemI := levenshtein.ComputeDistance(src.Address, dests[i].Address) 74 | distanceToItemJ := levenshtein.ComputeDistance(src.Address, dests[j].Address) 75 | if distanceToItemI != distanceToItemJ { 76 | return distanceToItemI < distanceToItemJ 77 | } 78 | return dests[i].Address < dests[j].Address 79 | }) 80 | } 81 | 82 | func toSlice(set map[Resource]bool) []Resource { 83 | result := []Resource{} 84 | for elem := range set { 85 | result = append(result, elem) 86 | } 87 | return result 88 | } 89 | -------------------------------------------------------------------------------- /prompt_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSortByLevenshteinDistance(t *testing.T) { 9 | type args struct { 10 | inputDests []Resource 11 | inputSrc Resource 12 | expect []Resource 13 | } 14 | resSrc := Resource{Address: "module.oldname.null_resource.resource_alpha"} 15 | resDest1 := Resource{Address: "module.newname.null_resource.resource_alpha"} 16 | resDest2 := Resource{Address: "module.newname.null_resource.resource_beta"} 17 | resDest3 := Resource{Address: "module.oldname.null_resource.other_resourceA"} 18 | resDest4 := Resource{Address: "module.oldname.null_resource.other_resourceB"} 19 | 20 | tests := []struct { 21 | name string 22 | args args 23 | }{ 24 | {name: "simple sort", args: args{inputDests: []Resource{resDest2, resDest1}, inputSrc: resSrc, expect: []Resource{resDest1, resDest2}}}, 25 | {name: "already sorted", args: args{inputDests: []Resource{resDest1, resDest2}, inputSrc: resSrc, expect: []Resource{resDest1, resDest2}}}, 26 | {name: "bigger test case", args: args{inputDests: []Resource{resDest3, resDest1, resDest2}, inputSrc: resSrc, expect: []Resource{resDest1, resDest2, resDest3}}}, 27 | {name: "sorts finally alphabetically", args: args{inputDests: []Resource{resDest4, resDest3}, inputSrc: resSrc, expect: []Resource{resDest3, resDest4}}}, 28 | } 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | if sortByLevenshteinDistance(tt.args.inputDests, tt.args.inputSrc); !reflect.DeepEqual(tt.args.inputDests, tt.args.expect) { 32 | t.Errorf("sortByLevenshteinDistance() = %v, want %v", tt.args.inputDests, tt.args.expect) 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /terraform.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "regexp" 9 | "strings" 10 | 11 | go_version "github.com/hashicorp/go-version" 12 | ) 13 | 14 | type resChanges struct { 15 | ResChanges []ResChange `json:"resource_changes"` 16 | } 17 | 18 | // ResChange represents a resource change in a Terraform plan. 19 | type ResChange struct { 20 | Address string 21 | Type string 22 | Change Change 23 | } 24 | 25 | // Change represents a list of actions to one resource in a Terraform plan. 26 | type Change struct { 27 | Actions []changeAction 28 | } 29 | 30 | type changeAction string 31 | 32 | const ( 33 | noOp changeAction = "no-op" 34 | create changeAction = "create" 35 | update changeAction = "update" 36 | del changeAction = "delete" 37 | ) 38 | 39 | // Resource represents a Terraform resource and consists of a type and an address. 40 | type Resource struct { 41 | Address string 42 | Type string 43 | } 44 | 45 | func terraformExec(cfg config, executeInDryRun bool, args []string, extraArgs ...string) error { 46 | args = append(extraArgs, args...) 47 | if cfg.dryrun && !executeInDryRun { 48 | fmt.Println("Dry-run, would have called: terraform", strings.Join(args, " ")) 49 | return nil 50 | } 51 | if cfg.verbose { 52 | fmt.Println("Calling: terraform", strings.Join(args, " ")) 53 | } 54 | cmd := exec.Command("terraform", args...) 55 | cmd.Stderr = os.Stderr 56 | cmd.Env = append(os.Environ(), 57 | "TF_INPUT=false", 58 | ) 59 | return cmd.Run() 60 | } 61 | 62 | func isPre012() (bool, error) { 63 | cmd := exec.Command("terraform", "version") 64 | cmdOutput := &bytes.Buffer{} 65 | cmd.Stdout = cmdOutput 66 | err := cmd.Run() 67 | if err != nil { 68 | return false, err 69 | } 70 | output := cmdOutput.Bytes() 71 | var ver = regexp.MustCompile(`Terraform v(\d+\.\d+\.\d+)`) 72 | result := ver.FindStringSubmatch(string(output)) 73 | v012, err := go_version.NewVersion("0.12") 74 | if err != nil { 75 | return false, err 76 | } 77 | current, err := go_version.NewVersion(result[1]) 78 | if err != nil { 79 | return false, err 80 | } 81 | return current.LessThan(v012), nil 82 | } 83 | --------------------------------------------------------------------------------