From e99a91b7708bf35f196bdaded5a1618150ab568c Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Tue, 31 Dec 2024 13:24:53 -0500 Subject: [PATCH 01/60] chore: migrate from `xanzy/go-gitlab` to `gitlab-org/api/client-go` (#5210) Signed-off-by: Rui Chen --- e2e/gitlab.go | 2 +- e2e/go.mod | 10 ++-- e2e/go.sum | 46 +++++++++++++------ go.mod | 7 +-- go.sum | 14 +++--- .../controllers/events/events_controller.go | 2 +- .../events/events_controller_test.go | 2 +- .../events/gitlab_request_parser_validator.go | 2 +- .../gitlab_request_parser_validator_test.go | 2 +- server/events/command_runner.go | 2 +- server/events/event_parser.go | 2 +- server/events/event_parser_test.go | 2 +- server/events/mocks/mock_event_parsing.go | 2 +- .../mocks/mock_gitlab_merge_request_getter.go | 2 +- server/events/vcs/gitlab_client.go | 2 +- server/events/vcs/gitlab_client_test.go | 2 +- 16 files changed, 60 insertions(+), 41 deletions(-) diff --git a/e2e/gitlab.go b/e2e/gitlab.go index 2226aa299d..a8f6449f49 100644 --- a/e2e/gitlab.go +++ b/e2e/gitlab.go @@ -20,7 +20,7 @@ import ( "os" "os/exec" - "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) type GitlabClient struct { diff --git a/e2e/go.mod b/e2e/go.mod index 1d706f3f8a..f3cdcc40c4 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -5,18 +5,18 @@ go 1.23.4 require ( github.com/google/go-github/v66 v66.0.0 github.com/hashicorp/go-multierror v1.1.1 - github.com/xanzy/go-gitlab v0.114.0 + gitlab.com/gitlab-org/api/client-go v0.118.0 ) require ( - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.36.0 // indirect ) diff --git a/e2e/go.sum b/e2e/go.sum index 17d5056f99..9b950972d6 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -2,10 +2,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -31,30 +31,46 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/xanzy/go-gitlab v0.114.0 h1:0wQr/KBckwrZPfEMjRqpUz0HmsKKON9UhCYv9KDy19M= -github.com/xanzy/go-gitlab v0.114.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/gitlab-org/api/client-go v0.118.0 h1:qHIEw+XHt+2xuk4iZGW8fc6t+gTLAGEmTA5Bzp/brxs= +gitlab.com/gitlab-org/api/client-go v0.118.0/go.mod h1:E+X2dndIYDuUfKVP0C3jhkWvTSE00BkLbCsXTY3edDo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.mod b/go.mod index 0328202cbd..a3d9fd5609 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,8 @@ require ( github.com/stretchr/testify v1.10.0 github.com/uber-go/tally/v4 v4.1.16 github.com/urfave/negroni/v3 v3.1.1 - github.com/xanzy/go-gitlab v0.114.0 + github.com/xanzy/go-gitlab v0.115.0 + gitlab.com/gitlab-org/api/client-go v0.118.0 go.etcd.io/bbolt v1.3.11 go.uber.org/zap v1.27.0 golang.org/x/term v0.27.0 @@ -88,7 +89,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/mock v1.6.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/css v1.0.1 // indirect @@ -140,6 +141,6 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.36.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index 5e21b61577..6b6ce265b0 100644 --- a/go.sum +++ b/go.sum @@ -199,8 +199,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -461,8 +461,8 @@ github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw= github.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs= -github.com/xanzy/go-gitlab v0.114.0 h1:0wQr/KBckwrZPfEMjRqpUz0HmsKKON9UhCYv9KDy19M= -github.com/xanzy/go-gitlab v0.114.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= +github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8= +github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -476,6 +476,8 @@ 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-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +gitlab.com/gitlab-org/api/client-go v0.118.0 h1:qHIEw+XHt+2xuk4iZGW8fc6t+gTLAGEmTA5Bzp/brxs= +gitlab.com/gitlab-org/api/client-go v0.118.0/go.mod h1:E+X2dndIYDuUfKVP0C3jhkWvTSE00BkLbCsXTY3edDo= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -806,8 +808,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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= diff --git a/server/controllers/events/events_controller.go b/server/controllers/events/events_controller.go index c838134132..57d5a8bb57 100644 --- a/server/controllers/events/events_controller.go +++ b/server/controllers/events/events_controller.go @@ -34,7 +34,7 @@ import ( "github.com/runatlantis/atlantis/server/events/vcs/gitea" "github.com/runatlantis/atlantis/server/logging" tally "github.com/uber-go/tally/v4" - gitlab "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) const githubHeader = "X-Github-Event" diff --git a/server/controllers/events/events_controller_test.go b/server/controllers/events/events_controller_test.go index 11bcec3445..ff68c1f77e 100644 --- a/server/controllers/events/events_controller_test.go +++ b/server/controllers/events/events_controller_test.go @@ -38,7 +38,7 @@ import ( "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/metrics" . "github.com/runatlantis/atlantis/testing" - gitlab "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) const githubHeader = "X-Github-Event" diff --git a/server/controllers/events/gitlab_request_parser_validator.go b/server/controllers/events/gitlab_request_parser_validator.go index 5d58dba831..22d2e08e0f 100644 --- a/server/controllers/events/gitlab_request_parser_validator.go +++ b/server/controllers/events/gitlab_request_parser_validator.go @@ -20,7 +20,7 @@ import ( "io" "net/http" - gitlab "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) const secretHeader = "X-Gitlab-Token" // #nosec diff --git a/server/controllers/events/gitlab_request_parser_validator_test.go b/server/controllers/events/gitlab_request_parser_validator_test.go index 184b9f00b7..fb61c4ff9d 100644 --- a/server/controllers/events/gitlab_request_parser_validator_test.go +++ b/server/controllers/events/gitlab_request_parser_validator_test.go @@ -22,7 +22,7 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/controllers/events" . "github.com/runatlantis/atlantis/testing" - gitlab "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) var parser = events.DefaultGitlabRequestParserValidator{} diff --git a/server/events/command_runner.go b/server/events/command_runner.go index fdd4b39153..dbc2e7e99b 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -30,7 +30,7 @@ import ( "github.com/runatlantis/atlantis/server/recovery" "github.com/runatlantis/atlantis/server/utils" tally "github.com/uber-go/tally/v4" - gitlab "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) const ( diff --git a/server/events/event_parser.go b/server/events/event_parser.go index 7ab18b07ca..e96b65ac72 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -34,7 +34,7 @@ import ( "github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver" "github.com/runatlantis/atlantis/server/events/vcs/gitea" "github.com/runatlantis/atlantis/server/logging" - "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) const gitlabPullOpened = "opened" diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index ef8f2de627..648807c742 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -30,7 +30,7 @@ import ( . "github.com/runatlantis/atlantis/server/events/vcs/testdata" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" - gitlab "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) var parser = events.EventParser{ diff --git a/server/events/mocks/mock_event_parsing.go b/server/events/mocks/mock_event_parsing.go index 85403ea95c..8732cadc1d 100644 --- a/server/events/mocks/mock_event_parsing.go +++ b/server/events/mocks/mock_event_parsing.go @@ -11,7 +11,7 @@ import ( models "github.com/runatlantis/atlantis/server/events/models" gitea0 "github.com/runatlantis/atlantis/server/events/vcs/gitea" logging "github.com/runatlantis/atlantis/server/logging" - go_gitlab "github.com/xanzy/go-gitlab" + go_gitlab "gitlab.com/gitlab-org/api/client-go" "reflect" "time" ) diff --git a/server/events/mocks/mock_gitlab_merge_request_getter.go b/server/events/mocks/mock_gitlab_merge_request_getter.go index dfaa1396b9..2b28aae238 100644 --- a/server/events/mocks/mock_gitlab_merge_request_getter.go +++ b/server/events/mocks/mock_gitlab_merge_request_getter.go @@ -6,7 +6,7 @@ package mocks import ( pegomock "github.com/petergtz/pegomock/v4" logging "github.com/runatlantis/atlantis/server/logging" - go_gitlab "github.com/xanzy/go-gitlab" + go_gitlab "gitlab.com/gitlab-org/api/client-go" "reflect" "time" ) diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go index fffe8c63e9..ba111c85e9 100644 --- a/server/events/vcs/gitlab_client.go +++ b/server/events/vcs/gitlab_client.go @@ -29,7 +29,7 @@ import ( "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs/common" "github.com/runatlantis/atlantis/server/logging" - "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) // gitlabMaxCommentLength is the maximum number of chars allowed by Gitlab in a diff --git a/server/events/vcs/gitlab_client_test.go b/server/events/vcs/gitlab_client_test.go index 8aee1e865a..d56bfa2f45 100644 --- a/server/events/vcs/gitlab_client_test.go +++ b/server/events/vcs/gitlab_client_test.go @@ -15,7 +15,7 @@ import ( "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" - "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" . "github.com/runatlantis/atlantis/testing" ) From d2c547674622f2828978c4f4e8d5e86ffda03bf5 Mon Sep 17 00:00:00 2001 From: Dan Urson Date: Tue, 31 Dec 2024 13:43:38 -0500 Subject: [PATCH 02/60] feat: Sign Atlantis containers before release (bonus points: speed up x86 builds) (#5207) Signed-off-by: Dan Urson Signed-off-by: Rui Chen Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Rui Chen --- .github/workflows/atlantis-image.yml | 37 +++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/atlantis-image.yml b/.github/workflows/atlantis-image.yml index 02f0f2dcec..0b8e8019df 100644 --- a/.github/workflows/atlantis-image.yml +++ b/.github/workflows/atlantis-image.yml @@ -53,6 +53,7 @@ jobs: strategy: matrix: image_type: [alpine, debian] + platform: [linux/arm64/v8, linux/amd64, linux/arm/v7] runs-on: ubuntu-24.04 env: # Set docker repo to either the fork or the main repo where the branch exists @@ -69,6 +70,11 @@ jobs: with: dockerfile: "Dockerfile" + - name: Set up Go + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + with: + go-version-file: "go.mod" + - name: Set up QEMU uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3 with: @@ -82,6 +88,10 @@ jobs: driver-opts: | image=moby/buildkit:v0.14.0 + - name: "Install cosign" + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + if: env.PUSH == 'true' && github.event_name != 'pull_request' + # release version is the name of the tag i.e. v0.10.0 # release version also has the image type appended i.e. v0.10.0-alpine # release tag is either pre-release or latest i.e. latest @@ -146,21 +156,38 @@ jobs: ATLANTIS_VERSION=${{ env.RELEASE_VERSION }} ATLANTIS_COMMIT=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} ATLANTIS_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} - platforms: linux/arm64/v8,linux/amd64,linux/arm/v7 + platforms: ${{ matrix.platform }} push: ${{ env.PUSH }} tags: ${{ steps.meta.outputs.tags }} target: ${{ matrix.image_type }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.description'] }} - - name: "Sign and Attest Image" - if: env.PUSH == 'true' + - name: "Create Image Attestation" + if: env.PUSH == 'true' && github.event_name != 'pull_request' uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-digest: ${{ steps.build.outputs.digest }} subject-name: ghcr.io/${{ github.repository }} push-to-registry: true + - name: "Sign images with environment annotations" + # no key needed, we're using the GitHub OIDC flow + # Only run on alpine/amd64 build to avoid signing multiple times + if: env.PUSH == 'true' && github.event_name != 'pull_request' && matrix.image_type == 'alpine' && matrix.platform == 'linux/amd64' + run: | + # Sign dev tags, version tags, and latest tags + echo "${TAGS}" | xargs -I {} cosign sign \ + --yes \ + --recursive=true \ + -a actor=${{ github.actor}} \ + -a ref_name=${{ github.ref_name}} \ + -a ref=${{ github.sha }} \ + {}@${DIGEST} + env: + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build.outputs.digest }} + test: needs: [changes] if: needs.changes.outputs.should-run-build == 'true' @@ -169,6 +196,7 @@ jobs: strategy: matrix: image_type: [alpine, debian] + platform: [linux/arm64/v8, linux/amd64, linux/arm/v7] env: # Set docker repo to either the fork or the main repo where the branch exists DOCKER_REPO: ghcr.io/${{ github.repository }} @@ -215,4 +243,5 @@ jobs: image_type: [alpine, debian] runs-on: ubuntu-24.04 steps: - - run: 'echo "No build required"' \ No newline at end of file + - run: 'echo "No build required"' + From d473fe0092fa8c04187b2ad05b12bb68d6f89de4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 18:45:20 +0000 Subject: [PATCH 03/60] fix(deps): update module github.com/google/go-github/v66 to v68 in go.mod (main) (#5209) Signed-off-by: Rui Chen Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Rui Chen --- e2e/github.go | 6 ++-- e2e/go.mod | 2 +- e2e/go.sum | 4 +-- go.mod | 5 ++- go.sum | 8 ++--- .../controllers/events/events_controller.go | 2 +- .../events/events_controller_e2e_test.go | 24 ++++++------- .../events/events_controller_test.go | 2 +- .../events/github_request_validator.go | 2 +- server/events/apply_command_runner_test.go | 6 ++-- server/events/command_runner.go | 2 +- server/events/command_runner_test.go | 34 +++++++++---------- server/events/event_parser.go | 2 +- server/events/event_parser_test.go | 16 ++++----- server/events/mocks/mock_event_parsing.go | 2 +- .../events/mocks/mock_github_pull_getter.go | 2 +- server/events/plan_command_runner_test.go | 4 +-- server/events/vcs/github_client.go | 12 +++---- server/events/vcs/github_credentials.go | 2 +- server/events/vcs/instrumented_client.go | 2 +- .../mocks/mock_github_pull_request_getter.go | 2 +- server/events/vcs/testdata/fixtures.go | 30 ++++++++-------- testdrive/github.go | 16 ++++----- testdrive/testdrive.go | 2 +- 24 files changed, 96 insertions(+), 93 deletions(-) diff --git a/e2e/github.go b/e2e/github.go index 5037b52e87..b86145d846 100644 --- a/e2e/github.go +++ b/e2e/github.go @@ -21,7 +21,7 @@ import ( "os/exec" "strings" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" ) type GithubClient struct { @@ -90,7 +90,7 @@ func (g GithubClient) CreateAtlantisWebhook(ctx context.Context, hookURL string) atlantisHook := &github.Hook{ Events: []string{"issue_comment", "pull_request", "push"}, Config: hookConfig, - Active: github.Bool(true), + Active: github.Ptr(true), } hook, _, err := g.client.Repositories.CreateHook(ctx, g.ownerName, g.repoName, atlantisHook) @@ -146,7 +146,7 @@ func (g GithubClient) GetAtlantisStatus(ctx context.Context, branchName string) func (g GithubClient) ClosePullRequest(ctx context.Context, pullRequestNumber int) error { // clean up - _, _, err := g.client.PullRequests.Edit(ctx, g.ownerName, g.repoName, pullRequestNumber, &github.PullRequest{State: github.String("closed")}) + _, _, err := g.client.PullRequests.Edit(ctx, g.ownerName, g.repoName, pullRequestNumber, &github.PullRequest{State: github.Ptr("closed")}) if err != nil { return fmt.Errorf("error while closing new pull request: %v", err) } diff --git a/e2e/go.mod b/e2e/go.mod index f3cdcc40c4..f600c3f52d 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -3,7 +3,7 @@ module github.com/runatlantis/atlantis/e2e go 1.23.4 require ( - github.com/google/go-github/v66 v66.0.0 + github.com/google/go-github/v68 v68.0.0 github.com/hashicorp/go-multierror v1.1.1 gitlab.com/gitlab-org/api/client-go v0.118.0 ) diff --git a/e2e/go.sum b/e2e/go.sum index 9b950972d6..825bce8d88 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -10,8 +10,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= -github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= +github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s= +github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/go.mod b/go.mod index a3d9fd5609..458983b9a1 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/go-playground/validator/v10 v10.23.0 github.com/go-test/deep v1.1.1 github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/go-github/v66 v66.0.0 + github.com/google/go-github/v68 v68.0.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 @@ -144,3 +144,6 @@ require ( google.golang.org/protobuf v1.36.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) + +// upstream pr to patch go-github to use v68, https://github.com/bradleyfalzon/ghinstallation/pull/137 +replace github.com/bradleyfalzon/ghinstallation/v2 => github.com/chenrui333/ghinstallation/v2 v2.12.1-0.20241231170237-36dcfb064b2f diff --git a/go.sum b/go.sum index 6b6ce265b0..65b17647c4 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= -github.com/bradleyfalzon/ghinstallation/v2 v2.12.0 h1:k8oVjGhZel2qmCUsYwSE34jPNT9DL2wCBOtugsHv26g= -github.com/bradleyfalzon/ghinstallation/v2 v2.12.0/go.mod h1:V4gJcNyAftH0rXpRp1SUVUuh+ACxOH1xOk/ZzkRHltg= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -92,6 +90,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenrui333/ghinstallation/v2 v2.12.1-0.20241231170237-36dcfb064b2f h1:TN3fEfE18MJ+o3Y4PMUWu1S9IVYL7a82G3LVa8zJ7/c= +github.com/chenrui333/ghinstallation/v2 v2.12.1-0.20241231170237-36dcfb064b2f/go.mod h1:EJ6fgedVEHa2kUyBTTvslJCXJafS/mhJNNKEOCspZXQ= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -216,8 +216,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 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/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= -github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= +github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s= +github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= diff --git a/server/controllers/events/events_controller.go b/server/controllers/events/events_controller.go index 57d5a8bb57..ba9dd1d5c0 100644 --- a/server/controllers/events/events_controller.go +++ b/server/controllers/events/events_controller.go @@ -22,7 +22,7 @@ import ( "strconv" "strings" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/mcdafydd/go-azuredevops/azuredevops" "github.com/microcosm-cc/bluemonday" "github.com/pkg/errors" diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index a9d4fe70a1..2727dd2bac 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -13,7 +13,7 @@ import ( "strings" "testing" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" @@ -1695,26 +1695,26 @@ func GitHubPullRequestParsed(headSHA string) *github.PullRequest { headSHA = "13940d121be73f656e2132c6d7b4c8e87878ac8d" } return &github.PullRequest{ - Number: github.Int(2), - State: github.String("open"), - HTMLURL: github.String("htmlurl"), + Number: github.Ptr(2), + State: github.Ptr("open"), + HTMLURL: github.Ptr("htmlurl"), Head: &github.PullRequestBranch{ Repo: &github.Repository{ - FullName: github.String("runatlantis/atlantis-tests"), - CloneURL: github.String("https://github.com/runatlantis/atlantis-tests.git"), + FullName: github.Ptr("runatlantis/atlantis-tests"), + CloneURL: github.Ptr("https://github.com/runatlantis/atlantis-tests.git"), }, - SHA: github.String(headSHA), - Ref: github.String("branch"), + SHA: github.Ptr(headSHA), + Ref: github.Ptr("branch"), }, Base: &github.PullRequestBranch{ Repo: &github.Repository{ - FullName: github.String("runatlantis/atlantis-tests"), - CloneURL: github.String("https://github.com/runatlantis/atlantis-tests.git"), + FullName: github.Ptr("runatlantis/atlantis-tests"), + CloneURL: github.Ptr("https://github.com/runatlantis/atlantis-tests.git"), }, - Ref: github.String("main"), + Ref: github.Ptr("main"), }, User: &github.User{ - Login: github.String("atlantisbot"), + Login: github.Ptr("atlantisbot"), }, } } diff --git a/server/controllers/events/events_controller_test.go b/server/controllers/events/events_controller_test.go index ff68c1f77e..f4c563552c 100644 --- a/server/controllers/events/events_controller_test.go +++ b/server/controllers/events/events_controller_test.go @@ -25,7 +25,7 @@ import ( "strings" "testing" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/mcdafydd/go-azuredevops/azuredevops" . "github.com/petergtz/pegomock/v4" events_controllers "github.com/runatlantis/atlantis/server/controllers/events" diff --git a/server/controllers/events/github_request_validator.go b/server/controllers/events/github_request_validator.go index 89ae67e6b2..dc8b89f560 100644 --- a/server/controllers/events/github_request_validator.go +++ b/server/controllers/events/github_request_validator.go @@ -19,7 +19,7 @@ import ( "io" "net/http" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" ) //go:generate pegomock generate --package mocks -o mocks/mock_github_request_validator.go GithubRequestValidator diff --git a/server/events/apply_command_runner_test.go b/server/events/apply_command_runner_test.go index 6f713710f6..6ef5873c90 100644 --- a/server/events/apply_command_runner_test.go +++ b/server/events/apply_command_runner_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/db" "github.com/runatlantis/atlantis/server/core/locking" @@ -54,7 +54,7 @@ func TestApplyCommandRunner_IsLocked(t *testing.T) { scopeNull, _, _ := metrics.NewLoggingScope(logger, "atlantis") pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(logger, testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(pull, nil) @@ -475,7 +475,7 @@ func TestApplyCommandRunner_ExecutionOrder(t *testing.T) { scopeNull, _, _ := metrics.NewLoggingScope(logger, "atlantis") pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} diff --git a/server/events/command_runner.go b/server/events/command_runner.go index dbc2e7e99b..fd544baabf 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -17,7 +17,7 @@ import ( "fmt" "strconv" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/mcdafydd/go-azuredevops/azuredevops" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/config/valid" diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 8764c08bea..cd9cbc10e4 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -27,7 +27,7 @@ import ( "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/metrics" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" . "github.com/petergtz/pegomock/v4" lockingmocks "github.com/runatlantis/atlantis/server/core/locking/mocks" "github.com/runatlantis/atlantis/server/events" @@ -506,7 +506,7 @@ func TestRunCommentCommand_DisableApplyAllDisabled(t *testing.T) { vcsClient := setup(t) applyCommandRunner.DisableApplyAll = true pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil) @@ -594,7 +594,7 @@ func TestRunCommentCommand_ClosedPull(t *testing.T) { " comment saying that this is not allowed") vcsClient := setup(t) pull := &github.PullRequest{ - State: github.String("closed"), + State: github.Ptr("closed"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.ClosedPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil) @@ -647,12 +647,12 @@ func TestRunUnlockCommand_VCSComment(t *testing.T) { }{ { name: "PR open", - prState: github.String("open"), + prState: github.Ptr("open"), }, { name: "PR closed", - prState: github.String("closed"), + prState: github.Ptr("closed"), }, } @@ -689,7 +689,7 @@ func TestRunUnlockCommandFail_VCSComment(t *testing.T) { vcsClient := setup(t) pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), @@ -713,7 +713,7 @@ func TestRunUnlockCommandFail_DisableUnlockLabel(t *testing.T) { vcsClient := setup(t) pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), @@ -737,7 +737,7 @@ func TestRunUnlockCommandFail_GetLabelsFail(t *testing.T) { vcsClient := setup(t) pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), @@ -763,7 +763,7 @@ func TestRunUnlockCommandDoesntRetrieveLabelsIfDisableUnlockLabelNotSet(t *testi vcsClient := setup(t) pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), @@ -865,7 +865,7 @@ func TestRunCommentCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_Fals When(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectResult{PlanSuccess: &models.PlanSuccess{}}) When(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil) - pull := &github.PullRequest{State: github.String("open")} + pull := &github.PullRequest{State: github.Ptr("open")} modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil) When(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil) @@ -907,7 +907,7 @@ func TestRunGenericPlanCommand_DeletePlans(t *testing.T) { When(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectResult{PlanSuccess: &models.PlanSuccess{}}) When(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil) - pull := &github.PullRequest{State: github.String("open")} + pull := &github.PullRequest{State: github.Ptr("open")} modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil) When(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil) @@ -1001,7 +1001,7 @@ func TestRunGenericPlanCommand_DiscardApprovals(t *testing.T) { When(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectResult{PlanSuccess: &models.PlanSuccess{}}) When(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil) - pull := &github.PullRequest{State: github.String("open")} + pull := &github.PullRequest{State: github.Ptr("open")} modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil) When(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil) @@ -1025,7 +1025,7 @@ func TestFailedApprovalCreatesFailedStatusUpdate(t *testing.T) { defer func() { autoMerger.GlobalAutomerge = false }() pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{ @@ -1071,7 +1071,7 @@ func TestApprovedPoliciesUpdateFailedPolicyStatus(t *testing.T) { defer func() { autoMerger.GlobalAutomerge = false }() pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{ @@ -1127,7 +1127,7 @@ func TestApplyMergeablityWhenPolicyCheckFails(t *testing.T) { defer func() { autoMerger.GlobalAutomerge = false }() pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{ @@ -1174,7 +1174,7 @@ func TestApplyWithAutoMerge_VSCMerge(t *testing.T) { vcsClient := setup(t) pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState} When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil) @@ -1217,7 +1217,7 @@ func TestRunApply_DiscardedProjects(t *testing.T) { Ok(t, err) Ok(t, boltDB.UpdateProjectStatus(pull, "default", ".", models.DiscardedPlanStatus)) ghPull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(ghPull, nil) When(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(ghPull))).ThenReturn(pull, pull.BaseRepo, testdata.GithubRepo, nil) diff --git a/server/events/event_parser.go b/server/events/event_parser.go index e96b65ac72..c9cae1c828 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -24,7 +24,7 @@ import ( giteasdk "code.gitea.io/sdk/gitea" "github.com/go-playground/validator/v10" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" lru "github.com/hashicorp/golang-lru/v2" "github.com/mcdafydd/go-azuredevops/azuredevops" "github.com/pkg/errors" diff --git a/server/events/event_parser_test.go b/server/events/event_parser_test.go index 648807c742..27515be71d 100644 --- a/server/events/event_parser_test.go +++ b/server/events/event_parser_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/mcdafydd/go-azuredevops/azuredevops" "github.com/mohae/deepcopy" "github.com/runatlantis/atlantis/server/events" @@ -68,12 +68,12 @@ func TestParseGithubIssueCommentEvent(t *testing.T) { comment := github.IssueCommentEvent{ Repo: &Repo, Issue: &github.Issue{ - Number: github.Int(1), - User: &github.User{Login: github.String("issue_user")}, - HTMLURL: github.String("https://github.com/runatlantis/atlantis/issues/1"), + Number: github.Ptr(1), + User: &github.User{Login: github.Ptr("issue_user")}, + HTMLURL: github.Ptr("https://github.com/runatlantis/atlantis/issues/1"), }, Comment: &github.IssueComment{ - User: &github.User{Login: github.String("comment_user")}, + User: &github.User{Login: github.Ptr("comment_user")}, }, } @@ -170,8 +170,8 @@ func TestParseGithubPullEventFromDraft(t *testing.T) { logger := logging.NewNoopLogger(t) // verify that close event treated as 'close' events by default closeEvent := deepcopy.Copy(PullEvent).(github.PullRequestEvent) - closeEvent.Action = github.String("closed") - closeEvent.PullRequest.Draft = github.Bool(true) + closeEvent.Action = github.Ptr("closed") + closeEvent.PullRequest.Draft = github.Ptr(true) _, evType, _, _, _, err := parser.ParseGithubPullEvent(logger, &closeEvent) Ok(t, err) @@ -179,7 +179,7 @@ func TestParseGithubPullEventFromDraft(t *testing.T) { // verify that draft PRs are treated as 'other' events by default testEvent := deepcopy.Copy(PullEvent).(github.PullRequestEvent) - testEvent.PullRequest.Draft = github.Bool(true) + testEvent.PullRequest.Draft = github.Ptr(true) _, evType, _, _, _, err = parser.ParseGithubPullEvent(logger, &testEvent) Ok(t, err) Equals(t, models.OtherPullEvent, evType) diff --git a/server/events/mocks/mock_event_parsing.go b/server/events/mocks/mock_event_parsing.go index 8732cadc1d..e6b72acacd 100644 --- a/server/events/mocks/mock_event_parsing.go +++ b/server/events/mocks/mock_event_parsing.go @@ -5,7 +5,7 @@ package mocks import ( gitea "code.gitea.io/sdk/gitea" - github "github.com/google/go-github/v66/github" + github "github.com/google/go-github/v68/github" azuredevops "github.com/mcdafydd/go-azuredevops/azuredevops" pegomock "github.com/petergtz/pegomock/v4" models "github.com/runatlantis/atlantis/server/events/models" diff --git a/server/events/mocks/mock_github_pull_getter.go b/server/events/mocks/mock_github_pull_getter.go index c904e371e4..c2be8a5fb3 100644 --- a/server/events/mocks/mock_github_pull_getter.go +++ b/server/events/mocks/mock_github_pull_getter.go @@ -4,7 +4,7 @@ package mocks import ( - github "github.com/google/go-github/v66/github" + github "github.com/google/go-github/v68/github" pegomock "github.com/petergtz/pegomock/v4" models "github.com/runatlantis/atlantis/server/events/models" logging "github.com/runatlantis/atlantis/server/logging" diff --git a/server/events/plan_command_runner_test.go b/server/events/plan_command_runner_test.go index c0085dc963..b1da0012e1 100644 --- a/server/events/plan_command_runner_test.go +++ b/server/events/plan_command_runner_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/db" "github.com/runatlantis/atlantis/server/events" @@ -472,7 +472,7 @@ func TestPlanCommandRunner_ExecutionOrder(t *testing.T) { scopeNull, _, _ := metrics.NewLoggingScope(logger, "atlantis") pull := &github.PullRequest{ - State: github.String("open"), + State: github.Ptr("open"), } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index 1000f73e07..9d13bb2587 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -25,7 +25,7 @@ import ( "strings" "time" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" @@ -268,8 +268,8 @@ func (g *GithubClient) HidePrevCommandComments(logger logging.SimpleLogging, rep nextPage := 0 for { comments, resp, err := g.client.Issues.ListComments(g.ctx, repo.Owner, repo.Name, pullNum, &github.IssueListCommentsOptions{ - Sort: github.String("created"), - Direction: github.String("asc"), + Sort: github.Ptr("created"), + Direction: github.Ptr("asc"), ListOptions: github.ListOptions{Page: nextPage}, }) if resp != nil { @@ -913,9 +913,9 @@ func (g *GithubClient) UpdateStatus(logger logging.SimpleLogging, repo models.Re logger.Info("Updating GitHub Check status for '%s' to '%s'", src, ghState) status := &github.RepoStatus{ - State: github.String(ghState), - Description: github.String(description), - Context: github.String(src), + State: github.Ptr(ghState), + Description: github.Ptr(description), + Context: github.Ptr(src), TargetURL: &url, } _, resp, err := g.client.Repositories.CreateStatus(g.ctx, repo.Owner, repo.Name, pull.HeadCommit, status) diff --git a/server/events/vcs/github_credentials.go b/server/events/vcs/github_credentials.go index e46b0e3c2c..ad6fadda61 100644 --- a/server/events/vcs/github_credentials.go +++ b/server/events/vcs/github_credentials.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/bradleyfalzon/ghinstallation/v2" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/pkg/errors" ) diff --git a/server/events/vcs/instrumented_client.go b/server/events/vcs/instrumented_client.go index adc0ca0abc..d5d5809d9c 100644 --- a/server/events/vcs/instrumented_client.go +++ b/server/events/vcs/instrumented_client.go @@ -3,7 +3,7 @@ package vcs import ( "strconv" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/metrics" diff --git a/server/events/vcs/mocks/mock_github_pull_request_getter.go b/server/events/vcs/mocks/mock_github_pull_request_getter.go index f8a809e44f..ad5670ca14 100644 --- a/server/events/vcs/mocks/mock_github_pull_request_getter.go +++ b/server/events/vcs/mocks/mock_github_pull_request_getter.go @@ -4,7 +4,7 @@ package mocks import ( - github "github.com/google/go-github/v66/github" + github "github.com/google/go-github/v68/github" pegomock "github.com/petergtz/pegomock/v4" models "github.com/runatlantis/atlantis/server/events/models" logging "github.com/runatlantis/atlantis/server/logging" diff --git a/server/events/vcs/testdata/fixtures.go b/server/events/vcs/testdata/fixtures.go index db17101876..aa18059bac 100644 --- a/server/events/vcs/testdata/fixtures.go +++ b/server/events/vcs/testdata/fixtures.go @@ -22,43 +22,43 @@ import ( "testing" "github.com/golang-jwt/jwt/v5" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/mcdafydd/go-azuredevops/azuredevops" ) var PullEvent = github.PullRequestEvent{ Sender: &github.User{ - Login: github.String("user"), + Login: github.Ptr("user"), }, Repo: &Repo, PullRequest: &Pull, - Action: github.String("opened"), + Action: github.Ptr("opened"), } var Pull = github.PullRequest{ Head: &github.PullRequestBranch{ - SHA: github.String("sha256"), - Ref: github.String("ref"), + SHA: github.Ptr("sha256"), + Ref: github.Ptr("ref"), Repo: &Repo, }, Base: &github.PullRequestBranch{ - SHA: github.String("sha256"), + SHA: github.Ptr("sha256"), Repo: &Repo, - Ref: github.String("basebranch"), + Ref: github.Ptr("basebranch"), }, - HTMLURL: github.String("html-url"), + HTMLURL: github.Ptr("html-url"), User: &github.User{ - Login: github.String("user"), + Login: github.Ptr("user"), }, - Number: github.Int(1), - State: github.String("open"), + Number: github.Ptr(1), + State: github.Ptr("open"), } var Repo = github.Repository{ - FullName: github.String("owner/repo"), - Owner: &github.User{Login: github.String("owner")}, - Name: github.String("repo"), - CloneURL: github.String("https://github.com/owner/repo.git"), + FullName: github.Ptr("owner/repo"), + Owner: &github.User{Login: github.Ptr("owner")}, + Name: github.Ptr("repo"), + CloneURL: github.Ptr("https://github.com/owner/repo.git"), } var ADPullEvent = azuredevops.Event{ diff --git a/testdrive/github.go b/testdrive/github.go index a56d3eee35..4fc8279678 100644 --- a/testdrive/github.go +++ b/testdrive/github.go @@ -18,7 +18,7 @@ import ( "strings" "time" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" ) var githubUsername string @@ -57,13 +57,13 @@ func (g *Client) CheckForkSuccess(ownerName string, forkRepoName string) bool { func (g *Client) CreateWebhook(ownerName string, repoName string, hookURL string) error { contentType := "json" hookConfig := &github.HookConfig{ - ContentType: &contentType, - URL: &hookURL, + ContentType: github.Ptr(contentType), + URL: github.Ptr(hookURL), } atlantisHook := &github.Hook{ Events: []string{"issue_comment", "pull_request", "pull_request_review", "push"}, Config: hookConfig, - Active: github.Bool(true), + Active: github.Ptr(true), } _, _, err := g.client.Repositories.CreateHook(g.ctx, ownerName, repoName, atlantisHook) return err @@ -87,10 +87,10 @@ func (g *Client) CreatePullRequest(ownerName string, repoName string, head strin // If not, create it. newPullRequest := &github.NewPullRequest{ - Title: github.String("Welcome to Atlantis!"), - Head: github.String(head), - Body: github.String(pullRequestBody), - Base: github.String(base), + Title: github.Ptr("Welcome to Atlantis!"), + Head: github.Ptr(head), + Body: github.Ptr(pullRequestBody), + Base: github.Ptr(base), } pull, _, err := g.client.PullRequests.Create(g.ctx, ownerName, repoName, newPullRequest) if err != nil { diff --git a/testdrive/testdrive.go b/testdrive/testdrive.go index 6847540fc0..9f2b61c6c7 100644 --- a/testdrive/testdrive.go +++ b/testdrive/testdrive.go @@ -31,7 +31,7 @@ import ( "time" "github.com/briandowns/spinner" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v68/github" "github.com/mitchellh/colorstring" "github.com/pkg/errors" ) From a9c3730c5f0121b6463d67bb8a0d917f6cf7bc71 Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Tue, 31 Dec 2024 15:24:40 -0500 Subject: [PATCH 04/60] chore: bump opentofu to 1.8.8, add tf 1.9.8, and drop 1.6/1.7 support (#5211) Signed-off-by: Rui Chen --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 186061acf4..ed8d0b5fe7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ARG GOLANG_TAG=1.23.4-alpine@sha256:6c5c9590f169f77c8046e45c611d3b28fe477789acd8 # renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp ARG DEFAULT_TERRAFORM_VERSION=1.10.3 # renovate: datasource=github-releases depName=opentofu/opentofu versioning=hashicorp -ARG DEFAULT_OPENTOFU_VERSION=1.8.7 +ARG DEFAULT_OPENTOFU_VERSION=1.8.8 # renovate: datasource=github-releases depName=open-policy-agent/conftest ARG DEFAULT_CONFTEST_VERSION=0.56.0 @@ -122,7 +122,7 @@ RUN ./download-release.sh \ "terraform" \ "${TARGETPLATFORM}" \ "${DEFAULT_TERRAFORM_VERSION}" \ - "1.6.6 1.7.5 1.8.5 ${DEFAULT_TERRAFORM_VERSION}" \ + "1.8.5 1.9.8 ${DEFAULT_TERRAFORM_VERSION}" \ && ./download-release.sh \ "tofu" \ "${TARGETPLATFORM}" \ From 9e0f3d1bb5084ba5177bdd9764de49b75e33544a Mon Sep 17 00:00:00 2001 From: Andrew Borg Date: Fri, 3 Jan 2025 00:06:01 -0500 Subject: [PATCH 05/60] feat: Support project-level Terraform distribution selection (#5167) Signed-off-by: Andrew Borg Co-authored-by: PePe Amengual <2208324+jamengual@users.noreply.github.com> Co-authored-by: Simon Heather <32168619+X-Guardian@users.noreply.github.com> --- cmd/server.go | 37 ++-- cmd/server_test.go | 41 +++++ .../docs/repo-level-atlantis-yaml.md | 15 ++ runatlantis.io/docs/server-configuration.md | 19 +- .../events/events_controller_e2e_test.go | 18 +- server/core/config/parser_validator_test.go | 25 +++ server/core/config/raw/project.go | 13 ++ server/core/config/valid/global_cfg.go | 3 + server/core/config/valid/repo_cfg.go | 1 + server/core/runtime/apply_step_runner.go | 25 ++- server/core/runtime/apply_step_runner_test.go | 77 ++++++-- server/core/runtime/env_step_runner_test.go | 7 +- server/core/runtime/import_step_runner.go | 21 ++- .../core/runtime/import_step_runner_test.go | 67 +++++-- server/core/runtime/init_step_runner.go | 13 +- server/core/runtime/init_step_runner_test.go | 164 +++++++++++++----- .../core/runtime/mocks/mock_async_tfexec.go | 31 ++-- .../runtime/models/shell_command_runner.go | 2 +- .../core/runtime/multienv_step_runner_test.go | 9 +- server/core/runtime/plan_step_runner.go | 38 ++-- server/core/runtime/plan_step_runner_test.go | 134 +++++++++++--- .../plan_type_step_runner_delegate_test.go | 145 ++++++++++++++++ .../core/runtime/policy_check_step_runner.go | 3 +- .../runtime/post_workflow_hook_runner_test.go | 7 +- .../runtime/pre_workflow_hook_runner_test.go | 7 +- server/core/runtime/run_step_runner.go | 55 +++--- server/core/runtime/run_step_runner_test.go | 53 ++++-- server/core/runtime/runtime.go | 7 +- server/core/runtime/show_step_runner.go | 18 +- server/core/runtime/show_step_runner_test.go | 49 +++++- server/core/runtime/state_rm_step_runner.go | 21 ++- .../core/runtime/state_rm_step_runner_test.go | 68 ++++++-- server/core/runtime/version_step_runner.go | 12 +- .../core/runtime/version_step_runner_test.go | 54 +++++- .../runtime/workspace_step_runner_delegate.go | 31 ++-- .../workspace_step_runner_delegate_test.go | 106 +++++++++-- .../{events => core}/terraform/ansi/strip.go | 0 .../terraform/ansi/strip_test.go | 0 server/core/terraform/distribution.go | 8 + .../mocks/mock_terraform_client.go | 59 ++++--- .../{ => tfclient}/terraform_client.go | 48 ++--- .../terraform_client_internal_test.go | 32 +++- .../{ => tfclient}/terraform_client_test.go | 51 +++--- server/events/command/project_context.go | 4 + server/events/command/scope_tags.go | 13 +- server/events/mock_workingdir_test.go | 5 +- server/events/project_command_builder.go | 8 +- .../project_command_builder_internal_test.go | 12 +- server/events/project_command_builder_test.go | 28 +-- .../events/project_command_context_builder.go | 11 +- .../project_command_context_builder_test.go | 4 +- server/events/project_command_runner_test.go | 6 +- server/server.go | 48 ++--- server/user_config.go | 3 +- 54 files changed, 1325 insertions(+), 411 deletions(-) rename server/{events => core}/terraform/ansi/strip.go (100%) rename server/{events => core}/terraform/ansi/strip_test.go (100%) rename server/core/terraform/{ => tfclient}/mocks/mock_terraform_client.go (79%) rename server/core/terraform/{ => tfclient}/terraform_client.go (91%) rename server/core/terraform/{ => tfclient}/terraform_client_internal_test.go (88%) rename server/core/terraform/{ => tfclient}/terraform_client_test.go (86%) diff --git a/cmd/server.go b/cmd/server.go index 5722b38cfa..aa8581e705 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -72,6 +72,7 @@ const ( CheckoutStrategyFlag = "checkout-strategy" ConfigFlag = "config" DataDirFlag = "data-dir" + DefaultTFDistributionFlag = "default-tf-distribution" DefaultTFVersionFlag = "default-tf-version" DisableApplyAllFlag = "disable-apply-all" DisableAutoplanFlag = "disable-autoplan" @@ -141,7 +142,7 @@ const ( SSLCertFileFlag = "ssl-cert-file" SSLKeyFileFlag = "ssl-key-file" RestrictFileList = "restrict-file-list" - TFDistributionFlag = "tf-distribution" + TFDistributionFlag = "tf-distribution" // deprecated for DefaultTFDistributionFlag TFDownloadFlag = "tf-download" TFDownloadURLFlag = "tf-download-url" UseTFPluginCache = "use-tf-plugin-cache" @@ -421,8 +422,8 @@ var stringFlags = map[string]stringFlag{ description: fmt.Sprintf("File containing x509 private key matching --%s.", SSLCertFileFlag), }, TFDistributionFlag: { - description: fmt.Sprintf("Which TF distribution to use. Can be set to %s or %s.", TFDistributionTerraform, TFDistributionOpenTofu), - defaultValue: DefaultTFDistribution, + description: "[Deprecated for --default-tf-distribution].", + hidden: true, }, TFDownloadURLFlag: { description: "Base URL to download Terraform versions from.", @@ -437,6 +438,10 @@ var stringFlags = map[string]stringFlag{ " Only set if using TFC/E as a remote backend." + " Should be specified via the ATLANTIS_TFE_TOKEN environment variable for security.", }, + DefaultTFDistributionFlag: { + description: fmt.Sprintf("Which TF distribution to use. Can be set to %s or %s.", TFDistributionTerraform, TFDistributionOpenTofu), + defaultValue: DefaultTFDistribution, + }, DefaultTFVersionFlag: { description: "Terraform version to default to (ex. v0.12.0). Will download if not yet on disk." + " If not set, Atlantis uses the terraform binary in its PATH.", @@ -840,12 +845,13 @@ func (s *ServerCmd) run() error { // Config looks good. Start the server. server, err := s.ServerCreator.NewServer(userConfig, server.Config{ - AllowForkPRsFlag: AllowForkPRsFlag, - AtlantisURLFlag: AtlantisURLFlag, - AtlantisVersion: s.AtlantisVersion, - DefaultTFVersionFlag: DefaultTFVersionFlag, - RepoConfigJSONFlag: RepoConfigJSONFlag, - SilenceForkPRErrorsFlag: SilenceForkPRErrorsFlag, + AllowForkPRsFlag: AllowForkPRsFlag, + AtlantisURLFlag: AtlantisURLFlag, + AtlantisVersion: s.AtlantisVersion, + DefaultTFDistributionFlag: DefaultTFDistributionFlag, + DefaultTFVersionFlag: DefaultTFVersionFlag, + RepoConfigJSONFlag: RepoConfigJSONFlag, + SilenceForkPRErrorsFlag: SilenceForkPRErrorsFlag, }) if err != nil { @@ -921,8 +927,11 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig, v *viper.Viper) { if c.RedisPort == 0 { c.RedisPort = DefaultRedisPort } - if c.TFDistribution == "" { - c.TFDistribution = DefaultTFDistribution + if c.TFDistribution != "" && c.DefaultTFDistribution == "" { + c.DefaultTFDistribution = c.TFDistribution + } + if c.DefaultTFDistribution == "" { + c.DefaultTFDistribution = DefaultTFDistribution } if c.TFDownloadURL == "" { c.TFDownloadURL = DefaultTFDownloadURL @@ -953,7 +962,7 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { return fmt.Errorf("invalid log level: must be one of %v", ValidLogLevels) } - if userConfig.TFDistribution != TFDistributionTerraform && userConfig.TFDistribution != TFDistributionOpenTofu { + if userConfig.DefaultTFDistribution != TFDistributionTerraform && userConfig.DefaultTFDistribution != TFDistributionOpenTofu { return fmt.Errorf("invalid tf distribution: expected one of %s or %s", TFDistributionTerraform, TFDistributionOpenTofu) } @@ -1172,6 +1181,10 @@ func (s *ServerCmd) deprecationWarnings(userConfig *server.UserConfig) error { // } // + if userConfig.TFDistribution != "" { + deprecatedFlags = append(deprecatedFlags, TFDistributionFlag) + } + if len(deprecatedFlags) > 0 { warning := "WARNING: " if len(deprecatedFlags) == 1 { diff --git a/cmd/server_test.go b/cmd/server_test.go index c14e43cdd6..7d7c1b52d5 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -73,6 +73,7 @@ var testFlags = map[string]interface{}{ CheckoutStrategyFlag: CheckoutStrategyMerge, CheckoutDepthFlag: 0, DataDirFlag: "/path", + DefaultTFDistributionFlag: "terraform", DefaultTFVersionFlag: "v0.11.0", DisableApplyAllFlag: true, DisableMarkdownFoldingFlag: true, @@ -977,6 +978,46 @@ func TestExecute_AutoplanFileList(t *testing.T) { } } +func TestExecute_ValidateDefaultTFDistribution(t *testing.T) { + cases := []struct { + description string + flags map[string]interface{} + expectErr string + }{ + { + "terraform", + map[string]interface{}{ + DefaultTFDistributionFlag: "terraform", + }, + "", + }, + { + "opentofu", + map[string]interface{}{ + DefaultTFDistributionFlag: "opentofu", + }, + "", + }, + { + "errs on invalid distribution", + map[string]interface{}{ + DefaultTFDistributionFlag: "invalid_distribution", + }, + "invalid tf distribution: expected one of terraform or opentofu", + }, + } + for _, testCase := range cases { + t.Log("Should validate default tf distribution when " + testCase.description) + c := setupWithDefaults(testCase.flags, t) + err := c.Execute() + if testCase.expectErr != "" { + ErrEquals(t, testCase.expectErr, err) + } else { + Ok(t, err) + } + } +} + func setup(flags map[string]interface{}, t *testing.T) *cobra.Command { vipr := viper.New() for k, v := range flags { diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index a5e89d20a4..25bf7ce160 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -66,6 +66,7 @@ projects: branch: /main/ dir: . workspace: default + terraform_distribution: terraform terraform_version: v0.11.0 delete_source_branch_on_merge: true repo_locking: true # deprecated: use repo_locks instead @@ -262,6 +263,20 @@ See [Custom Workflow Use Cases: Terragrunt](custom-workflows.md#terragrunt) See [Custom Workflow Use Cases: Running custom commands](custom-workflows.md#running-custom-commands) +### Terraform Distributions + +If you'd like to use a different distribution of Terraform than what is set +by the `--default-tf-version` flag, then set the `terraform_distribution` key: + +```yaml +version: 3 +projects: +- dir: project1 + terraform_distribution: opentofu +``` + +Atlantis will automatically download and use this distribution. Valid values are `terraform` and `opentofu`. + ### Terraform Versions If you'd like to use a different version of Terraform than what is in Atlantis' diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 54d53f0d60..cf290dc5ca 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -386,6 +386,16 @@ and set `--autoplan-modules` to `false`. Note that the atlantis user is restricted to `~/.atlantis`. If you set the `--data-dir` flag to a path outside of Atlantis its home directory, ensure that you grant the atlantis user the correct permissions. +### `--default-tf-distribution` + + ```bash + atlantis server --default-tf-distribution="terraform" + # or + ATLANTIS_DEFAULT_TF_DISTRIBUTION="terraform" + ``` + + Which TF distribution to use. Can be set to `terraform` or `opentofu`. + ### `--default-tf-version` ```bash @@ -1259,13 +1269,8 @@ This is useful when you have many projects and want to keep the pull request cle ### `--tf-distribution` - ```bash - atlantis server --tf-distribution="terraform" - # or - ATLANTIS_TF_DISTRIBUTION="terraform" - ``` - - Which TF distribution to use. Can be set to `terraform` or `opentofu`. + + Deprecated for `--default-tf-distribution`. ### `--tf-download` diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 2727dd2bac..3b66a28225 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -29,6 +29,7 @@ import ( mock_policy "github.com/runatlantis/atlantis/server/core/runtime/policy/mocks" "github.com/runatlantis/atlantis/server/core/terraform" terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/mocks" @@ -1319,7 +1320,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers mockDownloader := terraform_mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - terraformClient, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler) + terraformClient, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler) Ok(t, err) boltdb, err := db.New(dataDir) Ok(t, err) @@ -1346,6 +1347,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers } } + defaultTFDistribution := terraformClient.DefaultDistribution() defaultTFVersion := terraformClient.DefaultVersion() locker := events.NewDefaultWorkingDirLocker() parser := &config.ParserValidator{} @@ -1429,7 +1431,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers terraformClient, ) - showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFVersion) + showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion) Ok(t, err) @@ -1440,6 +1442,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers conftextExec.VersionCache = &LocalConftestCache{} policyCheckRunner, err := runtime.NewPolicyCheckStepRunner( + defaultTFDistribution, defaultTFVersion, conftextExec, ) @@ -1451,11 +1454,13 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers Locker: projectLocker, LockURLGenerator: &mockLockURLGenerator{}, InitStepRunner: &runtime.InitStepRunner{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTFVersion, + TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTFDistribution, + DefaultTFVersion: defaultTFVersion, }, PlanStepRunner: runtime.NewPlanStepRunner( terraformClient, + defaultTFDistribution, defaultTFVersion, statusUpdater, asyncTfExec, @@ -1465,10 +1470,11 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers ApplyStepRunner: &runtime.ApplyStepRunner{ TerraformExecutor: terraformClient, }, - ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFVersion), - StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFVersion), + ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion), + StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion), RunStepRunner: &runtime.RunStepRunner{ TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTFDistribution, DefaultTFVersion: defaultTFVersion, ProjectCmdOutputHandler: projectCmdOutputHandler, }, diff --git a/server/core/config/parser_validator_test.go b/server/core/config/parser_validator_test.go index c21187bc47..05299aa725 100644 --- a/server/core/config/parser_validator_test.go +++ b/server/core/config/parser_validator_test.go @@ -610,6 +610,31 @@ workflows: }, }, }, + { + description: "project field with terraform_distribution set to opentofu", + input: ` +version: 3 +projects: +- dir: . + workspace: myworkspace + terraform_distribution: opentofu +`, + exp: valid.RepoCfg{ + Version: 3, + Projects: []valid.Project{ + { + Dir: ".", + Workspace: "myworkspace", + TerraformDistribution: String("opentofu"), + Autoplan: valid.Autoplan{ + WhenModified: raw.DefaultAutoPlanWhenModified, + Enabled: true, + }, + }, + }, + Workflows: make(map[string]valid.Workflow), + }, + }, { description: "project dir with ..", input: ` diff --git a/server/core/config/raw/project.go b/server/core/config/raw/project.go index fe0e656a8c..5b389c8605 100644 --- a/server/core/config/raw/project.go +++ b/server/core/config/raw/project.go @@ -26,6 +26,7 @@ type Project struct { Dir *string `yaml:"dir,omitempty"` Workspace *string `yaml:"workspace,omitempty"` Workflow *string `yaml:"workflow,omitempty"` + TerraformDistribution *string `yaml:"terraform_distribution,omitempty"` TerraformVersion *string `yaml:"terraform_version,omitempty"` Autoplan *Autoplan `yaml:"autoplan,omitempty"` PlanRequirements []string `yaml:"plan_requirements,omitempty"` @@ -86,6 +87,7 @@ func (p Project) Validate() error { validation.Field(&p.PlanRequirements, validation.By(validPlanReq)), validation.Field(&p.ApplyRequirements, validation.By(validApplyReq)), validation.Field(&p.ImportRequirements, validation.By(validImportReq)), + validation.Field(&p.TerraformDistribution, validation.By(validDistribution)), validation.Field(&p.TerraformVersion, validation.By(VersionValidator)), validation.Field(&p.DependsOn, validation.By(DependsOn)), validation.Field(&p.Name, validation.By(validName)), @@ -118,6 +120,9 @@ func (p Project) ToValid() valid.Project { if p.TerraformVersion != nil { v.TerraformVersion, _ = version.NewVersion(*p.TerraformVersion) } + if p.TerraformDistribution != nil { + v.TerraformDistribution = p.TerraformDistribution + } if p.Autoplan == nil { v.Autoplan = DefaultAutoPlan() } else { @@ -202,3 +207,11 @@ func validImportReq(value interface{}) error { } return nil } + +func validDistribution(value interface{}) error { + distribution := value.(*string) + if distribution != nil && *distribution != "terraform" && *distribution != "opentofu" { + return fmt.Errorf("'%s' is not a valid terraform_distribution, only '%s' and '%s' are supported", *distribution, "terraform", "opentofu") + } + return nil +} diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go index b0bdc86822..48a78f7158 100644 --- a/server/core/config/valid/global_cfg.go +++ b/server/core/config/valid/global_cfg.go @@ -105,6 +105,7 @@ type MergedProjectCfg struct { AutoplanEnabled bool AutoMergeDisabled bool AutoMergeMethod string + TerraformDistribution *string TerraformVersion *version.Version RepoCfgVersion int PolicySets PolicySets @@ -412,6 +413,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro DependsOn: proj.DependsOn, Name: proj.GetName(), AutoplanEnabled: proj.Autoplan.Enabled, + TerraformDistribution: proj.TerraformDistribution, TerraformVersion: proj.TerraformVersion, RepoCfgVersion: rCfg.Version, PolicySets: g.PolicySets, @@ -438,6 +440,7 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo Workspace: workspace, Name: "", AutoplanEnabled: DefaultAutoPlanEnabled, + TerraformDistribution: nil, TerraformVersion: nil, PolicySets: g.PolicySets, DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge, diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go index 4612f72cec..8478ce3dd0 100644 --- a/server/core/config/valid/repo_cfg.go +++ b/server/core/config/valid/repo_cfg.go @@ -147,6 +147,7 @@ type Project struct { Workspace string Name *string WorkflowName *string + TerraformDistribution *string TerraformVersion *version.Version Autoplan Autoplan PlanRequirements []string diff --git a/server/core/runtime/apply_step_runner.go b/server/core/runtime/apply_step_runner.go index 2e223f2996..35a864cfc8 100644 --- a/server/core/runtime/apply_step_runner.go +++ b/server/core/runtime/apply_step_runner.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" version "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/utils" @@ -17,10 +18,11 @@ import ( // ApplyStepRunner runs `terraform apply`. type ApplyStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version - CommitStatusUpdater StatusUpdater - AsyncTFExec AsyncTFExec + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version + CommitStatusUpdater StatusUpdater + AsyncTFExec AsyncTFExec } func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { @@ -39,11 +41,19 @@ func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pa ctx.Log.Info("starting apply") var out string + tfDistribution := a.DefaultTFDistribution + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } + tfVersion := a.DefaultTFVersion + if ctx.TerraformVersion != nil { + tfVersion = ctx.TerraformVersion + } // TODO: Leverage PlanTypeStepRunnerDelegate here if IsRemotePlan(contents) { args := append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.EscapedCommentArgs...) - out, err = a.runRemoteApply(ctx, args, path, planPath, ctx.TerraformVersion, envs) + out, err = a.runRemoteApply(ctx, args, path, planPath, tfDistribution, tfVersion, envs) if err == nil { out = a.cleanRemoteApplyOutput(out) } @@ -51,7 +61,7 @@ func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pa // NOTE: we need to quote the plan path because Bitbucket Server can // have spaces in its repo owner names which is part of the path. args := append(append(append([]string{"apply", "-input=false"}, extraArgs...), ctx.EscapedCommentArgs...), fmt.Sprintf("%q", planPath)) - out, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, ctx.TerraformVersion, ctx.Workspace) + out, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, tfDistribution, tfVersion, ctx.Workspace) } // If the apply was successful, delete the plan. @@ -115,6 +125,7 @@ func (a *ApplyStepRunner) runRemoteApply( applyArgs []string, path string, absPlanPath string, + tfDistribution terraform.Distribution, tfVersion *version.Version, envs map[string]string) (string, error) { // The planfile contents are needed to ensure that the plan didn't change @@ -133,7 +144,7 @@ func (a *ApplyStepRunner) runRemoteApply( // Start the async command execution. ctx.Log.Debug("starting async tf remote operation") - inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfVersion, ctx.Workspace) + inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfDistribution, tfVersion, ctx.Workspace) var lines []string nextLineIsRunURL := false var runURL string diff --git a/server/core/runtime/apply_step_runner_test.go b/server/core/runtime/apply_step_runner_test.go index 2a31040c81..d9be33e1d6 100644 --- a/server/core/runtime/apply_step_runner_test.go +++ b/server/core/runtime/apply_step_runner_test.go @@ -14,7 +14,9 @@ import ( "github.com/runatlantis/atlantis/server/core/runtime" runtimemocks "github.com/runatlantis/atlantis/server/core/runtime/mocks" runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -59,17 +61,20 @@ func TestRun_Success(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) o := runtime.ApplyStepRunner{ - TerraformExecutor: terraform, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfDistribution, nil, "workspace") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -91,22 +96,24 @@ func TestRun_AppliesCorrectProjectPlan(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) o := runtime.ApplyStepRunner{ - TerraformExecutor: terraform, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, } - - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfDistribution, nil, "default") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } -func TestRun_UsesConfiguredTFVersion(t *testing.T) { +func TestApplyStepRunner_TestRun_UsesConfiguredTFVersion(t *testing.T) { tmpDir := t.TempDir() planPath := filepath.Join(tmpDir, "workspace.tfplan") err := os.WriteFile(planPath, nil, 0600) @@ -123,17 +130,55 @@ func TestRun_UsesConfiguredTFVersion(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) o := runtime.ApplyStepRunner{ - TerraformExecutor: terraform, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, } + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). + ThenReturn("output", nil) + output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) + Ok(t, err) + Equals(t, "output", output) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfDistribution, tfVersion, "workspace") + _, err = os.Stat(planPath) + Assert(t, os.IsNotExist(err), "planfile should be deleted") +} - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). +func TestApplyStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, nil, 0600) + Ok(t, err) + + logger := logging.NewNoopLogger(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + tfVersion, _ := version.NewVersion("0.11.0") + projTFDistribution := "opentofu" + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &projTFDistribution, + Log: logger, + } + + RegisterMockTestingT(t) + terraform := tfclientmocks.NewMockClient() + o := runtime.ApplyStepRunner{ + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, + } + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), NotEq[tf.Distribution](tfDistribution), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq(tmpDir), Eq([]string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}), Eq(map[string]string(nil)), NotEq[tf.Distribution](tfDistribution), Eq(tfVersion), Eq("workspace")) _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -197,7 +242,7 @@ func TestRun_UsingTarget(t *testing.T) { planPath := filepath.Join(tmpDir, "workspace.tfplan") err := os.WriteFile(planPath, nil, 0600) Ok(t, err) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() step := runtime.ApplyStepRunner{ TerraformExecutor: terraform, } @@ -361,7 +406,7 @@ type remoteApplyMock struct { } // RunCommandAsync fakes out running terraform async. -func (r *remoteApplyMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) { +func (r *remoteApplyMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ tf.Distribution, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) { r.CalledArgs = args in := make(chan string) diff --git a/server/core/runtime/env_step_runner_test.go b/server/core/runtime/env_step_runner_test.go index 0fe86f77f0..7772d56c5f 100644 --- a/server/core/runtime/env_step_runner_test.go +++ b/server/core/runtime/env_step_runner_test.go @@ -5,7 +5,9 @@ import ( "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/core/runtime" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" @@ -38,12 +40,15 @@ func TestEnvStepRunner_Run(t *testing.T) { }, } RegisterMockTestingT(t) - tfClient := mocks.NewMockClient() + tfClient := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, err := version.NewVersion("0.12.0") Ok(t, err) projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() runStepRunner := runtime.RunStepRunner{ TerraformExecutor: tfClient, + DefaultTFDistribution: tfDistribution, DefaultTFVersion: tfVersion, ProjectCmdOutputHandler: projectCmdOutputHandler, } diff --git a/server/core/runtime/import_step_runner.go b/server/core/runtime/import_step_runner.go index 0d5787a8ad..7f3a22b9b4 100644 --- a/server/core/runtime/import_step_runner.go +++ b/server/core/runtime/import_step_runner.go @@ -5,25 +5,32 @@ import ( "path/filepath" version "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/utils" ) type importStepRunner struct { - terraformExecutor TerraformExec - defaultTFVersion *version.Version + terraformExecutor TerraformExec + defaultTFDistribution terraform.Distribution + defaultTFVersion *version.Version } -func NewImportStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version) Runner { +func NewImportStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version) Runner { runner := &importStepRunner{ - terraformExecutor: terraformExecutor, - defaultTFVersion: defaultTfVersion, + terraformExecutor: terraformExecutor, + defaultTFDistribution: defaultTfDistribution, + defaultTFVersion: defaultTfVersion, } - return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner) + return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner) } func (p *importStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { + tfDistribution := p.defaultTFDistribution tfVersion := p.defaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } @@ -31,7 +38,7 @@ func (p *importStepRunner) Run(ctx command.ProjectContext, extraArgs []string, p importCmd := []string{"import"} importCmd = append(importCmd, extraArgs...) importCmd = append(importCmd, ctx.EscapedCommentArgs...) - out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), importCmd, envs, tfVersion, ctx.Workspace) + out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), importCmd, envs, tfDistribution, tfVersion, ctx.Workspace) // If the import was successful and a plan file exists, delete the plan. planPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)) diff --git a/server/core/runtime/import_step_runner_test.go b/server/core/runtime/import_step_runner_test.go index b10f182de9..d7cacf9a5f 100644 --- a/server/core/runtime/import_step_runner_test.go +++ b/server/core/runtime/import_step_runner_test.go @@ -8,7 +8,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -29,17 +31,19 @@ func TestImportStepRunner_Run_Success(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.15.0") - s := NewImportStepRunner(terraform, tfVersion) + s := NewImportStepRunner(terraform, tfDistribution, tfVersion) - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) commands := []string{"import", "-var", "foo=bar", "addr", "id"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace) _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -59,23 +63,66 @@ func TestImportStepRunner_Run_Workspace(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() tfVersion, _ := version.NewVersion("0.15.0") - s := NewImportStepRunner(terraform, tfVersion) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + s := NewImportStepRunner(terraform, tfDistribution, tfVersion) - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) // switch workspace - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfVersion, workspace) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfDistribution, tfVersion, workspace) // exec import commands := []string{"import", "-var", "foo=bar", "addr", "id"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace) + + _, err = os.Stat(planPath) + Assert(t, os.IsNotExist(err), "planfile should be deleted") +} + +func TestImportStepRunner_Run_UsesConfiguredDistribution(t *testing.T) { + logger := logging.NewNoopLogger(t) + workspace := "something" + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, fmt.Sprintf("%s.tfplan", workspace)) + err := os.WriteFile(planPath, nil, 0600) + Ok(t, err) + + projTFDistribution := "opentofu" + context := command.ProjectContext{ + Log: logger, + EscapedCommentArgs: []string{"-var", "foo=bar", "addr", "id"}, + Workspace: workspace, + TerraformDistribution: &projTFDistribution, + } + + RegisterMockTestingT(t) + terraform := tfclientmocks.NewMockClient() + tfVersion, _ := version.NewVersion("0.15.0") + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + s := NewImportStepRunner(terraform, tfDistribution, tfVersion) + + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). + ThenReturn("output", nil) + output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) + Ok(t, err) + Equals(t, "output", output) + + // switch workspace + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "show"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "select", workspace}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + + // exec import + commands := []string{"import", "-var", "foo=bar", "addr", "id"} + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq(commands), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") diff --git a/server/core/runtime/init_step_runner.go b/server/core/runtime/init_step_runner.go index 0c6de1b013..c8da3ffa48 100644 --- a/server/core/runtime/init_step_runner.go +++ b/server/core/runtime/init_step_runner.go @@ -5,14 +5,16 @@ import ( version "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/core/runtime/common" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/utils" ) // InitStep runs `terraform init`. type InitStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version } func (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { @@ -33,6 +35,11 @@ func (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pat } } + tfDistribution := i.DefaultTFDistribution + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } + tfVersion := i.DefaultTFVersion if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion @@ -56,7 +63,7 @@ func (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pat terraformInitCmd := append(terraformInitVerb, finalArgs...) - out, err := i.TerraformExecutor.RunCommandWithVersion(ctx, path, terraformInitCmd, envs, tfVersion, ctx.Workspace) + out, err := i.TerraformExecutor.RunCommandWithVersion(ctx, path, terraformInitCmd, envs, tfDistribution, tfVersion, ctx.Workspace) // Only include the init output if there was an error. Otherwise it's // unnecessary and lengthens the comment. if err != nil { diff --git a/server/core/runtime/init_step_runner_test.go b/server/core/runtime/init_step_runner_test.go index 45927591a6..86d029c2d8 100644 --- a/server/core/runtime/init_step_runner_test.go +++ b/server/core/runtime/init_step_runner_test.go @@ -12,7 +12,9 @@ import ( "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/runtime" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -20,6 +22,8 @@ import ( func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { RegisterMockTestingT(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) cases := []struct { version string expCmd string @@ -44,7 +48,7 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { for _, c := range cases { t.Run(c.version, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -55,10 +59,11 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { tfVersion, _ := version.NewVersion(c.version) iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) @@ -71,7 +76,74 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { if c.expCmd == "get" { expArgs = []string{c.expCmd, "-upgrade", "extra", "args"} } - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") + }) + } +} + +func TestInitStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) { + RegisterMockTestingT(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + cases := []struct { + version string + distribution string + expCmd string + }{ + { + "0.8.9", + "opentofu", + "get", + }, + { + "0.8.9", + "terraform", + "get", + }, + { + "0.9.0", + "opentofu", + "init", + }, + { + "0.9.1", + "terraform", + "init", + }, + } + + for _, c := range cases { + t.Run(c.version, func(t *testing.T) { + terraform := tfclientmocks.NewMockClient() + + logger := logging.NewNoopLogger(t) + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + Log: logger, + TerraformDistribution: &c.distribution, + } + + tfVersion, _ := version.NewVersion(c.version) + iso := runtime.InitStepRunner{ + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, + } + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). + ThenReturn("output", nil) + + output, err := iso.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) + Ok(t, err) + // When there is no error, should not return init output to PR. + Equals(t, "", output) + + // If using init then we specify -input=false but not for get. + expArgs := []string{c.expCmd, "-input=false", "-upgrade", "extra", "args"} + if c.expCmd == "get" { + expArgs = []string{c.expCmd, "-upgrade", "extra", "args"} + } + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq("/path"), Eq(expArgs), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq("workspace")) }) } } @@ -79,15 +151,17 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { func TestRun_ShowInitOutputOnError(t *testing.T) { // If there was an error during init then we want the output to be returned. RegisterMockTestingT(t) - tfClient := mocks.NewMockClient() + tfClient := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) - When(tfClient.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(tfClient.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", errors.New("error")) - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.11.0") iso := runtime.InitStepRunner{ - TerraformExecutor: tfClient, - DefaultTFVersion: tfVersion, + TerraformExecutor: tfClient, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } output, err := iso.Run(command.ProjectContext{ @@ -118,14 +192,16 @@ func TestRun_InitOmitsUpgradeFlagIfLockFileTracked(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() - + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.14.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, []string{"extra", "args"}, repoDir, map[string]string(nil)) @@ -134,27 +210,29 @@ func TestRun_InitOmitsUpgradeFlagIfLockFileTracked(t *testing.T) { Equals(t, "", output) expectedArgs := []string{"init", "-input=false", "extra", "args"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") } func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) { tmpDir := t.TempDir() RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ Workspace: "workspace", RepoRelDir: ".", Log: logger, } - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.14.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) @@ -163,7 +241,7 @@ func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) { Equals(t, "", output) expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") } func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing.T) { @@ -173,7 +251,7 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -181,13 +259,15 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing RepoRelDir: ".", Log: logger, } - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.13.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) @@ -196,7 +276,7 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing Equals(t, "", output) expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") } func TestRun_InitExtraArgsDeDupe(t *testing.T) { @@ -240,7 +320,7 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) { for _, c := range cases { t.Run(c.description, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -248,13 +328,15 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) { RepoRelDir: ".", Log: logger, } - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, c.extraArgs, "/path", map[string]string(nil)) @@ -262,7 +344,7 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) { // When there is no error, should not return init output to PR. Equals(t, "", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", c.expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", c.expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") }) } } @@ -276,17 +358,19 @@ func TestRun_InitDeletesLockFileIfPresentAndNotTracked(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.14.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) ctx := command.ProjectContext{ @@ -300,7 +384,7 @@ func TestRun_InitDeletesLockFileIfPresentAndNotTracked(t *testing.T) { Equals(t, "", output) expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") } func runCmd(t *testing.T, dir string, name string, args ...string) string { diff --git a/server/core/runtime/mocks/mock_async_tfexec.go b/server/core/runtime/mocks/mock_async_tfexec.go index 662571ed0b..453c80012d 100644 --- a/server/core/runtime/mocks/mock_async_tfexec.go +++ b/server/core/runtime/mocks/mock_async_tfexec.go @@ -7,6 +7,7 @@ import ( go_version "github.com/hashicorp/go-version" pegomock "github.com/petergtz/pegomock/v4" models "github.com/runatlantis/atlantis/server/core/runtime/models" + terraform "github.com/runatlantis/atlantis/server/core/terraform" command "github.com/runatlantis/atlantis/server/events/command" "reflect" "time" @@ -27,11 +28,11 @@ func NewMockAsyncTFExec(options ...pegomock.Option) *MockAsyncTFExec { func (mock *MockAsyncTFExec) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockAsyncTFExec) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (chan<- string, <-chan models.Line) { +func (mock *MockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) (chan<- string, <-chan models.Line) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockAsyncTFExec().") } - _params := []pegomock.Param{ctx, path, args, envs, v, workspace} + _params := []pegomock.Param{ctx, path, args, envs, d, v, workspace} _result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandAsync", _params, []reflect.Type{reflect.TypeOf((*chan<- string)(nil)).Elem(), reflect.TypeOf((*<-chan models.Line)(nil)).Elem()}) var _ret0 chan<- string var _ret1 <-chan models.Line @@ -91,8 +92,8 @@ type VerifierMockAsyncTFExec struct { timeout time.Duration } -func (verifier *VerifierMockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockAsyncTFExec_RunCommandAsync_OngoingVerification { - _params := []pegomock.Param{ctx, path, args, envs, v, workspace} +func (verifier *VerifierMockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) *MockAsyncTFExec_RunCommandAsync_OngoingVerification { + _params := []pegomock.Param{ctx, path, args, envs, d, v, workspace} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandAsync", _params, verifier.timeout) return &MockAsyncTFExec_RunCommandAsync_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -102,12 +103,12 @@ type MockAsyncTFExec_RunCommandAsync_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, *go_version.Version, string) { - ctx, path, args, envs, v, workspace := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], v[len(v)-1], workspace[len(workspace)-1] +func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, terraform.Distribution, *go_version.Version, string) { + ctx, path, args, envs, d, v, workspace := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], d[len(d)-1], v[len(v)-1], workspace[len(workspace)-1] } -func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []*go_version.Version, _param5 []string) { +func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []terraform.Distribution, _param5 []*go_version.Version, _param6 []string) { _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(_params) > 0 { if len(_params) > 0 { @@ -135,15 +136,21 @@ func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArgu } } if len(_params) > 4 { - _param4 = make([]*go_version.Version, len(c.methodInvocations)) + _param4 = make([]terraform.Distribution, len(c.methodInvocations)) for u, param := range _params[4] { - _param4[u] = param.(*go_version.Version) + _param4[u] = param.(terraform.Distribution) } } if len(_params) > 5 { - _param5 = make([]string, len(c.methodInvocations)) + _param5 = make([]*go_version.Version, len(c.methodInvocations)) for u, param := range _params[5] { - _param5[u] = param.(string) + _param5[u] = param.(*go_version.Version) + } + } + if len(_params) > 6 { + _param6 = make([]string, len(c.methodInvocations)) + for u, param := range _params[6] { + _param6[u] = param.(string) } } } diff --git a/server/core/runtime/models/shell_command_runner.go b/server/core/runtime/models/shell_command_runner.go index 50b9f7760f..cd613bf450 100644 --- a/server/core/runtime/models/shell_command_runner.go +++ b/server/core/runtime/models/shell_command_runner.go @@ -10,8 +10,8 @@ import ( "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/config/valid" + "github.com/runatlantis/atlantis/server/core/terraform/ansi" "github.com/runatlantis/atlantis/server/events/command" - "github.com/runatlantis/atlantis/server/events/terraform/ansi" "github.com/runatlantis/atlantis/server/jobs" ) diff --git a/server/core/runtime/multienv_step_runner_test.go b/server/core/runtime/multienv_step_runner_test.go index 360adce3f5..326307fdea 100644 --- a/server/core/runtime/multienv_step_runner_test.go +++ b/server/core/runtime/multienv_step_runner_test.go @@ -7,7 +7,9 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/runtime" - "github.com/runatlantis/atlantis/server/core/terraform/mocks" + "github.com/runatlantis/atlantis/server/core/terraform" + terraformmocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" @@ -45,12 +47,15 @@ func TestMultiEnvStepRunner_Run(t *testing.T) { }, } RegisterMockTestingT(t) - tfClient := mocks.NewMockClient() + tfClient := tfclientmocks.NewMockClient() + mockDownloader := terraformmocks.NewMockDownloader() + tfDistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, err := version.NewVersion("0.12.0") Ok(t, err) projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() runStepRunner := runtime.RunStepRunner{ TerraformExecutor: tfClient, + DefaultTFDistribution: tfDistribution, DefaultTFVersion: tfVersion, ProjectCmdOutputHandler: projectCmdOutputHandler, } diff --git a/server/core/runtime/plan_step_runner.go b/server/core/runtime/plan_step_runner.go index 7d99dc26bf..b3fc491351 100644 --- a/server/core/runtime/plan_step_runner.go +++ b/server/core/runtime/plan_step_runner.go @@ -9,6 +9,7 @@ import ( version "github.com/hashicorp/go-version" "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" ) @@ -26,34 +27,40 @@ var ( ) type planStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version - CommitStatusUpdater StatusUpdater - AsyncTFExec AsyncTFExec + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version + CommitStatusUpdater StatusUpdater + AsyncTFExec AsyncTFExec } -func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec) Runner { +func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec) Runner { runner := &planStepRunner{ - TerraformExecutor: terraformExecutor, - DefaultTFVersion: defaultTfVersion, - CommitStatusUpdater: commitStatusUpdater, - AsyncTFExec: asyncTFExec, + TerraformExecutor: terraformExecutor, + DefaultTFDistribution: defaultTfDistribution, + DefaultTFVersion: defaultTfVersion, + CommitStatusUpdater: commitStatusUpdater, + AsyncTFExec: asyncTFExec, } - return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner) + return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner) } func (p *planStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { + tfDistribution := p.DefaultTFDistribution tfVersion := p.DefaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } planFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)) planCmd := p.buildPlanCmd(ctx, extraArgs, path, tfVersion, planFile) - output, err := p.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), planCmd, envs, tfVersion, ctx.Workspace) + output, err := p.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), planCmd, envs, tfDistribution, tfVersion, ctx.Workspace) if p.isRemoteOpsErr(output, err) { ctx.Log.Debug("detected that this project is using TFE remote ops") - return p.remotePlan(ctx, extraArgs, path, tfVersion, planFile, envs) + return p.remotePlan(ctx, extraArgs, path, tfDistribution, tfVersion, planFile, envs) } if err != nil { return output, err @@ -72,14 +79,14 @@ func (p *planStepRunner) isRemoteOpsErr(output string, err error) bool { // remotePlan runs a terraform plan command compatible with TFE remote // operations. -func (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []string, path string, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) { +func (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []string, path string, tfDistribution terraform.Distribution, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) { argList := [][]string{ {"plan", "-input=false", "-refresh", "-no-color"}, extraArgs, ctx.EscapedCommentArgs, } args := p.flatten(argList) - output, err := p.runRemotePlan(ctx, args, path, tfVersion, envs) + output, err := p.runRemotePlan(ctx, args, path, tfDistribution, tfVersion, envs) if err != nil { return output, err } @@ -193,6 +200,7 @@ func (p *planStepRunner) runRemotePlan( ctx command.ProjectContext, cmdArgs []string, path string, + tfDistribution terraform.Distribution, tfVersion *version.Version, envs map[string]string) (string, error) { @@ -205,7 +213,7 @@ func (p *planStepRunner) runRemotePlan( // Start the async command execution. ctx.Log.Debug("starting async tf remote operation") - _, outCh := p.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), cmdArgs, envs, tfVersion, ctx.Workspace) + _, outCh := p.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), cmdArgs, envs, tfDistribution, tfVersion, ctx.Workspace) var lines []string nextLineIsRunURL := false var runURL string diff --git a/server/core/runtime/plan_step_runner_test.go b/server/core/runtime/plan_step_runner_test.go index f05336637c..6a16b03e3f 100644 --- a/server/core/runtime/plan_step_runner_test.go +++ b/server/core/runtime/plan_step_runner_test.go @@ -13,7 +13,9 @@ import ( "github.com/runatlantis/atlantis/server/core/runtime" runtimemocks "github.com/runatlantis/atlantis/server/core/runtime/mocks" runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -24,7 +26,7 @@ import ( func TestRun_AddsEnvVarFile(t *testing.T) { // Test that if env/workspace.tfvars file exists we use -var-file option. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() @@ -36,10 +38,12 @@ func TestRun_AddsEnvVarFile(t *testing.T) { err = os.WriteFile(envVarsFile, nil, 0600) Ok(t, err) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) // Using version >= 0.10 here so we don't expect any env commands. tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger(t) - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) expPlanArgs := []string{"plan", "-input=false", @@ -78,14 +82,14 @@ func TestRun_AddsEnvVarFile(t *testing.T) { Name: "repo", }, } - When(terraform.RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil) + When(terraform.RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("output", nil) output, err := s.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) // Verify that env select was never called since we're in version >= 0.10 - terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, tmpDir, []string{"env", "select", "workspace"}, map[string]string(nil), tfVersion, "workspace") - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, tmpDir, []string{"env", "select", "workspace"}, map[string]string(nil), tfDistribution, tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") Equals(t, "output", output) } @@ -93,12 +97,14 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { // Test that if running for a project, uses a different path for the plan // file. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger(t) - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) ctx := command.ProjectContext{ Log: logger, Workspace: "default", @@ -115,7 +121,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { Name: "repo", }, } - When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil) + When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("workspace\n", nil) expPlanArgs := []string{"plan", "-input=false", @@ -137,7 +143,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { "comment", "args", } - When(terraform.RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default")).ThenReturn("output", nil) + When(terraform.RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "default")).ThenReturn("output", nil) output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) @@ -173,16 +179,19 @@ Terraform will perform the following actions: - aws_security_group_rule.allow_all ` RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) When(terraform.RunCommandWithVersion( Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), + Any[tf.Distribution](), Any[*version.Version](), Any[string]())). Then(func(params []Param) ReturnValues { @@ -223,11 +232,13 @@ Terraform will perform the following actions: // Test that even if there's an error, we get the returned output. func TestRun_OutputOnErr(t *testing.T) { RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) expOutput := "expected output" expErrMsg := "error!" When(terraform.RunCommandWithVersion( @@ -235,6 +246,7 @@ func TestRun_OutputOnErr(t *testing.T) { Any[string](), Any[[]string](), Any[map[string]string](), + Any[tf.Distribution](), Any[*version.Version](), Any[string]())). Then(func(params []Param) ReturnValues { @@ -287,7 +299,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() When(terraform.RunCommandWithVersion( @@ -295,11 +307,14 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { Any[string](), Any[[]string](), Any[map[string]string](), + Any[tf.Distribution](), Any[*version.Version](), Any[string]())).ThenReturn("output", nil) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion(c.tfVersion) - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) ctx := command.ProjectContext{ Workspace: "default", RepoRelDir: ".", @@ -319,7 +334,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "default") }) } @@ -385,11 +400,13 @@ locally at this time. }, } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion(c.tfVersion) asyncTf := &remotePlanMock{} - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTf) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTf) absProjectPath := t.TempDir() // First, terraform workspace gets run. @@ -398,6 +415,7 @@ locally at this time. absProjectPath, []string{"workspace", "show"}, map[string]string(nil), + tfDistribution, tfVersion, "default")).ThenReturn("default\n", nil) @@ -438,7 +456,7 @@ locally at this time. planErr := errors.New("exit status 1: err") planOutput := "\n" + c.remoteOpsErr asyncTf.LinesToSend = remotePlanOutput - When(terraform.RunCommandWithVersion(ctx, absProjectPath, expPlanArgs, map[string]string(nil), tfVersion, "default")). + When(terraform.RunCommandWithVersion(ctx, absProjectPath, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "default")). ThenReturn(planOutput, planErr) output, err := s.Run(ctx, []string{"extra", "args"}, absProjectPath, map[string]string(nil)) @@ -536,6 +554,82 @@ Plan: 0 to add, 0 to change, 1 to destroy.`, output) } } +func TestPlanStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) { + RegisterMockTestingT(t) + + expPlanArgs := []string{ + "plan", + "-input=false", + "-refresh", + "-out", + fmt.Sprintf("%q", "/path/default.tfplan"), + "extra", + "args", + "comment", + "args", + } + + cases := []struct { + name string + tfVersion string + tfDistribution string + }{ + { + "stable version", + "0.12.0", + "terraform", + }, + { + "with prerelease", + "0.14.0-rc1", + "opentofu", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + terraform := tfclientmocks.NewMockClient() + commitStatusUpdater := runtimemocks.NewMockStatusUpdater() + asyncTfExec := runtimemocks.NewMockAsyncTFExec() + When(terraform.RunCommandWithVersion( + Any[command.ProjectContext](), + Any[string](), + Any[[]string](), + Any[map[string]string](), + Any[tf.Distribution](), + Any[*version.Version](), + Any[string]())).ThenReturn("output", nil) + + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + tfVersion, _ := version.NewVersion(c.tfVersion) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) + ctx := command.ProjectContext{ + Workspace: "default", + RepoRelDir: ".", + User: models.User{Username: "username"}, + EscapedCommentArgs: []string{"comment", "args"}, + Pull: models.PullRequest{ + Num: 2, + }, + BaseRepo: models.Repo{ + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + }, + TerraformDistribution: &c.tfDistribution, + } + + output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) + Ok(t, err) + Equals(t, "output", output) + + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq("/path"), Eq(expPlanArgs), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq("default")) + }) + } + +} + type remotePlanMock struct { // LinesToSend will be sent on the channel. LinesToSend string @@ -543,7 +637,7 @@ type remotePlanMock struct { CalledArgs []string } -func (r *remotePlanMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) { +func (r *remotePlanMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ tf.Distribution, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) { r.CalledArgs = args in := make(chan string) out := make(chan runtimemodels.Line) diff --git a/server/core/runtime/plan_type_step_runner_delegate_test.go b/server/core/runtime/plan_type_step_runner_delegate_test.go index 286ae9ad40..db4be0ff03 100644 --- a/server/core/runtime/plan_type_step_runner_delegate_test.go +++ b/server/core/runtime/plan_type_step_runner_delegate_test.go @@ -153,3 +153,148 @@ func TestRunDelegate(t *testing.T) { }) } + +var openTofuPlanFileContents = ` +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + - destroy + +OpenTofu will perform the following actions: + + - null_resource.hi[1] + + +Plan: 0 to add, 0 to change, 1 to destroy.` + +func TestRunDelegate_UsesConfiguredDistribution(t *testing.T) { + + RegisterMockTestingT(t) + + mockDefaultRunner := mocks.NewMockRunner() + mockRemoteRunner := mocks.NewMockRunner() + + subject := &planTypeStepRunnerDelegate{ + defaultRunner: mockDefaultRunner, + remotePlanRunner: mockRemoteRunner, + } + + tfDistribution := "opentofu" + tfVersion, _ := version.NewVersion("1.7.0") + + t.Run("Remote Runner Success", func(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, []byte("Atlantis: this plan was created by remote ops\n"+openTofuPlanFileContents), 0600) + Ok(t, err) + + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &tfDistribution, + TerraformVersion: tfVersion, + } + extraArgs := []string{"extra", "args"} + envs := map[string]string{} + + expectedOut := "some random output" + + When(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil) + + output, err := subject.Run(ctx, extraArgs, tmpDir, envs) + + mockDefaultRunner.VerifyWasCalled(Never()) + + Equals(t, expectedOut, output) + Ok(t, err) + + }) + + t.Run("Remote Runner Failure", func(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, []byte("Atlantis: this plan was created by remote ops\n"+openTofuPlanFileContents), 0600) + Ok(t, err) + + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &tfDistribution, + TerraformVersion: tfVersion, + } + extraArgs := []string{"extra", "args"} + envs := map[string]string{} + + expectedOut := "some random output" + + When(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New("err")) + + output, err := subject.Run(ctx, extraArgs, tmpDir, envs) + + mockDefaultRunner.VerifyWasCalled(Never()) + + Equals(t, expectedOut, output) + Assert(t, err != nil, "err should not be nil") + + }) + + t.Run("Local Runner Success", func(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, []byte(openTofuPlanFileContents), 0600) + Ok(t, err) + + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &tfDistribution, + TerraformVersion: tfVersion, + } + extraArgs := []string{"extra", "args"} + envs := map[string]string{} + + expectedOut := "some random output" + + When(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil) + + output, err := subject.Run(ctx, extraArgs, tmpDir, envs) + + mockRemoteRunner.VerifyWasCalled(Never()) + + Equals(t, expectedOut, output) + Ok(t, err) + + }) + + t.Run("Local Runner Failure", func(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, []byte(openTofuPlanFileContents), 0600) + Ok(t, err) + + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &tfDistribution, + TerraformVersion: tfVersion, + } + extraArgs := []string{"extra", "args"} + envs := map[string]string{} + + expectedOut := "some random output" + + When(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New("err")) + + output, err := subject.Run(ctx, extraArgs, tmpDir, envs) + + mockRemoteRunner.VerifyWasCalled(Never()) + + Equals(t, expectedOut, output) + Assert(t, err != nil, "err should not be nil") + + }) + +} diff --git a/server/core/runtime/policy_check_step_runner.go b/server/core/runtime/policy_check_step_runner.go index 98e4408bcb..2987875f18 100644 --- a/server/core/runtime/policy_check_step_runner.go +++ b/server/core/runtime/policy_check_step_runner.go @@ -3,6 +3,7 @@ package runtime import ( "github.com/hashicorp/go-version" "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" ) @@ -13,7 +14,7 @@ type policyCheckStepRunner struct { } // NewPolicyCheckStepRunner creates a new step runner from an executor workflow -func NewPolicyCheckStepRunner(defaultTfVersion *version.Version, executorWorkflow VersionedExecutorWorkflow) (Runner, error) { +func NewPolicyCheckStepRunner(defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, executorWorkflow VersionedExecutorWorkflow) (Runner, error) { policyCheckStepRunner := &policyCheckStepRunner{ versionEnsurer: executorWorkflow, executor: executorWorkflow, diff --git a/server/core/runtime/post_workflow_hook_runner_test.go b/server/core/runtime/post_workflow_hook_runner_test.go index 8bab373502..3a7d9499d0 100644 --- a/server/core/runtime/post_workflow_hook_runner_test.go +++ b/server/core/runtime/post_workflow_hook_runner_test.go @@ -8,7 +8,8 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/runtime" - "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tf "github.com/runatlantis/atlantis/server/core/terraform" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" "github.com/runatlantis/atlantis/server/logging" @@ -142,8 +143,8 @@ func TestPostWorkflowHookRunner_Run(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() - When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[*version.Version]())). + terraform := tfclientmocks.NewMockClient() + When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())). ThenReturn(nil) logger := logging.NewNoopLogger(t) diff --git a/server/core/runtime/pre_workflow_hook_runner_test.go b/server/core/runtime/pre_workflow_hook_runner_test.go index 40133c10a5..b621fa3e07 100644 --- a/server/core/runtime/pre_workflow_hook_runner_test.go +++ b/server/core/runtime/pre_workflow_hook_runner_test.go @@ -9,7 +9,8 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/runtime" - "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tf "github.com/runatlantis/atlantis/server/core/terraform" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" "github.com/runatlantis/atlantis/server/logging" @@ -162,8 +163,8 @@ func TestPreWorkflowHookRunner_Run(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() - When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[*version.Version]())). + terraform := tfclientmocks.NewMockClient() + When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())). ThenReturn(nil) logger := logging.NewNoopLogger(t) diff --git a/server/core/runtime/run_step_runner.go b/server/core/runtime/run_step_runner.go index 76629ba460..20d55caee6 100644 --- a/server/core/runtime/run_step_runner.go +++ b/server/core/runtime/run_step_runner.go @@ -9,14 +9,16 @@ import ( "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/runtime/models" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/jobs" ) // RunStepRunner runs custom commands. type RunStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version // TerraformBinDir is the directory where Atlantis downloads Terraform binaries. TerraformBinDir string ProjectCmdOutputHandler jobs.ProjectCommandOutputHandler @@ -31,12 +33,16 @@ func (r *RunStepRunner) Run( streamOutput bool, postProcessOutput valid.PostProcessRunOutputOption, ) (string, error) { + tfDistribution := r.DefaultTFDistribution tfVersion := r.DefaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } - err := r.TerraformExecutor.EnsureVersion(ctx.Log, tfVersion) + err := r.TerraformExecutor.EnsureVersion(ctx.Log, tfDistribution, tfVersion) if err != nil { err = fmt.Errorf("%s: Downloading terraform Version %s", err, tfVersion.String()) ctx.Log.Debug("error: %s", err) @@ -45,27 +51,28 @@ func (r *RunStepRunner) Run( baseEnvVars := os.Environ() customEnvVars := map[string]string{ - "ATLANTIS_TERRAFORM_VERSION": tfVersion.String(), - "BASE_BRANCH_NAME": ctx.Pull.BaseBranch, - "BASE_REPO_NAME": ctx.BaseRepo.Name, - "BASE_REPO_OWNER": ctx.BaseRepo.Owner, - "COMMENT_ARGS": strings.Join(ctx.EscapedCommentArgs, ","), - "DIR": path, - "HEAD_BRANCH_NAME": ctx.Pull.HeadBranch, - "HEAD_COMMIT": ctx.Pull.HeadCommit, - "HEAD_REPO_NAME": ctx.HeadRepo.Name, - "HEAD_REPO_OWNER": ctx.HeadRepo.Owner, - "PATH": fmt.Sprintf("%s:%s", os.Getenv("PATH"), r.TerraformBinDir), - "PLANFILE": filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)), - "SHOWFILE": filepath.Join(path, ctx.GetShowResultFileName()), - "POLICYCHECKFILE": filepath.Join(path, ctx.GetPolicyCheckResultFileName()), - "PROJECT_NAME": ctx.ProjectName, - "PULL_AUTHOR": ctx.Pull.Author, - "PULL_NUM": fmt.Sprintf("%d", ctx.Pull.Num), - "PULL_URL": ctx.Pull.URL, - "REPO_REL_DIR": ctx.RepoRelDir, - "USER_NAME": ctx.User.Username, - "WORKSPACE": ctx.Workspace, + "ATLANTIS_TERRAFORM_DISTRIBUTION": tfDistribution.BinName(), + "ATLANTIS_TERRAFORM_VERSION": tfVersion.String(), + "BASE_BRANCH_NAME": ctx.Pull.BaseBranch, + "BASE_REPO_NAME": ctx.BaseRepo.Name, + "BASE_REPO_OWNER": ctx.BaseRepo.Owner, + "COMMENT_ARGS": strings.Join(ctx.EscapedCommentArgs, ","), + "DIR": path, + "HEAD_BRANCH_NAME": ctx.Pull.HeadBranch, + "HEAD_COMMIT": ctx.Pull.HeadCommit, + "HEAD_REPO_NAME": ctx.HeadRepo.Name, + "HEAD_REPO_OWNER": ctx.HeadRepo.Owner, + "PATH": fmt.Sprintf("%s:%s", os.Getenv("PATH"), r.TerraformBinDir), + "PLANFILE": filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)), + "SHOWFILE": filepath.Join(path, ctx.GetShowResultFileName()), + "POLICYCHECKFILE": filepath.Join(path, ctx.GetPolicyCheckResultFileName()), + "PROJECT_NAME": ctx.ProjectName, + "PULL_AUTHOR": ctx.Pull.Author, + "PULL_NUM": fmt.Sprintf("%d", ctx.Pull.Num), + "PULL_URL": ctx.Pull.URL, + "REPO_REL_DIR": ctx.RepoRelDir, + "USER_NAME": ctx.User.Username, + "WORKSPACE": ctx.Workspace, } finalEnvVars := baseEnvVars diff --git a/server/core/runtime/run_step_runner_test.go b/server/core/runtime/run_step_runner_test.go index 4672fa2bb0..2429d88fe8 100644 --- a/server/core/runtime/run_step_runner_test.go +++ b/server/core/runtime/run_step_runner_test.go @@ -10,7 +10,9 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/runtime" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" @@ -20,11 +22,12 @@ import ( func TestRunStepRunner_Run(t *testing.T) { cases := []struct { - Command string - ProjectName string - ExpOut string - ExpErr string - Version string + Command string + ProjectName string + ExpOut string + ExpErr string + Version string + Distribution string }{ { Command: "", @@ -69,6 +72,18 @@ func TestRunStepRunner_Run(t *testing.T) { ProjectName: "my/project/name", ExpOut: "workspace=myworkspace version=0.11.0 dir=$DIR planfile=$DIR/my::project::name-myworkspace.tfplan showfile=$DIR/my::project::name-myworkspace.json project=my/project/name\n", }, + { + Command: "echo distribution=$ATLANTIS_TERRAFORM_DISTRIBUTION", + ProjectName: "my/project/name", + ExpOut: "distribution=terraform\n", + Distribution: "terraform", + }, + { + Command: "echo distribution=$ATLANTIS_TERRAFORM_DISTRIBUTION", + ProjectName: "my/project/name", + ExpOut: "distribution=tofu\n", + Distribution: "opentofu", + }, { Command: "echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME head_commit=$HEAD_COMMIT base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_url=$PULL_URL pull_author=$PULL_AUTHOR repo_rel_dir=$REPO_REL_DIR", ExpOut: "base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat head_commit=12345abcdef base_branch_name=main pull_num=2 pull_url=https://github.com/runatlantis/atlantis/pull/2 pull_author=acme repo_rel_dir=mydir\n", @@ -100,11 +115,17 @@ func TestRunStepRunner_Run(t *testing.T) { Ok(t, err) + projTFDistribution := "terraform" + if c.Distribution != "" { + projTFDistribution = c.Distribution + } + defaultVersion, _ := version.NewVersion("0.8") RegisterMockTestingT(t) - terraform := mocks.NewMockClient() - When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[*version.Version]())). + terraform := tfclientmocks.NewMockClient() + defaultDistribution := tf.NewDistributionTerraformWithDownloader(mocks.NewMockDownloader()) + When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())). ThenReturn(nil) logger := logging.NewNoopLogger(t) @@ -113,6 +134,7 @@ func TestRunStepRunner_Run(t *testing.T) { r := runtime.RunStepRunner{ TerraformExecutor: terraform, + DefaultTFDistribution: defaultDistribution, DefaultTFVersion: defaultVersion, TerraformBinDir: "/bin/dir", ProjectCmdOutputHandler: projectCmdOutputHandler, @@ -138,12 +160,13 @@ func TestRunStepRunner_Run(t *testing.T) { User: models.User{ Username: "acme-user", }, - Log: logger, - Workspace: "myworkspace", - RepoRelDir: "mydir", - TerraformVersion: projVersion, - ProjectName: c.ProjectName, - EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"}, + Log: logger, + Workspace: "myworkspace", + RepoRelDir: "mydir", + TerraformDistribution: &projTFDistribution, + TerraformVersion: projVersion, + ProjectName: c.ProjectName, + EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"}, } out, err := r.Run(ctx, nil, c.Command, tmpDir, map[string]string{"test": "var"}, true, valid.PostProcessRunOutputShow) if c.ExpErr != "" { @@ -157,8 +180,8 @@ func TestRunStepRunner_Run(t *testing.T) { expOut := strings.Replace(c.ExpOut, "$DIR", tmpDir, -1) Equals(t, expOut, out) - terraform.VerifyWasCalledOnce().EnsureVersion(logger, projVersion) - terraform.VerifyWasCalled(Never()).EnsureVersion(logger, defaultVersion) + terraform.VerifyWasCalledOnce().EnsureVersion(Eq(logger), NotEq(defaultDistribution), Eq(projVersion)) + terraform.VerifyWasCalled(Never()).EnsureVersion(Eq(logger), Eq(defaultDistribution), Eq(defaultVersion)) }) } diff --git a/server/core/runtime/runtime.go b/server/core/runtime/runtime.go index 52fc5180eb..35e571262b 100644 --- a/server/core/runtime/runtime.go +++ b/server/core/runtime/runtime.go @@ -11,6 +11,7 @@ import ( version "github.com/hashicorp/go-version" "github.com/pkg/errors" runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -26,8 +27,8 @@ const ( // TerraformExec brings the interface from TerraformClient into this package // without causing circular imports. type TerraformExec interface { - RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error) - EnsureVersion(log logging.SimpleLogging, v *version.Version) error + RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error) + EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error } // AsyncTFExec brings the interface from TerraformClient into this package @@ -43,7 +44,7 @@ type AsyncTFExec interface { // Callers can use the input channel to pass stdin input to the command. // If any error is passed on the out channel, there will be no // further output (so callers are free to exit). - RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan runtimemodels.Line) + RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (chan<- string, <-chan runtimemodels.Line) } // StatusUpdater brings the interface from CommitStatusUpdater into this package diff --git a/server/core/runtime/show_step_runner.go b/server/core/runtime/show_step_runner.go index ba89479b56..ed346bc184 100644 --- a/server/core/runtime/show_step_runner.go +++ b/server/core/runtime/show_step_runner.go @@ -6,15 +6,17 @@ import ( "github.com/hashicorp/go-version" "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" ) const minimumShowTfVersion string = "0.12.0" -func NewShowStepRunner(executor TerraformExec, defaultTFVersion *version.Version) (Runner, error) { +func NewShowStepRunner(executor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTFVersion *version.Version) (Runner, error) { showStepRunner := &showStepRunner{ - terraformExecutor: executor, - defaultTFVersion: defaultTFVersion, + terraformExecutor: executor, + defaultTfDistribution: defaultTfDistribution, + defaultTFVersion: defaultTFVersion, } remotePlanRunner := NullRunner{} runner := NewPlanTypeStepRunnerDelegate(showStepRunner, remotePlanRunner) @@ -23,12 +25,17 @@ func NewShowStepRunner(executor TerraformExec, defaultTFVersion *version.Version // showStepRunner runs terraform show on an existing plan file and outputs it to a json file type showStepRunner struct { - terraformExecutor TerraformExec - defaultTFVersion *version.Version + terraformExecutor TerraformExec + defaultTfDistribution terraform.Distribution + defaultTFVersion *version.Version } func (p *showStepRunner) Run(ctx command.ProjectContext, _ []string, path string, envs map[string]string) (string, error) { + tfDistribution := p.defaultTfDistribution tfVersion := p.defaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } @@ -41,6 +48,7 @@ func (p *showStepRunner) Run(ctx command.ProjectContext, _ []string, path string path, []string{"show", "-json", filepath.Clean(planFile)}, envs, + tfDistribution, tfVersion, ctx.Workspace, ) diff --git a/server/core/runtime/show_step_runner_test.go b/server/core/runtime/show_step_runner_test.go index 9803efb9ff..8c390014ad 100644 --- a/server/core/runtime/show_step_runner_test.go +++ b/server/core/runtime/show_step_runner_test.go @@ -9,7 +9,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -20,6 +22,8 @@ func TestShowStepRunnner(t *testing.T) { path := t.TempDir() resultPath := filepath.Join(path, "test-default.json") envs := map[string]string{"key": "val"} + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.12") context := command.ProjectContext{ Workspace: "default", @@ -29,17 +33,18 @@ func TestShowStepRunnner(t *testing.T) { RegisterMockTestingT(t) - mockExecutor := mocks.NewMockClient() + mockExecutor := tfclientmocks.NewMockClient() subject := showStepRunner{ - terraformExecutor: mockExecutor, - defaultTFVersion: tfVersion, + terraformExecutor: mockExecutor, + defaultTfDistribution: tfDistribution, + defaultTFVersion: tfVersion, } t.Run("success", func(t *testing.T) { When(mockExecutor.RunCommandWithVersion( - context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace, + context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfDistribution, tfVersion, context.Workspace, )).ThenReturn("success", nil) r, err := subject.Run(context, []string{}, path, envs) @@ -57,6 +62,8 @@ func TestShowStepRunnner(t *testing.T) { t.Run("success w/ version override", func(t *testing.T) { v, _ := version.NewVersion("0.13.0") + mockDownloader := mocks.NewMockDownloader() + d := tf.NewDistributionTerraformWithDownloader(mockDownloader) contextWithVersionOverride := command.ProjectContext{ Workspace: "default", @@ -66,7 +73,7 @@ func TestShowStepRunnner(t *testing.T) { } When(mockExecutor.RunCommandWithVersion( - contextWithVersionOverride, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, v, context.Workspace, + contextWithVersionOverride, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, d, v, context.Workspace, )).ThenReturn("success", nil) r, err := subject.Run(contextWithVersionOverride, []string{}, path, envs) @@ -81,9 +88,39 @@ func TestShowStepRunnner(t *testing.T) { }) + t.Run("success w/ distribution override", func(t *testing.T) { + + v, _ := version.NewVersion("0.13.0") + mockDownloader := mocks.NewMockDownloader() + d := tf.NewDistributionTerraformWithDownloader(mockDownloader) + projTFDistribution := "opentofu" + + contextWithDistributionOverride := command.ProjectContext{ + Workspace: "default", + ProjectName: "test", + Log: logger, + TerraformDistribution: &projTFDistribution, + } + + When(mockExecutor.RunCommandWithVersion( + Eq(contextWithDistributionOverride), Eq(path), Eq([]string{"show", "-json", filepath.Join(path, "test-default.tfplan")}), Eq(envs), NotEq(d), NotEq(v), Eq(context.Workspace), + )).ThenReturn("success", nil) + + r, err := subject.Run(contextWithDistributionOverride, []string{}, path, envs) + + Ok(t, err) + + actual, _ := os.ReadFile(resultPath) + + actualStr := string(actual) + Assert(t, actualStr == "success", "got expected result") + Assert(t, r == "success", "returned expected result") + + }) + t.Run("failure running command", func(t *testing.T) { When(mockExecutor.RunCommandWithVersion( - context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace, + context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfDistribution, tfVersion, context.Workspace, )).ThenReturn("success", errors.New("error")) _, err := subject.Run(context, []string{}, path, envs) diff --git a/server/core/runtime/state_rm_step_runner.go b/server/core/runtime/state_rm_step_runner.go index 3b4a08f102..42af97c006 100644 --- a/server/core/runtime/state_rm_step_runner.go +++ b/server/core/runtime/state_rm_step_runner.go @@ -5,25 +5,32 @@ import ( "path/filepath" version "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/utils" ) type stateRmStepRunner struct { - terraformExecutor TerraformExec - defaultTFVersion *version.Version + terraformExecutor TerraformExec + defaultTFDistribution terraform.Distribution + defaultTFVersion *version.Version } -func NewStateRmStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version) Runner { +func NewStateRmStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version) Runner { runner := &stateRmStepRunner{ - terraformExecutor: terraformExecutor, - defaultTFVersion: defaultTfVersion, + terraformExecutor: terraformExecutor, + defaultTFDistribution: defaultTfDistribution, + defaultTFVersion: defaultTfVersion, } - return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner) + return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner) } func (p *stateRmStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { + tfDistribution := p.defaultTFDistribution tfVersion := p.defaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } @@ -31,7 +38,7 @@ func (p *stateRmStepRunner) Run(ctx command.ProjectContext, extraArgs []string, stateRmCmd := []string{"state", "rm"} stateRmCmd = append(stateRmCmd, extraArgs...) stateRmCmd = append(stateRmCmd, ctx.EscapedCommentArgs...) - out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), stateRmCmd, envs, tfVersion, ctx.Workspace) + out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), stateRmCmd, envs, tfDistribution, tfVersion, ctx.Workspace) // If the state rm was successful and a plan file exists, delete the plan. planPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)) diff --git a/server/core/runtime/state_rm_step_runner_test.go b/server/core/runtime/state_rm_step_runner_test.go index df5e1036e8..194879f2bd 100644 --- a/server/core/runtime/state_rm_step_runner_test.go +++ b/server/core/runtime/state_rm_step_runner_test.go @@ -8,7 +8,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -29,17 +31,19 @@ func TestStateRmStepRunner_Run_Success(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() tfVersion, _ := version.NewVersion("0.15.0") - s := NewStateRmStepRunner(terraform, tfVersion) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + s := NewStateRmStepRunner(terraform, tfDistribution, tfVersion) - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, "default") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -59,23 +63,67 @@ func TestStateRmStepRunner_Run_Workspace(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() tfVersion, _ := version.NewVersion("0.15.0") - s := NewStateRmStepRunner(terraform, tfVersion) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + s := NewStateRmStepRunner(terraform, tfDistribution, tfVersion) - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) // switch workspace - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfVersion, workspace) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfDistribution, tfVersion, workspace) // exec state rm commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace) + + _, err = os.Stat(planPath) + Assert(t, os.IsNotExist(err), "planfile should be deleted") +} + +func TestStateRmStepRunner_Run_UsesConfiguredDistribution(t *testing.T) { + logger := logging.NewNoopLogger(t) + workspace := "something" + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, fmt.Sprintf("%s.tfplan", workspace)) + err := os.WriteFile(planPath, nil, 0600) + Ok(t, err) + + projTFDistribution := "opentofu" + + context := command.ProjectContext{ + Log: logger, + EscapedCommentArgs: []string{"-lock=false", "addr1", "addr2", "addr3"}, + Workspace: workspace, + TerraformDistribution: &projTFDistribution, + } + + RegisterMockTestingT(t) + terraform := tfclientmocks.NewMockClient() + tfVersion, _ := version.NewVersion("0.15.0") + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + s := NewStateRmStepRunner(terraform, tfDistribution, tfVersion) + + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). + ThenReturn("output", nil) + output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) + Ok(t, err) + Equals(t, "output", output) + + // switch workspace + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "show"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "select", workspace}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + + // exec state rm + commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"} + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq(commands), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") diff --git a/server/core/runtime/version_step_runner.go b/server/core/runtime/version_step_runner.go index c75c5396fb..db1f525743 100644 --- a/server/core/runtime/version_step_runner.go +++ b/server/core/runtime/version_step_runner.go @@ -4,22 +4,28 @@ import ( "path/filepath" "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" ) // VersionStepRunner runs a version command given a ctx type VersionStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version } // Run ensures a given version for the executable, builds the args from the project context and then runs executable returning the result func (v *VersionStepRunner) Run(ctx command.ProjectContext, _ []string, path string, envs map[string]string) (string, error) { + tfDistribution := v.DefaultTFDistribution tfVersion := v.DefaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } versionCmd := []string{"version"} - return v.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), versionCmd, envs, tfVersion, ctx.Workspace) + return v.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), versionCmd, envs, tfDistribution, tfVersion, ctx.Workspace) } diff --git a/server/core/runtime/version_step_runner_test.go b/server/core/runtime/version_step_runner_test.go index 55c4fc05f4..45bf890fab 100644 --- a/server/core/runtime/version_step_runner_test.go +++ b/server/core/runtime/version_step_runner_test.go @@ -5,7 +5,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -33,18 +35,62 @@ func TestRunVersionStep(t *testing.T) { }, } - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.15.0") tmpDir := t.TempDir() s := &VersionStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } t.Run("ensure runs", func(t *testing.T) { _, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"version"}, map[string]string(nil), tfVersion, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"version"}, map[string]string(nil), tfDistribution, tfVersion, "default") + Ok(t, err) + }) +} + +func TestVersionStepRunner_Run_UsesConfiguredDistribution(t *testing.T) { + RegisterMockTestingT(t) + logger := logging.NewNoopLogger(t) + workspace := "default" + projTFDistribution := "opentofu" + context := command.ProjectContext{ + Log: logger, + EscapedCommentArgs: []string{"comment", "args"}, + Workspace: workspace, + RepoRelDir: ".", + User: models.User{Username: "username"}, + Pull: models.PullRequest{ + Num: 2, + }, + BaseRepo: models.Repo{ + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + }, + TerraformDistribution: &projTFDistribution, + } + + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + tfVersion, _ := version.NewVersion("0.15.0") + tmpDir := t.TempDir() + + s := &VersionStepRunner{ + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, + } + + t.Run("ensure runs", func(t *testing.T) { + _, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"version"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq("default")) Ok(t, err) }) } diff --git a/server/core/runtime/workspace_step_runner_delegate.go b/server/core/runtime/workspace_step_runner_delegate.go index 9d77db44d0..5628a6a351 100644 --- a/server/core/runtime/workspace_step_runner_delegate.go +++ b/server/core/runtime/workspace_step_runner_delegate.go @@ -5,33 +5,40 @@ import ( "strings" "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" ) // workspaceStepRunnerDelegate ensures that a given step runner run on switched workspace type workspaceStepRunnerDelegate struct { - terraformExecutor TerraformExec - defaultTfVersion *version.Version - delegate Runner + terraformExecutor TerraformExec + defaultTfDistribution terraform.Distribution + defaultTfVersion *version.Version + delegate Runner } -func NewWorkspaceStepRunnerDelegate(terraformExecutor TerraformExec, defaultTfVersion *version.Version, delegate Runner) Runner { +func NewWorkspaceStepRunnerDelegate(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, delegate Runner) Runner { return &workspaceStepRunnerDelegate{ - terraformExecutor: terraformExecutor, - defaultTfVersion: defaultTfVersion, - delegate: delegate, + terraformExecutor: terraformExecutor, + defaultTfDistribution: defaultTfDistribution, + defaultTfVersion: defaultTfVersion, + delegate: delegate, } } func (r *workspaceStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { + tfDistribution := r.defaultTfDistribution tfVersion := r.defaultTfVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } // We only need to switch workspaces in version 0.9.*. In older versions, // there is no such thing as a workspace so we don't need to do anything. - if err := r.switchWorkspace(ctx, path, tfVersion, envs); err != nil { + if err := r.switchWorkspace(ctx, path, tfDistribution, tfVersion, envs); err != nil { return "", err } @@ -40,7 +47,7 @@ func (r *workspaceStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs // switchWorkspace changes the terraform workspace if necessary and will create // it if it doesn't exist. It handles differences between versions. -func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext, path string, tfVersion *version.Version, envs map[string]string) error { +func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext, path string, tfDistribution terraform.Distribution, tfVersion *version.Version, envs map[string]string) error { // In versions less than 0.9 there is no support for workspaces. noWorkspaceSupport := MustConstraint("<0.9").Check(tfVersion) // If the user tried to set a specific workspace in the comment but their @@ -63,7 +70,7 @@ func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext // already in the right workspace then no need to switch. This will save us // about ten seconds. This command is only available in > 0.10. if !runningZeroPointNine { - workspaceShowOutput, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "show"}, envs, tfVersion, ctx.Workspace) + workspaceShowOutput, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "show"}, envs, tfDistribution, tfVersion, ctx.Workspace) if err != nil { return err } @@ -78,11 +85,11 @@ func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext // To do this we can either select and catch the error or use list and then // look for the workspace. Both commands take the same amount of time so // that's why we're running select here. - _, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "select", ctx.Workspace}, envs, tfVersion, ctx.Workspace) + _, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "select", ctx.Workspace}, envs, tfDistribution, tfVersion, ctx.Workspace) if err != nil { // If terraform workspace select fails we run terraform workspace // new to create a new workspace automatically. - out, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "new", ctx.Workspace}, envs, tfVersion, ctx.Workspace) + out, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "new", ctx.Workspace}, envs, tfDistribution, tfVersion, ctx.Workspace) if err != nil { return fmt.Errorf("%s: %s", err, out) } diff --git a/server/core/runtime/workspace_step_runner_delegate_test.go b/server/core/runtime/workspace_step_runner_delegate_test.go index 2ef3032d50..e705e93b00 100644 --- a/server/core/runtime/workspace_step_runner_delegate_test.go +++ b/server/core/runtime/workspace_step_runner_delegate_test.go @@ -6,7 +6,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -16,7 +18,9 @@ import ( func TestRun_NoWorkspaceIn08(t *testing.T) { // We don't want any workspace commands to be run in 0.8. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.8") workspace := "default" logger := logging.NewNoopLogger(t) @@ -24,7 +28,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { Log: logger, Workspace: workspace, } - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) _, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) @@ -36,6 +40,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { "select", "workspace"}, map[string]string(nil), + tfDistribution, tfVersion, workspace) terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, @@ -44,6 +49,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { "select", "workspace"}, map[string]string(nil), + tfDistribution, tfVersion, workspace) } @@ -52,11 +58,13 @@ func TestRun_ErrWorkspaceIn08(t *testing.T) { // If they attempt to use a workspace other than default in 0.8 // we should error. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.8") logger := logging.NewNoopLogger(t) workspace := "notdefault" - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) _, err := s.Run(command.ProjectContext{ Log: logger, @@ -67,6 +75,8 @@ func TestRun_ErrWorkspaceIn08(t *testing.T) { func TestRun_SwitchesWorkspace(t *testing.T) { RegisterMockTestingT(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) cases := []struct { tfVersion string @@ -92,14 +102,14 @@ func TestRun_SwitchesWorkspace(t *testing.T) { for _, c := range cases { t.Run(c.tfVersion, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() tfVersion, _ := version.NewVersion(c.tfVersion) logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ Log: logger, Workspace: "workspace", } - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) _, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) @@ -111,12 +121,74 @@ func TestRun_SwitchesWorkspace(t *testing.T) { "select", "workspace"}, map[string]string(nil), + tfDistribution, tfVersion, "workspace") }) } } +func TestRun_SwitchesWorkspaceDistribution(t *testing.T) { + RegisterMockTestingT(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + + cases := []struct { + tfVersion string + tfDistribution string + expWorkspaceCmd string + }{ + { + "0.9.0", + "opentofu", + "env", + }, + { + "0.9.11", + "terraform", + "env", + }, + { + "0.10.0", + "terraform", + "workspace", + }, + { + "0.11.0", + "opentofu", + "workspace", + }, + } + + for _, c := range cases { + t.Run(c.tfVersion, func(t *testing.T) { + terraform := tfclientmocks.NewMockClient() + tfVersion, _ := version.NewVersion(c.tfVersion) + logger := logging.NewNoopLogger(t) + ctx := command.ProjectContext{ + Log: logger, + Workspace: "workspace", + TerraformDistribution: &c.tfDistribution, + } + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) + + _, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) + Ok(t, err) + + // Verify that env select was called as well as plan. + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), + Eq("/path"), + Eq([]string{c.expWorkspaceCmd, + "select", + "workspace"}), + Eq(map[string]string(nil)), + NotEq(tfDistribution), + Eq(tfVersion), + Eq("workspace")) + }) + } +} + func TestRun_CreatesWorkspace(t *testing.T) { // Test that if `workspace select` fails, we call `workspace new`. RegisterMockTestingT(t) @@ -145,7 +217,9 @@ func TestRun_CreatesWorkspace(t *testing.T) { for _, c := range cases { t.Run(c.tfVersion, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion(c.tfVersion) logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -163,20 +237,20 @@ func TestRun_CreatesWorkspace(t *testing.T) { Name: "repo", }, } - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) // Ensure that we actually try to switch workspaces by making the // output of `workspace show` to be a different name. - When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil) + When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil) expWorkspaceArgs := []string{c.expWorkspaceCommand, "select", "workspace"} - When(terraform.RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist")) + When(terraform.RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist")) _, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) // Verify that env select was called as well as plan. - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") }) } } @@ -185,7 +259,9 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) { // Tests that if workspace show says we're on the right workspace we don't // switch. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -203,12 +279,12 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) { Name: "repo", }, } - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) - When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) + When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("workspace\n", nil) _, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) // Verify that workspace select was never called. - terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, "/path", []string{"workspace", "select", "workspace"}, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, "/path", []string{"workspace", "select", "workspace"}, map[string]string(nil), tfDistribution, tfVersion, "workspace") } diff --git a/server/events/terraform/ansi/strip.go b/server/core/terraform/ansi/strip.go similarity index 100% rename from server/events/terraform/ansi/strip.go rename to server/core/terraform/ansi/strip.go diff --git a/server/events/terraform/ansi/strip_test.go b/server/core/terraform/ansi/strip_test.go similarity index 100% rename from server/events/terraform/ansi/strip_test.go rename to server/core/terraform/ansi/strip_test.go diff --git a/server/core/terraform/distribution.go b/server/core/terraform/distribution.go index 0fd781765d..dbeaf6a46b 100644 --- a/server/core/terraform/distribution.go +++ b/server/core/terraform/distribution.go @@ -18,6 +18,14 @@ type Distribution interface { ResolveConstraint(context.Context, string) (*version.Version, error) } +func NewDistribution(distribution string) Distribution { + tfDistribution := NewDistributionTerraform() + if distribution == "opentofu" { + tfDistribution = NewDistributionOpenTofu() + } + return tfDistribution +} + type DistributionOpenTofu struct { downloader Downloader } diff --git a/server/core/terraform/mocks/mock_terraform_client.go b/server/core/terraform/tfclient/mocks/mock_terraform_client.go similarity index 79% rename from server/core/terraform/mocks/mock_terraform_client.go rename to server/core/terraform/tfclient/mocks/mock_terraform_client.go index 279de1a751..9dca6ffd4b 100644 --- a/server/core/terraform/mocks/mock_terraform_client.go +++ b/server/core/terraform/tfclient/mocks/mock_terraform_client.go @@ -1,11 +1,12 @@ // Code generated by pegomock. DO NOT EDIT. -// Source: github.com/runatlantis/atlantis/server/core/terraform (interfaces: Client) +// Source: github.com/runatlantis/atlantis/server/core/terraform/tfclient (interfaces: Client) package mocks import ( go_version "github.com/hashicorp/go-version" pegomock "github.com/petergtz/pegomock/v4" + terraform "github.com/runatlantis/atlantis/server/core/terraform" command "github.com/runatlantis/atlantis/server/events/command" logging "github.com/runatlantis/atlantis/server/logging" "reflect" @@ -42,11 +43,11 @@ func (mock *MockClient) DetectVersion(log logging.SimpleLogging, projectDirector return _ret0 } -func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.Version) error { +func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *go_version.Version) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") } - _params := []pegomock.Param{log, v} + _params := []pegomock.Param{log, d, v} _result := pegomock.GetGenericMockFrom(mock).Invoke("EnsureVersion", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) var _ret0 error if len(_result) != 0 { @@ -57,11 +58,11 @@ func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.V return _ret0 } -func (mock *MockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (string, error) { +func (mock *MockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") } - _params := []pegomock.Param{ctx, path, args, envs, v, workspace} + _params := []pegomock.Param{ctx, path, args, envs, d, v, workspace} _result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandWithVersion", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var _ret0 string var _ret1 error @@ -148,8 +149,8 @@ func (c *MockClient_DetectVersion_OngoingVerification) GetAllCapturedArguments() return } -func (verifier *VerifierMockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.Version) *MockClient_EnsureVersion_OngoingVerification { - _params := []pegomock.Param{log, v} +func (verifier *VerifierMockClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *go_version.Version) *MockClient_EnsureVersion_OngoingVerification { + _params := []pegomock.Param{log, d, v} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "EnsureVersion", _params, verifier.timeout) return &MockClient_EnsureVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -159,12 +160,12 @@ type MockClient_EnsureVersion_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockClient_EnsureVersion_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, *go_version.Version) { - log, v := c.GetAllCapturedArguments() - return log[len(log)-1], v[len(v)-1] +func (c *MockClient_EnsureVersion_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, terraform.Distribution, *go_version.Version) { + log, d, v := c.GetAllCapturedArguments() + return log[len(log)-1], d[len(d)-1], v[len(v)-1] } -func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []*go_version.Version) { +func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []terraform.Distribution, _param2 []*go_version.Version) { _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(_params) > 0 { if len(_params) > 0 { @@ -174,17 +175,23 @@ func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() } } if len(_params) > 1 { - _param1 = make([]*go_version.Version, len(c.methodInvocations)) + _param1 = make([]terraform.Distribution, len(c.methodInvocations)) for u, param := range _params[1] { - _param1[u] = param.(*go_version.Version) + _param1[u] = param.(terraform.Distribution) + } + } + if len(_params) > 2 { + _param2 = make([]*go_version.Version, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(*go_version.Version) } } } return } -func (verifier *VerifierMockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification { - _params := []pegomock.Param{ctx, path, args, envs, v, workspace} +func (verifier *VerifierMockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification { + _params := []pegomock.Param{ctx, path, args, envs, d, v, workspace} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandWithVersion", _params, verifier.timeout) return &MockClient_RunCommandWithVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -194,12 +201,12 @@ type MockClient_RunCommandWithVersion_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, *go_version.Version, string) { - ctx, path, args, envs, v, workspace := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], v[len(v)-1], workspace[len(workspace)-1] +func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, terraform.Distribution, *go_version.Version, string) { + ctx, path, args, envs, d, v, workspace := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], d[len(d)-1], v[len(v)-1], workspace[len(workspace)-1] } -func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []*go_version.Version, _param5 []string) { +func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []terraform.Distribution, _param5 []*go_version.Version, _param6 []string) { _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(_params) > 0 { if len(_params) > 0 { @@ -227,15 +234,21 @@ func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArg } } if len(_params) > 4 { - _param4 = make([]*go_version.Version, len(c.methodInvocations)) + _param4 = make([]terraform.Distribution, len(c.methodInvocations)) for u, param := range _params[4] { - _param4[u] = param.(*go_version.Version) + _param4[u] = param.(terraform.Distribution) } } if len(_params) > 5 { - _param5 = make([]string, len(c.methodInvocations)) + _param5 = make([]*go_version.Version, len(c.methodInvocations)) for u, param := range _params[5] { - _param5[u] = param.(string) + _param5[u] = param.(*go_version.Version) + } + } + if len(_params) > 6 { + _param6 = make([]string, len(c.methodInvocations)) + for u, param := range _params[6] { + _param6[u] = param.(string) } } } diff --git a/server/core/terraform/terraform_client.go b/server/core/terraform/tfclient/terraform_client.go similarity index 91% rename from server/core/terraform/terraform_client.go rename to server/core/terraform/tfclient/terraform_client.go index d01525704b..5ef864db79 100644 --- a/server/core/terraform/terraform_client.go +++ b/server/core/terraform/tfclient/terraform_client.go @@ -13,8 +13,8 @@ // limitations under the License. // Modified hereafter by contributors to runatlantis/atlantis. // -// Package terraform handles the actual running of terraform commands. -package terraform +// Package tfclient handles the actual running of terraform commands. +package tfclient import ( "context" @@ -33,8 +33,9 @@ import ( "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/runtime/models" + "github.com/runatlantis/atlantis/server/core/terraform" + "github.com/runatlantis/atlantis/server/core/terraform/ansi" "github.com/runatlantis/atlantis/server/events/command" - "github.com/runatlantis/atlantis/server/events/terraform/ansi" "github.com/runatlantis/atlantis/server/jobs" "github.com/runatlantis/atlantis/server/logging" ) @@ -47,10 +48,10 @@ type Client interface { // RunCommandWithVersion executes terraform with args in path. If v is nil, // it will use the default Terraform version. workspace is the Terraform // workspace which should be set as an environment variable. - RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error) + RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error) // EnsureVersion makes sure that terraform version `v` is available to use - EnsureVersion(log logging.SimpleLogging, v *version.Version) error + EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error // DetectVersion Extracts required_version from Terraform configuration in the specified project directory. Returns nil if unable to determine the version. DetectVersion(log logging.SimpleLogging, projectDirectory string) *version.Version @@ -58,7 +59,7 @@ type Client interface { type DefaultClient struct { // Distribution handles logic specific to the TF distribution being used by Atlantis - distribution Distribution + distribution terraform.Distribution // defaultVersion is the default version of terraform to use if another // version isn't specified. @@ -102,7 +103,7 @@ var versionRegex = regexp.MustCompile("(?:Terraform|OpenTofu) v(.*?)(\\s.*)?\n") // NewClientWithDefaultVersion creates a new terraform client and pre-fetches the default version func NewClientWithDefaultVersion( log logging.SimpleLogging, - distribution Distribution, + distribution terraform.Distribution, binDir string, cacheDir string, tfeToken string, @@ -189,7 +190,7 @@ func NewClientWithDefaultVersion( func NewTestClient( log logging.SimpleLogging, - distribution Distribution, + distribution terraform.Distribution, binDir string, cacheDir string, tfeToken string, @@ -227,7 +228,7 @@ func NewTestClient( // Will asynchronously download the required version if it doesn't exist already. func NewClient( log logging.SimpleLogging, - distribution Distribution, + distribution terraform.Distribution, binDir string, cacheDir string, tfeToken string, @@ -256,6 +257,10 @@ func NewClient( ) } +func (c *DefaultClient) DefaultDistribution() terraform.Distribution { + return c.distribution +} + // Version returns the default version of Terraform we use if no other version // is defined. func (c *DefaultClient) DefaultVersion() *version.Version { @@ -326,14 +331,14 @@ func (c *DefaultClient) DetectVersion(log logging.SimpleLogging, projectDirector } // See Client.EnsureVersion. -func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, v *version.Version) error { +func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error { if v == nil { v = c.defaultVersion } var err error c.versionsLock.Lock() - _, err = ensureVersion(log, c.distribution, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed) + _, err = ensureVersion(log, d, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed) c.versionsLock.Unlock() if err != nil { return err @@ -343,9 +348,9 @@ func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, v *version.Vers } // See Client.RunCommandWithVersion. -func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (string, error) { +func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error) { if isAsyncEligibleCommand(args[0]) { - _, outCh := c.RunCommandAsync(ctx, path, args, customEnvVars, v, workspace) + _, outCh := c.RunCommandAsync(ctx, path, args, customEnvVars, d, v, workspace) var lines []string var err error @@ -362,7 +367,7 @@ func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path s output = ansi.Strip(output) return fmt.Sprintf("%s\n", output), err } - tfCmd, cmd, err := c.prepExecCmd(ctx.Log, v, workspace, path, args) + tfCmd, cmd, err := c.prepExecCmd(ctx.Log, d, v, workspace, path, args) if err != nil { return "", err } @@ -388,8 +393,8 @@ func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path s // prepExecCmd builds a ready to execute command based on the version of terraform // v, and args. It returns a printable representation of the command that will // be run and the actual command. -func (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, v *version.Version, workspace string, path string, args []string) (string, *exec.Cmd, error) { - tfCmd, envVars, err := c.prepCmd(log, v, workspace, path, args) +func (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, d terraform.Distribution, v *version.Version, workspace string, path string, args []string) (string, *exec.Cmd, error) { + tfCmd, envVars, err := c.prepCmd(log, d, v, workspace, path, args) if err != nil { return "", nil, err } @@ -401,7 +406,8 @@ func (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, v *version.Versio // prepCmd prepares a shell command (to be interpreted with `sh -c `) and set of environment // variables for running terraform. -func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, workspace string, path string, args []string) (string, []string, error) { +func (c *DefaultClient) prepCmd(log logging.SimpleLogging, d terraform.Distribution, v *version.Version, workspace string, path string, args []string) (string, []string, error) { + if v == nil { v = c.defaultVersion } @@ -413,7 +419,7 @@ func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, w } else { var err error c.versionsLock.Lock() - binPath, err = ensureVersion(log, c.distribution, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed) + binPath, err = ensureVersion(log, d, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed) c.versionsLock.Unlock() if err != nil { return "", nil, err @@ -446,8 +452,8 @@ func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, w // Callers can use the input channel to pass stdin input to the command. // If any error is passed on the out channel, there will be no // further output (so callers are free to exit). -func (c *DefaultClient) RunCommandAsync(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (chan<- string, <-chan models.Line) { - cmd, envVars, err := c.prepCmd(ctx.Log, v, workspace, path, args) +func (c *DefaultClient) RunCommandAsync(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, d terraform.Distribution, v *version.Version, workspace string) (chan<- string, <-chan models.Line) { + cmd, envVars, err := c.prepCmd(ctx.Log, d, v, workspace, path, args) if err != nil { // The signature of `RunCommandAsync` doesn't provide for returning an immediate error, only one // once reading the output. Since we won't be spawning a process, simulate that by sending the @@ -486,7 +492,7 @@ func MustConstraint(v string) version.Constraints { // It will download this version if we don't have it. func ensureVersion( log logging.SimpleLogging, - dist Distribution, + dist terraform.Distribution, versions map[string]string, v *version.Version, binDir string, diff --git a/server/core/terraform/terraform_client_internal_test.go b/server/core/terraform/tfclient/terraform_client_internal_test.go similarity index 88% rename from server/core/terraform/terraform_client_internal_test.go rename to server/core/terraform/tfclient/terraform_client_internal_test.go index f92a3fd2d2..9cde70e399 100644 --- a/server/core/terraform/terraform_client_internal_test.go +++ b/server/core/terraform/tfclient/terraform_client_internal_test.go @@ -1,4 +1,4 @@ -package terraform +package tfclient import ( "fmt" @@ -10,6 +10,8 @@ import ( version "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models" + "github.com/runatlantis/atlantis/server/core/terraform" + terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" @@ -120,7 +122,9 @@ func TestDefaultClient_RunCommandWithVersion_EnvVars(t *testing.T) { "DIR=$DIR", } customEnvVars := map[string]string{} - out, err := client.RunCommandWithVersion(ctx, tmp, args, customEnvVars, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + out, err := client.RunCommandWithVersion(ctx, tmp, args, customEnvVars, distribution, nil, "workspace") Ok(t, err) exp := fmt.Sprintf("TF_IN_AUTOMATION=true TF_PLUGIN_CACHE_DIR=%s WORKSPACE=workspace ATLANTIS_TERRAFORM_VERSION=0.11.11 DIR=%s\n", tmp, tmp) Equals(t, exp, out) @@ -163,7 +167,9 @@ func TestDefaultClient_RunCommandWithVersion_Error(t *testing.T) { "exit", "1", } - out, err := client.RunCommandWithVersion(ctx, tmp, args, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + out, err := client.RunCommandWithVersion(ctx, tmp, args, map[string]string{}, distribution, nil, "workspace") ErrEquals(t, fmt.Sprintf(`running 'echo dying && exit 1' in '%s': exit status 1`, tmp), err) // Test that we still get our output. Equals(t, "dying\n", out) @@ -209,7 +215,9 @@ func TestDefaultClient_RunCommandAsync_Success(t *testing.T) { "ATLANTIS_TERRAFORM_VERSION=$ATLANTIS_TERRAFORM_VERSION", "DIR=$DIR", } - _, outCh := client.RunCommandAsync(ctx, tmp, args, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + _, outCh := client.RunCommandAsync(ctx, tmp, args, map[string]string{}, distribution, nil, "workspace") out, err := waitCh(outCh) Ok(t, err) @@ -261,7 +269,9 @@ func TestDefaultClient_RunCommandAsync_BigOutput(t *testing.T) { _, err = f.WriteString(s) Ok(t, err) } - _, outCh := client.RunCommandAsync(ctx, tmp, []string{filename}, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + _, outCh := client.RunCommandAsync(ctx, tmp, []string{filename}, map[string]string{}, distribution, nil, "workspace") out, err := waitCh(outCh) Ok(t, err) @@ -301,7 +311,9 @@ func TestDefaultClient_RunCommandAsync_StderrOutput(t *testing.T) { overrideTF: "echo", projectCmdOutputHandler: projectCmdOutputHandler, } - _, outCh := client.RunCommandAsync(ctx, tmp, []string{"stderr", ">&2"}, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + _, outCh := client.RunCommandAsync(ctx, tmp, []string{"stderr", ">&2"}, map[string]string{}, distribution, nil, "workspace") out, err := waitCh(outCh) Ok(t, err) @@ -341,7 +353,9 @@ func TestDefaultClient_RunCommandAsync_ExitOne(t *testing.T) { overrideTF: "echo", projectCmdOutputHandler: projectCmdOutputHandler, } - _, outCh := client.RunCommandAsync(ctx, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + _, outCh := client.RunCommandAsync(ctx, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, distribution, nil, "workspace") out, err := waitCh(outCh) ErrEquals(t, fmt.Sprintf(`running 'sh -c' 'echo dying && exit 1' in '%s': exit status 1`, tmp), err) @@ -383,7 +397,9 @@ func TestDefaultClient_RunCommandAsync_Input(t *testing.T) { projectCmdOutputHandler: projectCmdOutputHandler, } - inCh, outCh := client.RunCommandAsync(ctx, tmp, []string{"a", "&&", "echo", "$a"}, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + inCh, outCh := client.RunCommandAsync(ctx, tmp, []string{"a", "&&", "echo", "$a"}, map[string]string{}, distribution, nil, "workspace") inCh <- "echo me\n" out, err := waitCh(outCh) diff --git a/server/core/terraform/terraform_client_test.go b/server/core/terraform/tfclient/terraform_client_test.go similarity index 86% rename from server/core/terraform/terraform_client_test.go rename to server/core/terraform/tfclient/terraform_client_test.go index 1c2c654495..50afd698a7 100644 --- a/server/core/terraform/terraform_client_test.go +++ b/server/core/terraform/tfclient/terraform_client_test.go @@ -11,7 +11,7 @@ // limitations under the License. // Modified hereafter by contributors to runatlantis/atlantis. -package terraform_test +package tfclient_test import ( "context" @@ -28,6 +28,7 @@ import ( "github.com/runatlantis/atlantis/cmd" "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/events/command" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" "github.com/runatlantis/atlantis/server/logging" @@ -42,12 +43,12 @@ func TestMustConstraint_PanicsOnBadConstraint(t *testing.T) { } }() - terraform.MustConstraint("invalid constraint") + tfclient.MustConstraint("invalid constraint") } func TestMustConstraint(t *testing.T) { t.Log("MustConstraint should return the constrain") - c := terraform.MustConstraint(">0.1") + c := tfclient.MustConstraint(">0.1") expectedConstraint, err := version.NewConstraint(">0.1") Ok(t, err) Equals(t, expectedConstraint.String(), c.String()) @@ -80,13 +81,13 @@ is 0.11.13. You can update by downloading from developer.hashicorp.com/terraform mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{"test": "123"}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{"test": "123"}, distribution, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -117,13 +118,13 @@ is 0.11.13. You can update by downloading from developer.hashicorp.com/terraform mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -141,7 +142,7 @@ func TestNewClient_NoTF(t *testing.T) { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - _, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + _, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) ErrEquals(t, "terraform not found in $PATH. Set --default-tf-version or download terraform from https://developer.hashicorp.com/terraform/downloads", err) } @@ -167,13 +168,13 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, false, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, false, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -198,13 +199,13 @@ func TestNewClient_DefaultTFFlagInBinDir(t *testing.T) { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logging.NewNoopLogger(t), distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logging.NewNoopLogger(t), distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -232,7 +233,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) { return []ReturnValue{binPath, err} }) distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) @@ -243,7 +244,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) { // Reset PATH so that it has sh. Ok(t, os.Setenv("PATH", orig)) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "") Ok(t, err) Equals(t, "\nTerraform v0.11.10\n\n", output) } @@ -255,7 +256,7 @@ func TestNewClient_BadVersion(t *testing.T) { projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - _, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + _, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) ErrEquals(t, "Malformed version: malformed", err) } @@ -283,11 +284,11 @@ func TestRunCommandWithVersion_DLsTF(t *testing.T) { return []ReturnValue{binPath, err} }) - c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, v, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, v, "") Assert(t, err == nil, "err: %s: %s", err, output) Equals(t, "\nTerraform v99.99.99\n\n", output) @@ -304,7 +305,7 @@ func TestEnsureVersion_downloaded(t *testing.T) { distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) downloadsAllowed := true - c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler) + c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) @@ -318,7 +319,7 @@ func TestEnsureVersion_downloaded(t *testing.T) { return []ReturnValue{binPath, err} }) - err = c.EnsureVersion(logger, v) + err = c.EnsureVersion(logger, distribution, v) Ok(t, err) @@ -337,7 +338,7 @@ func TestEnsureVersion_downloaded_customURL(t *testing.T) { downloadsAllowed := true customURL := "http://releases.example.com" - c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, customURL, downloadsAllowed, true, projectCmdOutputHandler) + c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, customURL, downloadsAllowed, true, projectCmdOutputHandler) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) @@ -351,7 +352,7 @@ func TestEnsureVersion_downloaded_customURL(t *testing.T) { return []ReturnValue{binPath, err} }) - err = c.EnsureVersion(logger, v) + err = c.EnsureVersion(logger, distribution, v) Ok(t, err) @@ -369,7 +370,7 @@ func TestEnsureVersion_downloaded_downloadingDisabled(t *testing.T) { distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) downloadsAllowed := false - c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler) + c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) @@ -377,7 +378,7 @@ func TestEnsureVersion_downloaded_downloadingDisabled(t *testing.T) { v, err := version.NewVersion("99.99.99") Ok(t, err) - err = c.EnsureVersion(logger, v) + err = c.EnsureVersion(logger, distribution, v) ErrContains(t, "could not find terraform version", err) ErrContains(t, "downloads are disabled", err) mockDownloader.VerifyWasCalled(Never()) @@ -501,7 +502,7 @@ terraform { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewTestClient( + c, err := tfclient.NewTestClient( logger, distribution, binDir, @@ -548,7 +549,7 @@ func TestExtractExactRegex(t *testing.T) { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) tests := []struct { diff --git a/server/events/command/project_context.go b/server/events/command/project_context.go index 8fff2831d6..670aaa6c01 100644 --- a/server/events/command/project_context.go +++ b/server/events/command/project_context.go @@ -93,6 +93,10 @@ type ProjectContext struct { // Steps are the sequence of commands we need to run for this project and this // stage. Steps []valid.Step + // TerraformDistribution is the distribution of terraform we should use when + // executing commands for this project. This can be set to nil in which case + // we will use the default Atlantis terraform distribution. + TerraformDistribution *string // TerraformVersion is the version of terraform we should use when executing // commands for this project. This can be set to nil in which case we will // use the default Atlantis terraform version. diff --git a/server/events/command/scope_tags.go b/server/events/command/scope_tags.go index 2f51d86c83..8416927eab 100644 --- a/server/events/command/scope_tags.go +++ b/server/events/command/scope_tags.go @@ -7,12 +7,13 @@ import ( ) type ProjectScopeTags struct { - BaseRepo string - PrNumber string - Project string - ProjectPath string - TerraformVersion string - Workspace string + BaseRepo string + PrNumber string + Project string + ProjectPath string + TerraformDistribution string + TerraformVersion string + Workspace string } func (s ProjectScopeTags) Loadtags() map[string]string { diff --git a/server/events/mock_workingdir_test.go b/server/events/mock_workingdir_test.go index c11b9e28bf..65d5fc00a7 100644 --- a/server/events/mock_workingdir_test.go +++ b/server/events/mock_workingdir_test.go @@ -4,12 +4,11 @@ package events import ( - "reflect" - "time" - pegomock "github.com/petergtz/pegomock/v4" models "github.com/runatlantis/atlantis/server/events/models" logging "github.com/runatlantis/atlantis/server/logging" + "reflect" + "time" ) type MockWorkingDir struct { diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index c52dee6360..275e8cbfbc 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -11,7 +11,7 @@ import ( tally "github.com/uber-go/tally/v4" "github.com/runatlantis/atlantis/server/core/config/valid" - "github.com/runatlantis/atlantis/server/core/terraform" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/metrics" @@ -59,7 +59,7 @@ func NewInstrumentedProjectCommandBuilder( IncludeGitUntrackedFiles bool, AutoDiscoverMode string, scope tally.Scope, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) *InstrumentedProjectCommandBuilder { scope = scope.SubScope("builder") @@ -119,7 +119,7 @@ func NewProjectCommandBuilder( IncludeGitUntrackedFiles bool, AutoDiscoverMode string, scope tally.Scope, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) *DefaultProjectCommandBuilder { return &DefaultProjectCommandBuilder{ ParserValidator: parserValidator, @@ -249,7 +249,7 @@ type DefaultProjectCommandBuilder struct { // User config option: Controls auto-discovery of projects in a repository. AutoDiscoverMode string // Handles the actual running of Terraform commands. - TerraformExecutor terraform.Client + TerraformExecutor tfclient.Client } // See ProjectCommandBuilder.BuildAutoplanCommands. diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index d020871b31..115657e38e 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -9,7 +9,7 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config" "github.com/runatlantis/atlantis/server/core/config/valid" - "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" @@ -648,7 +648,7 @@ projects: Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600)) } - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( false, @@ -865,7 +865,7 @@ projects: statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis") - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( false, @@ -1112,7 +1112,7 @@ workflows: } statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis") - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( true, @@ -1264,7 +1264,7 @@ projects: } statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis") - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( false, @@ -1406,7 +1406,7 @@ projects: } statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis") - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( false, diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index 7560b5d6de..30dec015a5 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" - terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/core/config" "github.com/runatlantis/atlantis/server/core/config/valid" @@ -233,7 +233,7 @@ terraform { scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") userConfig := defaultUserConfig - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() for _, c := range cases { t.Run(c.Description, func(t *testing.T) { @@ -616,7 +616,7 @@ projects: AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -804,7 +804,7 @@ projects: AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1133,7 +1133,7 @@ projects: AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1231,7 +1231,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { globalCfgArgs := valid.GlobalCfgArgs{} scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1317,7 +1317,7 @@ projects: scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") userConfig := defaultUserConfig - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1405,7 +1405,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) { AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1558,7 +1558,7 @@ projects: AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() When(terraformClient.DetectVersion(Any[logging.SimpleLogging](), Any[string]())).Then(func(params []Param) ReturnValues { projectName := filepath.Base(params[1].(string)) testVersion := testCase.Exp[projectName] @@ -1677,7 +1677,7 @@ projects: AllowAllRepoSettings: true, } scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1746,7 +1746,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman } globalCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs) - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( true, @@ -1834,7 +1834,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) { globalCfgArgs := valid.GlobalCfgArgs{ AllowAllRepoSettings: false, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1964,7 +1964,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_Single_With_RestrictFile Ok(t, err) } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, // policyChecksSupported @@ -2075,7 +2075,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_with_IncludeGitUntracked Ok(t, err) } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, // policyChecksSupported diff --git a/server/events/project_command_context_builder.go b/server/events/project_command_context_builder.go index 509fa728b8..8c1fe76516 100644 --- a/server/events/project_command_context_builder.go +++ b/server/events/project_command_context_builder.go @@ -5,7 +5,7 @@ import ( "github.com/google/uuid" "github.com/runatlantis/atlantis/server/core/config/valid" - "github.com/runatlantis/atlantis/server/core/terraform" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" tally "github.com/uber-go/tally/v4" @@ -38,7 +38,7 @@ type ProjectCommandContextBuilder interface { prjCfg valid.MergedProjectCfg, commentFlags []string, repoDir string, - automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool, terraformClient terraform.Client, + automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool, terraformClient tfclient.Client, ) []command.ProjectContext } @@ -59,7 +59,7 @@ func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext( commentFlags []string, repoDir string, automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) (projectCmds []command.ProjectContext) { cb.ProjectCounter.Inc(1) @@ -93,7 +93,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext( commentFlags []string, repoDir string, automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) (projectCmds []command.ProjectContext) { ctx.Log.Debug("Building project command context for %s", cmdName) @@ -166,7 +166,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( commentFlags []string, repoDir string, automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) (projectCmds []command.ProjectContext) { if prjCfg.PolicyCheck { ctx.Log.Debug("PolicyChecks are enabled") @@ -297,6 +297,7 @@ func newProjectCommandContext(ctx *command.Context, RePlanCmd: planCmd, RepoRelDir: projCfg.RepoRelDir, RepoConfigVersion: projCfg.RepoCfgVersion, + TerraformDistribution: projCfg.TerraformDistribution, TerraformVersion: projCfg.TerraformVersion, User: ctx.User, Verbose: verbose, diff --git a/server/events/project_command_context_builder_test.go b/server/events/project_command_context_builder_test.go index ff40645e0a..5e66cdb4a2 100644 --- a/server/events/project_command_context_builder_test.go +++ b/server/events/project_command_context_builder_test.go @@ -5,7 +5,7 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config/valid" - terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/mocks" @@ -47,7 +47,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) { expectedApplyCmt := "Apply Comment" expectedPlanCmt := "Plan Comment" - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() t.Run("with project name defined", func(t *testing.T) { When(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, projName, []string{})).ThenReturn(expectedPlanCmt) diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go index 13a75a1658..b013741647 100644 --- a/server/events/project_command_runner_test.go +++ b/server/events/project_command_runner_test.go @@ -23,7 +23,9 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/runtime" + "github.com/runatlantis/atlantis/server/core/terraform" tmocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/mocks" @@ -542,12 +544,14 @@ func TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) { // not running any Terraform. func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) { RegisterMockTestingT(t) - tfClient := tmocks.NewMockClient() + tfClient := tfclientmocks.NewMockClient() + tfDistribution := terraform.NewDistributionTerraformWithDownloader(tmocks.NewMockDownloader()) tfVersion, err := version.NewVersion("0.12.0") Ok(t, err) projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() run := runtime.RunStepRunner{ TerraformExecutor: tfClient, + DefaultTFDistribution: tfDistribution, DefaultTFVersion: tfVersion, ProjectCmdOutputHandler: projectCmdOutputHandler, } diff --git a/server/server.go b/server/server.go index 22f6db5498..a77eeddaf8 100644 --- a/server/server.go +++ b/server/server.go @@ -42,6 +42,7 @@ import ( "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/db" "github.com/runatlantis/atlantis/server/core/redis" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/jobs" "github.com/runatlantis/atlantis/server/metrics" "github.com/runatlantis/atlantis/server/scheduled" @@ -127,12 +128,13 @@ type Server struct { // Config holds config for server that isn't passed in by the user. type Config struct { - AllowForkPRsFlag string - AtlantisURLFlag string - AtlantisVersion string - DefaultTFVersionFlag string - RepoConfigJSONFlag string - SilenceForkPRErrorsFlag string + AllowForkPRsFlag string + AtlantisURLFlag string + AtlantisVersion string + DefaultTFDistributionFlag string + DefaultTFVersionFlag string + RepoConfigJSONFlag string + SilenceForkPRErrorsFlag string } // WebhookConfig is nested within UserConfig. It's used to configure webhooks. @@ -427,12 +429,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { ) } - distribution := terraform.NewDistributionTerraform() - if userConfig.TFDistribution == "opentofu" { - distribution = terraform.NewDistributionOpenTofu() - } + distribution := terraform.NewDistribution(userConfig.DefaultTFDistribution) - terraformClient, err := terraform.NewClient( + terraformClient, err := tfclient.NewClient( logger, distribution, binDir, @@ -449,7 +448,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { // are, then we don't error out because we don't have/want terraform // installed on our CI system where the unit tests run. if err != nil && flag.Lookup("test.v") == nil { - return nil, errors.Wrap(err, fmt.Sprintf("initializing %s", userConfig.TFDistribution)) + return nil, errors.Wrap(err, fmt.Sprintf("initializing %s", userConfig.DefaultTFDistribution)) } markdownRenderer := events.NewMarkdownRenderer( gitlabClient.SupportsCommonMark(), @@ -586,10 +585,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { userConfig.ExecutableName, allowCommands, ) + defaultTfDistribution := terraformClient.DefaultDistribution() defaultTfVersion := terraformClient.DefaultVersion() pendingPlanFinder := &events.DefaultPendingPlanFinder{} runStepRunner := &runtime.RunStepRunner{ TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTfDistribution, DefaultTFVersion: defaultTfVersion, TerraformBinDir: terraformClient.TerraformBinDir(), ProjectCmdOutputHandler: projectCmdOutputHandler, @@ -648,13 +649,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { terraformClient, ) - showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfVersion) + showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion) if err != nil { return nil, errors.Wrap(err, "initializing show step runner") } policyCheckStepRunner, err := runtime.NewPolicyCheckStepRunner( + defaultTfDistribution, defaultTfVersion, policy.NewConfTestExecutorWorkflow(logger, binDir, &policy.ConfTestGoGetterVersionDownloader{}), ) @@ -672,17 +674,19 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Locker: projectLocker, LockURLGenerator: router, InitStepRunner: &runtime.InitStepRunner{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTfVersion, + TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTfDistribution, + DefaultTFVersion: defaultTfVersion, }, - PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfVersion, commitStatusUpdater, terraformClient), + PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion, commitStatusUpdater, terraformClient), ShowStepRunner: showStepRunner, PolicyCheckStepRunner: policyCheckStepRunner, ApplyStepRunner: &runtime.ApplyStepRunner{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTfVersion, - CommitStatusUpdater: commitStatusUpdater, - AsyncTFExec: terraformClient, + TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTfDistribution, + DefaultTFVersion: defaultTfVersion, + CommitStatusUpdater: commitStatusUpdater, + AsyncTFExec: terraformClient, }, RunStepRunner: runStepRunner, EnvStepRunner: &runtime.EnvStepRunner{ @@ -695,8 +699,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { TerraformExecutor: terraformClient, DefaultTFVersion: defaultTfVersion, }, - ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTfVersion), - StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTfVersion), + ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion), + StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion), WorkingDir: workingDir, Webhooks: webhooksManager, WorkingDirLocker: workingDirLocker, diff --git a/server/user_config.go b/server/user_config.go index 10e6e6b9fc..9cd4f54675 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -109,7 +109,7 @@ type UserConfig struct { SSLCertFile string `mapstructure:"ssl-cert-file"` SSLKeyFile string `mapstructure:"ssl-key-file"` RestrictFileList bool `mapstructure:"restrict-file-list"` - TFDistribution string `mapstructure:"tf-distribution"` + TFDistribution string `mapstructure:"tf-distribution"` // deprecated in favor of DefaultTFDistribution TFDownload bool `mapstructure:"tf-download"` TFDownloadURL string `mapstructure:"tf-download-url"` TFEHostname string `mapstructure:"tfe-hostname"` @@ -117,6 +117,7 @@ type UserConfig struct { TFEToken string `mapstructure:"tfe-token"` VarFileAllowlist string `mapstructure:"var-file-allowlist"` VCSStatusName string `mapstructure:"vcs-status-name"` + DefaultTFDistribution string `mapstructure:"default-tf-distribution"` DefaultTFVersion string `mapstructure:"default-tf-version"` Webhooks []WebhookConfig `mapstructure:"webhooks" flag:"false"` WebBasicAuth bool `mapstructure:"web-basic-auth"` From 1c4f688c6accf5333539853bef08b44821bacdab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Jan 2025 21:20:51 -0800 Subject: [PATCH 06/60] chore(deps): update ghinstallation to 2.13.0 (#5212) Signed-off-by: Rui Chen Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Rui Chen Co-authored-by: PePe Amengual <2208324+jamengual@users.noreply.github.com> --- go.mod | 6 +----- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 458983b9a1..aff4490023 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( code.gitea.io/sdk/gitea v0.19.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/alicebob/miniredis/v2 v2.34.0 - github.com/bradleyfalzon/ghinstallation/v2 v2.12.0 + github.com/bradleyfalzon/ghinstallation/v2 v2.13.0 github.com/briandowns/spinner v1.23.1 github.com/cactus/go-statsd-client/v5 v5.1.0 github.com/go-ozzo/ozzo-validation v3.6.0+incompatible @@ -45,7 +45,6 @@ require ( github.com/stretchr/testify v1.10.0 github.com/uber-go/tally/v4 v4.1.16 github.com/urfave/negroni/v3 v3.1.1 - github.com/xanzy/go-gitlab v0.115.0 gitlab.com/gitlab-org/api/client-go v0.118.0 go.etcd.io/bbolt v1.3.11 go.uber.org/zap v1.27.0 @@ -144,6 +143,3 @@ require ( google.golang.org/protobuf v1.36.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) - -// upstream pr to patch go-github to use v68, https://github.com/bradleyfalzon/ghinstallation/pull/137 -replace github.com/bradleyfalzon/ghinstallation/v2 => github.com/chenrui333/ghinstallation/v2 v2.12.1-0.20241231170237-36dcfb064b2f diff --git a/go.sum b/go.sum index 65b17647c4..e67437d296 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bradleyfalzon/ghinstallation/v2 v2.13.0 h1:5FhjW93/YLQJDmPdeyMPw7IjAPzqsr+0jHPfrPz0sZI= +github.com/bradleyfalzon/ghinstallation/v2 v2.13.0/go.mod h1:EJ6fgedVEHa2kUyBTTvslJCXJafS/mhJNNKEOCspZXQ= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -90,8 +92,6 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chenrui333/ghinstallation/v2 v2.12.1-0.20241231170237-36dcfb064b2f h1:TN3fEfE18MJ+o3Y4PMUWu1S9IVYL7a82G3LVa8zJ7/c= -github.com/chenrui333/ghinstallation/v2 v2.12.1-0.20241231170237-36dcfb064b2f/go.mod h1:EJ6fgedVEHa2kUyBTTvslJCXJafS/mhJNNKEOCspZXQ= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -461,8 +461,6 @@ github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw= github.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs= -github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8= -github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= From 357ff4443a7b9ea0ee451b0f6dca682612caa00b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:39:04 -0500 Subject: [PATCH 07/60] chore(deps): update davidanson/markdownlint-cli2-action action to v19 in .github/workflows/website.yml (main) (#5214) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/website.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index afef23747f..15895346c6 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: markdown-lint - uses: DavidAnson/markdownlint-cli2-action@eb5ca3ab411449c66620fe7f1b3c9e10547144b0 # v18 + uses: DavidAnson/markdownlint-cli2-action@a23dae216ce3fee4db69da41fed90d2a4af801cf # v19 with: config: .markdownlint.yaml globs: 'runatlantis.io/**/*.md' From de1d8dcf263e31b7c677cb25496484988196d862 Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Mon, 6 Jan 2025 10:47:06 -0500 Subject: [PATCH 08/60] chore(renovate): only support most two recent releases 0.31 and 0.32 (#5220) Signed-off-by: Rui Chen --- .github/renovate.json5 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 8ca42d1d5d..c6cc7b846d 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -9,7 +9,8 @@ automerge: true, baseBranches: [ 'main', - '/^release-.*/', + 'release-0.31', + 'release-0.32', ], platformAutomerge: true, labels: [ From 416b5a3e45e9e1845bbeb10dcbf5a21c74345bbf Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Tue, 14 Jan 2025 17:54:51 -0500 Subject: [PATCH 09/60] chore(deps): bump ca-certificates to `20241121-r1` (#5235) Signed-off-by: Rui Chen --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ed8d0b5fe7..8f45556310 100644 --- a/Dockerfile +++ b/Dockerfile @@ -155,7 +155,7 @@ COPY --from=deps /usr/bin/git-lfs /usr/bin/git-lfs COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh # renovate: datasource=repology depName=alpine_3_21/ca-certificates versioning=loose -ENV CA_CERTIFICATES_VERSION="20241010" +ENV CA_CERTIFICATES_VERSION="20241121-r1" # Install packages needed to run Atlantis. # We place this last as it will bust less docker layer caches when packages update From 75dab169ce44459ae498cd15059e8d4898ef6a6a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 23:10:05 +0000 Subject: [PATCH 10/60] chore(deps): update alpine docker tag to v3.21.2 in dockerfile (main) (#5234) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Rui Chen --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8f45556310..e9f2f13702 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1@sha256:93bfd3b68c109427185cd78b4779fc82b484b0b7618e36d0f104d4d801e66d25 # what distro is the image being built for -ARG ALPINE_TAG=3.21.0@sha256:21dc6063fd678b478f57c0e13f47560d0ea4eeba26dfc947b2a4f81f686b9f45 +ARG ALPINE_TAG=3.21.2@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099 ARG DEBIAN_TAG=12.8-slim@sha256:d365f4920711a9074c4bcd178e8f457ee59250426441ab2a5f8106ed8fe948eb ARG GOLANG_TAG=1.23.4-alpine@sha256:6c5c9590f169f77c8046e45c611d3b28fe477789acd8d3762d23d4744de69812 From a611e6591c5b1429cb457bf0a1e1f52bc008f924 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 23:11:12 +0000 Subject: [PATCH 11/60] chore(deps): update docker/setup-qemu-action digest to 53851d1 in .github/workflows/testing-env-image.yml (main) (#5227) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/atlantis-image.yml | 2 +- .github/workflows/testing-env-image.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/atlantis-image.yml b/.github/workflows/atlantis-image.yml index 0b8e8019df..3fe04153ec 100644 --- a/.github/workflows/atlantis-image.yml +++ b/.github/workflows/atlantis-image.yml @@ -76,7 +76,7 @@ jobs: go-version-file: "go.mod" - name: Set up QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3 + uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3 with: image: tonistiigi/binfmt:latest platforms: arm64,arm diff --git a/.github/workflows/testing-env-image.yml b/.github/workflows/testing-env-image.yml index 44008e8a8b..bb2f53ef2f 100644 --- a/.github/workflows/testing-env-image.yml +++ b/.github/workflows/testing-env-image.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Set up QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3 + uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3 with: image: tonistiigi/binfmt:latest platforms: arm64,arm From 22a325ce8f48e6144d051f8c526dbcb09408072b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 23:16:04 +0000 Subject: [PATCH 12/60] chore(deps): update docker/build-push-action digest to b32b51a in .github/workflows/testing-env-image.yml (main) (#5225) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/atlantis-image.yml | 4 ++-- .github/workflows/testing-env-image.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/atlantis-image.yml b/.github/workflows/atlantis-image.yml index 3fe04153ec..5e974f0f85 100644 --- a/.github/workflows/atlantis-image.yml +++ b/.github/workflows/atlantis-image.yml @@ -146,7 +146,7 @@ jobs: - name: "Build ${{ env.PUSH == 'true' && 'and push' || '' }} ${{ env.DOCKER_REPO }} image" id: build if: contains(fromJson('["push", "pull_request"]'), github.event_name) - uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6 + uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6 with: cache-from: type=gha cache-to: type=gha,mode=max @@ -213,7 +213,7 @@ jobs: - name: "Build and load into Docker" if: contains(fromJson('["push", "pull_request"]'), github.event_name) - uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6 + uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6 with: cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/testing-env-image.yml b/.github/workflows/testing-env-image.yml index bb2f53ef2f..ebafe7eb4c 100644 --- a/.github/workflows/testing-env-image.yml +++ b/.github/workflows/testing-env-image.yml @@ -60,7 +60,7 @@ jobs: - run: echo "TODAY=$(date +"%Y.%m.%d")" >> $GITHUB_ENV - name: Build and push testing-env:${{env.TODAY}} image - uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6 + uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6 with: cache-from: type=gha cache-to: type=gha,mode=max From a34234178c579e28a1fb6e452b793285152210ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 03:05:19 +0000 Subject: [PATCH 13/60] chore(deps): update go in testing/dockerfile (main) (#5230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Dockerfile | 2 +- testing/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e9f2f13702..646d172e2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # what distro is the image being built for ARG ALPINE_TAG=3.21.2@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099 ARG DEBIAN_TAG=12.8-slim@sha256:d365f4920711a9074c4bcd178e8f457ee59250426441ab2a5f8106ed8fe948eb -ARG GOLANG_TAG=1.23.4-alpine@sha256:6c5c9590f169f77c8046e45c611d3b28fe477789acd8d3762d23d4744de69812 +ARG GOLANG_TAG=1.23.4-alpine@sha256:c23339199a08b0e12032856908589a6d41a0dab141b8b3b21f156fc571a3f1d3 # renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp ARG DEFAULT_TERRAFORM_VERSION=1.10.3 diff --git a/testing/Dockerfile b/testing/Dockerfile index 4442c1fe8b..875e4dc556 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23.4@sha256:7ea4c9dcb2b97ff8ee80a67db3d44f98c8ffa0d191399197007d8459c1453041 +FROM golang:1.23.4@sha256:585103a29aa6d4c98bbb45d2446e1fdf41441698bbdf707d1801f5708e479f04 RUN apt-get update && apt-get --no-install-recommends -y install unzip \ && apt-get clean \ From a7cbb37c37027c130953cd43aa9d84c18904ce5c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 00:52:03 -0500 Subject: [PATCH 14/60] chore(deps): update ghcr.io/runatlantis/testing-env:latest docker digest to 45ec58b in .github/workflows/test.yml (main) (#5236) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c151d134e..5884a2adfc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: if: needs.changes.outputs.should-run-tests == 'true' name: Tests runs-on: ubuntu-24.04 - container: ghcr.io/runatlantis/testing-env:latest@sha256:79991418aec4e5dcb1f18dc7b7bdf6ee37302a30a1e374c7bcf3eba9aadef68d + container: ghcr.io/runatlantis/testing-env:latest@sha256:45ec58ba11af5196fb70ced526ccb1996f0e58a7dbd93f7dcba96eed49209583 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 From a19a973e0615de4c8bdaa8372e40cb7d296fe053 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 00:57:41 -0500 Subject: [PATCH 15/60] chore(deps): update github/codeql-action digest to b6a472f in .github/workflows/codeql.yml (main) (#5229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a24704228d..2dc8574409 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -77,7 +77,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3 + uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -91,7 +91,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3 + uses: github/codeql-action/autobuild@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -104,7 +104,7 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3 + uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3 with: category: "/language:${{matrix.language}}" From dffd7ebf1047f144341db5048163c3748fa57286 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:40:02 -0500 Subject: [PATCH 16/60] chore(deps): update redis:7.4-alpine docker digest to 1bf97f2 in docker-compose.yml (main) (#5231) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2fc7fd4a9c..a9342ab364 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: depends_on: - atlantis redis: - image: redis:7.4-alpine@sha256:c1e88455c85225310bbea54816e9c3f4b5295815e6dbf80c34d40afc6df28275 + image: redis:7.4-alpine@sha256:1bf97f21f01b0e7bd4b7b34a26d3b9d8086e41e70c10f262e8a9e0b49b5116a0 restart: always ports: - 6379:6379 From 9afcb1092fc72d365eed4e55af9e0627fe8d28c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:59:54 +0000 Subject: [PATCH 17/60] chore(deps): update docker/build-push-action digest to 67a2d40 in .github/workflows/testing-env-image.yml (main) (#5238) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/atlantis-image.yml | 4 ++-- .github/workflows/testing-env-image.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/atlantis-image.yml b/.github/workflows/atlantis-image.yml index 5e974f0f85..e90cc65542 100644 --- a/.github/workflows/atlantis-image.yml +++ b/.github/workflows/atlantis-image.yml @@ -146,7 +146,7 @@ jobs: - name: "Build ${{ env.PUSH == 'true' && 'and push' || '' }} ${{ env.DOCKER_REPO }} image" id: build if: contains(fromJson('["push", "pull_request"]'), github.event_name) - uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6 + uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6 with: cache-from: type=gha cache-to: type=gha,mode=max @@ -213,7 +213,7 @@ jobs: - name: "Build and load into Docker" if: contains(fromJson('["push", "pull_request"]'), github.event_name) - uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6 + uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6 with: cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/testing-env-image.yml b/.github/workflows/testing-env-image.yml index ebafe7eb4c..1f080a67da 100644 --- a/.github/workflows/testing-env-image.yml +++ b/.github/workflows/testing-env-image.yml @@ -60,7 +60,7 @@ jobs: - run: echo "TODAY=$(date +"%Y.%m.%d")" >> $GITHUB_ENV - name: Build and push testing-env:${{env.TODAY}} image - uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6 + uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6 with: cache-from: type=gha cache-to: type=gha,mode=max From 7fa50b48694958ca7141dd75020a7c7e32cdbc6b Mon Sep 17 00:00:00 2001 From: Joe Cai Date: Sat, 18 Jan 2025 00:15:44 +1100 Subject: [PATCH 18/60] feat: hide successful policy check results when --quiet-policy-checks is set with multiple projects (#5168) Signed-off-by: Joe Cai --- .../events/events_controller_e2e_test.go | 36 +- .../exp-output-auto-policy-check-quiet.txt | 44 ++ server/events/command_runner_test.go | 2 +- server/events/markdown_renderer.go | 23 +- server/events/markdown_renderer_test.go | 478 +++++++++++++++++- .../templates/multi_project_policy.tmpl | 2 + .../multi_project_policy_unsuccessful.tmpl | 2 + server/server.go | 1 + 8 files changed, 558 insertions(+), 30 deletions(-) create mode 100644 server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-auto-policy-check-quiet.txt diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 3b66a28225..4588b04127 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -948,6 +948,25 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) { {"exp-output-merge.txt"}, }, }, + { + Description: "1 failing policy and 1 passing policy with --quiet-policy-checks", + RepoDir: "policy-checks-multi-projects", + ModifiedFiles: []string{"dir1/main.tf,", "dir2/main.tf"}, + PolicyCheck: true, + ExpAutoplan: true, + ExpPolicyChecks: true, + ExpQuietPolicyChecks: true, + ExpQuietPolicyCheckFailure: true, + Comments: []string{ + "atlantis apply", + }, + ExpReplies: [][]string{ + {"exp-output-autoplan.txt"}, + {"exp-output-auto-policy-check-quiet.txt"}, + {"exp-output-apply.txt"}, + {"exp-output-merge.txt"}, + }, + }, { Description: "failing policy without policies passing using extra args", RepoDir: "policy-checks-extra-args", @@ -1183,7 +1202,7 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) { userConfig.EnablePolicyChecksFlag = c.PolicyCheck userConfig.QuietPolicyChecks = c.ExpQuietPolicyChecks - ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, setupOption{}) + ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, setupOption{userConfig: userConfig}) // Set the repo to be cloned through the testing backdoor. repoDir, headSHA := initializeRepo(t, c.RepoDir) @@ -1274,13 +1293,13 @@ type setupOption struct { allowCommands []command.Name disableAutoplan bool disablePreWorkflowHooks bool + userConfig server.UserConfig } func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers.VCSEventsController, *vcsmocks.MockClient, *mocks.MockGithubPullGetter, *events.FileWorkspace) { allowForkPRs := false discardApprovalOnPlan := true dataDir, binDir, cacheDir := mkSubDirs(t) - // Mocks. e2eVCSClient := vcsmocks.NewMockClient() e2eStatusUpdater := &events.DefaultCommitStatusUpdater{Client: e2eVCSClient} @@ -1493,7 +1512,18 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers pullUpdater := &events.PullUpdater{ HidePrevPlanComments: false, VCSClient: e2eVCSClient, - MarkdownRenderer: events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false), + MarkdownRenderer: events.NewMarkdownRenderer( + false, // gitlabSupportsCommonMark + false, // disableApplyAll + false, // disableApply + false, // disableMarkdownFolding + false, // disableRepoLocking + false, // enableDiffMarkdownFormat + "", // markdownTemplateOverridesDir + "atlantis", // executableName + false, // hideUnchangedPlanComments + opt.userConfig.QuietPolicyChecks, // quietPolicyChecks + ), } autoMerger := &events.AutoMerger{ diff --git a/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-auto-policy-check-quiet.txt b/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-auto-policy-check-quiet.txt new file mode 100644 index 0000000000..57a3dfefe3 --- /dev/null +++ b/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-auto-policy-check-quiet.txt @@ -0,0 +1,44 @@ +Ran Policy Check for 2 projects: + +1. dir: `dir1` workspace: `default` +1. dir: `dir2` workspace: `default` +--- + +### 2. dir: `dir2` workspace: `default` +**Policy Check Failed**: Some policy sets did not pass. +#### Policy Set: `test_policy` +```diff +FAIL - - main - WARNING: Forbidden Resource creation is prohibited. + +1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions + +``` + + +#### Policy Approval Status: +``` +policy set: test_policy: requires: 1 approval(s), have: 0. +``` +* :heavy_check_mark: To **approve** this project, comment: + ```shell + atlantis approve_policies -d dir2 + ``` +* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url) +* :repeat: To re-run policies **plan** this project again by commenting: + ```shell + atlantis plan -d dir2 + ``` + +--- +* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment: + ```shell + atlantis approve_policies + ``` +* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment: + ```shell + atlantis unlock + ``` +* :repeat: To re-run policies **plan** this project again by commenting: + ```shell + atlantis plan + ``` diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index cd9cbc10e4..7b06d0f015 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -126,7 +126,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock pullUpdater = &events.PullUpdater{ HidePrevPlanComments: false, VCSClient: vcsClient, - MarkdownRenderer: events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false), + MarkdownRenderer: events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false, false), } autoMerger = &events.AutoMerger{ diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go index 4ce268c239..e685122b08 100644 --- a/server/events/markdown_renderer.go +++ b/server/events/markdown_renderer.go @@ -57,6 +57,7 @@ type MarkdownRenderer struct { markdownTemplates *template.Template executableName string hideUnchangedPlanComments bool + quietPolicyChecks bool } // commonData is data that all responses have. @@ -72,6 +73,7 @@ type commonData struct { EnableDiffMarkdownFormat bool ExecutableName string HideUnchangedPlanComments bool + QuietPolicyChecks bool VcsRequestType string } @@ -131,11 +133,12 @@ type policyCheckResultsData struct { } type projectResultTmplData struct { - Workspace string - RepoRelDir string - ProjectName string - Rendered string - NoChanges bool + Workspace string + RepoRelDir string + ProjectName string + Rendered string + NoChanges bool + IsSuccessful bool } // Initialize templates @@ -149,6 +152,7 @@ func NewMarkdownRenderer( markdownTemplateOverridesDir string, executableName string, hideUnchangedPlanComments bool, + quietPolicyChecks bool, ) *MarkdownRenderer { var templates *template.Template templates, _ = template.New("").Funcs(sprig.TxtFuncMap()).ParseFS(templatesFS, "templates/*.tmpl") @@ -166,6 +170,7 @@ func NewMarkdownRenderer( markdownTemplates: templates, executableName: executableName, hideUnchangedPlanComments: hideUnchangedPlanComments, + quietPolicyChecks: quietPolicyChecks, } } @@ -192,6 +197,7 @@ func (m *MarkdownRenderer) Render(ctx *command.Context, res command.Result, cmd EnableDiffMarkdownFormat: m.enableDiffMarkdownFormat, ExecutableName: m.executableName, HideUnchangedPlanComments: m.hideUnchangedPlanComments, + QuietPolicyChecks: m.quietPolicyChecks, VcsRequestType: vcsRequestType, } @@ -224,9 +230,10 @@ func (m *MarkdownRenderer) renderProjectResults(ctx *command.Context, results [] for _, result := range results { resultData := projectResultTmplData{ - Workspace: result.Workspace, - RepoRelDir: result.RepoRelDir, - ProjectName: result.ProjectName, + Workspace: result.Workspace, + RepoRelDir: result.RepoRelDir, + ProjectName: result.ProjectName, + IsSuccessful: result.IsSuccessful(), } if result.PlanSuccess != nil { result.PlanSuccess.TerraformOutput = strings.TrimSpace(result.PlanSuccess.TerraformOutput) diff --git a/server/events/markdown_renderer_test.go b/server/events/markdown_renderer_test.go index 39810dab13..2fb90c256b 100644 --- a/server/events/markdown_renderer_test.go +++ b/server/events/markdown_renderer_test.go @@ -60,7 +60,18 @@ func TestRenderErr(t *testing.T) { }, } - r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false) + r := events.NewMarkdownRenderer( + false, // gitlabSupportsCommonMark + false, // disableApplyAll + false, // disableApply + false, // disableMarkdownFolding + false, // disableRepoLocking + false, // enableDiffMarkdownFormat + "", // markdownTemplateOverridesDir + "atlantis", // executableName + false, // hideUnchangedPlanComments + false, // quietPolicyChecks + ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" logger.Info(logText) @@ -124,7 +135,18 @@ func TestRenderFailure(t *testing.T) { }, } - r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false) + r := events.NewMarkdownRenderer( + false, // gitlabSupportsCommonMark + false, // disableApplyAll + false, // disableApply + false, // disableMarkdownFolding + false, // disableRepoLocking + false, // enableDiffMarkdownFormat + "", // markdownTemplateOverridesDir + "atlantis", // executableName + false, // hideUnchangedPlanComments + false, // quietPolicyChecks + ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" logger.Info(logText) @@ -163,7 +185,18 @@ func TestRenderFailure(t *testing.T) { } func TestRenderErrAndFailure(t *testing.T) { - r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false) + r := events.NewMarkdownRenderer( + false, // gitlabSupportsCommonMark + false, // disableApplyAll + false, // disableApply + false, // disableMarkdownFolding + false, // disableRepoLocking + false, // enableDiffMarkdownFormat + "", // markdownTemplateOverridesDir + "atlantis", // executableName + false, // hideUnchangedPlanComments + false, // quietPolicyChecks + ) logger := logging.NewNoopLogger(t).WithHistory() ctx := &command.Context{ Log: logger, @@ -1159,7 +1192,392 @@ $$$ }, } - r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false) + r := events.NewMarkdownRenderer( + false, // gitlabSupportsCommonMark + false, // disableApplyAll + false, // disableApply + false, // disableMarkdownFolding + false, // disableRepoLocking + false, // enableDiffMarkdownFormat + "", // markdownTemplateOverridesDir + "atlantis", // executableName + false, // hideUnchangedPlanComments + false, // quietPolicyChecks + ) + logger := logging.NewNoopLogger(t).WithHistory() + logText := "log" + logger.Info(logText) + ctx := &command.Context{ + Log: logger, + Pull: models.PullRequest{ + BaseRepo: models.Repo{ + VCSHost: models.VCSHost{ + Type: models.Github, + }, + }, + }, + } + for _, c := range cases { + t.Run(c.Description, func(t *testing.T) { + res := command.Result{ + ProjectResults: c.ProjectResults, + } + for _, verbose := range []bool{true, false} { + t.Run(c.Description, func(t *testing.T) { + cmd := &events.CommentCommand{ + Name: c.Command, + SubName: c.SubCommand, + Verbose: verbose, + } + s := r.Render(ctx, res, cmd) + if !verbose { + Equals(t, normalize(c.Expected), normalize(s)) + } else { + log := fmt.Sprintf("[INFO] %s", logText) + Equals(t, normalize(c.Expected+ + fmt.Sprintf("
Log\n

\n\n```\n%s\n```\n

", log)), normalize(s)) + } + }) + } + }) + } +} + +func TestRenderProjectResultsWithQuietPolicyChecks(t *testing.T) { + cases := []struct { + Description string + Command command.Name + SubCommand string + ProjectResults []command.ProjectResult + VCSHost models.VCSHostType + Expected string + }{ + { + "single successful policy check with multiple policy sets and project name", + command.PolicyCheck, + "", + []command.ProjectResult{ + { + PolicyCheckResults: &models.PolicyCheckResults{ + PolicySetResults: []models.PolicySetResult{ + { + PolicySetName: "policy1", + PolicyOutput: `FAIL - - main - WARNING: Null Resource creation is prohibited. + +2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`, + Passed: false, + ReqApprovals: 1, + }, + { + PolicySetName: "policy2", + PolicyOutput: "2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions", + Passed: true, + ReqApprovals: 1, + }, + }, + LockURL: "lock-url", + RePlanCmd: "atlantis plan -d path -w workspace", + ApplyCmd: "atlantis apply -d path -w workspace", + }, + Workspace: "workspace", + RepoRelDir: "path", + ProjectName: "projectname", + }, + }, + models.Github, + ` +Ran Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$ + +#### Policy Set: $policy1$ +$$$diff +FAIL - - main - WARNING: Null Resource creation is prohibited. + +2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions +$$$ + +#### Policy Set: $policy2$ +$$$diff +2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions +$$$ + + +#### Policy Approval Status: +$$$ +policy set: policy1: requires: 1 approval(s), have: 0. +policy set: policy2: passed. +$$$ +* :heavy_check_mark: To **approve** this project, comment: + $$$shell + + $$$ +* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url) +* :repeat: To re-run policies **plan** this project again by commenting: + $$$shell + atlantis plan -d path -w workspace + $$$ + +--- +* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment: + $$$shell + atlantis apply + $$$ +* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment: + $$$shell + atlantis unlock + $$$ +`, + }, + { + "single successful policy check with project name", + command.PolicyCheck, + "", + []command.ProjectResult{ + { + PolicyCheckResults: &models.PolicyCheckResults{ + PolicySetResults: []models.PolicySetResult{ + { + PolicySetName: "policy1", + // strings.Repeat require to get wrapped result + PolicyOutput: strings.Repeat("line\n", 13) + `FAIL - - main - WARNING: Null Resource creation is prohibited. + +2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`, + Passed: false, + ReqApprovals: 1, + }, + }, + LockURL: "lock-url", + RePlanCmd: "atlantis plan -d path -w workspace", + ApplyCmd: "atlantis apply -d path -w workspace", + }, + Workspace: "workspace", + RepoRelDir: "path", + ProjectName: "projectname", + }, + }, + models.Github, + ` +Ran Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$ + +
Show Output + +#### Policy Set: $policy1$ +$$$diff +line +line +line +line +line +line +line +line +line +line +line +line +line +FAIL - - main - WARNING: Null Resource creation is prohibited. + +2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions +$$$ + + +
+ +#### Policy Approval Status: +$$$ +policy set: policy1: requires: 1 approval(s), have: 0. +$$$ +* :heavy_check_mark: To **approve** this project, comment: + $$$shell + + $$$ +* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url) +* :repeat: To re-run policies **plan** this project again by commenting: + $$$shell + atlantis plan -d path -w workspace + $$$ +$$$ +policy set: policy1: 2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions +$$$ + +--- +* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment: + $$$shell + atlantis apply + $$$ +* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment: + $$$shell + atlantis unlock + $$$ +`, + }, + { + "multiple successful policy checks", + command.PolicyCheck, + "", + []command.ProjectResult{ + { + Workspace: "workspace", + RepoRelDir: "path", + PolicyCheckResults: &models.PolicyCheckResults{ + PolicySetResults: []models.PolicySetResult{ + { + PolicySetName: "policy1", + PolicyOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", + Passed: true, + }, + }, + LockURL: "lock-url", + ApplyCmd: "atlantis apply -d path -w workspace", + RePlanCmd: "atlantis plan -d path -w workspace", + }, + }, + { + Workspace: "workspace", + RepoRelDir: "path2", + ProjectName: "projectname", + PolicyCheckResults: &models.PolicyCheckResults{ + PolicySetResults: []models.PolicySetResult{ + { + PolicySetName: "policy1", + PolicyOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", + Passed: true, + }, + }, LockURL: "lock-url2", + ApplyCmd: "atlantis apply -d path2 -w workspace", + RePlanCmd: "atlantis plan -d path2 -w workspace", + }, + }, + }, + models.Github, + ` +Ran Policy Check for 2 projects: + +1. dir: $path$ workspace: $workspace$ +1. project: $projectname$ dir: $path2$ workspace: $workspace$ +--- + +* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment: + $$$shell + atlantis apply + $$$ +* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment: + $$$shell + atlantis unlock + $$$ +`, + }, + { + "successful, failed, and errored policy check", + command.PolicyCheck, + "", + []command.ProjectResult{ + { + Workspace: "workspace", + RepoRelDir: "path", + PolicyCheckResults: &models.PolicyCheckResults{ + PolicySetResults: []models.PolicySetResult{ + { + PolicySetName: "policy1", + PolicyOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", + Passed: true, + }, + }, LockURL: "lock-url", + ApplyCmd: "atlantis apply -d path -w workspace", + RePlanCmd: "atlantis plan -d path -w workspace", + }, + }, + { + Workspace: "workspace", + RepoRelDir: "path2", + Failure: "failure", + PolicyCheckResults: &models.PolicyCheckResults{ + PolicySetResults: []models.PolicySetResult{ + { + PolicySetName: "policy1", + PolicyOutput: "4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions", + Passed: false, + ReqApprovals: 1, + }, + }, LockURL: "lock-url", + ApplyCmd: "atlantis apply -d path -w workspace", + RePlanCmd: "atlantis plan -d path -w workspace", + }, + }, + { + Workspace: "workspace", + RepoRelDir: "path3", + ProjectName: "projectname", + Error: errors.New("error"), + }, + }, + models.Github, + ` +Ran Policy Check for 3 projects: + +1. dir: $path$ workspace: $workspace$ +1. dir: $path2$ workspace: $workspace$ +1. project: $projectname$ dir: $path3$ workspace: $workspace$ +--- + +### 2. dir: $path2$ workspace: $workspace$ +**Policy Check Failed**: failure +#### Policy Set: $policy1$ +$$$diff +4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions +$$$ + + +#### Policy Approval Status: +$$$ +policy set: policy1: requires: 1 approval(s), have: 0. +$$$ +* :heavy_check_mark: To **approve** this project, comment: + $$$shell + + $$$ +* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url) +* :repeat: To re-run policies **plan** this project again by commenting: + $$$shell + atlantis plan -d path -w workspace + $$$ + +--- +### 3. project: $projectname$ dir: $path3$ workspace: $workspace$ +**Policy Check Error** +$$$ +error +$$$ + +--- +* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment: + $$$shell + atlantis approve_policies + $$$ +* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment: + $$$shell + atlantis unlock + $$$ +* :repeat: To re-run policies **plan** this project again by commenting: + $$$shell + atlantis plan + $$$ +`, + }, + } + + r := events.NewMarkdownRenderer( + false, // gitlabSupportsCommonMark + false, // disableApplyAll + false, // disableApply + false, // disableMarkdownFolding + false, // disableRepoLocking + false, // enableDiffMarkdownFormat + "", // markdownTemplateOverridesDir + "atlantis", // executableName + false, // hideUnchangedPlanComments + true, // quietPolicyChecks + ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" logger.Info(logText) @@ -1356,9 +1774,10 @@ $$$ false, // disableMarkdownFolding false, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -1540,9 +1959,10 @@ $$$ false, // disableMarkdownFolding false, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -1598,9 +2018,10 @@ func TestRenderCustomPolicyCheckTemplate_DisableApplyAll(t *testing.T) { false, // disableMarkdownFolding false, // disableRepoLocking false, // enableDiffMarkdownFormat - tmpDir, // MarkdownTemplateOverridesDir + tmpDir, // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -1672,9 +2093,10 @@ func TestRenderProjectResults_DisableFolding(t *testing.T) { true, // disableMarkdownFolding false, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -1781,9 +2203,10 @@ func TestRenderProjectResults_WrappedErr(t *testing.T) { false, // disableMarkdownFolding false, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -1926,9 +2349,10 @@ func TestRenderProjectResults_WrapSingleProject(t *testing.T) { false, // disableMarkdownFolding false, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -2076,9 +2500,10 @@ func TestRenderProjectResults_MultiProjectApplyWrapped(t *testing.T) { false, // disableMarkdownFolding false, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -2155,9 +2580,10 @@ func TestRenderProjectResults_MultiProjectPlanWrapped(t *testing.T) { false, // disableMarkdownFolding false, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -2381,9 +2807,10 @@ This plan was not saved because one or more projects failed and automerge requir false, // disableMarkdownFolding false, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -2937,9 +3364,10 @@ $$$ false, // disableMarkdownFolding true, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -3074,9 +3502,10 @@ $$$ false, // disableMarkdownFolding true, // disableRepoLocking false, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -3533,9 +3962,10 @@ func TestRenderProjectResultsWithEnableDiffMarkdownFormat(t *testing.T) { false, // disableMarkdownFolding false, // disableRepoLocking true, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" @@ -3588,9 +4018,10 @@ func BenchmarkRenderProjectResultsWithEnableDiffMarkdownFormat(b *testing.B) { false, // disableMarkdownFolding false, // disableRepoLocking true, // enableDiffMarkdownFormat - "", // MarkdownTemplateOverridesDir + "", // markdownTemplateOverridesDir "atlantis", // executableName false, // hideUnchangedPlanComments + false, // quietPolicyChecks ) logger := logging.NewNoopLogger(b).WithHistory() logText := "log" @@ -3793,7 +4224,18 @@ Ran Plan for 3 projects: }, } - r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", true) + r := events.NewMarkdownRenderer( + false, // gitlabSupportsCommonMark + false, // disableApplyAll + false, // disableApply + false, // disableMarkdownFolding + false, // disableRepoLocking + false, // enableDiffMarkdownFormat + "", // markdownTemplateOverridesDir + "atlantis", // executableName + true, // hideUnchangedPlanComments + false, // quietPolicyChecks + ) logger := logging.NewNoopLogger(t).WithHistory() logText := "log" logger.Info(logText) diff --git a/server/events/templates/multi_project_policy.tmpl b/server/events/templates/multi_project_policy.tmpl index add574fde4..276dfe2b72 100644 --- a/server/events/templates/multi_project_policy.tmpl +++ b/server/events/templates/multi_project_policy.tmpl @@ -2,8 +2,10 @@ {{ template "multiProjectHeader" . -}} {{ $disableApplyAll := .DisableApplyAll -}} {{ $hideUnchangedPlans := .HideUnchangedPlanComments -}} +{{ $quietPolicyChecks := .QuietPolicyChecks -}} {{ range $i, $result := .Results -}} {{ if (and $hideUnchangedPlans $result.NoChanges) }}{{continue}}{{end -}} +{{ if (and $quietPolicyChecks $result.IsSuccessful) }}{{continue}}{{end -}} ### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}` {{ $result.Rendered }} diff --git a/server/events/templates/multi_project_policy_unsuccessful.tmpl b/server/events/templates/multi_project_policy_unsuccessful.tmpl index 039dd9ce7c..7e11821dfd 100644 --- a/server/events/templates/multi_project_policy_unsuccessful.tmpl +++ b/server/events/templates/multi_project_policy_unsuccessful.tmpl @@ -1,7 +1,9 @@ {{ define "multiProjectPolicyUnsuccessful" -}} {{ template "multiProjectHeader" . -}} {{ $disableApplyAll := .DisableApplyAll -}} +{{ $quietPolicyChecks := .QuietPolicyChecks -}} {{ range $i, $result := .Results -}} +{{ if (and $quietPolicyChecks $result.IsSuccessful) }}{{continue}}{{end -}} ### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}` {{ $result.Rendered }} diff --git a/server/server.go b/server/server.go index a77eeddaf8..6f4e31c497 100644 --- a/server/server.go +++ b/server/server.go @@ -460,6 +460,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { userConfig.MarkdownTemplateOverridesDir, userConfig.ExecutableName, userConfig.HideUnchangedPlanComments, + userConfig.QuietPolicyChecks, ) var lockingClient locking.Locker From f3ed6c44446b1fac80bffb8b48f661d60a66734c Mon Sep 17 00:00:00 2001 From: Lukas Aldershaab Date: Fri, 17 Jan 2025 16:12:03 +0100 Subject: [PATCH 19/60] build: Enable Docker Go Cross-compile to improve build times (#5223) Signed-off-by: Lukas Peter Aldershaab --- Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 646d172e2a..7c77d6682a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,11 @@ ARG DEFAULT_CONFTEST_VERSION=0.56.0 # Stage 1: build artifact and download deps -FROM golang:${GOLANG_TAG} AS builder +FROM --platform=$BUILDPLATFORM golang:${GOLANG_TAG} AS builder + +# These are automatically populated by Docker +ARG TARGETOS +ARG TARGETARCH ARG ATLANTIS_VERSION=dev ENV ATLANTIS_VERSION=${ATLANTIS_VERSION} @@ -42,7 +46,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \ COPY . /app RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ - CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X 'main.version=${ATLANTIS_VERSION}' -X 'main.commit=${ATLANTIS_COMMIT}' -X 'main.date=${ATLANTIS_DATE}'" -v -o atlantis . + CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags "-s -w -X 'main.version=${ATLANTIS_VERSION}' -X 'main.commit=${ATLANTIS_COMMIT}' -X 'main.date=${ATLANTIS_DATE}'" -v -o atlantis . FROM debian:${DEBIAN_TAG} AS debian-base From cfe9df7ea61de8cf72a812b4d61a4fd95dc86d2e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 00:26:48 +0000 Subject: [PATCH 20/60] chore(deps): update dependency hashicorp/terraform to v1.10.4 in testdrive/utils.go (main) (#5248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Dockerfile | 2 +- testdrive/utils.go | 2 +- testing/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7c77d6682a..cb14a17f4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ ARG DEBIAN_TAG=12.8-slim@sha256:d365f4920711a9074c4bcd178e8f457ee59250426441ab2a ARG GOLANG_TAG=1.23.4-alpine@sha256:c23339199a08b0e12032856908589a6d41a0dab141b8b3b21f156fc571a3f1d3 # renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp -ARG DEFAULT_TERRAFORM_VERSION=1.10.3 +ARG DEFAULT_TERRAFORM_VERSION=1.10.4 # renovate: datasource=github-releases depName=opentofu/opentofu versioning=hashicorp ARG DEFAULT_OPENTOFU_VERSION=1.8.8 # renovate: datasource=github-releases depName=open-policy-agent/conftest diff --git a/testdrive/utils.go b/testdrive/utils.go index 872e750d4f..50f3bf2555 100644 --- a/testdrive/utils.go +++ b/testdrive/utils.go @@ -35,7 +35,7 @@ import ( ) const hashicorpReleasesURL = "https://releases.hashicorp.com" -const terraformVersion = "1.10.3" // renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp +const terraformVersion = "1.10.4" // renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp const ngrokDownloadURL = "https://bin.equinox.io/c/4VmDzA7iaHb" const ngrokAPIURL = "localhost:41414" // We hope this isn't used. const atlantisPort = 4141 diff --git a/testing/Dockerfile b/testing/Dockerfile index 875e4dc556..a676d3b858 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -6,7 +6,7 @@ RUN apt-get update && apt-get --no-install-recommends -y install unzip \ # Install Terraform # renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp -ENV TERRAFORM_VERSION=1.10.3 +ENV TERRAFORM_VERSION=1.10.4 RUN case $(uname -m) in x86_64|amd64) ARCH="amd64" ;; aarch64|arm64|armv7l) ARCH="arm64" ;; esac && \ wget -nv -O terraform.zip https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${ARCH}.zip && \ mkdir -p /usr/local/bin/tf/versions/${TERRAFORM_VERSION} && \ From 3e52125f876d10e3eb46fbc6b79286f1c4057fa2 Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Sun, 19 Jan 2025 14:28:58 -0500 Subject: [PATCH 21/60] chore: Fix build CI mismatch (#5253) Signed-off-by: Luke Massa --- .github/workflows/atlantis-image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/atlantis-image.yml b/.github/workflows/atlantis-image.yml index e90cc65542..0d11dedafd 100644 --- a/.github/workflows/atlantis-image.yml +++ b/.github/workflows/atlantis-image.yml @@ -241,6 +241,7 @@ jobs: strategy: matrix: image_type: [alpine, debian] + platform: [linux/arm64/v8, linux/amd64, linux/arm/v7] runs-on: ubuntu-24.04 steps: - run: 'echo "No build required"' From 2350db934998de158b8bc898b151e400f18751a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:31:05 +0000 Subject: [PATCH 22/60] chore(deps): update golangci/golangci-lint-action digest to ec5d184 in .github/workflows/lint.yml (main) (#5243) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bcfb8bc3c0..894e42de15 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -55,7 +55,7 @@ jobs: go-version-file: go.mod - name: golangci-lint - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6 + uses: golangci/golangci-lint-action@ec5d18412c0aeab7936cb16880d708ba2a64e1ae # v6 with: # renovate: datasource=github-releases depName=golangci/golangci-lint version: v1.62.2 From bbff4dbc4fdc8fd8d05a3c8633d9b9683a85be5f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:31:57 +0000 Subject: [PATCH 23/60] chore(deps): update ghcr.io/runatlantis/testing-env:latest docker digest to 3d7b17d in .github/workflows/test.yml (main) (#5240) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5884a2adfc..7d53ebac06 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: if: needs.changes.outputs.should-run-tests == 'true' name: Tests runs-on: ubuntu-24.04 - container: ghcr.io/runatlantis/testing-env:latest@sha256:45ec58ba11af5196fb70ced526ccb1996f0e58a7dbd93f7dcba96eed49209583 + container: ghcr.io/runatlantis/testing-env:latest@sha256:3d7b17d02ced2cb68ecc9d2ea3d2bef61fe8da52cf1631e4dff4de6503cb7237 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 From 693c092a8c1fd4655fc487d716b643ae5a376bd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 14:32:32 -0500 Subject: [PATCH 24/60] chore(deps): bump katex from 0.16.15 to 0.16.21 (#5251) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Luke Massa --- package-lock.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01ce075145..16bf10ee0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3070,15 +3070,14 @@ } }, "node_modules/katex": { - "version": "0.16.15", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.15.tgz", - "integrity": "sha512-yE9YJIEAk2aZ+FL/G8r+UGw0CTUzEA8ZFy6E+8tc3spHUKq3qBnzCkI1CQwGoI9atJhVyFPEypQsTY7mJ1Pi9w==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "dev": true, "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], - "license": "MIT", "dependencies": { "commander": "^8.3.0" }, From eac23567aeca31aae323b595c1e8f52cd1250a32 Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Sun, 19 Jan 2025 16:07:18 -0500 Subject: [PATCH 25/60] chore: Split up buildAllCommandsByCfg for readability and modularity (#5245) Signed-off-by: Luke Massa --- server/events/project_command_builder.go | 300 ++++++++++++----------- 1 file changed, 155 insertions(+), 145 deletions(-) diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 275e8cbfbc..84a6fc860f 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -318,118 +318,69 @@ func (p *DefaultProjectCommandBuilder) BuildStateRmCommands(ctx *command.Context return p.buildProjectCommand(ctx, cmd) } -// buildAllCommandsByCfg builds init contexts for all projects we determine were -// modified in this ctx. -func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Context, cmdName command.Name, subCmdName string, commentFlags []string, verbose bool) ([]command.ProjectContext, error) { - // We'll need the list of modified files. - modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull) +// shouldSkipClone determines whether we should skip cloning for a given context +func (p *DefaultProjectCommandBuilder) shouldSkipClone(ctx *command.Context, modifiedFiles []string) (bool, error) { + // NOTE: We discard this work here and end up doing it again after + // cloning to ensure all the return values are set properly with + // the actual clone directory. + + if !p.SkipCloneNoChanges || !p.VCSClient.SupportsSingleFileDownload(ctx.Pull.BaseRepo) { + return false, nil + } + repoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID()) + hasRepoCfg, repoCfgData, err := p.VCSClient.GetFileContent(ctx.Log, ctx.Pull, repoCfgFile) if err != nil { - return nil, err + return false, errors.Wrapf(err, "downloading %s", repoCfgFile) } - - if p.IncludeGitUntrackedFiles { - ctx.Log.Debug(("'include-git-untracked-files' option is set, getting untracked files")) - untrackedFiles, err := p.WorkingDir.GetGitUntrackedFiles(ctx.Log, ctx.HeadRepo, ctx.Pull, DefaultWorkspace) - if err != nil { - return nil, err - } - modifiedFiles = append(modifiedFiles, untrackedFiles...) + // We can only skip if we determine that none of the modified files belong to projects configured in a repo config + if !hasRepoCfg { + return false, nil } - - ctx.Log.Debug("%d files were modified in this pull request. Modified files: %v", len(modifiedFiles), modifiedFiles) - - // Get default AutoDiscoverMode from userConfig/globalConfig - defaultAutoDiscoverMode := valid.AutoDiscoverMode(p.AutoDiscoverMode) - globalAutoDiscover := p.GlobalCfg.RepoAutoDiscoverCfg(ctx.Pull.BaseRepo.ID()) - if globalAutoDiscover != nil { - defaultAutoDiscoverMode = globalAutoDiscover.Mode + repoCfg, err := p.ParserValidator.ParseRepoCfgData(repoCfgData, p.GlobalCfg, ctx.Pull.BaseRepo.ID(), ctx.Pull.BaseBranch) + if err != nil { + return false, errors.Wrapf(err, "parsing %s", repoCfgFile) } + ctx.Log.Info("successfully parsed remote %s file", repoCfgFile) - if p.SkipCloneNoChanges && p.VCSClient.SupportsSingleFileDownload(ctx.Pull.BaseRepo) { - repoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID()) - hasRepoCfg, repoCfgData, err := p.VCSClient.GetFileContent(ctx.Log, ctx.Pull, repoCfgFile) - if err != nil { - return nil, errors.Wrapf(err, "downloading %s", repoCfgFile) - } - - if hasRepoCfg { - repoCfg, err := p.ParserValidator.ParseRepoCfgData(repoCfgData, p.GlobalCfg, ctx.Pull.BaseRepo.ID(), ctx.Pull.BaseBranch) - if err != nil { - return nil, errors.Wrapf(err, "parsing %s", repoCfgFile) - } - ctx.Log.Info("successfully parsed remote %s file", repoCfgFile) - - if repoCfg.AutoDiscover != nil { - defaultAutoDiscoverMode = repoCfg.AutoDiscover.Mode - } - // If auto discover is enabled, we never want to skip cloning - if !repoCfg.AutoDiscoverEnabled(defaultAutoDiscoverMode) { - if len(repoCfg.Projects) > 0 { - matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, "", nil) - if err != nil { - return nil, err - } - ctx.Log.Info("%d projects are changed on MR %d based on their when_modified config", len(matchingProjects), ctx.Pull.Num) - if len(matchingProjects) == 0 { - ctx.Log.Info("skipping repo clone since no project was modified") - return []command.ProjectContext{}, nil - } - } else { - ctx.Log.Info("no projects are defined in %s. Will resume automatic detection", repoCfgFile) - } - } else { - ctx.Log.Info("automatic project discovery enabled. Will resume automatic detection") - } - // NOTE: We discard this work here and end up doing it again after - // cloning to ensure all the return values are set properly with - // the actual clone directory. - } + // If auto discover is enabled, we never want to skip cloning + if p.autoDiscoverModeEnabled(ctx, repoCfg) { + ctx.Log.Info("automatic project discovery enabled. Will resume automatic detection") + return false, nil } - // Need to lock the workspace we're about to clone to. - workspace := DefaultWorkspace - - unlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, workspace, DefaultRepoRelDir) - if err != nil { - ctx.Log.Warn("workspace was locked") - return nil, err + if len(repoCfg.Projects) == 0 { + ctx.Log.Info("no projects are defined in %s. Will resume automatic detection", repoCfgFile) + return false, nil } - ctx.Log.Debug("got workspace lock") - defer unlockFn() - repoDir, _, err := p.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, workspace) + matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, "", nil) if err != nil { - return nil, err + return false, err } - // Parse config file if it exists. - repoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID()) - hasRepoCfg, err := p.ParserValidator.HasRepoCfg(repoDir, repoCfgFile) - if err != nil { - return nil, errors.Wrapf(err, "looking for '%s' file in '%s'", repoCfgFile, repoDir) + ctx.Log.Info("%d projects are changed on MR %d based on their when_modified config", len(matchingProjects), ctx.Pull.Num) + if len(matchingProjects) == 0 { + ctx.Log.Info("skipping repo clone since no project was modified") + return true, nil } - var projCtxs []command.ProjectContext - var repoCfg valid.RepoCfg + return false, nil - if hasRepoCfg { - // If there's a repo cfg with projects then we'll use it to figure out which projects - // should be planed. - repoCfg, err = p.ParserValidator.ParseRepoCfg(repoDir, p.GlobalCfg, ctx.Pull.BaseRepo.ID(), ctx.Pull.BaseBranch) - if err != nil { - return nil, errors.Wrapf(err, "parsing %s", repoCfgFile) - } - ctx.Log.Info("successfully parsed %s file", repoCfgFile) - // It's possible we've already set defaultAutoDiscoverMode - // from the config file while checking whether we can skip - // cloning. We still need to set it here in the case that - // we were not able to check whether we can skip cloning - // and thus were not able to previously fetch the repo - // config. - if repoCfg.AutoDiscover != nil { - defaultAutoDiscoverMode = repoCfg.AutoDiscover.Mode - } +} + +// autoDiscoverModeEnabled determines whether to use autodiscover +func (p *DefaultProjectCommandBuilder) autoDiscoverModeEnabled(ctx *command.Context, repoCfg valid.RepoCfg) bool { + defaultAutoDiscoverMode := valid.AutoDiscoverMode(p.AutoDiscoverMode) + globalAutoDiscover := p.GlobalCfg.RepoAutoDiscoverCfg(ctx.Pull.BaseRepo.ID()) + if globalAutoDiscover != nil { + defaultAutoDiscoverMode = globalAutoDiscover.Mode } + return repoCfg.AutoDiscoverEnabled(defaultAutoDiscoverMode) +} + +// getMergedProjectCfgs gets all merged project configs for building commands given a context and a clone repo +func (p *DefaultProjectCommandBuilder) getMergedProjectCfgs(ctx *command.Context, repoDir string, modifiedFiles []string, repoCfg valid.RepoCfg, hasRepoCfg bool, repoCfgFile string) ([]valid.MergedProjectCfg, error) { + mergedCfgs := make([]valid.MergedProjectCfg, 0) moduleInfo, err := FindModuleProjects(repoDir, p.AutoDetectModuleFiles) if err != nil { @@ -437,23 +388,6 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex } ctx.Log.Debug("moduleInfo for '%s' (matching '%s') = %v", repoDir, p.AutoDetectModuleFiles, moduleInfo) - automerge := p.EnableAutoMerge - parallelApply := p.EnableParallelApply - parallelPlan := p.EnableParallelPlan - abortOnExecutionOrderFail := DefaultAbortOnExecutionOrderFail - if hasRepoCfg { - if repoCfg.Automerge != nil { - automerge = *repoCfg.Automerge - } - if repoCfg.ParallelApply != nil { - parallelApply = *repoCfg.ParallelApply - } - if repoCfg.ParallelPlan != nil { - parallelPlan = *repoCfg.ParallelPlan - } - abortOnExecutionOrderFail = repoCfg.AbortOnExecutionOrderFail - } - if len(repoCfg.Projects) > 0 { matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, repoDir, moduleInfo) if err != nil { @@ -464,26 +398,11 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex for _, mp := range matchingProjects { ctx.Log.Debug("determining config for project at dir: '%s' workspace: '%s'", mp.Dir, mp.Workspace) mergedCfg := p.GlobalCfg.MergeProjectCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), mp, repoCfg) - - projCtxs = append(projCtxs, - p.ProjectCommandContextBuilder.BuildProjectContext( - ctx, - cmdName, - subCmdName, - mergedCfg, - commentFlags, - repoDir, - automerge, - parallelApply, - parallelPlan, - verbose, - abortOnExecutionOrderFail, - p.TerraformExecutor, - )...) + mergedCfgs = append(mergedCfgs, mergedCfg) } } - if repoCfg.AutoDiscoverEnabled(defaultAutoDiscoverMode) { + if p.autoDiscoverModeEnabled(ctx, repoCfg) { // If there is no config file or it specified no projects, then we'll plan each project that // our algorithm determines was modified. if hasRepoCfg { @@ -526,23 +445,114 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex } pCfg := p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), mp.Path, pWorkspace) + mergedCfgs = append(mergedCfgs, pCfg) + } + } + return mergedCfgs, nil +} - projCtxs = append(projCtxs, - p.ProjectCommandContextBuilder.BuildProjectContext( - ctx, - cmdName, - subCmdName, - pCfg, - commentFlags, - repoDir, - automerge, - parallelApply, - parallelPlan, - verbose, - abortOnExecutionOrderFail, - p.TerraformExecutor, - )...) +// buildAllCommandsByCfg builds init contexts for all projects we determine were +// modified in this ctx. +func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Context, cmdName command.Name, subCmdName string, commentFlags []string, verbose bool) ([]command.ProjectContext, error) { + // We'll need the list of modified files. + modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull) + if err != nil { + return nil, err + } + + if p.IncludeGitUntrackedFiles { + ctx.Log.Debug(("'include-git-untracked-files' option is set, getting untracked files")) + untrackedFiles, err := p.WorkingDir.GetGitUntrackedFiles(ctx.Log, ctx.HeadRepo, ctx.Pull, DefaultWorkspace) + if err != nil { + return nil, err } + modifiedFiles = append(modifiedFiles, untrackedFiles...) + } + + ctx.Log.Debug("%d files were modified in this pull request. Modified files: %v", len(modifiedFiles), modifiedFiles) + + shouldSkipClone, err := p.shouldSkipClone(ctx, modifiedFiles) + if err != nil { + return nil, err + } + if shouldSkipClone { + return []command.ProjectContext{}, nil + } + + // Need to lock the workspace we're about to clone to. + workspace := DefaultWorkspace + + unlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, workspace, DefaultRepoRelDir) + if err != nil { + ctx.Log.Warn("workspace was locked") + return nil, err + } + ctx.Log.Debug("got workspace lock") + defer unlockFn() + + repoDir, _, err := p.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, workspace) + if err != nil { + return nil, err + } + + // Parse config file if it exists. + repoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID()) + hasRepoCfg, err := p.ParserValidator.HasRepoCfg(repoDir, repoCfgFile) + if err != nil { + return nil, errors.Wrapf(err, "looking for '%s' file in '%s'", repoCfgFile, repoDir) + } + + var projCtxs []command.ProjectContext + var repoCfg valid.RepoCfg + + if hasRepoCfg { + // If there's a repo cfg with projects then we'll use it to figure out which projects + // should be planed. + repoCfg, err = p.ParserValidator.ParseRepoCfg(repoDir, p.GlobalCfg, ctx.Pull.BaseRepo.ID(), ctx.Pull.BaseBranch) + if err != nil { + return nil, errors.Wrapf(err, "parsing %s", repoCfgFile) + } + ctx.Log.Info("successfully parsed %s file", repoCfgFile) + } + + mergedProjectCfgs, err := p.getMergedProjectCfgs(ctx, repoDir, modifiedFiles, repoCfg, hasRepoCfg, repoCfgFile) + if err != nil { + return nil, err + } + + automerge := p.EnableAutoMerge + parallelApply := p.EnableParallelApply + parallelPlan := p.EnableParallelPlan + abortOnExecutionOrderFail := DefaultAbortOnExecutionOrderFail + if hasRepoCfg { + if repoCfg.Automerge != nil { + automerge = *repoCfg.Automerge + } + if repoCfg.ParallelApply != nil { + parallelApply = *repoCfg.ParallelApply + } + if repoCfg.ParallelPlan != nil { + parallelPlan = *repoCfg.ParallelPlan + } + abortOnExecutionOrderFail = repoCfg.AbortOnExecutionOrderFail + } + + for _, mergedProjectCfg := range mergedProjectCfgs { + projCtxs = append(projCtxs, + p.ProjectCommandContextBuilder.BuildProjectContext( + ctx, + cmdName, + subCmdName, + mergedProjectCfg, + commentFlags, + repoDir, + automerge, + parallelApply, + parallelPlan, + verbose, + abortOnExecutionOrderFail, + p.TerraformExecutor, + )...) } sort.Slice(projCtxs, func(i, j int) bool { From 713bc7dee70308228466cdef02b7b2d651051261 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 01:40:36 +0000 Subject: [PATCH 26/60] chore(deps): update davidanson/markdownlint-cli2-action digest to 05f3221 in .github/workflows/website.yml (main) (#5256) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/website.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 15895346c6..5b329549ce 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: markdown-lint - uses: DavidAnson/markdownlint-cli2-action@a23dae216ce3fee4db69da41fed90d2a4af801cf # v19 + uses: DavidAnson/markdownlint-cli2-action@05f32210e84442804257b2a6f20b273450ec8265 # v19 with: config: .markdownlint.yaml globs: 'runatlantis.io/**/*.md' From 7b4576a2009b8648bb4c3f9bfee1179c048397f4 Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Sun, 19 Jan 2025 23:23:28 -0500 Subject: [PATCH 27/60] chore: Clarify logs in determining merged project configs (#5255) Signed-off-by: Luke Massa --- server/events/project_command_builder.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 84a6fc860f..c06059dd33 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -379,7 +379,7 @@ func (p *DefaultProjectCommandBuilder) autoDiscoverModeEnabled(ctx *command.Cont } // getMergedProjectCfgs gets all merged project configs for building commands given a context and a clone repo -func (p *DefaultProjectCommandBuilder) getMergedProjectCfgs(ctx *command.Context, repoDir string, modifiedFiles []string, repoCfg valid.RepoCfg, hasRepoCfg bool, repoCfgFile string) ([]valid.MergedProjectCfg, error) { +func (p *DefaultProjectCommandBuilder) getMergedProjectCfgs(ctx *command.Context, repoDir string, modifiedFiles []string, repoCfg valid.RepoCfg) ([]valid.MergedProjectCfg, error) { mergedCfgs := make([]valid.MergedProjectCfg, 0) moduleInfo, err := FindModuleProjects(repoDir, p.AutoDetectModuleFiles) @@ -403,17 +403,8 @@ func (p *DefaultProjectCommandBuilder) getMergedProjectCfgs(ctx *command.Context } if p.autoDiscoverModeEnabled(ctx, repoCfg) { - // If there is no config file or it specified no projects, then we'll plan each project that - // our algorithm determines was modified. - if hasRepoCfg { - if len(repoCfg.Projects) == 0 { - ctx.Log.Info("no projects are defined in %s. Will resume automatic detection", repoCfgFile) - } else { - ctx.Log.Info("automatic project discovery enabled. Will resume automatic detection") - } - } else { - ctx.Log.Info("found no %s file", repoCfgFile) - } + ctx.Log.Info("automatic project discovery enabled. Will run automatic detection") + // build a module index for projects that are explicitly included allModifiedProjects := p.ProjectFinder.DetermineProjects( ctx.Log, modifiedFiles, ctx.Pull.BaseRepo.FullName, repoDir, p.AutoplanFileList, moduleInfo) @@ -513,9 +504,11 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex return nil, errors.Wrapf(err, "parsing %s", repoCfgFile) } ctx.Log.Info("successfully parsed %s file", repoCfgFile) + } else { + ctx.Log.Info("repo config file %s is absent, using global defaults", repoCfg) } - mergedProjectCfgs, err := p.getMergedProjectCfgs(ctx, repoDir, modifiedFiles, repoCfg, hasRepoCfg, repoCfgFile) + mergedProjectCfgs, err := p.getMergedProjectCfgs(ctx, repoDir, modifiedFiles, repoCfg) if err != nil { return nil, err } From 4830ad79394eaf13456a17d1a9ecd1da3f7cfd46 Mon Sep 17 00:00:00 2001 From: RB <7775707+nitrocode@users.noreply.github.com> Date: Mon, 20 Jan 2025 10:57:55 -0600 Subject: [PATCH 28/60] docs: github emu mention on hiding prev comments (#5221) Signed-off-by: RB <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/server-configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index cf290dc5ca..3db678573d 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -763,7 +763,7 @@ based on the organization or user that triggered the webhook. ATLANTIS_GH_USER="myuser" ``` - GitHub username of API user. + GitHub username of API user. This user is also used by the flag `--hide-user-plan-comments` and will need to be updated if migrating to github EMU. ### `--gh-webhook-secret` @@ -844,6 +844,7 @@ based on the organization or user that triggered the webhook. Hide previous plan comments to declutter PRs. This is only supported in GitHub and GitLab currently. This is not enabled by default. When using Github App, you need to set `--gh-app-slug` to enable this feature. + For github, ensure the `--gh-user` is set appropriately or comments will not be hidden. ### `--hide-unchanged-plan-comments` From 49ddf706c6b0072d0456672545cd88cd17b15884 Mon Sep 17 00:00:00 2001 From: Dan Urson Date: Mon, 20 Jan 2025 15:54:40 -0500 Subject: [PATCH 29/60] fix: ensure multi-image builds succeed (#5257) Signed-off-by: Dan Urson --- .github/workflows/atlantis-image.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/atlantis-image.yml b/.github/workflows/atlantis-image.yml index 0d11dedafd..e341bc0fb6 100644 --- a/.github/workflows/atlantis-image.yml +++ b/.github/workflows/atlantis-image.yml @@ -53,7 +53,6 @@ jobs: strategy: matrix: image_type: [alpine, debian] - platform: [linux/arm64/v8, linux/amd64, linux/arm/v7] runs-on: ubuntu-24.04 env: # Set docker repo to either the fork or the main repo where the branch exists @@ -156,7 +155,7 @@ jobs: ATLANTIS_VERSION=${{ env.RELEASE_VERSION }} ATLANTIS_COMMIT=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} ATLANTIS_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} - platforms: ${{ matrix.platform }} + platforms: linux/arm64/v8, linux/amd64, linux/arm/v7 push: ${{ env.PUSH }} tags: ${{ steps.meta.outputs.tags }} target: ${{ matrix.image_type }} @@ -173,13 +172,11 @@ jobs: - name: "Sign images with environment annotations" # no key needed, we're using the GitHub OIDC flow - # Only run on alpine/amd64 build to avoid signing multiple times - if: env.PUSH == 'true' && github.event_name != 'pull_request' && matrix.image_type == 'alpine' && matrix.platform == 'linux/amd64' + if: env.PUSH == 'true' && github.event_name != 'pull_request' run: | # Sign dev tags, version tags, and latest tags echo "${TAGS}" | xargs -I {} cosign sign \ --yes \ - --recursive=true \ -a actor=${{ github.actor}} \ -a ref_name=${{ github.ref_name}} \ -a ref=${{ github.sha }} \ @@ -203,7 +200,6 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3 # https://github.com/docker/build-push-action/issues/761#issuecomment-1575006515 From 0bdcdec4415196bcc9ccd0c3c8da627b8290cc5d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 01:35:26 +0000 Subject: [PATCH 30/60] chore(deps): update dependency git-lfs/git-lfs to v3.6.1 in dockerfile (main) (#5258) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cb14a17f4d..aeccec396f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -98,7 +98,7 @@ RUN AVAILABLE_CONFTEST_VERSIONS=${DEFAULT_CONFTEST_VERSION} && \ # install git-lfs # renovate: datasource=github-releases depName=git-lfs/git-lfs -ENV GIT_LFS_VERSION=3.6.0 +ENV GIT_LFS_VERSION=3.6.1 RUN case ${TARGETPLATFORM} in \ "linux/amd64") GIT_LFS_ARCH=amd64 ;; \ From 69054861afbd55d216d6a5e9ea0100652d248436 Mon Sep 17 00:00:00 2001 From: Clement Debiaune Date: Wed, 22 Jan 2025 04:56:22 +0000 Subject: [PATCH 31/60] feat: Trim backticks from MR comments (for Gitlab) (#5244) Signed-off-by: Clement Debiaune Co-authored-by: PePe Amengual <2208324+jamengual@users.noreply.github.com> Co-authored-by: Simon Heather <32168619+X-Guardian@users.noreply.github.com> --- server/events/comment_parser.go | 1 + server/events/comment_parser_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index 829c15ced9..36cf0651ec 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -145,6 +145,7 @@ type CommentParseResult struct { // - atlantis import ADDRESS ID func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) CommentParseResult { comment := strings.TrimSpace(rawComment) + comment = strings.Trim(comment, "`") if multiLineRegex.MatchString(comment) { return CommentParseResult{Ignore: true} diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index 88ededcfff..bf48469302 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -144,6 +144,33 @@ func TestParse_HelpResponse(t *testing.T) { } } +func TestParse_TrimCommandString(t *testing.T) { + t.Log("commands should be trimmed of whitespace and backtick (helps with Gitlab copy/paste issues)") + allowCommandsCases := [][]command.Name{ + command.AllCommentCommands, + {}, // empty case + } + helpComments := []string{ + "`atlantis help`", + "` atlantis help `", + "`atlantis help` ", + " `atlantis help", + } + for _, allowCommandCase := range allowCommandsCases { + for _, c := range helpComments { + t.Run(fmt.Sprintf("%s with allow commands %v", c, allowCommandCase), func(t *testing.T) { + commentParser := events.CommentParser{ + GithubUser: "github-user", + ExecutableName: "atlantis", + AllowCommands: allowCommandCase, + } + r := commentParser.Parse(c, models.Github) + Equals(t, commentParser.HelpComment(), r.CommentResponse) + }) + } + } +} + func TestParse_UnusedArguments(t *testing.T) { t.Log("if there are unused flags we return an error") cases := []struct { From 6642a885b6fb5c108c48baae04df3a53b9282313 Mon Sep 17 00:00:00 2001 From: Lukas Aldershaab Date: Wed, 22 Jan 2025 06:08:00 +0100 Subject: [PATCH 32/60] feat: Add rate limit handling for GitHub client (#5226) Signed-off-by: Lukas Peter Aldershaab Co-authored-by: PePe Amengual <2208324+jamengual@users.noreply.github.com> --- go.mod | 1 + go.sum | 2 + server/events/vcs/github_client.go | 16 ++++++-- server/events/vcs/github_client_test.go | 52 +++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index aff4490023..6e02a24ca1 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/go-ozzo/ozzo-validation v3.6.0+incompatible github.com/go-playground/validator/v10 v10.23.0 github.com/go-test/deep v1.1.1 + github.com/gofri/go-github-ratelimit v1.1.0 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-github/v68 v68.0.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 diff --git a/go.sum b/go.sum index e67437d296..08a87b8937 100644 --- a/go.sum +++ b/go.sum @@ -163,6 +163,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4 github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofri/go-github-ratelimit v1.1.0 h1:ijQ2bcv5pjZXNil5FiwglCg8wc9s8EgjTmNkqjw8nuk= +github.com/gofri/go-github-ratelimit v1.1.0/go.mod h1:OnCi5gV+hAG/LMR7llGhU7yHt44se9sYgKPnafoL7RY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index 9d13bb2587..c75c0e07bd 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "github.com/gofri/go-github-ratelimit/github_ratelimit" "github.com/google/go-github/v68/github" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/events/command" @@ -124,15 +125,24 @@ func NewGithubClient(hostname string, credentials GithubCredentials, config Gith return nil, errors.Wrap(err, "error initializing github authentication transport") } + transportWithRateLimit, err := github_ratelimit.NewRateLimitWaiterClient( + transport.Transport, + github_ratelimit.WithTotalSleepLimit(time.Minute, func(callbackContext *github_ratelimit.CallbackContext) { + logger.Warn("github rate limit exceeded total sleep time, requests will fail to avoid penalties from github") + })) + if err != nil { + return nil, errors.Wrap(err, "error initializing github rate limit transport") + } + var graphqlURL string var client *github.Client if hostname == "github.com" { - client = github.NewClient(transport) + client = github.NewClient(transportWithRateLimit) graphqlURL = "https://api.github.com/graphql" } else { apiURL := resolveGithubAPIURL(hostname) // TODO: Deprecated: Use NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) instead - client, err = github.NewEnterpriseClient(apiURL.String(), apiURL.String(), transport) //nolint:staticcheck + client, err = github.NewEnterpriseClient(apiURL.String(), apiURL.String(), transportWithRateLimit) //nolint:staticcheck if err != nil { return nil, err } @@ -140,7 +150,7 @@ func NewGithubClient(hostname string, credentials GithubCredentials, config Gith } // Use the client from shurcooL's githubv4 library for queries. - v4Client := githubv4.NewEnterpriseClient(graphqlURL, transport) + v4Client := githubv4.NewEnterpriseClient(graphqlURL, transportWithRateLimit) user, err := credentials.GetUser() logger.Debug("GH User: %s", user) diff --git a/server/events/vcs/github_client_test.go b/server/events/vcs/github_client_test.go index 8d4912616d..d53c5544b9 100644 --- a/server/events/vcs/github_client_test.go +++ b/server/events/vcs/github_client_test.go @@ -11,6 +11,7 @@ import ( "os" "strings" "testing" + "time" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" @@ -1713,3 +1714,54 @@ func TestGithubClient_GetPullLabels_EmptyResponse(t *testing.T) { Ok(t, err) Equals(t, 0, len(labels)) } + +func TestGithubClient_SecondaryRateLimitHandling_CreateComment(t *testing.T) { + logger := logging.NewNoopLogger(t) + calls := 0 + maxCalls := 2 + + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/api/v3/repos/owner/repo/issues/1/comments" { + t.Errorf("Unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + return + } + + if calls < maxCalls { + // Secondary rate limiting, x-ratelimit-remaining must be > 0 + w.Header().Set("x-ratelimit-remaining", "1") + w.Header().Set("x-ratelimit-reset", fmt.Sprintf("%d", time.Now().Unix()+1)) + w.WriteHeader(http.StatusForbidden) + w.Write([]byte(`{"message": "You have exceeded a secondary rate limit"}`)) // nolint: errcheck + } else { + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"id": 1, "body": "Test comment"}`)) // nolint: errcheck + } + calls++ + }), + ) + + testServerURL, err := url.Parse(testServer.URL) + Ok(t, err) + + client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{User: "user", Token: "pass"}, vcs.GithubConfig{}, 0, logger) + Ok(t, err) + defer disableSSLVerification()() + + // Simulate creating a comment + repo := models.Repo{ + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + } + pullNum := 1 + comment := "Test comment" + + err = client.CreateComment(logger, repo, pullNum, comment, "") + Ok(t, err) + + // Verify that the number of calls is greater than maxCalls, indicating that retries occurred + Assert(t, calls > maxCalls, "Expected more than %d calls due to rate limiting, but got %d", maxCalls, calls) + +} From 5fb6b0be12b66385ce5040263e5e5251cbc4335e Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Wed, 22 Jan 2025 00:20:56 -0500 Subject: [PATCH 33/60] chore: Add test to make sure flags are up to date and sorted (#4845) Signed-off-by: Luke Massa Co-authored-by: PePe Amengual <2208324+jamengual@users.noreply.github.com> --- cmd/server_test.go | 120 +++++++++++- runatlantis.io/docs/server-configuration.md | 194 +++++++++++--------- 2 files changed, 218 insertions(+), 96 deletions(-) diff --git a/cmd/server_test.go b/cmd/server_test.go index 7d7c1b52d5..ad54d10fb2 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -14,10 +14,13 @@ package cmd import ( + "bufio" + "cmp" "fmt" "os" "path/filepath" "reflect" + "slices" "strings" "testing" @@ -226,21 +229,32 @@ func TestExecute_Flags(t *testing.T) { } } -func TestUserConfigAllTested(t *testing.T) { - t.Log("All settings in userConfig should be tested.") - +func getUserConfigKeysWithFlags() []string { + var ret []string u := reflect.TypeOf(server.UserConfig{}) for i := 0; i < u.NumField(); i++ { userConfigKey := u.Field(i).Tag.Get("mapstructure") + // By default, we expect all fields in UserConfig to have flags defined in server.go and tested here in server_test.go + // Some fields are too complicated to have flags, so are only expressible in the config yaml + flagKey := u.Field(i).Tag.Get("flag") + if flagKey == "false" { + continue + } + ret = append(ret, userConfigKey) + + } + return ret + +} + +func TestUserConfigAllTested(t *testing.T) { + t.Log("All settings in userConfig should be tested.") + + for _, userConfigKey := range getUserConfigKeysWithFlags() { + t.Run(userConfigKey, func(t *testing.T) { - // By default, we expect all fields in UserConfig to have flags defined in server.go and tested here in server_test.go - // Some fields are too complicated to have flags, so are only expressible in the config yaml - flagKey := u.Field(i).Tag.Get("flag") - if flagKey == "false" { - return - } // If a setting is configured in server.UserConfig, it should be tested here. If there is no corresponding const // for specifying the flag, that probably means one *also* needs to be added to server.go if _, ok := testFlags[userConfigKey]; !ok { @@ -252,6 +266,94 @@ func TestUserConfigAllTested(t *testing.T) { } +func getDocumentedFlags(t *testing.T) []string { + + var ret []string + docFile := "../runatlantis.io/docs/server-configuration.md" + + file, err := os.Open(docFile) + Ok(t, err) + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "### ") { + continue + } + split := strings.Split(line, "`") + if len(split) != 3 { + t.Errorf("Unexpected line in %s: %s", docFile, line) + continue + } + flag := split[1] + if !strings.HasPrefix(flag, "--") { + t.Errorf("Unexpected line in %s: %s", docFile, line) + continue + } + flag = strings.TrimPrefix(flag, "--") + ret = append(ret, flag) + } + + err = scanner.Err() + Ok(t, err) + + return ret +} + +func testIsSorted[S ~[]E, E cmp.Ordered](t *testing.T, x S) { + // TODO: This is n^2, probably a better algorithm for this + // Also, this works best for lists that are mostly sorted, if the whole thing is wrong, it's just + // going to say that every individual element is out of order + for i, elem := range x { + for j, compareTo := range x { + if i == j { + continue + } + if i > j && cmp.Less(elem, compareTo) { + t.Errorf("%v is out of order (should be before %v)", elem, compareTo) + break + } + if i < j && cmp.Less(compareTo, elem) { + t.Errorf("%v is out of order (should be after %v)", elem, compareTo) + break + } + } + } +} + +func TestAllFlagsDocumented(t *testing.T) { + // This is not a unit test per se, but is a helpful way of making sure when flags are added/removed + // the corresponding documentation is kept up-to-date. + t.Log("All flags in userConfig should have documentation in server-configuration.md.") + + userConfigKeys := getUserConfigKeysWithFlags() + documentedFlags := getDocumentedFlags(t) + + testIsSorted(t, documentedFlags) + slices.Sort(userConfigKeys) + slices.Sort(documentedFlags) + + for _, userConfigKey := range userConfigKeys { + _, found := slices.BinarySearch(documentedFlags, userConfigKey) + if !found { + t.Errorf("Found undocumented config key: %s", userConfigKey) + } + } + + for _, documentedFlag := range documentedFlags { + // --help and --config are documented but don't have a setting on userConfig + if documentedFlag == "help" || documentedFlag == "config" { + continue + } + _, found := slices.BinarySearch(userConfigKeys, documentedFlag) + if !found { + t.Errorf("Found documentation for flag that doesn't exist: %s", documentedFlag) + } + } + +} + func TestExecute_ConfigFile(t *testing.T) { t.Log("Should use all the values from the config file.") // Use yaml package to quote values that need quoting diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 3db678573d..12a2f883dd 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -440,6 +440,16 @@ and set `--autoplan-modules` to `false`. If `disable-autoplan` property is `true`, this flag has no effect. +### `--disable-global-apply-lock` + + ```bash + atlantis server --disable-global-apply-lock + # or + ATLANTIS_DISABLE_GLOBAL_APPLY_LOCK=true + ``` + + If true, removes button in the UI that allows users to globally disable apply commands. + ### `--disable-markdown-folding` ```bash @@ -470,6 +480,16 @@ and set `--autoplan-modules` to `false`. Stops atlantis from unlocking a pull request with this label. Defaults to "" (feature disabled). +### `--discard-approval-on-plan` + + ```bash + atlantis server --discard-approval-on-plan + # or + ATLANTIS_DISCARD_APPROVAL_ON_PLAN=true + ``` + + If set, discard approval if a new plan has been executed. Currently only supported in Github. + ### `--emoji-reaction` ```bash @@ -548,66 +568,6 @@ and set `--autoplan-modules` to `false`. Fail and do not run the requested Atlantis command if any of the pre workflow hooks error. -### `--gitea-base-url` - - ```bash - atlantis server --gitea-base-url="http://your-gitea.corp:7990/basepath" - # or - ATLANTIS_GITEA_BASE_URL="http://your-gitea.corp:7990/basepath" - ``` - - Base URL of Gitea installation. Must include `http://` or `https://`. Defaults to `https://gitea.com` if left empty/absent. - -### `--gitea-token` - - ```bash - atlantis server --gitea-token="token" - # or (recommended) - ATLANTIS_GITEA_TOKEN="token" - ``` - - Gitea app password of API user. - -### `--gitea-user` - - ```bash - atlantis server --gitea-user="myuser" - # or - ATLANTIS_GITEA_USER="myuser" - ``` - - Gitea username of API user. - -### `--gitea-webhook-secret` - - ```bash - atlantis server --gitea-webhook-secret="secret" - # or (recommended) - ATLANTIS_GITEA_WEBHOOK_SECRET="secret" - ``` - - Secret used to validate Gitea webhooks. - - ::: warning SECURITY WARNING - If not specified, Atlantis won't be able to validate that the incoming webhook call came from Gitea. - This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. - ::: - -### `--gitea-page-size` - - ```bash - atlantis server --gitea-page-size=30 - # or (recommended) - ATLANTIS_GITEA_PAGE_SIZE=30 - ``` - - Number of items on a single page in Gitea paged responses. - - ::: warning Configuration dependent - The default value conforms to the Gitea server's standard config setting: DEFAULT_PAGING_NUM - The highest valid value depends on the Gitea server's config setting: MAX_RESPONSE_ITEMS - ::: - ### `--gh-allow-mergeable-bypass-apply` ```bash @@ -644,6 +604,21 @@ and set `--autoplan-modules` to `false`. After which Atlantis will display your new app's credentials: your app's ID, its generated `--gh-webhook-secret` and the contents of the file for `--gh-app-key-file`. Update your Atlantis config accordingly, and restart the server. ::: +### `--gh-app-installation-id` + + ```bash + atlantis server --gh-app-installation-id="123" + # or + ATLANTIS_GH_APP_INSTALLATION_ID="123" + ``` + +The installation ID of a specific instance of a GitHub application. Normally this value is +derived by querying GitHub for the list of installations of the ID supplied via `--gh-app-id` and selecting +the first one found and where multiple installations results in an error. Use this flag if you have multiple +instances of Atlantis but you want to use a single already-installed GitHub app for all of them. You would normally do this if +you are running a proxy as your single GitHub application that will proxy to an appropriate Atlantis instance +based on the organization or user that triggered the webhook. + ### `--gh-app-key` ```bash @@ -689,21 +664,6 @@ and set `--autoplan-modules` to `false`. Hostname of your GitHub Enterprise installation. If using [GitHub.com](https://github.com), don't set. Defaults to `github.com`. -### `--gh-app-installation-id` - - ```bash - atlantis server --gh-app-installation-id="123" - # or - ATLANTIS_GH_APP_INSTALLATION_ID="123" - ``` - -The installation ID of a specific instance of a GitHub application. Normally this value is -derived by querying GitHub for the list of installations of the ID supplied via `--gh-app-id` and selecting -the first one found and where multiple installations results in an error. Use this flag if you have multiple -instances of Atlantis but you want to use a single already-installed GitHub app for all of them. You would normally do this if -you are running a proxy as your single GitHub application that will proxy to an appropriate Atlantis instance -based on the organization or user that triggered the webhook. - ### `--gh-org` ```bash @@ -780,6 +740,66 @@ based on the organization or user that triggered the webhook. This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. ::: +### `--gitea-base-url` + + ```bash + atlantis server --gitea-base-url="http://your-gitea.corp:7990/basepath" + # or + ATLANTIS_GITEA_BASE_URL="http://your-gitea.corp:7990/basepath" + ``` + + Base URL of Gitea installation. Must include `http://` or `https://`. Defaults to `https://gitea.com` if left empty/absent. + +### `--gitea-page-size` + + ```bash + atlantis server --gitea-page-size=30 + # or (recommended) + ATLANTIS_GITEA_PAGE_SIZE=30 + ``` + + Number of items on a single page in Gitea paged responses. + + ::: warning Configuration dependent + The default value conforms to the Gitea server's standard config setting: DEFAULT_PAGING_NUM + The highest valid value depends on the Gitea server's config setting: MAX_RESPONSE_ITEMS + ::: + +### `--gitea-token` + + ```bash + atlantis server --gitea-token="token" + # or (recommended) + ATLANTIS_GITEA_TOKEN="token" + ``` + + Gitea app password of API user. + +### `--gitea-user` + + ```bash + atlantis server --gitea-user="myuser" + # or + ATLANTIS_GITEA_USER="myuser" + ``` + + Gitea username of API user. + +### `--gitea-webhook-secret` + + ```bash + atlantis server --gitea-webhook-secret="secret" + # or (recommended) + ATLANTIS_GITEA_WEBHOOK_SECRET="secret" + ``` + + Secret used to validate Gitea webhooks. + + ::: warning SECURITY WARNING + If not specified, Atlantis won't be able to validate that the incoming webhook call came from Gitea. + This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. + ::: + ### `--gitlab-hostname` ```bash @@ -858,18 +878,6 @@ Remove no-changes plan comments from the pull request. This is useful when you have many projects and want to keep the pull request clean from useless comments. -### `--include-git-untracked-files` - - ```bash - atlantis server --include-git-untracked-files - # or - ATLANTIS_INCLUDE_GIT_UNTRACKED_FILES=true - ``` - - Include git untracked files in the Atlantis modified file list. - Used for example with CDKTF pre-workflow hooks that dynamically generate - Terraform files. - ### `--ignore-vcs-status-names` ```bash @@ -884,6 +892,18 @@ This is useful when you have many projects and want to keep the pull request cle from other Atlantis services when checking if the PR is mergeable. Currently only implemented for GitHub. +### `--include-git-untracked-files` + + ```bash + atlantis server --include-git-untracked-files + # or + ATLANTIS_INCLUDE_GIT_UNTRACKED_FILES=true + ``` + + Include git untracked files in the Atlantis modified file list. + Used for example with CDKTF pre-workflow hooks that dynamically generate + Terraform files. + ### `--locking-db-type` ```bash From 66a831ec4d5b900e9a2748efbcc23eb08813f944 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:23:34 -0500 Subject: [PATCH 34/60] chore(deps): update actions/stale digest to 5bef64f in .github/workflows/stale.yml (main) (#5262) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 0236da84c9..8d94509d5d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write # for actions/stale to close stale PRs runs-on: ubuntu-24.04 steps: - - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 with: stale-pr-message: 'This issue is stale because it has been open for 1 month with no activity. Remove stale label or comment or this will be closed in 1 month.' stale-issue-message: This issue is stale because it has been open for 1 month with no activity. Remove stale label or comment or this will be closed in 1 month.' From a189c956cd0121ca367a8f7d526573061596b6a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:24:21 -0500 Subject: [PATCH 35/60] chore(deps): update actions/setup-go digest to f111f33 in .github/workflows/test.yml (main) (#5260) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 894e42de15..781b160476 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # need to setup go toolchain explicitly - - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 with: go-version-file: go.mod diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 255b5c5209..3c0c6efa96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: with: submodules: true - - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 with: go-version-file: go.mod diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d53ebac06..6c2d527012 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # need to setup go toolchain explicitly - - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 with: go-version-file: go.mod @@ -119,7 +119,7 @@ jobs: NGROK_AUTH_TOKEN: ${{ secrets.ATLANTISBOT_NGROK_AUTH_TOKEN }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 with: go-version-file: go.mod @@ -156,7 +156,7 @@ jobs: NGROK_AUTH_TOKEN: ${{ secrets.ATLANTISBOT_NGROK_AUTH_TOKEN }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 with: go-version-file: go.mod From 46e181e725e45e396c58ce7846cd1bad185f3591 Mon Sep 17 00:00:00 2001 From: Piotr Pawluk <146927496+zendesk-piotrpawluk@users.noreply.github.com> Date: Thu, 23 Jan 2025 04:39:20 +0100 Subject: [PATCH 36/60] feat: add http webhook (#5233) Signed-off-by: Piotr Pawluk Co-authored-by: PePe Amengual <2208324+jamengual@users.noreply.github.com> --- cmd/server.go | 11 ++ cmd/server_test.go | 1 + runatlantis.io/.vitepress/sidebars.ts | 2 +- .../sending-notifications-via-webhooks.md | 151 ++++++++++++++++++ runatlantis.io/docs/server-configuration.md | 14 +- runatlantis.io/docs/using-slack-hooks.md | 64 -------- server/events/project_command_runner.go | 13 +- server/events/webhooks/http.go | 65 ++++++++ server/events/webhooks/http_test.go | 127 +++++++++++++++ server/events/webhooks/webhooks.go | 41 +++-- server/events/webhooks/webhooks_test.go | 58 ++++--- server/server.go | 18 ++- server/user_config.go | 35 ++++ server/user_config_test.go | 45 ++++++ 14 files changed, 537 insertions(+), 108 deletions(-) create mode 100644 runatlantis.io/docs/sending-notifications-via-webhooks.md delete mode 100644 runatlantis.io/docs/using-slack-hooks.md create mode 100644 server/events/webhooks/http.go create mode 100644 server/events/webhooks/http_test.go diff --git a/cmd/server.go b/cmd/server.go index aa8581e705..b71c5b2da3 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -153,6 +153,7 @@ const ( TFELocalExecutionModeFlag = "tfe-local-execution-mode" TFETokenFlag = "tfe-token" WriteGitCredsFlag = "write-git-creds" // nolint: gosec + WebhookHttpHeaders = "webhook-http-headers" WebBasicAuthFlag = "web-basic-auth" WebUsernameFlag = "web-username" WebPasswordFlag = "web-password" @@ -460,6 +461,12 @@ var stringFlags = map[string]stringFlag{ description: "Name used to identify Atlantis for pull request statuses.", defaultValue: DefaultVCSStatusName, }, + WebhookHttpHeaders: { + description: "Additional headers added to each HTTP POST payload when using HTTP webhooks provided as a JSON string." + + " The map key is the header name and the value is the header value (string) or values (array of string)." + + " For example: `{\"Authorization\":\"Bearer some-token\",\"X-Custom-Header\":[\"value1\",\"value2\"]}`.", + defaultValue: "", + }, WebUsernameFlag: { description: "Username used for Web Basic Authentication on Atlantis HTTP Middleware", defaultValue: DefaultWebUsername, @@ -1069,6 +1076,10 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { return errors.Wrapf(err, "invalid --%s", AllowCommandsFlag) } + if _, err := userConfig.ToWebhookHttpHeaders(); err != nil { + return errors.Wrapf(err, "invalid --%s", WebhookHttpHeaders) + } + return nil } diff --git a/cmd/server_test.go b/cmd/server_test.go index ad54d10fb2..90d45c65df 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -151,6 +151,7 @@ var testFlags = map[string]interface{}{ VarFileAllowlistFlag: "/path", VCSStatusName: "my-status", IgnoreVCSStatusNames: "", + WebhookHttpHeaders: `{"Authorization":"Bearer some-token","X-Custom-Header":["value1","value2"]}`, WebBasicAuthFlag: false, WebPasswordFlag: "atlantis", WebUsernameFlag: "atlantis", diff --git a/runatlantis.io/.vitepress/sidebars.ts b/runatlantis.io/.vitepress/sidebars.ts index 5bcabfc4bc..9afc20f780 100644 --- a/runatlantis.io/.vitepress/sidebars.ts +++ b/runatlantis.io/.vitepress/sidebars.ts @@ -44,7 +44,7 @@ const en = [ { text: "Checkout Strategy", link: "/docs/checkout-strategy" }, { text: "Terraform Versions", link: "/docs/terraform-versions" }, { text: "Terraform Cloud", link: "/docs/terraform-cloud" }, - { text: "Using Slack Hooks", link: "/docs/using-slack-hooks" }, + { text: "Sending Notifications via Webhooks", link: "/docs/sending-notifications-via-webhooks" }, { text: "Stats", link: "/docs/stats" }, { text: "FAQ", link: "/docs/faq" }, ] diff --git a/runatlantis.io/docs/sending-notifications-via-webhooks.md b/runatlantis.io/docs/sending-notifications-via-webhooks.md new file mode 100644 index 0000000000..9272a25c89 --- /dev/null +++ b/runatlantis.io/docs/sending-notifications-via-webhooks.md @@ -0,0 +1,151 @@ +# Sending notifications via webhooks + +It is possible to send notifications to external systems whenever an apply is being done. + +You can make requests to any HTTP endpoint or send messages directly to your Slack channel. + +::: tip NOTE +Currently only `apply` events are supported. +::: + +## Configuration + +Webhooks are configured in Atlantis [server-side configuration](server-configuration.md). +There can be many webhooks: sending notifications to different destinations or for different +workspaces/branches. Here is example configuration to send Slack messages for every apply: + +```yaml +webhooks: +- event: apply + kind: slack + channel: my-channel-id +``` + +If you are deploying Atlantis as a Helm chart, this can be implemented via the `config` parameter available for [chart customizations](https://github.com/runatlantis/helm-charts#customization): + +```yaml +## Use Server Side Config, +## ref: https://www.runatlantis.io/docs/server-configuration.html +config: | + --- + webhooks: + - event: apply + kind: slack + channel: my-channel-id +``` + +### Filter on workspace/branch + +To limit notifications to particular workspaces or branches, use `workspace-regex` or `branch-regex` parameters. +If the workspace **and** branch matches respective regex, an event will be sent. Note that empty regular expression +(a result of unset parameter) matches every string. + +## Using HTTP webhooks + +You can send POST requests with JSON payload to any HTTP/HTTPS server. + +### Configuring Atlantis + +In your Atlantis [server-side configuration](server-configuration.md) you can add the following: + +```yaml +webhooks: +- event: apply + kind: http + url: https://example.com/hooks +``` + +The `apply` event information will be POSTed to `https://example.com/hooks`. + +You can supply any additional headers with `--webhook-http-headers` parameter (or environment variable), +for example for authentication purposes. See [webhook-http-headers](server-configuration.md#webhook-http-headers) for details. + +### JSON payload + +The payload is a JSON-marshalled [ApplyResult](https://pkg.go.dev/github.com/runatlantis/atlantis/server/events/webhooks#ApplyResult) struct. + +Example payload: + +```json +{ + "Workspace": "default", + "Repo": { + "FullName": "octocat/Hello-World", + "Owner": "octocat", + "Name": "Hello-World", + "CloneURL": "https://:@github.com/octocat/Hello-World.git", + "SanitizedCloneURL": "https://:@github.com/octocat/Hello-World.git", + "VCSHost": { + "Hostname": "github.com", + "Type": 0 + } + }, + "Pull": { + "Num": 2137, + "HeadCommit": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", + "URL": "https://github.com/octocat/Hello-World/pull/2137", + "HeadBranch": "feature/some-branch", + "BaseBranch": "main", + "Author": "octocat", + "State": 0, + "BaseRepo": { + "FullName": "octocat/Hello-World", + "Owner": "octocat", + "Name": "Hello-World", + "CloneURL": "https://:@github.com/octocat/Hello-World.git", + "SanitizedCloneURL": "https://:@github.com/octocat/Hello-World.git", + "VCSHost": { + "Hostname": "github.com", + "Type": 0 + } + } + }, + "User": { + "Username": "octocat", + "Teams": null + }, + "Success": true, + "Directory": "terraform/example", + "ProjectName": "example-project" +} +``` + +## Using Slack hooks + +For this you'll need to: + +* Create a Bot user in Slack +* Configure Atlantis to send notifications to Slack. + +### Configuring Slack for Atlantis + +* Go to [Slack: Apps](https://api.slack.com/apps) +* Click the `Create New App` button +* Select `From scratch` in the dialog that opens +* Give it a name, e.g. `atlantis-bot`. +* Select your Slack workspace +* Click `Create App` +* On the left go to `oAuth & Permissions` +* Scroll down to Scopes | Bot Token Scopes and add the following OAuth scopes: + * `channels:read` + * `chat:write` + * `groups:read` + * `incoming-webhook` + * `mpim:read` +* Install the app onto your Slack workspace +* Copy the `Bot User OAuth Token` and provide it to Atlantis by using `--slack-token=xoxb-xxxxxxxxxxx` or via the environment `ATLANTIS_SLACK_TOKEN=xoxb-xxxxxxxxxxx`. +* Create a channel in your Slack workspace (e.g. `my-channel`) or use existing +* Add the app to Created channel or existing channel ( click channel name then tab integrations, there Click "Add apps" + +### Configuring Atlantis + +After following the above steps it is time to configure Atlantis. Assuming you have already provided the `slack-token` (via parameter or environment variable) you can now instruct Atlantis to send `apply` events to Slack. + +In your Atlantis [server-side configuration](server-configuration.md) you can now add the following: + +```yaml +webhooks: +- event: apply + kind: slack + channel: my-channel-id +``` diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 12a2f883dd..b55a363c95 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -1254,7 +1254,7 @@ This is useful when you have many projects and want to keep the pull request cle ATLANTIS_SLACK_TOKEN='token' ``` - API token for Slack notifications. See [Using Slack hooks](using-slack-hooks.md). + API token for Slack notifications. See [Using Slack hooks](sending-notifications-via-webhooks.md#using-slack-hooks). ### `--ssl-cert-file` @@ -1425,6 +1425,18 @@ The effect of the race condition is more evident when using parallel configurati Username used for Basic Authentication on the Atlantis web service. Defaults to `atlantis`. +### `--webhook-http-headers` + + ```bash + atlantis server --webhook-http-headers='{"Authorization":"Bearer some-token","X-Custom-Header":["value1","value2"]}' + # or + ATLANTIS_WEBHOOK_HTTP_HEADERS='{"Authorization":"Bearer some-token","X-Custom-Header":["value1","value2"]}' + ``` + + Additional headers added to each HTTP POST payload when using [http webhooks](sending-notifications-via-webhooks.md#using-http-webhooks) + provided as a JSON string. The map key is the header name and the value is the header value + (string) or values (array of string). + ### `--websocket-check-origin` ```bash diff --git a/runatlantis.io/docs/using-slack-hooks.md b/runatlantis.io/docs/using-slack-hooks.md deleted file mode 100644 index 572b0857f8..0000000000 --- a/runatlantis.io/docs/using-slack-hooks.md +++ /dev/null @@ -1,64 +0,0 @@ -# Using Slack hooks - -It is possible to use Slack to send notifications to your Slack channel whenever an apply is being done. - -::: tip NOTE -Currently only `apply` events are supported. -::: - -For this you'll need to: - -* Create a Bot user in Slack -* Configure Atlantis to send notifications to Slack. - -## Configuring Slack for Atlantis - -* Go to [Slack: Apps](https://api.slack.com/apps) -* Click the `Create New App` button -* Select `From scratch` in the dialog that opens -* Give it a name, e.g. `atlantis-bot`. -* Select your Slack workspace -* Click `Create App` -* On the left go to `oAuth & Permissions` -* Scroll down to Scopes | Bot Token Scopes and add the following OAuth scopes: - * `channels:read` - * `chat:write` - * `groups:read` - * `incoming-webhook` - * `mpim:read` -* Install the app onto your Slack workspace -* Copy the `Bot User OAuth Token` and provide it to Atlantis by using `--slack-token=xoxb-xxxxxxxxxxx` or via the environment `ATLANTIS_SLACK_TOKEN=xoxb-xxxxxxxxxxx`. -* Create a channel in your Slack workspace (e.g. `my-channel`) or use existing -* Add the app to Created channel or existing channel ( click channel name then tab integrations, there Click "Add apps" - -## Configuring Atlantis - -After following the above steps it is time to configure Atlantis. Assuming you have already provided the `slack-token` (via parameter or environment variable) you can now instruct Atlantis to send `apply` events to Slack. - -In your Atlantis configuration you can now add the following: - -```yaml -webhooks: -- event: apply - workspace-regex: .* - branch-regex: .* - kind: slack - channel: my-channel-id -``` - -If you are deploying Atlantis as a Helm chart, this can be implemented via the `config` parameter available for [chart customizations](https://github.com/runatlantis/helm-charts#customization): - -```yaml -## Use Server Side Config, -## ref: https://www.runatlantis.io/docs/server-configuration.html -config: | - --- - webhooks: - - event: apply - workspace-regex: .* - branch-regex: .* - kind: slack - channel: my-channel-id -``` - -The `apply` event information will be sent to the `my-channel-id` Slack channel. diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 76f9ba9202..79e1d7899c 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -660,12 +660,13 @@ func (p *DefaultProjectCommandRunner) doApply(ctx command.ProjectContext) (apply outputs, err := p.runSteps(ctx.Steps, ctx, absPath) p.Webhooks.Send(ctx.Log, webhooks.ApplyResult{ // nolint: errcheck - Workspace: ctx.Workspace, - User: ctx.User, - Repo: ctx.Pull.BaseRepo, - Pull: ctx.Pull, - Success: err == nil, - Directory: ctx.RepoRelDir, + Workspace: ctx.Workspace, + User: ctx.User, + Repo: ctx.Pull.BaseRepo, + Pull: ctx.Pull, + Success: err == nil, + Directory: ctx.RepoRelDir, + ProjectName: ctx.ProjectName, }) if err != nil { diff --git a/server/events/webhooks/http.go b/server/events/webhooks/http.go new file mode 100644 index 0000000000..6f540ac154 --- /dev/null +++ b/server/events/webhooks/http.go @@ -0,0 +1,65 @@ +package webhooks + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + + "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/logging" +) + +// HttpWebhook sends webhooks to any HTTP destination. +type HttpWebhook struct { + Client *HttpClient + WorkspaceRegex *regexp.Regexp + BranchRegex *regexp.Regexp + URL string +} + +// Send sends the webhook to URL if workspace and branch matches their respective regex. +func (h *HttpWebhook) Send(_ logging.SimpleLogging, applyResult ApplyResult) error { + if !h.WorkspaceRegex.MatchString(applyResult.Workspace) || !h.BranchRegex.MatchString(applyResult.Pull.BaseBranch) { + return nil + } + if err := h.doSend(applyResult); err != nil { + return errors.Wrap(err, fmt.Sprintf("sending webhook to %q", h.URL)) + } + return nil +} + +func (h *HttpWebhook) doSend(applyResult ApplyResult) error { + body, err := json.Marshal(applyResult) + if err != nil { + return err + } + req, err := http.NewRequest("POST", h.URL, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + for header, values := range h.Client.Headers { + for _, value := range values { + req.Header.Add(header, value) + } + } + resp, err := h.Client.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("returned status code %d with response %q", resp.StatusCode, respBody) + } + return nil +} + +// HttpClient wraps http.Client allowing to add arbitrary Headers to a request. +type HttpClient struct { + Client *http.Client + Headers map[string][]string +} diff --git a/server/events/webhooks/http_test.go b/server/events/webhooks/http_test.go new file mode 100644 index 0000000000..66862cf054 --- /dev/null +++ b/server/events/webhooks/http_test.go @@ -0,0 +1,127 @@ +package webhooks_test + +import ( + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/webhooks" + "github.com/runatlantis/atlantis/server/logging" + . "github.com/runatlantis/atlantis/testing" +) + +var httpApplyResult = webhooks.ApplyResult{ + Workspace: "production", + Repo: models.Repo{ + FullName: "runatlantis/atlantis", + }, + Pull: models.PullRequest{ + Num: 1, + URL: "url", + BaseBranch: "main", + }, + User: models.User{ + Username: "lkysow", + }, + Success: true, +} + +func TestHttpWebhookWithHeaders(t *testing.T) { + expectedHeaders := map[string][]string{ + "Authorization": {"Bearer token"}, + "X-Custom-Header": {"value1", "value2"}, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Equals(t, r.Header.Get("Content-Type"), "application/json") + for k, v := range expectedHeaders { + Equals(t, r.Header.Values(k), v) + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + webhook := webhooks.HttpWebhook{ + Client: &webhooks.HttpClient{Client: http.DefaultClient, Headers: expectedHeaders}, + URL: server.URL, + WorkspaceRegex: regexp.MustCompile(".*"), + BranchRegex: regexp.MustCompile(".*"), + } + + err := webhook.Send(logging.NewNoopLogger(t), httpApplyResult) + Ok(t, err) +} + +func TestHttpWebhookNoHeaders(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Equals(t, r.Header.Get("Content-Type"), "application/json") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + webhook := webhooks.HttpWebhook{ + Client: &webhooks.HttpClient{Client: http.DefaultClient}, + URL: server.URL, + WorkspaceRegex: regexp.MustCompile(".*"), + BranchRegex: regexp.MustCompile(".*"), + } + + err := webhook.Send(logging.NewNoopLogger(t), httpApplyResult) + Ok(t, err) +} + +func TestHttpWebhook500(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + webhook := webhooks.HttpWebhook{ + Client: &webhooks.HttpClient{Client: http.DefaultClient}, + URL: server.URL, + WorkspaceRegex: regexp.MustCompile(".*"), + BranchRegex: regexp.MustCompile(".*"), + } + + err := webhook.Send(logging.NewNoopLogger(t), httpApplyResult) + ErrContains(t, "sending webhook", err) +} + +func TestHttpNoRegexMatch(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Assert(t, false, "webhook should not be sent") + })) + defer server.Close() + + tt := []struct { + name string + wr *regexp.Regexp + br *regexp.Regexp + }{ + { + name: "no workspace match", + wr: regexp.MustCompile("other"), + br: regexp.MustCompile(".*"), + }, + { + name: "no branch match", + wr: regexp.MustCompile(".*"), + br: regexp.MustCompile("other"), + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + webhook := webhooks.HttpWebhook{ + Client: &webhooks.HttpClient{Client: http.DefaultClient}, + URL: server.URL, + WorkspaceRegex: tc.wr, + BranchRegex: tc.br, + } + err := webhook.Send(logging.NewNoopLogger(t), httpApplyResult) + Ok(t, err) + }) + } +} diff --git a/server/events/webhooks/webhooks.go b/server/events/webhooks/webhooks.go index c4b43239a7..09d74371ed 100644 --- a/server/events/webhooks/webhooks.go +++ b/server/events/webhooks/webhooks.go @@ -24,6 +24,7 @@ import ( ) const SlackKind = "slack" +const HttpKind = "http" const ApplyEvent = "apply" //go:generate pegomock generate --package mocks -o mocks/mock_sender.go Sender @@ -36,12 +37,13 @@ type Sender interface { // ApplyResult is the result of a terraform apply. type ApplyResult struct { - Workspace string - Repo models.Repo - Pull models.PullRequest - User models.User - Success bool - Directory string + Workspace string + Repo models.Repo + Pull models.PullRequest + User models.User + Success bool + Directory string + ProjectName string } // MultiWebhookSender sends multiple webhooks for each one it's configured for. @@ -55,9 +57,15 @@ type Config struct { BranchRegex string Kind string Channel string + URL string } -func NewMultiWebhookSender(configs []Config, client SlackClient) (*MultiWebhookSender, error) { +type Clients struct { + Slack SlackClient + Http *HttpClient +} + +func NewMultiWebhookSender(configs []Config, clients Clients) (*MultiWebhookSender, error) { var webhooks []Sender for _, c := range configs { wr, err := regexp.Compile(c.WorkspaceRegex) @@ -76,19 +84,30 @@ func NewMultiWebhookSender(configs []Config, client SlackClient) (*MultiWebhookS } switch c.Kind { case SlackKind: - if !client.TokenIsSet() { + if !clients.Slack.TokenIsSet() { return nil, errors.New("must specify top-level \"slack-token\" if using a webhook of \"kind: slack\"") } if c.Channel == "" { return nil, errors.New("must specify \"channel\" if using a webhook of \"kind: slack\"") } - slack, err := NewSlack(wr, br, c.Channel, client) + slack, err := NewSlack(wr, br, c.Channel, clients.Slack) if err != nil { return nil, err } webhooks = append(webhooks, slack) + case HttpKind: + if c.URL == "" { + return nil, errors.New("must specify \"url\" if using a webhook of \"kind: http\"") + } + httpWebhook := &HttpWebhook{ + Client: clients.Http, + WorkspaceRegex: wr, + BranchRegex: br, + URL: c.URL, + } + webhooks = append(webhooks, httpWebhook) default: - return nil, fmt.Errorf("\"kind: %s\" not supported. Only \"kind: %s\" is supported right now", c.Kind, SlackKind) + return nil, fmt.Errorf("\"kind: %s\" not supported. Only \"kind: %s\" and \"kind: %s\" are supported right now", c.Kind, SlackKind, HttpKind) } } @@ -101,7 +120,7 @@ func NewMultiWebhookSender(configs []Config, client SlackClient) (*MultiWebhookS func (w *MultiWebhookSender) Send(log logging.SimpleLogging, result ApplyResult) error { for _, w := range w.Webhooks { if err := w.Send(log, result); err != nil { - log.Warn("error sending slack webhook: %s", err) + log.Warn("error sending webhook: %s", err) } } return nil diff --git a/server/events/webhooks/webhooks_test.go b/server/events/webhooks/webhooks_test.go index 5ee00bf599..edcd80c025 100644 --- a/server/events/webhooks/webhooks_test.go +++ b/server/events/webhooks/webhooks_test.go @@ -14,6 +14,7 @@ package webhooks_test import ( + "net/http" "strings" "testing" @@ -43,15 +44,22 @@ func validConfigs() []webhooks.Config { return []webhooks.Config{validConfig} } +func validClients() webhooks.Clients { + return webhooks.Clients{ + Slack: mocks.NewMockSlackClient(), + Http: &webhooks.HttpClient{Client: http.DefaultClient}, + } +} + func TestNewWebhooksManager_InvalidWorkspaceRegex(t *testing.T) { t.Log("When given an invalid workspace regex in a config, an error is returned") RegisterMockTestingT(t) - client := mocks.NewMockSlackClient() + clients := validClients() invalidRegex := "(" configs := validConfigs() configs[0].WorkspaceRegex = invalidRegex - _, err := webhooks.NewMultiWebhookSender(configs, client) + _, err := webhooks.NewMultiWebhookSender(configs, clients) Assert(t, err != nil, "expected error") Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected regex error") } @@ -59,12 +67,12 @@ func TestNewWebhooksManager_InvalidWorkspaceRegex(t *testing.T) { func TestNewWebhooksManager_InvalidBranchRegex(t *testing.T) { t.Log("When given an invalid branch regex in a config, an error is returned") RegisterMockTestingT(t) - client := mocks.NewMockSlackClient() + clients := validClients() invalidRegex := "(" configs := validConfigs() configs[0].BranchRegex = invalidRegex - _, err := webhooks.NewMultiWebhookSender(configs, client) + _, err := webhooks.NewMultiWebhookSender(configs, clients) Assert(t, err != nil, "expected error") Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected regex error") } @@ -72,13 +80,13 @@ func TestNewWebhooksManager_InvalidBranchRegex(t *testing.T) { func TestNewWebhooksManager_InvalidBranchAndWorkspaceRegex(t *testing.T) { t.Log("When given an invalid branch and invalid workspace regex in a config, an error is returned") RegisterMockTestingT(t) - client := mocks.NewMockSlackClient() + clients := validClients() invalidRegex := "(" configs := validConfigs() configs[0].WorkspaceRegex = invalidRegex configs[0].BranchRegex = invalidRegex - _, err := webhooks.NewMultiWebhookSender(configs, client) + _, err := webhooks.NewMultiWebhookSender(configs, clients) Assert(t, err != nil, "expected error") Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected regex error") } @@ -86,10 +94,10 @@ func TestNewWebhooksManager_InvalidBranchAndWorkspaceRegex(t *testing.T) { func TestNewWebhooksManager_NoEvent(t *testing.T) { t.Log("When the event key is not specified in a config, an error is returned") RegisterMockTestingT(t) - client := mocks.NewMockSlackClient() + clients := validClients() configs := validConfigs() configs[0].Event = "" - _, err := webhooks.NewMultiWebhookSender(configs, client) + _, err := webhooks.NewMultiWebhookSender(configs, clients) Assert(t, err != nil, "expected error") Equals(t, "must specify \"kind\" and \"event\" keys for webhooks", err.Error()) } @@ -97,12 +105,12 @@ func TestNewWebhooksManager_NoEvent(t *testing.T) { func TestNewWebhooksManager_UnsupportedEvent(t *testing.T) { t.Log("When given an unsupported event in a config, an error is returned") RegisterMockTestingT(t) - client := mocks.NewMockSlackClient() + clients := validClients() unsupportedEvent := "badevent" configs := validConfigs() configs[0].Event = unsupportedEvent - _, err := webhooks.NewMultiWebhookSender(configs, client) + _, err := webhooks.NewMultiWebhookSender(configs, clients) Assert(t, err != nil, "expected error") Equals(t, "\"event: badevent\" not supported. Only \"event: apply\" is supported right now", err.Error()) } @@ -110,10 +118,10 @@ func TestNewWebhooksManager_UnsupportedEvent(t *testing.T) { func TestNewWebhooksManager_NoKind(t *testing.T) { t.Log("When the kind key is not specified in a config, an error is returned") RegisterMockTestingT(t) - client := mocks.NewMockSlackClient() + clients := validClients() configs := validConfigs() configs[0].Kind = "" - _, err := webhooks.NewMultiWebhookSender(configs, client) + _, err := webhooks.NewMultiWebhookSender(configs, clients) Assert(t, err != nil, "expected error") Equals(t, "must specify \"kind\" and \"event\" keys for webhooks", err.Error()) } @@ -121,14 +129,14 @@ func TestNewWebhooksManager_NoKind(t *testing.T) { func TestNewWebhooksManager_UnsupportedKind(t *testing.T) { t.Log("When given an unsupported kind in a config, an error is returned") RegisterMockTestingT(t) - client := mocks.NewMockSlackClient() + clients := validClients() unsupportedKind := "badkind" configs := validConfigs() configs[0].Kind = unsupportedKind - _, err := webhooks.NewMultiWebhookSender(configs, client) + _, err := webhooks.NewMultiWebhookSender(configs, clients) Assert(t, err != nil, "expected error") - Equals(t, "\"kind: badkind\" not supported. Only \"kind: slack\" is supported right now", err.Error()) + Equals(t, "\"kind: badkind\" not supported. Only \"kind: slack\" and \"kind: http\" are supported right now", err.Error()) } func TestNewWebhooksManager_NoConfigSuccess(t *testing.T) { @@ -136,23 +144,27 @@ func TestNewWebhooksManager_NoConfigSuccess(t *testing.T) { t.Log("passing any client should succeed") var emptyConfigs []webhooks.Config emptyToken := "" - m, err := webhooks.NewMultiWebhookSender(emptyConfigs, webhooks.NewSlackClient(emptyToken)) + anyClients := webhooks.Clients{ + Slack: webhooks.NewSlackClient(emptyToken), + Http: &webhooks.HttpClient{Client: http.DefaultClient}, + } + m, err := webhooks.NewMultiWebhookSender(emptyConfigs, anyClients) Ok(t, err) Equals(t, 0, len(m.Webhooks)) // nolint: staticcheck t.Log("passing nil client should succeed") - m, err = webhooks.NewMultiWebhookSender(emptyConfigs, nil) + m, err = webhooks.NewMultiWebhookSender(emptyConfigs, webhooks.Clients{}) Ok(t, err) Equals(t, 0, len(m.Webhooks)) // nolint: staticcheck } func TestNewWebhooksManager_SingleConfigSuccess(t *testing.T) { t.Log("When there is one valid config, function should succeed") RegisterMockTestingT(t) - client := mocks.NewMockSlackClient() - When(client.TokenIsSet()).ThenReturn(true) + clients := validClients() + When(clients.Slack.TokenIsSet()).ThenReturn(true) configs := validConfigs() - m, err := webhooks.NewMultiWebhookSender(configs, client) + m, err := webhooks.NewMultiWebhookSender(configs, clients) Ok(t, err) Equals(t, 1, len(m.Webhooks)) // nolint: staticcheck } @@ -160,15 +172,15 @@ func TestNewWebhooksManager_SingleConfigSuccess(t *testing.T) { func TestNewWebhooksManager_MultipleConfigSuccess(t *testing.T) { t.Log("When there are multiple valid configs, function should succeed") RegisterMockTestingT(t) - client := mocks.NewMockSlackClient() - When(client.TokenIsSet()).ThenReturn(true) + clients := validClients() + When(clients.Slack.TokenIsSet()).ThenReturn(true) var configs []webhooks.Config nConfigs := 5 for i := 0; i < nConfigs; i++ { configs = append(configs, validConfig) } - m, err := webhooks.NewMultiWebhookSender(configs, client) + m, err := webhooks.NewMultiWebhookSender(configs, clients) Ok(t, err) Equals(t, nConfigs, len(m.Webhooks)) // nolint: staticcheck } diff --git a/server/server.go b/server/server.go index 6f4e31c497..3bd9f6fb51 100644 --- a/server/server.go +++ b/server/server.go @@ -149,11 +149,14 @@ type WebhookConfig struct { // that is being modified for this event. If the regex matches, we'll // send the webhook, ex. "main.*". BranchRegex string `mapstructure:"branch-regex"` - // Kind is the type of webhook we should send, ex. slack. + // Kind is the type of webhook we should send, ex. slack or http. Kind string `mapstructure:"kind"` // Channel is the channel to send this webhook to. It only applies to // slack webhooks. Should be without '#'. Channel string `mapstructure:"channel"` + // URL is the URL where to deliver this webhook. It only applies to + // http webhooks. + URL string `mapstructure:"url"` } //go:embed static @@ -379,10 +382,21 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Event: c.Event, Kind: c.Kind, WorkspaceRegex: c.WorkspaceRegex, + URL: c.URL, } webhooksConfig = append(webhooksConfig, config) } - webhooksManager, err := webhooks.NewMultiWebhookSender(webhooksConfig, webhooks.NewSlackClient(userConfig.SlackToken)) + webhookHeaders, err := userConfig.ToWebhookHttpHeaders() + if err != nil { + return nil, errors.Wrap(err, "parsing webhook http headers") + } + webhooksManager, err := webhooks.NewMultiWebhookSender( + webhooksConfig, + webhooks.Clients{ + Slack: webhooks.NewSlackClient(userConfig.SlackToken), + Http: &webhooks.HttpClient{Client: http.DefaultClient, Headers: webhookHeaders}, + }, + ) if err != nil { return nil, errors.Wrap(err, "initializing webhooks") } diff --git a/server/user_config.go b/server/user_config.go index 9cd4f54675..3f27aa323d 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -1,8 +1,11 @@ package server import ( + "encoding/json" "strings" + "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" ) @@ -120,6 +123,7 @@ type UserConfig struct { DefaultTFDistribution string `mapstructure:"default-tf-distribution"` DefaultTFVersion string `mapstructure:"default-tf-version"` Webhooks []WebhookConfig `mapstructure:"webhooks" flag:"false"` + WebhookHttpHeaders string `mapstructure:"webhook-http-headers"` WebBasicAuth bool `mapstructure:"web-basic-auth"` WebUsername string `mapstructure:"web-username"` WebPassword string `mapstructure:"web-password"` @@ -152,6 +156,37 @@ func (u UserConfig) ToAllowCommandNames() ([]command.Name, error) { return allowCommands, nil } +// ToWebhookHttpHeaders parses WebhookHttpHeaders into a map of HTTP headers. +func (u UserConfig) ToWebhookHttpHeaders() (map[string][]string, error) { + if u.WebhookHttpHeaders == "" { + return nil, nil + } + + var m map[string]interface{} + err := json.Unmarshal([]byte(u.WebhookHttpHeaders), &m) + if err != nil { + return nil, err + } + headers := make(map[string][]string) + for name, rawValue := range m { + switch val := rawValue.(type) { + case []interface{}: + for _, v := range val { + s, ok := v.(string) + if !ok { + return nil, errors.Errorf("expected string array element, got %T", v) + } + headers[name] = append(headers[name], s) + } + case string: + headers[name] = []string{val} + default: + return nil, errors.Errorf("expected string or array, got %T", val) + } + } + return headers, nil +} + // ToLogLevel returns the LogLevel object corresponding to the user-passed // log level. func (u UserConfig) ToLogLevel() logging.LogLevel { diff --git a/server/user_config_test.go b/server/user_config_test.go index 225049f335..b37f04cf8b 100644 --- a/server/user_config_test.go +++ b/server/user_config_test.go @@ -3,6 +3,8 @@ package server_test import ( "testing" + "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" @@ -69,6 +71,49 @@ func TestUserConfig_ToAllowCommandNames(t *testing.T) { } } +func TestUserConfig_ToWebhookHttpHeaders(t *testing.T) { + tcs := []struct { + name string + given string + want map[string][]string + err error + }{ + { + name: "empty", + given: "", + want: nil, + }, + { + name: "happy path", + given: `{"Authorization":"Bearer some-token","X-Custom-Header":["value1","value2"]}`, + want: map[string][]string{ + "Authorization": {"Bearer some-token"}, + "X-Custom-Header": {"value1", "value2"}, + }, + }, + { + name: "invalid json", + given: `{"X-Custom-Header":true}`, + err: errors.New("expected string or array, got bool"), + }, + { + name: "invalid json array element", + given: `{"X-Custom-Header":[1, 2]}`, + err: errors.New("expected string array element, got float64"), + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + u := server.UserConfig{ + WebhookHttpHeaders: tc.given, + } + got, err := u.ToWebhookHttpHeaders() + Equals(t, tc.want, got) + Equals(t, tc.err, err) + }) + } +} + func TestUserConfig_ToLogLevel(t *testing.T) { cases := []struct { userLvl string From 6bb4e9f5cbf88a320a38e1638e3ee8a92314e9ad Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Thu, 23 Jan 2025 12:33:48 -0500 Subject: [PATCH 37/60] chore(deps): bump vite to 5.4.12 (#5266) Signed-off-by: Rui Chen --- .github/workflows/website.yml | 2 ++ package-lock.json | 7 ++++--- package.json | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 5b329549ce..49f676cda1 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -87,6 +87,7 @@ jobs: # twitter.com => too many redirections # www.flaticon.com => 403 error # www.freepik.com => 403 error + # ngrok.com => 406 error - run: | ./muffet \ -e 'https://medium.com/runatlantis' \ @@ -94,6 +95,7 @@ jobs: -e 'https://twitter.com/*' \ -e 'https://www.flaticon.com/*' \ -e 'https://www.freepik.com/*' \ + -e 'https://ngrok.com/*' \ -e 'https://github\.com/runatlantis/atlantis/edit/main/.*' \ -e 'https://github.com/runatlantis/helm-charts#customization' \ -e 'https://github.com/sethvargo/atlantis-on-gke/blob/master/terraform/tls.tf#L64-L84' \ diff --git a/package-lock.json b/package-lock.json index 16bf10ee0d..1a3739258b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "markdownlint-cli": "^0.40.0", "mermaid": "^10.9.1", "sitemap-ts": "^1.7.3", + "vite": "^5.4.12", "vitepress": "^1.2.3", "vitepress-plugin-mermaid": "^2.0.16", "vue": "^3.4.27" @@ -4903,9 +4904,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "version": "5.4.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz", + "integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index bdd8fe208c..4b9915e265 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "markdownlint-cli": "^0.40.0", "mermaid": "^10.9.1", "sitemap-ts": "^1.7.3", + "vite": "^5.4.12", "vitepress": "^1.2.3", "vitepress-plugin-mermaid": "^2.0.16", "vue": "^3.4.27" From 33be84be87eb90c3d13b8a3b0f181e9553a16d84 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:22:20 -0500 Subject: [PATCH 38/60] chore(deps): update github/codeql-action digest to ee117c9 in .github/workflows/codeql.yml (main) (#5263) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2dc8574409..81023f682a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -77,7 +77,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3 + uses: github/codeql-action/init@ee117c905ab18f32fa0f66c2fe40ecc8013f3e04 # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -91,7 +91,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3 + uses: github/codeql-action/autobuild@ee117c905ab18f32fa0f66c2fe40ecc8013f3e04 # v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -104,7 +104,7 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3 + uses: github/codeql-action/analyze@ee117c905ab18f32fa0f66c2fe40ecc8013f3e04 # v3 with: category: "/language:${{matrix.language}}" From 7bc33378a839526c2d8ff29a800a213b4310f72f Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Thu, 23 Jan 2025 18:03:22 -0500 Subject: [PATCH 39/60] feat: Add ignore path to autodiscover using glob (#5267) Signed-off-by: Luke Massa --- go.mod | 1 + go.sum | 2 + .../docs/repo-level-atlantis-yaml.md | 11 +++ .../docs/server-side-repo-config.md | 3 + server/core/config/raw/autodiscover.go | 35 ++++++- server/core/config/raw/autodiscover_test.go | 80 +++++++++++++++- server/core/config/raw/repo_cfg_test.go | 9 +- server/core/config/valid/autodiscover.go | 3 +- server/core/config/valid/repo_cfg.go | 20 ++++ server/core/config/valid/repo_cfg_test.go | 95 ++++++++++++++++++- server/events/project_command_builder.go | 10 +- server/events/project_command_builder_test.go | 72 ++++++++++++++ 12 files changed, 331 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 6e02a24ca1..7d8ddce3e0 100644 --- a/go.mod +++ b/go.mod @@ -76,6 +76,7 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudflare/circl v1.3.9 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/go.sum b/go.sum index 08a87b8937..699c99f807 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= +github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bradleyfalzon/ghinstallation/v2 v2.13.0 h1:5FhjW93/YLQJDmPdeyMPw7IjAPzqsr+0jHPfrPz0sZI= github.com/bradleyfalzon/ghinstallation/v2 v2.13.0/go.mod h1:EJ6fgedVEHa2kUyBTTvslJCXJafS/mhJNNKEOCspZXQ= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index 25bf7ce160..da7defd51e 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -57,6 +57,8 @@ version: 3 automerge: true autodiscover: mode: auto + ignore_paths: + - some/path delete_source_branch_on_merge: true parallel_plan: true parallel_apply: true @@ -405,6 +407,15 @@ the manual configuration will take precedence. Use this feature when some projects require specific configuration in a repo with many projects yet it's still desirable for Atlantis to plan/apply for projects not enumerated in the config. +```yaml +autodiscover: + mode: "enabled" + ignore_paths: + - dir/* +``` + +Autodiscover can also be configured to skip over directories that match a path glob (as defined [here](https://pkg.go.dev/github.com/bmatcuk/doublestar/v4)) + ### Custom Backend Config See [Custom Workflow Use Cases: Custom Backend Config](custom-workflows.md#custom-backend-config) diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 2469eec4d7..892466a747 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -99,6 +99,9 @@ repos: # autodiscover defines how atlantis should automatically discover projects in this repository. autodiscover: mode: auto + # Optionally ignore some paths for autodiscovery by a glob path + ignore_paths: + - foo/* # id can also be an exact match. - id: github.com/myorg/specific-repo diff --git a/server/core/config/raw/autodiscover.go b/server/core/config/raw/autodiscover.go index 156128d271..1be3c0e166 100644 --- a/server/core/config/raw/autodiscover.go +++ b/server/core/config/raw/autodiscover.go @@ -1,14 +1,20 @@ package raw import ( + "fmt" + "strings" + + "github.com/bmatcuk/doublestar/v4" validation "github.com/go-ozzo/ozzo-validation" + "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/config/valid" ) var DefaultAutoDiscoverMode = valid.AutoDiscoverAutoMode type AutoDiscover struct { - Mode *valid.AutoDiscoverMode `yaml:"mode,omitempty"` + Mode *valid.AutoDiscoverMode `yaml:"mode,omitempty"` + IgnorePaths []string `yaml:"ignore_paths,omitempty"` } func (a AutoDiscover) ToValid() *valid.AutoDiscover { @@ -20,19 +26,44 @@ func (a AutoDiscover) ToValid() *valid.AutoDiscover { v.Mode = DefaultAutoDiscoverMode } + v.IgnorePaths = a.IgnorePaths + return &v } func (a AutoDiscover) Validate() error { + + ignoreValid := func(value interface{}) error { + strSlice := value.([]string) + if strSlice == nil { + return nil + } + for _, ignore := range strSlice { + // A beginning slash isn't necessary since they are specifying a relative path, not an absolute one. + // Rejecting `/...` also allows us to potentially use `/.*/` as regexes in the future + if strings.HasPrefix(ignore, "/") { + return errors.New("pattern must not begin with a slash '/'") + } + + if !doublestar.ValidatePattern(ignore) { + return fmt.Errorf("invalid pattern: %s", ignore) + } + + } + return nil + } + res := validation.ValidateStruct(&a, // If a.Mode is nil, this should still pass validation. validation.Field(&a.Mode, validation.In(valid.AutoDiscoverAutoMode, valid.AutoDiscoverDisabledMode, valid.AutoDiscoverEnabledMode)), + validation.Field(&a.IgnorePaths, validation.By(ignoreValid)), ) return res } func DefaultAutoDiscover() *valid.AutoDiscover { return &valid.AutoDiscover{ - Mode: DefaultAutoDiscoverMode, + Mode: DefaultAutoDiscoverMode, + IgnorePaths: nil, } } diff --git a/server/core/config/raw/autodiscover_test.go b/server/core/config/raw/autodiscover_test.go index 9164913126..a1b45c77ee 100644 --- a/server/core/config/raw/autodiscover_test.go +++ b/server/core/config/raw/autodiscover_test.go @@ -19,16 +19,20 @@ func TestAutoDiscover_UnmarshalYAML(t *testing.T) { description: "omit unset fields", input: "", exp: raw.AutoDiscover{ - Mode: nil, + Mode: nil, + IgnorePaths: nil, }, }, { description: "all fields set", input: ` mode: enabled +ignore_paths: + - foobar `, exp: raw.AutoDiscover{ - Mode: &autoDiscoverEnabled, + Mode: &autoDiscoverEnabled, + IgnorePaths: []string{"foobar"}, }, }, } @@ -86,6 +90,67 @@ func TestAutoDiscover_Validate(t *testing.T) { }, errContains: String("valid value"), }, + { + description: "ignore set with leading slash", + input: raw.AutoDiscover{ + Mode: &autoDiscoverAuto, + IgnorePaths: []string{ + "/foo", + }, + }, + errContains: String("pattern must not begin with a slash '/'"), + }, + { + description: `ignore set to broken pattern \`, + input: raw.AutoDiscover{ + Mode: &autoDiscoverAuto, + IgnorePaths: []string{ + `\`, + }, + }, + errContains: String(`invalid pattern: \`), + }, + { + description: "ignore set to broken pattern [", + input: raw.AutoDiscover{ + Mode: &autoDiscoverAuto, + IgnorePaths: []string{ + "[", + }, + }, + errContains: String("invalid pattern: ["), + }, + { + description: "ignore set to valid pattern", + input: raw.AutoDiscover{ + Mode: &autoDiscoverAuto, + IgnorePaths: []string{ + "foo*", + }, + }, + errContains: nil, + }, + { + description: "ignore set to long pattern", + input: raw.AutoDiscover{ + Mode: &autoDiscoverAuto, + IgnorePaths: []string{ + "foo/**/bar/baz/??", + }, + }, + errContains: nil, + }, + { + description: "ignore set to one valid and one invalid pattern", + input: raw.AutoDiscover{ + Mode: &autoDiscoverAuto, + IgnorePaths: []string{ + "foo", + "foo[", + }, + }, + errContains: String("invalid pattern: foo["), + }, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { @@ -109,16 +174,25 @@ func TestAutoDiscover_ToValid(t *testing.T) { description: "nothing set", input: raw.AutoDiscover{}, exp: &valid.AutoDiscover{ - Mode: valid.AutoDiscoverAutoMode, + Mode: valid.AutoDiscoverAutoMode, + IgnorePaths: nil, }, }, { description: "value set", input: raw.AutoDiscover{ Mode: &autoDiscoverEnabled, + IgnorePaths: []string{ + "foo", + "bar/*", + }, }, exp: &valid.AutoDiscover{ Mode: valid.AutoDiscoverEnabledMode, + IgnorePaths: []string{ + "foo", + "bar/*", + }, }, }, } diff --git a/server/core/config/raw/repo_cfg_test.go b/server/core/config/raw/repo_cfg_test.go index 245f2d56d2..2c405da34f 100644 --- a/server/core/config/raw/repo_cfg_test.go +++ b/server/core/config/raw/repo_cfg_test.go @@ -130,6 +130,8 @@ version: 3 automerge: true autodiscover: mode: enabled + ignore_paths: + - foo/* parallel_apply: true parallel_plan: false repo_locks: @@ -157,8 +159,11 @@ allowed_regexp_prefixes: - dev/ - staging/`, exp: raw.RepoCfg{ - Version: Int(3), - AutoDiscover: &raw.AutoDiscover{Mode: &autoDiscoverEnabled}, + Version: Int(3), + AutoDiscover: &raw.AutoDiscover{ + Mode: &autoDiscoverEnabled, + IgnorePaths: []string{"foo/*"}, + }, Automerge: Bool(true), ParallelApply: Bool(true), ParallelPlan: Bool(false), diff --git a/server/core/config/valid/autodiscover.go b/server/core/config/valid/autodiscover.go index c131c3bffe..1e78309a54 100644 --- a/server/core/config/valid/autodiscover.go +++ b/server/core/config/valid/autodiscover.go @@ -10,5 +10,6 @@ const ( ) type AutoDiscover struct { - Mode AutoDiscoverMode + Mode AutoDiscoverMode + IgnorePaths []string } diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go index 8478ce3dd0..0d782c5d2a 100644 --- a/server/core/config/valid/repo_cfg.go +++ b/server/core/config/valid/repo_cfg.go @@ -8,7 +8,9 @@ import ( "regexp" "strings" + "github.com/bmatcuk/doublestar/v4" version "github.com/hashicorp/go-version" + "github.com/pkg/errors" ) // RepoCfg is the atlantis.yaml config after it's been parsed and validated. @@ -111,6 +113,24 @@ func (r RepoCfg) AutoDiscoverEnabled(defaultAutoDiscoverMode AutoDiscoverMode) b return autoDiscoverMode == AutoDiscoverEnabledMode } +func (r RepoCfg) IsPathIgnoredForAutoDiscover(path string) (bool, error) { + if r.AutoDiscover == nil || r.AutoDiscover.IgnorePaths == nil { + return false, nil + } + for i := 0; i < len(r.AutoDiscover.IgnorePaths); i++ { + matches, err := doublestar.Match(r.AutoDiscover.IgnorePaths[i], path) + if err != nil { + // Per documentation https://pkg.go.dev/github.com/bmatcuk/doublestar, this only + // occurs if the pattern itself is invalid, and we already checked this when parsing raw config + return false, errors.Wrap(err, "unexpectedly found invalid ignore pattern (this is a bug, should have been validated at startup)") + } + if matches { + return true, nil + } + } + return false, nil +} + // validateWorkspaceAllowed returns an error if repoCfg defines projects in // repoRelDir but none of them use workspace. We want this to be an error // because if users have gone to the trouble of defining projects in repoRelDir diff --git a/server/core/config/valid/repo_cfg_test.go b/server/core/config/valid/repo_cfg_test.go index 9b94994f51..8a2b68803f 100644 --- a/server/core/config/valid/repo_cfg_test.go +++ b/server/core/config/valid/repo_cfg_test.go @@ -316,10 +316,103 @@ func TestConfig_AutoDiscoverEnabled(t *testing.T) { AutoDiscover: nil, } if c.repoAutoDiscover != "" { - r.AutoDiscover = &valid.AutoDiscover{c.repoAutoDiscover} + r.AutoDiscover = &valid.AutoDiscover{ + Mode: c.repoAutoDiscover, + } } enabled := r.AutoDiscoverEnabled(c.defaultAutoDiscover) Equals(t, c.expEnabled, enabled) }) } } + +func TestConfig_IsPathIgnoredForAutoDiscover(t *testing.T) { + cases := []struct { + description string + repoCfg valid.RepoCfg + path string + expIgnored bool + }{ + { + description: "auto discover unconfigured", + repoCfg: valid.RepoCfg{}, + path: "foo", + expIgnored: false, + }, + { + description: "auto discover configured, but not path", + repoCfg: valid.RepoCfg{ + AutoDiscover: &valid.AutoDiscover{}, + }, + path: "foo", + expIgnored: false, + }, + { + description: "paths do not match pattern", + repoCfg: valid.RepoCfg{ + AutoDiscover: &valid.AutoDiscover{ + IgnorePaths: []string{ + "bar", + }, + }, + }, + path: "foo", + expIgnored: false, + }, + { + description: "path does match pattern", + repoCfg: valid.RepoCfg{ + AutoDiscover: &valid.AutoDiscover{ + IgnorePaths: []string{ + "fo?", + }}, + }, + path: "foo", + expIgnored: true, + }, + { + description: "one path matches pattern, another doesn't", + repoCfg: valid.RepoCfg{ + AutoDiscover: &valid.AutoDiscover{ + IgnorePaths: []string{ + "fo*", + "ba*", + }}, + }, + path: "foo", + expIgnored: true, + }, + { + description: "long path does match pattern", + repoCfg: valid.RepoCfg{ + AutoDiscover: &valid.AutoDiscover{ + IgnorePaths: []string{ + "foo/*/baz", + }, + }, + }, + path: "foo/bar/baz", + expIgnored: true, + }, + { + description: "long path does not match pattern", + repoCfg: valid.RepoCfg{ + AutoDiscover: &valid.AutoDiscover{ + IgnorePaths: []string{ + "foo/*/baz", + }, + }, + }, + path: "foo/bar/boo", + expIgnored: false, + }, + } + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + + enabled, err := c.repoCfg.IsPathIgnoredForAutoDiscover(c.path) + Ok(t, err) + Equals(t, c.expIgnored, enabled) + }) + } +} diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index c06059dd33..e0087dbb1f 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -420,7 +420,15 @@ func (p *DefaultProjectCommandBuilder) getMergedProjectCfgs(ctx *command.Context configuredProjDirs[filepath.Clean(configProj.Dir)] = true } for _, mp := range allModifiedProjects { - _, dirExists := configuredProjDirs[filepath.Clean(mp.Path)] + path := filepath.Clean(mp.Path) + ignore, err := repoCfg.IsPathIgnoredForAutoDiscover(path) + if err != nil { + return nil, err + } + if ignore { + continue + } + _, dirExists := configuredProjDirs[path] if !dirExists { modifiedProjects = append(modifiedProjects, mp) } diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index 30dec015a5..bb16148893 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -1078,6 +1078,78 @@ projects: }, }, }, + "autodiscover enabled but project excluded by autodiscover ignore": { + DirStructure: map[string]interface{}{ + "project1": map[string]interface{}{ + "main.tf": nil, + }, + "project2": map[string]interface{}{ + "main.tf": nil, + }, + "project3": map[string]interface{}{ + "main.tf": nil, + }, + }, + AtlantisYAML: `version: 3 +autodiscover: + mode: enabled + ignore_paths: + - project3 +projects: +- name: project1-custom-name + dir: project1`, + ModifiedFiles: []string{"project1/main.tf", "project2/main.tf", "project3/main.tf"}, + // project2 is autodiscovered, but autodiscover was ignored for project3 + // project1 is configured explicitly so added + Exp: []expCtxFields{ + { + ProjectName: "project1-custom-name", + RepoRelDir: "project1", + Workspace: "default", + }, + { + ProjectName: "", + RepoRelDir: "project2", + Workspace: "default", + }, + }, + }, + "autodiscover enabled but ignoring explicit project": { + DirStructure: map[string]interface{}{ + "project1": map[string]interface{}{ + "main.tf": nil, + }, + "project2": map[string]interface{}{ + "main.tf": nil, + }, + "project3": map[string]interface{}{ + "main.tf": nil, + }, + }, + AtlantisYAML: `version: 3 +autodiscover: + mode: enabled + ignore_paths: + - project1 +projects: +- name: project1-custom-name + dir: project1`, + ModifiedFiles: []string{"project1/main.tf", "project2/main.tf"}, + // project2 is autodiscover-ignored, but configured explicitly so added + // project1 is autodiscoverd as normal + Exp: []expCtxFields{ + { + ProjectName: "project1-custom-name", + RepoRelDir: "project1", + Workspace: "default", + }, + { + ProjectName: "", + RepoRelDir: "project2", + Workspace: "default", + }, + }, + }, "autodiscover enabled but project excluded by empty when_modified": { DirStructure: map[string]interface{}{ "project1": map[string]interface{}{ From cbd70c98866fc1f316f6353482fe3f567946a1e3 Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Fri, 24 Jan 2025 16:25:44 -0500 Subject: [PATCH 40/60] chore: Simplify use of the doublestar Match for ignore paths (#5272) Signed-off-by: Luke Massa --- server/core/config/valid/repo_cfg.go | 20 ++++++++------------ server/core/config/valid/repo_cfg_test.go | 5 ++--- server/events/project_command_builder.go | 6 +----- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go index 0d782c5d2a..afdb31412b 100644 --- a/server/core/config/valid/repo_cfg.go +++ b/server/core/config/valid/repo_cfg.go @@ -10,7 +10,6 @@ import ( "github.com/bmatcuk/doublestar/v4" version "github.com/hashicorp/go-version" - "github.com/pkg/errors" ) // RepoCfg is the atlantis.yaml config after it's been parsed and validated. @@ -113,22 +112,19 @@ func (r RepoCfg) AutoDiscoverEnabled(defaultAutoDiscoverMode AutoDiscoverMode) b return autoDiscoverMode == AutoDiscoverEnabledMode } -func (r RepoCfg) IsPathIgnoredForAutoDiscover(path string) (bool, error) { +func (r RepoCfg) IsPathIgnoredForAutoDiscover(path string) bool { if r.AutoDiscover == nil || r.AutoDiscover.IgnorePaths == nil { - return false, nil + return false } for i := 0; i < len(r.AutoDiscover.IgnorePaths); i++ { - matches, err := doublestar.Match(r.AutoDiscover.IgnorePaths[i], path) - if err != nil { - // Per documentation https://pkg.go.dev/github.com/bmatcuk/doublestar, this only - // occurs if the pattern itself is invalid, and we already checked this when parsing raw config - return false, errors.Wrap(err, "unexpectedly found invalid ignore pattern (this is a bug, should have been validated at startup)") - } - if matches { - return true, nil + // Per documentation https://pkg.go.dev/github.com/bmatcuk/doublestar, if you run ValidatePattern() + // against a pattern, which we do, you can run MatchUnvalidated for a slight performance gain, + // and also no need to explicitly check for an error + if doublestar.MatchUnvalidated(r.AutoDiscover.IgnorePaths[i], path) { + return true } } - return false, nil + return false } // validateWorkspaceAllowed returns an error if repoCfg defines projects in diff --git a/server/core/config/valid/repo_cfg_test.go b/server/core/config/valid/repo_cfg_test.go index 8a2b68803f..c35870ee60 100644 --- a/server/core/config/valid/repo_cfg_test.go +++ b/server/core/config/valid/repo_cfg_test.go @@ -410,9 +410,8 @@ func TestConfig_IsPathIgnoredForAutoDiscover(t *testing.T) { for _, c := range cases { t.Run(c.description, func(t *testing.T) { - enabled, err := c.repoCfg.IsPathIgnoredForAutoDiscover(c.path) - Ok(t, err) - Equals(t, c.expIgnored, enabled) + ignored := c.repoCfg.IsPathIgnoredForAutoDiscover(c.path) + Equals(t, c.expIgnored, ignored) }) } } diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index e0087dbb1f..2e42cfc8a4 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -421,11 +421,7 @@ func (p *DefaultProjectCommandBuilder) getMergedProjectCfgs(ctx *command.Context } for _, mp := range allModifiedProjects { path := filepath.Clean(mp.Path) - ignore, err := repoCfg.IsPathIgnoredForAutoDiscover(path) - if err != nil { - return nil, err - } - if ignore { + if repoCfg.IsPathIgnoredForAutoDiscover(path) { continue } _, dirExists := configuredProjDirs[path] From 23bd14f884bbc8e008df4931e480c946f06b4f87 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 01:31:26 +0000 Subject: [PATCH 41/60] chore(deps): update docker/build-push-action digest to ca877d9 in .github/workflows/testing-env-image.yml (main) (#5273) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/atlantis-image.yml | 4 ++-- .github/workflows/testing-env-image.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/atlantis-image.yml b/.github/workflows/atlantis-image.yml index e341bc0fb6..631df74160 100644 --- a/.github/workflows/atlantis-image.yml +++ b/.github/workflows/atlantis-image.yml @@ -145,7 +145,7 @@ jobs: - name: "Build ${{ env.PUSH == 'true' && 'and push' || '' }} ${{ env.DOCKER_REPO }} image" id: build if: contains(fromJson('["push", "pull_request"]'), github.event_name) - uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6 + uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6 with: cache-from: type=gha cache-to: type=gha,mode=max @@ -209,7 +209,7 @@ jobs: - name: "Build and load into Docker" if: contains(fromJson('["push", "pull_request"]'), github.event_name) - uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6 + uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6 with: cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/testing-env-image.yml b/.github/workflows/testing-env-image.yml index 1f080a67da..8ea7eb655b 100644 --- a/.github/workflows/testing-env-image.yml +++ b/.github/workflows/testing-env-image.yml @@ -60,7 +60,7 @@ jobs: - run: echo "TODAY=$(date +"%Y.%m.%d")" >> $GITHUB_ENV - name: Build and push testing-env:${{env.TODAY}} image - uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6 + uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6 with: cache-from: type=gha cache-to: type=gha,mode=max From 5e4a35b596e6bb3775ad0f4be95523803f183579 Mon Sep 17 00:00:00 2001 From: StepSecurity Bot Date: Fri, 24 Jan 2025 20:45:00 -0800 Subject: [PATCH 42/60] ci: [StepSecurity] Apply security best practices (#5271) Signed-off-by: StepSecurity Bot Signed-off-by: Rui Chen Co-authored-by: RB <7775707+nitrocode@users.noreply.github.com> Co-authored-by: Rui Chen --- .github/workflows/atlantis-image.yml | 20 ++++++++++++++++++ .github/workflows/codeql.yml | 15 ++++++++++++++ .github/workflows/dependency-review.yml | 27 +++++++++++++++++++++++++ .github/workflows/labeler.yml | 5 +++++ .github/workflows/lint.yml | 15 ++++++++++++++ .github/workflows/pr-lint.yml | 5 +++++ .github/workflows/pr-size-labeler.yml | 5 +++++ .github/workflows/release.yml | 5 +++++ .github/workflows/renovate-config.yml | 5 +++++ .github/workflows/scorecard.yml | 5 +++++ .github/workflows/stale.yml | 5 +++++ .github/workflows/test.yml | 25 +++++++++++++++++++++++ .github/workflows/testing-env-image.yml | 18 +++++++++++++++++ .github/workflows/website.yml | 15 ++++++++++++++ .pre-commit-config.yaml | 22 ++++++++++++++++++++ 15 files changed, 192 insertions(+) create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/atlantis-image.yml b/.github/workflows/atlantis-image.yml index 631df74160..ce99be593a 100644 --- a/.github/workflows/atlantis-image.yml +++ b/.github/workflows/atlantis-image.yml @@ -29,6 +29,11 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 id: changes @@ -61,6 +66,11 @@ jobs: PUSH: ${{ github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) }} steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # Lint the Dockerfile first before setting anything up @@ -199,6 +209,11 @@ jobs: DOCKER_REPO: ghcr.io/${{ github.repository }} steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3 @@ -240,5 +255,10 @@ jobs: platform: [linux/arm64/v8, linux/amd64, linux/arm/v7] runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - run: 'echo "No build required"' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 81023f682a..1c57b817cc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -43,6 +43,11 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 id: changes @@ -72,6 +77,11 @@ jobs: # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -117,4 +127,9 @@ jobs: language: [ 'go', 'javascript' ] runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - run: 'echo "No build required"' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..1b495dbce7 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: 'Dependency Review' + uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index de8bd74352..1097aa7f9f 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -19,4 +19,9 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 781b160476..951538e9b6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,6 +30,11 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 id: changes @@ -47,6 +52,11 @@ jobs: name: Linting runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # need to setup go toolchain explicitly @@ -66,4 +76,9 @@ jobs: name: Linting runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - run: 'echo "No build required"' diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 6ec8adfc59..470135b108 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -15,6 +15,11 @@ jobs: name: Validate PR title runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-size-labeler.yml b/.github/workflows/pr-size-labeler.yml index 4e48b776d9..79e85a92a3 100644 --- a/.github/workflows/pr-size-labeler.yml +++ b/.github/workflows/pr-size-labeler.yml @@ -12,6 +12,11 @@ jobs: runs-on: ubuntu-latest name: Label the PR size steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: codelytv/pr-size-labeler@c7a55a022747628b50f3eb5bf863b9e796b8f274 # v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c0c6efa96..1a4d08613b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,11 @@ jobs: goreleaser: runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: submodules: true diff --git a/.github/workflows/renovate-config.yml b/.github/workflows/renovate-config.yml index 06283df876..899efb81f2 100644 --- a/.github/workflows/renovate-config.yml +++ b/.github/workflows/renovate-config.yml @@ -19,6 +19,11 @@ jobs: validate: runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 - run: npx --package renovate -c 'renovate-config-validator' diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b3cfe0671e..df1fdaed38 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -19,6 +19,11 @@ jobs: id-token: write steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2 + with: + egress-policy: audit + - name: 'Checkout code' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8d94509d5d..aedbc5f510 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,6 +12,11 @@ jobs: pull-requests: write # for actions/stale to close stale PRs runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 with: stale-pr-message: 'This issue is stale because it has been open for 1 month with no activity. Remove stale label or comment or this will be closed in 1 month.' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c2d527012..d1447c4bd2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,11 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 id: changes @@ -50,6 +55,11 @@ jobs: runs-on: ubuntu-24.04 container: ghcr.io/runatlantis/testing-env:latest@sha256:3d7b17d02ced2cb68ecc9d2ea3d2bef61fe8da52cf1631e4dff4de6503cb7237 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # need to setup go toolchain explicitly @@ -106,6 +116,11 @@ jobs: name: Tests runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - run: 'echo "No build required"' e2e-github: @@ -118,6 +133,11 @@ jobs: ATLANTIS_GH_TOKEN: ${{ secrets.ATLANTISBOT_GITHUB_TOKEN }} NGROK_AUTH_TOKEN: ${{ secrets.ATLANTISBOT_NGROK_AUTH_TOKEN }} steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 with: @@ -155,6 +175,11 @@ jobs: ATLANTIS_GITLAB_TOKEN: ${{ secrets.ATLANTISBOT_GITLAB_TOKEN }} NGROK_AUTH_TOKEN: ${{ secrets.ATLANTISBOT_NGROK_AUTH_TOKEN }} steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 with: diff --git a/.github/workflows/testing-env-image.yml b/.github/workflows/testing-env-image.yml index 8ea7eb655b..e42cdab80a 100644 --- a/.github/workflows/testing-env-image.yml +++ b/.github/workflows/testing-env-image.yml @@ -15,6 +15,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: changes: permissions: @@ -25,6 +28,11 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 id: changes @@ -40,6 +48,11 @@ jobs: name: Build Testing Env Image runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Set up QEMU @@ -77,4 +90,9 @@ jobs: name: Build Testing Env Image runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - run: 'echo "No build required"' diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 49f676cda1..7e70378595 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -26,6 +26,11 @@ jobs: if: github.event.pull_request.draft == false runs-on: ubuntu-24.04 steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 id: changes @@ -46,6 +51,11 @@ jobs: name: Website Link Check runs-on: ubuntu-latest steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: markdown-lint @@ -112,4 +122,9 @@ jobs: name: Website Link Check runs-on: ubuntu-latest steps: + - name: Harden Runner + uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 + with: + egress-policy: audit + - run: 'echo "No build required"' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..691d45e43b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: +- repo: https://github.com/gitleaks/gitleaks + rev: v8.16.3 + hooks: + - id: gitleaks +- repo: https://github.com/golangci/golangci-lint + rev: v1.52.2 + hooks: + - id: golangci-lint +- repo: https://github.com/jumanjihouse/pre-commit-hooks + rev: 3.0.0 + hooks: + - id: shellcheck +- repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.38.0 + hooks: + - id: eslint +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace From c864248ec23dd034b9f87fd39f12718c9bba0022 Mon Sep 17 00:00:00 2001 From: Edward <21154977+ragne@users.noreply.github.com> Date: Sat, 25 Jan 2025 14:33:29 +0100 Subject: [PATCH 43/60] feat: Enable `hide-prev-plan-comments` Feature for BitBucket Cloud (#4495) Signed-off-by: Simon Heather <32168619+X-Guardian@users.noreply.github.com> --- runatlantis.io/docs/access-credentials.md | 2 +- runatlantis.io/docs/server-configuration.md | 11 +- server/events/vcs/bitbucketcloud/client.go | 89 +++++- .../events/vcs/bitbucketcloud/client_test.go | 157 ++++++++++ server/events/vcs/bitbucketcloud/models.go | 28 ++ .../vcs/bitbucketcloud/testdata/comments.json | 272 ++++++++++++++++++ .../vcs/bitbucketcloud/testdata/user.json | 33 +++ 7 files changed, 586 insertions(+), 6 deletions(-) create mode 100644 server/events/vcs/bitbucketcloud/testdata/comments.json create mode 100644 server/events/vcs/bitbucketcloud/testdata/user.json diff --git a/runatlantis.io/docs/access-credentials.md b/runatlantis.io/docs/access-credentials.md index d7b76573ce..34023d5fdd 100644 --- a/runatlantis.io/docs/access-credentials.md +++ b/runatlantis.io/docs/access-credentials.md @@ -132,7 +132,7 @@ A new permission for `Actions` has been added, which is required for checking if * Create an App Password by following [BitBucket Cloud: Create an app password](https://support.atlassian.com/bitbucket-cloud/docs/create-an-app-password/) * Label the password "atlantis" -* Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them +* Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them. If you want to enable the [hide-prev-plan-comments](./server-configuration#hide-prev-plan-comments) feature and thus delete old comments, please add **Account**: **Read** as well. * Record the access token ### Bitbucket Server (aka Stash) diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index b55a363c95..6fd82de944 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -863,9 +863,14 @@ based on the organization or user that triggered the webhook. ``` Hide previous plan comments to declutter PRs. This is only supported in - GitHub and GitLab currently. This is not enabled by default. When using Github App, you need to set `--gh-app-slug` to enable this feature. - For github, ensure the `--gh-user` is set appropriately or comments will not be hidden. - + GitHub and GitLab and Bitbucket currently and is not enabled by default. + + For Bitbucket, the comments are deleted rather than hidden as Bitbucket does not support hiding comments. + + For GitHub, ensure the `--gh-user` is set appropriately or comments will not be hidden. + + When using the GitHub App, you need to set `--gh-app-slug` to enable this feature. + ### `--hide-unchanged-plan-comments` ```bash diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index b777030237..c0fdb8e590 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "strings" "unicode/utf8" validator "github.com/go-playground/validator/v10" @@ -39,6 +40,8 @@ func NewClient(httpClient *http.Client, username string, password string, atlant } } +var MY_UUID = "" + // GetModifiedFiles returns the names of files that were modified in the merge request // relative to the repo root, e.g. parent/child/file.txt. func (b *Client) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) { @@ -107,10 +110,92 @@ func (b *Client) ReactToComment(_ logging.SimpleLogging, _ models.Repo, _ int, _ return nil } -func (b *Client) HidePrevCommandComments(_ logging.SimpleLogging, _ models.Repo, _ int, _ string, _ string) error { +func (b *Client) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, _ string) error { + // there is no way to hide comment, so delete them instead + me, err := b.GetMyUUID() + if err != nil { + return errors.Wrapf(err, "Cannot get my uuid! Please check required scope of the auth token!") + } + logger.Debug("My bitbucket user UUID is: %s", me) + + comments, err := b.GetPullRequestComments(repo, pullNum) + if err != nil { + return err + } + + for _, c := range comments { + logger.Debug("Comment is %v", c.Content.Raw) + if strings.EqualFold(*c.User.UUID, me) { + // do the same crude filtering as github client does + body := strings.Split(c.Content.Raw, "\n") + logger.Debug("Body is %s", body) + if len(body) == 0 { + continue + } + firstLine := strings.ToLower(body[0]) + if strings.Contains(firstLine, strings.ToLower(command)) { + // we found our old comment that references that command + logger.Debug("Deleting comment with id %s", *c.ID) + err = b.DeletePullRequestComment(repo, pullNum, *c.ID) + if err != nil { + return err + } + } + } + } + return nil +} + +func (b *Client) DeletePullRequestComment(repo models.Repo, pullNum int, commentId int) error { + path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d/comments/%d", b.BaseURL, repo.FullName, pullNum, commentId) + _, err := b.makeRequest("DELETE", path, nil) + if err != nil { + return err + } return nil } +func (b *Client) GetPullRequestComments(repo models.Repo, pullNum int) (comments []PullRequestComment, err error) { + path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d/comments", b.BaseURL, repo.FullName, pullNum) + res, err := b.makeRequest("GET", path, nil) + if err != nil { + return comments, err + } + + var pulls PullRequestComments + if err := json.Unmarshal(res, &pulls); err != nil { + return comments, errors.Wrapf(err, "Could not parse response %q", string(res)) + } + return pulls.Values, nil +} + +func (b *Client) GetMyUUID() (uuid string, err error) { + if MY_UUID == "" { + path := fmt.Sprintf("%s/2.0/user", b.BaseURL) + resp, err := b.makeRequest("GET", path, nil) + + if err != nil { + return uuid, err + } + + var user User + if err := json.Unmarshal(resp, &user); err != nil { + return uuid, errors.Wrapf(err, "Could not parse response %q", string(resp)) + } + + if err := validator.New().Struct(user); err != nil { + return uuid, errors.Wrapf(err, "API response %q was missing a field", string(resp)) + } + + uuid = *user.UUID + MY_UUID = uuid + + return uuid, nil + } else { + return MY_UUID, nil + } +} + // PullIsApproved returns true if the merge request was approved. func (b *Client) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (approvalStatus models.ApprovalStatus, err error) { path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d", b.BaseURL, repo.FullName, pull.Num) @@ -254,7 +339,7 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b defer resp.Body.Close() // nolint: errcheck requestStr := fmt.Sprintf("%s %s", method, path) - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { respBody, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("making request %q unexpected status code: %d, body: %s", requestStr, resp.StatusCode, string(respBody)) } diff --git a/server/events/vcs/bitbucketcloud/client_test.go b/server/events/vcs/bitbucketcloud/client_test.go index 59108a14f3..5b5d1ab4af 100644 --- a/server/events/vcs/bitbucketcloud/client_test.go +++ b/server/events/vcs/bitbucketcloud/client_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "testing" "github.com/runatlantis/atlantis/server/events/models" @@ -367,3 +368,159 @@ func TestClient_MarkdownPullLink(t *testing.T) { exp := "#1" Equals(t, exp, s) } + +func TestClient_GetMyUUID(t *testing.T) { + json, err := os.ReadFile(filepath.Join("testdata", "user.json")) + Ok(t, err) + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/2.0/user": + w.Write(json) // nolint: errcheck + return + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + defer testServer.Close() + + client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") + client.BaseURL = testServer.URL + v, _ := client.GetMyUUID() + Equals(t, v, "{00000000-0000-0000-0000-000000000001}") +} + +func TestClient_GetComment(t *testing.T) { + json, err := os.ReadFile(filepath.Join("testdata", "comments.json")) + Ok(t, err) + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments": + w.Write(json) // nolint: errcheck + return + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + defer testServer.Close() + + client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") + client.BaseURL = testServer.URL + v, _ := client.GetPullRequestComments( + models.Repo{ + FullName: "myorg/myrepo", + Owner: "owner", + Name: "myrepo", + CloneURL: "", + SanitizedCloneURL: "", + VCSHost: models.VCSHost{ + Type: models.BitbucketCloud, + Hostname: "bitbucket.org", + }, + }, 5) + + Equals(t, len(v), 5) + exp := "Plan" + Assert(t, strings.Contains(v[1].Content.Raw, exp), "Comment should contain word \"%s\", has \"%s\"", exp, v[1].Content.Raw) +} + +func TestClient_DeleteComment(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments/1": + if r.Method == "DELETE" { + w.WriteHeader(http.StatusNoContent) + } + return + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + defer testServer.Close() + + client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") + client.BaseURL = testServer.URL + err := client.DeletePullRequestComment( + models.Repo{ + FullName: "myorg/myrepo", + Owner: "owner", + Name: "myrepo", + CloneURL: "", + SanitizedCloneURL: "", + VCSHost: models.VCSHost{ + Type: models.BitbucketCloud, + Hostname: "bitbucket.org", + }, + }, 5, 1) + Ok(t, err) +} + +func TestClient_HidePRComments(t *testing.T) { + logger := logging.NewNoopLogger(t) + comments, err := os.ReadFile(filepath.Join("testdata", "comments.json")) + Ok(t, err) + json, err := os.ReadFile(filepath.Join("testdata", "user.json")) + Ok(t, err) + + called := 0 + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + // we have two comments in the test file + // The code is going to delete them all and then create a new one + case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments/498931882": + if r.Method == "DELETE" { + w.WriteHeader(http.StatusNoContent) + } + w.Write([]byte("")) // nolint: errcheck + called += 1 + return + // This is the second one + case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments/498931784": + if r.Method == "DELETE" { + http.Error(w, "", http.StatusNoContent) + } + w.Write([]byte("")) // nolint: errcheck + called += 1 + return + case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments/49893111": + Assert(t, r.Method != "DELETE", "Shouldn't delete this one") + return + case "/2.0/repositories/myorg/myrepo/pullrequests/5/comments": + w.Write(comments) // nolint: errcheck + return + case "/2.0/user": + w.Write(json) // nolint: errcheck + return + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + defer testServer.Close() + + client := bitbucketcloud.NewClient(http.DefaultClient, "user", "pass", "runatlantis.io") + client.BaseURL = testServer.URL + err = client.HidePrevCommandComments(logger, + models.Repo{ + FullName: "myorg/myrepo", + Owner: "owner", + Name: "myrepo", + CloneURL: "", + SanitizedCloneURL: "", + VCSHost: models.VCSHost{ + Type: models.BitbucketCloud, + Hostname: "bitbucket.org", + }, + }, 5, "plan", "") + Ok(t, err) + Equals(t, 2, called) +} diff --git a/server/events/vcs/bitbucketcloud/models.go b/server/events/vcs/bitbucketcloud/models.go index 1da27ebbe3..d33fc14da6 100644 --- a/server/events/vcs/bitbucketcloud/models.go +++ b/server/events/vcs/bitbucketcloud/models.go @@ -45,6 +45,34 @@ type Repository struct { FullName *string `json:"full_name,omitempty" validate:"required"` Links Links `json:"links,omitempty" validate:"required"` } + +type User struct { + Type *string `json:"type,omitempty" validate:"required"` + CreateOn *string `json:"created_on" validate:"required"` + DisplayName *string `json:"display_name" validate:"required"` + Username *string `json:"username" validate:"required"` + UUID *string `json:"uuid" validate:"required"` +} + +type UserInComment struct { + Type *string `json:"type,omitempty" validate:"required"` + Nickname *string `json:"nickname" validate:"required"` + DisplayName *string `json:"display_name" validate:"required"` + UUID *string `json:"uuid" validate:"required"` +} + +type PullRequestComment struct { + ID *int `json:"id,omitempty" validate:"required"` + User *UserInComment `json:"user" validate:"required"` + Content *struct { + Raw string `json:"raw"` + } `json:"content" validate:"required"` +} + +type PullRequestComments struct { + Values []PullRequestComment `json:"values,omitempty"` +} + type PullRequest struct { ID *int `json:"id,omitempty" validate:"required"` Source *BranchMeta `json:"source,omitempty" validate:"required"` diff --git a/server/events/vcs/bitbucketcloud/testdata/comments.json b/server/events/vcs/bitbucketcloud/testdata/comments.json new file mode 100644 index 0000000000..746accc259 --- /dev/null +++ b/server/events/vcs/bitbucketcloud/testdata/comments.json @@ -0,0 +1,272 @@ +{ + "values": [ + { + "id": 498931784, + "created_on": "2024-05-07T12:21:45.858898+00:00", + "updated_on": "2024-05-07T12:21:45.859011+00:00", + "content": { + "type": "rendered", + "raw": "atlantis plan", + "markup": "markdown", + "html": "

atlantis plan

" + }, + "user": { + "display_name": "Ragne", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/EM-3.png" + }, + "html": { + "href": "https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/" + } + }, + "type": "user", + "uuid": "{00000000-0000-0000-0000-000000000001}", + "account_id": "000000:00000000-0000-0000-0000-000000000001", + "nickname": "Ragne" + }, + "deleted": false, + "pending": false, + "type": "pullrequest_comment", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931784" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931784" + } + }, + "pullrequest": { + "type": "pullrequest", + "id": 5, + "title": "for test", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5" + } + } + } + }, + { + "id": 498931802, + "created_on": "2024-05-07T12:21:48.737851+00:00", + "updated_on": "2024-05-07T12:21:48.737927+00:00", + "content": { + "type": "rendered", + "raw": "Ran Plan for 0 projects:", + "markup": "markdown", + "html": "

Ran Plan for 0 projects:

" + }, + "user": { + "display_name": "bb bot", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B600000000-0000-0000-0000-000000000000%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/d5c4bac76953df92f47d1dea43fcdba0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FKB-4.png" + }, + "html": { + "href": "https://bitbucket.org/%7B600000000-0000-0000-0000-000000000000%7D/" + } + }, + "type": "user", + "uuid": "{600000000-0000-0000-0000-000000000000}", + "account_id": "00000000-0000-0000-0000-000000000000", + "nickname": "bb bot" + }, + "deleted": false, + "pending": false, + "type": "pullrequest_comment", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931802" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931802" + } + }, + "pullrequest": { + "type": "pullrequest", + "id": 5, + "title": "for test", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5" + } + } + } + }, + { + "id": 498931882, + "created_on": "2024-05-07T12:22:01.870344+00:00", + "updated_on": "2024-05-07T12:22:01.870462+00:00", + "content": { + "type": "rendered", + "raw": "atlantis plan", + "markup": "markdown", + "html": "

atlantis plan

" + }, + "user": { + "display_name": "Ragne", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/EM-3.png" + }, + "html": { + "href": "https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/" + } + }, + "type": "user", + "uuid": "{00000000-0000-0000-0000-000000000001}", + "account_id": "000000:00000000-0000-0000-0000-000000000001", + "nickname": "Ragne" + }, + "deleted": false, + "pending": false, + "type": "pullrequest_comment", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931882" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931882" + } + }, + "pullrequest": { + "type": "pullrequest", + "id": 5, + "title": "for test", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5" + } + } + } + }, + { + "id": 498931901, + "created_on": "2024-05-07T12:22:04.981415+00:00", + "updated_on": "2024-05-07T12:22:04.981490+00:00", + "content": { + "type": "rendered", + "raw": "Ran Plan for 0 projects:", + "markup": "markdown", + "html": "

Ran Plan for 0 projects:

" + }, + "user": { + "display_name": "bb bot", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B600000000-0000-0000-0000-000000000000%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/d5c4bac76953df92f47d1dea43fcdba0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FKB-4.png" + }, + "html": { + "href": "https://bitbucket.org/%7B600000000-0000-0000-0000-000000000000%7D/" + } + }, + "type": "user", + "uuid": "{600000000-0000-0000-0000-000000000000}", + "account_id": "00000000-0000-0000-0000-000000000000", + "nickname": "bb bot" + }, + "deleted": false, + "pending": false, + "type": "pullrequest_comment", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931901" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931901" + } + }, + "pullrequest": { + "type": "pullrequest", + "id": 5, + "title": "for test", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5" + } + } + } + }, + { + "id": 49893111, + "created_on": "2024-05-07T12:22:05.981415+00:00", + "updated_on": "2024-05-07T12:22:05.981490+00:00", + "content": { + "type": "rendered", + "raw": "Ran Apply for 0 projects:", + "markup": "markdown", + "html": "

Ran Apply for 0 projects:

" + }, + "user": { + "display_name": "bb bot", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B600000000-0000-0000-0000-000000000000%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/d5c4bac76953df92f47d1dea43fcdba0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FKB-4.png" + }, + "html": { + "href": "https://bitbucket.org/%7B600000000-0000-0000-0000-000000000000%7D/" + } + }, + "type": "user", + "uuid": "{600000000-0000-0000-0000-000000000000}", + "account_id": "00000000-0000-0000-0000-000000000000", + "nickname": "bb bot" + }, + "deleted": false, + "pending": false, + "type": "pullrequest_comment", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931901" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931901" + } + }, + "pullrequest": { + "type": "pullrequest", + "id": 5, + "title": "for test", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5" + }, + "html": { + "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/5" + } + } + } + } + ], + "pagelen": 10, + "size": 4, + "page": 1 +} diff --git a/server/events/vcs/bitbucketcloud/testdata/user.json b/server/events/vcs/bitbucketcloud/testdata/user.json new file mode 100644 index 0000000000..336f27832a --- /dev/null +++ b/server/events/vcs/bitbucketcloud/testdata/user.json @@ -0,0 +1,33 @@ +{ + "display_name": "bb bot", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/RR-3.png" + }, + "repositories": { + "href": "https://api.bitbucket.org/2.0/repositories/%7B00000000-0000-0000-0000-000000000001%7D" + }, + "snippets": { + "href": "https://api.bitbucket.org/2.0/snippets/%7B00000000-0000-0000-0000-000000000001%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/" + }, + "hooks": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7B00000000-0000-0000-0000-000000000001%7D/hooks" + } + }, + "created_on": "2024-02-01T12:08:46.355300+00:00", + "type": "user", + "uuid": "{00000000-0000-0000-0000-000000000001}", + "has_2fa_enabled": null, + "username": "bb-bot", + "is_staff": false, + "account_id": "000000:00000000-0000-0000-0000-000000000001", + "nickname": "bb bot", + "account_status": "active", + "location": null +} From e2eacdcd340f8c838f93b0f5783819389c6a3d9f Mon Sep 17 00:00:00 2001 From: Eka Cahya Pratama Date: Sun, 26 Jan 2025 00:30:38 +0700 Subject: [PATCH 44/60] fix: Return correct status when using custom policy (#5156) Signed-off-by: bakuljajan --- server/core/runtime/run_step_runner.go | 5 +- server/core/runtime/run_step_runner_test.go | 145 ++++++++++---------- 2 files changed, 76 insertions(+), 74 deletions(-) diff --git a/server/core/runtime/run_step_runner.go b/server/core/runtime/run_step_runner.go index 20d55caee6..5fa32896d7 100644 --- a/server/core/runtime/run_step_runner.go +++ b/server/core/runtime/run_step_runner.go @@ -95,9 +95,10 @@ func (r *RunStepRunner) Run( err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, output) if !ctx.CustomPolicyCheck { ctx.Log.Debug("error: %s", err) - return "", err + } else { + ctx.Log.Debug("Treating custom policy tool error exit code as a policy failure. Error output: %s", err) } - ctx.Log.Debug("Treating custom policy tool error exit code as a policy failure. Error output: %s", err) + return "", err } switch postProcessOutput { diff --git a/server/core/runtime/run_step_runner_test.go b/server/core/runtime/run_step_runner_test.go index 2429d88fe8..51289f8c49 100644 --- a/server/core/runtime/run_step_runner_test.go +++ b/server/core/runtime/run_step_runner_test.go @@ -100,89 +100,90 @@ func TestRunStepRunner_Run(t *testing.T) { ExpOut: "args=-target=resource1,-target=resource2\n", }, } + for _, customPolicyCheck := range []bool{false, true} { + for _, c := range cases { + var projVersion *version.Version + var err error - for _, c := range cases { + projVersion, err = version.NewVersion("v0.11.0") - var projVersion *version.Version - var err error - - projVersion, err = version.NewVersion("v0.11.0") + if c.Version != "" { + projVersion, err = version.NewVersion(c.Version) + Ok(t, err) + } - if c.Version != "" { - projVersion, err = version.NewVersion(c.Version) Ok(t, err) - } - Ok(t, err) - - projTFDistribution := "terraform" - if c.Distribution != "" { - projTFDistribution = c.Distribution - } + projTFDistribution := "terraform" + if c.Distribution != "" { + projTFDistribution = c.Distribution + } - defaultVersion, _ := version.NewVersion("0.8") + defaultVersion, _ := version.NewVersion("0.8") - RegisterMockTestingT(t) - terraform := tfclientmocks.NewMockClient() - defaultDistribution := tf.NewDistributionTerraformWithDownloader(mocks.NewMockDownloader()) - When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())). - ThenReturn(nil) + RegisterMockTestingT(t) + terraform := tfclientmocks.NewMockClient() + defaultDistribution := tf.NewDistributionTerraformWithDownloader(mocks.NewMockDownloader()) + When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())). + ThenReturn(nil) - logger := logging.NewNoopLogger(t) - projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() - tmpDir := t.TempDir() + logger := logging.NewNoopLogger(t) + projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() + tmpDir := t.TempDir() - r := runtime.RunStepRunner{ - TerraformExecutor: terraform, - DefaultTFDistribution: defaultDistribution, - DefaultTFVersion: defaultVersion, - TerraformBinDir: "/bin/dir", - ProjectCmdOutputHandler: projectCmdOutputHandler, - } - t.Run(c.Command, func(t *testing.T) { - ctx := command.ProjectContext{ - BaseRepo: models.Repo{ - Name: "basename", - Owner: "baseowner", - }, - HeadRepo: models.Repo{ - Name: "headname", - Owner: "headowner", - }, - Pull: models.PullRequest{ - Num: 2, - URL: "https://github.com/runatlantis/atlantis/pull/2", - HeadBranch: "add-feat", - HeadCommit: "12345abcdef", - BaseBranch: "main", - Author: "acme", - }, - User: models.User{ - Username: "acme-user", - }, - Log: logger, - Workspace: "myworkspace", - RepoRelDir: "mydir", - TerraformDistribution: &projTFDistribution, - TerraformVersion: projVersion, - ProjectName: c.ProjectName, - EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"}, - } - out, err := r.Run(ctx, nil, c.Command, tmpDir, map[string]string{"test": "var"}, true, valid.PostProcessRunOutputShow) - if c.ExpErr != "" { - ErrContains(t, c.ExpErr, err) - return + r := runtime.RunStepRunner{ + TerraformExecutor: terraform, + DefaultTFDistribution: defaultDistribution, + DefaultTFVersion: defaultVersion, + TerraformBinDir: "/bin/dir", + ProjectCmdOutputHandler: projectCmdOutputHandler, } - Ok(t, err) - // Replace $DIR in the exp with the actual temp dir. We do this - // here because when constructing the cases we don't yet know the - // temp dir. - expOut := strings.Replace(c.ExpOut, "$DIR", tmpDir, -1) - Equals(t, expOut, out) + t.Run(fmt.Sprintf("%s_CustomPolicyCheck=%v", c.Command, customPolicyCheck), func(t *testing.T) { + ctx := command.ProjectContext{ + BaseRepo: models.Repo{ + Name: "basename", + Owner: "baseowner", + }, + HeadRepo: models.Repo{ + Name: "headname", + Owner: "headowner", + }, + Pull: models.PullRequest{ + Num: 2, + URL: "https://github.com/runatlantis/atlantis/pull/2", + HeadBranch: "add-feat", + HeadCommit: "12345abcdef", + BaseBranch: "main", + Author: "acme", + }, + User: models.User{ + Username: "acme-user", + }, + Log: logger, + Workspace: "myworkspace", + RepoRelDir: "mydir", + TerraformDistribution: &projTFDistribution, + TerraformVersion: projVersion, + ProjectName: c.ProjectName, + EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"}, + CustomPolicyCheck: customPolicyCheck, + } + out, err := r.Run(ctx, nil, c.Command, tmpDir, map[string]string{"test": "var"}, true, valid.PostProcessRunOutputShow) + if c.ExpErr != "" { + ErrContains(t, c.ExpErr, err) + return + } + Ok(t, err) + // Replace $DIR in the exp with the actual temp dir. We do this + // here because when constructing the cases we don't yet know the + // temp dir. + expOut := strings.Replace(c.ExpOut, "$DIR", tmpDir, -1) + Equals(t, expOut, out) - terraform.VerifyWasCalledOnce().EnsureVersion(Eq(logger), NotEq(defaultDistribution), Eq(projVersion)) - terraform.VerifyWasCalled(Never()).EnsureVersion(Eq(logger), Eq(defaultDistribution), Eq(defaultVersion)) + terraform.VerifyWasCalledOnce().EnsureVersion(Eq(logger), NotEq(defaultDistribution), Eq(projVersion)) + terraform.VerifyWasCalled(Never()).EnsureVersion(Eq(logger), Eq(defaultDistribution), Eq(defaultVersion)) - }) + }) + } } } From 90b1b13b71ec30b3ca3c6838646518ffb6be6f75 Mon Sep 17 00:00:00 2001 From: Pierre Guinoiseau Date: Sun, 26 Jan 2025 07:39:37 +1300 Subject: [PATCH 45/60] feat: Add support for GitLab groups (#4001) Signed-off-by: Pierre Guinoiseau --- cmd/server.go | 12 ++++ cmd/server_test.go | 1 + runatlantis.io/docs/server-configuration.md | 15 +++++ server/core/config/valid/policies.go | 14 +++++ server/core/config/valid/policies_test.go | 63 +++++++++++++++++++ .../events/command/team_allowlist_checker.go | 14 +++++ server/events/command_runner.go | 8 +-- server/events/command_runner_test.go | 4 +- .../events/external_team_allowlist_checker.go | 4 ++ server/events/project_command_runner.go | 3 +- server/events/project_command_runner_test.go | 2 +- server/events/vcs/azuredevops_client.go | 2 +- server/events/vcs/bitbucketcloud/client.go | 2 +- server/events/vcs/bitbucketserver/client.go | 2 +- server/events/vcs/client.go | 2 +- server/events/vcs/gitea/client.go | 2 +- server/events/vcs/github_client.go | 3 +- server/events/vcs/github_client_test.go | 12 ++-- server/events/vcs/gitlab_client.go | 45 +++++++++++-- server/events/vcs/gitlab_client_test.go | 56 +++++++++++++++-- server/events/vcs/mocks/mock_client.go | 30 +++++---- .../events/vcs/not_configured_vcs_client.go | 2 +- server/events/vcs/proxy.go | 4 +- .../gitlab-group-membership-success.json | 22 +++++++ .../vcs/testdata/gitlab-user-success.json | 11 ++++ server/server.go | 17 ++++- server/user_config.go | 1 + 27 files changed, 308 insertions(+), 45 deletions(-) create mode 100644 server/events/vcs/testdata/gitlab-group-membership-success.json create mode 100644 server/events/vcs/testdata/gitlab-user-success.json diff --git a/cmd/server.go b/cmd/server.go index b71c5b2da3..7682d795f5 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -107,6 +107,7 @@ const ( GiteaUserFlag = "gitea-user" GiteaWebhookSecretFlag = "gitea-webhook-secret" // nolint: gosec GiteaPageSizeFlag = "gitea-page-size" + GitlabGroupAllowlistFlag = "gitlab-group-allowlist" GitlabHostnameFlag = "gitlab-hostname" GitlabTokenFlag = "gitlab-token" GitlabUserFlag = "gitlab-user" @@ -360,6 +361,17 @@ var stringFlags = map[string]stringFlag{ "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + "Should be specified via the ATLANTIS_GITEA_WEBHOOK_SECRET environment variable.", }, + GitlabGroupAllowlistFlag: { + description: "Comma separated list of key-value pairs representing the GitLab groups and the operations that " + + "the members of a particular group are allowed to perform. " + + "The format is {group}:{command},{group}:{command}. " + + "Valid values for 'command' are 'plan', 'apply' and '*', e.g. 'myorg/dev:plan,myorg/ops:apply,myorg/devops:*'" + + "This example gives the users from the 'myorg/dev' GitLab group the permissions to execute the 'plan' command, " + + "the 'myorg/ops' group the permissions to execute the 'apply' command, " + + "and allows the 'myorg/devops' group to perform any operation. If this argument is not provided, the default value (*:*) " + + "will be used and the default behavior will be to not check permissions " + + "and to allow users from any group to perform any operation.", + }, GitlabHostnameFlag: { description: "Hostname of your GitLab Enterprise installation. If using gitlab.com, no need to set.", defaultValue: DefaultGitlabHostname, diff --git a/cmd/server_test.go b/cmd/server_test.go index 90d45c65df..049fb66832 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -104,6 +104,7 @@ var testFlags = map[string]interface{}{ GiteaUserFlag: "gitea-user", GiteaWebhookSecretFlag: "gitea-secret", GiteaPageSizeFlag: 30, + GitlabGroupAllowlistFlag: "", GitlabHostnameFlag: "gitlab-hostname", GitlabTokenFlag: "gitlab-token", GitlabUserFlag: "gitlab-user", diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 6fd82de944..18387db51b 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -800,6 +800,21 @@ based on the organization or user that triggered the webhook. This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. ::: +### `--gitlab-group-allowlist` + + ```bash + atlantis server --gitlab-group-allowlist="myorg/mygroup:plan, myorg/secteam:apply, myorg/devops:apply, myorg/devops:import" + # or + ATLANTIS_GITLAB_GROUP_ALLOWLIST="myorg/mygroup:plan, myorg/secteam:apply, myorg/devops:apply, myorg/devops:import" + ``` + + Comma-separated list of GitLab groups and permission pairs. + + By default, any group can plan and apply. + + ::: warning NOTE + Atlantis needs to be able to view the listed group members, inaccessible or non-existent groups are silently ignored. + ### `--gitlab-hostname` ```bash diff --git a/server/core/config/valid/policies.go b/server/core/config/valid/policies.go index 6aee54179c..718338d05b 100644 --- a/server/core/config/valid/policies.go +++ b/server/core/config/valid/policies.go @@ -1,6 +1,7 @@ package valid import ( + "slices" "strings" version "github.com/hashicorp/go-version" @@ -67,3 +68,16 @@ func (o *PolicyOwners) IsOwner(username string, userTeams []string) bool { return false } + +// Return all owner teams from all policy sets +func (p *PolicySets) AllTeams() []string { + teams := p.Owners.Teams + for _, policySet := range p.PolicySets { + for _, team := range policySet.Owners.Teams { + if !slices.Contains(teams, team) { + teams = append(teams, team) + } + } + } + return teams +} diff --git a/server/core/config/valid/policies_test.go b/server/core/config/valid/policies_test.go index c575a4585a..5147dd8686 100644 --- a/server/core/config/valid/policies_test.go +++ b/server/core/config/valid/policies_test.go @@ -120,3 +120,66 @@ func TestPoliciesConfig_IsOwners(t *testing.T) { }) } } + +func TestPoliciesConfig_AllTeams(t *testing.T) { + cases := []struct { + description string + input valid.PolicySets + expResult []string + }{ + { + description: "has only top-level team owner", + input: valid.PolicySets{ + Owners: valid.PolicyOwners{ + Teams: []string{ + "team1", + }, + }, + }, + expResult: []string{"team1"}, + }, + { + description: "has only policy-level team owner", + input: valid.PolicySets{ + PolicySets: []valid.PolicySet{ + { + Name: "policy1", + Owners: valid.PolicyOwners{ + Teams: []string{ + "team2", + }, + }, + }, + }, + }, + expResult: []string{"team2"}, + }, + { + description: "has both top-level and policy-level team owners", + input: valid.PolicySets{ + Owners: valid.PolicyOwners{ + Teams: []string{ + "team1", + }, + }, + PolicySets: []valid.PolicySet{ + { + Name: "policy1", + Owners: valid.PolicyOwners{ + Teams: []string{ + "team2", + }, + }, + }, + }, + }, + expResult: []string{"team1", "team2"}, + }, + } + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + result := c.input.AllTeams() + Equals(t, c.expResult, result) + }) + } +} diff --git a/server/events/command/team_allowlist_checker.go b/server/events/command/team_allowlist_checker.go index 5c58873650..f71684a6d7 100644 --- a/server/events/command/team_allowlist_checker.go +++ b/server/events/command/team_allowlist_checker.go @@ -21,6 +21,9 @@ type TeamAllowlistChecker interface { // IsCommandAllowedForAnyTeam determines if any of the specified teams can perform the specified action IsCommandAllowedForAnyTeam(ctx models.TeamAllowlistCheckerContext, teams []string, command string) bool + + // AllTeams returns all teams configured in the allowlist + AllTeams() []string } // DefaultTeamAllowlistChecker implements checking the teams and the operations that the members @@ -84,3 +87,14 @@ func (checker *DefaultTeamAllowlistChecker) IsCommandAllowedForAnyTeam(ctx model } return false } + +// AllTeams returns all teams configured in the allowlist +func (checker *DefaultTeamAllowlistChecker) AllTeams() []string { + var teamNames []string + for _, rule := range checker.rules { + for key := range rule { + teamNames = append(teamNames, key) + } + } + return teamNames +} diff --git a/server/events/command_runner.go b/server/events/command_runner.go index fd544baabf..daa66356d2 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -157,7 +157,7 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo // Check if the user who triggered the autoplan has permissions to run 'plan'. if c.TeamAllowlistChecker != nil && c.TeamAllowlistChecker.HasRules() { - err := c.fetchUserTeams(baseRepo, &user) + err := c.fetchUserTeams(log, baseRepo, &user) if err != nil { log.Err("Unable to fetch user teams: %s", err) return @@ -300,7 +300,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead // Check if the user who commented has the permissions to execute the 'plan' or 'apply' commands if c.TeamAllowlistChecker != nil && c.TeamAllowlistChecker.HasRules() { - err := c.fetchUserTeams(baseRepo, &user) + err := c.fetchUserTeams(log, baseRepo, &user) if err != nil { c.Logger.Err("Unable to fetch user teams: %s", err) return @@ -491,8 +491,8 @@ func (c *DefaultCommandRunner) ensureValidRepoMetadata( return } -func (c *DefaultCommandRunner) fetchUserTeams(repo models.Repo, user *models.User) error { - teams, err := c.VCSClient.GetTeamNamesForUser(repo, *user) +func (c *DefaultCommandRunner) fetchUserTeams(logger logging.SimpleLogging, repo models.Repo, user *models.User) error { + teams, err := c.VCSClient.GetTeamNamesForUser(logger, repo, *user) if err != nil { return err } diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 7b06d0f015..b3b88c40d0 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -313,7 +313,7 @@ func TestRunCommentCommand_TeamAllowListChecker(t *testing.T) { When(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil) ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan}) - vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(testdata.GithubRepo, testdata.User) + vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(ch.Logger, testdata.GithubRepo, testdata.User) vcsClient.VerifyWasCalledOnce().CreateComment( Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq("Ran Plan for 0 projects:"), Eq("plan")) }) @@ -331,7 +331,7 @@ func TestRunCommentCommand_TeamAllowListChecker(t *testing.T) { When(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil) ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan}) - vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(testdata.GithubRepo, testdata.User) + vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(ch.Logger, testdata.GithubRepo, testdata.User) vcsClient.VerifyWasCalledOnce().CreateComment( Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq("Ran Plan for 0 projects:"), Eq("plan")) }) diff --git a/server/events/external_team_allowlist_checker.go b/server/events/external_team_allowlist_checker.go index 9f3fe419ef..6592b6b181 100644 --- a/server/events/external_team_allowlist_checker.go +++ b/server/events/external_team_allowlist_checker.go @@ -39,6 +39,10 @@ func (checker *ExternalTeamAllowlistChecker) IsCommandAllowedForAnyTeam(ctx mode return checker.checkOutputResults(out) } +func (checker *ExternalTeamAllowlistChecker) AllTeams() []string { + return []string{} +} + func (checker *ExternalTeamAllowlistChecker) buildCommandString(ctx models.TeamAllowlistCheckerContext, teams []string, command string) string { // Build command string // Format is "$external_cmd $external_args $command $repo $teams" diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 79e1d7899c..0558fe35aa 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -225,6 +225,7 @@ type DefaultProjectCommandRunner struct { VcsClient vcs.Client Locker ProjectLocker LockURLGenerator LockURLGenerator + Logger logging.SimpleLogging InitStepRunner StepRunner PlanStepRunner StepRunner ShowStepRunner StepRunner @@ -367,7 +368,7 @@ func (p *DefaultProjectCommandRunner) doApprovePolicies(ctx command.ProjectConte // Only query the users team membership if any teams have been configured as owners on any policy set(s). if policySetCfg.HasTeamOwners() { // A convenient way to access vcsClient. Not sure if best way. - userTeams, err := p.VcsClient.GetTeamNamesForUser(ctx.Pull.BaseRepo, ctx.User) + userTeams, err := p.VcsClient.GetTeamNamesForUser(p.Logger, ctx.Pull.BaseRepo, ctx.User) if err != nil { ctx.Log.Err("unable to get team membership for user: %s", err) return nil, "", err diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go index b013741647..382bda6d18 100644 --- a/server/events/project_command_runner_test.go +++ b/server/events/project_command_runner_test.go @@ -1280,7 +1280,7 @@ func TestDefaultProjectCommandRunner_ApprovePolicies(t *testing.T) { } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num, Author: testdata.User.Username} - When(runner.VcsClient.GetTeamNamesForUser(testdata.GithubRepo, testdata.User)).ThenReturn(c.userTeams, nil) + When(runner.VcsClient.GetTeamNamesForUser(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.User))).ThenReturn(c.userTeams, nil) ctx := command.ProjectContext{ User: testdata.User, Log: logging.NewNoopLogger(t), diff --git a/server/events/vcs/azuredevops_client.go b/server/events/vcs/azuredevops_client.go index 35c303bae2..77ccf948e5 100644 --- a/server/events/vcs/azuredevops_client.go +++ b/server/events/vcs/azuredevops_client.go @@ -393,7 +393,7 @@ func SplitAzureDevopsRepoFullName(repoFullName string) (owner string, project st } // GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). -func (g *AzureDevopsClient) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { //nolint: revive +func (g *AzureDevopsClient) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) { //nolint: revive return nil, nil } diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index c0fdb8e590..e56f2eb9f5 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -351,7 +351,7 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b } // GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). -func (b *Client) GetTeamNamesForUser(_ models.Repo, _ models.User) ([]string, error) { +func (b *Client) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) { return nil, nil } diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go index 058b411100..47f61a526a 100644 --- a/server/events/vcs/bitbucketserver/client.go +++ b/server/events/vcs/bitbucketserver/client.go @@ -350,7 +350,7 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b } // GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). -func (b *Client) GetTeamNamesForUser(_ models.Repo, _ models.User) ([]string, error) { +func (b *Client) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) { return nil, nil } diff --git a/server/events/vcs/client.go b/server/events/vcs/client.go index 9e32981a82..3344ffa6e9 100644 --- a/server/events/vcs/client.go +++ b/server/events/vcs/client.go @@ -42,7 +42,7 @@ type Client interface { DiscardReviews(repo models.Repo, pull models.PullRequest) error MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error MarkdownPullLink(pull models.PullRequest) (string, error) - GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) + GetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) ([]string, error) // GetFileContent a repository file content from VCS (which support fetch a single file from repository) // The first return value indicates whether the repo contains a file or not diff --git a/server/events/vcs/gitea/client.go b/server/events/vcs/gitea/client.go index e971534288..e262d8d820 100644 --- a/server/events/vcs/gitea/client.go +++ b/server/events/vcs/gitea/client.go @@ -413,7 +413,7 @@ func (c *GiteaClient) MarkdownPullLink(pull models.PullRequest) (string, error) } // GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). -func (c *GiteaClient) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { +func (c *GiteaClient) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) { // TODO: implement return nil, errors.New("GetTeamNamesForUser not (yet) implemented for Gitea client") } diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index c75c0e07bd..89a79bf98d 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -1019,7 +1019,8 @@ func (g *GithubClient) MarkdownPullLink(pull models.PullRequest) (string, error) // GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). // https://docs.github.com/en/graphql/reference/objects#organization -func (g *GithubClient) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { +func (g *GithubClient) GetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) ([]string, error) { + logger.Debug("Getting GitHub team names for user '%s'", user) orgName := repo.Owner variables := map[string]interface{}{ "orgName": githubv4.String(orgName), diff --git a/server/events/vcs/github_client_test.go b/server/events/vcs/github_client_test.go index d53c5544b9..6d54df4c59 100644 --- a/server/events/vcs/github_client_test.go +++ b/server/events/vcs/github_client_test.go @@ -1398,11 +1398,13 @@ func TestGithubClient_GetTeamNamesForUser(t *testing.T) { Ok(t, err) defer disableSSLVerification()() - teams, err := client.GetTeamNamesForUser(models.Repo{ - Owner: "testrepo", - }, models.User{ - Username: "testuser", - }) + teams, err := client.GetTeamNamesForUser( + logger, + models.Repo{ + Owner: "testrepo", + }, models.User{ + Username: "testuser", + }) Ok(t, err) Equals(t, []string{"Frontend Developers", "frontend-developers", "Employees", "employees"}, teams) } diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go index ba111c85e9..2ac61a403f 100644 --- a/server/events/vcs/gitlab_client.go +++ b/server/events/vcs/gitlab_client.go @@ -41,6 +41,8 @@ type GitlabClient struct { Client *gitlab.Client // Version is set to the server version. Version *version.Version + // All GitLab groups configured in allowlists and policies + ConfiguredGroups []string // PollingInterval is the time between successive polls, where applicable. PollingInterval time.Duration // PollingInterval is the total duration for which to poll, where applicable. @@ -56,11 +58,12 @@ var commonMarkSupported = MustConstraint(">=11.1") var gitlabClientUnderTest = false // NewGitlabClient returns a valid GitLab client. -func NewGitlabClient(hostname string, token string, logger logging.SimpleLogging) (*GitlabClient, error) { +func NewGitlabClient(hostname string, token string, configuredGroups []string, logger logging.SimpleLogging) (*GitlabClient, error) { logger.Debug("Creating new GitLab client for %s", hostname) client := &GitlabClient{ - PollingInterval: time.Second, - PollingTimeout: time.Second * 30, + ConfiguredGroups: configuredGroups, + PollingInterval: time.Second, + PollingTimeout: time.Second * 30, } // Create the client differently depending on the base URL. @@ -620,9 +623,39 @@ func MustConstraint(constraint string) version.Constraints { return c } -// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). -func (g *GitlabClient) GetTeamNamesForUser(_ models.Repo, _ models.User) ([]string, error) { - return nil, nil +// GetTeamNamesForUser returns the names of the GitLab groups that the user belongs to. +// The user membership is checked in each group from configuredTeams, groups +// that the Atlantis user doesn't have access to are silently ignored. +func (g *GitlabClient) GetTeamNamesForUser(logger logging.SimpleLogging, _ models.Repo, user models.User) ([]string, error) { + logger.Debug("Getting GitLab group names for user '%s'", user) + var teamNames []string + + users, resp, err := g.Client.Users.ListUsers(&gitlab.ListUsersOptions{Username: &user.Username}) + if resp.StatusCode == http.StatusNotFound { + return teamNames, nil + } + if err != nil { + return nil, errors.Wrapf(err, "GET /users returned: %d", resp.StatusCode) + } else if len(users) == 0 { + return nil, errors.Wrap(err, "GET /users returned no user") + } else if len(users) > 1 { + // Theoretically impossible, just being extra safe + return nil, errors.Wrap(err, "GET /users returned more than 1 user") + } + userID := users[0].ID + for _, groupName := range g.ConfiguredGroups { + membership, resp, err := g.Client.GroupMembers.GetGroupMember(groupName, userID) + if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden { + continue + } + if err != nil { + return nil, errors.Wrapf(err, "GET /groups/%s/members/%d returned: %d", groupName, userID, resp.StatusCode) + } + if resp.StatusCode == http.StatusOK && membership.State == "active" { + teamNames = append(teamNames, groupName) + } + } + return teamNames, nil } // GetFileContent a repository file content from VCS (which support fetch a single file from repository) diff --git a/server/events/vcs/gitlab_client_test.go b/server/events/vcs/gitlab_client_test.go index d56bfa2f45..49c9a0f8f0 100644 --- a/server/events/vcs/gitlab_client_test.go +++ b/server/events/vcs/gitlab_client_test.go @@ -94,7 +94,7 @@ func TestNewGitlabClient_BaseURL(t *testing.T) { for _, c := range cases { t.Run(c.Hostname, func(t *testing.T) { log := logging.NewNoopLogger(t) - client, err := NewGitlabClient(c.Hostname, "token", log) + client, err := NewGitlabClient(c.Hostname, "token", []string{}, log) Ok(t, err) Equals(t, c.ExpBaseURL, client.Client.BaseURL().String()) }) @@ -887,7 +887,7 @@ func TestGitlabClient_MarkdownPullLink(t *testing.T) { logger := logging.NewNoopLogger(t) gitlabClientUnderTest = true defer func() { gitlabClientUnderTest = false }() - client, err := NewGitlabClient("gitlab.com", "token", logger) + client, err := NewGitlabClient("gitlab.com", "token", []string{}, logger) Ok(t, err) pull := models.PullRequest{Num: 1} s, _ := client.MarkdownPullLink(pull) @@ -1039,7 +1039,7 @@ func TestGitlabClient_HideOldComments(t *testing.T) { } } -func TestGithubClient_GetPullLabels(t *testing.T) { +func TestGitlabClient_GetPullLabels(t *testing.T) { logger := logging.NewNoopLogger(t) mergeSuccessWithLabel, err := os.ReadFile("testdata/gitlab-merge-success-with-label.json") Ok(t, err) @@ -1076,7 +1076,7 @@ func TestGithubClient_GetPullLabels(t *testing.T) { Equals(t, []string{"work in progress"}, labels) } -func TestGithubClient_GetPullLabels_EmptyResponse(t *testing.T) { +func TestGitlabClient_GetPullLabels_EmptyResponse(t *testing.T) { logger := logging.NewNoopLogger(t) pipelineSuccess, err := os.ReadFile("testdata/gitlab-pipeline-success.json") Ok(t, err) @@ -1110,3 +1110,51 @@ func TestGithubClient_GetPullLabels_EmptyResponse(t *testing.T) { Ok(t, err) Equals(t, 0, len(labels)) } + +// GetTeamNamesForUser returns the names of the GitLab groups that the user belongs to. +func TestGitlabClient_GetTeamNamesForUser(t *testing.T) { + logger := logging.NewNoopLogger(t) + + groupMembershipSuccess, err := os.ReadFile("testdata/gitlab-group-membership-success.json") + Ok(t, err) + + userSuccess, err := os.ReadFile("testdata/gitlab-user-success.json") + Ok(t, err) + + configuredGroups := []string{"someorg/group1", "someorg/group2", "someorg/group3", "someorg/group4"} + testServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/api/v4/users?username=testuser": + w.WriteHeader(http.StatusOK) + w.Write(userSuccess) // nolint: errcheck + case "/api/v4/groups/someorg%2Fgroup1/members/123", "/api/v4/groups/someorg%2Fgroup2/members/123": + w.WriteHeader(http.StatusOK) + w.Write(groupMembershipSuccess) // nolint: errcheck + case "/api/v4/groups/someorg%2Fgroup3/members/123": + http.Error(w, "forbidden", http.StatusForbidden) + case "/api/v4/groups/someorg%2Fgroup4/members/123": + http.Error(w, "not found", http.StatusNotFound) + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + } + })) + internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) + Ok(t, err) + client := &GitlabClient{ + Client: internalClient, + Version: nil, + ConfiguredGroups: configuredGroups, + } + + teams, err := client.GetTeamNamesForUser( + logger, + models.Repo{ + Owner: "someorg", + }, models.User{ + Username: "testuser", + }) + Ok(t, err) + Equals(t, []string{"someorg/group1", "someorg/group2"}, teams) +} diff --git a/server/events/vcs/mocks/mock_client.go b/server/events/vcs/mocks/mock_client.go index e798b8b79a..3b4bd7dbf7 100644 --- a/server/events/vcs/mocks/mock_client.go +++ b/server/events/vcs/mocks/mock_client.go @@ -136,11 +136,11 @@ func (mock *MockClient) GetPullLabels(logger logging.SimpleLogging, repo models. return _ret0, _ret1 } -func (mock *MockClient) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { +func (mock *MockClient) GetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) ([]string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") } - _params := []pegomock.Param{repo, user} + _params := []pegomock.Param{logger, repo, user} _result := pegomock.GetGenericMockFrom(mock).Invoke("GetTeamNamesForUser", _params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var _ret0 []string var _ret1 error @@ -576,8 +576,8 @@ func (c *MockClient_GetPullLabels_OngoingVerification) GetAllCapturedArguments() return } -func (verifier *VerifierMockClient) GetTeamNamesForUser(repo models.Repo, user models.User) *MockClient_GetTeamNamesForUser_OngoingVerification { - _params := []pegomock.Param{repo, user} +func (verifier *VerifierMockClient) GetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) *MockClient_GetTeamNamesForUser_OngoingVerification { + _params := []pegomock.Param{logger, repo, user} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetTeamNamesForUser", _params, verifier.timeout) return &MockClient_GetTeamNamesForUser_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -587,24 +587,30 @@ type MockClient_GetTeamNamesForUser_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockClient_GetTeamNamesForUser_OngoingVerification) GetCapturedArguments() (models.Repo, models.User) { - repo, user := c.GetAllCapturedArguments() - return repo[len(repo)-1], user[len(user)-1] +func (c *MockClient_GetTeamNamesForUser_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.User) { + logger, repo, user := c.GetAllCapturedArguments() + return logger[len(logger)-1], repo[len(repo)-1], user[len(user)-1] } -func (c *MockClient_GetTeamNamesForUser_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.User) { +func (c *MockClient_GetTeamNamesForUser_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.User) { _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(_params) > 0 { if len(_params) > 0 { - _param0 = make([]models.Repo, len(c.methodInvocations)) + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) for u, param := range _params[0] { - _param0[u] = param.(models.Repo) + _param0[u] = param.(logging.SimpleLogging) } } if len(_params) > 1 { - _param1 = make([]models.User, len(c.methodInvocations)) + _param1 = make([]models.Repo, len(c.methodInvocations)) for u, param := range _params[1] { - _param1[u] = param.(models.User) + _param1[u] = param.(models.Repo) + } + } + if len(_params) > 2 { + _param2 = make([]models.User, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(models.User) } } } diff --git a/server/events/vcs/not_configured_vcs_client.go b/server/events/vcs/not_configured_vcs_client.go index 41b14ad2c6..0a48f210b6 100644 --- a/server/events/vcs/not_configured_vcs_client.go +++ b/server/events/vcs/not_configured_vcs_client.go @@ -60,7 +60,7 @@ func (a *NotConfiguredVCSClient) MarkdownPullLink(_ models.PullRequest) (string, func (a *NotConfiguredVCSClient) err() error { return fmt.Errorf("atlantis was not configured to support repos from %s", a.Host.String()) } -func (a *NotConfiguredVCSClient) GetTeamNamesForUser(_ models.Repo, _ models.User) ([]string, error) { +func (a *NotConfiguredVCSClient) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) { return nil, a.err() } diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index 68aa45bf58..71d66116b1 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -97,8 +97,8 @@ func (d *ClientProxy) MarkdownPullLink(pull models.PullRequest) (string, error) return d.clients[pull.BaseRepo.VCSHost.Type].MarkdownPullLink(pull) } -func (d *ClientProxy) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { - return d.clients[repo.VCSHost.Type].GetTeamNamesForUser(repo, user) +func (d *ClientProxy) GetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) ([]string, error) { + return d.clients[repo.VCSHost.Type].GetTeamNamesForUser(logger, repo, user) } func (d *ClientProxy) GetFileContent(logger logging.SimpleLogging, pull models.PullRequest, fileName string) (bool, []byte, error) { diff --git a/server/events/vcs/testdata/gitlab-group-membership-success.json b/server/events/vcs/testdata/gitlab-group-membership-success.json new file mode 100644 index 0000000000..897c4438ad --- /dev/null +++ b/server/events/vcs/testdata/gitlab-group-membership-success.json @@ -0,0 +1,22 @@ +{ + "access_level": 50, + "created_at": "2023-11-28T01:23:45.789Z", + "created_by": { + "id": 456, + "username": "someone", + "name": "Someone", + "state": "active", + "locked": false, + "avatar_url": "https://gitlab.com/uploads/-/system/user/avatar/456/avatar.png", + "web_url": "https://gitlab.com/someone" + }, + "expires_at": null, + "id": 123, + "username": "testuser", + "name": "Test User", + "state": "active", + "locked": false, + "avatar_url": "https://gitlab.com/uploads/-/system/user/avatar/123/avatar.png", + "web_url": "https://gitlab.com/testuser", + "membership_state": "active" +} diff --git a/server/events/vcs/testdata/gitlab-user-success.json b/server/events/vcs/testdata/gitlab-user-success.json new file mode 100644 index 0000000000..0b87fc9e12 --- /dev/null +++ b/server/events/vcs/testdata/gitlab-user-success.json @@ -0,0 +1,11 @@ +[ + { + "id": 123, + "username": "testuser", + "name": "Test User", + "state": "active", + "locked": false, + "avatar_url": "https://gitlab.com/uploads/-/system/user/avatar/123/avatar.png", + "web_url": "https://gitlab.com/testuser" + } +] diff --git a/server/server.go b/server/server.go index 3bd9f6fb51..97363fcdf3 100644 --- a/server/server.go +++ b/server/server.go @@ -28,6 +28,7 @@ import ( "os" "os/signal" "path/filepath" + "slices" "sort" "strings" "syscall" @@ -274,7 +275,15 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { if userConfig.GitlabUser != "" { supportedVCSHosts = append(supportedVCSHosts, models.Gitlab) var err error - gitlabClient, err = vcs.NewGitlabClient(userConfig.GitlabHostname, userConfig.GitlabToken, logger) + + gitlabGroupAllowlistChecker, err := command.NewTeamAllowlistChecker(userConfig.GitlabGroupAllowlist) + if err != nil { + return nil, err + } + + gitlabGroups := slices.Concat(gitlabGroupAllowlistChecker.AllTeams(), globalCfg.PolicySets.AllTeams()) + slices.Sort(gitlabGroups) + gitlabClient, err = vcs.NewGitlabClient(userConfig.GitlabHostname, userConfig.GitlabToken, slices.Compact(gitlabGroups), logger) if err != nil { return nil, err } @@ -688,6 +697,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { VcsClient: vcsClient, Locker: projectLocker, LockURLGenerator: router, + Logger: logger, InitStepRunner: &runtime.InitStepRunner{ TerraformExecutor: terraformClient, DefaultTFDistribution: defaultTfDistribution, @@ -853,6 +863,11 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { ExtraArgs: globalCfg.TeamAuthz.Args, ExternalTeamAllowlistRunner: &runtime.DefaultExternalTeamAllowlistRunner{}, } + } else if userConfig.GitlabUser != "" { + teamAllowlistChecker, err = command.NewTeamAllowlistChecker(userConfig.GitlabGroupAllowlist) + if err != nil { + return nil, err + } } else { teamAllowlistChecker, err = command.NewTeamAllowlistChecker(userConfig.GithubTeamAllowlist) if err != nil { diff --git a/server/user_config.go b/server/user_config.go index 3f27aa323d..787c6a49e0 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -69,6 +69,7 @@ type UserConfig struct { GiteaWebhookSecret string `mapstructure:"gitea-webhook-secret"` GiteaPageSize int `mapstructure:"gitea-page-size"` GitlabHostname string `mapstructure:"gitlab-hostname"` + GitlabGroupAllowlist string `mapstructure:"gitlab-group-allowlist"` GitlabToken string `mapstructure:"gitlab-token"` GitlabUser string `mapstructure:"gitlab-user"` GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` From 05659b8cdbc916262f1d7151b8f396eae1a01b6f Mon Sep 17 00:00:00 2001 From: Joshua Spork Date: Sat, 25 Jan 2025 12:03:51 -0700 Subject: [PATCH 46/60] feat: BitBucket Cloud: add support for webhook secrets (#4275) Signed-off-by: Joshua Spork --- cmd/server.go | 9 +-- cmd/server_test.go | 12 ---- runatlantis.io/docs/deployment.md | 10 +-- runatlantis.io/docs/security.md | 15 ---- runatlantis.io/docs/server-configuration.md | 3 +- runatlantis.io/docs/webhook-secrets.md | 5 -- runatlantis.io/guide/testing-locally.md | 6 +- .../controllers/events/events_controller.go | 11 ++- .../vcs/bitbucketcloud/request_validation.go | 72 +++++++++++++++++++ .../bitbucketcloud/request_validation_test.go | 24 +++++++ 10 files changed, 114 insertions(+), 53 deletions(-) create mode 100644 server/events/vcs/bitbucketcloud/request_validation.go create mode 100644 server/events/vcs/bitbucketcloud/request_validation_test.go diff --git a/cmd/server.go b/cmd/server.go index 7682d795f5..9c6ade1001 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -262,7 +262,7 @@ var stringFlags = map[string]stringFlag{ defaultValue: DefaultBitbucketBaseURL, }, BitbucketWebhookSecretFlag: { - description: "Secret used to validate Bitbucket webhooks. Only Bitbucket Server supports webhook secrets." + + description: "Secret used to validate Bitbucket webhooks." + " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from Bitbucket. " + "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + "Should be specified via the ATLANTIS_BITBUCKET_WEBHOOK_SECRET environment variable.", @@ -1034,10 +1034,6 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { return fmt.Errorf("--%s cannot contain ://, should be hostnames only", RepoAllowlistFlag) } - if userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret != "" { - return fmt.Errorf("--%s cannot be specified for Bitbucket Cloud because it is not supported by Bitbucket", BitbucketWebhookSecretFlag) - } - parsed, err := url.Parse(userConfig.BitbucketBaseURL) if err != nil { return fmt.Errorf("error parsing --%s flag value %q: %s", BitbucketWebhookSecretFlag, userConfig.BitbucketBaseURL, err) @@ -1185,9 +1181,6 @@ func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) { if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL != DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret == "" && !s.SilenceOutput { s.Logger.Warn("no Bitbucket webhook secret set. This could allow attackers to spoof requests from Bitbucket") } - if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && !s.SilenceOutput { - s.Logger.Warn("Bitbucket Cloud does not support webhook secrets. This could allow attackers to spoof requests from Bitbucket. Ensure you are allowing only Bitbucket IPs") - } if userConfig.AzureDevopsWebhookUser != "" && userConfig.AzureDevopsWebhookPassword == "" && !s.SilenceOutput { s.Logger.Warn("no Azure DevOps webhook user and password set. This could allow attackers to spoof requests from Azure DevOps.") } diff --git a/cmd/server_test.go b/cmd/server_test.go index 049fb66832..ea73de2905 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -940,18 +940,6 @@ func TestExecute_ADUser(t *testing.T) { Equals(t, "user", passedConfig.AzureDevopsUser) } -// If using bitbucket cloud, webhook secrets are not supported. -func TestExecute_BitbucketCloudWithWebhookSecret(t *testing.T) { - c := setup(map[string]interface{}{ - BitbucketUserFlag: "user", - BitbucketTokenFlag: "token", - RepoAllowlistFlag: "*", - BitbucketWebhookSecretFlag: "my secret", - }, t) - err := c.Execute() - ErrEquals(t, "--bitbucket-webhook-secret cannot be specified for Bitbucket Cloud because it is not supported by Bitbucket", err) -} - // Base URL must have a scheme. func TestExecute_BitbucketServerBaseURLScheme(t *testing.T) { c := setup(map[string]interface{}{ diff --git a/runatlantis.io/docs/deployment.md b/runatlantis.io/docs/deployment.md index dfe5ae27cc..5df948bc64 100644 --- a/runatlantis.io/docs/deployment.md +++ b/runatlantis.io/docs/deployment.md @@ -117,10 +117,6 @@ echo -n "yoursecret" > webhook-secret kubectl create secret generic atlantis-vcs --from-file=token --from-file=webhook-secret ``` -::: tip Note -If you're using Bitbucket Cloud then there is no webhook secret since it's not supported. -::: - Next, edit the manifests below as follows: 1. Replace `` in `image: ghcr.io/runatlantis/atlantis:` with the most recent version from [GitHub: Atlantis latest release](https://github.com/runatlantis/atlantis/releases/latest). @@ -231,6 +227,11 @@ spec: secretKeyRef: name: atlantis-vcs key: token + - name: ATLANTIS_BITBUCKET_WEBHOOK_SECRET + valueFrom: + secretKeyRef: + name: atlantis-vcs + key: webhook-secret ### End Bitbucket Config ### ### Azure DevOps Config ### @@ -742,6 +743,7 @@ atlantis server \ --atlantis-url="$URL" \ --bitbucket-user="$USERNAME" \ --bitbucket-token="$TOKEN" \ +--bitbucket-webhook-secret="$SECRET" \ --repo-allowlist="$REPO_ALLOWLIST" ``` diff --git a/runatlantis.io/docs/security.md b/runatlantis.io/docs/security.md index 0f5d8df4c6..daa2bdfecb 100644 --- a/runatlantis.io/docs/security.md +++ b/runatlantis.io/docs/security.md @@ -22,21 +22,6 @@ Atlantis could be exploited by * Running malicious custom build commands specified in an `atlantis.yaml` file. Atlantis uses the `atlantis.yaml` file from the pull request branch, **not** `main`. * Someone adding `atlantis plan/apply` comments on your valid pull requests causing terraform to run when you don't want it to. -## Bitbucket Cloud (bitbucket.org) - -::: danger -Bitbucket Cloud does not support webhook secrets. This could allow attackers to spoof requests from Bitbucket. Ensure you are allowing only Bitbucket IPs. -::: -Bitbucket Cloud doesn't support webhook secrets. This means that an attacker could -make fake requests to Atlantis that look like they're coming from Bitbucket. - -If you are specifying `--repo-allowlist` then they could only fake requests pertaining -to those repos so the most damage they could do would be to plan/apply on your -own repos. - -To prevent this, allowlist [Bitbucket's IP addresses](https://confluence.atlassian.com/bitbucket/what-are-the-bitbucket-cloud-ip-addresses-i-should-use-to-configure-my-corporate-firewall-343343385.html) - (see Outbound IPv4 addresses). - ## Mitigations ### Don't Use On Public Repos diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 18387db51b..352696cb42 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -330,8 +330,7 @@ and set `--autoplan-modules` to `false`. ATLANTIS_BITBUCKET_WEBHOOK_SECRET="secret" ``` - Secret used to validate Bitbucket webhooks. Only Bitbucket Server supports webhook secrets. - For Bitbucket.org, see [Security](security.md#bitbucket-cloud-bitbucket-org) for mitigations. + Secret used to validate Bitbucket webhooks. ::: warning SECURITY WARNING If not specified, Atlantis won't be able to validate that the incoming webhook call came from Bitbucket. diff --git a/runatlantis.io/docs/webhook-secrets.md b/runatlantis.io/docs/webhook-secrets.md index 4e2ab1a059..e63f9bc9fb 100644 --- a/runatlantis.io/docs/webhook-secrets.md +++ b/runatlantis.io/docs/webhook-secrets.md @@ -20,11 +20,6 @@ Azure DevOps uses Basic authentication for webhooks rather than webhook secrets. An app-wide token is generated during [GitHub App setup](access-credentials.md#github-app). You can recover it by navigating to the [GitHub app settings page](https://github.com/settings/apps) and selecting "Edit" next to your Atlantis app's name. Token appears after clicking "Edit" under the Webhook header. ::: -::: warning -Bitbucket.org **does not** support webhook secrets. -To mitigate, use repo allowlists and IP allowlists. See [Security](security.md#bitbucket-cloud-bitbucket-org) for more information. -::: - ## Generating A Webhook Secret You can use any random string generator to create your Webhook secret. It should be > 24 characters. diff --git a/runatlantis.io/guide/testing-locally.md b/runatlantis.io/guide/testing-locally.md index 4e00c923de..ae7131f6af 100644 --- a/runatlantis.io/guide/testing-locally.md +++ b/runatlantis.io/guide/testing-locally.md @@ -48,11 +48,7 @@ URL="https://{YOUR_HOSTNAME}.ngrok.io" GitHub and GitLab use webhook secrets so clients can verify that the webhooks came from them. -::: warning -Bitbucket Cloud (bitbucket.org) doesn't use webhook secrets so if you're using Bitbucket Cloud you can skip this step. -When you're ready to do a production deploy of Atlantis you should allowlist [Bitbucket IPs](https://confluence.atlassian.com/bitbucket/what-are-the-bitbucket-cloud-ip-addresses-i-should-use-to-configure-my-corporate-firewall-343343385.html) -to ensure the webhooks are coming from them. -::: + Create a random string of any length (you can use [random.org](https://www.random.org/strings/)) and set an environment variable: diff --git a/server/controllers/events/events_controller.go b/server/controllers/events/events_controller.go index ba9dd1d5c0..cdd3d3247f 100644 --- a/server/controllers/events/events_controller.go +++ b/server/controllers/events/events_controller.go @@ -50,7 +50,7 @@ const giteaRequestIDHeader = "X-Gitea-Delivery" const bitbucketEventTypeHeader = "X-Event-Key" const bitbucketCloudRequestIDHeader = "X-Request-UUID" const bitbucketServerRequestIDHeader = "X-Request-ID" -const bitbucketServerSignatureHeader = "X-Hub-Signature" +const bitbucketSignatureHeader = "X-Hub-Signature" // The URL used for Azure DevOps test webhooks const azuredevopsTestURL = "https://fabrikam.visualstudio.com/DefaultCollection/_apis/git/repositories/4bc14d40-c903-45e2-872e-0462c7748079" @@ -223,12 +223,19 @@ func (e *VCSEventsController) handleGithubPost(w http.ResponseWriter, r *http.Re func (e *VCSEventsController) handleBitbucketCloudPost(w http.ResponseWriter, r *http.Request) { eventType := r.Header.Get(bitbucketEventTypeHeader) reqID := r.Header.Get(bitbucketCloudRequestIDHeader) + sig := r.Header.Get(bitbucketSignatureHeader) defer r.Body.Close() // nolint: errcheck body, err := io.ReadAll(r.Body) if err != nil { e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID) return } + if len(e.BitbucketWebhookSecret) > 0 { + if err := bitbucketcloud.ValidateSignature(body, sig, e.BitbucketWebhookSecret); err != nil { + e.respond(w, logging.Warn, http.StatusBadRequest, "%s", errors.Wrap(err, "request did not pass validation").Error()) + return + } + } switch eventType { case bitbucketcloud.PullCreatedHeader, bitbucketcloud.PullUpdatedHeader, bitbucketcloud.PullFulfilledHeader, bitbucketcloud.PullRejectedHeader: e.Logger.Debug("handling as pull request state changed event") @@ -246,7 +253,7 @@ func (e *VCSEventsController) handleBitbucketCloudPost(w http.ResponseWriter, r func (e *VCSEventsController) handleBitbucketServerPost(w http.ResponseWriter, r *http.Request) { eventType := r.Header.Get(bitbucketEventTypeHeader) reqID := r.Header.Get(bitbucketServerRequestIDHeader) - sig := r.Header.Get(bitbucketServerSignatureHeader) + sig := r.Header.Get(bitbucketSignatureHeader) defer r.Body.Close() // nolint: errcheck body, err := io.ReadAll(r.Body) if err != nil { diff --git a/server/events/vcs/bitbucketcloud/request_validation.go b/server/events/vcs/bitbucketcloud/request_validation.go new file mode 100644 index 0000000000..e4df275bb0 --- /dev/null +++ b/server/events/vcs/bitbucketcloud/request_validation.go @@ -0,0 +1,72 @@ +package bitbucketcloud + +import ( + "crypto/hmac" + "crypto/sha1" // nolint: gosec + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "fmt" + "hash" + "strings" + + "github.com/pkg/errors" +) + +// Attribution: This code is taken from https://github.com/google/go-github. + +func ValidateSignature(payload []byte, signature string, secretKey []byte) error { + messageMAC, hashFunc, err := messageMAC(signature) + if err != nil { + return err + } + if !checkMAC(payload, messageMAC, secretKey, hashFunc) { + return errors.New("payload signature check failed") + } + return nil +} + +// genMAC generates the HMAC signature for a message provided the secret key +// and hashFunc. +func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { + mac := hmac.New(hashFunc, key) + // nolint: errcheck + mac.Write(message) + return mac.Sum(nil) +} + +// checkMAC reports whether messageMAC is a valid HMAC tag for message. +func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { + expectedMAC := genMAC(message, key, hashFunc) + return hmac.Equal(messageMAC, expectedMAC) +} + +// messageMAC returns the hex-decoded HMAC tag from the signature and its +// corresponding hash function. +func messageMAC(signature string) ([]byte, func() hash.Hash, error) { + if signature == "" { + return nil, nil, errors.New("missing signature") + } + sigParts := strings.SplitN(signature, "=", 2) + if len(sigParts) != 2 { + return nil, nil, fmt.Errorf("error parsing signature %q", signature) + } + + var hashFunc func() hash.Hash + switch sigParts[0] { + case "sha1": + hashFunc = sha1.New + case "sha256": + hashFunc = sha256.New + case "sha512": + hashFunc = sha512.New + default: + return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) + } + + buf, err := hex.DecodeString(sigParts[1]) + if err != nil { + return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) + } + return buf, hashFunc, nil +} diff --git a/server/events/vcs/bitbucketcloud/request_validation_test.go b/server/events/vcs/bitbucketcloud/request_validation_test.go new file mode 100644 index 0000000000..63969a4b12 --- /dev/null +++ b/server/events/vcs/bitbucketcloud/request_validation_test.go @@ -0,0 +1,24 @@ +package bitbucketcloud_test + +import ( + "testing" + + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" + . "github.com/runatlantis/atlantis/testing" +) + +func TestValidateSignature(t *testing.T) { + body := `{"eventKey":"pr:comment:added","date":"2018-07-24T15:10:05+0200","actor":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"pullRequest":{"id":5,"version":0,"title":"another.tf edited online with Bitbucket","state":"OPEN","open":true,"closed":false,"createdDate":1532365427513,"updatedDate":1532365427513,"fromRef":{"id":"refs/heads/lkysow/anothertf-1532365422773","displayId":"lkysow/anothertf-1532365422773","latestCommit":"b52b8e254e956654dcdb394d0ccba9199f420427","repository":{"slug":"atlantis-example","id":1,"name":"atlantis-example","scmId":"git","state":"AVAILABLE","statusMessage":"Available","forkable":true,"project":{"key":"AT","id":1,"name":"atlantis","public":false,"type":"NORMAL"},"public":false}},"toRef":{"id":"refs/heads/master","displayId":"master","latestCommit":"0a338874369017deba7c22e99e6000932724282f","repository":{"slug":"atlantis-example","id":1,"name":"atlantis-example","scmId":"git","state":"AVAILABLE","statusMessage":"Available","forkable":true,"project":{"key":"AT","id":1,"name":"atlantis","public":false,"type":"NORMAL"},"public":false}},"locked":false,"author":{"user":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"role":"AUTHOR","approved":false,"status":"UNAPPROVED"},"reviewers":[],"participants":[]},"comment":{"properties":{"repositoryId":1},"id":65,"version":0,"text":"comment","author":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"createdDate":1532437805137,"updatedDate":1532437805137,"comments":[],"tasks":[]}}` + secret := "mysecret" + sig := `sha256=ed11f92d1565a4de586727fe8260558277d58009e8957a79eb4749a7009ce083` + err := bitbucketcloud.ValidateSignature([]byte(body), sig, []byte(secret)) + Ok(t, err) +} + +func TestValidateSignature_Invalid(t *testing.T) { + body := `"eventKey":"pr:comment:added","date":"2018-07-24T15:10:05+0200","actor":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"pullRequest":{"id":5,"version":0,"title":"another.tf edited online with Bitbucket","state":"OPEN","open":true,"closed":false,"createdDate":1532365427513,"updatedDate":1532365427513,"fromRef":{"id":"refs/heads/lkysow/anothertf-1532365422773","displayId":"lkysow/anothertf-1532365422773","latestCommit":"b52b8e254e956654dcdb394d0ccba9199f420427","repository":{"slug":"atlantis-example","id":1,"name":"atlantis-example","scmId":"git","state":"AVAILABLE","statusMessage":"Available","forkable":true,"project":{"key":"AT","id":1,"name":"atlantis","public":false,"type":"NORMAL"},"public":false}},"toRef":{"id":"refs/heads/main","displayId":"main","latestCommit":"0a338874369017deba7c22e99e6000932724282f","repository":{"slug":"atlantis-example","id":1,"name":"atlantis-example","scmId":"git","state":"AVAILABLE","statusMessage":"Available","forkable":true,"project":{"key":"AT","id":1,"name":"atlantis","public":false,"type":"NORMAL"},"public":false}},"locked":false,"author":{"user":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"role":"AUTHOR","approved":false,"status":"UNAPPROVED"},"reviewers":[],"participants":[]},"comment":{"properties":{"repositoryId":1},"id":65,"version":0,"text":"comment","author":{"name":"lkysow","emailAddress":"lkysow@gmail.com","id":1,"displayName":"Luke Kysow","active":true,"slug":"lkysow","type":"NORMAL"},"createdDate":1532437805137,"updatedDate":1532437805137,"comments":[],"tasks":[]}}` + secret := "mysecret" + sig := `sha256=ed11f92d1565a4de586727fe8260558277d58009e8957a79eb4749a7009ce083` + err := bitbucketcloud.ValidateSignature([]byte(body), sig, []byte(secret)) + ErrEquals(t, "payload signature check failed", err) +} From caa9544324ff7a03143f47284d00e028bbb932f7 Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Sun, 26 Jan 2025 11:49:58 -0500 Subject: [PATCH 47/60] chore: Go mod tidy (#5274) Signed-off-by: Luke Massa --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 7d8ddce3e0..57ce0646b0 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( code.gitea.io/sdk/gitea v0.19.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/alicebob/miniredis/v2 v2.34.0 + github.com/bmatcuk/doublestar/v4 v4.8.0 github.com/bradleyfalzon/ghinstallation/v2 v2.13.0 github.com/briandowns/spinner v1.23.1 github.com/cactus/go-statsd-client/v5 v5.1.0 @@ -76,7 +77,6 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudflare/circl v1.3.9 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect From 12c9a6cb2f5cb27f0db5bbb583f230ca35369da7 Mon Sep 17 00:00:00 2001 From: Simon Heather <32168619+X-Guardian@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:06:17 +0000 Subject: [PATCH 48/60] chore: Revert "chore: Fix build CI mismatch" (#5277) --- .github/workflows/atlantis-image.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/atlantis-image.yml b/.github/workflows/atlantis-image.yml index ce99be593a..ef18a97b78 100644 --- a/.github/workflows/atlantis-image.yml +++ b/.github/workflows/atlantis-image.yml @@ -252,7 +252,6 @@ jobs: strategy: matrix: image_type: [alpine, debian] - platform: [linux/arm64/v8, linux/amd64, linux/arm/v7] runs-on: ubuntu-24.04 steps: - name: Harden Runner From 98bb8532783b5203fe3ec838e9311fe30e81dd73 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:10:02 +0000 Subject: [PATCH 49/60] chore(deps): update ghcr.io/runatlantis/testing-env:latest docker digest to e6bfa93 in .github/workflows/test.yml (main) (#5276) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d1447c4bd2..80eb760d68 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: if: needs.changes.outputs.should-run-tests == 'true' name: Tests runs-on: ubuntu-24.04 - container: ghcr.io/runatlantis/testing-env:latest@sha256:3d7b17d02ced2cb68ecc9d2ea3d2bef61fe8da52cf1631e4dff4de6503cb7237 + container: ghcr.io/runatlantis/testing-env:latest@sha256:e6bfa93e7b649feb2f209cb67b24245bcd89a3bb27411ee1402206b14b4358c1 steps: - name: Harden Runner uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4 From 6647416168b3278cc8ec0f4f180e2f13107cea93 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 19:43:42 +0000 Subject: [PATCH 50/60] chore(deps): update dependency go to v1.23.5 in go.mod (main) (#5268) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Simon Heather <32168619+X-Guardian@users.noreply.github.com> --- Dockerfile | 2 +- e2e/go.mod | 2 +- go.mod | 2 +- testing/Dockerfile | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index aeccec396f..c24d6a65de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # what distro is the image being built for ARG ALPINE_TAG=3.21.2@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099 ARG DEBIAN_TAG=12.8-slim@sha256:d365f4920711a9074c4bcd178e8f457ee59250426441ab2a5f8106ed8fe948eb -ARG GOLANG_TAG=1.23.4-alpine@sha256:c23339199a08b0e12032856908589a6d41a0dab141b8b3b21f156fc571a3f1d3 +ARG GOLANG_TAG=1.23.5-alpine@sha256:47d337594bd9e667d35514b241569f95fb6d95727c24b19468813d596d5ae596 # renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp ARG DEFAULT_TERRAFORM_VERSION=1.10.4 diff --git a/e2e/go.mod b/e2e/go.mod index f600c3f52d..ba7f5a24a5 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -1,6 +1,6 @@ module github.com/runatlantis/atlantis/e2e -go 1.23.4 +go 1.23.5 require ( github.com/google/go-github/v68 v68.0.0 diff --git a/go.mod b/go.mod index 57ce0646b0..03730a8f60 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/runatlantis/atlantis -go 1.23.4 +go 1.23.5 require ( code.gitea.io/sdk/gitea v0.19.0 diff --git a/testing/Dockerfile b/testing/Dockerfile index a676d3b858..db82b93cda 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23.4@sha256:585103a29aa6d4c98bbb45d2446e1fdf41441698bbdf707d1801f5708e479f04 +FROM golang:1.23.5@sha256:8c10f21bec412f08f73aa7b97ca5ac5f28a39d8a88030ad8a339fd0a781d72b4 RUN apt-get update && apt-get --no-install-recommends -y install unzip \ && apt-get clean \ From 6bf6acf5a5e9635c0c60325a698f1c672e7af446 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 01:39:36 +0000 Subject: [PATCH 51/60] chore(deps): update github/codeql-action digest to f6091c0 in .github/workflows/codeql.yml (main) (#5278) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1c57b817cc..f93452ed70 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -87,7 +87,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ee117c905ab18f32fa0f66c2fe40ecc8013f3e04 # v3 + uses: github/codeql-action/init@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -101,7 +101,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ee117c905ab18f32fa0f66c2fe40ecc8013f3e04 # v3 + uses: github/codeql-action/autobuild@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -114,7 +114,7 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ee117c905ab18f32fa0f66c2fe40ecc8013f3e04 # v3 + uses: github/codeql-action/analyze@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3 with: category: "/language:${{matrix.language}}" From 03ea1a1e1ff76329b16fe879ec22f60241631c48 Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Mon, 27 Jan 2025 22:11:10 -0500 Subject: [PATCH 52/60] chore: Remove some references to pkg/errors (#5270) Signed-off-by: Luke Massa --- server/core/config/parser_validator.go | 18 +++++++----------- server/core/config/parser_validator_test.go | 6 ++++-- server/core/config/raw/autodiscover.go | 2 +- server/core/config/raw/global_cfg.go | 12 +++++++++--- server/core/config/raw/project.go | 7 +++++-- server/core/config/raw/raw.go | 8 ++++++-- server/core/config/raw/raw_test.go | 3 ++- 7 files changed, 34 insertions(+), 22 deletions(-) diff --git a/server/core/config/parser_validator.go b/server/core/config/parser_validator.go index 06e19101e0..0c12b7aeb6 100644 --- a/server/core/config/parser_validator.go +++ b/server/core/config/parser_validator.go @@ -3,6 +3,7 @@ package config import ( "bytes" "encoding/json" + "errors" "fmt" "io" "os" @@ -11,7 +12,7 @@ import ( validation "github.com/go-ozzo/ozzo-validation" shlex "github.com/google/shlex" - "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/core/config/raw" "github.com/runatlantis/atlantis/server/core/config/valid" yaml "gopkg.in/yaml.v3" @@ -29,11 +30,11 @@ func (p *ParserValidator) HasRepoCfg(absRepoDir, repoConfigFile string) (bool, e const invalidExtensionFilename = "atlantis.yml" _, err := os.Stat(p.repoCfgPath(absRepoDir, invalidExtensionFilename)) if err == nil { - return false, errors.Errorf("found %q as config file; rename using the .yaml extension", invalidExtensionFilename) + return false, fmt.Errorf("found %q as config file; rename using the .yaml extension", invalidExtensionFilename) } _, err = os.Stat(p.repoCfgPath(absRepoDir, repoConfigFile)) - if os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { return false, nil } return err == nil, err @@ -48,12 +49,7 @@ func (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.Global configData, err := os.ReadFile(configFile) // nolint: gosec if err != nil { - if !os.IsNotExist(err) { - return valid.RepoCfg{}, errors.Wrapf(err, "unable to read %s file", repoConfigFile) - } - // Don't wrap os.IsNotExist errors because we want our callers to be - // able to detect if it's a NotExist err. - return valid.RepoCfg{}, err + return valid.RepoCfg{}, fmt.Errorf("unable to read %s file: %w", repoConfigFile, err) } return p.ParseRepoCfgData(configData, globalCfg, repoID, branch) } @@ -115,7 +111,7 @@ func (p *ParserValidator) ParseRepoCfgData(repoCfgData []byte, globalCfg valid.G func (p *ParserValidator) ParseGlobalCfg(configFile string, defaultCfg valid.GlobalCfg) (valid.GlobalCfg, error) { configData, err := os.ReadFile(configFile) // nolint: gosec if err != nil { - return valid.GlobalCfg{}, errors.Wrapf(err, "unable to read %s file", configFile) + return valid.GlobalCfg{}, fmt.Errorf("unable to read %s file: %w", configFile, err) } if len(configData) == 0 { return valid.GlobalCfg{}, fmt.Errorf("file %s was empty", configFile) @@ -204,7 +200,7 @@ func (p *ParserValidator) applyLegacyShellParsing(cfg *valid.RepoCfg) error { if s.StepName == "run" { split, err := shlex.Split(s.RunCommand) if err != nil { - return errors.Wrapf(err, "unable to parse %q", s.RunCommand) + return fmt.Errorf("unable to parse %q: %w", s.RunCommand, err) } s.RunCommand = strings.Join(split, " ") } diff --git a/server/core/config/parser_validator_test.go b/server/core/config/parser_validator_test.go index 05299aa725..abe6f98933 100644 --- a/server/core/config/parser_validator_test.go +++ b/server/core/config/parser_validator_test.go @@ -1,7 +1,9 @@ package config_test import ( + "errors" "fmt" + "io/fs" "os" "path/filepath" "regexp" @@ -50,14 +52,14 @@ func TestHasRepoCfg_InvalidFileExtension(t *testing.T) { func TestParseRepoCfg_DirDoesNotExist(t *testing.T) { r := config.ParserValidator{} _, err := r.ParseRepoCfg("/not/exist", globalCfg, "", "") - Assert(t, os.IsNotExist(err), "exp not exist err") + Assert(t, errors.Is(err, fs.ErrNotExist), "exp not exist err") } func TestParseRepoCfg_FileDoesNotExist(t *testing.T) { tmpDir := t.TempDir() r := config.ParserValidator{} _, err := r.ParseRepoCfg(tmpDir, globalCfg, "", "") - Assert(t, os.IsNotExist(err), "exp not exist err") + Assert(t, errors.Is(err, fs.ErrNotExist), "exp not exist err") } func TestParseRepoCfg_BadPermissions(t *testing.T) { diff --git a/server/core/config/raw/autodiscover.go b/server/core/config/raw/autodiscover.go index 1be3c0e166..fdf18fbec4 100644 --- a/server/core/config/raw/autodiscover.go +++ b/server/core/config/raw/autodiscover.go @@ -1,12 +1,12 @@ package raw import ( + "errors" "fmt" "strings" "github.com/bmatcuk/doublestar/v4" validation "github.com/go-ozzo/ozzo-validation" - "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/config/valid" ) diff --git a/server/core/config/raw/global_cfg.go b/server/core/config/raw/global_cfg.go index bdc1f6697d..127e22e3cf 100644 --- a/server/core/config/raw/global_cfg.go +++ b/server/core/config/raw/global_cfg.go @@ -1,12 +1,12 @@ package raw import ( + "errors" "fmt" "regexp" "strings" validation "github.com/go-ozzo/ozzo-validation" - "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/utils" ) @@ -184,7 +184,10 @@ func (r Repo) Validate() error { return nil } _, err := regexp.Compile(id[1 : len(id)-1]) - return errors.Wrapf(err, "parsing: %s", id) + if err != nil { + return fmt.Errorf("parsing: %s: %w", id, err) + } + return nil } branchValid := func(value interface{}) error { @@ -197,7 +200,10 @@ func (r Repo) Validate() error { } withoutSlashes := branch[1 : len(branch)-1] _, err := regexp.Compile(withoutSlashes) - return errors.Wrapf(err, "parsing: %s", branch) + if err != nil { + return fmt.Errorf("parsing: %s: %w", branch, err) + } + return nil } repoConfigFileValid := func(value interface{}) error { diff --git a/server/core/config/raw/project.go b/server/core/config/raw/project.go index 5b389c8605..74d12539bf 100644 --- a/server/core/config/raw/project.go +++ b/server/core/config/raw/project.go @@ -1,6 +1,7 @@ package raw import ( + "errors" "fmt" "net/url" "path/filepath" @@ -9,7 +10,6 @@ import ( validation "github.com/go-ozzo/ozzo-validation" version "github.com/hashicorp/go-version" - "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/config/valid" ) @@ -75,7 +75,10 @@ func (p Project) Validate() error { } withoutSlashes := branch[1 : len(branch)-1] _, err := regexp.Compile(withoutSlashes) - return errors.Wrapf(err, "parsing: %s", branch) + if err != nil { + return fmt.Errorf("parsing: %s: %w", branch, err) + } + return nil } DependsOn := func(value interface{}) error { diff --git a/server/core/config/raw/raw.go b/server/core/config/raw/raw.go index d10625255c..97dd0615a3 100644 --- a/server/core/config/raw/raw.go +++ b/server/core/config/raw/raw.go @@ -4,8 +4,9 @@ package raw import ( + "fmt" + version "github.com/hashicorp/go-version" - "github.com/pkg/errors" ) // VersionValidator helper function to validate binary version. @@ -16,5 +17,8 @@ func VersionValidator(value interface{}) error { return nil } _, err := version.NewVersion(*strPtr) - return errors.Wrapf(err, "version %q could not be parsed", *strPtr) + if err != nil { + return fmt.Errorf("version %q could not be parsed: %w", *strPtr, err) + } + return nil } diff --git a/server/core/config/raw/raw_test.go b/server/core/config/raw/raw_test.go index fd1886cdbe..0d8fdca9a3 100644 --- a/server/core/config/raw/raw_test.go +++ b/server/core/config/raw/raw_test.go @@ -4,7 +4,8 @@ import ( "io" "strings" - "github.com/pkg/errors" + "errors" + "gopkg.in/yaml.v3" ) From 107967ad3beb12a167a60c6c8c8e160ab9a000cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:38:48 +0000 Subject: [PATCH 53/60] chore(deps): update actions/setup-node digest to 1d0ff46 in .github/workflows/website.yml (main) (#5279) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/renovate-config.yml | 2 +- .github/workflows/website.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/renovate-config.yml b/.github/workflows/renovate-config.yml index 899efb81f2..6122970549 100644 --- a/.github/workflows/renovate-config.yml +++ b/.github/workflows/renovate-config.yml @@ -25,5 +25,5 @@ jobs: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4 - run: npx --package renovate -c 'renovate-config-validator' diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 7e70378595..ee65114b8b 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -65,7 +65,7 @@ jobs: globs: 'runatlantis.io/**/*.md' - name: setup npm - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4 with: node-version: '20' cache: 'npm' From 73d16735a5ffe25a094cc862d3da71f119c643ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 01:51:46 +0000 Subject: [PATCH 54/60] chore(deps): update github/codeql-action digest to 17a820b in .github/workflows/codeql.yml (main) (#5283) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f93452ed70..38b6a8f9d7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -87,7 +87,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3 + uses: github/codeql-action/init@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -101,7 +101,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3 + uses: github/codeql-action/autobuild@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -114,7 +114,7 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3 + uses: github/codeql-action/analyze@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3 with: category: "/language:${{matrix.language}}" From 472c691617ce77b1ec6080bd40e26fe467761d05 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 00:52:29 +0000 Subject: [PATCH 55/60] chore(deps): update github/codeql-action digest to dd74661 in .github/workflows/codeql.yml (main) (#5284) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 38b6a8f9d7..15ec766647 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -87,7 +87,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3 + uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -101,7 +101,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3 + uses: github/codeql-action/autobuild@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -114,7 +114,7 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3 + uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3 with: category: "/language:${{matrix.language}}" From 7a79c55ebf063fc2eb657d1e5f7c4ca5fcff66c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 00:45:27 +0000 Subject: [PATCH 56/60] chore(deps): update dependency hashicorp/terraform to v1.10.5 in testdrive/utils.go (main) (#5285) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Dockerfile | 2 +- testdrive/utils.go | 2 +- testing/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index c24d6a65de..3be802bd29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ ARG DEBIAN_TAG=12.8-slim@sha256:d365f4920711a9074c4bcd178e8f457ee59250426441ab2a ARG GOLANG_TAG=1.23.5-alpine@sha256:47d337594bd9e667d35514b241569f95fb6d95727c24b19468813d596d5ae596 # renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp -ARG DEFAULT_TERRAFORM_VERSION=1.10.4 +ARG DEFAULT_TERRAFORM_VERSION=1.10.5 # renovate: datasource=github-releases depName=opentofu/opentofu versioning=hashicorp ARG DEFAULT_OPENTOFU_VERSION=1.8.8 # renovate: datasource=github-releases depName=open-policy-agent/conftest diff --git a/testdrive/utils.go b/testdrive/utils.go index 50f3bf2555..04d8561cee 100644 --- a/testdrive/utils.go +++ b/testdrive/utils.go @@ -35,7 +35,7 @@ import ( ) const hashicorpReleasesURL = "https://releases.hashicorp.com" -const terraformVersion = "1.10.4" // renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp +const terraformVersion = "1.10.5" // renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp const ngrokDownloadURL = "https://bin.equinox.io/c/4VmDzA7iaHb" const ngrokAPIURL = "localhost:41414" // We hope this isn't used. const atlantisPort = 4141 diff --git a/testing/Dockerfile b/testing/Dockerfile index db82b93cda..c997796c20 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -6,7 +6,7 @@ RUN apt-get update && apt-get --no-install-recommends -y install unzip \ # Install Terraform # renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp -ENV TERRAFORM_VERSION=1.10.4 +ENV TERRAFORM_VERSION=1.10.5 RUN case $(uname -m) in x86_64|amd64) ARCH="amd64" ;; aarch64|arm64|armv7l) ARCH="arm64" ;; esac && \ wget -nv -O terraform.zip https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${ARCH}.zip && \ mkdir -p /usr/local/bin/tf/versions/${TERRAFORM_VERSION} && \ From be06063668c49a1c90127cbe8a4b5a68b3576e74 Mon Sep 17 00:00:00 2001 From: Simon Heather <32168619+X-Guardian@users.noreply.github.com> Date: Sat, 1 Feb 2025 19:12:54 +0000 Subject: [PATCH 57/60] fix: Pre Workflow Hook VCS Combined Status Check Set to Pending Twice (#5242) Signed-off-by: X-Guardian <32168619+X-Guardian@users.noreply.github.com> --- server/controllers/api_controller.go | 11 +++++++ server/controllers/api_controller_test.go | 5 +++ .../events/events_controller_e2e_test.go | 1 + server/events/apply_command_runner.go | 4 --- server/events/command_runner.go | 18 +++++++++++ server/events/command_runner_test.go | 31 +++++++------------ server/events/plan_command_runner.go | 9 ------ .../pre_workflow_hooks_command_runner.go | 12 ------- 8 files changed, 47 insertions(+), 44 deletions(-) diff --git a/server/controllers/api_controller.go b/server/controllers/api_controller.go index ff85371469..29037ec9a0 100644 --- a/server/controllers/api_controller.go +++ b/server/controllers/api_controller.go @@ -33,6 +33,7 @@ type APIController struct { RepoAllowlistChecker *events.RepoAllowlistChecker Scope tally.Scope VCSClient vcs.Client + CommitStatusUpdater events.CommitStatusUpdater } type APIRequest struct { @@ -150,6 +151,11 @@ func (a *APIController) apiPlan(request *APIRequest, ctx *command.Context) (*com return nil, err } + // Update the combined plan commit status to pending + if err := a.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil { + ctx.Log.Warn("unable to update plan commit status: %s", err) + } + var projectResults []command.ProjectResult for i, cmd := range cmds { err = a.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cc[i]) @@ -173,6 +179,11 @@ func (a *APIController) apiApply(request *APIRequest, ctx *command.Context) (*co return nil, err } + // Update the combined apply commit status to pending + if err := a.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Apply); err != nil { + ctx.Log.Warn("unable to update apply commit status: %s", err) + } + var projectResults []command.ProjectResult for i, cmd := range cmds { err = a.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cc[i]) diff --git a/server/controllers/api_controller_test.go b/server/controllers/api_controller_test.go index 3b3aa520aa..778c41bee2 100644 --- a/server/controllers/api_controller_test.go +++ b/server/controllers/api_controller_test.go @@ -94,6 +94,10 @@ func setup(t *testing.T) (controllers.APIController, *MockProjectCommandBuilder, When(postWorkflowHooksCommandRunner.RunPostHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(nil) + commitStatusUpdater := NewMockCommitStatusUpdater() + + When(commitStatusUpdater.UpdateCombined(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Any[models.CommitStatus](), Any[command.Name]())).ThenReturn(nil) + ac := controllers.APIController{ APISecret: []byte(atlantisToken), Locker: locker, @@ -107,6 +111,7 @@ func setup(t *testing.T) (controllers.APIController, *MockProjectCommandBuilder, PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, VCSClient: vcsClient, RepoAllowlistChecker: repoAllowlistChecker, + CommitStatusUpdater: commitStatusUpdater, } return ac, projectCommandBuilder, projectCommandRunner } diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 4588b04127..d06b237b9f 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1647,6 +1647,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, PullStatusFetcher: backend, DisableAutoplan: opt.disableAutoplan, + CommitStatusUpdater: commitStatusUpdater, } repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") diff --git a/server/events/apply_command_runner.go b/server/events/apply_command_runner.go index 6c69032910..c68110e518 100644 --- a/server/events/apply_command_runner.go +++ b/server/events/apply_command_runner.go @@ -94,10 +94,6 @@ func (a *ApplyCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) { return } - if err = a.commitStatusUpdater.UpdateCombined(ctx.Log, baseRepo, pull, models.PendingCommitStatus, cmd.CommandName()); err != nil { - ctx.Log.Warn("unable to update commit status: %s", err) - } - // Get the mergeable status before we set any build statuses of our own. // We do this here because when we set a "Pending" status, if users have // required the Atlantis status checks to pass, then we've now changed diff --git a/server/events/command_runner.go b/server/events/command_runner.go index daa66356d2..30a82105a9 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -202,6 +202,12 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo cmd := &CommentCommand{ Name: command.Autoplan, } + + // Update the combined plan commit status to pending + if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil { + ctx.Log.Warn("unable to update plan commit status: %s", err) + } + err = c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd) if err != nil { @@ -354,6 +360,18 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead return } + // Update the combined plan or apply commit status to pending + switch cmd.Name { + case command.Plan: + if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil { + ctx.Log.Warn("unable to update plan commit status: %s", err) + } + case command.Apply: + if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Apply); err != nil { + ctx.Log.Warn("unable to update apply commit status: %s", err) + } + } + err = c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd) if err != nil { diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index b3b88c40d0..4e32994824 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -253,6 +253,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, PullStatusFetcher: testConfig.backend, + CommitStatusUpdater: commitUpdater, } return vcsClient @@ -440,15 +441,8 @@ func TestRunCommentCommandApply_NoProjects_SilenceEnabled(t *testing.T) { ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Apply}) vcsClient.VerifyWasCalled(Never()).CreateComment( Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()) - commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount( - Any[logging.SimpleLogging](), - Any[models.Repo](), - Any[models.PullRequest](), - Eq[models.CommitStatus](models.SuccessCommitStatus), - Eq[command.Name](command.Apply), - Eq(0), - Eq(0), - ) + commitUpdater.VerifyWasCalledOnce().UpdateCombined( + Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Eq(models.PendingCommitStatus), Eq(command.Apply)) } func TestRunCommentCommandApprovePolicy_NoProjects_SilenceEnabled(t *testing.T) { @@ -463,15 +457,6 @@ func TestRunCommentCommandApprovePolicy_NoProjects_SilenceEnabled(t *testing.T) ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.ApprovePolicies}) vcsClient.VerifyWasCalled(Never()).CreateComment( Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()) - commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount( - Any[logging.SimpleLogging](), - Any[models.Repo](), - Any[models.PullRequest](), - Eq[models.CommitStatus](models.SuccessCommitStatus), - Eq[command.Name](command.PolicyCheck), - Eq(0), - Eq(0), - ) } func TestRunCommentCommandUnlock_NoProjects_SilenceEnabled(t *testing.T) { @@ -485,6 +470,8 @@ func TestRunCommentCommandUnlock_NoProjects_SilenceEnabled(t *testing.T) { ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Unlock}) vcsClient.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()) + commitUpdater.VerifyWasCalled(Never()).UpdateCombined( + Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Eq(models.PendingCommitStatus), Any[command.Name]()) } func TestRunCommentCommandImport_NoProjects_SilenceEnabled(t *testing.T) { @@ -535,7 +522,7 @@ func TestRunCommentCommand_DisableAutoplan(t *testing.T) { CommandName: command.Plan, }, }, nil) - + When(commitUpdater.UpdateCombinedCount(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Any[models.CommitStatus](), Any[command.Name](), Any[int](), Any[int]())).ThenReturn(nil) ch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, modelPull, testdata.User) projectCommandBuilder.VerifyWasCalled(Never()).BuildAutoplanCommands(Any[*command.Context]()) } @@ -831,6 +818,10 @@ func TestRunAutoplanCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_Fal ch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User) pendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp) lockingLocker.VerifyWasCalledOnce().UnlockByPull(testdata.Pull.BaseRepo.FullName, testdata.Pull.Num) + commitUpdater.VerifyWasCalledOnce().UpdateCombined(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), + Eq(models.PendingCommitStatus), Eq(command.Plan)) + commitUpdater.VerifyWasCalled(Never()).UpdateCombined(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), + Eq(models.FailedCommitStatus), Any[command.Name]()) } func TestRunAutoplanCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_True(t *testing.T) { @@ -853,6 +844,8 @@ func TestRunAutoplanCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_Tru ch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User) pendingPlanFinder.VerifyWasCalled(Never()).DeletePlans(Any[string]()) lockingLocker.VerifyWasCalled(Never()).UnlockByPull(Any[string](), Any[int]()) + commitUpdater.VerifyWasCalledOnce().UpdateCombined(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), + Eq(models.PendingCommitStatus), Eq(command.Plan)) } func TestRunCommentCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_False(t *testing.T) { diff --git a/server/events/plan_command_runner.go b/server/events/plan_command_runner.go index c1cc3e81e0..a9652b0cab 100644 --- a/server/events/plan_command_runner.go +++ b/server/events/plan_command_runner.go @@ -114,11 +114,6 @@ func (p *PlanCommandRunner) runAutoplan(ctx *command.Context) { return } - // At this point we are sure Atlantis has work to do, so set commit status to pending - if err := p.commitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil { - ctx.Log.Warn("unable to update plan commit status: %s", err) - } - // discard previous plans that might not be relevant anymore ctx.Log.Debug("deleting previous plans and locks") p.deletePlans(ctx) @@ -188,10 +183,6 @@ func (p *PlanCommandRunner) run(ctx *command.Context, cmd *CommentCommand) { } } - if err = p.commitStatusUpdater.UpdateCombined(ctx.Log, baseRepo, pull, models.PendingCommitStatus, command.Plan); err != nil { - ctx.Log.Warn("unable to update commit status: %s", err) - } - projectCmds, err := p.prjCmdBuilder.BuildPlanCommands(ctx, cmd) if err != nil { if statusErr := p.commitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Plan); statusErr != nil { diff --git a/server/events/pre_workflow_hooks_command_runner.go b/server/events/pre_workflow_hooks_command_runner.go index 7d152c7328..be175d0b7d 100644 --- a/server/events/pre_workflow_hooks_command_runner.go +++ b/server/events/pre_workflow_hooks_command_runner.go @@ -69,18 +69,6 @@ func (w *DefaultPreWorkflowHooksCommandRunner) RunPreHooks(ctx *command.Context, escapedArgs = escapeArgs(cmd.Flags) } - // Update the plan or apply commit status to pending whilst the pre workflow hook is running - switch cmd.Name { - case command.Plan: - if err := w.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil { - ctx.Log.Warn("unable to update plan commit status: %s", err) - } - case command.Apply: - if err := w.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Apply); err != nil { - ctx.Log.Warn("unable to update apply commit status: %s", err) - } - } - err = w.runHooks( models.WorkflowHookCommandContext{ BaseRepo: ctx.Pull.BaseRepo, From 8157a8db24f81c5204fd0e53728d12b802b46938 Mon Sep 17 00:00:00 2001 From: Simon Heather <32168619+X-Guardian@users.noreply.github.com> Date: Sat, 1 Feb 2025 19:17:35 +0000 Subject: [PATCH 58/60] fix: Workspace Error when include-git-untracked-files is true (#5288) Signed-off-by: X-Guardian <32168619+X-Guardian@users.noreply.github.com> --- server/events/project_command_builder.go | 31 +++++++------ server/events/project_command_builder_test.go | 44 ++++++++++++------- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 2e42cfc8a4..f626d4b605 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -455,23 +455,17 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex return nil, err } - if p.IncludeGitUntrackedFiles { - ctx.Log.Debug(("'include-git-untracked-files' option is set, getting untracked files")) - untrackedFiles, err := p.WorkingDir.GetGitUntrackedFiles(ctx.Log, ctx.HeadRepo, ctx.Pull, DefaultWorkspace) + ctx.Log.Debug("%d files were modified in this pull request. Modified files: %v", len(modifiedFiles), modifiedFiles) + + // If we're not including git untracked files, we can skip the clone if there are no modified files. + if !p.IncludeGitUntrackedFiles { + shouldSkipClone, err := p.shouldSkipClone(ctx, modifiedFiles) if err != nil { return nil, err } - modifiedFiles = append(modifiedFiles, untrackedFiles...) - } - - ctx.Log.Debug("%d files were modified in this pull request. Modified files: %v", len(modifiedFiles), modifiedFiles) - - shouldSkipClone, err := p.shouldSkipClone(ctx, modifiedFiles) - if err != nil { - return nil, err - } - if shouldSkipClone { - return []command.ProjectContext{}, nil + if shouldSkipClone { + return []command.ProjectContext{}, nil + } } // Need to lock the workspace we're about to clone to. @@ -490,6 +484,15 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex return nil, err } + if p.IncludeGitUntrackedFiles { + ctx.Log.Debug(("'include-git-untracked-files' option is set, getting untracked files")) + untrackedFiles, err := p.WorkingDir.GetGitUntrackedFiles(ctx.Log, ctx.HeadRepo, ctx.Pull, DefaultWorkspace) + if err != nil { + return nil, err + } + modifiedFiles = append(modifiedFiles, untrackedFiles...) + } + // Parse config file if it exists. repoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID()) hasRepoCfg, err := p.ParserValidator.HasRepoCfg(repoDir, repoCfgFile) diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index bb16148893..e74c563ed7 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -45,7 +45,7 @@ var defaultUserConfig = struct { AutoplanFileList: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", RestrictFileList: false, SilenceNoProjects: false, - IncludeGitUntrackedFiles: true, + IncludeGitUntrackedFiles: false, AutoDiscoverMode: "auto", } @@ -1695,27 +1695,40 @@ projects: // Test that we don't clone the repo if there were no changes based on the atlantis.yaml file. func TestDefaultProjectCommandBuilder_SkipCloneNoChanges(t *testing.T) { cases := []struct { - AtlantisYAML string - ExpectedCtxs int - ExpectedClones InvocationCountMatcher - ModifiedFiles []string + AtlantisYAML string + ExpectedCtxs int + ExpectedClones InvocationCountMatcher + ModifiedFiles []string + IncludeGitUntrackedFiles bool }{ { AtlantisYAML: ` version: 3 projects: - dir: dir1`, - ExpectedCtxs: 0, - ExpectedClones: Never(), - ModifiedFiles: []string{"dir2/main.tf"}, + ExpectedCtxs: 0, + ExpectedClones: Never(), + ModifiedFiles: []string{"dir2/main.tf"}, + IncludeGitUntrackedFiles: false, + }, + { + AtlantisYAML: ` +version: 3 +projects: +- dir: dir1`, + ExpectedCtxs: 0, + ExpectedClones: Once(), + ModifiedFiles: []string{"dir2/main.tf"}, + IncludeGitUntrackedFiles: true, }, { AtlantisYAML: ` version: 3 parallel_plan: true`, - ExpectedCtxs: 0, - ExpectedClones: Once(), - ModifiedFiles: []string{"README.md"}, + ExpectedCtxs: 0, + ExpectedClones: Once(), + ModifiedFiles: []string{"README.md"}, + IncludeGitUntrackedFiles: false, }, { AtlantisYAML: ` @@ -1724,9 +1737,10 @@ autodiscover: mode: enabled projects: - dir: dir1`, - ExpectedCtxs: 0, - ExpectedClones: Once(), - ModifiedFiles: []string{"dir2/main.tf"}, + ExpectedCtxs: 0, + ExpectedClones: Once(), + ModifiedFiles: []string{"dir2/main.tf"}, + IncludeGitUntrackedFiles: false, }, } @@ -1770,7 +1784,7 @@ projects: userConfig.AutoplanFileList, userConfig.RestrictFileList, userConfig.SilenceNoProjects, - userConfig.IncludeGitUntrackedFiles, + c.IncludeGitUntrackedFiles, userConfig.AutoDiscoverMode, scope, terraformClient, From a470d428636c3eaf2928db5b229369ee993f0f5c Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Sat, 1 Feb 2025 17:11:26 -0500 Subject: [PATCH 59/60] chore: Add doc for how to rotate gitlab token (#5290) Signed-off-by: Luke Massa --- e2e/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 e2e/README.md diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000000..fcaec9ad74 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,15 @@ +# End to end tests + +Tests run against actual repos in various VCS providers + +## Configuration + +### Gitlab + +User: https://gitlab.com/atlantis-tests +Email: maintainers@runatlantis.io + +To rotate token: +1. Login to account +2. Select avatar -> Edit Profile -> Access tokens -> Add new token +3. Create a new token, and upload it to Github Action as environment secret `ATLANTIS_GITLAB_TOKEN`. From 8018a2f3a12e1716a19c4c64c135f61e814b27bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 2 Feb 2025 01:27:38 +0000 Subject: [PATCH 60/60] chore(deps): update dependency raviqqe/muffet to v2.10.7 in .github/workflows/website.yml (main) (#5292) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/website.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index ee65114b8b..f67514bd8e 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -73,7 +73,7 @@ jobs: - name: run http-server env: # renovate: datasource=github-releases depName=raviqqe/muffet - MUFFET_VERSION: 2.10.6 + MUFFET_VERSION: 2.10.7 run: | # install raviqqe/muffet to check for broken links. curl -Ls https://github.com/raviqqe/muffet/releases/download/v${MUFFET_VERSION}/muffet_linux_amd64.tar.gz | tar -xz