Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/cluster_upgrade_to_next_kubernetes_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ var clusterUpgradeCmd = &cobra.Command{
utils.PrintlnError(err)
}

if utils.IsTerminalClusterState(*status.Status) {
if utils.IsTerminalClusterState(status.Status) {
break
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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-20260512064301-eef39158fba8
github.com/qovery/qovery-client-go v0.0.0-20260609072636-f548ebe903f2
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ 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-20260512064301-eef39158fba8 h1:jk/MDhyGR91u46XOoxDFnu2pL7mB3dLCpU9vLy62vjE=
github.com/qovery/qovery-client-go v0.0.0-20260512064301-eef39158fba8/go.mod h1:mcXeQtxR4AIGIBaWLhy52S16UwL8/1fcDywDuSK1BZ4=
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/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=
Expand Down
10 changes: 5 additions & 5 deletions pkg/admin_cluster_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ func (service AdminClusterBatchDeployServiceImpl) Deploy(clusters []ClusterDetai
}

// Trigger a deployment only when the target status is in terminal state
if utils.IsTerminalClusterState(*clusterStatus.Status) {
if utils.IsTerminalClusterState(clusterStatus.Status) {
utils.Println(fmt.Sprintf("[Organization '%s' - Cluster '%s'] - Starting deployment - https://console.qovery.com/organization/%s/cluster/%s/cluster-logs", cluster.OrganizationName, cluster.ClusterName, cluster.OrganizationId, cluster.ClusterId))
var err error
if service.UpgradeClusterNewK8sVersion != nil {
Expand All @@ -573,7 +573,7 @@ func (service AdminClusterBatchDeployServiceImpl) Deploy(clusters []ClusterDetai
cluster.CurrentStatus = "DEPLOYING"
currentDeployingClustersByClusterId[cluster.ClusterId] = cluster
} else {
status := fmt.Sprintf("%v", *clusterStatus.Status) // only solution to get the underlying enum's string value
status := fmt.Sprintf("%v", clusterStatus.Status) // only solution to get the underlying enum's string value
utils.Println(fmt.Sprintf("[Organization '%s' - Cluster '%s'] - Cluster's state is '%s' (not a terminal state), sending it to waiting queue to be processed later", cluster.OrganizationName, cluster.ClusterName, status))
pendingClusters = append(pendingClusters, cluster)
}
Expand Down Expand Up @@ -607,11 +607,11 @@ func (service AdminClusterBatchDeployServiceImpl) Deploy(clusters []ClusterDetai
}

// set cluster status
status := fmt.Sprintf("%v", *clusterStatus.Status) // only solution to get the underlying enum's string value
status := fmt.Sprintf("%v", clusterStatus.Status) // only solution to get the underlying enum's string value
cluster.CurrentStatus = status
// Mark the deployment as finished only if terminal state OR status is "INTERNAL_ERROR" (specific case)
if utils.IsTerminalClusterState(*clusterStatus.Status) || cluster.CurrentStatus == "INTERNAL_ERROR" {
utils.Println(fmt.Sprintf("[Organization '%s' - Cluster '%s'] - Cluster deployed with '%s' status ", cluster.OrganizationName, cluster.ClusterName, *clusterStatus.Status))
if utils.IsTerminalClusterState(clusterStatus.Status) || cluster.CurrentStatus == "INTERNAL_ERROR" {
utils.Println(fmt.Sprintf("[Organization '%s' - Cluster '%s'] - Cluster deployed with '%s' status ", cluster.OrganizationName, cluster.ClusterName, clusterStatus.Status))

processedClusters = append(processedClusters, cluster)
clustersToRemoveFromMap = append(clustersToRemoveFromMap, clusterId)
Expand Down
66 changes: 65 additions & 1 deletion pkg/admin_load_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"os"
"os/exec"
"strings"

"github.com/go-jose/go-jose/v4/json"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -57,9 +58,18 @@ func LoadCredentials(clusterId string, doNotConnectToBastion bool) error {
if err := os.Setenv("KUBECONFIG", filePath); err != nil {
return fmt.Errorf("failed to set KUBECONFIG: %w", err)
}
if kubeconfigRequiresQoveryCommand(kubeconfig) {
if _, err := exec.LookPath("qovery"); err != nil {
utils.PrintlnInfo(fmt.Sprintf("KUBECONFIG uses qovery as an exec credential command, but qovery was not found in PATH: %v", err))
}
}
return StartChildShell()
}

func kubeconfigRequiresQoveryCommand(kubeconfig string) bool {
return strings.Contains(kubeconfig, "command: qovery")
}

func StartChildShell() error {
// Get the user's default shell
shell := os.Getenv("SHELL")
Expand Down Expand Up @@ -137,7 +147,12 @@ func getClusterCredentials(clusterId string) []utils.Var {
log.Fatal(err)
}

return clusterCredentialsFromPayload(clusterId, payload)
}

func clusterCredentialsFromPayload(clusterId string, payload map[string]string) []utils.Var {
var clusterCreds []utils.Var
isGcpPayload := isGcpCredentialsPayload(payload)
for key, value := range payload {
switch key {
case "access_key_id":
Expand All @@ -147,7 +162,13 @@ func getClusterCredentials(clusterId string) []utils.Var {
case "aws_session_token":
clusterCreds = append(clusterCreds, utils.Var{Key: "AWS_SESSION_TOKEN", Value: value})
case "region":
clusterCreds = append(clusterCreds, utils.Var{Key: "AWS_DEFAULT_REGION", Value: value})
if isGcpPayload {
if _, hasGcpRegion := payload["gcp_region"]; !hasGcpRegion {
clusterCreds = appendGcpRegionVars(clusterCreds, value)
}
} else {
clusterCreds = append(clusterCreds, utils.Var{Key: "AWS_DEFAULT_REGION", Value: value})
}
case "scaleway_access_key":
clusterCreds = append(clusterCreds, utils.Var{Key: "SCW_ACCESS_KEY", Value: value})
case "scaleway_secret_key":
Expand All @@ -161,7 +182,50 @@ func getClusterCredentials(clusterId string) []utils.Var {

clusterCreds = append(clusterCreds, utils.Var{Key: "CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE", Value: filepath})
clusterCreds = append(clusterCreds, utils.Var{Key: "GOOGLE_CREDENTIALS", Value: value})
case "gcp_access_token":
filepath := utils.WriteInFile(clusterId, "google_access_token", []byte(value))

clusterCreds = append(clusterCreds, utils.Var{Key: "GOOGLE_OAUTH_ACCESS_TOKEN", Value: value})
clusterCreds = append(clusterCreds, utils.Var{Key: "CLOUDSDK_AUTH_ACCESS_TOKEN_FILE", Value: filepath})
case "gcp_project_id":
clusterCreds = appendGcpProjectVars(clusterCreds, value)
case "gcp_region":
clusterCreds = appendGcpRegionVars(clusterCreds, value)
case "gcp_access_token_expiration":
clusterCreds = append(clusterCreds, utils.Var{Key: "GOOGLE_OAUTH_ACCESS_TOKEN_EXPIRATION", Value: value})
case "gcp_credentials_type":
clusterCreds = append(clusterCreds, utils.Var{Key: "GCP_CREDENTIALS_TYPE", Value: value})
}
}
return clusterCreds
}

func appendGcpProjectVars(clusterCreds []utils.Var, projectId string) []utils.Var {
clusterCreds = append(clusterCreds, utils.Var{Key: "GOOGLE_PROJECT", Value: projectId})
clusterCreds = append(clusterCreds, utils.Var{Key: "GOOGLE_CLOUD_PROJECT", Value: projectId})
clusterCreds = append(clusterCreds, utils.Var{Key: "CLOUDSDK_CORE_PROJECT", Value: projectId})
return clusterCreds
}

func appendGcpRegionVars(clusterCreds []utils.Var, region string) []utils.Var {
clusterCreds = append(clusterCreds, utils.Var{Key: "GOOGLE_REGION", Value: region})
clusterCreds = append(clusterCreds, utils.Var{Key: "CLOUDSDK_COMPUTE_REGION", Value: region})
return clusterCreds
}

func isGcpCredentialsPayload(payload map[string]string) bool {
gcpKeys := []string{
"json_credentials",
"gcp_access_token",
"gcp_project_id",
"gcp_region",
"gcp_access_token_expiration",
"gcp_credentials_type",
}
for _, key := range gcpKeys {
if _, ok := payload[key]; ok {
return true
}
}
return false
}
84 changes: 84 additions & 0 deletions pkg/admin_load_credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package pkg

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/qovery/qovery-cli/utils"
)

func TestClusterCredentialsFromPayload(t *testing.T) {
t.Run("Should map legacy GCP json credentials", func(t *testing.T) {
// given
clusterId := "test-legacy-gcp"
payload := map[string]string{
"json_credentials": "base64-json",
}

// when
credentials := clusterCredentialsFromPayload(clusterId, payload)

// then
assert.Contains(t, credentials, utils.Var{Key: "GOOGLE_CREDENTIALS", Value: "base64-json"})
assert.Contains(t, credentials, utils.Var{Key: "CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE", Value: "/tmp/qovery_test-legacy-gcp/google_creds.json"})
})

t.Run("Should map GCP workload identity federation access token credentials", func(t *testing.T) {
// given
payload := map[string]string{
"gcp_access_token": "access-token",
"gcp_project_id": "project-id",
"gcp_region": "europe-west1",
"gcp_access_token_expiration": "2026-06-08T17:00:00Z",
"gcp_credentials_type": "WORKLOAD_IDENTITY_FEDERATION",
}

// when
credentials := clusterCredentialsFromPayload("test-wif-gcp", payload)

// then
assert.Contains(t, credentials, utils.Var{Key: "GOOGLE_OAUTH_ACCESS_TOKEN", Value: "access-token"})
assert.Contains(t, credentials, utils.Var{Key: "CLOUDSDK_AUTH_ACCESS_TOKEN_FILE", Value: "/tmp/qovery_test-wif-gcp/google_access_token"})
assert.Contains(t, credentials, utils.Var{Key: "GOOGLE_PROJECT", Value: "project-id"})
assert.Contains(t, credentials, utils.Var{Key: "GOOGLE_CLOUD_PROJECT", Value: "project-id"})
assert.Contains(t, credentials, utils.Var{Key: "CLOUDSDK_CORE_PROJECT", Value: "project-id"})
assert.Contains(t, credentials, utils.Var{Key: "GOOGLE_REGION", Value: "europe-west1"})
assert.Contains(t, credentials, utils.Var{Key: "CLOUDSDK_COMPUTE_REGION", Value: "europe-west1"})
assert.Contains(t, credentials, utils.Var{Key: "GOOGLE_OAUTH_ACCESS_TOKEN_EXPIRATION", Value: "2026-06-08T17:00:00Z"})
assert.Contains(t, credentials, utils.Var{Key: "GCP_CREDENTIALS_TYPE", Value: "WORKLOAD_IDENTITY_FEDERATION"})
})

t.Run("Should not map generic region to AWS_DEFAULT_REGION for GCP credentials", func(t *testing.T) {
// given
payload := map[string]string{
"gcp_access_token": "access-token",
"gcp_project_id": "project-id",
"gcp_credentials_type": "WORKLOAD_IDENTITY_FEDERATION",
"region": "europe-west9",
}

// when
credentials := clusterCredentialsFromPayload("test-wif-gcp-region", payload)

// then
assert.Contains(t, credentials, utils.Var{Key: "GOOGLE_REGION", Value: "europe-west9"})
assert.Contains(t, credentials, utils.Var{Key: "CLOUDSDK_COMPUTE_REGION", Value: "europe-west9"})
assert.NotContains(t, credentials, utils.Var{Key: "AWS_DEFAULT_REGION", Value: "europe-west9"})
})

t.Run("Should keep mapping generic region to AWS_DEFAULT_REGION for AWS credentials", func(t *testing.T) {
// given
payload := map[string]string{
"access_key_id": "access-key",
"secret_access_key": "secret-key",
"region": "eu-west-3",
}

// when
credentials := clusterCredentialsFromPayload("test-aws", payload)

// then
assert.Contains(t, credentials, utils.Var{Key: "AWS_DEFAULT_REGION", Value: "eu-west-3"})
})
}
22 changes: 16 additions & 6 deletions pkg/cluster/cluster_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ func MockListClusters(organization *qovery.Organization, clusters []qovery.Clust

func MockDeployCluster(organization *qovery.Organization, cluster *qovery.Cluster, clusterState *qovery.ClusterStateEnum) {
var clusterStatus = qovery.ClusterStatus{
ClusterId: &cluster.Id,
Status: clusterState,
ClusterId: cluster.Id,
Status: clusterStateOrDefault(clusterState),
Reason: qovery.DEPLOYMENTINFRAREASON_UNSPECIFIED,
}
var url = fmt.Sprint("https://api.qovery.com/organization/", organization.Id, "/cluster/", cluster.Id, "/deploy")
httpmock.RegisterResponder("POST", url,
Expand All @@ -49,8 +50,9 @@ func MockDeployCluster(organization *qovery.Organization, cluster *qovery.Cluste

func MockStopCluster(organization *qovery.Organization, cluster *qovery.Cluster, clusterState *qovery.ClusterStateEnum) {
var clusterStatus = qovery.ClusterStatus{
ClusterId: &cluster.Id,
Status: clusterState,
ClusterId: cluster.Id,
Status: clusterStateOrDefault(clusterState),
Reason: qovery.DEPLOYMENTINFRAREASON_UNSPECIFIED,
}
var url = fmt.Sprint("https://api.qovery.com/organization/", organization.Id, "/cluster/", cluster.Id, "/stop")
httpmock.RegisterResponder("POST", url,
Expand All @@ -65,8 +67,9 @@ func MockStopCluster(organization *qovery.Organization, cluster *qovery.Cluster,

func MockGetClusterStatus(organization *qovery.Organization, cluster *qovery.Cluster, clusterState *qovery.ClusterStateEnum) {
var clusterStatus = qovery.ClusterStatus{
ClusterId: &cluster.Id,
Status: clusterState,
ClusterId: cluster.Id,
Status: clusterStateOrDefault(clusterState),
Reason: qovery.DEPLOYMENTINFRAREASON_UNSPECIFIED,
}
var url = fmt.Sprint("https://api.qovery.com/organization/", organization.Id, "/cluster/", cluster.Id, "/status")
httpmock.RegisterResponder("GET", url,
Expand All @@ -79,6 +82,13 @@ func MockGetClusterStatus(organization *qovery.Organization, cluster *qovery.Clu
})
}

func clusterStateOrDefault(clusterState *qovery.ClusterStateEnum) qovery.ClusterStateEnum {
if clusterState == nil {
return qovery.CLUSTERSTATEENUM_DEPLOYED
}
return *clusterState
}

func MockCreateCluster(organization *qovery.Organization) {
var url = fmt.Sprint("https://api.qovery.com/organization/", organization.Id, "/cluster")
httpmock.RegisterResponder("POST", url,
Expand Down
4 changes: 2 additions & 2 deletions pkg/cluster/cluster_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (service *ClusterServiceImpl) DeployCluster(organizationName string, cluste
return err
}

if utils.IsTerminalClusterState(*status.Status) {
if utils.IsTerminalClusterState(status.Status) {
break
}

Expand Down Expand Up @@ -124,7 +124,7 @@ func (service *ClusterServiceImpl) StopCluster(organizationName string, clusterN
return err
}

if utils.IsTerminalClusterState(*status.Status) {
if utils.IsTerminalClusterState(status.Status) {
break
}

Expand Down
Loading
Loading