Remove tfexec, allow TF_ environment vars and log them (#2264)

* Remove tfexec, allow TF_ environment vars and log them

Signed-off-by: Spike Curtis <spike@coder.com>

* fixup: commented code, long lines

Signed-off-by: Spike Curtis <spike@coder.com>

* rename executor methods to remove get

Signed-off-by: Spike Curtis <spike@coder.com>

* don't log terraform environment variables we don't know are safe

Signed-off-by: Spike Curtis <spike@coder.com>

* Disable linting of fake secret

Signed-off-by: Spike Curtis <spike@coder.com>

* drop parse support and move logger into terraform package

Signed-off-by: Spike Curtis <spike@coder.com>

* disable testpackage linter on internal package test

Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
Spike Curtis
2022-06-16 10:50:39 -07:00
committed by GitHub
parent 82c4b80c67
commit 552dad6919
8 changed files with 691 additions and 374 deletions

13
go.mod
View File

@ -5,9 +5,6 @@ go 1.18
// Required until https://github.com/manifoldco/promptui/pull/169 is merged.
replace github.com/manifoldco/promptui => github.com/kylecarbs/promptui v0.8.1-0.20201231190244-d8f2159af2b2
// Required until https://github.com/hashicorp/terraform-exec/pull/275 and https://github.com/hashicorp/terraform-exec/pull/276 are merged.
replace github.com/hashicorp/terraform-exec => github.com/kylecarbs/terraform-exec v0.15.1-0.20220202050609-a1ce7181b180
// Required until https://github.com/hashicorp/terraform-config-inspect/pull/74 is merged.
replace github.com/hashicorp/terraform-config-inspect => github.com/kylecarbs/terraform-config-inspect v0.0.0-20211215004401-bbc517866b88
@ -77,7 +74,6 @@ require (
github.com/hashicorp/hc-install v0.3.2
github.com/hashicorp/hcl/v2 v2.12.0
github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f
github.com/hashicorp/terraform-exec v0.15.0
github.com/hashicorp/terraform-json v0.14.0
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87
github.com/jedib0t/go-pretty/v6 v6.3.2
@ -136,13 +132,19 @@ require (
require github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
require (
github.com/agnivade/levenshtein v1.0.1 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/vektah/gqlparser/v2 v2.4.4 // indirect
github.com/yuin/goldmark v1.4.12 // indirect
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/agnivade/levenshtein v1.0.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
@ -237,7 +239,6 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/yashtewari/glob-intersection v0.1.0 // indirect
github.com/yuin/goldmark v1.4.12 // indirect
github.com/zclconf/go-cty v1.10.0 // indirect
github.com/zeebo/errs v1.2.2 // indirect
go.opencensus.io v0.23.0 // indirect

20
go.sum
View File

@ -136,7 +136,6 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@ -144,7 +143,6 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
@ -167,7 +165,6 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
@ -523,7 +520,6 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -596,13 +592,10 @@ github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -904,14 +897,12 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E=
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hc-install v0.3.1/go.mod h1:3LCdWcCDS1gaHC9mhHCGbkYfoY6vdsKohGjugbZdZak=
github.com/hashicorp/hc-install v0.3.2 h1:oiQdJZvXmkNcRcEOOfM5n+VTsvNjWQeOjfAoO6dKSH8=
github.com/hashicorp/hc-install v0.3.2/go.mod h1:xMG6Tr8Fw1WFjlxH0A9v61cW15pFwgEGqEz0V4jisHs=
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
@ -924,7 +915,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk=
github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s=
github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
@ -991,7 +981,6 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.3.2 h1:+46BKrPFAyhAn3MTT3vzvZc+qvWAX23yviAlBG9zAxA=
github.com/jedib0t/go-pretty/v6 v6.3.2/go.mod h1:B1WBBWnJhW9jnk7GHxY+p9NlmNwf/KUb4hKsRk6BdBQ=
@ -1036,7 +1025,6 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
@ -1084,8 +1072,6 @@ github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e h1:OP0ZMFeZkU
github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
github.com/kylecarbs/terraform-config-inspect v0.0.0-20211215004401-bbc517866b88 h1:tvG/qs5c4worwGyGnbbb4i/dYYLjpFwDMqcIT3awAf8=
github.com/kylecarbs/terraform-config-inspect v0.0.0-20211215004401-bbc517866b88/go.mod h1:Z0Nnk4+3Cy89smEbrq+sl1bxc9198gIP4I7wcQF6Kqs=
github.com/kylecarbs/terraform-exec v0.15.1-0.20220202050609-a1ce7181b180 h1:yafC0pmxjs18fnO5RdKFLSItJIjYwGfSHTfcUvlZb3E=
github.com/kylecarbs/terraform-exec v0.15.1-0.20220202050609-a1ce7181b180/go.mod h1:lRENyXw1BL5V0FCCE8lsW3XoVLRLnxM54jrlYSyXpvM=
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=
@ -1167,7 +1153,6 @@ github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
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-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
@ -1466,7 +1451,6 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -1526,6 +1510,7 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag
github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@ -1586,7 +1571,6 @@ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgq
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
@ -1622,7 +1606,6 @@ github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0=
github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
@ -2419,7 +2402,6 @@ gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -0,0 +1,391 @@
package terraform
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"golang.org/x/xerrors"
"github.com/hashicorp/go-version"
tfjson "github.com/hashicorp/terraform-json"
"github.com/coder/coder/provisionersdk/proto"
)
type executor struct {
binaryPath string
cachePath string
workdir string
}
func (e executor) basicEnv() []string {
// Required for "terraform init" to find "git" to
// clone Terraform modules.
env := os.Environ()
// Only Linux reliably works with the Terraform plugin
// cache directory. It's unknown why this is.
if e.cachePath != "" && runtime.GOOS == "linux" {
env = append(env, "TF_PLUGIN_CACHE_DIR="+e.cachePath)
}
return env
}
func (e executor) execWriteOutput(ctx context.Context, args, env []string, stdOutWriter, stdErrWriter io.WriteCloser) (err error) {
defer func() {
closeErr := stdOutWriter.Close()
if err == nil && closeErr != nil {
err = closeErr
}
closeErr = stdErrWriter.Close()
if err == nil && closeErr != nil {
err = closeErr
}
}()
// #nosec
cmd := exec.CommandContext(ctx, e.binaryPath, args...)
cmd.Dir = e.workdir
cmd.Stdout = stdOutWriter
cmd.Stderr = stdErrWriter
cmd.Env = env
return cmd.Run()
}
func (e executor) execParseJSON(ctx context.Context, args, env []string, v interface{}) error {
// #nosec
cmd := exec.CommandContext(ctx, e.binaryPath, args...)
cmd.Dir = e.workdir
cmd.Env = env
out := &bytes.Buffer{}
stdErr := &bytes.Buffer{}
cmd.Stdout = out
cmd.Stderr = stdErr
err := cmd.Run()
if err != nil {
errString, _ := io.ReadAll(stdErr)
return xerrors.Errorf("%s: %w", errString, err)
}
dec := json.NewDecoder(out)
dec.UseNumber()
err = dec.Decode(v)
if err != nil {
return xerrors.Errorf("decode terraform json: %w", err)
}
return nil
}
func (e executor) checkMinVersion(ctx context.Context) error {
v, err := e.version(ctx)
if err != nil {
return err
}
if !v.GreaterThanOrEqual(minimumTerraformVersion) {
return xerrors.Errorf(
"terraform version %q is too old. required >= %q",
v.String(),
minimumTerraformVersion.String())
}
return nil
}
func (e executor) version(ctx context.Context) (*version.Version, error) {
// #nosec
cmd := exec.CommandContext(ctx, e.binaryPath, "version", "-json")
out, err := cmd.Output()
if err != nil {
return nil, err
}
vj := tfjson.VersionOutput{}
err = json.Unmarshal(out, &vj)
if err != nil {
return nil, err
}
return version.NewVersion(vj.Version)
}
func (e executor) init(ctx context.Context, logr logger) error {
outWriter, doneOut := logWriter(logr, proto.LogLevel_DEBUG)
errWriter, doneErr := logWriter(logr, proto.LogLevel_ERROR)
defer func() {
<-doneOut
<-doneErr
}()
return e.execWriteOutput(ctx, []string{"init"}, e.basicEnv(), outWriter, errWriter)
}
// revive:disable-next-line:flag-parameter
func (e executor) plan(ctx context.Context, env, vars []string, logr logger, destroy bool) (*proto.Provision_Response, error) {
planfilePath := filepath.Join(e.workdir, "terraform.tfplan")
args := []string{
"plan",
"-no-color",
"-input=false",
"-json",
"-refresh=true",
"-out=" + planfilePath,
}
if destroy {
args = append(args, "-destroy")
}
for _, variable := range vars {
args = append(args, "-var", variable)
}
outWriter, doneOut := provisionLogWriter(logr)
errWriter, doneErr := logWriter(logr, proto.LogLevel_ERROR)
defer func() {
<-doneOut
<-doneErr
}()
err := e.execWriteOutput(ctx, args, env, outWriter, errWriter)
if err != nil {
return nil, xerrors.Errorf("terraform plan: %w", err)
}
resources, err := e.planResources(ctx, planfilePath)
if err != nil {
return nil, err
}
return &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: resources,
},
},
}, nil
}
func (e executor) planResources(ctx context.Context, planfilePath string) ([]*proto.Resource, error) {
plan, err := e.showPlan(ctx, planfilePath)
if err != nil {
return nil, xerrors.Errorf("show terraform plan file: %w", err)
}
rawGraph, err := e.graph(ctx)
if err != nil {
return nil, xerrors.Errorf("graph: %w", err)
}
return ConvertResources(plan.PlannedValues.RootModule, rawGraph)
}
func (e executor) showPlan(ctx context.Context, planfilePath string) (*tfjson.Plan, error) {
args := []string{"show", "-json", "-no-color", planfilePath}
p := new(tfjson.Plan)
err := e.execParseJSON(ctx, args, e.basicEnv(), p)
return p, err
}
func (e executor) graph(ctx context.Context) (string, error) {
// #nosec
cmd := exec.CommandContext(ctx, e.binaryPath, "graph")
cmd.Dir = e.workdir
cmd.Env = e.basicEnv()
out, err := cmd.Output()
if err != nil {
return "", xerrors.Errorf("graph: %w", err)
}
return string(out), nil
}
// revive:disable-next-line:flag-parameter
func (e executor) apply(ctx context.Context, env, vars []string, logr logger, destroy bool,
) (*proto.Provision_Response, error) {
args := []string{
"apply",
"-no-color",
"-auto-approve",
"-input=false",
"-json",
"-refresh=true",
}
if destroy {
args = append(args, "-destroy")
}
for _, variable := range vars {
args = append(args, "-var", variable)
}
outWriter, doneOut := provisionLogWriter(logr)
errWriter, doneErr := logWriter(logr, proto.LogLevel_ERROR)
defer func() {
<-doneOut
<-doneErr
}()
err := e.execWriteOutput(ctx, args, env, outWriter, errWriter)
if err != nil {
return nil, xerrors.Errorf("terraform apply: %w", err)
}
resources, err := e.stateResources(ctx)
if err != nil {
return nil, err
}
statefilePath := filepath.Join(e.workdir, "terraform.tfstate")
stateContent, err := os.ReadFile(statefilePath)
if err != nil {
return nil, xerrors.Errorf("read statefile %q: %w", statefilePath, err)
}
return &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: resources,
State: stateContent,
},
},
}, nil
}
func (e executor) stateResources(ctx context.Context) ([]*proto.Resource, error) {
state, err := e.state(ctx)
if err != nil {
return nil, err
}
rawGraph, err := e.graph(ctx)
if err != nil {
return nil, xerrors.Errorf("get terraform graph: %w", err)
}
var resources []*proto.Resource
if state.Values != nil {
resources, err = ConvertResources(state.Values.RootModule, rawGraph)
if err != nil {
return nil, err
}
}
return resources, nil
}
func (e executor) state(ctx context.Context) (*tfjson.State, error) {
args := []string{"show", "-json"}
state := &tfjson.State{}
err := e.execParseJSON(ctx, args, e.basicEnv(), state)
if err != nil {
return nil, xerrors.Errorf("terraform show state: %w", err)
}
return state, nil
}
type logger interface {
Log(*proto.Log) error
}
type streamLogger struct {
stream proto.DRPCProvisioner_ProvisionStream
}
func (s streamLogger) Log(l *proto.Log) error {
return s.stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Log{
Log: l,
},
})
}
// logWriter creates a WriteCloser that will log each line of text at the given level. The WriteCloser must be closed
// by the caller to end logging, after which the returned channel will be closed to indicate that logging of the written
// data has finished. Failure to close the WriteCloser will leak a goroutine.
func logWriter(logr logger, level proto.LogLevel) (io.WriteCloser, <-chan any) {
r, w := io.Pipe()
done := make(chan any)
go readAndLog(logr, r, done, level)
return w, done
}
func readAndLog(logr logger, r io.Reader, done chan<- any, level proto.LogLevel) {
defer close(done)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
err := logr.Log(&proto.Log{Level: level, Output: scanner.Text()})
if err != nil {
// Not much we can do. We can't log because logging is itself breaking!
return
}
}
}
// provisionLogWriter creates a WriteCloser that will log each JSON formatted terraform log. The WriteCloser must be
// closed by the caller to end logging, after which the returned channel will be closed to indicate that logging of the
// written data has finished. Failure to close the WriteCloser will leak a goroutine.
func provisionLogWriter(logr logger) (io.WriteCloser, <-chan any) {
r, w := io.Pipe()
done := make(chan any)
go provisionReadAndLog(logr, r, done)
return w, done
}
func provisionReadAndLog(logr logger, reader io.Reader, done chan<- any) {
defer close(done)
decoder := json.NewDecoder(reader)
for {
var log terraformProvisionLog
err := decoder.Decode(&log)
if err != nil {
return
}
logLevel := convertTerraformLogLevel(log.Level, logr)
err = logr.Log(&proto.Log{Level: logLevel, Output: log.Message})
if err != nil {
// Not much we can do. We can't log because logging is itself breaking!
return
}
if log.Diagnostic == nil {
continue
}
// If the diagnostic is provided, let's provide a bit more info!
logLevel = convertTerraformLogLevel(log.Diagnostic.Severity, logr)
if err != nil {
continue
}
err = logr.Log(&proto.Log{Level: logLevel, Output: log.Diagnostic.Detail})
if err != nil {
// Not much we can do. We can't log because logging is itself breaking!
return
}
}
}
func convertTerraformLogLevel(logLevel string, logr logger) proto.LogLevel {
switch strings.ToLower(logLevel) {
case "trace":
return proto.LogLevel_TRACE
case "debug":
return proto.LogLevel_DEBUG
case "info":
return proto.LogLevel_INFO
case "warn":
return proto.LogLevel_WARN
case "error":
return proto.LogLevel_ERROR
default:
_ = logr.Log(&proto.Log{
Level: proto.LogLevel_WARN,
Output: fmt.Sprintf("unable to convert log level %s", logLevel),
})
return proto.LogLevel_INFO
}
}
type terraformProvisionLog struct {
Level string `json:"@level"`
Message string `json:"@message"`
Diagnostic *terraformProvisionLogDiagnostic `json:"diagnostic"`
}
type terraformProvisionLogDiagnostic struct {
Severity string `json:"severity"`
Summary string `json:"summary"`
Detail string `json:"detail"`
}

