From 488e99c9282b8afb60625d75af0189676c0c6602 Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Fri, 29 May 2026 20:11:14 -0400 Subject: [PATCH 1/5] feat(QOV-1953): add --read-only flag to cluster kubeconfig and get-token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cluster kubeconfig --read-only: downloads a kubeconfig with read-only exec plugin (calls get-token --read-only), output file named kubeconfig-readonly-.yaml - cluster get-token --read-only: requests a SA-backed read-only token instead of an admin cloud-provider token - All existing callers pass readOnly=false explicitly — no behavior change CLI will compile once qovery-client-go is regenerated from the spec (ReadOnly() method on ApiGetClusterKubeconfigRequest and ApiGetClusterTokenByClusterIdRequest). --- cmd/admin_k9s.go | 2 +- cmd/cluster_get_token.go | 9 ++++++--- cmd/cluster_kubeconfig.go | 21 ++++++++++++++------- pkg/admin_load_credentials.go | 2 +- pkg/cluster.go | 13 +++++++++++-- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/cmd/admin_k9s.go b/cmd/admin_k9s.go index a4e3f9d1..afa4438b 100644 --- a/cmd/admin_k9s.go +++ b/cmd/admin_k9s.go @@ -46,7 +46,7 @@ func launchK9s(args []string) { } clusterId := args[0] - kubeconfig := pkg.GetKubeconfigByClusterId(clusterId) + kubeconfig := pkg.GetKubeconfigByClusterId(clusterId, false) filePath := utils.WriteInFile(clusterId, "kubeconfig", []byte(kubeconfig)) if err := os.Setenv("KUBECONFIG", filePath); err != nil { log.Fatal(err) diff --git a/cmd/cluster_get_token.go b/cmd/cluster_get_token.go index 711b0048..4d427a09 100644 --- a/cmd/cluster_get_token.go +++ b/cmd/cluster_get_token.go @@ -7,17 +7,20 @@ import ( "github.com/spf13/cobra" ) +var getTokenReadOnly bool + var getTokenCommand = &cobra.Command{ Use: "get-token", Short: "Get token for a cluster ID", Run: func(cmd *cobra.Command, args []string) { validateGetTokenFlags() - getToken() + getToken(getTokenReadOnly) }, } func init() { getTokenCommand.Flags().StringVarP(&clusterId, "cluster-id", "c", "", "Cluster ID") + getTokenCommand.Flags().BoolVarP(&getTokenReadOnly, "read-only", "r", false, "Get a read-only service account token instead of an admin token") clusterCmd.AddCommand(getTokenCommand) } @@ -27,7 +30,7 @@ func validateGetTokenFlags() { } } -func getToken() { - response := pkg.GetTokenByClusterId(clusterId) +func getToken(readOnly bool) { + response := pkg.GetTokenByClusterId(clusterId, readOnly) utils.Println(response) } diff --git a/cmd/cluster_kubeconfig.go b/cmd/cluster_kubeconfig.go index 38e5e77f..f1b5f2f4 100644 --- a/cmd/cluster_kubeconfig.go +++ b/cmd/cluster_kubeconfig.go @@ -12,19 +12,25 @@ import ( "github.com/spf13/cobra" ) +var readOnlyKubeconfig bool + var downloadKubeconfigCmd = &cobra.Command{ Use: "kubeconfig", Short: "Retrieve kubeconfig with a cluster ID", Run: func(cmd *cobra.Command, args []string) { validateKubeconfigFlags() - kubeconfigFilename := downloadKubeconfig(clusterId) + kubeconfigFilename := downloadKubeconfig(clusterId, readOnlyKubeconfig) log.Info("Kubeconfig file created in the current directory.") log.Info("Execute `export KUBECONFIG=" + kubeconfigFilename + "` to use it.") + if readOnlyKubeconfig { + log.Info("This kubeconfig uses read-only access (ServiceAccount with view ClusterRole).") + } }, } func init() { downloadKubeconfigCmd.Flags().StringVarP(&clusterId, "cluster-id", "c", "", "Cluster ID") + downloadKubeconfigCmd.Flags().BoolVarP(&readOnlyKubeconfig, "read-only", "r", false, "Download a read-only kubeconfig backed by a Kubernetes service account with the view ClusterRole") clusterCmd.AddCommand(downloadKubeconfigCmd) } @@ -35,11 +41,9 @@ func validateKubeconfigFlags() { } } -func downloadKubeconfig(clusterId string) string { - // download kubeconfig - kubeconfig := pkg.GetKubeconfigByClusterId(clusterId) +func downloadKubeconfig(clusterId string, readOnly bool) string { + kubeconfig := pkg.GetKubeconfigByClusterId(clusterId, readOnly) - // get current working directory dir, err := os.Getwd() if err != nil { @@ -47,8 +51,11 @@ func downloadKubeconfig(clusterId string) string { os.Exit(1) } - kubeconfigFilename := filepath.Join(dir, "kubeconfig-"+clusterId+".yaml") - // create a file in the current folder + suffix := "" + if readOnly { + suffix = "-readonly" + } + kubeconfigFilename := filepath.Join(dir, "kubeconfig"+suffix+"-"+clusterId+".yaml") writeError := os.WriteFile(kubeconfigFilename, []byte(kubeconfig), 0600) if writeError != nil { utils.PrintlnError(writeError) diff --git a/pkg/admin_load_credentials.go b/pkg/admin_load_credentials.go index 8756eb25..b88477d9 100644 --- a/pkg/admin_load_credentials.go +++ b/pkg/admin_load_credentials.go @@ -53,7 +53,7 @@ func LoadCredentials(clusterId string, doNotConnectToBastion bool) error { } utils.PrintlnInfo(fmt.Sprintf("Set environment variable %s for child process", cred.Key)) } - kubeconfig := GetKubeconfigByClusterId(clusterId) + kubeconfig := GetKubeconfigByClusterId(clusterId, false) filePath := utils.WriteInFile(clusterId, "kubeconfig", []byte(kubeconfig)) if err := os.Setenv("KUBECONFIG", filePath); err != nil { return fmt.Errorf("failed to set KUBECONFIG: %w", err) diff --git a/pkg/cluster.go b/pkg/cluster.go index e4140e29..fc6154df 100644 --- a/pkg/cluster.go +++ b/pkg/cluster.go @@ -10,7 +10,7 @@ import ( "github.com/qovery/qovery-client-go" ) -func GetKubeconfigByClusterId(clusterId string) string { +func GetKubeconfigByClusterId(clusterId string, readOnly bool) string { qoveryClient := GetQoveryClientInstance() request := qoveryClient.ClustersAPI.GetClusterKubeconfig( @@ -18,6 +18,11 @@ func GetKubeconfigByClusterId(clusterId string) string { "00000000-0000-0000-000000000000", clusterId, ).WithTokenFromCli(true) + + if readOnly { + request = request.ReadOnly(true) + } + response, httpResponse, err := qoveryClient.ClustersAPI.GetClusterKubeconfigExecute(request) if err != nil { utils.PrintlnError(err) @@ -50,10 +55,14 @@ func UpdateClusterKubeconfig(organizationId string, clusterId string, kubeconfig return nil } -func GetTokenByClusterId(clusterId string) string { +func GetTokenByClusterId(clusterId string, readOnly bool) string { qoveryClient := GetQoveryClientInstance() request := qoveryClient.DefaultAPI.GetClusterTokenByClusterId(context.Background(), clusterId) + if readOnly { + request = request.ReadOnly(true) + } + _, response, err := qoveryClient.DefaultAPI.GetClusterTokenByClusterIdExecute(request) if err != nil { utils.PrintlnError(err) From 08d43b2c183a479ea1cf817d6a0148679c8d478b Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Mon, 1 Jun 2026 14:46:37 +0200 Subject: [PATCH 2/5] fix(QOV-1953): do not pass read_only to token endpoint (not implemented yet) --- pkg/cluster.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/cluster.go b/pkg/cluster.go index fc6154df..3d539ff4 100644 --- a/pkg/cluster.go +++ b/pkg/cluster.go @@ -58,11 +58,10 @@ func UpdateClusterKubeconfig(organizationId string, clusterId string, kubeconfig func GetTokenByClusterId(clusterId string, readOnly bool) string { qoveryClient := GetQoveryClientInstance() + // readOnly is accepted by the CLI flag but not yet passed to the server — + // token generation for the qovery-readonly SA requires cluster-agent gRPC (future work). + _ = readOnly request := qoveryClient.DefaultAPI.GetClusterTokenByClusterId(context.Background(), clusterId) - if readOnly { - request = request.ReadOnly(true) - } - _, response, err := qoveryClient.DefaultAPI.GetClusterTokenByClusterIdExecute(request) if err != nil { utils.PrintlnError(err) From 7888a2a852801ef9ac76428c9a5a710081ef3ec0 Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Tue, 2 Jun 2026 10:14:51 +0200 Subject: [PATCH 3/5] feat(QOV-1953): restore ReadOnly() call on token endpoint --- pkg/cluster.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cluster.go b/pkg/cluster.go index 3d539ff4..28321ff8 100644 --- a/pkg/cluster.go +++ b/pkg/cluster.go @@ -58,10 +58,10 @@ func UpdateClusterKubeconfig(organizationId string, clusterId string, kubeconfig func GetTokenByClusterId(clusterId string, readOnly bool) string { qoveryClient := GetQoveryClientInstance() - // readOnly is accepted by the CLI flag but not yet passed to the server — - // token generation for the qovery-readonly SA requires cluster-agent gRPC (future work). - _ = readOnly request := qoveryClient.DefaultAPI.GetClusterTokenByClusterId(context.Background(), clusterId) + if readOnly { + request = request.ReadOnly(true) + } _, response, err := qoveryClient.DefaultAPI.GetClusterTokenByClusterIdExecute(request) if err != nil { utils.PrintlnError(err) From 75ed7f99e68ee99563fd193dd667eda7e22e9757 Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Wed, 10 Jun 2026 12:25:30 +0200 Subject: [PATCH 4/5] chore(QOV-1953): bump qovery-client-go with read_only param Also pass readOnly=false to downloadKubeconfig from the new terraform setup-backend caller added on main. --- cmd/terraform_setup_backend.go | 2 +- go.mod | 2 +- go.sum | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/terraform_setup_backend.go b/cmd/terraform_setup_backend.go index 68d96999..4e91ddf7 100644 --- a/cmd/terraform_setup_backend.go +++ b/cmd/terraform_setup_backend.go @@ -31,7 +31,7 @@ var terraformSetupBackendCmd = &cobra.Command{ utils.Println(fmt.Sprintf("Preparing backend.tf file for terraform `%s` of environment `%s`", terraform.Name, env.Name)) // Download kubeconfig to connect to the cluster - kubeconfigPath := downloadKubeconfig(env.ClusterId) + kubeconfigPath := downloadKubeconfig(env.ClusterId, false) // Create kubeclient to retrieve the namespace of the tfstate secret kubeconfig, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) diff --git a/go.mod b/go.mod index 44fef392..c512badb 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/posthog/posthog-go v1.12.5 github.com/pterm/pterm v0.12.83 - github.com/qovery/qovery-client-go v0.0.0-20260609072636-f548ebe903f2 + github.com/qovery/qovery-client-go v0.0.0-20260610095547-986d768ca7f9 github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 diff --git a/go.sum b/go.sum index e17d13f8..191ef11f 100644 --- a/go.sum +++ b/go.sum @@ -191,6 +191,10 @@ github.com/pterm/pterm v0.12.83 h1:ie+YmGmA727VuhxBlyGr74Ks+7McV6kT99IB8EU80aA= github.com/pterm/pterm v0.12.83/go.mod h1:xlgc6bFWyJIMtmLJvGim+L7jhSReilOlOnodeIYe4Tk= github.com/qovery/qovery-client-go v0.0.0-20260609072636-f548ebe903f2 h1:R6lG3dFH9/N7k7Hz+MMMCY1y3c0N9rmY6NAAjy1dkos= github.com/qovery/qovery-client-go v0.0.0-20260609072636-f548ebe903f2/go.mod h1:mcXeQtxR4AIGIBaWLhy52S16UwL8/1fcDywDuSK1BZ4= +github.com/qovery/qovery-client-go v0.0.0-20260609115652-6441e57b57d0 h1:Aym0jIz4MHNx4NooERldZuedlRGDVj1z7gACCzgQ2ho= +github.com/qovery/qovery-client-go v0.0.0-20260609115652-6441e57b57d0/go.mod h1:mcXeQtxR4AIGIBaWLhy52S16UwL8/1fcDywDuSK1BZ4= +github.com/qovery/qovery-client-go v0.0.0-20260610095547-986d768ca7f9 h1:vYYPlj1RNfR/8RXTE8ATWsguR25mVxktq3HNIUqPhyA= +github.com/qovery/qovery-client-go v0.0.0-20260610095547-986d768ca7f9/go.mod h1:mcXeQtxR4AIGIBaWLhy52S16UwL8/1fcDywDuSK1BZ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= From 6973c3019974dbc7804ee95f232eb0674ca4802c Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Wed, 10 Jun 2026 12:27:43 +0200 Subject: [PATCH 5/5] chore(QOV-1953): go mod tidy --- go.sum | 4 ---- 1 file changed, 4 deletions(-) diff --git a/go.sum b/go.sum index 191ef11f..0702a9f1 100644 --- a/go.sum +++ b/go.sum @@ -189,10 +189,6 @@ github.com/posthog/posthog-go v1.12.5 h1:l/x3mpqisXJ0sTOyyRutsTQAgiWYuJT1uhN4cQr github.com/posthog/posthog-go v1.12.5/go.mod h1:xsVOW9YImilUcazwPNEq4PJDqEZf2KeCS758zXjwkPg= github.com/pterm/pterm v0.12.83 h1:ie+YmGmA727VuhxBlyGr74Ks+7McV6kT99IB8EU80aA= github.com/pterm/pterm v0.12.83/go.mod h1:xlgc6bFWyJIMtmLJvGim+L7jhSReilOlOnodeIYe4Tk= -github.com/qovery/qovery-client-go v0.0.0-20260609072636-f548ebe903f2 h1:R6lG3dFH9/N7k7Hz+MMMCY1y3c0N9rmY6NAAjy1dkos= -github.com/qovery/qovery-client-go v0.0.0-20260609072636-f548ebe903f2/go.mod h1:mcXeQtxR4AIGIBaWLhy52S16UwL8/1fcDywDuSK1BZ4= -github.com/qovery/qovery-client-go v0.0.0-20260609115652-6441e57b57d0 h1:Aym0jIz4MHNx4NooERldZuedlRGDVj1z7gACCzgQ2ho= -github.com/qovery/qovery-client-go v0.0.0-20260609115652-6441e57b57d0/go.mod h1:mcXeQtxR4AIGIBaWLhy52S16UwL8/1fcDywDuSK1BZ4= github.com/qovery/qovery-client-go v0.0.0-20260610095547-986d768ca7f9 h1:vYYPlj1RNfR/8RXTE8ATWsguR25mVxktq3HNIUqPhyA= github.com/qovery/qovery-client-go v0.0.0-20260610095547-986d768ca7f9/go.mod h1:mcXeQtxR4AIGIBaWLhy52S16UwL8/1fcDywDuSK1BZ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=