diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae7594e7..0cf242d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: mkdir -p build go build -v -o build/tfswitch mkdir `pwd`/bin/ - find ./test-data/* -type d -print0 | while read -r -d $'\0' TEST_PATH; do + find ./test-data/integration-tests/* -type d -print0 | while read -r -d $'\0' TEST_PATH; do ./build/tfswitch -c "${TEST_PATH}" -b `pwd`/bin/terraform || exit 1 done diff --git a/go.mod b/go.mod index 709ee2d2..73fe234b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/gookit/slog v0.5.5 github.com/hashicorp/go-version v1.6.0 - github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80 + github.com/hashicorp/hcl/v2 v2.20.1 github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72 github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/go-homedir v1.1.0 @@ -16,36 +16,37 @@ require ( ) require ( - github.com/agext/levenshtein v1.2.2 // indirect - github.com/apparentlymart/go-textseg v1.0.0 // indirect - github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gookit/goutil v0.6.15 // indirect github.com/gookit/gsr v0.1.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/hcl/v2 v2.0.0 // indirect + github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/zclconf/go-cty v1.1.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sync v0.5.0 // indirect + github.com/zclconf/go-cty v1.14.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.20.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 992a3508..d32fc500 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,28 @@ -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= -github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= -github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= -github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= -github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo= @@ -35,66 +31,50 @@ github.com/gookit/gsr v0.1.0 h1:0gadWaYGU4phMs0bma38t+Do5OZowRMEVlHv31p0Zig= github.com/gookit/gsr v0.1.0/go.mod h1:7wv4Y4WCnil8+DlDYHBjidzrEzfHhXEoFjEA0pPPWpI= github.com/gookit/slog v0.5.5 h1:XoyK3NilKzuC/umvnqTQDHTOnpC8R6pvlr/ht9PyfgU= github.com/gookit/slog v0.5.5/go.mod h1:RfIwzoaQ8wZbKdcqG7+3EzbkMqcp2TUn3mcaSZAw2EQ= -github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8= -github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= -github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80 h1:PFfGModn55JA0oBsvFghhj0v93me+Ctr3uHC/UmFAls= -github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0= +github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= +github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc= +github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72 h1:nZ5gGjbe5o7XUu1d7j+Y5Ztcxlp+yaumTKH9i0D3wlg= github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72/go.mod h1:l8HcFPm9cQh6Q0KSWoYPiePqMvRFenybP1CH2MjKdlg= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0= github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= +github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= @@ -102,8 +82,7 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -113,56 +92,37 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= -github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw= -github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= +github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= diff --git a/lib/checksum.go b/lib/checksum.go index b0b0d2db..73de2e20 100644 --- a/lib/checksum.go +++ b/lib/checksum.go @@ -33,29 +33,27 @@ func getChecksumFromHashFile(signatureFilePath string, terraformFileName string) // checkChecksumMatches This will calculate and compare the check sum of the downloaded zip file func checkChecksumMatches(hashFile string, targetFile *os.File) bool { + logger.Debugf("Checksum comparison for %q", targetFile.Name()) var fileHandlersToClose []*os.File fileHandlersToClose = append(fileHandlersToClose, targetFile) + defer closeFileHandlers(fileHandlersToClose) _, fileName := filepath.Split(targetFile.Name()) expectedChecksum, err := getChecksumFromHashFile(hashFile, fileName) if err != nil { - closeFileHandlers(fileHandlersToClose) logger.Errorf("Could not get checksum from file %q: %v", hashFile, err) return false } hash := sha256.New() if _, err := io.Copy(hash, targetFile); err != nil { - closeFileHandlers(fileHandlersToClose) logger.Errorf("Checksum calculation failed for %q: %v", fileName, err) return false } checksum := hex.EncodeToString(hash.Sum(nil)) if expectedChecksum != checksum { - closeFileHandlers(fileHandlersToClose) logger.Errorf("Checksum mismatch for %q. Expected: %q, calculated: %v", fileName, expectedChecksum, checksum) return false } - closeFileHandlers(fileHandlersToClose) return true } diff --git a/lib/checksum_test.go b/lib/checksum_test.go index cb68df06..60a77369 100644 --- a/lib/checksum_test.go +++ b/lib/checksum_test.go @@ -18,6 +18,7 @@ func Test_getChecksumFromHashFile(t *testing.T) { } func Test_checkChecksumMatches(t *testing.T) { + InitLogger("TRACE") targetFile, err := os.Open("../test-data/checksum-check-file") if err != nil { t.Errorf("[Error]: Could not open testfile for signature verification.") diff --git a/lib/common.go b/lib/common.go index b82a8f16..a201f02d 100644 --- a/lib/common.go +++ b/lib/common.go @@ -30,16 +30,6 @@ func createFile(path string) { logger.Infof("==> done creating %q file", path) } -func createDirIfNotExist(dir string) { - if _, err := os.Stat(dir); os.IsNotExist(err) { - logger.Infof("Creating directory for terraform: %v", dir) - err = os.MkdirAll(dir, 0755) - if err != nil { - logger.Panic("Unable to create %q directory for terraform: %v", dir, err) - } - } -} - func cleanUp(path string) { removeContents(path) removeFiles(path) diff --git a/lib/defaults.go b/lib/defaults.go index 2bf1f297..bdcde1c0 100644 --- a/lib/defaults.go +++ b/lib/defaults.go @@ -30,3 +30,13 @@ func GetDefaultBin() string { } return defaultBin } + +const ( + DefaultMirror = "https://releases.hashicorp.com/terraform" + DefaultLatest = "" + installFile = "terraform" + installPath = ".terraform.versions" + recentFile = "RECENT" + tfDarwinArm64StartVersion = "1.0.2" + VersionPrefix = "terraform_" +) diff --git a/lib/download.go b/lib/download.go index f8cfdf87..a03a6b55 100644 --- a/lib/download.go +++ b/lib/download.go @@ -121,7 +121,7 @@ func downloadFromURL(installLocation string, url string) (string, error) { func downloadPublicKey(installLocation string, targetFileName string) error { logger.Debugf("Looking up public key file at %q", targetFileName) - publicKeyFileExists := FileExists(targetFileName) + publicKeyFileExists := FileExistsAndIsNotDir(targetFileName) if !publicKeyFileExists { // Public key does not exist. Let's grab it from hashicorp pubKeyFile, errDl := downloadFromURL(installLocation, PubKeyUri) diff --git a/lib/download_test.go b/lib/download_test.go index e10daf0f..863ac16b 100644 --- a/lib/download_test.go +++ b/lib/download_test.go @@ -25,14 +25,14 @@ func TestDownloadFromURL_FileNameMatch(t *testing.T) { logger.Fatalf("Could not detect home directory") } - logger.Infof("Current home directory: %q", home) + t.Logf("Current home directory: %q", home) var installLocation = "" if runtime.GOOS != "windows" { installLocation = filepath.Join(home, installPath) } else { installLocation = installPath } - logger.Infof("Install Location: %v", installLocation) + t.Logf("install Location: %v", installLocation) // create /.terraform.versions_test/ directory to store code if _, err := os.Stat(installLocation); os.IsNotExist(err) { @@ -74,7 +74,7 @@ func TestDownloadFromURL_FileNameMatch(t *testing.T) { t.Cleanup(func() { defer os.Remove(tempDir) - logger.Infof("Cleanup temporary directory %q", tempDir) + t.Logf("Cleanup temporary directory %q", tempDir) }) } diff --git a/lib/files.go b/lib/files.go index 26cf8ecc..e8992364 100644 --- a/lib/files.go +++ b/lib/files.go @@ -4,19 +4,20 @@ import ( "archive/zip" "bufio" "bytes" + "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" + "sync" ) // RenameFile : rename file name func RenameFile(src string, dest string) { + logger.Debugf("Renaming file %q to %q", src, dest) err := os.Rename(src, dest) if err != nil { - logger.Error(err) - return + logger.Fatal(err) } } @@ -46,68 +47,39 @@ func CheckFileExist(file string) bool { // Unzip will decompress a zip archive, moving all files and folders // within the zip file (parameter 1) to an output directory (parameter 2). func Unzip(src string, dest string) ([]string, error) { + logger.Debugf("Unzipping file %q", src) var filenames []string - r, err := zip.OpenReader(src) + reader, err := zip.OpenReader(src) if err != nil { return filenames, err } - defer r.Close() - - for _, f := range r.File { - - filePath, _ := filepath.Abs(f.Name) - rc, err := f.Open() - if err != nil { - return filenames, err - } - defer rc.Close() - - // Store filename/path for returning and using later on - fpath := filepath.Join(dest, f.Name) - filenames = append(filenames, fpath) - - if f.FileInfo().IsDir() { - - // Make Folder - _ = os.MkdirAll(fpath, os.ModePerm) - + defer reader.Close() + destination, err := filepath.Abs(dest) + if err != nil { + logger.Fatalf("Could not open destination: %v", err) + } + var wg sync.WaitGroup + for _, f := range reader.File { + wg.Add(1) + unzipErr := unzipFile(f, destination, &wg) + if unzipErr != nil { + logger.Fatalf("Error unzipping %v", unzipErr) } else { - if !strings.Contains(filePath, "..") { - // Make File - if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { - return filenames, err - } - - outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return filenames, err - } - - _, err = io.Copy(outFile, rc) - - // Close the file without defer to close before next iteration of loop - _ = outFile.Close() - - if err != nil { - return filenames, err - } - } - + filenames = append(filenames, filepath.Join(destination, f.Name)) } } return filenames, nil } -// CreateDirIfNotExist : create directory if directory does not exist -func CreateDirIfNotExist(dir string) { +// createDirIfNotExist : create directory if directory does not exist +func createDirIfNotExist(dir string) { if _, err := os.Stat(dir); os.IsNotExist(err) { logger.Infof("Creating directory for terraform binary at %q", dir) err = os.MkdirAll(dir, 0755) if err != nil { - logger.Error(err) - logger.Panicf("Unable to create directory for terraform binary at: %v", dir) + logger.Panicf("Unable to create %q directory for terraform: %v", dir, err) } } } @@ -188,10 +160,9 @@ func CheckDirHasTGBin(dir, prefix string) bool { exist := false - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) if err != nil { logger.Fatal(err) - os.Exit(1) } res := []string{} for _, f := range files { @@ -230,7 +201,56 @@ func GetCurrentDirectory() string { dir, err := os.Getwd() //get current directory if err != nil { logger.Fatalf("Failed to get current directory %v", err) - os.Exit(1) } return dir } + +func unzipFile(f *zip.File, destination string, wg *sync.WaitGroup) error { + defer wg.Done() + // 1. Check if file paths are not vulnerable to Zip Slip + filePath := filepath.Join(destination, f.Name) + if !strings.HasPrefix(filePath, filepath.Clean(destination)+string(os.PathSeparator)) { + return fmt.Errorf("Invalid file path: %q", filePath) + } + + // 2. Create directory tree + if f.FileInfo().IsDir() { + logger.Debugf("Extracting directory %q", filePath) + if err := os.MkdirAll(filePath, os.ModePerm); err != nil { + return err + } + return nil + } + + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return err + } + + // 3. Create a destination file for unzipped content + destinationFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + defer func(destinationFile *os.File) { + _ = destinationFile.Close() + }(destinationFile) + if err != nil { + return err + } + + // 4. Unzip the content of a file and copy it to the destination file + zippedFile, err := f.Open() + defer func(zippedFile io.ReadCloser) { + _ = zippedFile.Close() + }(zippedFile) + if err != nil { + return err + } + + logger.Debugf("Extracting File %q", destinationFile.Name()) + if _, err := io.Copy(destinationFile, zippedFile); err != nil { + return err + } + logger.Debugf("Closing destination file handler %q", destinationFile.Name()) + _ = destinationFile.Close() + logger.Debugf("Closing zipped file handler %q", f.Name) + _ = zippedFile.Close() + return nil +} diff --git a/lib/files_test.go b/lib/files_test.go index 7f59a9d1..1726cf82 100644 --- a/lib/files_test.go +++ b/lib/files_test.go @@ -25,7 +25,7 @@ func TestRenameFile(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installLocation := filepath.Join(homedir, installPath) @@ -71,7 +71,7 @@ func TestRemoveFiles(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installLocation := filepath.Join(homedir, installPath) @@ -106,11 +106,11 @@ func TestUnzip(t *testing.T) { installPath := "/.terraform.versions_test/" absPath, _ := filepath.Abs("../test-data/test-data.zip") - logger.Infof("Absolute Path: %q", absPath) + t.Logf("Absolute Path: %q", absPath) homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installLocation := filepath.Join(homedir, installPath) @@ -119,7 +119,7 @@ func TestUnzip(t *testing.T) { files, errUnzip := Unzip(absPath, installLocation) if errUnzip != nil { - logger.Fatalf("Unable to unzip %q file: %v", absPath, errUnzip) + t.Errorf("Unable to unzip %q file: %v", absPath, errUnzip) } tst := strings.Join(files, "") @@ -140,7 +140,7 @@ func TestCreateDirIfNotExist(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installLocation := filepath.Join(homedir, installPath) @@ -153,7 +153,7 @@ func TestCreateDirIfNotExist(t *testing.T) { t.Error("Directory should not exist") } - CreateDirIfNotExist(installLocation) + createDirIfNotExist(installLocation) t.Logf("Creating directory %v", installLocation) if _, err := os.Stat(installLocation); err == nil { @@ -175,7 +175,7 @@ func TestWriteLines(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installLocation := filepath.Join(homedir, installPath) @@ -188,7 +188,7 @@ func TestWriteLines(t *testing.T) { if errWrite != nil { t.Logf("Write should work %v (unexpected)", errWrite) - logger.Fatal(errWrite) + t.Error(errWrite) } else { var ( file *os.File @@ -198,7 +198,7 @@ func TestWriteLines(t *testing.T) { lines []string ) if file, errOpen = os.Open(recentFilePath); errOpen != nil { - logger.Fatal(errOpen) + t.Error(errOpen) } reader := bufio.NewReader(file) @@ -218,12 +218,12 @@ func TestWriteLines(t *testing.T) { } if errRead != nil { - logger.Fatalf("Error: %s", errRead) + t.Errorf("Error: %s", errRead) } for _, line := range lines { if !semverRegex.MatchString(line) { - logger.Fatalf("Write to file is not invalid: %s", line) + t.Errorf("Write to file is not invalid: %s", line) break } } @@ -243,7 +243,7 @@ func TestReadLines(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installLocation := filepath.Join(homedir, installPath) @@ -258,13 +258,13 @@ func TestReadLines(t *testing.T) { ) if file, errCreate = os.Create(recentFilePath); errCreate != nil { - logger.Fatalf("Error: %s", errCreate) + t.Errorf("Error: %s", errCreate) } for _, item := range test_array { _, err := file.WriteString(strings.TrimSpace(item) + "\n") if err != nil { - logger.Fatalf("Error: %s", err) + t.Errorf("Error: %s", err) break } } @@ -272,12 +272,12 @@ func TestReadLines(t *testing.T) { lines, errRead := ReadLines(recentFilePath) if errRead != nil { - logger.Fatalf("Error: %s", errRead) + t.Errorf("Error: %s", errRead) } for _, line := range lines { if !semverRegex.MatchString(line) { - logger.Fatalf("Write to file is not invalid: %s", line) + t.Errorf("Write to file is not invalid: %s", line) } } @@ -295,7 +295,7 @@ func TestIsDirEmpty(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installLocation := filepath.Join(homedir, installPath) @@ -330,7 +330,7 @@ func TestCheckDirHasTFBin(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installLocation := filepath.Join(homedir, installPath) @@ -359,7 +359,7 @@ func TestPath(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installLocation := filepath.Join(homedir, installPath) @@ -400,7 +400,7 @@ func TestGetFileName(t *testing.T) { func TestConvertExecutableExt(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } installPath := "/.terraform.versions_test/" diff --git a/lib/install.go b/lib/install.go index 2c37c4a4..e23098b7 100644 --- a/lib/install.go +++ b/lib/install.go @@ -2,6 +2,7 @@ package lib import ( "fmt" + "github.com/manifoldco/promptui" "os" "path/filepath" "runtime" @@ -11,22 +12,12 @@ import ( "github.com/mitchellh/go-homedir" ) -const ( - installFile = "terraform" - versionPrefix = "terraform_" - installPath = ".terraform.versions" - recentFile = "RECENT" - defaultBin = "/usr/local/bin/terraform" //default bin installation dir - tfDarwinArm64StartVersion = "1.0.2" -) - var ( installLocation = "/tmp" ) // initialize : removes existing symlink to terraform binary based on provided binPath func initialize(binPath string) { - /* find terraform binary location if terraform is already installed*/ cmd := NewCommand(binPath) next := cmd.Find() @@ -44,42 +35,39 @@ func initialize(binPath string) { if symlinkExist { RemoveSymlink(binPath) } - } -// GetInstallLocation : get location where the terraform binary will be installed, +// getInstallLocation : get location where the terraform binary will be installed, // will create a directory in the home location if it does not exist -func GetInstallLocation() string { +func getInstallLocation() string { /* get current user */ - homedir, errCurr := homedir.Dir() + homeDir, errCurr := homedir.Dir() if errCurr != nil { logger.Fatal(errCurr) os.Exit(1) } - userCommon := homedir + userCommon := homeDir /* set installation location */ installLocation = filepath.Join(userCommon, installPath) /* Create local installation directory if it does not exist */ - CreateDirIfNotExist(installLocation) - + createDirIfNotExist(installLocation) return installLocation - } -// Install : Install the provided version in the argument -func Install(tfversion string, binPath string, mirrorURL string) { +// install : install the provided version in the argument +func install(tfversion string, binPath string, mirrorURL string) { /* Check to see if user has permission to the default bin location which is "/usr/local/bin/terraform" * If user does not have permission to default bin location, proceed to create $HOME/bin and install the tfswitch there * Inform user that they don't have permission to default location, therefore tfswitch was installed in $HOME/bin * Tell users to add $HOME/bin to their path */ - binPath = InstallableBinLocation(binPath) + binPath = installableBinLocation(binPath) initialize(binPath) //initialize path - installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file + installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file goarch := runtime.GOARCH goos := runtime.GOOS @@ -92,7 +80,7 @@ func Install(tfversion string, binPath string, mirrorURL string) { } /* check if selected version already downloaded */ - installFileVersionPath := ConvertExecutableExt(filepath.Join(installLocation, versionPrefix+tfversion)) + installFileVersionPath := ConvertExecutableExt(filepath.Join(installLocation, VersionPrefix+tfversion)) fileExist := CheckFileExist(installFileVersionPath) /* if selected version already exist, */ @@ -108,7 +96,7 @@ func Install(tfversion string, binPath string, mirrorURL string) { /* set symlink to desired version */ CreateSymlink(installFileVersionPath, binPath) logger.Infof("Switched terraform to version %q", tfversion) - AddRecent(tfversion) //add to recent file for faster lookup + addRecent(tfversion) //add to recent file for faster lookup os.Exit(0) } @@ -120,19 +108,17 @@ func Install(tfversion string, binPath string, mirrorURL string) { /* if selected version already exist, */ /* proceed to download it from the hashicorp release page */ - zipFile, errDownload := DownloadFromURL(installLocation, mirrorURL, tfversion, versionPrefix, goos, goarch) + zipFile, errDownload := DownloadFromURL(installLocation, mirrorURL, tfversion, VersionPrefix, goos, goarch) /* If unable to download file from url, exit(1) immediately */ if errDownload != nil { logger.Fatalf("Error downloading: %s", errDownload) - os.Exit(1) } /* unzip the downloaded zipfile */ _, errUnzip := Unzip(zipFile, installLocation) if errUnzip != nil { logger.Fatalf("Unable to unzip %q file: %v", zipFile, errUnzip) - os.Exit(1) } /* rename unzipped file to terraform version name - terraform_x.x.x */ @@ -152,14 +138,14 @@ func Install(tfversion string, binPath string, mirrorURL string) { /* set symlink to desired version */ CreateSymlink(installFileVersionPath, binPath) logger.Infof("Switched terraform to version %q", tfversion) - AddRecent(tfversion) //add to recent file for faster lookup + addRecent(tfversion) //add to recent file for faster lookup os.Exit(0) } -// AddRecent : add to recent file -func AddRecent(requestedVersion string) { +// addRecent : add to recent file +func addRecent(requestedVersion string) { - installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file + installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file versionFile := filepath.Join(installLocation, recentFile) fileExist := CheckFileExist(versionFile) @@ -172,7 +158,7 @@ func AddRecent(requestedVersion string) { } for _, line := range lines { - if !ValidVersionFormat(line) { + if !validVersionFormat(line) { logger.Infof("File %q is dirty (recreating cache file)", versionFile) RemoveFiles(versionFile) CreateRecentFile(requestedVersion) @@ -180,17 +166,17 @@ func AddRecent(requestedVersion string) { } } - versionExist := VersionExist(requestedVersion, lines) + versionExist := versionExist(requestedVersion, lines) if !versionExist { if len(lines) >= 3 { _, lines = lines[len(lines)-1], lines[:len(lines)-1] lines = append([]string{requestedVersion}, lines...) - WriteLines(lines, versionFile) + _ = WriteLines(lines, versionFile) } else { lines = append([]string{requestedVersion}, lines...) - WriteLines(lines, versionFile) + _ = WriteLines(lines, versionFile) } } @@ -199,17 +185,17 @@ func AddRecent(requestedVersion string) { } } -// GetRecentVersions : get recent version from file -func GetRecentVersions() ([]string, error) { +// getRecentVersions : get recent version from file +func getRecentVersions() ([]string, error) { - installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file + installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file versionFile := filepath.Join(installLocation, recentFile) fileExist := CheckFileExist(versionFile) if fileExist { lines, errRead := ReadLines(versionFile) - outputRecent := []string{} + var outputRecent []string if errRead != nil { logger.Errorf("Error reading %q file: %f", versionFile, errRead) @@ -221,7 +207,7 @@ func GetRecentVersions() ([]string, error) { If any version is invalid, it will be considered dirty and the recent file will be removed */ - if !ValidVersionFormat(line) { + if !validVersionFormat(line) { RemoveFiles(versionFile) return nil, errRead } @@ -238,12 +224,10 @@ func GetRecentVersions() ([]string, error) { return nil, nil } -// CreateRecentFile : create a recent file +// CreateRecentFile : create RECENT file func CreateRecentFile(requestedVersion string) { - - installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file - - WriteLines([]string{requestedVersion}, filepath.Join(installLocation, recentFile)) + installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file + _ = WriteLines([]string{requestedVersion}, filepath.Join(installLocation, recentFile)) } // ConvertExecutableExt : convert excutable with local OS extension @@ -259,9 +243,9 @@ func ConvertExecutableExt(fpath string) string { } } -// InstallableBinLocation : Checks if terraform is installable in the location provided by the user. +// installableBinLocation : Checks if terraform is installable in the location provided by the user. // If not, create $HOME/bin. Ask users to add $HOME/bin to $PATH and return $HOME/bin as install location -func InstallableBinLocation(userBinPath string) string { +func installableBinLocation(userBinPath string) string { homedir, errCurr := homedir.Dir() if errCurr != nil { @@ -289,7 +273,7 @@ func InstallableBinLocation(userBinPath string) string { } else { //if ~/bin directory does not exist, create ~/bin for terraform installation logger.Noticef("Unable to write to %q", userBinPath) logger.Infof("Creating bin directory at %q", filepath.Join(homedir, "bin")) - CreateDirIfNotExist(filepath.Join(homedir, "bin")) //create ~/bin + createDirIfNotExist(filepath.Join(homedir, "bin")) //create ~/bin logger.Warnf("Run `export PATH=\"$PATH:%s\"` to append bin to $PATH", filepath.Join(homedir, "bin")) return filepath.Join(homedir, "bin", "terraform") } @@ -302,3 +286,95 @@ func InstallableBinLocation(userBinPath string) string { os.Exit(1) return "" } + +// InstallLatestVersion install latest stable tf version +func InstallLatestVersion(customBinaryPath, mirrorURL string) { + tfversion, _ := getTFLatest(mirrorURL) + install(tfversion, customBinaryPath, mirrorURL) +} + +// InstallLatestImplicitVersion install latest - argument (version) must be provided +func InstallLatestImplicitVersion(requestedVersion, customBinaryPath, mirrorURL string, preRelease bool) { + _, err := version.NewConstraint(requestedVersion) + if err != nil { + logger.Errorf("Error parsing constraint %q: %v", requestedVersion, err) + } + tfversion, err := getTFLatestImplicit(mirrorURL, preRelease, requestedVersion) + if err == nil && tfversion != "" { + install(tfversion, customBinaryPath, mirrorURL) + } + logger.Errorf("Error parsing constraint %q: %v", requestedVersion, err) + PrintInvalidMinorTFVersion() +} + +// InstallVersion install with provided version as argument +func InstallVersion(arg, customBinaryPath, mirrorURL string) { + if validVersionFormat(arg) { + requestedVersion := arg + + //check to see if the requested version has been downloaded before + installLocation := getInstallLocation() + installFileVersionPath := ConvertExecutableExt(filepath.Join(installLocation, VersionPrefix+requestedVersion)) + recentDownloadFile := CheckFileExist(installFileVersionPath) + if recentDownloadFile { + ChangeSymlink(installFileVersionPath, customBinaryPath) + logger.Infof("Switched terraform to version %q", requestedVersion) + addRecent(requestedVersion) //add to recent file for faster lookup + os.Exit(0) + } + + // If the requested version had not been downloaded before + // Set list all true - all versions including beta and rc will be displayed + tflist, _ := getTFList(mirrorURL, true) // Get list of versions + exist := versionExist(requestedVersion, tflist) // Check if version exists before downloading it + + if exist { + install(requestedVersion, customBinaryPath, mirrorURL) + } else { + logger.Fatal("The provided terraform version does not exist.\n Try `tfswitch -l` to see all available versions") + os.Exit(1) + } + + } else { + PrintInvalidTFVersion() + logger.Error("Args must be a valid terraform version") + UsageMessage() + os.Exit(1) + } +} + +// InstallOption displays & installs tf version +/* listAll = true - all versions including beta and rc will be displayed */ +/* listAll = false - only official stable release are displayed */ +func InstallOption(listAll bool, customBinaryPath, mirrorURL string) { + tflist, _ := getTFList(mirrorURL, listAll) // Get list of versions + recentVersions, _ := getRecentVersions() // Get recent versions from RECENT file + tflist = append(recentVersions, tflist...) // Append recent versions to the top of the list + tflist = removeDuplicateVersions(tflist) // Remove duplicate version + + if len(tflist) == 0 { + logger.Fatalf("Terraform version list is empty: %s", mirrorURL) + os.Exit(1) + } + + /* prompt user to select version of terraform */ + prompt := promptui.Select{ + Label: "Select Terraform version", + Items: tflist, + } + + _, tfversion, errPrompt := prompt.Run() + tfversion = strings.Trim(tfversion, " *recent") //trim versions with the string " *recent" appended + + if errPrompt != nil { + if errPrompt.Error() == "^C" { + // Cancel execution + os.Exit(1) + } else { + logger.Fatalf("Prompt failed %v", errPrompt) + } + } + + install(tfversion, customBinaryPath, mirrorURL) + os.Exit(0) +} diff --git a/lib/install_test.go b/lib/install_test.go index 0ec6a95b..12fc1d98 100644 --- a/lib/install_test.go +++ b/lib/install_test.go @@ -1,28 +1,12 @@ package lib_test import ( - "os" "os/user" - "runtime" "testing" "github.com/mitchellh/go-homedir" ) -// TestAddRecent : Create a file, check filename exist, -// rename file, check new filename exit -func GetInstallLocation(installPath string) string { - return string(os.PathSeparator) + installPath + string(os.PathSeparator) -} - -func getInstallFile(installFile string) string { - if runtime.GOOS == "windows" { - return installFile + ".exe" - } - - return installFile -} - func TestInstall(t *testing.T) { t.Run("User should exist", diff --git a/lib/list_versions.go b/lib/list_versions.go index a5c8759f..af151f10 100644 --- a/lib/list_versions.go +++ b/lib/list_versions.go @@ -2,7 +2,7 @@ package lib import ( "fmt" - "io/ioutil" + "io" "net/http" "os" "reflect" @@ -14,12 +14,12 @@ type tfVersionList struct { tflist []string } -// GetTFList : Get the list of available terraform version given the hashicorp url -func GetTFList(mirrorURL string, preRelease bool) ([]string, error) { - - result, error := GetTFURLBody(mirrorURL) - if error != nil { - return nil, error +// getTFList : Get the list of available terraform version given the hashicorp url +func getTFList(mirrorURL string, preRelease bool) ([]string, error) { + logger.Debugf("Get list of terraform versions") + result, err := getTFURLBody(mirrorURL) + if err != nil { + return nil, err } var tfVersionList tfVersionList @@ -42,19 +42,16 @@ func GetTFList(mirrorURL string, preRelease bool) ([]string, error) { } if len(tfVersionList.tflist) == 0 { - logger.Infof("Cannot get version list from mirror: %s", mirrorURL) + logger.Errorf("Cannot get version list from mirror: %s", mirrorURL) } - return tfVersionList.tflist, nil - } -// GetTFLatest : Get the latest terraform version given the hashicorp url -func GetTFLatest(mirrorURL string) (string, error) { - - result, error := GetTFURLBody(mirrorURL) - if error != nil { - return "", error +// getTFLatest : Get the latest terraform version given the hashicorp url +func getTFLatest(mirrorURL string) (string, error) { + result, err := getTFURLBody(mirrorURL) + if err != nil { + return "", err } // Getting versions from body; should return match /X.X.X/ where X is a number semver := `\/?(\d+\.\d+\.\d+)\/?"` @@ -66,15 +63,14 @@ func GetTFLatest(mirrorURL string) (string, error) { return trimstr, nil } } - return "", nil } -// GetTFLatestImplicit : Get the latest implicit terraform version given the hashicorp url -func GetTFLatestImplicit(mirrorURL string, preRelease bool, version string) (string, error) { +// getTFLatestImplicit : Get the latest implicit terraform version given the hashicorp url +func getTFLatestImplicit(mirrorURL string, preRelease bool, version string) (string, error) { if preRelease == true { - //TODO: use GetTFList() instead of GetTFURLBody - versions, error := GetTFURLBody(mirrorURL) + //TODO: use getTFList() instead of getTFURLBody + versions, error := getTFURLBody(mirrorURL) if error != nil { return "", error } @@ -93,7 +89,7 @@ func GetTFLatestImplicit(mirrorURL string, preRelease bool, version string) (str } } else if preRelease == false { listAll := false - tflist, _ := GetTFList(mirrorURL, listAll) //get list of versions + tflist, _ := getTFList(mirrorURL, listAll) //get list of versions version = fmt.Sprintf("~> %v", version) semv, err := SemVerParser(&version, tflist) if err != nil { @@ -104,29 +100,27 @@ func GetTFLatestImplicit(mirrorURL string, preRelease bool, version string) (str return "", nil } -// GetTFURLBody : Get list of terraform versions from hashicorp releases -func GetTFURLBody(mirrorURL string) ([]string, error) { +// getTFURLBody : Get list of terraform versions from hashicorp releases +func getTFURLBody(mirrorURL string) ([]string, error) { hasSlash := strings.HasSuffix(mirrorURL, "/") - if !hasSlash { //if does not have slash - append slash + if !hasSlash { + //if it does not have slash - append slash mirrorURL = fmt.Sprintf("%s/", mirrorURL) } resp, errURL := http.Get(mirrorURL) if errURL != nil { logger.Fatalf("Error getting url: %v", errURL) - os.Exit(1) } defer resp.Body.Close() if resp.StatusCode != 200 { logger.Fatalf("Error retrieving contents from url: %s", mirrorURL) - os.Exit(1) } - body, errBody := ioutil.ReadAll(resp.Body) + body, errBody := io.ReadAll(resp.Body) if errBody != nil { logger.Fatalf("Error reading body: %v", errBody) - os.Exit(1) } bodyString := string(body) @@ -135,9 +129,8 @@ func GetTFURLBody(mirrorURL string) ([]string, error) { return result, nil } -// VersionExist : check if requested version exist -func VersionExist(val interface{}, array interface{}) (exists bool) { - +// versionExist : check if requested version exist +func versionExist(val interface{}, array interface{}) (exists bool) { exists = false switch reflect.TypeOf(array).Kind() { case reflect.Slice: @@ -149,13 +142,14 @@ func VersionExist(val interface{}, array interface{}) (exists bool) { return exists } } + default: + panic("unhandled default case") } - return exists } -// RemoveDuplicateVersions : remove duplicate version -func RemoveDuplicateVersions(elements []string) []string { +// removeDuplicateVersions : remove duplicate version +func removeDuplicateVersions(elements []string) []string { // Use map to record duplicates as we find them. encountered := map[string]bool{} result := []string{} @@ -175,20 +169,19 @@ func RemoveDuplicateVersions(elements []string) []string { return result } -// ValidVersionFormat : returns valid version format +// validVersionFormat : returns valid version format /* For example: 0.1.2 = valid // For example: 0.1.2-beta1 = valid // For example: 0.1.2-alpha = valid // For example: a.1.2 = invalid // For example: 0.1. 2 = invalid */ -func ValidVersionFormat(version string) bool { +func validVersionFormat(version string) bool { // Getting versions from body; should return match /X.X.X-@/ where X is a number,@ is a word character between a-z or A-Z // Follow https://semver.org/spec/v1.0.0-beta.html // Check regular expression at https://rubular.com/r/ju3PxbaSBALpJB semverRegex := regexp.MustCompile(`^(\d+\.\d+\.\d+)(-[a-zA-z]+\d*)?$`) - return semverRegex.MatchString(version) } @@ -197,10 +190,31 @@ func ValidVersionFormat(version string) bool { // For example: a.1.2 = invalid // For example: 0.1.2 = invalid */ -func ValidMinorVersionFormat(version string) bool { +func validMinorVersionFormat(version string) bool { // Getting versions from body; should return match /X.X./ where X is a number semverRegex := regexp.MustCompile(`^(\d+\.\d+)$`) return semverRegex.MatchString(version) } + +// ShowLatestVersion show install latest stable tf version +func ShowLatestVersion(mirrorURL string) { + tfversion, _ := getTFLatest(mirrorURL) + logger.Infof("%s", tfversion) +} + +// ShowLatestImplicitVersion show latest - argument (version) must be provided +func ShowLatestImplicitVersion(requestedVersion, mirrorURL string, preRelease bool) { + if validMinorVersionFormat(requestedVersion) { + tfversion, _ := getTFLatestImplicit(mirrorURL, preRelease, requestedVersion) + if len(tfversion) > 0 { + logger.Infof("%s", tfversion) + } else { + logger.Fatal("The provided terraform version does not exist.\n Try `tfswitch -l` to see all available versions") + os.Exit(1) + } + } else { + PrintInvalidMinorTFVersion() + } +} diff --git a/lib/list_versions_test.go b/lib/list_versions_test.go index 6830cdb1..d83291d6 100644 --- a/lib/list_versions_test.go +++ b/lib/list_versions_test.go @@ -12,8 +12,7 @@ const ( // TestGetTFList : Get list from hashicorp func TestGetTFList(t *testing.T) { - listAll := true - list, _ := GetTFList(hashiURL, listAll) + list, _ := getTFList(hashiURL, true) val := "0.1.0" var exists bool @@ -30,7 +29,7 @@ func TestGetTFList(t *testing.T) { } if !exists { - logger.Fatalf("Not able to find version: %s", val) + t.Errorf("Not able to find version: %s", val) } else { t.Log("Write versions exist (expected)") } @@ -40,12 +39,12 @@ func TestGetTFList(t *testing.T) { // TestRemoveDuplicateVersions : test to removed duplicate func TestRemoveDuplicateVersions(t *testing.T) { - test_array := []string{"0.0.1", "0.0.2", "0.0.3", "0.0.1", "0.12.0-beta1", "0.12.0-beta1"} + testArray := []string{"0.0.1", "0.0.2", "0.0.3", "0.0.1", "0.12.0-beta1", "0.12.0-beta1"} - list := RemoveDuplicateVersions(test_array) + list := removeDuplicateVersions(testArray) - if len(list) == len(test_array) { - logger.Fatalf("Not able to remove duplicate: %s\n", test_array) + if len(list) == len(testArray) { + t.Errorf("Not able to remove duplicate: %s\n", testArray) } else { t.Log("Write versions exist (expected)") } @@ -58,92 +57,92 @@ func TestValidVersionFormat(t *testing.T) { var version string version = "0.11.8" - valid := ValidVersionFormat(version) + valid := validVersionFormat(version) if valid == true { t.Logf("Valid version format : %s (expected)", version) } else { - logger.Fatalf("Failed to verify version format: %s\n", version) + t.Errorf("Failed to verify version format: %s\n", version) } version = "1.11.9" - valid = ValidVersionFormat(version) + valid = validVersionFormat(version) if valid == true { t.Logf("Valid version format : %s (expected)", version) } else { - logger.Fatalf("Failed to verify version format: %s\n", version) + t.Errorf("Failed to verify version format: %s\n", version) } version = "1.11.a" - valid = ValidVersionFormat(version) + valid = validVersionFormat(version) if valid == false { t.Logf("Invalid version format : %s (expected)", version) } else { - logger.Fatalf("Failed to verify version format: %s\n", version) + t.Errorf("Failed to verify version format: %s\n", version) } version = "22323" - valid = ValidVersionFormat(version) + valid = validVersionFormat(version) if valid == false { t.Logf("Invalid version format : %s (expected)", version) } else { - logger.Fatalf("Failed to verify version format: %s\n", version) + t.Errorf("Failed to verify version format: %s\n", version) } version = "@^&*!)!" - valid = ValidVersionFormat(version) + valid = validVersionFormat(version) if valid == false { t.Logf("Invalid version format : %s (expected)", version) } else { - logger.Fatalf("Failed to verify version format: %s\n", version) + t.Errorf("Failed to verify version format: %s\n", version) } version = "1.11.9-beta1" - valid = ValidVersionFormat(version) + valid = validVersionFormat(version) if valid == true { t.Logf("Valid version format : %s (expected)", version) } else { - logger.Fatalf("Failed to verify version format: %s\n", version) + t.Errorf("Failed to verify version format: %s\n", version) } version = "0.12.0-rc2" - valid = ValidVersionFormat(version) + valid = validVersionFormat(version) if valid == true { t.Logf("Valid version format : %s (expected)", version) } else { - logger.Fatalf("Failed to verify version format: %s\n", version) + t.Errorf("Failed to verify version format: %s\n", version) } version = "1.11.4-boom" - valid = ValidVersionFormat(version) + valid = validVersionFormat(version) if valid == true { t.Logf("Valid version format : %s (expected)", version) } else { - logger.Fatalf("Failed to verify version format: %s\n", version) + t.Errorf("Failed to verify version format: %s\n", version) } version = "1.11.4-1" - valid = ValidVersionFormat(version) + valid = validVersionFormat(version) if valid == false { t.Logf("Invalid version format : %s (expected)", version) } else { - logger.Fatalf("Failed to verify version format: %s\n", version) + t.Errorf("Failed to verify version format: %s\n", version) } } diff --git a/lib/logging.go b/lib/logging.go index ec38d531..cc7466a0 100644 --- a/lib/logging.go +++ b/lib/logging.go @@ -3,20 +3,47 @@ package lib import ( "github.com/gookit/slog" "github.com/gookit/slog/handler" + "os" ) -const loggingTemplate = "{{datetime}} {{level}} [{{caller}}] {{message}} {{data}} {{extra}}\n" - -var logger = InitLogger() +var ( + loggingTemplateDebug = "{{datetime}} {{level}} [{{caller}}] {{message}} {{data}} {{extra}}\n" + loggingTemplate = "{{datetime}} {{level}} {{message}} {{data}} {{extra}}\n" + logger *slog.Logger + NormalLogging = slog.Levels{slog.PanicLevel, slog.FatalLevel, slog.ErrorLevel, slog.WarnLevel, slog.InfoLevel} + NoticeLogging = slog.Levels{slog.PanicLevel, slog.FatalLevel, slog.ErrorLevel, slog.WarnLevel, slog.InfoLevel, slog.NoticeLevel} + DebugLogging = slog.Levels{slog.PanicLevel, slog.FatalLevel, slog.ErrorLevel, slog.WarnLevel, slog.InfoLevel, slog.NoticeLevel, slog.DebugLevel} + TraceLogging = slog.Levels{slog.PanicLevel, slog.FatalLevel, slog.ErrorLevel, slog.WarnLevel, slog.InfoLevel, slog.NoticeLevel, slog.DebugLevel, slog.TraceLevel} +) -func InitLogger() *slog.Logger { +func InitLogger(logLevel string) *slog.Logger { formatter := slog.NewTextFormatter() formatter.EnableColor = true formatter.ColorTheme = slog.ColorTheme formatter.TimeFormat = "15:04:05.000" - formatter.SetTemplate(loggingTemplate) - h := handler.NewConsoleHandler(slog.AllLevels) + + var h *handler.ConsoleHandler + switch logLevel { + case "TRACE": + h = handler.NewConsoleHandler(TraceLogging) + formatter.SetTemplate(loggingTemplateDebug) + break + case "DEBUG": + h = handler.NewConsoleHandler(DebugLogging) + formatter.SetTemplate(loggingTemplateDebug) + break + case "NOTICE": + h = handler.NewConsoleHandler(NoticeLogging) + formatter.SetTemplate(loggingTemplateDebug) + break + default: + h = handler.NewConsoleHandler(NormalLogging) + formatter.SetTemplate(loggingTemplate) + } + h.SetFormatter(formatter) - logger := slog.NewWithHandlers(h) - return logger + newLogger := slog.NewWithHandlers(h) + newLogger.ExitFunc = os.Exit + logger = newLogger + return newLogger } diff --git a/lib/param_parsing/environment.go b/lib/param_parsing/environment.go new file mode 100644 index 00000000..086ac12a --- /dev/null +++ b/lib/param_parsing/environment.go @@ -0,0 +1,8 @@ +package param_parsing + +import "os" + +func GetParamsFromEnvironment(params Params) Params { + params.Version = os.Getenv("TF_VERSION") + return params +} diff --git a/lib/param_parsing/environment_test.go b/lib/param_parsing/environment_test.go new file mode 100644 index 00000000..c6a45cae --- /dev/null +++ b/lib/param_parsing/environment_test.go @@ -0,0 +1,17 @@ +package param_parsing + +import ( + "os" + "testing" +) + +func TestGetParamsFromEnvironment_version_from_env(t *testing.T) { + var params Params + expected := "1.0.0_from_env" + _ = os.Setenv("TF_VERSION", expected) + params = initParams(params) + params = GetParamsFromEnvironment(params) + if params.Version != expected { + t.Error("Determined version is not matching. Got " + params.Version + ", expected " + expected) + } +} diff --git a/lib/param_parsing/parameters.go b/lib/param_parsing/parameters.go new file mode 100644 index 00000000..e211f922 --- /dev/null +++ b/lib/param_parsing/parameters.go @@ -0,0 +1,98 @@ +package param_parsing + +import ( + "github.com/gookit/slog" + "github.com/pborman/getopt" + "github.com/warrensbox/terraform-switcher/lib" +) + +type Params struct { + ChDirPath string + CustomBinaryPath string + DefaultVersion string + HelpFlag bool + LatestFlag bool + LatestPre string + LatestStable string + ListAllFlag bool + LogLevel string + MirrorURL string + ShowLatestFlag bool + ShowLatestPre string + ShowLatestStable string + Version string + VersionFlag bool +} + +var logger *slog.Logger + +func GetParameters() Params { + var params Params + params = initParams(params) + + getopt.StringVarLong(¶ms.ChDirPath, "chdir", 'c', "Switch to a different working directory before executing the given command. Ex: tfswitch --chdir terraform_project will run tfswitch in the terraform_project directory") + getopt.StringVarLong(¶ms.CustomBinaryPath, "bin", 'b', "Custom binary path. Ex: tfswitch -b "+lib.ConvertExecutableExt("/Users/username/bin/terraform")) + getopt.StringVarLong(¶ms.DefaultVersion, "default", 'd', "Default to this version in case no other versions could be detected. Ex: tfswitch --default 1.2.4") + getopt.BoolVarLong(¶ms.HelpFlag, "help", 'h', "Displays help message") + getopt.BoolVarLong(¶ms.LatestFlag, "latest", 'u', "Get latest stable version") + getopt.StringVarLong(¶ms.LatestPre, "latest-pre", 'p', "Latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest)") + getopt.StringVarLong(¶ms.LatestStable, "latest-stable", 's', "Latest implicit version based on a constraint. Ex: tfswitch --latest-stable 0.13.0 downloads 0.13.7 and 0.13 downloads 0.15.5 (latest)") + getopt.BoolVarLong(¶ms.ListAllFlag, "list-all", 'l', "List all versions of terraform - including beta and rc") + getopt.StringVarLong(¶ms.LogLevel, "log-level", 'g', "Set loglevel for tfswitch. One of (INFO, NOTICE, DEBUG, TRACE)") + getopt.StringVarLong(¶ms.MirrorURL, "mirror", 'm', "install from a remote API other than the default. Default: "+lib.DefaultMirror) + getopt.BoolVarLong(¶ms.ShowLatestFlag, "show-latest", 'U', "Show latest stable version") + getopt.StringVarLong(¶ms.ShowLatestPre, "show-latest-pre", 'P', "Show latest pre-release implicit version. Ex: tfswitch --show-latest-pre 0.13 prints 0.13.0-rc1 (latest)") + getopt.StringVarLong(¶ms.ShowLatestStable, "show-latest-stable", 'S', "Show latest implicit version. Ex: tfswitch --show-latest-stable 0.13 prints 0.13.7 (latest)") + getopt.BoolVarLong(¶ms.VersionFlag, "version", 'v', "Displays the version of tfswitch") + + // Parse the command line parameters to fetch stuff like chdir + getopt.Parse() + + logger = lib.InitLogger(params.LogLevel) + var err error + // Read configuration files + if tomlFileExists(params) { + params, err = getParamsTOML(params) + } else if tfSwitchFileExists(params) { + params, err = GetParamsFromTfSwitch(params) + } else if terraformVersionFileExists(params) { + params, err = GetParamsFromTerraformVersion(params) + } else if isTerraformModule(params) { + params, _ = GetVersionFromVersionsTF(params) + } else if terraGruntFileExists(params) { + params, err = GetVersionFromTerragrunt(params) + } else { + params = GetParamsFromEnvironment(params) + } + if err != nil { + logger.Fatalf("Error parsing configuration file: %q", err) + } + + // Parse again to overwrite anything that might by defined on the cli AND in any config file (CLI always wins) + getopt.Parse() + args := getopt.Args() + if len(args) == 1 { + /* version provided on command line as arg */ + params.Version = args[0] + } + return params +} + +func initParams(params Params) Params { + params.ChDirPath = lib.GetCurrentDirectory() + params.CustomBinaryPath = lib.ConvertExecutableExt(lib.GetDefaultBin()) + params.DefaultVersion = lib.DefaultLatest + params.HelpFlag = false + params.LatestFlag = false + params.LatestPre = lib.DefaultLatest + params.LatestStable = lib.DefaultLatest + params.ListAllFlag = false + params.LogLevel = "INFO" + params.MirrorURL = lib.DefaultMirror + params.ShowLatestFlag = false + params.ShowLatestPre = lib.DefaultLatest + params.ShowLatestStable = lib.DefaultLatest + params.Version = lib.DefaultLatest + params.VersionFlag = false + return params +} diff --git a/lib/param_parsing/parameters_test.go b/lib/param_parsing/parameters_test.go new file mode 100644 index 00000000..b36cdf06 --- /dev/null +++ b/lib/param_parsing/parameters_test.go @@ -0,0 +1,80 @@ +package param_parsing + +import ( + "github.com/pborman/getopt" + "os" + "testing" +) + +func TestGetParameters_version_from_args(t *testing.T) { + expected := "0.13args" + os.Args = []string{"cmd", expected} + params := GetParameters() + actual := params.Version + if actual != expected { + t.Error("Version Param was not parsed correctly. Actual: " + actual + ", Expected: " + expected) + } + t.Cleanup(func() { + getopt.CommandLine = getopt.New() + }) +} + +func TestGetParameters_params_are_overridden_by_toml_file(t *testing.T) { + t.Cleanup(func() { + getopt.CommandLine = getopt.New() + }) + expected := "../../test-data/integration-tests/test_tfswitchtoml" + os.Args = []string{"cmd", "--chdir=" + expected} + params := GetParameters() + actual := params.ChDirPath + if actual != expected { + t.Error("ChDir Param was not parsed correctly. Actual: " + actual + ", Expected: " + expected) + } + + expected = "/usr/local/bin/terraform_from_toml" + actual = params.CustomBinaryPath + if actual != expected { + t.Error("CustomBinaryPath Param was not as expected. Actual: " + actual + ", Expected: " + expected) + } + expected = "0.11.4" + actual = params.Version + if actual != expected { + t.Error("Version Param was not as expected. Actual: " + actual + ", Expected: " + expected) + } +} +func TestGetParameters_toml_params_are_overridden_by_cli(t *testing.T) { + t.Cleanup(func() { + getopt.CommandLine = getopt.New() + }) + expected := "../../test-data/integration-tests/test_tfswitchtoml" + os.Args = []string{"cmd", "--chdir=" + expected, "--bin=/usr/test/bin"} + params := GetParameters() + actual := params.ChDirPath + if actual != expected { + t.Error("ChDir Param was not parsed correctly. Actual: " + actual + ", Expected: " + expected) + } + + expected = "/usr/test/bin" + actual = params.CustomBinaryPath + if actual != expected { + t.Error("CustomBinaryPath Param was not as expected. Actual: " + actual + ", Expected: " + expected) + } + expected = "0.11.4" + actual = params.Version + if actual != expected { + t.Error("Version Param was not as expected. Actual: " + actual + ", Expected: " + expected) + } +} + +func TestGetParameters_check_config_precedence(t *testing.T) { + t.Cleanup(func() { + getopt.CommandLine = getopt.New() + }) + os.Args = []string{"cmd", "--chdir=../../test-data/integration-tests/test_precedence"} + parameters := prepare() + parameters = GetParameters() + expected := "0.11.3" + if parameters.Version != expected { + t.Error("Version Param was not as expected. Actual: " + parameters.Version + ", Expected: " + expected) + } +} diff --git a/lib/param_parsing/terraform_version.go b/lib/param_parsing/terraform_version.go new file mode 100644 index 00000000..eed498ef --- /dev/null +++ b/lib/param_parsing/terraform_version.go @@ -0,0 +1,29 @@ +package param_parsing + +import ( + "github.com/warrensbox/terraform-switcher/lib" + "os" + "path/filepath" + "strings" +) + +const terraformVersionFileName = ".terraform-version" + +func GetParamsFromTerraformVersion(params Params) (Params, error) { + filePath := filepath.Join(params.ChDirPath, terraformVersionFileName) + if lib.CheckFileExist(filePath) { + logger.Infof("Reading configuration from %q", filePath) + content, err := os.ReadFile(filePath) + if err != nil { + logger.Errorf("Could not read file content at %q: %v", filePath, err) + return params, err + } + params.Version = strings.TrimSpace(string(content)) + } + return params, nil +} + +func terraformVersionFileExists(params Params) bool { + filePath := filepath.Join(params.ChDirPath, terraformVersionFileName) + return lib.CheckFileExist(filePath) +} diff --git a/lib/param_parsing/terraform_version_test.go b/lib/param_parsing/terraform_version_test.go new file mode 100644 index 00000000..9e4ac4ba --- /dev/null +++ b/lib/param_parsing/terraform_version_test.go @@ -0,0 +1,24 @@ +package param_parsing + +import ( + "testing" +) + +func TestGetParamsFromTerraformVersion(t *testing.T) { + var params Params + params.ChDirPath = "../../test-data/integration-tests/test_terraform-version" + params, _ = GetParamsFromTerraformVersion(params) + expected := "0.11.0" + if params.Version != expected { + t.Errorf("Version from .terraform-version not read correctly. Got: %v, Expect: %v", params.Version, expected) + } +} + +func TestGetParamsFromTerraformVersion_no_file(t *testing.T) { + var params Params + params.ChDirPath = "../../test-data/skip-integration-tests/test_no_file" + params, _ = GetParamsFromTerraformVersion(params) + if params.Version != "" { + t.Errorf("Expected emtpy version string. Got: %v", params.Version) + } +} diff --git a/lib/param_parsing/terragrunt.go b/lib/param_parsing/terragrunt.go new file mode 100644 index 00000000..ff50c9fe --- /dev/null +++ b/lib/param_parsing/terragrunt.go @@ -0,0 +1,43 @@ +package param_parsing + +import ( + "fmt" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclparse" + "github.com/warrensbox/terraform-switcher/lib" + "path/filepath" +) + +const terraGruntFileName = "terragrunt.hcl" + +type terragruntVersionConstraints struct { + TerraformVersionConstraint string `hcl:"terraform_version_constraint"` +} + +func GetVersionFromTerragrunt(params Params) (Params, error) { + filePath := filepath.Join(params.ChDirPath, terraGruntFileName) + if lib.CheckFileExist(filePath) { + logger.Infof("Reading configuration from %q", filePath) + parser := hclparse.NewParser() + hclFile, diagnostics := parser.ParseHCLFile(filePath) + if diagnostics.HasErrors() { + return params, fmt.Errorf("unable to parse HCL file %q", filePath) + } + var versionFromTerragrunt terragruntVersionConstraints + diagnostics = gohcl.DecodeBody(hclFile.Body, nil, &versionFromTerragrunt) + if diagnostics.HasErrors() { + return params, fmt.Errorf("could not decode body of HCL file %q", filePath) + } + version, err := lib.GetSemver(versionFromTerragrunt.TerraformVersionConstraint, params.MirrorURL) + if err != nil { + return params, fmt.Errorf("no version found matching %q", versionFromTerragrunt.TerraformVersionConstraint) + } + params.Version = version + } + return params, nil +} + +func terraGruntFileExists(params Params) bool { + filePath := filepath.Join(params.ChDirPath, terraGruntFileName) + return lib.CheckFileExist(filePath) +} diff --git a/lib/param_parsing/terragrunt_test.go b/lib/param_parsing/terragrunt_test.go new file mode 100644 index 00000000..118baaf8 --- /dev/null +++ b/lib/param_parsing/terragrunt_test.go @@ -0,0 +1,48 @@ +package param_parsing + +import ( + "github.com/hashicorp/go-version" + "github.com/warrensbox/terraform-switcher/lib" + "strings" + "testing" +) + +func TestGetVersionFromTerragrunt(t *testing.T) { + var params Params + params = initParams(params) + params.ChDirPath = "../../test-data/integration-tests/test_terragrunt_hcl" + params, _ = GetVersionFromTerragrunt(params) + v1, _ := version.NewVersion("0.13") + v2, _ := version.NewVersion("0.14") + actualVersion, _ := version.NewVersion(params.Version) + if !actualVersion.GreaterThanOrEqual(v1) || !actualVersion.LessThan(v2) { + t.Error("Determined version is not between 0.13 and 0.14") + } +} + +func TestGetVersionTerragrunt_with_no_terragrunt_file(t *testing.T) { + var params Params + logger = lib.InitLogger("DEBUG") + params = initParams(params) + params.ChDirPath = "../../test-data/skip-integration-tests/test_no_file" + params, _ = GetVersionFromTerragrunt(params) + if params.Version != "" { + t.Error("Version should be empty") + } +} + +func TestGetVersionFromTerragrunt_erroneous_file(t *testing.T) { + var params Params + logger = lib.InitLogger("DEBUG") + params = initParams(params) + params.ChDirPath = "../../test-data/skip-integration-tests/test_terragrunt_error_hcl" + params, err := GetVersionFromTerragrunt(params) + if err == nil { + t.Error("Expected error but got none.") + } else { + expectedError := "could not decode body of HCL file" + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("Expected error to contain '%q', got '%q'", expectedError, err) + } + } +} diff --git a/lib/param_parsing/tfswitch.go b/lib/param_parsing/tfswitch.go new file mode 100644 index 00000000..89e78a24 --- /dev/null +++ b/lib/param_parsing/tfswitch.go @@ -0,0 +1,29 @@ +package param_parsing + +import ( + "github.com/warrensbox/terraform-switcher/lib" + "os" + "path/filepath" + "strings" +) + +const tfSwitchFileName = ".tfswitchrc" + +func GetParamsFromTfSwitch(params Params) (Params, error) { + filePath := filepath.Join(params.ChDirPath, tfSwitchFileName) + if lib.CheckFileExist(filePath) { + logger.Infof("Reading configuration from %q", filePath) + content, err := os.ReadFile(filePath) + if err != nil { + logger.Errorf("Could not read file content from %q: %v", filePath, err) + return params, err + } + params.Version = strings.TrimSpace(string(content)) + } + return params, nil +} + +func tfSwitchFileExists(params Params) bool { + filePath := filepath.Join(params.ChDirPath, tfSwitchFileName) + return lib.CheckFileExist(filePath) +} diff --git a/lib/param_parsing/tfswitch_test.go b/lib/param_parsing/tfswitch_test.go new file mode 100644 index 00000000..8d5b39df --- /dev/null +++ b/lib/param_parsing/tfswitch_test.go @@ -0,0 +1,24 @@ +package param_parsing + +import ( + "testing" +) + +func TestGetParamsFromTfSwitch(t *testing.T) { + var params Params + params.ChDirPath = "../../test-data/integration-tests/test_tfswitchrc" + params, _ = GetParamsFromTfSwitch(params) + expected := "0.10.5" + if params.Version != expected { + t.Error("Version from tfswitchrc not read correctly. Actual: " + params.Version + ", Expected: " + expected) + } +} + +func TestGetParamsFromTfSwitch_no_file(t *testing.T) { + var params Params + params.ChDirPath = "../../test-data/skip-integration-tests/test_no_file" + params, _ = GetParamsFromTfSwitch(params) + if params.Version != "" { + t.Errorf("Expected emtpy version string. Got: %v", params.Version) + } +} diff --git a/lib/param_parsing/toml.go b/lib/param_parsing/toml.go new file mode 100644 index 00000000..e1748c73 --- /dev/null +++ b/lib/param_parsing/toml.go @@ -0,0 +1,44 @@ +package param_parsing + +import ( + "github.com/spf13/viper" + "github.com/warrensbox/terraform-switcher/lib" + "path/filepath" +) + +const tfSwitchTOMLFileName = ".tfswitch.toml" + +// getParamsTOML parses everything in the toml file, return required version and bin path +func getParamsTOML(params Params) (Params, error) { + tomlPath := filepath.Join(params.ChDirPath, tfSwitchTOMLFileName) + if tomlFileExists(params) { + logger.Infof("Reading configuration from %q", tomlPath) + configfileName := lib.GetFileName(tfSwitchTOMLFileName) + viperParser := viper.New() + viperParser.SetConfigType("toml") + viperParser.SetConfigName(configfileName) + viperParser.AddConfigPath(params.ChDirPath) + + errs := viperParser.ReadInConfig() // Find and read the config file + if errs != nil { + logger.Errorf("Could not to read %q: %v", tomlPath, errs) + return params, errs + } + + if viperParser.Get("bin") != nil { + params.CustomBinaryPath = viperParser.GetString("bin") + } + if viperParser.Get("log-level") != nil { + params.LogLevel = viperParser.GetString("log-level") + } + if viperParser.Get("version") != nil { + params.Version = viperParser.GetString("version") + } + } + return params, nil +} + +func tomlFileExists(params Params) bool { + tomlPath := filepath.Join(params.ChDirPath, tfSwitchTOMLFileName) + return lib.CheckFileExist(tomlPath) +} diff --git a/lib/param_parsing/toml_test.go b/lib/param_parsing/toml_test.go new file mode 100644 index 00000000..063f757e --- /dev/null +++ b/lib/param_parsing/toml_test.go @@ -0,0 +1,59 @@ +package param_parsing + +import ( + "github.com/warrensbox/terraform-switcher/lib" + "testing" +) + +func prepare() Params { + var params Params + params.ChDirPath = "../../test-data/integration-tests/test_tfswitchtoml" + logger = lib.InitLogger("DEBUG") + return params +} + +func TestGetParamsTOML_BinaryPath(t *testing.T) { + expected := "/usr/local/bin/terraform_from_toml" + params := prepare() + params, _ = getParamsTOML(params) + if params.CustomBinaryPath != expected { + t.Errorf("BinaryPath not matching. Got %v, expected %v", params.CustomBinaryPath, expected) + } +} + +func TestGetParamsTOML_Version(t *testing.T) { + expected := "0.11.4" + params := prepare() + params, _ = getParamsTOML(params) + if params.Version != expected { + t.Errorf("Version not matching. Got %v, expected %v", params.Version, expected) + } +} + +func TestGetParamsTOML_log_level(t *testing.T) { + expected := "NOTICE" + params := prepare() + params, _ = getParamsTOML(params) + if params.LogLevel != expected { + t.Errorf("Version not matching. Got %v, expected %v", params.LogLevel, expected) + } +} + +func TestGetParamsTOML_no_file(t *testing.T) { + var params Params + params.ChDirPath = "../../test-data/skip-integration-tests/test_no_file" + params, _ = getParamsTOML(params) + if params.Version != "" { + t.Errorf("Expected emtpy version string. Got: %v", params.Version) + } +} + +func TestGetParamsTOML_error_in_file(t *testing.T) { + logger = lib.InitLogger("DEBUG") + var params Params + params.ChDirPath = "../../test-data/skip-integration-tests/test_tfswitchtoml_error" + params, err := getParamsTOML(params) + if err == nil { + t.Errorf("Expected error for reading erroneous toml file. Got nil") + } +} diff --git a/lib/param_parsing/versiontf.go b/lib/param_parsing/versiontf.go new file mode 100644 index 00000000..38852c4f --- /dev/null +++ b/lib/param_parsing/versiontf.go @@ -0,0 +1,28 @@ +package param_parsing + +import ( + "github.com/hashicorp/terraform-config-inspect/tfconfig" + "github.com/warrensbox/terraform-switcher/lib" +) + +func GetVersionFromVersionsTF(params Params) (Params, error) { + logger.Infof("Reading version from terraform module at %q", params.ChDirPath) + module, err := tfconfig.LoadModule(params.ChDirPath) + if err != nil { + logger.Errorf("Could not load terraform module at %q", params.ChDirPath) + return params, err.Err() + } + tfConstraint := module.RequiredCore[0] + version, err2 := lib.GetSemver(tfConstraint, params.MirrorURL) + if err2 != nil { + logger.Errorf("No version found matching %q", tfConstraint) + return params, err2 + } + params.Version = version + return params, nil +} + +func isTerraformModule(params Params) bool { + module, err := tfconfig.LoadModule(params.ChDirPath) + return err == nil && len(module.RequiredCore) > 0 +} diff --git a/lib/param_parsing/versiontf_test.go b/lib/param_parsing/versiontf_test.go new file mode 100644 index 00000000..245f9ec5 --- /dev/null +++ b/lib/param_parsing/versiontf_test.go @@ -0,0 +1,54 @@ +package param_parsing + +import ( + "fmt" + "github.com/hashicorp/go-version" + "github.com/warrensbox/terraform-switcher/lib" + "testing" +) + +func TestGetVersionFromVersionsTF(t *testing.T) { + logger = lib.InitLogger("DEBUG") + var params Params + params = initParams(params) + params.ChDirPath = "../../test-data/integration-tests/test_versiontf" + params, _ = GetVersionFromVersionsTF(params) + v1, _ := version.NewVersion("1.0.0") + v2, _ := version.NewVersion("2.0.0") + actualVersion, _ := version.NewVersion(params.Version) + if !actualVersion.GreaterThanOrEqual(v1) || !actualVersion.LessThan(v2) { + t.Error("Determined version is not between 1.0.0 and 2.0.0") + } +} + +func TestGetVersionFromVersionsTF_erroneous_file(t *testing.T) { + logger = lib.InitLogger("DEBUG") + var params Params + params = initParams(params) + params.ChDirPath = "../../test-data/skip-integration-tests/test_versiontf_error" + params, err := GetVersionFromVersionsTF(params) + if err == nil { + t.Error("Expected error got nil") + } else { + expected := "error parsing constraint: Malformed constraint: ~527> 1.0.0" + if fmt.Sprint(err) != expected { + t.Errorf("Expected error %q, got %q", expected, err) + } + } +} + +func TestGetVersionFromVersionsTF_non_existent_constraint(t *testing.T) { + logger = lib.InitLogger("DEBUG") + var params Params + params = initParams(params) + params.ChDirPath = "../../test-data/skip-integration-tests/test_versiontf_non_existent" + params, err := GetVersionFromVersionsTF(params) + if err == nil { + t.Error("Expected error got nil") + } else { + expected := "did not find version matching constraint: > 99999.0.0" + if fmt.Sprint(err) != expected { + t.Errorf("Expected error %q, got %q", expected, err) + } + } +} diff --git a/lib/semver.go b/lib/semver.go index 38bbb90d..fa8f138a 100644 --- a/lib/semver.go +++ b/lib/semver.go @@ -7,17 +7,16 @@ import ( semver "github.com/hashicorp/go-version" ) -// GetSemver : returns version that will be installed based on server constaint provided -func GetSemver(tfconstraint *string, mirrorURL *string) (string, error) { - +// GetSemver : returns version that will be installed based on server constraint provided +func GetSemver(tfconstraint string, mirrorURL string) (string, error) { listAll := true - tflist, _ := GetTFList(*mirrorURL, listAll) //get list of versions - logger.Infof("Reading required version from constraint: %q", *tfconstraint) - tfversion, err := SemVerParser(tfconstraint, tflist) + tflist, _ := getTFList(mirrorURL, listAll) //get list of versions + logger.Infof("Reading required version from constraint: %q", tfconstraint) + tfversion, err := SemVerParser(&tfconstraint, tflist) return tfversion, err } -// ValidateSemVer : Goes through the list of terraform version, return a valid tf version for contraint provided +// SemVerParser : Goes through the list of terraform version, return a valid tf version for contraint provided func SemVerParser(tfconstraint *string, tflist []string) (string, error) { tfversion := "" constraints, err := semver.NewConstraint(*tfconstraint) //NewConstraint returns a Constraints instance that a Version instance can be checked against @@ -39,23 +38,23 @@ func SemVerParser(tfconstraint *string, tflist []string) (string, error) { for _, element := range versions { if constraints.Check(element) { // Validate a version against a constraint tfversion = element.String() - logger.Infof("Matched version: %q", tfversion) - if ValidVersionFormat(tfversion) { //check if version format is correct + if validVersionFormat(tfversion) { //check if version format is correct + logger.Infof("Matched version: %q", tfversion) return tfversion, nil + } else { + PrintInvalidTFVersion() } } } - - PrintInvalidTFVersion() - return "", fmt.Errorf("error parsing constraint: %s", *tfconstraint) + return "", fmt.Errorf("did not find version matching constraint: %s", *tfconstraint) } -// Print invalid TF version +// PrintInvalidTFVersion Print invalid TF version func PrintInvalidTFVersion() { logger.Info("Version does not exist or invalid terraform version format.\n Format should be #.#.# or #.#.#-@# where # are numbers and @ are word characters.\n For example, 0.11.7 and 0.11.9-beta1 are valid versions") } -// Print invalid TF version +// PrintInvalidMinorTFVersion Print invalid minor TF version func PrintInvalidMinorTFVersion() { logger.Info("Invalid minor terraform version format.\n Format should be #.# where # are numbers. For example, 0.11 is valid version") } diff --git a/lib/symlink.go b/lib/symlink.go index 19e48350..91bd9052 100644 --- a/lib/symlink.go +++ b/lib/symlink.go @@ -91,8 +91,8 @@ func CheckSymlink(symlinkPath string) bool { // ChangeSymlink : move symlink to existing binary func ChangeSymlink(binVersionPath string, binPath string) { - //installLocation = GetInstallLocation() //get installation location - this is where we will put our terraform binary file - binPath = InstallableBinLocation(binPath) + //installLocation = getInstallLocation() //get installation location - this is where we will put our terraform binary file + binPath = installableBinLocation(binPath) /* remove current symlink if exist*/ symlinkExist := CheckSymlink(binPath) diff --git a/lib/symlink_test.go b/lib/symlink_test.go index baf3a55e..cf9d1123 100644 --- a/lib/symlink_test.go +++ b/lib/symlink_test.go @@ -21,7 +21,7 @@ func TestCreateSymlink(t *testing.T) { home, err := homedir.Dir() if err != nil { - logger.Fatalf("Could not detect home directory.") + t.Errorf("Could not detect home directory.") } symlinkPathSrc := filepath.Join(home, testSymlinkSrc) symlinkPathDest := filepath.Join(home, testSymlinkDest) @@ -29,7 +29,7 @@ func TestCreateSymlink(t *testing.T) { // Create file for test as windows does not like no source create, err := os.Create(symlinkPathDest) if err != nil { - logger.Fatalf("Could not create test dest file for symlink at %v", symlinkPathDest) + t.Errorf("Could not create test dest file for symlink at %v", symlinkPathDest) } defer create.Close() @@ -40,7 +40,7 @@ func TestCreateSymlink(t *testing.T) { t.Logf("Symlink does not exist %v [expected]", ln) } else { t.Logf("Symlink exist %v [expected]", ln) - os.Remove(symlinkPathSrc) + _ = os.Remove(symlinkPathSrc) t.Logf("Removed existing symlink for testing purposes") } } @@ -65,8 +65,8 @@ func TestCreateSymlink(t *testing.T) { } } - os.Remove(symlinkPathSrc) - os.Remove(symlinkPathDest) + _ = os.Remove(symlinkPathSrc) + _ = os.Remove(symlinkPathDest) } // TestRemoveSymlink : check if symlink exist-create if does not exist, @@ -79,7 +79,7 @@ func TestRemoveSymlink(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } symlinkPathSrc := filepath.Join(homedir, testSymlinkSrc) symlinkPathDest := filepath.Join(homedir, testSymlinkDest) @@ -114,7 +114,7 @@ func TestCheckSymlink(t *testing.T) { homedir, errCurr := homedir.Dir() if errCurr != nil { - logger.Fatal(errCurr) + t.Error(errCurr) } symlinkPathSrc := filepath.Join(homedir, testSymlinkSrc) symlinkPathDest := filepath.Join(homedir, testSymlinkDest) @@ -136,5 +136,5 @@ func TestCheckSymlink(t *testing.T) { t.Logf("Symlink does not exist %v [unexpected]", ln) } - os.Remove(symlinkPathSrc) + _ = os.Remove(symlinkPathSrc) } diff --git a/lib/utils.go b/lib/utils.go index 0bcddda9..f024430f 100644 --- a/lib/utils.go +++ b/lib/utils.go @@ -1,11 +1,13 @@ package lib import ( + "fmt" + "github.com/pborman/getopt" "os" ) -// FileExists checks if a file exists and is not a directory before we try using it to prevent further errors -func FileExists(filename string) bool { +// FileExistsAndIsNotDir checks if a file exists and is not a directory before we try using it to prevent further errors +func FileExistsAndIsNotDir(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false @@ -19,3 +21,9 @@ func closeFileHandlers(handlers []*os.File) { _ = handler.Close() } } + +func UsageMessage() { + fmt.Print("\n\n") + getopt.PrintUsage(os.Stderr) + fmt.Println("Supply the terraform version as an argument, or choose from a menu") +} diff --git a/main.go b/main.go index c9d75edf..3d69004c 100644 --- a/main.go +++ b/main.go @@ -19,469 +19,56 @@ package main import ( "fmt" - "os" - "path/filepath" - "strings" - - semver "github.com/hashicorp/go-version" - "github.com/hashicorp/hcl2/gohcl" - "github.com/hashicorp/hcl2/hclparse" - "github.com/hashicorp/terraform-config-inspect/tfconfig" - "github.com/mitchellh/go-homedir" - - "github.com/manifoldco/promptui" - "github.com/pborman/getopt" - "github.com/spf13/viper" - lib "github.com/warrensbox/terraform-switcher/lib" + "github.com/warrensbox/terraform-switcher/lib/param_parsing" + "os" ) -const ( - defaultMirror = "https://releases.hashicorp.com/terraform" - defaultLatest = "" - tfvFilename = ".terraform-version" - rcFilename = ".tfswitchrc" - tomlFilename = ".tfswitch.toml" - tgHclFilename = "terragrunt.hcl" - versionPrefix = "terraform_" -) - -var logger = lib.InitLogger() +var parameters = param_parsing.GetParameters() +var logger = lib.InitLogger(parameters.LogLevel) var version string func main() { - dir := lib.GetCurrentDirectory() - custBinPath := getopt.StringLong("bin", 'b', lib.ConvertExecutableExt(lib.GetDefaultBin()), "Custom binary path. Ex: tfswitch -b "+lib.ConvertExecutableExt("/Users/username/bin/terraform")) - listAllFlag := getopt.BoolLong("list-all", 'l', "List all versions of terraform - including beta and rc") - latestPre := getopt.StringLong("latest-pre", 'p', defaultLatest, "Latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest)") - showLatestPre := getopt.StringLong("show-latest-pre", 'P', defaultLatest, "Show latest pre-release implicit version. Ex: tfswitch --show-latest-pre 0.13 prints 0.13.0-rc1 (latest)") - latestStable := getopt.StringLong("latest-stable", 's', defaultLatest, "Latest implicit version based on a constraint. Ex: tfswitch --latest-stable 0.13.0 downloads 0.13.7 and 0.13 downloads 0.15.5 (latest)") - showLatestStable := getopt.StringLong("show-latest-stable", 'S', defaultLatest, "Show latest implicit version. Ex: tfswitch --show-latest-stable 0.13 prints 0.13.7 (latest)") - latestFlag := getopt.BoolLong("latest", 'u', "Get latest stable version") - showLatestFlag := getopt.BoolLong("show-latest", 'U', "Show latest stable version") - mirrorURL := getopt.StringLong("mirror", 'm', defaultMirror, "Install from a remote API other than the default. Default: "+defaultMirror) - chDirPath := getopt.StringLong("chdir", 'c', dir, "Switch to a different working directory before executing the given command. Ex: tfswitch --chdir terraform_project will run tfswitch in the terraform_project directory") - versionFlag := getopt.BoolLong("version", 'v', "Displays the version of tfswitch") - defaultVersion := getopt.StringLong("default", 'd', defaultLatest, "Default to this version in case no other versions could be detected. Ex: tfswitch --default 1.2.4") - - getopt.StringVarLong(&lib.PubKeyId, "public-key-id", 'k', "The ID of the public key to check the checksums against") - getopt.StringVarLong(&lib.PubKeyPrefix, "public-key-prefix", 'x', "The prefix of the public key. i.e. \"hashicorp_\"") - getopt.StringVarLong(&lib.PubKeyUri, "public-key-uri", 'y', "The URI to download the public key from") - - helpFlag := getopt.BoolLong("help", 'h', "Displays help message") - _ = versionFlag - - getopt.Parse() - args := getopt.Args() - - homedir, err := homedir.Dir() - if err != nil { - logger.Fatalf("Unable to get home directory: %v", err) - os.Exit(1) - } - TFVersionFile := filepath.Join(*chDirPath, tfvFilename) //settings for .terraform-version file in current directory (tfenv compatible) - RCFile := filepath.Join(*chDirPath, rcFilename) //settings for .tfswitchrc file in current directory (backward compatible purpose) - TOMLConfigFile := filepath.Join(*chDirPath, tomlFilename) //settings for .tfswitch.toml file in current directory (option to specify bin directory) - HomeTOMLConfigFile := filepath.Join(homedir, tomlFilename) //settings for .tfswitch.toml file in home directory (option to specify bin directory) - TGHACLFile := filepath.Join(*chDirPath, tgHclFilename) //settings for terragrunt.hcl file in current directory (option to specify bin directory) switch { - case *versionFlag: - //if *versionFlag { + case parameters.VersionFlag: if version != "" { fmt.Printf("Version: %s\n", version) } else { fmt.Println("Version not defined during build.") } - case *helpFlag: - //} else if *helpFlag { - usageMessage() - /* Checks if the .tfswitch.toml file exist in home or current directory - * This block checks to see if the tfswitch toml file is provided in the current path. - * If the .tfswitch.toml file exist, it has a higher precedence than the .tfswitchrc file - * You can specify the custom binary path and the version you desire - * If you provide a custom binary path with the -b option, this will override the bin value in the toml file - * If you provide a version on the command line, this will override the version value in the toml file - */ - case lib.FileExists(TOMLConfigFile) || lib.FileExists(HomeTOMLConfigFile): - version := "" - binPath := *custBinPath - if lib.FileExists(TOMLConfigFile) { //read from toml from current directory - version, binPath = getParamsTOML(binPath, *chDirPath) - } else { // else read from toml from home directory - version, binPath = getParamsTOML(binPath, homedir) - } - - switch { - /* GIVEN A TOML FILE, */ + os.Exit(0) + case parameters.HelpFlag: + lib.UsageMessage() + os.Exit(0) + case parameters.ListAllFlag: /* show all terraform version including betas and RCs*/ - case *listAllFlag: - listAll := true //set list all true - all versions including beta and rc will be displayed - installOption(listAll, &binPath, mirrorURL) + lib.InstallOption(true, parameters.CustomBinaryPath, parameters.MirrorURL) + case parameters.LatestPre != "": /* latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest) */ - case *latestPre != "": - preRelease := true - installLatestImplicitVersion(*latestPre, &binPath, mirrorURL, preRelease) - /* latest implicit version. Ex: tfswitch --latest 0.13 downloads 0.13.5 (latest) */ - case *latestStable != "": - preRelease := false - installLatestImplicitVersion(*latestStable, &binPath, mirrorURL, preRelease) + lib.InstallLatestImplicitVersion(parameters.LatestPre, parameters.CustomBinaryPath, parameters.MirrorURL, true) + case parameters.ShowLatestPre != "": + /* show latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest) */ + lib.ShowLatestImplicitVersion(parameters.ShowLatestPre, parameters.MirrorURL, true) + case parameters.LatestStable != "": + /* latest implicit version. Ex: tfswitch --latest-stable 0.13 downloads 0.13.5 (latest) */ + lib.InstallLatestImplicitVersion(parameters.LatestStable, parameters.CustomBinaryPath, parameters.MirrorURL, false) + case parameters.ShowLatestStable != "": + /* show latest implicit stable version. Ex: tfswitch --show-latest-stable 0.13 downloads 0.13.5 (latest) */ + lib.ShowLatestImplicitVersion(parameters.ShowLatestStable, parameters.MirrorURL, false) + case parameters.LatestFlag: /* latest stable version */ - case *latestFlag: - installLatestVersion(&binPath, mirrorURL) - /* version provided on command line as arg */ - case len(args) == 1: - installVersion(args[0], &binPath, mirrorURL) - /* provide an tfswitchrc file (IN ADDITION TO A TOML FILE) */ - case lib.FileExists(RCFile) && len(args) == 0: - readingFileMsg(rcFilename) - tfversion := retrieveFileContents(RCFile) - installVersion(tfversion, &binPath, mirrorURL) - /* if .terraform-version file found (IN ADDITION TO A TOML FILE) */ - case lib.FileExists(TFVersionFile) && len(args) == 0: - readingFileMsg(tfvFilename) - tfversion := retrieveFileContents(TFVersionFile) - installVersion(tfversion, &binPath, mirrorURL) - /* if versions.tf file found (IN ADDITION TO A TOML FILE) */ - case checkTFModuleFileExist(*chDirPath) && len(args) == 0: - installTFProvidedModule(*chDirPath, &binPath, mirrorURL) - /* if Terraform Version environment variable is set */ - case checkTFEnvExist() && len(args) == 0 && version == "": - tfversion := os.Getenv("TF_VERSION") - logger.Infof("\"TF_VERSION\" environment variable value: %q", tfversion) - installVersion(tfversion, &binPath, mirrorURL) - /* if terragrunt.hcl file found (IN ADDITION TO A TOML FILE) */ - case lib.FileExists(TGHACLFile) && checkVersionDefinedHCL(&TGHACLFile) && len(args) == 0: - installTGHclFile(&TGHACLFile, &binPath, mirrorURL) - // if no arg is provided - but toml file is provided - case version != "": - installVersion(version, &binPath, mirrorURL) - default: - listAll := false //set list all false - only official release will be displayed - installOption(listAll, &binPath, mirrorURL) - } - - /* show all terraform version including betas and RCs*/ - case *listAllFlag: - installWithListAll(custBinPath, mirrorURL) - - /* latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest) */ - case *latestPre != "": - preRelease := true - installLatestImplicitVersion(*latestPre, custBinPath, mirrorURL, preRelease) - - /* show latest pre-release implicit version. Ex: tfswitch --latest-pre 0.13 downloads 0.13.0-rc1 (latest) */ - case *showLatestPre != "": - preRelease := true - showLatestImplicitVersion(*showLatestPre, custBinPath, mirrorURL, preRelease) - - /* latest implicit version. Ex: tfswitch --latest 0.13 downloads 0.13.5 (latest) */ - case *latestStable != "": - preRelease := false - installLatestImplicitVersion(*latestStable, custBinPath, mirrorURL, preRelease) - - /* show latest implicit stable version. Ex: tfswitch --latest 0.13 downloads 0.13.5 (latest) */ - case *showLatestStable != "": - preRelease := false - showLatestImplicitVersion(*showLatestStable, custBinPath, mirrorURL, preRelease) - - /* latest stable version */ - case *latestFlag: - installLatestVersion(custBinPath, mirrorURL) - - /* show latest stable version */ - case *showLatestFlag: - showLatestVersion(custBinPath, mirrorURL) - - /* version provided on command line as arg */ - case len(args) == 1: - installVersion(args[0], custBinPath, mirrorURL) - - /* provide an tfswitchrc file */ - case lib.FileExists(RCFile) && len(args) == 0: - readingFileMsg(rcFilename) - tfversion := retrieveFileContents(RCFile) - installVersion(tfversion, custBinPath, mirrorURL) - - /* if .terraform-version file found */ - case lib.FileExists(TFVersionFile) && len(args) == 0: - readingFileMsg(tfvFilename) - tfversion := retrieveFileContents(TFVersionFile) - installVersion(tfversion, custBinPath, mirrorURL) - - /* if versions.tf file found */ - case checkTFModuleFileExist(*chDirPath) && len(args) == 0: - installTFProvidedModule(*chDirPath, custBinPath, mirrorURL) - - /* if terragrunt.hcl file found */ - case lib.FileExists(TGHACLFile) && checkVersionDefinedHCL(&TGHACLFile) && len(args) == 0: - installTGHclFile(&TGHACLFile, custBinPath, mirrorURL) - - /* if Terraform Version environment variable is set */ - case checkTFEnvExist() && len(args) == 0: - tfversion := os.Getenv("TF_VERSION") - logger.Infof("\"TF_VERSION\" environment variable value: %q", tfversion) - installVersion(tfversion, custBinPath, mirrorURL) - - /* if default version is provided - Pick this instead of going for prompt */ - case *defaultVersion != "": - installVersion(*defaultVersion, custBinPath, mirrorURL) - - // if no arg is provided + lib.InstallLatestVersion(parameters.CustomBinaryPath, parameters.MirrorURL) + case parameters.ShowLatestFlag: + /* show latest stable version */ + lib.ShowLatestVersion(parameters.MirrorURL) + case parameters.Version != "": + lib.InstallVersion(parameters.Version, parameters.CustomBinaryPath, parameters.MirrorURL) + case parameters.DefaultVersion != "": + /* if default version is provided - Pick this instead of going for prompt */ + lib.InstallVersion(parameters.DefaultVersion, parameters.CustomBinaryPath, parameters.MirrorURL) default: - listAll := false //set list all false - only official release will be displayed - installOption(listAll, custBinPath, mirrorURL) - } -} - -/* Helper functions */ - -// install with all possible versions, including beta and rc -func installWithListAll(custBinPath, mirrorURL *string) { - listAll := true //set list all true - all versions including beta and rc will be displayed - installOption(listAll, custBinPath, mirrorURL) -} - -// install latest stable tf version -func installLatestVersion(custBinPath, mirrorURL *string) { - tfversion, _ := lib.GetTFLatest(*mirrorURL) - lib.Install(tfversion, *custBinPath, *mirrorURL) -} - -// show install latest stable tf version -func showLatestVersion(custBinPath, mirrorURL *string) { - tfversion, _ := lib.GetTFLatest(*mirrorURL) - logger.Infof("%s", tfversion) -} - -// install latest - argument (version) must be provided -func installLatestImplicitVersion(requestedVersion string, custBinPath, mirrorURL *string, preRelease bool) { - _, err := semver.NewConstraint(requestedVersion) - if err != nil { - logger.Errorf("Error parsing constraint %q: %v", requestedVersion, err) - } - //if lib.ValidMinorVersionFormat(requestedVersion) { - tfversion, err := lib.GetTFLatestImplicit(*mirrorURL, preRelease, requestedVersion) - if err == nil && tfversion != "" { - lib.Install(tfversion, *custBinPath, *mirrorURL) - } - logger.Errorf("Error parsing constraint %q: %v", requestedVersion, err) - lib.PrintInvalidMinorTFVersion() -} - -// show latest - argument (version) must be provided -func showLatestImplicitVersion(requestedVersion string, custBinPath, mirrorURL *string, preRelease bool) { - if lib.ValidMinorVersionFormat(requestedVersion) { - tfversion, _ := lib.GetTFLatestImplicit(*mirrorURL, preRelease, requestedVersion) - if len(tfversion) > 0 { - logger.Infof("%s", tfversion) - } else { - logger.Fatal("The provided terraform version does not exist.\n Try `tfswitch -l` to see all available versions") - os.Exit(1) - } - } else { - lib.PrintInvalidMinorTFVersion() - } -} - -// install with provided version as argument -func installVersion(arg string, custBinPath *string, mirrorURL *string) { - if lib.ValidVersionFormat(arg) { - requestedVersion := arg - - //check to see if the requested version has been downloaded before - installLocation := lib.GetInstallLocation() - installFileVersionPath := lib.ConvertExecutableExt(filepath.Join(installLocation, versionPrefix+requestedVersion)) - recentDownloadFile := lib.CheckFileExist(installFileVersionPath) - if recentDownloadFile { - lib.ChangeSymlink(installFileVersionPath, *custBinPath) - logger.Infof("Switched terraform to version %q", requestedVersion) - lib.AddRecent(requestedVersion) //add to recent file for faster lookup - os.Exit(0) - } - - //if the requested version had not been downloaded before - listAll := true //set list all true - all versions including beta and rc will be displayed - tflist, _ := lib.GetTFList(*mirrorURL, listAll) //get list of versions - exist := lib.VersionExist(requestedVersion, tflist) //check if version exist before downloading it - - if exist { - lib.Install(requestedVersion, *custBinPath, *mirrorURL) - } else { - logger.Fatal("The provided terraform version does not exist.\n Try `tfswitch -l` to see all available versions") - os.Exit(1) - } - - } else { - lib.PrintInvalidTFVersion() - logger.Error("Args must be a valid terraform version") - usageMessage() - os.Exit(1) - } -} - -// retrive file content of regular file -func retrieveFileContents(file string) string { - fileContents, err := os.ReadFile(file) - if err != nil { - logger.Fatalf("Failed reading %q file: %v\n Follow the README.md instructions for setup: https://github.com/warrensbox/terraform-switcher/blob/master/README.md", tfvFilename, err) - os.Exit(1) - } - tfversion := strings.TrimSuffix(string(fileContents), "\n") - return tfversion -} - -// Print message reading file content of : -func readingFileMsg(filename string) { - logger.Infof("Reading file %q", filename) -} - -// fileExists checks if a file exists and is not a directory before we -// try using it to prevent further errors. -func checkTFModuleFileExist(dir string) bool { - - module, _ := tfconfig.LoadModule(dir) - if len(module.RequiredCore) >= 1 { - return true - } - return false -} - -// checkTFEnvExist - checks if the TF_VERSION environment variable is set -func checkTFEnvExist() bool { - tfversion := os.Getenv("TF_VERSION") - if tfversion != "" { - return true - } - return false -} - -/* parses everything in the toml file, return required version and bin path */ -func getParamsTOML(binPath string, dir string) (string, string) { - path, err := homedir.Dir() - - if err != nil { - logger.Fatalf("Unable to get home directory: %v", err) - os.Exit(1) - } - - if dir == path { - path = "home directory" - } else { - path = "current directory" - } - logger.Infof("Reading %q configuration from %q", tomlFilename, path) // Takes the default bin (defaultBin) if user does not specify bin path - configfileName := lib.GetFileName(tomlFilename) //get the config file - viper.SetConfigType("toml") - viper.SetConfigName(configfileName) - viper.AddConfigPath(dir) - - errs := viper.ReadInConfig() // Find and read the config file - if errs != nil { - logger.Fatalf("Failed to read %q: %v", tomlFilename, errs) - os.Exit(1) - } - - bin := viper.Get("bin") // read custom binary location - if binPath == lib.ConvertExecutableExt(lib.GetDefaultBin()) && bin != nil { // if the bin path is the same as the default binary path and if the custom binary is provided in the toml file (use it) - binPath = os.ExpandEnv(bin.(string)) - } - //logger.Debug(binPath) // Uncomment this to debug - version := viper.Get("version") //attempt to get the version if it's provided in the toml - if version == nil { - version = "" - } - - return version.(string), binPath -} - -func usageMessage() { - fmt.Print("\n\n") - getopt.PrintUsage(os.Stderr) - fmt.Println("Supply the terraform version as an argument, or choose from a menu") -} - -/* installOption : displays & installs tf version */ -/* listAll = true - all versions including beta and rc will be displayed */ -/* listAll = false - only official stable release are displayed */ -func installOption(listAll bool, custBinPath, mirrorURL *string) { - tflist, _ := lib.GetTFList(*mirrorURL, listAll) //get list of versions - recentVersions, _ := lib.GetRecentVersions() //get recent versions from RECENT file - tflist = append(recentVersions, tflist...) //append recent versions to the top of the list - tflist = lib.RemoveDuplicateVersions(tflist) //remove duplicate version - - if len(tflist) == 0 { - logger.Fatalf("Terraform version list is empty: %s", *mirrorURL) - os.Exit(1) - } - - /* prompt user to select version of terraform */ - prompt := promptui.Select{ - Label: "Select Terraform version", - Items: tflist, - } - - _, tfversion, errPrompt := prompt.Run() - tfversion = strings.Trim(tfversion, " *recent") //trim versions with the string " *recent" appended - - if errPrompt != nil { - logger.Fatalf("Prompt failed %v", errPrompt) - os.Exit(1) - } - - lib.Install(tfversion, *custBinPath, *mirrorURL) - os.Exit(0) -} - -// install when tf file is provided -func installTFProvidedModule(dir string, custBinPath, mirrorURL *string) { - logger.Infof("Reading required version from terraform module at %q", dir) - module, _ := tfconfig.LoadModule(dir) - tfconstraint := module.RequiredCore[0] //we skip duplicated definitions and use only first one - installFromConstraint(&tfconstraint, custBinPath, mirrorURL) -} - -// install using a version constraint -func installFromConstraint(tfconstraint *string, custBinPath, mirrorURL *string) { - - tfversion, err := lib.GetSemver(tfconstraint, mirrorURL) - if err == nil { - lib.Install(tfversion, *custBinPath, *mirrorURL) - } - logger.Fatalf("No version found to match constraint: %v.\n Follow the README.md instructions for setup: https://github.com/warrensbox/terraform-switcher/blob/master/README.md", err) - os.Exit(1) -} - -// Install using version constraint from terragrunt file -func installTGHclFile(tgFile *string, custBinPath, mirrorURL *string) { - logger.Infof("Terragrunt file found: %q", *tgFile) - parser := hclparse.NewParser() - file, diags := parser.ParseHCLFile(*tgFile) //use hcl parser to parse HCL file - if diags.HasErrors() { - logger.Fatalf("Unable to parse %q file", *tgFile) - os.Exit(1) - } - var version terragruntVersionConstraints - gohcl.DecodeBody(file.Body, nil, &version) - installFromConstraint(&version.TerraformVersionConstraint, custBinPath, mirrorURL) -} - -type terragruntVersionConstraints struct { - TerraformVersionConstraint string `hcl:"terraform_version_constraint"` -} - -// check if version is defined in hcl file /* lazy-emergency fix - will improve later */ -func checkVersionDefinedHCL(tgFile *string) bool { - parser := hclparse.NewParser() - file, diags := parser.ParseHCLFile(*tgFile) //use hcl parser to parse HCL file - if diags.HasErrors() { - logger.Fatalf("Unable to parse %q file", *tgFile) - os.Exit(1) - } - var version terragruntVersionConstraints - gohcl.DecodeBody(file.Body, nil, &version) - if version == (terragruntVersionConstraints{}) { - return false + // Set list all false - only official release will be displayed + lib.InstallOption(false, parameters.CustomBinaryPath, parameters.MirrorURL) } - return true } diff --git a/main_test.go b/main_test.go deleted file mode 100644 index e6be58fb..00000000 --- a/main_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package main_test - -import ( - "os/user" - "testing" - - "github.com/mitchellh/go-homedir" -) - -// TestMain : check to see if user exist -func TestMain(t *testing.T) { - - t.Run("User should exist", - func(t *testing.T) { - _, errCurr := user.Current() - if errCurr != nil { - t.Errorf("Unable to get user %v [unexpected]", errCurr) - } - _, errCurr = homedir.Dir() - if errCurr != nil { - t.Errorf("Unable to get user home directory: %v [unexpected]", errCurr) - } - }, - ) -} diff --git a/test-data/test_terraform-version/.terraform-version b/test-data/integration-tests/test_precedence/.terraform-version similarity index 100% rename from test-data/test_terraform-version/.terraform-version rename to test-data/integration-tests/test_precedence/.terraform-version diff --git a/test-data/integration-tests/test_precedence/.tfswitch.toml b/test-data/integration-tests/test_precedence/.tfswitch.toml new file mode 100644 index 00000000..b5ee7fe5 --- /dev/null +++ b/test-data/integration-tests/test_precedence/.tfswitch.toml @@ -0,0 +1,2 @@ +bin = "/usr/local/bin/terraform_from_toml" +version = "0.11.3" diff --git a/test-data/test_tfswitchrc/.tfswitchrc b/test-data/integration-tests/test_precedence/.tfswitchrc similarity index 100% rename from test-data/test_tfswitchrc/.tfswitchrc rename to test-data/integration-tests/test_precedence/.tfswitchrc diff --git a/test-data/integration-tests/test_precedence/terragrunt.hcl b/test-data/integration-tests/test_precedence/terragrunt.hcl new file mode 100644 index 00000000..9237b428 --- /dev/null +++ b/test-data/integration-tests/test_precedence/terragrunt.hcl @@ -0,0 +1 @@ +terraform_version_constraint = ">= 0.13, < 0.14" diff --git a/test-data/integration-tests/test_terraform-version/.terraform-version b/test-data/integration-tests/test_terraform-version/.terraform-version new file mode 100644 index 00000000..d9df1bbc --- /dev/null +++ b/test-data/integration-tests/test_terraform-version/.terraform-version @@ -0,0 +1 @@ +0.11.0 diff --git a/test-data/integration-tests/test_terragrunt_hcl/terragrunt.hcl b/test-data/integration-tests/test_terragrunt_hcl/terragrunt.hcl new file mode 100644 index 00000000..9237b428 --- /dev/null +++ b/test-data/integration-tests/test_terragrunt_hcl/terragrunt.hcl @@ -0,0 +1 @@ +terraform_version_constraint = ">= 0.13, < 0.14" diff --git a/test-data/integration-tests/test_tfswitchrc/.tfswitchrc b/test-data/integration-tests/test_tfswitchrc/.tfswitchrc new file mode 100644 index 00000000..9028ec63 --- /dev/null +++ b/test-data/integration-tests/test_tfswitchrc/.tfswitchrc @@ -0,0 +1 @@ +0.10.5 diff --git a/test-data/integration-tests/test_tfswitchtoml/.tfswitch.toml b/test-data/integration-tests/test_tfswitchtoml/.tfswitch.toml new file mode 100644 index 00000000..1089c0b4 --- /dev/null +++ b/test-data/integration-tests/test_tfswitchtoml/.tfswitch.toml @@ -0,0 +1,3 @@ +bin = "/usr/local/bin/terraform_from_toml" +version = "0.11.4" +log-level = "NOTICE" diff --git a/test-data/test_versiontf/version.tf b/test-data/integration-tests/test_versiontf/version.tf similarity index 100% rename from test-data/test_versiontf/version.tf rename to test-data/integration-tests/test_versiontf/version.tf diff --git a/test-data/skip-integration-tests/test_no_file/dummy_file b/test-data/skip-integration-tests/test_no_file/dummy_file new file mode 100644 index 00000000..e69de29b diff --git a/test-data/skip-integration-tests/test_terragrunt_error_hcl/terragrunt.hcl b/test-data/skip-integration-tests/test_terragrunt_error_hcl/terragrunt.hcl new file mode 100644 index 00000000..ab8ebe77 --- /dev/null +++ b/test-data/skip-integration-tests/test_terragrunt_error_hcl/terragrunt.hcl @@ -0,0 +1 @@ +terraform_vers_agrtz_ion_constraint = ">= 0.13, < 0.14" diff --git a/test-data/skip-integration-tests/test_tfswitchtoml_error/.tfswitch.toml b/test-data/skip-integration-tests/test_tfswitchtoml_error/.tfswitch.toml new file mode 100644 index 00000000..1ad6ffa2 --- /dev/null +++ b/test-data/skip-integration-tests/test_tfswitchtoml_error/.tfswitch.toml @@ -0,0 +1,3 @@ +bin sdf= "/usr/local/bin/terraform_from_toml" +version =sdfh "0.11.4" +log-level =w35 "NOTICE" diff --git a/test-data/skip-integration-tests/test_versiontf_error/version.tf b/test-data/skip-integration-tests/test_versiontf_error/version.tf new file mode 100644 index 00000000..8c37904d --- /dev/null +++ b/test-data/skip-integration-tests/test_versiontf_error/version.tf @@ -0,0 +1,8 @@ +terraform { + required_version = "~527> 1.0.0" + + required_providers { + aws = ">325= 2.52.0" + kubernetes = ">32= 1.11.1" + } +} diff --git a/test-data/skip-integration-tests/test_versiontf_non_existent/version.tf b/test-data/skip-integration-tests/test_versiontf_non_existent/version.tf new file mode 100644 index 00000000..8b8612f8 --- /dev/null +++ b/test-data/skip-integration-tests/test_versiontf_non_existent/version.tf @@ -0,0 +1,8 @@ +terraform { + required_version = "> 99999.0.0" + + required_providers { + aws = ">= 2.52.0" + kubernetes = ">= 1.11.1" + } +} diff --git a/test-data/test_terragrunt_hcl/terragrunt.hcl b/test-data/test_terragrunt_hcl/terragrunt.hcl deleted file mode 100644 index 806b304f..00000000 --- a/test-data/test_terragrunt_hcl/terragrunt.hcl +++ /dev/null @@ -1,16 +0,0 @@ -include { - path = "${find_in_parent_folders()}" -} - -terraform { - source = "..." - - extra_arguments "variables" { - commands = get_terraform_commands_that_need_vars() - } -} - inputs = merge( - jsondecode(file("${find_in_parent_folders("general.tfvars")}")) -) - -terraform_version_constraint=">= 0.13, < 0.14" \ No newline at end of file diff --git a/test-data/test_tfswitchtoml/.tfswitch.toml b/test-data/test_tfswitchtoml/.tfswitch.toml deleted file mode 100644 index f234e9cc..00000000 --- a/test-data/test_tfswitchtoml/.tfswitch.toml +++ /dev/null @@ -1,2 +0,0 @@ -bin = "/usr/local/bin/terraform" -version = "0.11.3"