View File

@ -0,0 +1,63 @@
// nolint:testpackage
package terraform
import (
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/provisionersdk/proto"
)
type mockLogger struct {
logs []*proto.Log
retVal error
}
func (m *mockLogger) Log(l *proto.Log) error {
m.logs = append(m.logs, l)
return m.retVal
}
func TestLogWriter_Mainline(t *testing.T) {
t.Parallel()
logr := &mockLogger{retVal: nil}
writer, doneLogging := logWriter(logr, proto.LogLevel_INFO)
_, err := writer.Write([]byte(`Sitting in an English garden
Waiting for the sun
If the sun don't come you get a tan
From standing in the English rain`))
require.NoError(t, err)
err = writer.Close()
require.NoError(t, err)
<-doneLogging
expected := []*proto.Log{
{Level: proto.LogLevel_INFO, Output: "Sitting in an English garden"},
{Level: proto.LogLevel_INFO, Output: "Waiting for the sun"},
{Level: proto.LogLevel_INFO, Output: "If the sun don't come you get a tan"},
{Level: proto.LogLevel_INFO, Output: "From standing in the English rain"},
}
require.Equal(t, logr.logs, expected)
}
func TestLogWriter_SendError(t *testing.T) {
t.Parallel()
logr := &mockLogger{retVal: xerrors.New("Goo goo g'joob")}
writer, doneLogging := logWriter(logr, proto.LogLevel_INFO)
_, err := writer.Write([]byte(`Sitting in an English garden
Waiting for the sun
If the sun don't come you get a tan
From standing in the English rain`))
require.NoError(t, err)
err = writer.Close()
require.NoError(t, err)
<-doneLogging
expected := []*proto.Log{{Level: proto.LogLevel_INFO, Output: "Sitting in an English garden"}}
require.Equal(t, logr.logs, expected)
}

