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/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 diff --git a/go.mod b/go.mod index a29317c2..d9a3e411 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 ) @@ -32,21 +32,22 @@ 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/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 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 +75,18 @@ 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 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 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..a1b10a32 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/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= @@ -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..957912b2 100644 --- a/pkg/commands/environment.go +++ b/pkg/commands/environment.go @@ -1,6 +1,7 @@ package commands import ( + "context" "errors" "os" "path/filepath" @@ -72,6 +73,11 @@ func addEnvironmentCmd(topLevel *cobra.Command) { 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") @@ -90,6 +96,11 @@ func addEnvironmentCmd(topLevel *cobra.Command) { 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") environmentDivergenceCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "[optional] Kubeconfig file path") @@ -100,13 +111,18 @@ 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") - // 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 ") + environmentPlanCmd.Flags().StringVarP(&clusterName, "cluster-name", "c", "live", "[optional] Cluster name") + } var environmentCmd = &cobra.Command{ @@ -164,6 +180,7 @@ var environmentPlanCmd = &cobra.Command{ `), PreRun: upgradeIfNotLatest, Run: func(cmd *cobra.Command, args []string) { + contextLogger := log.WithFields(log.Fields{"subcommand": "plan"}) ghConfig := &github.GithubClientConfig{ @@ -171,12 +188,26 @@ var environmentPlanCmd = &cobra.Command{ Owner: "ministryofjustice", } - applier := &environment.Apply{ - Options: &optFlags, - GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), + 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" + } + + 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.Plan() + err = applier.Plan() if err != nil { contextLogger.Fatal(err) } @@ -217,9 +248,23 @@ var environmentApplyCmd = &cobra.Command{ Owner: "ministryofjustice", } - applier := &environment.Apply{ - Options: &optFlags, - GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), + 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" + } + + 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 @@ -283,12 +328,26 @@ var environmentDestroyCmd = &cobra.Command{ Owner: "ministryofjustice", } - applier := &environment.Apply{ - Options: &optFlags, - GithubClient: github.NewGithubClient(ghConfig, optFlags.GithubToken), + 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" + } + + 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.go b/pkg/environment/apply.go index 35ba3fff..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 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 @@ -31,10 +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:"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 diff --git a/pkg/environment/apply_test.go b/pkg/environment/apply_test.go index 3620cc9e..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/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 6b18124d..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/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 0a2c5823..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/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/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/environment/namespace.go b/pkg/environment/namespace.go index 229322de..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/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 new file mode 100644 index 00000000..89d6bd19 --- /dev/null +++ b/pkg/github/auth_type.go @@ -0,0 +1,226 @@ +package github + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/google/go-github/v74/github" +) + +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, clusterName) + if err != nil { + return "", fmt.Errorf("failed to get pr details: %v", err) + } + branch = prDetails[0] + namespace = prDetails[1] + if namespace == "" { + return "", fmt.Errorf("namespace not found in pr %d", prNumber) + } + } else if namespace != "" { + // 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, 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" + } + + return authType, nil +} + +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.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() + 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 { + // split file path by "/" + pathParts := strings.Split(file.GetFilename(), "/") + // check if path matches expected pattern + if len(pathParts) >= 5 && pathParts[0] == "namespaces" && pathParts[1] == clusterName+".cloud-platform.service.justice.gov.uk" { + namespace = pathParts[2] + break + } + } + + 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, 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: cleanBranch, + } + + fileContent, _, _, err := c.V3.Repositories.GetContents(ctx, c.Owner, c.Repository, path, opt) + if err != nil { + // 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() + 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 +func extractAuthTypeDefault(patch string) string { + // More robust: allow for extra whitespace and ignore indentation + var inVarBlock bool + for _, line := range splitLines(patch) { + l := trimSpace(line) + if !inVarBlock && len(l) > 0 && l[0] == '+' && containsVarAuthType(l) { + inVarBlock = true + continue + } + if inVarBlock { + if l == "+}" || l == "+ }" { + inVarBlock = false + continue + } + if len(l) > 0 && (l[0] == '+' || l[0] == ' ') { + _, val, ok := parseDefaultLine(l) + if 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) + // 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 { + return "", "", false + } + // 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 +} + +// 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 == "{" { + return true + } + } + } + return false +} + +// 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 aad62562..b0e67d6f 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/v74/github" + "github.com/jferrl/go-githubauth" "github.com/ministryofjustice/cloud-platform-cli/pkg/util" "github.com/shurcooL/githubv4" "golang.org/x/oauth2" @@ -14,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) @@ -62,6 +65,46 @@ func NewGithubClient(config *GithubClientConfig, token string) *GithubClient { } } +func NewGithubAppClient(config *GithubClientConfig, key, appid, installid string) *GithubClient { + privateKey := []byte(key) + + appIDInt, err := strconv.ParseInt(appid, 10, 64) + if err != nil { + 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("[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("[NewGithubAppClient] Failed to create ApplicationTokenSource: %v\n", err) + fmt.Println("[NewGithubAppClient] Check if the private key has been set correctly") + 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..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/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 489ed90b..e7cc7002 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/v74/github" "github.com/stretchr/testify/assert" ) @@ -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) + }) + } +} diff --git a/pkg/mocks/github/GithubIface.go b/pkg/mocks/github/GithubIface.go index 4c11029c..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/github" + github "github.com/google/go-github/v74/github" mock "github.com/stretchr/testify/mock" pkggithub "github.com/ministryofjustice/cloud-platform-cli/pkg/github"