From 66e7440a06f0dd9a737b812dad033f0ef9ef5a9c Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Fri, 29 Aug 2025 16:12:25 +0100 Subject: [PATCH 01/12] feat(Go): creating a new client and plan option in environemnt --- go.mod | 22 ++++++----- go.sum | 49 +++++++++++++----------- pkg/commands/environment.go | 61 ++++++++++++++++++++++++++++++ pkg/environment/apply.go | 14 +++---- pkg/environment/apply_test.go | 2 +- pkg/environment/divergence.go | 2 +- pkg/environment/divergence_test.go | 2 +- pkg/environment/namespace.go | 2 +- pkg/github/client.go | 38 ++++++++++++++++++- pkg/github/client_iface.go | 2 +- pkg/github/client_test.go | 2 +- pkg/mocks/github/GithubIface.go | 2 +- 12 files changed, 150 insertions(+), 48 deletions(-) diff --git a/go.mod b/go.mod index a29317c2..159d8697 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/ministryofjustice/cloud-platform-cli -go 1.23 +go 1.25 require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/agext/levenshtein v1.2.3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/gookit/color v1.5.4 github.com/hashicorp/hcl/v2 v2.19.1 github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -19,10 +19,10 @@ require ( github.com/spf13/viper v1.18.2 github.com/zclconf/go-cty v1.14.1 golang.org/x/crypto v0.19.0 // indirect - golang.org/x/mod v0.14.0 + golang.org/x/mod v0.25.0 golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.26.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 ) @@ -33,20 +33,22 @@ require ( github.com/dlclark/regexp2 v1.10.0 github.com/google/go-cmdtest v0.4.0 github.com/google/go-github v17.0.0+incompatible + github.com/google/go-github/v68 v68.0.0 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hc-install v0.6.2 github.com/hashicorp/terraform-exec v0.20.0 github.com/hashicorp/terraform-json v0.21.0 github.com/jedib0t/go-pretty/v6 v6.5.3 + github.com/jferrl/go-githubauth v1.3.0 github.com/kelseyhightower/envconfig v1.4.0 - github.com/migueleliasweb/go-github-mock v0.0.22 + github.com/migueleliasweb/go-github-mock v1.4.0 github.com/ministryofjustice/cloud-platform-environments v1.2.1-0.20230712165212-61f4971d3baa github.com/ministryofjustice/cloud-platform-go-library v0.0.0-20220803122921-1ca1153b1730 github.com/rs/zerolog v1.31.0 github.com/shurcooL/githubv4 v0.0.0-20220922232305-70b4d362a8cb github.com/slack-go/slack v0.12.5 github.com/stretchr/testify v1.8.4 - golang.org/x/oauth2 v0.16.0 + golang.org/x/oauth2 v0.30.0 k8s.io/api v0.26.3 k8s.io/apimachinery v0.26.3 k8s.io/client-go v0.26.3 @@ -74,16 +76,17 @@ require ( github.com/go-openapi/swag v0.21.1 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-github/v56 v56.0.0 // indirect + github.com/google/go-github/v73 v73.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/renameio v0.1.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.4.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect @@ -133,8 +136,7 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/term v0.17.0 // indirect - golang.org/x/time v0.5.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + golang.org/x/time v0.12.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e5d96dc5..94dffd97 100644 --- a/go.sum +++ b/go.sum @@ -135,7 +135,6 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -162,6 +161,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -212,13 +213,14 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/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.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= -github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= +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-github/v73 v73.0.0 h1:aR+Utnh+Y4mMkS+2qLQwcQ/cF9mOTpdwnzlaw//rG24= +github.com/google/go-github/v73 v73.0.0/go.mod h1:fa6w8+/V+edSU0muqdhCVY7Beh1M8F1IlQPZIANKIYw= 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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -244,8 +246,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= @@ -281,6 +283,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.5.3 h1:GIXn6Er/anHTkVUoufs7ptEvxdD6KIhR7Axa2wYCPF0= github.com/jedib0t/go-pretty/v6 v6.5.3/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg= +github.com/jferrl/go-githubauth v1.3.0 h1:eW5An6RhhuX/IkqfOYEzBh66RwFrrkwRMzaGbEssRrY= +github.com/jferrl/go-githubauth v1.3.0/go.mod h1:B+IZ+R0heTfIGxhm7wC7b52B3ADh/AfD0bKY5vktBV4= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -335,8 +339,8 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM= github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/migueleliasweb/go-github-mock v0.0.22 h1:iUvUKmYd7sFq/wrb9TrbEdvc30NaYxLZNtz7Uv2D+AQ= -github.com/migueleliasweb/go-github-mock v0.0.22/go.mod h1:UVvZ3S9IdTTRqThr1lgagVaua3Jl1bmY4E+C/Vybbn4= +github.com/migueleliasweb/go-github-mock v1.4.0 h1:pQ6K8r348m2q79A8Khb0PbEeNQV7t3h1xgECV+jNpXk= +github.com/migueleliasweb/go-github-mock v1.4.0/go.mod h1:/DUmhXkxrgVlDOVBqGoUXkV4w0ms5n1jDQHotYm135o= github.com/ministryofjustice/cloud-platform-environments v1.2.1-0.20230712165212-61f4971d3baa h1:ttFqd4Ks4CizU51l7aKp3XuEI87t20VoRnn6O8ARj9Q= github.com/ministryofjustice/cloud-platform-environments v1.2.1-0.20230712165212-61f4971d3baa/go.mod h1:i3PdaTg3t4U25VUT4qkUn5MNVGhyP1sSnSnTp6UV7OE= github.com/ministryofjustice/cloud-platform-go-library v0.0.0-20220803122921-1ca1153b1730 h1:4u9OcC1NTIARZjKBdop1qY3hxC2DGaHEE+i43BFh+aI= @@ -544,8 +548,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -594,8 +598,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -608,6 +612,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -679,14 +685,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -732,8 +737,8 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -760,8 +765,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index f5c25744..67a26dc8 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -39,6 +39,7 @@ func addEnvironmentCmd(topLevel *cobra.Command) { environmentDivergenceCmd, environmentEcrCmd, environmentPlanCmd, + environmentAppPlanCmd, // For testing only this should replace environmentPlanCmd environmentPrototypeCmd, environmentRdsCmd, environmentS3Cmd, @@ -107,6 +108,16 @@ func addEnvironmentCmd(topLevel *cobra.Command) { environmentPlanCmd.Flags().StringVar(&optFlags.ClusterCtx, "cluster", "", "cluster context from kubeconfig file") environmentPlanCmd.Flags().StringVar(&optFlags.ClusterDir, "clusterdir", "", "folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name") environmentPlanCmd.PersistentFlags().BoolVar(&optFlags.RedactedEnv, "redact", true, "Redact the terraform output before printing") + + // For testing only this should replace environmentPlanCmd + environmentAppPlanCmd.Flags().IntVar(&optFlags.PRNumber, "pr-number", 0, "Pull request ID or number to which you want to perform the plan") + environmentAppPlanCmd.Flags().StringVarP(&optFlags.Namespace, "namespace", "n", "", "Namespace which you want to perform the plan") + + // Re-use the environmental variable TF_VAR_github_token to call Github Client which is needed to perform terraform operations on each namespace + environmentAppPlanCmd.Flags().StringVar(&optFlags.GithubToken, "github-token", os.Getenv("TF_VAR_github_token"), "Personal access Token from Github ") + environmentAppPlanCmd.Flags().StringVar(&optFlags.KubecfgPath, "kubecfg", filepath.Join(homedir.HomeDir(), ".kube", "config"), "path to kubeconfig file") + environmentAppPlanCmd.Flags().StringVar(&optFlags.ClusterCtx, "cluster", "", "cluster context from kubeconfig file") + environmentAppPlanCmd.Flags().StringVar(&optFlags.ClusterDir, "clusterdir", "", "folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name") } var environmentCmd = &cobra.Command{ @@ -183,6 +194,56 @@ var environmentPlanCmd = &cobra.Command{ }, } +// environmentAppPlanCmd represents the appplan command (for testing only) +var environmentAppPlanCmd = &cobra.Command{ + Use: "appplan", + Short: `Perform a terraform plan and kubectl apply --dry-run=client for a given namespace using either -namespace flag or the + the namespace in the given PR Id/Number`, + Long: ` + Perform a kubectl apply --dry-run=client and a terraform plan for a given namespace using either -namespace flag or the + the namespace in the given PR Id/Number + + Along with the mandatory input flag, the below environments variables needs to be set + TF_VAR_cluster_name - e.g. "cp-1902-02" to get the vpc details for some modules like rds, es + TF_VAR_cluster_state_bucket - State where the cluster state is stored + TF_VAR_cluster_state_key - folder name/state key inside the state bucket where cluster state is stored + TF_VAR_github_owner - Github owner: ministryofjustice + TF_VAR_github_cloud_platform_concourse_bot_app_id: cloud platform concourse bot app id + TF_VAR_github_cloud_platform_concourse_bot_installation_id: cloud platform concourse bot installation id + TF_VAR_github_cloud_platform_concourse_bot_pem_file: cloud platform concourse bot pem file + TF_VAR_kubernetes_cluster - Full name of the Cluster e.g. XXXXXX.gr7.eu-west2.eks.amazonaws.com + PINGDOM_API_TOKEN - API Token to access pingdom + PIPELINE_TERRAFORM_STATE_LOCK_TABLE - DynamoDB table where the state lock is stored + PIPELINE_STATE_BUCKET - State bucket where the environments state is stored e.g cloud-platform-terraform-state + PIPELINE_STATE_KEY_PREFIX - State key/ folder where the environments terraform state is stored e.g cloud-platform-environments + PIPELINE_STATE_REGION - State region of the bucket e.g. eu-west-1 + PIPELINE_CLUSTER - Cluster name/folder inside namespaces/ in cloud-platform-environments + PIPELINE_CLUSTER_STATE - Cluster name/folder inside the state bucket where the environments terraform state is stored. for "live" the state is stored under "live-1.cloud-platform.service..." + `, + Example: heredoc.Doc(` + $ cloud-platform environment plan + `), + PreRun: upgradeIfNotLatest, + Run: func(cmd *cobra.Command, args []string) { + contextLogger := log.WithFields(log.Fields{"subcommand": "plan"}) + + ghConfig := &github.GithubClientConfig{ + Repository: "cloud-platform-environments", + Owner: "ministryofjustice", + } + + applier := &environment.Apply{ + Options: &optFlags, + GithubClient: github.NewGihubAppClient(ghConfig, optFlags.AppID, optFlags.InstallID, optFlags.PemFile), + } + + err := applier.Plan() + if err != nil { + contextLogger.Fatal(err) + } + }, +} + var environmentApplyCmd = &cobra.Command{ Use: "apply", Short: `Perform a terraform apply and kubectl apply for a given namespace`, diff --git a/pkg/environment/apply.go b/pkg/environment/apply.go index 35ba3fff..2949191c 100644 --- a/pkg/environment/apply.go +++ b/pkg/environment/apply.go @@ -15,13 +15,13 @@ import ( // Options are used to configure plan/apply sessions. // These options are normally passed via flags in a command line. type Options struct { - Namespace, KubecfgPath, ClusterCtx, ClusterDir, GithubToken string - PRNumber int - BuildUrl string - AllNamespaces bool - EnableApplySkip, RedactedEnv, SkipProdDestroy bool - BatchApplyIndex, BatchApplySize int - OnlySkipFileChanged, IsApplyPipeline bool + Namespace, KubecfgPath, ClusterCtx, ClusterDir, GithubToken, AppID, InstallID, PemFile string + PRNumber int + BuildUrl string + AllNamespaces bool + EnableApplySkip, RedactedEnv, SkipProdDestroy bool + BatchApplyIndex, BatchApplySize int + OnlySkipFileChanged, IsApplyPipeline bool } // RequiredEnvVars is used to store values such as TF_VAR_ , github and pingdom tokens diff --git a/pkg/environment/apply_test.go b/pkg/environment/apply_test.go index 3620cc9e..c73ae63f 100644 --- a/pkg/environment/apply_test.go +++ b/pkg/environment/apply_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/google/go-github/github" + "github.com/google/go-github/v68/github" "github.com/ministryofjustice/cloud-platform-cli/pkg/environment/mocks" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" diff --git a/pkg/environment/divergence.go b/pkg/environment/divergence.go index 6b18124d..231b2bd6 100644 --- a/pkg/environment/divergence.go +++ b/pkg/environment/divergence.go @@ -5,7 +5,7 @@ import ( "fmt" mapset "github.com/deckarep/golang-set/v2" - "github.com/google/go-github/github" + "github.com/google/go-github/v68/github" "golang.org/x/oauth2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" diff --git a/pkg/environment/divergence_test.go b/pkg/environment/divergence_test.go index 0a2c5823..09ab46bf 100644 --- a/pkg/environment/divergence_test.go +++ b/pkg/environment/divergence_test.go @@ -7,7 +7,7 @@ import ( "testing" mapset "github.com/deckarep/golang-set/v2" - "github.com/google/go-github/github" + "github.com/google/go-github/v68/github" "github.com/migueleliasweb/go-github-mock/src/mock" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/environment/namespace.go b/pkg/environment/namespace.go index 229322de..956414d3 100644 --- a/pkg/environment/namespace.go +++ b/pkg/environment/namespace.go @@ -5,7 +5,7 @@ import ( "os" "strings" - gogithub "github.com/google/go-github/github" + gogithub "github.com/google/go-github/v68/github" "github.com/ministryofjustice/cloud-platform-cli/pkg/util" "gopkg.in/yaml.v2" v1 "k8s.io/api/core/v1" diff --git a/pkg/github/client.go b/pkg/github/client.go index aad62562..e9d5914d 100644 --- a/pkg/github/client.go +++ b/pkg/github/client.go @@ -3,9 +3,11 @@ package github import ( "context" "fmt" + "strconv" "strings" - "github.com/google/go-github/github" + "github.com/google/go-github/v68/github" + "github.com/jferrl/go-githubauth" "github.com/ministryofjustice/cloud-platform-cli/pkg/util" "github.com/shurcooL/githubv4" "golang.org/x/oauth2" @@ -62,6 +64,40 @@ func NewGithubClient(config *GithubClientConfig, token string) *GithubClient { } } +func NewGihubAppClient(config *GithubClientConfig, key, appid, installid string) *GithubClient { + privateKey := []byte(key) + + appIDInt, err := strconv.ParseInt(appid, 10, 64) + if err != nil { + return nil + } + + installIDInt, err := strconv.ParseInt(installid, 10, 64) + if err != nil { + return nil + } + + appTokenSource, err := githubauth.NewApplicationTokenSource(appIDInt, privateKey) + if err != nil { + return nil + } + + installationTokenSource := githubauth.NewInstallationTokenSource(installIDInt, appTokenSource) + + oauthHttpClient := oauth2.NewClient(context.Background(), installationTokenSource) + + v3 := github.NewClient(oauthHttpClient) + v4 := githubv4.NewClient(oauthHttpClient) + + return &GithubClient{ + V3: v3, + V4: v4, + Repository: config.Repository, + Owner: config.Owner, + PullRequests: v3.PullRequests, + } +} + // ListMergedPRs takes date and number of PRs count as input, search the github using Graphql api for // list of PRs (title,url) between the first and last date provided func (gh *GithubClient) ListMergedPRs(date util.Date, count int) ([]Nodes, error) { diff --git a/pkg/github/client_iface.go b/pkg/github/client_iface.go index c953a309..cad66ec5 100644 --- a/pkg/github/client_iface.go +++ b/pkg/github/client_iface.go @@ -1,7 +1,7 @@ package github import ( - "github.com/google/go-github/github" + "github.com/google/go-github/v68/github" "github.com/ministryofjustice/cloud-platform-cli/pkg/util" ) diff --git a/pkg/github/client_test.go b/pkg/github/client_test.go index 489ed90b..7e2848e8 100644 --- a/pkg/github/client_test.go +++ b/pkg/github/client_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/google/go-github/github" + "github.com/google/go-github/v68/github" "github.com/stretchr/testify/assert" ) diff --git a/pkg/mocks/github/GithubIface.go b/pkg/mocks/github/GithubIface.go index 4c11029c..492182d8 100644 --- a/pkg/mocks/github/GithubIface.go +++ b/pkg/mocks/github/GithubIface.go @@ -3,7 +3,7 @@ package mocks import ( - github "github.com/google/go-github/github" + github "github.com/google/go-github/v68/github" mock "github.com/stretchr/testify/mock" pkggithub "github.com/ministryofjustice/cloud-platform-cli/pkg/github" From 3b25603b0e73132f6fead8c8ed8763a825f94550 Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Mon, 1 Sep 2025 13:37:22 +0100 Subject: [PATCH 02/12] feat(Go): adding cobra Command variables --- go.mod | 2 +- pkg/commands/environment.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 159d8697..dbe6da41 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,6 @@ require ( github.com/deckarep/golang-set/v2 v2.6.0 github.com/dlclark/regexp2 v1.10.0 github.com/google/go-cmdtest v0.4.0 - github.com/google/go-github v17.0.0+incompatible github.com/google/go-github/v68 v68.0.0 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hc-install v0.6.2 @@ -80,6 +79,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic v0.6.9 // indirect + github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-github/v73 v73.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index 67a26dc8..3a64574d 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -114,7 +114,9 @@ func addEnvironmentCmd(topLevel *cobra.Command) { environmentAppPlanCmd.Flags().StringVarP(&optFlags.Namespace, "namespace", "n", "", "Namespace which you want to perform the plan") // Re-use the environmental variable TF_VAR_github_token to call Github Client which is needed to perform terraform operations on each namespace - environmentAppPlanCmd.Flags().StringVar(&optFlags.GithubToken, "github-token", os.Getenv("TF_VAR_github_token"), "Personal access Token from Github ") + environmentAppPlanCmd.Flags().StringVar(&optFlags.AppID, "github-appid", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_app_id"), "App ID ") + environmentAppPlanCmd.Flags().StringVar(&optFlags.InstallID, "github-installation-id", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id"), "Installation ID ") + environmentAppPlanCmd.Flags().StringVar(&optFlags.PemFile, "github-pem-file", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file"), "PEM file ") environmentAppPlanCmd.Flags().StringVar(&optFlags.KubecfgPath, "kubecfg", filepath.Join(homedir.HomeDir(), ".kube", "config"), "path to kubeconfig file") environmentAppPlanCmd.Flags().StringVar(&optFlags.ClusterCtx, "cluster", "", "cluster context from kubeconfig file") environmentAppPlanCmd.Flags().StringVar(&optFlags.ClusterDir, "clusterdir", "", "folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name") From 4f33a35c43ae961412e0de07abb7baed523aa463 Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Tue, 2 Sep 2025 12:32:30 +0100 Subject: [PATCH 03/12] feat(Go): Added debug --- pkg/commands/environment.go | 16 +++++++++++++--- pkg/environment/apply.go | 4 ++++ pkg/environment/init.go | 4 ++++ pkg/github/client.go | 3 +++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index 3a64574d..9c3653c0 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -112,8 +112,6 @@ func addEnvironmentCmd(topLevel *cobra.Command) { // For testing only this should replace environmentPlanCmd environmentAppPlanCmd.Flags().IntVar(&optFlags.PRNumber, "pr-number", 0, "Pull request ID or number to which you want to perform the plan") environmentAppPlanCmd.Flags().StringVarP(&optFlags.Namespace, "namespace", "n", "", "Namespace which you want to perform the plan") - - // Re-use the environmental variable TF_VAR_github_token to call Github Client which is needed to perform terraform operations on each namespace environmentAppPlanCmd.Flags().StringVar(&optFlags.AppID, "github-appid", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_app_id"), "App ID ") environmentAppPlanCmd.Flags().StringVar(&optFlags.InstallID, "github-installation-id", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id"), "Installation ID ") environmentAppPlanCmd.Flags().StringVar(&optFlags.PemFile, "github-pem-file", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file"), "PEM file ") @@ -234,9 +232,21 @@ var environmentAppPlanCmd = &cobra.Command{ Owner: "ministryofjustice", } + contextLogger.Infof("AppID: %q, InstallID: %q, PemFile (first 20 chars): %q", optFlags.AppID, optFlags.InstallID, func() string { + if len(optFlags.PemFile) > 20 { + return optFlags.PemFile[:20] + "..." + } + return optFlags.PemFile + }()) + + ghClient := github.NewGihubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID) + if ghClient == nil { + contextLogger.Fatal("Failed to create Github App client: check AppID, InstallID, and PemFile are set and valid.") + } + applier := &environment.Apply{ Options: &optFlags, - GithubClient: github.NewGihubAppClient(ghConfig, optFlags.AppID, optFlags.InstallID, optFlags.PemFile), + GithubClient: ghClient, } err := applier.Plan() diff --git a/pkg/environment/apply.go b/pkg/environment/apply.go index 2949191c..05d2cb0d 100644 --- a/pkg/environment/apply.go +++ b/pkg/environment/apply.go @@ -35,6 +35,10 @@ type RequiredEnvVars struct { SlackBotToken string `required:"false" envconfig:"SLACK_BOT_TOKEN"` SlackWebhookUrl string `required:"false" envconfig:"SLACK_WEBHOOK_URL"` pingdomapitoken string `required:"true" envconfig:"PINGDOM_API_TOKEN"` + + cloud_platform_concourse_bot_app_id string `required:"true" envconfig:"TF_VAR_github_cloud_platform_concourse_bot_app_id"` + cloud_platform_concourse_bot_installation_id string `required:"true" envconfig:"TF_VAR_github_cloud_platform_concourse_bot_installation_id"` + cloud_platform_concourse_bot_pem_file string `required:"true" envconfig:"TF_VAR_github_cloud_platform_concourse_bot_pem_file"` } // Apply is used to store objects in a Apply/Plan session diff --git a/pkg/environment/init.go b/pkg/environment/init.go index d4777529..6d01f573 100644 --- a/pkg/environment/init.go +++ b/pkg/environment/init.go @@ -22,6 +22,10 @@ func (a *Apply) Initialize() { a.RequiredEnvVars.SlackWebhookUrl = reqEnvVars.SlackWebhookUrl a.RequiredEnvVars.pingdomapitoken = reqEnvVars.pingdomapitoken + a.RequiredEnvVars.cloud_platform_concourse_bot_app_id = reqEnvVars.cloud_platform_concourse_bot_app_id + a.RequiredEnvVars.cloud_platform_concourse_bot_installation_id = reqEnvVars.cloud_platform_concourse_bot_installation_id + a.RequiredEnvVars.cloud_platform_concourse_bot_pem_file = reqEnvVars.cloud_platform_concourse_bot_pem_file + // Set KUBE_CONFIG_PATH to the path of the kubeconfig file // This is needed for terraform to be able to connect to the cluster when a different kubecfg is passed if err := os.Setenv("KUBE_CONFIG_PATH", a.Options.KubecfgPath); err != nil { diff --git a/pkg/github/client.go b/pkg/github/client.go index e9d5914d..25fec054 100644 --- a/pkg/github/client.go +++ b/pkg/github/client.go @@ -69,16 +69,19 @@ func NewGihubAppClient(config *GithubClientConfig, key, appid, installid string) appIDInt, err := strconv.ParseInt(appid, 10, 64) if err != nil { + fmt.Printf("[NewGihubAppClient] Failed to parse appid '%s': %v\n", appid, err) return nil } installIDInt, err := strconv.ParseInt(installid, 10, 64) if err != nil { + fmt.Printf("[NewGihubAppClient] Failed to parse installid '%s': %v\n", installid, err) return nil } appTokenSource, err := githubauth.NewApplicationTokenSource(appIDInt, privateKey) if err != nil { + fmt.Printf("[NewGihubAppClient] Failed to create ApplicationTokenSource: %v\n", err) return nil } From 0897540b1198e8ec4608073b99f8bfe4ddcaa078 Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Wed, 3 Sep 2025 16:47:36 +0100 Subject: [PATCH 04/12] feat(Go): add flag for type and pass them to env plan to toggle app or token auth --- local-setup/go.mod | 3 + local-setup/localsetup.go | 164 ++++++++++++++++++++++++++++++++++++ pkg/commands/environment.go | 101 ++++++---------------- pkg/environment/apply.go | 22 ++--- 4 files changed, 203 insertions(+), 87 deletions(-) create mode 100644 local-setup/go.mod create mode 100644 local-setup/localsetup.go diff --git a/local-setup/go.mod b/local-setup/go.mod new file mode 100644 index 00000000..bdd52ccc --- /dev/null +++ b/local-setup/go.mod @@ -0,0 +1,3 @@ +module github.com/ministryofjustice/cloud-platform-cli/local-setup + +go 1.25.0 diff --git a/local-setup/localsetup.go b/local-setup/localsetup.go new file mode 100644 index 00000000..7273f9c4 --- /dev/null +++ b/local-setup/localsetup.go @@ -0,0 +1,164 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/exec" + "syscall" +) + +var ( + AWSProfile = "moj-cp" + clusterName string + kubeConfig string + clusterArray []string + home, _ = os.UserHomeDir() + colourCyan = "\033[36m" + colourReset = "\033[0m" + colourYellow = "\033[33m" + colourRed = "\033[31m" +) + +func setAWSEnv(ns string) { + fmt.Println(string(colourYellow), "\nSetting AWS Configuration", string(colourReset)) + + os.Setenv("AWS_PROFILE", ns) + + fmt.Println(string(colourCyan), "AWS_PROFILE:", string(colourReset), os.Getenv("AWS_PROFILE")) + +} + +func setKubeEnv(clusterName string) bool { + var returnOuput bool + fmt.Println(string(colourYellow), "\nSetting Kube Configuration", string(colourReset)) + + kubeConfig = home + "/.kube/" + clusterName + "/config" + + // these are the three kube variables expected by kubectl + os.Setenv("KUBECONFIG", kubeConfig) + // this is needed for kubectl provider + os.Setenv("KUBE_CONFIG", os.Getenv("KUBECONFIG")) + os.Setenv("KUBE_CONFIG_PATH", os.Getenv("KUBECONFIG")) + + fmt.Println(string(colourCyan), "KUBECONFIG:", string(colourReset), os.Getenv("KUBECONFIG")) + fmt.Println(string(colourCyan), "KUBE_CONFIG:", string(colourReset), os.Getenv("KUBE_CONFIG")) + fmt.Println(string(colourCyan), "KUBE_CONFIG_PATH:", string(colourReset), os.Getenv("KUBE_CONFIG_PATH")) + + return returnOuput +} + +func setTFWksp(namespace string) error { + // tf workspace to the cluster name + fmt.Println(string(colourYellow), "\nUpdating Terraform Workspace") + err := os.Setenv("TF_WORKSPACE", namespace) + if err != nil { + return err + } else { + fmt.Println(string(colourCyan), "TF_WORKSPACE:", string(colourReset), os.Getenv("TF_WORKSPACE")) + } + return nil +} + +func setNamespaceTF(namespace string) { + os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_app_id", `aws ssm get-parameter --name "/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_app_id" --with-decryption --profile moj-cp --query "Parameter.Value" --output text`) + os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id", `aws ssm get-parameter --name "/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_installation_id" --with-decryption --profile moj-cp --query "Parameter.Value" --output text`) + os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file", `aws ssm get-parameter --name "/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_pem_file" --with-decryption --profile moj-cp --query "Parameter.Value" --output text`) + os.Setenv("PIPELINE_STATE_BUCKET", "cloud-platform-terraform-state") + os.Setenv("PIPELINE_STATE_KEY_PREFIX", "cloud-platform-environments") + os.Setenv("PIPELINE_TERRAFORM_STATE_LOCK_TABLE", "cloud-platform-environments-terraform-lock") + os.Setenv("PIPELINE_STATE_REGION", "eu-west-1") + os.Setenv("PIPELINE_CLUSTER", "arn:aws:eks:eu-west-2:754256621582:cluster/live") + os.Setenv("PIPELINE_CLUSTER_DIR", "live.cloud-platform.service.justice.gov.uk") + os.Setenv("PIPELINE_CLUSTER_STATE", "live-1.cloud-platform.service.justice.gov.uk") + os.Setenv("PIPELINE_STATE_KEY_PREFIX", fmt.Sprintf("cloud-platform-environments/live-1.cloud-platform.service.justice.gov.uk/%v/terraform.tfstate", namespace)) + os.Setenv("TF_VAR_cluster_name", "live-1") + os.Setenv("TF_VAR_vpc_name", "live-1") + os.Setenv("TF_VAR_eks_cluster_name", "live") + os.Setenv("TF_VAR_cluster_state_bucket", "cloud-platform-terraform-state") + os.Setenv("TF_VAR_github_owner", "ministryofjustice") + os.Setenv("TF_VAR_kubernetes_cluster", "DF366E49809688A3B16EEC29707D8C09.gr7.eu-west-2.eks.amazonaws.com") + os.Setenv("PINGDOM_API_TOKEN", `aws ssm get-parameter --name "/cloud-platform/infrastructure/components/pingdom_api_token" --with-decryption --profile moj-cp --query "Parameter.Value" --output text`) +} + +func setTerm(clusterName string) { + fmt.Println(string(colourYellow), "Updating Kube Context") + cmd := exec.Command("aws", "eks", "update-kubeconfig", "--name", clusterName, "--region", "eu-west-2") + cmd.Run() + + log.Println(string(colourYellow), "\nSetting Terminal Context", string(colourReset)) + cmd = exec.Command("kubectl", "config", "current-context") + out, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(string(colourRed), "(setTerm)CombinedOutput: ", err, string(colourReset)) + } + + kconfig := string(out) + fmt.Println(string(colourYellow), "\nSetting Terminal Environment") + os.Setenv("KUBE_PS1", kconfig+": ") + errsys := syscall.Exec(os.Getenv("SHELL"), []string{os.Getenv("SHELL")}, os.Environ()) + if errsys != nil { + log.Fatal(string(colourRed), "(setTerm)errsys: ", errsys, string(colourReset)) + } +} + +func contains(arg string) bool { + for _, cluster := range clusterArray { + if cluster == arg { + return true + } + } + return false +} + +func namespaceEnv(namespace *string) { + var arg string + setAWSEnv(AWSProfile) + clusterArray = []string{"live", "manager", "live-2"} + fmt.Println("Please enter cluster name:") + fmt.Scanln(&arg) + // retry loop with max three attempts + for i := 0; i < 3; i++ { + if contains(arg) { + break + } else { + fmt.Println("Please select a cluster from the list:") + fmt.Scanln(&arg) + } + } + if !contains(arg) { + log.Fatalf(string(colourRed), "Cluster name is incorrect", string(colourReset)) + } + + clusterName = arg + + b := setKubeEnv(clusterName) + if !b { + log.Fatalf(string(colourRed), "Error setting kube config", string(colourReset)) + } + + err := setTFWksp(*namespace) + if err != nil { + log.Fatalf(string(colourRed), "Error setting Terraform workspace: %v", err, string(colourReset)) + } + + setNamespaceTF(*namespace) + + setTerm(clusterName) +} + +func main() { + h, err := os.UserHomeDir() + if err != nil { + log.Fatalf("User Home: %v", err) + } + arg := flag.String("namespace", "", "namespace name") + namespace := arg + home = h + + flag.Parse() + + namespaceEnv(namespace) + +} diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index 9c3653c0..6bc99494 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -39,7 +39,6 @@ func addEnvironmentCmd(topLevel *cobra.Command) { environmentDivergenceCmd, environmentEcrCmd, environmentPlanCmd, - environmentAppPlanCmd, // For testing only this should replace environmentPlanCmd environmentPrototypeCmd, environmentRdsCmd, environmentS3Cmd, @@ -101,23 +100,17 @@ func addEnvironmentCmd(topLevel *cobra.Command) { // e.g. if this is the Pull request to perform the apply: https://github.com/ministryofjustice/cloud-platform-environments/pull/8370, the pr ID is 8370. environmentPlanCmd.Flags().IntVar(&optFlags.PRNumber, "pr-number", 0, "Pull request ID or number to which you want to perform the plan") environmentPlanCmd.Flags().StringVarP(&optFlags.Namespace, "namespace", "n", "", "Namespace which you want to perform the plan") - + environmentPlanCmd.Flags().StringVarP(&optFlags.Type, "type", "t", "", "Type of plan to perform: app or token") // Re-use the environmental variable TF_VAR_github_token to call Github Client which is needed to perform terraform operations on each namespace environmentPlanCmd.Flags().StringVar(&optFlags.GithubToken, "github-token", os.Getenv("TF_VAR_github_token"), "Personal access Token from Github ") environmentPlanCmd.Flags().StringVar(&optFlags.KubecfgPath, "kubecfg", filepath.Join(homedir.HomeDir(), ".kube", "config"), "path to kubeconfig file") environmentPlanCmd.Flags().StringVar(&optFlags.ClusterCtx, "cluster", "", "cluster context from kubeconfig file") environmentPlanCmd.Flags().StringVar(&optFlags.ClusterDir, "clusterdir", "", "folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name") environmentPlanCmd.PersistentFlags().BoolVar(&optFlags.RedactedEnv, "redact", true, "Redact the terraform output before printing") + environmentPlanCmd.Flags().StringVar(&optFlags.AppID, "github-appid", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_app_id"), "App ID ") + environmentPlanCmd.Flags().StringVar(&optFlags.InstallID, "github-installation-id", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id"), "Installation ID ") + environmentPlanCmd.Flags().StringVar(&optFlags.PemFile, "github-pem-file", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file"), "PEM file ") - // For testing only this should replace environmentPlanCmd - environmentAppPlanCmd.Flags().IntVar(&optFlags.PRNumber, "pr-number", 0, "Pull request ID or number to which you want to perform the plan") - environmentAppPlanCmd.Flags().StringVarP(&optFlags.Namespace, "namespace", "n", "", "Namespace which you want to perform the plan") - environmentAppPlanCmd.Flags().StringVar(&optFlags.AppID, "github-appid", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_app_id"), "App ID ") - environmentAppPlanCmd.Flags().StringVar(&optFlags.InstallID, "github-installation-id", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id"), "Installation ID ") - environmentAppPlanCmd.Flags().StringVar(&optFlags.PemFile, "github-pem-file", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file"), "PEM file ") - environmentAppPlanCmd.Flags().StringVar(&optFlags.KubecfgPath, "kubecfg", filepath.Join(homedir.HomeDir(), ".kube", "config"), "path to kubeconfig file") - environmentAppPlanCmd.Flags().StringVar(&optFlags.ClusterCtx, "cluster", "", "cluster context from kubeconfig file") - environmentAppPlanCmd.Flags().StringVar(&optFlags.ClusterDir, "clusterdir", "", "folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name") } var environmentCmd = &cobra.Command{ @@ -175,56 +168,7 @@ var environmentPlanCmd = &cobra.Command{ `), PreRun: upgradeIfNotLatest, Run: func(cmd *cobra.Command, args []string) { - contextLogger := log.WithFields(log.Fields{"subcommand": "plan"}) - - ghConfig := &github.GithubClientConfig{ - Repository: "cloud-platform-environments", - Owner: "ministryofjustice", - } - applier := &environment.Apply{ - Options: &optFlags, - GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), - } - - err := applier.Plan() - if err != nil { - contextLogger.Fatal(err) - } - }, -} - -// environmentAppPlanCmd represents the appplan command (for testing only) -var environmentAppPlanCmd = &cobra.Command{ - Use: "appplan", - Short: `Perform a terraform plan and kubectl apply --dry-run=client for a given namespace using either -namespace flag or the - the namespace in the given PR Id/Number`, - Long: ` - Perform a kubectl apply --dry-run=client and a terraform plan for a given namespace using either -namespace flag or the - the namespace in the given PR Id/Number - - Along with the mandatory input flag, the below environments variables needs to be set - TF_VAR_cluster_name - e.g. "cp-1902-02" to get the vpc details for some modules like rds, es - TF_VAR_cluster_state_bucket - State where the cluster state is stored - TF_VAR_cluster_state_key - folder name/state key inside the state bucket where cluster state is stored - TF_VAR_github_owner - Github owner: ministryofjustice - TF_VAR_github_cloud_platform_concourse_bot_app_id: cloud platform concourse bot app id - TF_VAR_github_cloud_platform_concourse_bot_installation_id: cloud platform concourse bot installation id - TF_VAR_github_cloud_platform_concourse_bot_pem_file: cloud platform concourse bot pem file - TF_VAR_kubernetes_cluster - Full name of the Cluster e.g. XXXXXX.gr7.eu-west2.eks.amazonaws.com - PINGDOM_API_TOKEN - API Token to access pingdom - PIPELINE_TERRAFORM_STATE_LOCK_TABLE - DynamoDB table where the state lock is stored - PIPELINE_STATE_BUCKET - State bucket where the environments state is stored e.g cloud-platform-terraform-state - PIPELINE_STATE_KEY_PREFIX - State key/ folder where the environments terraform state is stored e.g cloud-platform-environments - PIPELINE_STATE_REGION - State region of the bucket e.g. eu-west-1 - PIPELINE_CLUSTER - Cluster name/folder inside namespaces/ in cloud-platform-environments - PIPELINE_CLUSTER_STATE - Cluster name/folder inside the state bucket where the environments terraform state is stored. for "live" the state is stored under "live-1.cloud-platform.service..." - `, - Example: heredoc.Doc(` - $ cloud-platform environment plan - `), - PreRun: upgradeIfNotLatest, - Run: func(cmd *cobra.Command, args []string) { contextLogger := log.WithFields(log.Fields{"subcommand": "plan"}) ghConfig := &github.GithubClientConfig{ @@ -232,26 +176,31 @@ var environmentAppPlanCmd = &cobra.Command{ Owner: "ministryofjustice", } - contextLogger.Infof("AppID: %q, InstallID: %q, PemFile (first 20 chars): %q", optFlags.AppID, optFlags.InstallID, func() string { - if len(optFlags.PemFile) > 20 { - return optFlags.PemFile[:20] + "..." + if optFlags.Type == "app" { + ghClient := github.NewGihubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID) + if ghClient == nil { + contextLogger.Fatal("Failed to create Github App client: check AppID, InstallID, and PemFile are set and valid.") } - return optFlags.PemFile - }()) - ghClient := github.NewGihubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID) - if ghClient == nil { - contextLogger.Fatal("Failed to create Github App client: check AppID, InstallID, and PemFile are set and valid.") - } + applier := &environment.Apply{ + Options: &optFlags, + GithubClient: ghClient, + } - applier := &environment.Apply{ - Options: &optFlags, - GithubClient: ghClient, - } + err := applier.Plan() + if err != nil { + contextLogger.Fatal(err) + } + } else { + applier := &environment.Apply{ + Options: &optFlags, + GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), + } - err := applier.Plan() - if err != nil { - contextLogger.Fatal(err) + err := applier.Plan() + if err != nil { + contextLogger.Fatal(err) + } } }, } diff --git a/pkg/environment/apply.go b/pkg/environment/apply.go index 05d2cb0d..9fcbd5ac 100644 --- a/pkg/environment/apply.go +++ b/pkg/environment/apply.go @@ -15,13 +15,13 @@ import ( // Options are used to configure plan/apply sessions. // These options are normally passed via flags in a command line. type Options struct { - Namespace, KubecfgPath, ClusterCtx, ClusterDir, GithubToken, AppID, InstallID, PemFile string - PRNumber int - BuildUrl string - AllNamespaces bool - EnableApplySkip, RedactedEnv, SkipProdDestroy bool - BatchApplyIndex, BatchApplySize int - OnlySkipFileChanged, IsApplyPipeline bool + Namespace, KubecfgPath, ClusterCtx, ClusterDir, GithubToken, AppID, InstallID, PemFile, Type string + PRNumber int + BuildUrl string + AllNamespaces bool + EnableApplySkip, RedactedEnv, SkipProdDestroy bool + BatchApplyIndex, BatchApplySize int + OnlySkipFileChanged, IsApplyPipeline bool } // RequiredEnvVars is used to store values such as TF_VAR_ , github and pingdom tokens @@ -31,14 +31,14 @@ type RequiredEnvVars struct { clusterstatebucket string `required:"true" envconfig:"TF_VAR_cluster_state_bucket"` kubernetescluster string `required:"true" envconfig:"TF_VAR_kubernetes_cluster"` githubowner string `required:"true" envconfig:"TF_VAR_github_owner"` - githubtoken string `required:"true" envconfig:"TF_VAR_github_token"` + githubtoken string `required:"false" envconfig:"TF_VAR_github_token"` SlackBotToken string `required:"false" envconfig:"SLACK_BOT_TOKEN"` SlackWebhookUrl string `required:"false" envconfig:"SLACK_WEBHOOK_URL"` pingdomapitoken string `required:"true" envconfig:"PINGDOM_API_TOKEN"` - cloud_platform_concourse_bot_app_id string `required:"true" envconfig:"TF_VAR_github_cloud_platform_concourse_bot_app_id"` - cloud_platform_concourse_bot_installation_id string `required:"true" envconfig:"TF_VAR_github_cloud_platform_concourse_bot_installation_id"` - cloud_platform_concourse_bot_pem_file string `required:"true" envconfig:"TF_VAR_github_cloud_platform_concourse_bot_pem_file"` + cloud_platform_concourse_bot_app_id string `required:"false" envconfig:"TF_VAR_github_cloud_platform_concourse_bot_app_id"` + cloud_platform_concourse_bot_installation_id string `required:"false" envconfig:"TF_VAR_github_cloud_platform_concourse_bot_installation_id"` + cloud_platform_concourse_bot_pem_file string `required:"false" envconfig:"TF_VAR_github_cloud_platform_concourse_bot_pem_file"` } // Apply is used to store objects in a Apply/Plan session From 58354e3d68dfbc453bb494d50716a3569f2a9bc9 Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Thu, 4 Sep 2025 14:41:14 +0100 Subject: [PATCH 05/12] feat(Go): add a way of checking auth type for the plan --- pkg/commands/environment.go | 12 +++- pkg/environment/apply.go | 14 ++--- pkg/github/auth_type.go | 108 ++++++++++++++++++++++++++++++++++++ pkg/github/client.go | 2 +- 4 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 pkg/github/auth_type.go diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index 6bc99494..d500d487 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -1,6 +1,7 @@ package commands import ( + "context" "errors" "os" "path/filepath" @@ -100,7 +101,6 @@ func addEnvironmentCmd(topLevel *cobra.Command) { // e.g. if this is the Pull request to perform the apply: https://github.com/ministryofjustice/cloud-platform-environments/pull/8370, the pr ID is 8370. environmentPlanCmd.Flags().IntVar(&optFlags.PRNumber, "pr-number", 0, "Pull request ID or number to which you want to perform the plan") environmentPlanCmd.Flags().StringVarP(&optFlags.Namespace, "namespace", "n", "", "Namespace which you want to perform the plan") - environmentPlanCmd.Flags().StringVarP(&optFlags.Type, "type", "t", "", "Type of plan to perform: app or token") // Re-use the environmental variable TF_VAR_github_token to call Github Client which is needed to perform terraform operations on each namespace environmentPlanCmd.Flags().StringVar(&optFlags.GithubToken, "github-token", os.Getenv("TF_VAR_github_token"), "Personal access Token from Github ") environmentPlanCmd.Flags().StringVar(&optFlags.KubecfgPath, "kubecfg", filepath.Join(homedir.HomeDir(), ".kube", "config"), "path to kubeconfig file") @@ -176,8 +176,14 @@ var environmentPlanCmd = &cobra.Command{ Owner: "ministryofjustice", } - if optFlags.Type == "app" { - ghClient := github.NewGihubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID) + // get authtype this is only needed for migration purposes once users are all using github app this can be removed + authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).SearchAuthTypeDefaultInPR(context.Background(), optFlags.PRNumber) + if err != nil { + contextLogger.Fatalf("Failed to get auth_type from PR: %v", err) + } + + if authType == "app" { + ghClient := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID) if ghClient == nil { contextLogger.Fatal("Failed to create Github App client: check AppID, InstallID, and PemFile are set and valid.") } diff --git a/pkg/environment/apply.go b/pkg/environment/apply.go index 9fcbd5ac..af0a7650 100644 --- a/pkg/environment/apply.go +++ b/pkg/environment/apply.go @@ -15,13 +15,13 @@ import ( // Options are used to configure plan/apply sessions. // These options are normally passed via flags in a command line. type Options struct { - Namespace, KubecfgPath, ClusterCtx, ClusterDir, GithubToken, AppID, InstallID, PemFile, Type string - PRNumber int - BuildUrl string - AllNamespaces bool - EnableApplySkip, RedactedEnv, SkipProdDestroy bool - BatchApplyIndex, BatchApplySize int - OnlySkipFileChanged, IsApplyPipeline bool + Namespace, KubecfgPath, ClusterCtx, ClusterDir, GithubToken, AppID, InstallID, PemFile string + PRNumber int + BuildUrl string + AllNamespaces bool + EnableApplySkip, RedactedEnv, SkipProdDestroy bool + BatchApplyIndex, BatchApplySize int + OnlySkipFileChanged, IsApplyPipeline bool } // RequiredEnvVars is used to store values such as TF_VAR_ , github and pingdom tokens diff --git a/pkg/github/auth_type.go b/pkg/github/auth_type.go new file mode 100644 index 00000000..89f685bf --- /dev/null +++ b/pkg/github/auth_type.go @@ -0,0 +1,108 @@ +package github + +import ( + "context" + "fmt" + + "github.com/google/go-github/v68/github" +) + +func (c *GithubClient) SearchAuthTypeDefaultInPR(ctx context.Context, prNumber int) (string, error) { + opt := &github.ListOptions{PerPage: 100} + for { + files, resp, err := c.PullRequests.ListFiles(ctx, c.Owner, c.Repository, prNumber, opt) + if err != nil { + return "", fmt.Errorf("error listing files in pull request: %v", err) + } + for _, file := range files { + if file.GetFilename() == "variables.tf" && file.GetPatch() != "" { + // Try to extract default value for variable "auth_type" + if defVal := extractAuthTypeDefault(file.GetPatch()); defVal != "" { + return defVal, nil + } + } + } + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + return "", fmt.Errorf("auth_type variable with default not found in PR") +} + +// extractAuthTypeDefault parses a patch and returns the default value for variable "auth_type" if present +func extractAuthTypeDefault(patch string) string { + // Look for lines like: +variable "auth_type" { ... default = "something" ... } + var inVarBlock bool + for _, line := range splitLines(patch) { + if !inVarBlock && (line == "+variable \"auth_type\" {" || line == "+variable \"auth_type\"{") { + inVarBlock = true + } else if inVarBlock { + if line == "+}" { + inVarBlock = false + } else if len(line) > 0 && (line[0] == '+' || line[0] == ' ') { + // Look for default assignment + if _, val, ok := parseDefaultLine(line); ok { + return val + } + } + } + } + return "" +} + +// parseDefaultLine tries to parse a line like '+ default = "something"' and returns the value +func parseDefaultLine(line string) (string, string, bool) { + // Remove leading + and whitespace + l := line + if len(l) > 0 && l[0] == '+' { + l = l[1:] + } + l = trimSpace(l) + if len(l) >= 8 && l[:8] == "default=" { + l = l[8:] + } else if len(l) >= 9 && l[:8] == "default " { + l = l[8:] + if l[0] == '=' { + l = l[1:] + } + } else if len(l) >= 10 && l[:9] == "default =" { + l = l[9:] + } else { + return "", "", false + } + l = trimSpace(l) + // Remove quotes if present + if len(l) > 1 && l[0] == '"' && l[len(l)-1] == '"' { + return "default", l[1 : len(l)-1], true + } + return "default", l, true +} + +// splitLines splits a string into lines +func splitLines(s string) []string { + var lines []string + start := 0 + for i := 0; i < len(s); i++ { + if s[i] == '\n' { + lines = append(lines, s[start:i]) + start = i + 1 + } + } + if start < len(s) { + lines = append(lines, s[start:]) + } + return lines +} + +// trimSpace trims leading and trailing whitespace +func trimSpace(s string) string { + i, j := 0, len(s)-1 + for i <= j && (s[i] == ' ' || s[i] == '\t') { + i++ + } + for j >= i && (s[j] == ' ' || s[j] == '\t') { + j-- + } + return s[i : j+1] +} diff --git a/pkg/github/client.go b/pkg/github/client.go index 25fec054..3a0c965b 100644 --- a/pkg/github/client.go +++ b/pkg/github/client.go @@ -64,7 +64,7 @@ func NewGithubClient(config *GithubClientConfig, token string) *GithubClient { } } -func NewGihubAppClient(config *GithubClientConfig, key, appid, installid string) *GithubClient { +func NewGithubAppClient(config *GithubClientConfig, key, appid, installid string) *GithubClient { privateKey := []byte(key) appIDInt, err := strconv.ParseInt(appid, 10, 64) From 6729b1dea3386fdfa9753aca888b470231032583 Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Thu, 4 Sep 2025 15:02:26 +0100 Subject: [PATCH 06/12] feat(Go): update plan, apply and destroy commands --- go.mod | 2 +- go.sum | 4 +- local-setup/localsetup.go | 84 ++++++++++++++++++------------ pkg/commands/environment.go | 67 +++++++++++++++--------- pkg/environment/apply_test.go | 2 +- pkg/environment/divergence.go | 2 +- pkg/environment/divergence_test.go | 2 +- pkg/environment/namespace.go | 2 +- pkg/github/auth_type.go | 76 ++++++++++++++++++++------- pkg/github/client.go | 2 +- pkg/github/client_iface.go | 2 +- pkg/github/client_test.go | 2 +- pkg/mocks/github/GithubIface.go | 2 +- 13 files changed, 162 insertions(+), 87 deletions(-) diff --git a/go.mod b/go.mod index dbe6da41..d9a3e411 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/deckarep/golang-set/v2 v2.6.0 github.com/dlclark/regexp2 v1.10.0 github.com/google/go-cmdtest v0.4.0 - github.com/google/go-github/v68 v68.0.0 + github.com/google/go-github/v74 v74.0.0 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hc-install v0.6.2 github.com/hashicorp/terraform-exec v0.20.0 diff --git a/go.sum b/go.sum index 94dffd97..a1b10a32 100644 --- a/go.sum +++ b/go.sum @@ -217,10 +217,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -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-github/v73 v73.0.0 h1:aR+Utnh+Y4mMkS+2qLQwcQ/cF9mOTpdwnzlaw//rG24= github.com/google/go-github/v73 v73.0.0/go.mod h1:fa6w8+/V+edSU0muqdhCVY7Beh1M8F1IlQPZIANKIYw= +github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM= +github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak= 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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/local-setup/localsetup.go b/local-setup/localsetup.go index 7273f9c4..d245e5bd 100644 --- a/local-setup/localsetup.go +++ b/local-setup/localsetup.go @@ -6,14 +6,15 @@ import ( "log" "os" "os/exec" + "strings" "syscall" ) var ( - AWSProfile = "moj-cp" - clusterName string - kubeConfig string - clusterArray []string + awsProfile = flag.String("aws-profile", "", "AWS Profile to use") + account = flag.String("account", "754256621582", "AWS Account ID") + namespace = flag.String("namespace", "", "Namespace to set the environment for") + clusterArray = []string{"live", "manager", "live-2"} home, _ = os.UserHomeDir() colourCyan = "\033[36m" colourReset = "\033[0m" @@ -30,11 +31,10 @@ func setAWSEnv(ns string) { } -func setKubeEnv(clusterName string) bool { - var returnOuput bool +func setKubeEnv() error { fmt.Println(string(colourYellow), "\nSetting Kube Configuration", string(colourReset)) - kubeConfig = home + "/.kube/" + clusterName + "/config" + kubeConfig := home + "/.kube/config" // these are the three kube variables expected by kubectl os.Setenv("KUBECONFIG", kubeConfig) @@ -46,7 +46,7 @@ func setKubeEnv(clusterName string) bool { fmt.Println(string(colourCyan), "KUBE_CONFIG:", string(colourReset), os.Getenv("KUBE_CONFIG")) fmt.Println(string(colourCyan), "KUBE_CONFIG_PATH:", string(colourReset), os.Getenv("KUBE_CONFIG_PATH")) - return returnOuput + return nil } func setTFWksp(namespace string) error { @@ -61,25 +61,38 @@ func setTFWksp(namespace string) error { return nil } -func setNamespaceTF(namespace string) { - os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_app_id", `aws ssm get-parameter --name "/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_app_id" --with-decryption --profile moj-cp --query "Parameter.Value" --output text`) - os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id", `aws ssm get-parameter --name "/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_installation_id" --with-decryption --profile moj-cp --query "Parameter.Value" --output text`) - os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file", `aws ssm get-parameter --name "/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_pem_file" --with-decryption --profile moj-cp --query "Parameter.Value" --output text`) +func setNamespaceTF(namespace, clusterName string) { + getSSMParam := func(paramName string) string { + cmd := exec.Command("aws", "ssm", "get-parameter", "--name", paramName, "--with-decryption", "--profile", *awsProfile, "--query", "Parameter.Value", "--output", "text") + out, err := cmd.Output() + if err != nil { + log.Fatalf("Failed to get SSM parameter %s: %v", paramName, err) + } + return strings.TrimSpace(string(out)) + } + + os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_app_id", getSSMParam("/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_app_id")) + os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id", getSSMParam("/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_installation_id")) + os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file", getSSMParam("/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_pem_file")) + os.Setenv("PINGDOM_API_TOKEN", getSSMParam("/cloud-platform/infrastructure/components/pingdom_api_token")) os.Setenv("PIPELINE_STATE_BUCKET", "cloud-platform-terraform-state") - os.Setenv("PIPELINE_STATE_KEY_PREFIX", "cloud-platform-environments") + os.Setenv("PIPELINE_STATE_KEY_PREFIX", "cloud-platform-environments/") os.Setenv("PIPELINE_TERRAFORM_STATE_LOCK_TABLE", "cloud-platform-environments-terraform-lock") os.Setenv("PIPELINE_STATE_REGION", "eu-west-1") - os.Setenv("PIPELINE_CLUSTER", "arn:aws:eks:eu-west-2:754256621582:cluster/live") - os.Setenv("PIPELINE_CLUSTER_DIR", "live.cloud-platform.service.justice.gov.uk") - os.Setenv("PIPELINE_CLUSTER_STATE", "live-1.cloud-platform.service.justice.gov.uk") - os.Setenv("PIPELINE_STATE_KEY_PREFIX", fmt.Sprintf("cloud-platform-environments/live-1.cloud-platform.service.justice.gov.uk/%v/terraform.tfstate", namespace)) - os.Setenv("TF_VAR_cluster_name", "live-1") - os.Setenv("TF_VAR_vpc_name", "live-1") - os.Setenv("TF_VAR_eks_cluster_name", "live") + os.Setenv("PIPELINE_CLUSTER", fmt.Sprintf("arn:aws:eks:eu-west-2:%v:cluster/%v", *account, clusterName)) + os.Setenv("PIPELINE_CLUSTER_DIR", fmt.Sprintf("%v.cloud-platform.service.justice.gov.uk", clusterName)) + os.Setenv("TF_VAR_eks_cluster_name", fmt.Sprintf("%v", clusterName)) + os.Setenv("TF_VAR_cluster_state_bucket", "cloud-platform-terraform-state") os.Setenv("TF_VAR_github_owner", "ministryofjustice") os.Setenv("TF_VAR_kubernetes_cluster", "DF366E49809688A3B16EEC29707D8C09.gr7.eu-west-2.eks.amazonaws.com") - os.Setenv("PINGDOM_API_TOKEN", `aws ssm get-parameter --name "/cloud-platform/infrastructure/components/pingdom_api_token" --with-decryption --profile moj-cp --query "Parameter.Value" --output text`) + + if clusterName == "live" { + clusterName = "live-1" + } + os.Setenv("PIPELINE_CLUSTER_STATE", fmt.Sprintf("%v.cloud-platform.service.justice.gov.uk", clusterName)) + os.Setenv("TF_VAR_cluster_name", fmt.Sprintf("%v", clusterName)) + os.Setenv("TF_VAR_vpc_name", fmt.Sprintf("%v", clusterName)) } func setTerm(clusterName string) { @@ -114,8 +127,7 @@ func contains(arg string) bool { func namespaceEnv(namespace *string) { var arg string - setAWSEnv(AWSProfile) - clusterArray = []string{"live", "manager", "live-2"} + setAWSEnv(*awsProfile) fmt.Println("Please enter cluster name:") fmt.Scanln(&arg) // retry loop with max three attempts @@ -131,34 +143,38 @@ func namespaceEnv(namespace *string) { log.Fatalf(string(colourRed), "Cluster name is incorrect", string(colourReset)) } - clusterName = arg + clusterName := arg - b := setKubeEnv(clusterName) - if !b { - log.Fatalf(string(colourRed), "Error setting kube config", string(colourReset)) + err := setKubeEnv() + if err != nil { + log.Fatalf(string(colourRed), "Error setting kube config: %v", err, string(colourReset)) } - err := setTFWksp(*namespace) + err = setTFWksp(*namespace) if err != nil { log.Fatalf(string(colourRed), "Error setting Terraform workspace: %v", err, string(colourReset)) } - setNamespaceTF(*namespace) + setNamespaceTF(*namespace, clusterName) setTerm(clusterName) } func main() { + flag.Parse() + if *namespace == "" { + log.Fatalf(string(colourRed), "Namespace is required", string(colourReset)) + } + + if *awsProfile == "" { + log.Fatalf(string(colourRed), "AWS Profile is required", string(colourReset)) + } + h, err := os.UserHomeDir() if err != nil { log.Fatalf("User Home: %v", err) } - arg := flag.String("namespace", "", "namespace name") - namespace := arg home = h - - flag.Parse() - namespaceEnv(namespace) } diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index d500d487..ac323c9a 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -182,31 +182,22 @@ var environmentPlanCmd = &cobra.Command{ contextLogger.Fatalf("Failed to get auth_type from PR: %v", err) } + var applier *environment.Apply if authType == "app" { - ghClient := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID) - if ghClient == nil { - contextLogger.Fatal("Failed to create Github App client: check AppID, InstallID, and PemFile are set and valid.") - } - - applier := &environment.Apply{ + applier = &environment.Apply{ Options: &optFlags, - GithubClient: ghClient, - } - - err := applier.Plan() - if err != nil { - contextLogger.Fatal(err) + GithubClient: github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID), } } else { - applier := &environment.Apply{ + applier = &environment.Apply{ Options: &optFlags, GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), } + } - err := applier.Plan() - if err != nil { - contextLogger.Fatal(err) - } + err = applier.Plan() + if err != nil { + contextLogger.Fatal(err) } }, } @@ -245,9 +236,23 @@ var environmentApplyCmd = &cobra.Command{ Owner: "ministryofjustice", } - applier := &environment.Apply{ - Options: &optFlags, - GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), + // get authtype this is only needed for migration purposes once users are all using github app this can be removed + authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).SearchAuthTypeDefaultInPR(context.Background(), optFlags.PRNumber) + if err != nil { + contextLogger.Fatalf("Failed to get auth_type from PR: %v", err) + } + + var applier *environment.Apply + if authType == "app" { + applier = &environment.Apply{ + Options: &optFlags, + GithubClient: github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID), + } + } else { + applier = &environment.Apply{ + Options: &optFlags, + GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), + } } // if -namespace or a prNumber is provided, apply on given namespace @@ -311,12 +316,26 @@ var environmentDestroyCmd = &cobra.Command{ Owner: "ministryofjustice", } - applier := &environment.Apply{ - Options: &optFlags, - GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), + // get authtype this is only needed for migration purposes once users are all using github app this can be removed + authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).SearchAuthTypeDefaultInPR(context.Background(), optFlags.PRNumber) + if err != nil { + contextLogger.Fatalf("Failed to get auth_type from PR: %v", err) + } + + var applier *environment.Apply + if authType == "app" { + applier = &environment.Apply{ + Options: &optFlags, + GithubClient: github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID), + } + } else { + applier = &environment.Apply{ + Options: &optFlags, + GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), + } } - err := applier.Destroy() + err = applier.Destroy() if err != nil { contextLogger.Fatal(err) } diff --git a/pkg/environment/apply_test.go b/pkg/environment/apply_test.go index c73ae63f..8036ffbe 100644 --- a/pkg/environment/apply_test.go +++ b/pkg/environment/apply_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/google/go-github/v68/github" + "github.com/google/go-github/v74/github" "github.com/ministryofjustice/cloud-platform-cli/pkg/environment/mocks" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" diff --git a/pkg/environment/divergence.go b/pkg/environment/divergence.go index 231b2bd6..eef9784e 100644 --- a/pkg/environment/divergence.go +++ b/pkg/environment/divergence.go @@ -5,7 +5,7 @@ import ( "fmt" mapset "github.com/deckarep/golang-set/v2" - "github.com/google/go-github/v68/github" + "github.com/google/go-github/v74/github" "golang.org/x/oauth2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" diff --git a/pkg/environment/divergence_test.go b/pkg/environment/divergence_test.go index 09ab46bf..c68f9fb3 100644 --- a/pkg/environment/divergence_test.go +++ b/pkg/environment/divergence_test.go @@ -7,7 +7,7 @@ import ( "testing" mapset "github.com/deckarep/golang-set/v2" - "github.com/google/go-github/v68/github" + "github.com/google/go-github/v74/github" "github.com/migueleliasweb/go-github-mock/src/mock" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/environment/namespace.go b/pkg/environment/namespace.go index 956414d3..476c6e79 100644 --- a/pkg/environment/namespace.go +++ b/pkg/environment/namespace.go @@ -5,7 +5,7 @@ import ( "os" "strings" - gogithub "github.com/google/go-github/v68/github" + gogithub "github.com/google/go-github/v74/github" "github.com/ministryofjustice/cloud-platform-cli/pkg/util" "gopkg.in/yaml.v2" v1 "k8s.io/api/core/v1" diff --git a/pkg/github/auth_type.go b/pkg/github/auth_type.go index 89f685bf..6181e393 100644 --- a/pkg/github/auth_type.go +++ b/pkg/github/auth_type.go @@ -3,8 +3,10 @@ package github import ( "context" "fmt" + "os" + "strings" - "github.com/google/go-github/v68/github" + "github.com/google/go-github/v74/github" ) func (c *GithubClient) SearchAuthTypeDefaultInPR(ctx context.Context, prNumber int) (string, error) { @@ -15,7 +17,8 @@ func (c *GithubClient) SearchAuthTypeDefaultInPR(ctx context.Context, prNumber i return "", fmt.Errorf("error listing files in pull request: %v", err) } for _, file := range files { - if file.GetFilename() == "variables.tf" && file.GetPatch() != "" { + fmt.Fprintf(os.Stderr, "PR FILE: %s patch-len=%d\n", file.GetFilename(), len(file.GetPatch())) // DEBUG + if strings.HasSuffix(file.GetFilename(), "variables.tf") && file.GetPatch() != "" { // Try to extract default value for variable "auth_type" if defVal := extractAuthTypeDefault(file.GetPatch()); defVal != "" { return defVal, nil @@ -32,17 +35,26 @@ func (c *GithubClient) SearchAuthTypeDefaultInPR(ctx context.Context, prNumber i // extractAuthTypeDefault parses a patch and returns the default value for variable "auth_type" if present func extractAuthTypeDefault(patch string) string { - // Look for lines like: +variable "auth_type" { ... default = "something" ... } + // More robust: allow for extra whitespace and ignore indentation var inVarBlock bool for _, line := range splitLines(patch) { - if !inVarBlock && (line == "+variable \"auth_type\" {" || line == "+variable \"auth_type\"{") { + l := trimSpace(line) + fmt.Fprintf(os.Stderr, "PATCH LINE: %s\n", l) // DEBUG + if !inVarBlock && len(l) > 0 && l[0] == '+' && containsVarAuthType(l) { + fmt.Fprintf(os.Stderr, "-- Entering auth_type variable block\n") // DEBUG inVarBlock = true - } else if inVarBlock { - if line == "+}" { + continue + } + if inVarBlock { + if l == "+}" || l == "+ }" { + fmt.Fprintf(os.Stderr, "-- Exiting variable block\n") // DEBUG inVarBlock = false - } else if len(line) > 0 && (line[0] == '+' || line[0] == ' ') { - // Look for default assignment - if _, val, ok := parseDefaultLine(line); ok { + continue + } + if len(l) > 0 && (l[0] == '+' || l[0] == ' ') { + key, val, ok := parseDefaultLine(l) + if ok { + fmt.Fprintf(os.Stderr, "-- Found default line: key=%s val=%s\n", key, val) // DEBUG return val } } @@ -59,19 +71,21 @@ func parseDefaultLine(line string) (string, string, bool) { l = l[1:] } l = trimSpace(l) - if len(l) >= 8 && l[:8] == "default=" { - l = l[8:] - } else if len(l) >= 9 && l[:8] == "default " { - l = l[8:] - if l[0] == '=' { - l = l[1:] + // Accept various spacings: default=, default =, default = + if len(l) >= 7 && l[:7] == "default" { + rest := l[7:] + rest = trimSpace(rest) + if len(rest) > 0 && rest[0] == '=' { + rest = rest[1:] + rest = trimSpace(rest) + l = rest + fmt.Fprintf(os.Stderr, "parseDefaultLine: found default assignment, value: %s\n", l) // DEBUG + } else { + return "", "", false } - } else if len(l) >= 10 && l[:9] == "default =" { - l = l[9:] } else { return "", "", false } - l = trimSpace(l) // Remove quotes if present if len(l) > 1 && l[0] == '"' && l[len(l)-1] == '"' { return "default", l[1 : len(l)-1], true @@ -79,6 +93,32 @@ func parseDefaultLine(line string) (string, string, bool) { return "default", l, true } +// containsVarAuthType checks if a line contains the start of the auth_type variable block +func containsVarAuthType(line string) bool { + // Accept: +variable "auth_type" {, +variable "auth_type"{, with any whitespace + if len(line) < 18 { + return false + } + // Remove leading + and whitespace + l := line + if l[0] == '+' { + l = l[1:] + } + l = trimSpace(l) + if len(l) >= 18 && l[:8] == "variable" { + rest := l[8:] + rest = trimSpace(rest) + if len(rest) >= 12 && rest[:11] == "\"auth_type\"" { + rest2 := rest[11:] + rest2 = trimSpace(rest2) + if rest2 == "{" || rest2 == "{" { + return true + } + } + } + return false +} + // splitLines splits a string into lines func splitLines(s string) []string { var lines []string diff --git a/pkg/github/client.go b/pkg/github/client.go index 3a0c965b..f00ae4cf 100644 --- a/pkg/github/client.go +++ b/pkg/github/client.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/google/go-github/v68/github" + "github.com/google/go-github/v74/github" "github.com/jferrl/go-githubauth" "github.com/ministryofjustice/cloud-platform-cli/pkg/util" "github.com/shurcooL/githubv4" diff --git a/pkg/github/client_iface.go b/pkg/github/client_iface.go index cad66ec5..1b09a759 100644 --- a/pkg/github/client_iface.go +++ b/pkg/github/client_iface.go @@ -1,7 +1,7 @@ package github import ( - "github.com/google/go-github/v68/github" + "github.com/google/go-github/v74/github" "github.com/ministryofjustice/cloud-platform-cli/pkg/util" ) diff --git a/pkg/github/client_test.go b/pkg/github/client_test.go index 7e2848e8..7f89291a 100644 --- a/pkg/github/client_test.go +++ b/pkg/github/client_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/google/go-github/v68/github" + "github.com/google/go-github/v74/github" "github.com/stretchr/testify/assert" ) diff --git a/pkg/mocks/github/GithubIface.go b/pkg/mocks/github/GithubIface.go index 492182d8..146d8b70 100644 --- a/pkg/mocks/github/GithubIface.go +++ b/pkg/mocks/github/GithubIface.go @@ -3,7 +3,7 @@ package mocks import ( - github "github.com/google/go-github/v68/github" + github "github.com/google/go-github/v74/github" mock "github.com/stretchr/testify/mock" pkggithub "github.com/ministryofjustice/cloud-platform-cli/pkg/github" From 049a14225e15b83add781b4d101059c71a6605e0 Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Tue, 9 Sep 2025 14:11:29 +0100 Subject: [PATCH 07/12] feat: add nightly image release for testing --- .github/workflows/build-release-nightly.yml | 61 +++++++ Dockerfile | 2 +- local-setup/go.mod | 3 - local-setup/localsetup.go | 180 -------------------- 4 files changed, 62 insertions(+), 184 deletions(-) create mode 100644 .github/workflows/build-release-nightly.yml delete mode 100644 local-setup/go.mod delete mode 100644 local-setup/localsetup.go diff --git a/.github/workflows/build-release-nightly.yml b/.github/workflows/build-release-nightly.yml new file mode 100644 index 00000000..dd8728f0 --- /dev/null +++ b/.github/workflows/build-release-nightly.yml @@ -0,0 +1,61 @@ +name: Build and push a new release + +on: + push: + branches: + - add-app-auth + +permissions: + contents: write + +jobs: + build: + name: GoReleaser build + runs-on: ubuntu-latest + + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 ##v4.2.2 + with: + fetch-depth: 0 # See: https://goreleaser.com/ci/actions/ + - name: Fetch all tags + run: git fetch --force --tags + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 ##v5.5.0 + with: + go-version: " 1.25.x" + id: go + + docker: + name: Build container and push to DockerHub + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 ##v4.2.2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 ##v3.6.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 ##v3.10.0 + + - name: Login to DockerHub + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 ##v3.4.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Get Tags for Image + id: metadata + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 ##v5.7.0 + with: + images: cloudplatformmoj/cloud-platform-cli-nightly + tags: | + type=raw,value=0.0.1 + - name: Build and push + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 ##v6.9.0 + with: + context: . + push: true + tags: ${{ steps.metadata.outputs.tags }} + build-args: CLOUD_PLATFORM_CLI_VERSION=${{ steps.metadata.outputs.tags }} diff --git a/Dockerfile b/Dockerfile index e6236188..980bfae9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build Cloud Platform tools (CLI) -FROM golang:1.24.3-bookworm AS cli_builder +FROM golang:1.25.1-bookworm AS cli_builder ENV \ CGO_ENABLED=0 \ diff --git a/local-setup/go.mod b/local-setup/go.mod deleted file mode 100644 index bdd52ccc..00000000 --- a/local-setup/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/ministryofjustice/cloud-platform-cli/local-setup - -go 1.25.0 diff --git a/local-setup/localsetup.go b/local-setup/localsetup.go deleted file mode 100644 index d245e5bd..00000000 --- a/local-setup/localsetup.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "os" - "os/exec" - "strings" - "syscall" -) - -var ( - awsProfile = flag.String("aws-profile", "", "AWS Profile to use") - account = flag.String("account", "754256621582", "AWS Account ID") - namespace = flag.String("namespace", "", "Namespace to set the environment for") - clusterArray = []string{"live", "manager", "live-2"} - home, _ = os.UserHomeDir() - colourCyan = "\033[36m" - colourReset = "\033[0m" - colourYellow = "\033[33m" - colourRed = "\033[31m" -) - -func setAWSEnv(ns string) { - fmt.Println(string(colourYellow), "\nSetting AWS Configuration", string(colourReset)) - - os.Setenv("AWS_PROFILE", ns) - - fmt.Println(string(colourCyan), "AWS_PROFILE:", string(colourReset), os.Getenv("AWS_PROFILE")) - -} - -func setKubeEnv() error { - fmt.Println(string(colourYellow), "\nSetting Kube Configuration", string(colourReset)) - - kubeConfig := home + "/.kube/config" - - // these are the three kube variables expected by kubectl - os.Setenv("KUBECONFIG", kubeConfig) - // this is needed for kubectl provider - os.Setenv("KUBE_CONFIG", os.Getenv("KUBECONFIG")) - os.Setenv("KUBE_CONFIG_PATH", os.Getenv("KUBECONFIG")) - - fmt.Println(string(colourCyan), "KUBECONFIG:", string(colourReset), os.Getenv("KUBECONFIG")) - fmt.Println(string(colourCyan), "KUBE_CONFIG:", string(colourReset), os.Getenv("KUBE_CONFIG")) - fmt.Println(string(colourCyan), "KUBE_CONFIG_PATH:", string(colourReset), os.Getenv("KUBE_CONFIG_PATH")) - - return nil -} - -func setTFWksp(namespace string) error { - // tf workspace to the cluster name - fmt.Println(string(colourYellow), "\nUpdating Terraform Workspace") - err := os.Setenv("TF_WORKSPACE", namespace) - if err != nil { - return err - } else { - fmt.Println(string(colourCyan), "TF_WORKSPACE:", string(colourReset), os.Getenv("TF_WORKSPACE")) - } - return nil -} - -func setNamespaceTF(namespace, clusterName string) { - getSSMParam := func(paramName string) string { - cmd := exec.Command("aws", "ssm", "get-parameter", "--name", paramName, "--with-decryption", "--profile", *awsProfile, "--query", "Parameter.Value", "--output", "text") - out, err := cmd.Output() - if err != nil { - log.Fatalf("Failed to get SSM parameter %s: %v", paramName, err) - } - return strings.TrimSpace(string(out)) - } - - os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_app_id", getSSMParam("/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_app_id")) - os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id", getSSMParam("/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_installation_id")) - os.Setenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file", getSSMParam("/cloud-platform/infrastructure/components/github_cloud_platform_concourse_bot_pem_file")) - os.Setenv("PINGDOM_API_TOKEN", getSSMParam("/cloud-platform/infrastructure/components/pingdom_api_token")) - os.Setenv("PIPELINE_STATE_BUCKET", "cloud-platform-terraform-state") - os.Setenv("PIPELINE_STATE_KEY_PREFIX", "cloud-platform-environments/") - os.Setenv("PIPELINE_TERRAFORM_STATE_LOCK_TABLE", "cloud-platform-environments-terraform-lock") - os.Setenv("PIPELINE_STATE_REGION", "eu-west-1") - os.Setenv("PIPELINE_CLUSTER", fmt.Sprintf("arn:aws:eks:eu-west-2:%v:cluster/%v", *account, clusterName)) - os.Setenv("PIPELINE_CLUSTER_DIR", fmt.Sprintf("%v.cloud-platform.service.justice.gov.uk", clusterName)) - os.Setenv("TF_VAR_eks_cluster_name", fmt.Sprintf("%v", clusterName)) - - os.Setenv("TF_VAR_cluster_state_bucket", "cloud-platform-terraform-state") - os.Setenv("TF_VAR_github_owner", "ministryofjustice") - os.Setenv("TF_VAR_kubernetes_cluster", "DF366E49809688A3B16EEC29707D8C09.gr7.eu-west-2.eks.amazonaws.com") - - if clusterName == "live" { - clusterName = "live-1" - } - os.Setenv("PIPELINE_CLUSTER_STATE", fmt.Sprintf("%v.cloud-platform.service.justice.gov.uk", clusterName)) - os.Setenv("TF_VAR_cluster_name", fmt.Sprintf("%v", clusterName)) - os.Setenv("TF_VAR_vpc_name", fmt.Sprintf("%v", clusterName)) -} - -func setTerm(clusterName string) { - fmt.Println(string(colourYellow), "Updating Kube Context") - cmd := exec.Command("aws", "eks", "update-kubeconfig", "--name", clusterName, "--region", "eu-west-2") - cmd.Run() - - log.Println(string(colourYellow), "\nSetting Terminal Context", string(colourReset)) - cmd = exec.Command("kubectl", "config", "current-context") - out, err := cmd.CombinedOutput() - if err != nil { - log.Fatal(string(colourRed), "(setTerm)CombinedOutput: ", err, string(colourReset)) - } - - kconfig := string(out) - fmt.Println(string(colourYellow), "\nSetting Terminal Environment") - os.Setenv("KUBE_PS1", kconfig+": ") - errsys := syscall.Exec(os.Getenv("SHELL"), []string{os.Getenv("SHELL")}, os.Environ()) - if errsys != nil { - log.Fatal(string(colourRed), "(setTerm)errsys: ", errsys, string(colourReset)) - } -} - -func contains(arg string) bool { - for _, cluster := range clusterArray { - if cluster == arg { - return true - } - } - return false -} - -func namespaceEnv(namespace *string) { - var arg string - setAWSEnv(*awsProfile) - fmt.Println("Please enter cluster name:") - fmt.Scanln(&arg) - // retry loop with max three attempts - for i := 0; i < 3; i++ { - if contains(arg) { - break - } else { - fmt.Println("Please select a cluster from the list:") - fmt.Scanln(&arg) - } - } - if !contains(arg) { - log.Fatalf(string(colourRed), "Cluster name is incorrect", string(colourReset)) - } - - clusterName := arg - - err := setKubeEnv() - if err != nil { - log.Fatalf(string(colourRed), "Error setting kube config: %v", err, string(colourReset)) - } - - err = setTFWksp(*namespace) - if err != nil { - log.Fatalf(string(colourRed), "Error setting Terraform workspace: %v", err, string(colourReset)) - } - - setNamespaceTF(*namespace, clusterName) - - setTerm(clusterName) -} - -func main() { - flag.Parse() - if *namespace == "" { - log.Fatalf(string(colourRed), "Namespace is required", string(colourReset)) - } - - if *awsProfile == "" { - log.Fatalf(string(colourRed), "AWS Profile is required", string(colourReset)) - } - - h, err := os.UserHomeDir() - if err != nil { - log.Fatalf("User Home: %v", err) - } - home = h - namespaceEnv(namespace) - -} From bd29577c925896fea4ab850e600d1d55999dd1f2 Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Tue, 9 Sep 2025 15:35:46 +0100 Subject: [PATCH 08/12] feat(Go): change authtype err to set default instead of fail as is shouldnt exit on this error --- .github/workflows/build-release-nightly.yml | 61 --------------------- pkg/commands/environment.go | 9 ++- 2 files changed, 6 insertions(+), 64 deletions(-) delete mode 100644 .github/workflows/build-release-nightly.yml diff --git a/.github/workflows/build-release-nightly.yml b/.github/workflows/build-release-nightly.yml deleted file mode 100644 index dd8728f0..00000000 --- a/.github/workflows/build-release-nightly.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Build and push a new release - -on: - push: - branches: - - add-app-auth - -permissions: - contents: write - -jobs: - build: - name: GoReleaser build - runs-on: ubuntu-latest - - steps: - - name: Check out code into the Go module directory - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 ##v4.2.2 - with: - fetch-depth: 0 # See: https://goreleaser.com/ci/actions/ - - name: Fetch all tags - run: git fetch --force --tags - - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 ##v5.5.0 - with: - go-version: " 1.25.x" - id: go - - docker: - name: Build container and push to DockerHub - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 ##v4.2.2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 ##v3.6.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 ##v3.10.0 - - - name: Login to DockerHub - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 ##v3.4.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Get Tags for Image - id: metadata - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 ##v5.7.0 - with: - images: cloudplatformmoj/cloud-platform-cli-nightly - tags: | - type=raw,value=0.0.1 - - name: Build and push - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 ##v6.9.0 - with: - context: . - push: true - tags: ${{ steps.metadata.outputs.tags }} - build-args: CLOUD_PLATFORM_CLI_VERSION=${{ steps.metadata.outputs.tags }} diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index ac323c9a..54ee08f9 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -179,7 +179,8 @@ var environmentPlanCmd = &cobra.Command{ // get authtype this is only needed for migration purposes once users are all using github app this can be removed authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).SearchAuthTypeDefaultInPR(context.Background(), optFlags.PRNumber) if err != nil { - contextLogger.Fatalf("Failed to get auth_type from PR: %v", err) + contextLogger.Printf("Failed to get auth_type from PR: %v, defaulting to token auth", err) + authType = "token" } var applier *environment.Apply @@ -239,7 +240,8 @@ var environmentApplyCmd = &cobra.Command{ // get authtype this is only needed for migration purposes once users are all using github app this can be removed authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).SearchAuthTypeDefaultInPR(context.Background(), optFlags.PRNumber) if err != nil { - contextLogger.Fatalf("Failed to get auth_type from PR: %v", err) + contextLogger.Printf("Failed to get auth_type from PR: %v, defaulting to token auth", err) + authType = "token" } var applier *environment.Apply @@ -319,7 +321,8 @@ var environmentDestroyCmd = &cobra.Command{ // get authtype this is only needed for migration purposes once users are all using github app this can be removed authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).SearchAuthTypeDefaultInPR(context.Background(), optFlags.PRNumber) if err != nil { - contextLogger.Fatalf("Failed to get auth_type from PR: %v", err) + contextLogger.Printf("Failed to get auth_type from PR: %v, defaulting to token auth", err) + authType = "token" } var applier *environment.Apply From b052efad5c9dd28e521b6f0464175756901ea82e Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Thu, 25 Sep 2025 11:19:17 +0100 Subject: [PATCH 09/12] feat(Go): Add missing environments flags to the env command for apply and destory --- pkg/commands/environment.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index 54ee08f9..e5b0ac7d 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -72,6 +72,9 @@ func addEnvironmentCmd(topLevel *cobra.Command) { environmentApplyCmd.PersistentFlags().BoolVar(&optFlags.RedactedEnv, "redact", true, "Redact the terraform output before printing") environmentApplyCmd.Flags().StringVar(&optFlags.BuildUrl, "build-url", "", "The concourse apply build url") environmentApplyCmd.Flags().BoolVar(&optFlags.IsApplyPipeline, "is-apply-pipeline", false, "is this running in the apply pipelines") + environmentApplyCmd.Flags().StringVar(&optFlags.AppID, "github-appid", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_app_id"), "App ID ") + environmentApplyCmd.Flags().StringVar(&optFlags.InstallID, "github-installation-id", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id"), "Installation ID ") + environmentApplyCmd.Flags().StringVar(&optFlags.PemFile, "github-pem-file", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file"), "PEM file ") environmentBumpModuleCmd.Flags().StringVarP(&module, "module", "m", "", "Module to upgrade the version") environmentBumpModuleCmd.Flags().StringVarP(&moduleVersion, "module-version", "v", "", "Semantic version to bump a module to") @@ -90,6 +93,9 @@ func addEnvironmentCmd(topLevel *cobra.Command) { environmentDestroyCmd.Flags().StringVar(&optFlags.ClusterDir, "clusterdir", "", "folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name") environmentDestroyCmd.PersistentFlags().BoolVar(&optFlags.RedactedEnv, "redact", true, "Redact the terraform output before printing") environmentDestroyCmd.Flags().BoolVar(&optFlags.SkipProdDestroy, "skip-prod-destroy", true, "skip prod namespaces from destroy namespace") + environmentDestroyCmd.Flags().StringVar(&optFlags.AppID, "github-appid", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_app_id"), "App ID ") + environmentDestroyCmd.Flags().StringVar(&optFlags.InstallID, "github-installation-id", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id"), "Installation ID ") + environmentDestroyCmd.Flags().StringVar(&optFlags.PemFile, "github-pem-file", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file"), "PEM file ") environmentDivergenceCmd.Flags().StringVarP(&clusterName, "cluster-name", "c", "live", "[optional] Cluster name") environmentDivergenceCmd.Flags().StringVarP(&githubToken, "github-token", "g", "", "[required] Github token") From 9e4423b5dba595f93ff2a3f4ddbc8863869c027d Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Fri, 26 Sep 2025 14:21:24 +0100 Subject: [PATCH 10/12] feat(Go): Update authtype to be based on branch instead of pr so that it can be used across all pipelines --- pkg/commands/environment.go | 9 ++-- pkg/github/auth_type.go | 89 ++++++++++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index e5b0ac7d..1be4261e 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -182,8 +182,7 @@ var environmentPlanCmd = &cobra.Command{ Owner: "ministryofjustice", } - // get authtype this is only needed for migration purposes once users are all using github app this can be removed - authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).SearchAuthTypeDefaultInPR(context.Background(), optFlags.PRNumber) + authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).FlagCheckAuthType(context.Background(), optFlags.PRNumber, optFlags.Namespace) if err != nil { contextLogger.Printf("Failed to get auth_type from PR: %v, defaulting to token auth", err) authType = "token" @@ -243,8 +242,7 @@ var environmentApplyCmd = &cobra.Command{ Owner: "ministryofjustice", } - // get authtype this is only needed for migration purposes once users are all using github app this can be removed - authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).SearchAuthTypeDefaultInPR(context.Background(), optFlags.PRNumber) + authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).FlagCheckAuthType(context.Background(), optFlags.PRNumber, optFlags.Namespace) if err != nil { contextLogger.Printf("Failed to get auth_type from PR: %v, defaulting to token auth", err) authType = "token" @@ -324,8 +322,7 @@ var environmentDestroyCmd = &cobra.Command{ Owner: "ministryofjustice", } - // get authtype this is only needed for migration purposes once users are all using github app this can be removed - authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).SearchAuthTypeDefaultInPR(context.Background(), optFlags.PRNumber) + authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).FlagCheckAuthType(context.Background(), optFlags.PRNumber, optFlags.Namespace) if err != nil { contextLogger.Printf("Failed to get auth_type from PR: %v, defaulting to token auth", err) authType = "token" diff --git a/pkg/github/auth_type.go b/pkg/github/auth_type.go index 6181e393..64b823d8 100644 --- a/pkg/github/auth_type.go +++ b/pkg/github/auth_type.go @@ -9,28 +9,85 @@ import ( "github.com/google/go-github/v74/github" ) -func (c *GithubClient) SearchAuthTypeDefaultInPR(ctx context.Context, prNumber int) (string, error) { - opt := &github.ListOptions{PerPage: 100} - for { - files, resp, err := c.PullRequests.ListFiles(ctx, c.Owner, c.Repository, prNumber, opt) +func (c *GithubClient) FlagCheckAuthType(ctx context.Context, prNumber int, namespace string) (string, error) { + var branch string + if prNumber == 0 && namespace == "" { + return "", fmt.Errorf("either -pr-number or -namespace flag is required") + } else if prNumber > 0 { + // get namespace from PR + prDetails, err := c.PRDetails(context.Background(), prNumber) if err != nil { - return "", fmt.Errorf("error listing files in pull request: %v", err) + return "", fmt.Errorf("failed to get pr details: %v", err) } - for _, file := range files { - fmt.Fprintf(os.Stderr, "PR FILE: %s patch-len=%d\n", file.GetFilename(), len(file.GetPatch())) // DEBUG - if strings.HasSuffix(file.GetFilename(), "variables.tf") && file.GetPatch() != "" { - // Try to extract default value for variable "auth_type" - if defVal := extractAuthTypeDefault(file.GetPatch()); defVal != "" { - return defVal, nil - } - } + branch = prDetails[0] + namespace = prDetails[1] + if namespace == "" { + return "", fmt.Errorf("namespace not found in pr %d", prNumber) } - if resp.NextPage == 0 { + } else if namespace != "" { + branch = "main" + } + + // get authtype this is only needed for migration purposes once users are all using github app this can be removed + authType, err := c.SearchAuthTypeInRepo(context.Background(), namespace, branch) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get auth_type from PR: %v, defaulting to token auth\n", err) // DEBUG + authType = "token" + } + + return authType, nil +} + +func (c *GithubClient) PRDetails(ctx context.Context, prNumber int) ([]string, error) { + var namespace string + pr, _, err := c.V3.PullRequests.Get(ctx, c.Owner, c.Repository, prNumber) + if err != nil { + return nil, fmt.Errorf("error getting PR %d: %v", prNumber, err) + } + branch := pr.GetHead().GetRef() + fmt.Println(branch) + files, _, err := c.V3.PullRequests.ListFiles(ctx, c.Owner, c.Repository, prNumber, nil) + if err != nil { + return nil, fmt.Errorf("error listing files for PR %d: %v", prNumber, err) + } + for _, file := range files { + fmt.Println(file.Filename) + // split file path by "/" + pathParts := strings.Split(file.GetFilename(), "/") + // check if path matches expected pattern + if len(pathParts) >= 5 && pathParts[0] == "namespaces" && pathParts[1] == "live.cloud-platform.service.justice.gov.uk" { + namespace = pathParts[2] break } - opt.Page = resp.NextPage + fmt.Println(namespace) } - return "", fmt.Errorf("auth_type variable with default not found in PR") + + prDetails := []string{branch, namespace} + return prDetails, nil +} + +// search repo for auth_type variable default in a PR depending on namespace directory name +func (c *GithubClient) SearchAuthTypeInRepo(ctx context.Context, namespace, branch string) (string, error) { + path := fmt.Sprintf("namespaces/live.cloud-platform.service.justice.gov.uk/%s/resources/variables.tf", namespace) + opt := &github.RepositoryContentGetOptions{ + Ref: branch, + } + fileContent, _, _, err := c.V3.Repositories.GetContents(ctx, c.Owner, c.Repository, path, opt) + if err != nil { + return "", fmt.Errorf("error getting directory contents for %s: %v", path, err) + } + + content, err := fileContent.GetContent() + if err != nil { + return "", fmt.Errorf("error getting file content for %s: %v", path, err) + } + + // Try to extract default value for variable "auth_type" + if defVal := extractAuthTypeDefault(content); defVal != "" { + return defVal, nil + } + + return "", fmt.Errorf("auth_type variable with default not found in %s", path) } // extractAuthTypeDefault parses a patch and returns the default value for variable "auth_type" if present From b64a0148e4bf2ea57a38590989406e5f2a97b898 Mon Sep 17 00:00:00 2001 From: jackstockley89 Date: Fri, 26 Sep 2025 14:56:22 +0100 Subject: [PATCH 11/12] feat(Go): add clusterName variable --- pkg/commands/environment.go | 12 ++- pkg/github/auth_type.go | 63 +++++++++----- pkg/github/client.go | 10 ++- pkg/github/client_test.go | 165 ++++++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 27 deletions(-) diff --git a/pkg/commands/environment.go b/pkg/commands/environment.go index 1be4261e..957912b2 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -72,9 +72,11 @@ func addEnvironmentCmd(topLevel *cobra.Command) { environmentApplyCmd.PersistentFlags().BoolVar(&optFlags.RedactedEnv, "redact", true, "Redact the terraform output before printing") environmentApplyCmd.Flags().StringVar(&optFlags.BuildUrl, "build-url", "", "The concourse apply build url") environmentApplyCmd.Flags().BoolVar(&optFlags.IsApplyPipeline, "is-apply-pipeline", false, "is this running in the apply pipelines") + environmentApplyCmd.Flags().StringVar(&optFlags.AppID, "github-appid", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_app_id"), "App ID ") environmentApplyCmd.Flags().StringVar(&optFlags.InstallID, "github-installation-id", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id"), "Installation ID ") environmentApplyCmd.Flags().StringVar(&optFlags.PemFile, "github-pem-file", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file"), "PEM file ") + environmentApplyCmd.Flags().StringVarP(&clusterName, "cluster-name", "c", "live", "[optional] Cluster name") environmentBumpModuleCmd.Flags().StringVarP(&module, "module", "m", "", "Module to upgrade the version") environmentBumpModuleCmd.Flags().StringVarP(&moduleVersion, "module-version", "v", "", "Semantic version to bump a module to") @@ -93,9 +95,11 @@ func addEnvironmentCmd(topLevel *cobra.Command) { environmentDestroyCmd.Flags().StringVar(&optFlags.ClusterDir, "clusterdir", "", "folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name") environmentDestroyCmd.PersistentFlags().BoolVar(&optFlags.RedactedEnv, "redact", true, "Redact the terraform output before printing") environmentDestroyCmd.Flags().BoolVar(&optFlags.SkipProdDestroy, "skip-prod-destroy", true, "skip prod namespaces from destroy namespace") + environmentDestroyCmd.Flags().StringVar(&optFlags.AppID, "github-appid", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_app_id"), "App ID ") environmentDestroyCmd.Flags().StringVar(&optFlags.InstallID, "github-installation-id", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id"), "Installation ID ") environmentDestroyCmd.Flags().StringVar(&optFlags.PemFile, "github-pem-file", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file"), "PEM file ") + environmentDestroyCmd.Flags().StringVarP(&clusterName, "cluster-name", "c", "live", "[optional] Cluster name") environmentDivergenceCmd.Flags().StringVarP(&clusterName, "cluster-name", "c", "live", "[optional] Cluster name") environmentDivergenceCmd.Flags().StringVarP(&githubToken, "github-token", "g", "", "[required] Github token") @@ -113,9 +117,11 @@ func addEnvironmentCmd(topLevel *cobra.Command) { environmentPlanCmd.Flags().StringVar(&optFlags.ClusterCtx, "cluster", "", "cluster context from kubeconfig file") environmentPlanCmd.Flags().StringVar(&optFlags.ClusterDir, "clusterdir", "", "folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name") environmentPlanCmd.PersistentFlags().BoolVar(&optFlags.RedactedEnv, "redact", true, "Redact the terraform output before printing") + environmentPlanCmd.Flags().StringVar(&optFlags.AppID, "github-appid", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_app_id"), "App ID ") environmentPlanCmd.Flags().StringVar(&optFlags.InstallID, "github-installation-id", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_installation_id"), "Installation ID ") environmentPlanCmd.Flags().StringVar(&optFlags.PemFile, "github-pem-file", os.Getenv("TF_VAR_github_cloud_platform_concourse_bot_pem_file"), "PEM file ") + environmentPlanCmd.Flags().StringVarP(&clusterName, "cluster-name", "c", "live", "[optional] Cluster name") } @@ -182,7 +188,7 @@ var environmentPlanCmd = &cobra.Command{ Owner: "ministryofjustice", } - authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).FlagCheckAuthType(context.Background(), optFlags.PRNumber, optFlags.Namespace) + authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).FlagCheckAuthType(context.Background(), optFlags.PRNumber, optFlags.Namespace, clusterName) if err != nil { contextLogger.Printf("Failed to get auth_type from PR: %v, defaulting to token auth", err) authType = "token" @@ -242,7 +248,7 @@ var environmentApplyCmd = &cobra.Command{ Owner: "ministryofjustice", } - authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).FlagCheckAuthType(context.Background(), optFlags.PRNumber, optFlags.Namespace) + authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).FlagCheckAuthType(context.Background(), optFlags.PRNumber, optFlags.Namespace, clusterName) if err != nil { contextLogger.Printf("Failed to get auth_type from PR: %v, defaulting to token auth", err) authType = "token" @@ -322,7 +328,7 @@ var environmentDestroyCmd = &cobra.Command{ Owner: "ministryofjustice", } - authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).FlagCheckAuthType(context.Background(), optFlags.PRNumber, optFlags.Namespace) + authType, err := github.NewGithubAppClient(ghConfig, optFlags.PemFile, optFlags.AppID, optFlags.InstallID).FlagCheckAuthType(context.Background(), optFlags.PRNumber, optFlags.Namespace, clusterName) if err != nil { contextLogger.Printf("Failed to get auth_type from PR: %v, defaulting to token auth", err) authType = "token" diff --git a/pkg/github/auth_type.go b/pkg/github/auth_type.go index 64b823d8..89d6bd19 100644 --- a/pkg/github/auth_type.go +++ b/pkg/github/auth_type.go @@ -9,13 +9,13 @@ import ( "github.com/google/go-github/v74/github" ) -func (c *GithubClient) FlagCheckAuthType(ctx context.Context, prNumber int, namespace string) (string, error) { +func (c *GithubClient) FlagCheckAuthType(ctx context.Context, prNumber int, namespace, clusterName string) (string, error) { var branch string if prNumber == 0 && namespace == "" { return "", fmt.Errorf("either -pr-number or -namespace flag is required") } else if prNumber > 0 { // get namespace from PR - prDetails, err := c.PRDetails(context.Background(), prNumber) + prDetails, err := c.PRDetails(context.Background(), prNumber, clusterName) if err != nil { return "", fmt.Errorf("failed to get pr details: %v", err) } @@ -25,11 +25,12 @@ func (c *GithubClient) FlagCheckAuthType(ctx context.Context, prNumber int, name return "", fmt.Errorf("namespace not found in pr %d", prNumber) } } else if namespace != "" { - branch = "main" + // get branch from from local + branch = getCurrentBranch() } // get authtype this is only needed for migration purposes once users are all using github app this can be removed - authType, err := c.SearchAuthTypeInRepo(context.Background(), namespace, branch) + authType, err := c.SearchAuthTypeInRepo(context.Background(), namespace, branch, clusterName) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get auth_type from PR: %v, defaulting to token auth\n", err) // DEBUG authType = "token" @@ -38,28 +39,39 @@ func (c *GithubClient) FlagCheckAuthType(ctx context.Context, prNumber int, name return authType, nil } -func (c *GithubClient) PRDetails(ctx context.Context, prNumber int) ([]string, error) { +func getCurrentBranch() string { + branchBytes, err := os.ReadFile(".git/HEAD") + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read .git/HEAD: %v, defaulting to main\n", err) // DEBUG + return "main" + } + branchRef := strings.TrimSpace(string(branchBytes)) + if strings.HasPrefix(branchRef, "ref: refs/heads/") { + return strings.TrimPrefix(branchRef, "ref: refs/heads/") + } + fmt.Fprintf(os.Stderr, "Unexpected format in .git/HEAD: %s, defaulting to main\n", branchRef) // DEBUG + return "main" +} + +func (c *GithubClient) PRDetails(ctx context.Context, prNumber int, clusterName string) ([]string, error) { var namespace string - pr, _, err := c.V3.PullRequests.Get(ctx, c.Owner, c.Repository, prNumber) + pr, _, err := c.PullRequests.Get(ctx, c.Owner, c.Repository, prNumber) if err != nil { return nil, fmt.Errorf("error getting PR %d: %v", prNumber, err) } branch := pr.GetHead().GetRef() - fmt.Println(branch) - files, _, err := c.V3.PullRequests.ListFiles(ctx, c.Owner, c.Repository, prNumber, nil) + files, _, err := c.PullRequests.ListFiles(ctx, c.Owner, c.Repository, prNumber, nil) if err != nil { return nil, fmt.Errorf("error listing files for PR %d: %v", prNumber, err) } for _, file := range files { - fmt.Println(file.Filename) // split file path by "/" pathParts := strings.Split(file.GetFilename(), "/") // check if path matches expected pattern - if len(pathParts) >= 5 && pathParts[0] == "namespaces" && pathParts[1] == "live.cloud-platform.service.justice.gov.uk" { + if len(pathParts) >= 5 && pathParts[0] == "namespaces" && pathParts[1] == clusterName+".cloud-platform.service.justice.gov.uk" { namespace = pathParts[2] break } - fmt.Println(namespace) } prDetails := []string{branch, namespace} @@ -67,14 +79,27 @@ func (c *GithubClient) PRDetails(ctx context.Context, prNumber int) ([]string, e } // search repo for auth_type variable default in a PR depending on namespace directory name -func (c *GithubClient) SearchAuthTypeInRepo(ctx context.Context, namespace, branch string) (string, error) { - path := fmt.Sprintf("namespaces/live.cloud-platform.service.justice.gov.uk/%s/resources/variables.tf", namespace) +func (c *GithubClient) SearchAuthTypeInRepo(ctx context.Context, namespace, branch, clusterName string) (string, error) { + path := fmt.Sprintf("namespaces/%s.cloud-platform.service.justice.gov.uk/%s/resources/variables.tf", clusterName, namespace) + + // Clean up branch reference - remove head/ prefix if present + cleanBranch := strings.TrimPrefix(branch, "head/") + opt := &github.RepositoryContentGetOptions{ - Ref: branch, + Ref: cleanBranch, } + fileContent, _, _, err := c.V3.Repositories.GetContents(ctx, c.Owner, c.Repository, path, opt) if err != nil { - return "", fmt.Errorf("error getting directory contents for %s: %v", path, err) + // Try fallback: check if the file exists in main branch + mainOpt := &github.RepositoryContentGetOptions{ + Ref: "main", + } + var fallbackErr error + fileContent, _, _, fallbackErr = c.V3.Repositories.GetContents(ctx, c.Owner, c.Repository, path, mainOpt) + if fallbackErr != nil { + return "", fmt.Errorf("error getting file %s (tried branch %s and main): %v", path, cleanBranch, err) + } } content, err := fileContent.GetContent() @@ -96,22 +121,18 @@ func extractAuthTypeDefault(patch string) string { var inVarBlock bool for _, line := range splitLines(patch) { l := trimSpace(line) - fmt.Fprintf(os.Stderr, "PATCH LINE: %s\n", l) // DEBUG if !inVarBlock && len(l) > 0 && l[0] == '+' && containsVarAuthType(l) { - fmt.Fprintf(os.Stderr, "-- Entering auth_type variable block\n") // DEBUG inVarBlock = true continue } if inVarBlock { if l == "+}" || l == "+ }" { - fmt.Fprintf(os.Stderr, "-- Exiting variable block\n") // DEBUG inVarBlock = false continue } if len(l) > 0 && (l[0] == '+' || l[0] == ' ') { - key, val, ok := parseDefaultLine(l) + _, val, ok := parseDefaultLine(l) if ok { - fmt.Fprintf(os.Stderr, "-- Found default line: key=%s val=%s\n", key, val) // DEBUG return val } } @@ -168,7 +189,7 @@ func containsVarAuthType(line string) bool { if len(rest) >= 12 && rest[:11] == "\"auth_type\"" { rest2 := rest[11:] rest2 = trimSpace(rest2) - if rest2 == "{" || rest2 == "{" { + if rest2 == "{" { return true } } diff --git a/pkg/github/client.go b/pkg/github/client.go index f00ae4cf..b0e67d6f 100644 --- a/pkg/github/client.go +++ b/pkg/github/client.go @@ -16,6 +16,7 @@ import ( var _ GithubPullRequestsService = (*github.PullRequestsService)(nil) type GithubPullRequestsService interface { + Get(ctx context.Context, owner, repo string, number int) (*github.PullRequest, *github.Response, error) ListFiles(ctx context.Context, owner string, repo string, number int, opt *github.ListOptions) ([]*github.CommitFile, *github.Response, error) IsMerged(ctx context.Context, owner string, repo string, number int) (bool, *github.Response, error) Create(ctx context.Context, owner string, repo string, pr *github.NewPullRequest) (*github.PullRequest, *github.Response, error) @@ -69,19 +70,22 @@ func NewGithubAppClient(config *GithubClientConfig, key, appid, installid string appIDInt, err := strconv.ParseInt(appid, 10, 64) if err != nil { - fmt.Printf("[NewGihubAppClient] Failed to parse appid '%s': %v\n", appid, err) + fmt.Printf("[NewGithubAppClient] Failed to parse appid, value returned:'%s'\nerror message: %v\n", appid, err) + fmt.Println("[NewGithubAppClient] Check if the appid has been set correctly") return nil } installIDInt, err := strconv.ParseInt(installid, 10, 64) if err != nil { - fmt.Printf("[NewGihubAppClient] Failed to parse installid '%s': %v\n", installid, err) + fmt.Printf("[NewGithubAppClient] Failed to parse installid, value returned:'%s'\nerror message: %v\n", installid, err) + fmt.Println("[NewGithubAppClient] Check if the installid has been set correctly") return nil } appTokenSource, err := githubauth.NewApplicationTokenSource(appIDInt, privateKey) if err != nil { - fmt.Printf("[NewGihubAppClient] Failed to create ApplicationTokenSource: %v\n", err) + fmt.Printf("[NewGithubAppClient] Failed to create ApplicationTokenSource: %v\n", err) + fmt.Println("[NewGithubAppClient] Check if the private key has been set correctly") return nil } diff --git a/pkg/github/client_test.go b/pkg/github/client_test.go index 7f89291a..e7cc7002 100644 --- a/pkg/github/client_test.go +++ b/pkg/github/client_test.go @@ -14,6 +14,10 @@ type mockGithub struct { merged bool } +func (m *mockGithub) Get(ctx context.Context, owner, repo string, number int) (*github.PullRequest, *github.Response, error) { + return nil, nil, nil +} + func (m *mockGithub) ListFiles(ctx context.Context, owner string, repo string, number int, opt *github.ListOptions) ([]*github.CommitFile, *github.Response, error) { return m.resp, nil, nil } @@ -110,3 +114,164 @@ func TestGithubClient_IsMerged(t *testing.T) { t.Errorf("GithubClient.IsMerged() = %v, want %v", got, true) } } + +type mockPullRequestService struct { + pr *github.PullRequest + files []*github.CommitFile +} + +func (m *mockPullRequestService) Get(ctx context.Context, owner, repo string, number int) (*github.PullRequest, *github.Response, error) { + return m.pr, nil, nil +} + +func (m *mockPullRequestService) ListFiles(ctx context.Context, owner, repo string, number int, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) { + return m.files, nil, nil +} + +func (m *mockPullRequestService) IsMerged(ctx context.Context, owner, repo string, number int) (bool, *github.Response, error) { + return true, nil, nil +} + +func (m *mockPullRequestService) Create(ctx context.Context, owner, repo string, pr *github.NewPullRequest) (*github.PullRequest, *github.Response, error) { + return nil, nil, nil +} + +func (m *mockPullRequestService) List(ctx context.Context, owner, repo string, opts *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error) { + return nil, nil, nil +} + +func TestGithubClient_PRDetails(t *testing.T) { + tests := []struct { + name string + prNumber int + clusterName string + pr *github.PullRequest + files []*github.CommitFile + want []string + wantErr bool + errContains string + }{ + { + name: "successful extraction with valid namespace path", + prNumber: 123, + clusterName: "live", + pr: &github.PullRequest{ + Head: &github.PullRequestBranch{ + Ref: github.Ptr("feature-branch"), + }, + }, + files: []*github.CommitFile{ + { + Filename: github.Ptr("namespaces/live.cloud-platform.service.justice.gov.uk/test-namespace/resources/variables.tf"), + }, + { + Filename: github.Ptr("other/file.txt"), + }, + }, + want: []string{"feature-branch", "test-namespace"}, + wantErr: false, + }, + { + name: "no matching namespace path", + prNumber: 124, + clusterName: "live", + pr: &github.PullRequest{ + Head: &github.PullRequestBranch{ + Ref: github.Ptr("another-branch"), + }, + }, + files: []*github.CommitFile{ + { + Filename: github.Ptr("other/path/file.txt"), + }, + { + Filename: github.Ptr("random/file.yaml"), + }, + }, + want: []string{"another-branch", ""}, + wantErr: false, + }, + { + name: "multiple files with first matching", + prNumber: 125, + clusterName: "live", + pr: &github.PullRequest{ + Head: &github.PullRequestBranch{ + Ref: github.Ptr("dev-branch"), + }, + }, + files: []*github.CommitFile{ + { + Filename: github.Ptr("namespaces/live.cloud-platform.service.justice.gov.uk/first-namespace/resources/00-namespace.yaml"), + }, + { + Filename: github.Ptr("namespaces/live.cloud-platform.service.justice.gov.uk/second-namespace/resources/main.tf"), + }, + }, + want: []string{"dev-branch", "first-namespace"}, + wantErr: false, + }, + { + name: "path with insufficient parts", + prNumber: 126, + clusterName: "live", + pr: &github.PullRequest{ + Head: &github.PullRequestBranch{ + Ref: github.Ptr("short-path-branch"), + }, + }, + files: []*github.CommitFile{ + { + Filename: github.Ptr("namespaces/live.cloud-platform.service.justice.gov.uk"), + }, + }, + want: []string{"short-path-branch", ""}, + wantErr: false, + }, + { + name: "path with different cluster name", + prNumber: 126, + clusterName: "staging", + pr: &github.PullRequest{ + Head: &github.PullRequestBranch{ + Ref: github.Ptr("short-path-branch"), + }, + }, + files: []*github.CommitFile{ + { + Filename: github.Ptr("namespaces/staging.cloud-platform.service.justice.gov.uk/test-namespace/resources/variables.tf"), + }, + }, + want: []string{"short-path-branch", "test-namespace"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockPRService := &mockPullRequestService{ + pr: tt.pr, + files: tt.files, + } + + c := &GithubClient{ + Owner: "test-owner", + Repository: "test-repo", + PullRequests: mockPRService, + } + + got, err := c.PRDetails(context.Background(), tt.prNumber, tt.clusterName) + + if tt.wantErr { + assert.Error(t, err) + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} From 91d236011251e5d53c90ddc1de2c7a57151f37af Mon Sep 17 00:00:00 2001 From: jackstockley89 <45042046+jackstockley89@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:33:59 +0000 Subject: [PATCH 12/12] docs(cobra): update auto-generated documentation --- doc/cloud-platform_environment_apply.md | 32 +++++++++++++---------- doc/cloud-platform_environment_destroy.md | 22 +++++++++------- doc/cloud-platform_environment_plan.md | 20 ++++++++------ 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/doc/cloud-platform_environment_apply.md b/doc/cloud-platform_environment_apply.md index 41f24a2b..fb5d2436 100644 --- a/doc/cloud-platform_environment_apply.md +++ b/doc/cloud-platform_environment_apply.md @@ -38,20 +38,24 @@ $ cloud-platform environment apply -n ### Options ``` - --all-namespaces Apply all namespaces with -all-namespaces - --batch-apply-index int Starting index for Apply to a batch of namespaces - --batch-apply-size int Number of namespaces to apply in a batch - --build-url string The concourse apply build url - --cluster string cluster context from kubeconfig file - --clusterdir string folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name - --enable-apply-skip Enable skipping apply for a namespace - --github-token string Personal access Token from Github - -h, --help help for apply - --is-apply-pipeline is this running in the apply pipelines - --kubecfg string path to kubeconfig file (default "/home/runner/.kube/config") - -n, --namespace string Namespace which you want to perform the apply - --pr-number int Pull request ID or number to which you want to perform the apply - --redact Redact the terraform output before printing (default true) + --all-namespaces Apply all namespaces with -all-namespaces + --batch-apply-index int Starting index for Apply to a batch of namespaces + --batch-apply-size int Number of namespaces to apply in a batch + --build-url string The concourse apply build url + --cluster string cluster context from kubeconfig file + -c, --cluster-name string [optional] Cluster name (default "live") + --clusterdir string folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name + --enable-apply-skip Enable skipping apply for a namespace + --github-appid string App ID + --github-installation-id string Installation ID + --github-pem-file string PEM file + --github-token string Personal access Token from Github + -h, --help help for apply + --is-apply-pipeline is this running in the apply pipelines + --kubecfg string path to kubeconfig file (default "/home/runner/.kube/config") + -n, --namespace string Namespace which you want to perform the apply + --pr-number int Pull request ID or number to which you want to perform the apply + --redact Redact the terraform output before printing (default true) ``` ### Options inherited from parent commands diff --git a/doc/cloud-platform_environment_destroy.md b/doc/cloud-platform_environment_destroy.md index 9082559a..f09024ad 100644 --- a/doc/cloud-platform_environment_destroy.md +++ b/doc/cloud-platform_environment_destroy.md @@ -38,15 +38,19 @@ $ cloud-platform environment destroy -n ### Options ``` - --cluster string cluster context from kubeconfig file - --clusterdir string folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name - --github-token string Personal access Token from Github - -h, --help help for destroy - --kubecfg string path to kubeconfig file (default "/home/runner/.kube/config") - -n, --namespace string Namespace which you want to perform the destroy - --pr-number int Pull request ID or number to which you want to perform the destroy - --redact Redact the terraform output before printing (default true) - --skip-prod-destroy skip prod namespaces from destroy namespace (default true) + --cluster string cluster context from kubeconfig file + -c, --cluster-name string [optional] Cluster name (default "live") + --clusterdir string folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name + --github-appid string App ID + --github-installation-id string Installation ID + --github-pem-file string PEM file + --github-token string Personal access Token from Github + -h, --help help for destroy + --kubecfg string path to kubeconfig file (default "/home/runner/.kube/config") + -n, --namespace string Namespace which you want to perform the destroy + --pr-number int Pull request ID or number to which you want to perform the destroy + --redact Redact the terraform output before printing (default true) + --skip-prod-destroy skip prod namespaces from destroy namespace (default true) ``` ### Options inherited from parent commands diff --git a/doc/cloud-platform_environment_plan.md b/doc/cloud-platform_environment_plan.md index 039f6962..6553cb7f 100644 --- a/doc/cloud-platform_environment_plan.md +++ b/doc/cloud-platform_environment_plan.md @@ -39,14 +39,18 @@ $ cloud-platform environment plan ### Options ``` - --cluster string cluster context from kubeconfig file - --clusterdir string folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name - --github-token string Personal access Token from Github - -h, --help help for plan - --kubecfg string path to kubeconfig file (default "/home/runner/.kube/config") - -n, --namespace string Namespace which you want to perform the plan - --pr-number int Pull request ID or number to which you want to perform the plan - --redact Redact the terraform output before printing (default true) + --cluster string cluster context from kubeconfig file + -c, --cluster-name string [optional] Cluster name (default "live") + --clusterdir string folder name under namespaces/ inside cloud-platform-environments repo referring to full cluster name + --github-appid string App ID + --github-installation-id string Installation ID + --github-pem-file string PEM file + --github-token string Personal access Token from Github + -h, --help help for plan + --kubecfg string path to kubeconfig file (default "/home/runner/.kube/config") + -n, --namespace string Namespace which you want to perform the plan + --pr-number int Pull request ID or number to which you want to perform the plan + --redact Redact the terraform output before printing (default true) ``` ### Options inherited from parent commands