View File

@ -15,7 +15,7 @@ import (
)
// Parse extracts Terraform variables from source-code.
func (*terraform) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_ParseStream) error {
func (*server) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_ParseStream) error {
// Load the module and print any parse errors.
module, diags := tfconfig.LoadModule(request.Directory)
if diags.HasErrors() {

View File

@ -1,33 +1,21 @@
package terraform
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/hashicorp/terraform-exec/tfexec"
tfjson "github.com/hashicorp/terraform-json"
"golang.org/x/xerrors"
"github.com/coder/coder/provisionersdk"
"github.com/coder/coder/provisionersdk/proto"
)
var (
// noStateRegex is matched against the output from `terraform state show`
noStateRegex = regexp.MustCompile(`no state`)
)
// Provision executes `terraform apply`.
func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
// Provision executes `terraform apply` or `terraform plan` for dry runs.
func (t *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
logr := streamLogger{stream: stream}
shutdown, shutdownFunc := context.WithCancel(stream.Context())
defer shutdownFunc()
@ -58,36 +46,15 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro
}()
start := request.GetStart()
terraform, err := tfexec.NewTerraform(start.Directory, t.binaryPath)
if err != nil {
return xerrors.Errorf("create new terraform executor: %w", err)
}
version, _, err := terraform.Version(shutdown, false)
if err != nil {
return xerrors.Errorf("get terraform version: %w", err)
e := t.executor(start.Directory)
if err := e.checkMinVersion(stream.Context()); err != nil {
return err
}
if !version.GreaterThanOrEqual(minimumTerraformVersion) {
return xerrors.Errorf("terraform version %q is too old. required >= %q", version.String(), minimumTerraformVersion.String())
}
terraformEnv := map[string]string{}
// Required for "terraform init" to find "git" to
// clone Terraform modules.
for _, env := range os.Environ() {
parts := strings.SplitN(env, "=", 2)
if len(parts) < 2 {
continue
}
terraformEnv[parts[0]] = parts[1]
}
// Only Linux reliably works with the Terraform plugin
// cache directory. It's unknown why this is.
if t.cachePath != "" && runtime.GOOS == "linux" {
terraformEnv["TF_PLUGIN_CACHE_DIR"] = t.cachePath
}
err = terraform.SetEnv(terraformEnv)
if err != nil {
return xerrors.Errorf("set terraform env: %w", err)
if err := logTerraformEnvVars(logr); err != nil {
return err
}
statefilePath := filepath.Join(start.Directory, "terraform.tfstate")
@ -98,179 +65,51 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro
}
}
reader, writer := io.Pipe()
go func(reader *io.PipeReader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
_ = stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_ERROR,
Output: scanner.Text(),
},
},
})
}
}(reader)
terraform.SetStderr(writer)
err = terraform.Init(shutdown)
_ = reader.Close()
_ = writer.Close()
if err != nil {
return xerrors.Errorf("initialize terraform: %w", err)
}
terraform.SetStderr(io.Discard)
env := os.Environ()
env = append(env,
"CODER_AGENT_URL="+start.Metadata.CoderUrl,
"CODER_WORKSPACE_TRANSITION="+strings.ToLower(start.Metadata.WorkspaceTransition.String()),
"CODER_WORKSPACE_NAME="+start.Metadata.WorkspaceName,
"CODER_WORKSPACE_OWNER="+start.Metadata.WorkspaceOwner,
"CODER_WORKSPACE_ID="+start.Metadata.WorkspaceId,
"CODER_WORKSPACE_OWNER_ID="+start.Metadata.WorkspaceOwnerId,
)
for key, value := range provisionersdk.AgentScriptEnv() {
env = append(env, key+"="+value)
}
vars := []string{}
for _, param := range start.ParameterValues {
switch param.DestinationScheme {
case proto.ParameterDestination_ENVIRONMENT_VARIABLE:
env = append(env, fmt.Sprintf("%s=%s", param.Name, param.Value))
case proto.ParameterDestination_PROVISIONER_VARIABLE:
vars = append(vars, fmt.Sprintf("%s=%s", param.Name, param.Value))
default:
return xerrors.Errorf("unsupported parameter type %q for %q", param.DestinationScheme, param.Name)
}
}
closeChan := make(chan struct{})
reader, writer = io.Pipe()
defer reader.Close()
defer writer.Close()
go func() {
defer close(closeChan)
decoder := json.NewDecoder(reader)
for {
var log terraformProvisionLog
err := decoder.Decode(&log)
if err != nil {
return
}
logLevel, err := convertTerraformLogLevel(log.Level)
if err != nil {
// Not a big deal, but we should handle this at some point!
continue
}
_ = stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: logLevel,
Output: log.Message,
},
},
})
if log.Diagnostic == nil {
continue
}
// If the diagnostic is provided, let's provide a bit more info!
logLevel, err = convertTerraformLogLevel(log.Diagnostic.Severity)
if err != nil {
continue
}
_ = stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: logLevel,
Output: log.Diagnostic.Detail,
},
},
})
}
}()
// If we're destroying, exit early if there's no state. This is necessary to
// avoid any cases where a workspace is "locked out" of terraform due to
// e.g. bad template param values and cannot be deleted. This is just for
// contingency, in the future we will try harder to prevent workspaces being
// broken this hard.
if start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY {
_, err := pullTerraformState(shutdown, terraform, statefilePath)
if xerrors.Is(err, os.ErrNotExist) {
_ = stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "The terraform state does not exist, there is nothing to do",
},
if start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY && len(start.State) == 0 {
_ = stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "The terraform state does not exist, there is nothing to do",
},
})
},
})
return stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
})
}
if err != nil {
err = xerrors.Errorf("get terraform state: %w", err)
_ = stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Error: err.Error(),
},
},
})
return err
}
return stream.Send(&proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
})
}
planfilePath := filepath.Join(start.Directory, "terraform.tfplan")
var args []string
t.logger.Debug(shutdown, "running initialization")
err = e.init(stream.Context(), logr)
if err != nil {
return xerrors.Errorf("initialize terraform: %w", err)
}
t.logger.Debug(shutdown, "ran initialization")
env, err := provisionEnv(start)
if err != nil {
return err
}
vars, err := provisionVars(start)
if err != nil {
return err
}
var resp *proto.Provision_Response
if start.DryRun {
args = []string{
"plan",
"-no-color",
"-input=false",
"-json",
"-refresh=true",
"-out=" + planfilePath,
}
resp, err = e.plan(shutdown, env, vars, logr,
start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY)
} else {
args = []string{
"apply",
"-no-color",
"-auto-approve",
"-input=false",
"-json",
"-refresh=true",
}
resp, err = e.apply(shutdown, env, vars, logr,
start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY)
}
if start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY {
args = append(args, "-destroy")
}
for _, variable := range vars {
args = append(args, "-var", variable)
}
// #nosec
cmd := exec.CommandContext(stream.Context(), t.binaryPath, args...)
go func() {
select {
case <-stream.Context().Done():
return
case <-shutdown.Done():
_ = cmd.Process.Signal(os.Interrupt)
}
}()
cmd.Stdout = writer
cmd.Env = env
cmd.Dir = terraform.WorkingDir()
err = cmd.Run()
if err != nil {
if start.DryRun {
if shutdown.Err() != nil {
@ -297,141 +136,87 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro
},
})
}
_ = reader.Close()
<-closeChan
var resp *proto.Provision_Response
if start.DryRun {
resp, err = parseTerraformPlan(stream.Context(), terraform, planfilePath)
} else {
resp, err = parseTerraformApply(stream.Context(), terraform, statefilePath)
}
if err != nil {
return err
}
return stream.Send(resp)
}
func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfilePath string) (*proto.Provision_Response, error) {
plan, err := terraform.ShowPlanFile(ctx, planfilePath)
if err != nil {
return nil, xerrors.Errorf("show terraform plan file: %w", err)
}
rawGraph, err := terraform.Graph(ctx)
if err != nil {
return nil, xerrors.Errorf("graph: %w", err)
}
resources, err := ConvertResources(plan.PlannedValues.RootModule, rawGraph)
if err != nil {
return nil, err
}
return &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: resources,
},
},
}, nil
}
func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, statefilePath string) (*proto.Provision_Response, error) {
_, err := os.Stat(statefilePath)
statefileExisted := err == nil
state, err := pullTerraformState(ctx, terraform, statefilePath)
if err != nil {
return nil, xerrors.Errorf("get terraform state: %w", err)
}
rawGraph, err := terraform.Graph(ctx)
if err != nil {
return nil, xerrors.Errorf("get terraform graph: %w", err)
}
var resources []*proto.Resource
if state.Values != nil {
resources, err = ConvertResources(state.Values.RootModule, rawGraph)
if err != nil {
return nil, err
func provisionVars(start *proto.Provision_Start) ([]string, error) {
vars := []string{}
for _, param := range start.ParameterValues {
switch param.DestinationScheme {
case proto.ParameterDestination_ENVIRONMENT_VARIABLE:
continue
case proto.ParameterDestination_PROVISIONER_VARIABLE:
vars = append(vars, fmt.Sprintf("%s=%s", param.Name, param.Value))
default:
return nil, xerrors.Errorf("unsupported parameter type %q for %q", param.DestinationScheme, param.Name)
}
}
return vars, nil
}
var stateContent []byte
// We only want to restore state if it's not hosted remotely.
if statefileExisted {
stateContent, err = os.ReadFile(statefilePath)
if err != nil {
return nil, xerrors.Errorf("read statefile %q: %w", statefilePath, err)
func provisionEnv(start *proto.Provision_Start) ([]string, error) {
env := os.Environ()
env = append(env,
"CODER_AGENT_URL="+start.Metadata.CoderUrl,
"CODER_WORKSPACE_TRANSITION="+strings.ToLower(start.Metadata.WorkspaceTransition.String()),
"CODER_WORKSPACE_NAME="+start.Metadata.WorkspaceName,
"CODER_WORKSPACE_OWNER="+start.Metadata.WorkspaceOwner,
"CODER_WORKSPACE_ID="+start.Metadata.WorkspaceId,
"CODER_WORKSPACE_OWNER_ID="+start.Metadata.WorkspaceOwnerId,
)
for key, value := range provisionersdk.AgentScriptEnv() {
env = append(env, key+"="+value)
}
for _, param := range start.ParameterValues {
switch param.DestinationScheme {
case proto.ParameterDestination_ENVIRONMENT_VARIABLE:
env = append(env, fmt.Sprintf("%s=%s", param.Name, param.Value))
case proto.ParameterDestination_PROVISIONER_VARIABLE:
continue
default:
return nil, xerrors.Errorf("unsupported parameter type %q for %q", param.DestinationScheme, param.Name)
}
}
return &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
State: stateContent,
Resources: resources,
},
},
}, nil
return env, nil
}
// pullTerraformState pulls and merges any remote terraform state into the given
// path and reads the merged state. If there is no state, `os.ErrNotExist` will
// be returned.
func pullTerraformState(ctx context.Context, terraform *tfexec.Terraform, statefilePath string) (*tfjson.State, error) {
statefile, err := os.OpenFile(statefilePath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, xerrors.Errorf("open statefile %q: %w", statefilePath, err)
var (
// tfEnvSafeToPrint is the set of terraform environment variables that we are quite sure won't contain secrets,
// and therefore it's ok to log their values
tfEnvSafeToPrint = map[string]bool{
"TF_LOG": true,
"TF_LOG_PATH": true,
"TF_INPUT": true,
"TF_DATA_DIR": true,
"TF_WORKSPACE": true,
"TF_IN_AUTOMATION": true,
"TF_REGISTRY_DISCOVERY_RETRY": true,
"TF_REGISTRY_CLIENT_TIMEOUT": true,
"TF_CLI_CONFIG_FILE": true,
"TF_IGNORE": true,
}
defer statefile.Close()
)
// #nosec
cmd := exec.CommandContext(ctx, terraform.ExecPath(), "state", "pull")
cmd.Dir = terraform.WorkingDir()
cmd.Stdout = statefile
err = cmd.Run()
if err != nil {
return nil, xerrors.Errorf("pull terraform state: %w", err)
}
state, err := terraform.ShowStateFile(ctx, statefilePath)
if err != nil {
if noStateRegex.MatchString(err.Error()) {
return nil, os.ErrNotExist
func logTerraformEnvVars(logr logger) error {
env := os.Environ()
for _, e := range env {
if strings.HasPrefix(e, "TF_") {
parts := strings.SplitN(e, "=", 2)
if len(parts) != 2 {
panic("os.Environ() returned vars not in key=value form")
}
if !tfEnvSafeToPrint[parts[0]] {
parts[1] = "<value redacted>"
}
err := logr.Log(&proto.Log{
Level: proto.LogLevel_WARN,
Output: fmt.Sprintf("terraform environment variable: %s=%s", parts[0], parts[1]),
})
if err != nil {
return err
}
}
return nil, xerrors.Errorf("show terraform state: %w", err)
}
return state, nil
}
type terraformProvisionLog struct {
Level string `json:"@level"`
Message string `json:"@message"`
Diagnostic *terraformProvisionLogDiagnostic `json:"diagnostic"`
}
type terraformProvisionLogDiagnostic struct {
Severity string `json:"severity"`
Summary string `json:"summary"`
Detail string `json:"detail"`
}
func convertTerraformLogLevel(logLevel string) (proto.LogLevel, error) {
switch strings.ToLower(logLevel) {
case "trace":
return proto.LogLevel_TRACE, nil
case "debug":
return proto.LogLevel_DEBUG, nil
case "info":
return proto.LogLevel_INFO, nil
case "warn":
return proto.LogLevel_WARN, nil
case "error":
return proto.LogLevel_ERROR, nil
default:
return proto.LogLevel(0), xerrors.Errorf("invalid log level %q", logLevel)
}
return nil
}

View File

@ -1,4 +1,4 @@
//go:build linux
//go:build linux || darwin
package terraform_test
@ -22,24 +22,7 @@ import (
"github.com/coder/coder/provisionersdk/proto"
)
func TestProvision(t *testing.T) {
t.Parallel()
provider := `
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.4.2"
}
}
}
provider "coder" {
}
`
t.Log(provider)
func setupProvisioner(t *testing.T) (context.Context, proto.DRPCProvisionerClient) {
client, server := provisionersdk.TransportPipe()
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(func() {
@ -57,6 +40,13 @@ provider "coder" {
assert.NoError(t, err)
}()
api := proto.NewDRPCProvisionerClient(provisionersdk.Conn(client))
return ctx, api
}
func TestProvision(t *testing.T) {
t.Parallel()
ctx, api := setupProvisioner(t)
testCases := []struct {
Name string
@ -69,6 +59,7 @@ provider "coder" {
ErrorContains string
// If ExpectLogContains is not empty, then the logs should contain it.
ExpectLogContains string
DryRun bool
}{
{
Name: "single-variable",
@ -103,10 +94,38 @@ provider "coder" {
Response: &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Error: "exit status 1",
Error: "terraform apply: exit status 1",
},
},
},
ExpectLogContains: "No value for required variable",
},
{
Name: "missing-variable-dry-run",
Files: map[string]string{
"main.tf": `variable "A" {
}`,
},
ErrorContains: "terraform plan:",
ExpectLogContains: "No value for required variable",
DryRun: true,
},
{
Name: "single-resource-dry-run",
Files: map[string]string{
"main.tf": `resource "null_resource" "A" {}`,
},
Response: &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "A",
Type: "null_resource",
}},
},
},
},
DryRun: true,
},
{
Name: "single-resource",
@ -129,7 +148,7 @@ provider "coder" {
Files: map[string]string{
"main.tf": `a`,
},
ErrorContains: "configuration is invalid",
ErrorContains: "initialize terraform",
ExpectLogContains: "Argument or block definition required",
},
{
@ -137,7 +156,7 @@ provider "coder" {
Files: map[string]string{
"main.tf": `;asdf;`,
},
ErrorContains: "configuration is invalid",
ErrorContains: "initialize terraform",
ExpectLogContains: `The ";" character is not valid.`,
},
{
@ -157,6 +176,26 @@ provider "coder" {
},
ExpectLogContains: "nothing to do",
},
{
Name: "unsupported-parameter-scheme",
Files: map[string]string{
"main.tf": "",
},
Request: &proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
ParameterValues: []*proto.ParameterValue{
{
DestinationScheme: 88,
Name: "UNSUPPORTED",
Value: "sadface",
},
},
},
},
},
ErrorContains: "unsupported parameter type",
},
}
for _, testCase := range testCases {
@ -174,6 +213,7 @@ provider "coder" {
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Directory: directory,
DryRun: testCase.DryRun,
},
},
}
@ -250,3 +290,50 @@ provider "coder" {
})
}
}
// nolint:paralleltest
func TestProvision_ExtraEnv(t *testing.T) {
// #nosec
secretValue := "oinae3uinxase"
t.Setenv("TF_LOG", "INFO")
t.Setenv("TF_SUPERSECRET", secretValue)
ctx, api := setupProvisioner(t)
directory := t.TempDir()
path := filepath.Join(directory, "main.tf")
err := os.WriteFile(path, []byte(`resource "null_resource" "A" {}`), 0600)
require.NoError(t, err)
request := &proto.Provision_Request{
Type: &proto.Provision_Request_Start{
Start: &proto.Provision_Start{
Directory: directory,
Metadata: &proto.Provision_Metadata{
WorkspaceTransition: proto.WorkspaceTransition_START,
},
},
},
}
response, err := api.Provision(ctx)
require.NoError(t, err)
err = response.Send(request)
require.NoError(t, err)
found := false
for {
msg, err := response.Recv()
require.NoError(t, err)
if log := msg.GetLog(); log != nil {
if strings.Contains(log.Output, "TF_LOG") {
found = true
}
require.NotContains(t, log.Output, secretValue)
}
if c := msg.GetComplete(); c != nil {
require.Empty(t, c.Error)
break
}
}
require.True(t, found)
}

View File

@ -70,15 +70,23 @@ func Serve(ctx context.Context, options *ServeOptions) error {
options.BinaryPath = absoluteBinary
}
}
return provisionersdk.Serve(ctx, &terraform{
return provisionersdk.Serve(ctx, &server{
binaryPath: options.BinaryPath,
cachePath: options.CachePath,
logger: options.Logger,
}, options.ServeOptions)
}
type terraform struct {
type server struct {
binaryPath string
cachePath string
logger slog.Logger
}
func (t server) executor(workdir string) executor {
return executor{
binaryPath: t.binaryPath,
cachePath: t.cachePath,
workdir: workdir,
}
}