From d964e772cca93c476e67f4573946605f4fe086e0 Mon Sep 17 00:00:00 2001 From: Javier Vela Date: Sun, 12 Jan 2025 17:14:58 +0100 Subject: [PATCH 1/3] feat: ske kubeconfig create merge kubeconfig into the default kubeconfig file Signed-off-by: Javier Vela --- docs/stackit_ske_kubeconfig.md | 2 +- docs/stackit_ske_kubeconfig_create.md | 16 +- internal/cmd/ske/kubeconfig/create/create.go | 25 +-- internal/pkg/services/ske/utils/utils.go | 41 +++++ internal/pkg/services/ske/utils/utils_test.go | 161 ++++++++++++++++-- 5 files changed, 207 insertions(+), 38 deletions(-) diff --git a/docs/stackit_ske_kubeconfig.md b/docs/stackit_ske_kubeconfig.md index 79d8f0381..5c7d3adf0 100644 --- a/docs/stackit_ske_kubeconfig.md +++ b/docs/stackit_ske_kubeconfig.md @@ -30,6 +30,6 @@ stackit ske kubeconfig [flags] ### SEE ALSO * [stackit ske](./stackit_ske.md) - Provides functionality for SKE -* [stackit ske kubeconfig create](./stackit_ske_kubeconfig_create.md) - Creates a kubeconfig for an SKE cluster +* [stackit ske kubeconfig create](./stackit_ske_kubeconfig_create.md) - Creates or update a kubeconfig for an SKE cluster * [stackit ske kubeconfig login](./stackit_ske_kubeconfig_login.md) - Login plugin for kubernetes clients diff --git a/docs/stackit_ske_kubeconfig_create.md b/docs/stackit_ske_kubeconfig_create.md index b63225e7e..d4b28df9b 100644 --- a/docs/stackit_ske_kubeconfig_create.md +++ b/docs/stackit_ske_kubeconfig_create.md @@ -1,14 +1,16 @@ ## stackit ske kubeconfig create -Creates a kubeconfig for an SKE cluster +Creates or update a kubeconfig for an SKE cluster ### Synopsis -Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster. +Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster, if the config exits in the kubeconfig file the information will be updated. -By default the kubeconfig is created in the .kube folder, in the user's home directory. The kubeconfig file will be overwritten if it already exists. +By default, the kubeconfig information of the SKE cluster is merged into the default kubeconfig file of the current user. If the kubeconfig file doesn't exist, a new one will be created. You can override this behavior by specifying a custom filepath with the --filepath flag. + An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 1h. + Note that the format is , e.g. 30d for 30 days and you can't combine units. ``` @@ -18,19 +20,19 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] ### Examples ``` - Create a kubeconfig for the SKE cluster with name "my-cluster" + Create a kubeconfig for the SKE cluster with name "my-cluster. If the config exits in the kubeconfig file the information will be updated." $ stackit ske kubeconfig create my-cluster Get a login kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command. $ stackit ske kubeconfig create my-cluster --login - Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days + Create o kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated. $ stackit ske kubeconfig create my-cluster --expiration 30d - Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months + Create or update a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months. If the config exits in the kubeconfig file the information will be updated. $ stackit ske kubeconfig create my-cluster --expiration 2M - Create a kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath + Create or update a kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath. If the config exits in the kubeconfig file the information will be updated. $ stackit ske kubeconfig create my-cluster --filepath /path/to/config Get a kubeconfig for the SKE cluster with name "my-cluster" without writing it to a file and format the output as json diff --git a/internal/cmd/ske/kubeconfig/create/create.go b/internal/cmd/ske/kubeconfig/create/create.go index be86251ab..64a47d52d 100644 --- a/internal/cmd/ske/kubeconfig/create/create.go +++ b/internal/cmd/ske/kubeconfig/create/create.go @@ -40,30 +40,30 @@ type inputModel struct { func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("create %s", clusterNameArg), - Short: "Creates a kubeconfig for an SKE cluster", + Short: "Creates or update a kubeconfig for an SKE cluster", Long: fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s", - "Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster.", - "By default the kubeconfig is created in the .kube folder, in the user's home directory. The kubeconfig file will be overwritten if it already exists.", - "You can override this behavior by specifying a custom filepath with the --filepath flag.", - "An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 1h.", + "Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster, if the config exits in the kubeconfig file the information will be updated.", + "By default, the kubeconfig information of the SKE cluster is merged into the default kubeconfig file of the current user. If the kubeconfig file doesn't exist, a new one will be created.", + "You can override this behavior by specifying a custom filepath with the --filepath flag.\n", + "An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 1h.\n", "Note that the format is , e.g. 30d for 30 days and you can't combine units."), Args: args.SingleArg(clusterNameArg, nil), Example: examples.Build( examples.NewExample( - `Create a kubeconfig for the SKE cluster with name "my-cluster"`, + `Create a kubeconfig for the SKE cluster with name "my-cluster. If the config exits in the kubeconfig file the information will be updated."`, "$ stackit ske kubeconfig create my-cluster"), examples.NewExample( `Get a login kubeconfig for the SKE cluster with name "my-cluster". `+ "This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command.", "$ stackit ske kubeconfig create my-cluster --login"), examples.NewExample( - `Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days`, + `Create o kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated.`, "$ stackit ske kubeconfig create my-cluster --expiration 30d"), examples.NewExample( - `Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months`, + `Create or update a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months. If the config exits in the kubeconfig file the information will be updated.`, "$ stackit ske kubeconfig create my-cluster --expiration 2M"), examples.NewExample( - `Create a kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath`, + `Create or update a kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath. If the config exits in the kubeconfig file the information will be updated.`, "$ stackit ske kubeconfig create my-cluster --filepath /path/to/config"), examples.NewExample( `Get a kubeconfig for the SKE cluster with name "my-cluster" without writing it to a file and format the output as json`, @@ -83,7 +83,7 @@ func NewCmd(p *print.Printer) *cobra.Command { } if !model.AssumeYes && !model.DisableWriting { - prompt := fmt.Sprintf("Are you sure you want to create a kubeconfig for SKE cluster %q? This will OVERWRITE your current kubeconfig file, if it exists.", model.ClusterName) + prompt := fmt.Sprintf("Are you sure you want to update your kubeconfig for SKE cluster %q? This will update your kubeconfig file. \n If it the kubeconfig file doesn´t exists, it will create a new one.", model.ClusterName) err = p.PromptForConfirmation(prompt) if err != nil { return err @@ -137,10 +137,11 @@ func NewCmd(p *print.Printer) *cobra.Command { } if !model.DisableWriting { - err = skeUtils.WriteConfigFile(kubeconfigPath, kubeconfig) + err = skeUtils.MergeKubeConfig(kubeconfigPath, kubeconfig) if err != nil { return fmt.Errorf("write kubeconfig file: %w", err) } + p.Outputf("\nSet kubectl context to %s with: kubectl config use-context %s\n", model.ClusterName, model.ClusterName) } return outputResult(p, model, kubeconfigPath, respKubeconfig, respLogin) @@ -260,7 +261,7 @@ func outputResult(p *print.Printer, model *inputModel, kubeconfigPath string, re if respKubeconfig != nil { expiration = fmt.Sprintf(", with expiration date %v (UTC)", *respKubeconfig.ExpirationTimestamp) } - p.Outputf("Created kubeconfig file for cluster %s in %q%s\n", model.ClusterName, kubeconfigPath, expiration) + p.Outputf("Updated kubeconfig file for cluster %s in %q%s\n", model.ClusterName, kubeconfigPath, expiration) return nil } diff --git a/internal/pkg/services/ske/utils/utils.go b/internal/pkg/services/ske/utils/utils.go index 366467643..af433b647 100644 --- a/internal/pkg/services/ske/utils/utils.go +++ b/internal/pkg/services/ske/utils/utils.go @@ -8,6 +8,8 @@ import ( "strconv" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "k8s.io/client-go/tools/clientcmd" + "github.com/stackitcloud/stackit-sdk-go/services/ske" "golang.org/x/mod/semver" ) @@ -228,6 +230,45 @@ func ConvertToSeconds(timeStr string) (*string, error) { return utils.Ptr(strconv.FormatUint(result, 10)), nil } +// Merge new Kubeconfig into existing Kubeconfig. If it doesn´t exits, creates a new one +func MergeKubeConfig(pathDestionationKubeConfig, contentNewKubeConfig string) error { + if contentNewKubeConfig == "" { + return fmt.Errorf("no data to merge. the new kubeconfig is empty") + } + + newConfig, err := clientcmd.Load([]byte(contentNewKubeConfig)) + if err != nil { + return fmt.Errorf("error loading new kubeconfig: %w", err) + } + + // if the destionation kubeconfig does not exist, create a new one + if _, err := os.Stat(pathDestionationKubeConfig); os.IsNotExist(err) { + return WriteConfigFile(pathDestionationKubeConfig, contentNewKubeConfig) + } + + existingConfig, err := clientcmd.LoadFromFile(pathDestionationKubeConfig) + if err != nil { + return fmt.Errorf("error loading existing kubeconfig: %w", err) + } + + for name, authInfo := range newConfig.AuthInfos { + existingConfig.AuthInfos[name] = authInfo + } + for name, context := range newConfig.Contexts { + existingConfig.Contexts[name] = context + } + for name, cluster := range newConfig.Clusters { + existingConfig.Clusters[name] = cluster + } + + err = clientcmd.WriteToFile(*existingConfig, pathDestionationKubeConfig) + if err != nil { + return fmt.Errorf("error writing merged kubeconfig: %w", err) + } + + return nil +} + // WriteConfigFile writes the given data to the given path. // The directory is created if it does not exist. func WriteConfigFile(configPath, data string) error { diff --git a/internal/pkg/services/ske/utils/utils_test.go b/internal/pkg/services/ske/utils/utils_test.go index c3efcfe88..d07207663 100644 --- a/internal/pkg/services/ske/utils/utils_test.go +++ b/internal/pkg/services/ske/utils/utils_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "k8s.io/client-go/tools/clientcmd" "github.com/google/go-cmp/cmp" "github.com/google/uuid" @@ -20,7 +21,47 @@ var ( ) const ( - testClusterName = "test-cluster" + testClusterName = "test-cluster" + existingKubeConfig = ` +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJSjFTZ1NWTjhnMmt3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRBeE1UQXhNakkxTlRSYUZ3MHpOVEF4TURneE1qTXdOVFJhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUM4ZXIwam1aS05STlR6Z2dCV3Q1cXMvaW94NXkxY2xBMHBGRHYwOWNmMGtmVGRVQWE3bmpqU0F2WlYKVFpsQlFFaW40Um9PTm1TZzdVMzVWN3FMSW56UVNmZXFuYi9wK05pODhDbkZvMThleUVnb3pHQklTTFpHK0EybQpuNFFEV3k3bVV1UUxFRnpjNjFpazdBQ0F5akZwRDlVdkdSdkxxVGJTQWcwYitYbktqbUUyWVgzTnRLbnJWOUN0CktrTG83K2JSa0MyemNkVnlraExhODhaR1BORUhjdVp2Uk0zQW5NclVGdGVvc0Fjb09xVW4xK09mYlhwUUlsTC8KKzBvRjcwN09Vc2tOUit0WEp4Z1VXL1R4Q0lONTYwU2E4eDVlWjB2VTZNR3ZOSTYwZ3h2S1lGL0pKa0pxU0NwNQovWWhpVmZ2QnNOSG5tVUZsNEdpOGFVMFNVTjRiQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJTUlkxVVhOamlMbFJLWktuSHJWRU55djA4aUp6QVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQVFaOVcwVFdvMQp4UFhPZU9xWHV6aFgzSkRoY0JVRkZyUVlOcHBMSmtqOWdUVm5Eck16b1dmeW9FQXRtT1ZQWURuTnEyTFhOSnpmClltd3RiUGxPemhGYkpWZVBWR0tLZktrUXZ1K3BhZGRtUHRhTzdUcnZqblRHeDhXczJadE5xK20wbkRGRUN4SDkKc1o2K1IycWhBUWNnSGdQWFZQdTdxSXFmbkNWRDkyeGprTE40c2JLZjRMb2x0R3hZbTBTWVZuY09rTFlBL3BvawpqTCsvODRJQXRrRXlEL21VdVF4MEsyVzFvVUM4dDRyMUlPZ3Y3OHZQMkRDRlBuZDVvbTJBM1dCNHY2dUFNZWc0Cnk3Y3FTcjBlSzJhNFQvMUtpTEdzYXI1V01ONTNwMjFiOGJMSTlISGNJMkh6c0tOdEdpNGFOT0hsWWkwUFgrUW0KT3U4NW4ycVdwSUxmCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://127.0.0.1:61274 + name: existing-cluster +contexts: +- context: + cluster: existing-cluster + user: existing-cluster + name: existing-cluster +current-context: existing-cluster +kind: Config +preferences: {} +users: +- name: existing-cluster + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJYWFEL3lTemlKM1F3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRBeE1UQXhNakkxTlRSYUZ3MHlOakF4TVRBeE1qTXdOVFJhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEQSs2WjkKWU02RC9DK2VNWnJQRHZoR0VIRk4zeDVXdFMrVWlsb1F3QkJBSXdUNXFQczVnSERWK1cyWjdjT3VGNVFEYlpyUQo3dktWSUtlWXQ0Mk9SZytYQktibHhDV1VpdFZDdmZZbHJYKzlaY0JGL2dFaVBjOE9aK2h0Q1pPNlgyZ3d0WVNOCkgwZ1lLOTlhOFRWUWxlWm9Eem93WlE0Um5aSjhkRGo1STA2blRjdkk3bDBlMWt3VnM5aXFLRHpyekRhYnhqb0EKamZkcUpiZTVkOFc0ZTloTTRBdVRUbFRkWmFVTWFnUHhyaWxEOU9mUXhaUmlReFIzNkhSOHZabm9TcndXeWh5ZApqall0TFQvcE00UXAybUU5NFJqVWE2ekNUVlJKeWduY3RHVnpDRi84RDc1TVU4OVhmVjltQVV5L3BoR1M5MDdjCjlXbzE4Um42TytHNHYwdFRBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkpGalZSYzJPSXVWRXBrcQpjZXRVUTNLL1R5SW5NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUMySVRwUlM1SlU3bGpkeDVRMlkwQzBkZG8yCm9PSmp5TUhVQXJ5ZTIyM2xOd1R1OTNHZXkvUjNIOHNpYWxDRURXdFR0cCsrY3BucW1ON05ia3UvWFI5SUlFdlIKYTNZS3VvbGdOTGtLaEtqMWQ0NVAxeEs0VE5CV1hSV2FMbksxcTdLVWxWWHp2bjdSN3RDY0NtNk90S3d4OUl2WgorRGhUU0pobFEzTVNmNXhjMUdOMm9qb0pPWmVlOXFNc3R1RzdPUVl1M08yUitYVUIwRHgzNnlPeFR2S0NBZ24xCm55Yk5FS0Nia1BmTXdvSU5aTm9iSWE3Y2VHcTdOMzRHaCs3Vi9iazUrQmhoTzVJRTRPeDYvUUxQc1B2ZGtOZHcKSkFyclQ3QytHSkF1UzNXQ2dYUXRyRWFyT3drWHhqajFPc3NuNjdMNlpONG01SkYzWHViSmdQUGZ3L2NECi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBd1B1bWZXRE9nL3d2bmpHYXp3NzRSaEJ4VGQ4ZVZyVXZsSXBhRU1BUVFDTUUrYWo3Ck9ZQncxZmx0bWUzRHJoZVVBMjJhME83eWxTQ25tTGVOamtZUGx3U201Y1FsbElyVlFyMzJKYTEvdldYQVJmNEIKSWozUERtZm9iUW1UdWw5b01MV0VqUjlJR0N2Zld2RTFVSlhtYUE4Nk1HVU9FWjJTZkhRNCtTTk9wMDNMeU81ZApIdFpNRmJQWXFpZzg2OHcybThZNkFJMzNhaVczdVhmRnVIdllUT0FMazA1VTNXV2xER29EOGE0cFEvVG4wTVdVCllrTVVkK2gwZkwyWjZFcThGc29jblk0MkxTMC82VE9FS2RwaFBlRVkxR3Vzd2sxVVNjb0ozTFJsY3doZi9BKysKVEZQUFYzMWZaZ0ZNdjZZUmt2ZE8zUFZxTmZFWitqdmh1TDlMVXdJREFRQUJBb0lCQVFDS0lWWFk5anE3VS8zTgpjRm9MalA1K1AvU3B0V01rMHdsY2UrN2RnR3ZoVEcrYU42NmlTT0g2OWs3UjE5S3hRS1VzRXY2MlArSVloY2dRClVvbWE1V0R4U2w0ZnBkYjBUSzg2MTNkaEhwK0pORlI4aE1QUSs0YkNHL1BNWUFlQ1poblFpNHgxNm9jUzdnd3cKTHVoblp2UUZWYWpqek9GV0VJQXlYb29OSVkyQng3bjlzRlBGYmZSK1NOVVhuWHNHemFkMlArVmIyTkFCUjRFLwp4K2dYWlhFKzFnU0RhK25ZVHBiaG1hd3hreStEQnZBQlRWTzlWY2J2ZWoybDZ2WjAwK2lMTm9rYjF1UmJmbzNECkdEN2RZTjRYdCtwWXRMdFJYRGNqb2Q3OXpFcmJ4UkE4ZWoxblllOFpXQUNZa0ZOT3lpRHlJY3dFbWtDNXhlcHAKS1ByRGVCeEpBb0dCQU5XYzI4cFY4SDhRWm1Hb25QQkNZUUNrY2NLYnpEaXpwa0ZKMlZNVXZ5TG1Ia0w5bWlWUApQb1RsdXF4T2htMHhyRlNRaEFTQUlUaG0rWHN0c0pYdjNSd2dIZVdadTluUEVPeWpRcG02bTNEa1ZVK29kdTRGCnYwa25qdlduUTRPZnVQeDlCV3UyN1I3d1VBNHBqNUk0MGtlMVovdDZwdzZjeWFBckZ5L01HODZmQW9HQkFPZEcKMXRocFNUT3dZbEltWWoxNDdTSVJyb0VaSTNSaUNBVUh1ME54VEJObk5WL1JNVDdaNGVpZkRMMndXc0s1Q1Y0aQpFR2hBODRxYVB0dTFCaVhwTmdpMDBBdllWUGN6d0VDa3hocFdBeTJVRGZSc2FENnNYQ04ycVdtcGdjQzBTOWpICkdqUkdnSVFselhWNFVVcHFTYzVEUDZBYUFzRkhxVU1aT3dRZTgyck5Bb0dCQU5FN2FLbml6Y09ZQzhDQ2lONXAKRmx5cnRtWVpkc3JmWk95MGFqT1BzYng4VEkzdm04b0p1Y0l3eDAwNVNVQ3hsQXZzMWZNV2tmT09JYlkreGFYSApvZnVIbGVFc1dTejZQcWliTFlRb25WTFJ4S0pXNzg4clAvZG0wUWZiZ3l6dENTUC9UWXo1UzMrdmdhcXRtTnh2CjNjQ3hkcDJEd1JoMkNLUmpNTDMzbmhFZkFvR0FDNmNRRUJ0TjZ1TEtNV1Zwc2JzMEIzRm9uMnlLMHNSVnJ4c3kKbmpWSkpma2ZRVktpN28yL3loNnBYNjFSQlZxWlZEclhKTW1RKzd6RnlnQVc3VFlRMk9OelVBVjRVblF6RFk2Lwp4SGZzOVJEdW14QVRPSVVxcDBiRlJtT1ovQUdaaUxTUFoyN2Q3c3FRelloZ1lDVjJ6b09vNHdJc2ZWeUU5TEtDCnZMUnFnMGtDZ1lFQXlJRUdjeHQxcTIwdUhYUTFLTU92V2xWUUJCQklPUUJjeXoyR0djcWFGOHhSKzJCOGc3R2YKbEh4dHBvaTNNQUxTVXlhOTQzZEpMUHA4Q0xSOTBkQWtqZ1JROURPN2wyYWlWYWVncTA0NURCMnBwN05YVlc4NgptUXFPZUJRYzcyY0ZYdk9YZmRKUUQwME5HZThlS0VjTWN2QlhxTVIrSUtEdGozcGlKVjlsSHpBPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=` + newKubeConfig = `apiVersion: v1 +clusters: + - cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJTjAvdmZkM3RCeGd3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRBeE1URXhOekV4TVRkYUZ3MHpOVEF4TURreE56RTJNVGRhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURJS25lRWZrM0F0WWlhanZyYWcwdU1zZUd1Q3BuZW80OXl3T0NFSmF0ZnVncVZXVXJ3cVd1WEdjUVgKTWp5MTZEVGxlR2YxeS83NXJuRUY1cld0Vm5wMDlNc0w1NW5YM0ZnT21SY3ozNmxtYTBOMmdMQU5RR0VmZU50NQpsa0Y5R2t6VFZMVy84alNWcXRkaTBCTm8xejEya0FCUm5yM1M0bWU0cExma0xFeWZKQTFQcnlpVUp0NnFBbldrCkUwV2RxbmJJMGRHQWZpZ3hTVFRZK09PMExWbjdJaG1QTGpPVEhHb0JRaW1DL091ZEZFK01FZG1kQkNOTHgzeE4KRDlSbk1taUxjVkVlSDlvVTFjYUdRamRIbXhnRUpJbStTOVdmWDZuRSsxOUpDZ0dkTS9KaFVtT0xRQWg4NzhMcQptc085WlNYdXFweW9ROTBhRDBDaFNNdzJyOXBQQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRMXRjTE5rMmVjRkFJRDl5citZMnUyaHI4OWJEQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQWdUZGJkTzZQNQo2M2hiZVRsS1E2UkpzRlkrdUdIeXcyMXNGU205Ni9vblZhOS91SjNQZ3BsMndKaFhpanZmZnNQamg2ekpkdTJXCll4WWkxcHdEWGZtMHpsNHJQMEcwQmkzL2Y1VkU0dkRnSmUwcDRKdkx2MWVmclZBcGhpakJiRkFHVTh6WVVPdEUKM2pGNy92ZDkvVUwxRWwzNVNRZjdEWWJhQ2NndzByS0tiNkQwaUZJcjJCRFZqbE01VDhqRzdETEk0a3pXTzFaTQpmNHh4ay9MQjBpY1R0a1RVRGQzcjBtZmFzNUdqR0lDR2QzbUpHbWY3bzFScXVyYlZ3dmVPWE5oL2tud2hnNGZqCitsTjJvaHpuaWdkTVNNQ1FnbDQ2NlowQTZvVDUrNUV6a2JwYS8yRDQ1cVN0ZGZBbTNtQ0RhdHdUelc1RlBudFMKMm0weVo2ZWVydkE4Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://127.0.0.1:55209 + name: my-new-super-ske-cluster +contexts: + - context: + cluster: my-new-super-ske-cluster + user: my-new-super-ske-cluster + name: my-new-super-ske-cluster +current-context: my-new-super-ske-cluster +kind: Config +preferences: {} +users: + - name: my-new-super-ske-cluster + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJUmpoS0w0dlJWSFV3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRBeE1URXhOekV4TVRkYUZ3MHlOakF4TVRFeE56RTJNVGRhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEVE5WSmEKekJHZWU4OXVRNjVZWEdhT1pwTWJTZE9tcWFyNUlVbkRTUEpMbHdJKzkyWVRrcFBKcXFncWEwa2FZYVdZUmFlTQpCNVlDeTRpNjNXSTBYYlgvMW9LNUFPZ2xXL1FwcGczWnc5K3ZPYXdtdEpqUHQ1T2xEVWRONGdmYm40TjV1OWpoCmltQ09wak5VL285NzNZZy9nM3pqNi9nUm9EYldhaW5wSDltTk1nOHFTS0xaNkNpUlp2VjZuYkgyVDVSa3ZVVWgKUDNWN09CZE1oUlp3MW1rVVRQVXY5T056VVBubFFaS3hwWXphYjBiZm92eFd6UDhxQkVIdk9xaXZoWFhaaGp1bApaTU1OMjYrN2RyS3lCWS8rRnBmeGpqb3AyZytUSlMxNHhhOTh0dCtqT3dUUkI5aWh1WUQzTnlVbEZXVjhiUG51CnJqSW52ckxVcjkvQzB2cmhBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkRXMXdzMlRaNXdVQWdQMwpLdjVqYTdhR3Z6MXNNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFZQkpld0ZwMTJnbkxQM1hGQ09JaXRZZWVnCkVmMjQwLysvaVFUUXQreHNjTU1ITGF4VjNFNEgxZ3JyNDdXUjE0bDdlbE1ING5qWnZzU3djSUZsa1RieVR6eW0KeW9XamhQQ0M2WWpzZHFEM2Vlc1ZpV2xhZkthczFrNmtmWHhVR2EvSUtQNzJoQ2tub2pia2o3amlSdjgrMTd5NgpKa2JIaXNYLzFqM2R1VHVIdDNORXJnNmNud0M5MGlldjZFZVFaV0oxaG5NSHhDMkRYMEdvOW14ZDlPYWFVODdBCkhBNDMzRnVJQWpoZjRWN2Vma3dGQU1ZMEhZSjZQaFZqTXdNWmdKczhLSHhVdjl3Y0xYMlFPUC9TSmhRZUtMV1UKYTFHTWlzTFBNc2NmL2JjU051SVpxMTR5S0xSelEwL1FIUW1PVVdSZDIva002MmxhbFl5Rlk2V0J4cCt3Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBMHpWU1dzd1JubnZQYmtPdVdGeG1qbWFURzBuVHBxbXErU0ZKdzBqeVM1Y0NQdmRtCkU1S1R5YXFvS210SkdtR2xtRVduakFlV0FzdUl1dDFpTkYyMS85YUN1UURvSlZ2MEthWU4yY1Bmcnptc0pyU1kKejdlVHBRMUhUZUlIMjUrRGVidlk0WXBnanFZelZQNlBlOTJJUDROODQrdjRFYUEyMW1vcDZSL1pqVElQS2tpaQoyZWdva1diMWVwMng5aytVWkwxRklUOTFlemdYVElVV2NOWnBGRXoxTC9UamMxRDU1VUdTc2FXTTJtOUczNkw4ClZzei9LZ1JCN3pxb3I0VjEyWVk3cFdURERkdXZ1M2F5c2dXUC9oYVg4WTQ2S2RvUGt5VXRlTVd2ZkxiZm96c0UKMFFmWW9ibUE5emNsSlJWbGZHejU3cTR5Sjc2eTFLL2Z3dEw2NFFJREFRQUJBb0lCQVFDRDBQV1RJV1dscWRQdQpGMk9LVmpEVGt3VWd0TlRaWVc4SmlWTUdCRkxrQmwwcWV6RkQ2ZWsrcGJuS3I2YXlSbHNaUysram4yQnFZaWoxCnB4R1JhU01iaHYrVEF4UGZyU0lYbEVGMHRhQzNOYUZSanNrSWFxUkZFS0o5NHlIUVdoK3VMQ1RScnBGUXRqMjMKUUNEQXg2UXZMNXNVak1NSURSdnNlZG1xVzJ4bGg4UkF5RUdYVi9sUmJ5ZTdEOTIrWVpwd21kV3dsa2tiZy8yTQowdHF1R1k0Qk1XTFY0K09DVlNmVWVEWU1nZkZIL0RVWThUdUIvNitzVm9rUnhLalhYbjYzN1c4Q2dJWUVaQngrCkE5TG8vYk1YN0RaSDRmS0RyRCsycVQ1SDNUTDFIc3BtSXJ1Mi84RllCZ08ySjNzZVdHdHdtelVXalVzL2ExSGoKdXZMamNCTjVBb0dCQU83YitESTBsdFRGT29MSERISGNZdXZqMTYydU96bk51ejNXa1R0Sng3QzZJSVpVd2YwSQpuM2pJWXhKRi9yVVZUZzZPbU5XNXpGdDA2QTVWQitwZ2RNblFhOHMybVNldFlKVW51eE42emRsOGJoblZ6dXUzCi8walM3cU1pWGg5aU8vRlZ6VDNxcnNqU1VnMmNCRTA3WlZweU0ycVNMUlkrVmdiRDg4aUdUbXhiQW9HQkFPSmQKWWVNc1JpVVZ5Wk5sZU1ra3puS2pjYXoyOE9Vb0NyZjd0dVhaYUpqRDdWZncyWmNBd0cvZG5lZ3M2YmEvck54bgplMXU3Rm05VlNTR2pNejJEaC9QdlNuQlZReGtQeHo1ZFRja2V0RUJSQk1XaVV1enI2UUFXdmZudEZXcWNZTkpvClBCVWY3c2k4Wk1rMjJpanR1OWxEVnRRUFpJdDZUMzJrb0Z3eHNrcHpBb0dBYjQ0c2pNWWk2NXh4aDBLUGZWNEEKbFVzRUlBbVBmNSttSTJ0aXlOM2NkWjE0TTBUQ2xQckNBQmNXcmlJaW8xQWY5SXlFdE16aHRKVVZEQnlLWmR4RwpyenE0SFdDU2h3Vmlaa2I0Q0ZFQ2N1QzZTemFnUFZiaDA1RXdBdUM2Tk00Y1VNcFI0T2tLV0tCaDBobGJxUFprCmo2bG1lZzlySDBoZHhTc2ZZRGZaeUtFQ2dZQnZZMVk4ekZlRC9qR2YxMG5WYU1neC94MTc2RlBuMzRsT3VZMXAKazA3MkJVdHdmN01DckRzRmtQOFg5YW5YNUgveVFQV2gwUEVjUGRKcnUvd0Y1QWh0VDYzSWt4d2VZL1krU1BseQo0eW45a0NDU0ErdGNiRVhPWm1KN2JsK2dnMnpkZks4OEVlZVZYYWNXb0dnL3hhUXZLQVM4K3dvVjNFenJYYXdQClVlRVM0d0tCZ1FEUm9QbXkvNloySUdERkRReWt3YmFMRDlvQlZqN3BJSTI0NmlLM1hwQmRtRGFVR0hLYnRiNmUKYXNYRWNQQmp0enYvTzVOM2dlZWFYREduaW5XcXJJZm1FTzIyMDhmQ0VCc0RWc3RQMDhxRnorekFSMnJEQm9xbQpFVkwxN0o0Q2J6Tlh4bStOT1R6aVhCN2tLVWhNQUFBbmkwcXQ1QXN0QlJpcENuMER4Y2JpekE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= +` ) type skeClientMocked struct { @@ -482,33 +523,34 @@ func TestConvertToSeconds(t *testing.T) { } } -func TestWriteConfigFile(t *testing.T) { +func TestMergeKubeConfig(t *testing.T) { tests := []struct { - description string - location string - kubeconfig string - isValid bool - isLocationDir bool - isLocationEmpty bool - expectedErr string + description string + location string + kubeconfig string + existingKubeconfig string + isValid bool + isLocationDir bool + isLocationEmpty bool + expectedErr string }{ { description: "base", location: filepath.Join("base", "config"), - kubeconfig: "kubeconfig", + kubeconfig: newKubeConfig, isValid: true, }, { description: "empty location", location: "", - kubeconfig: "kubeconfig", + kubeconfig: newKubeConfig, isValid: false, isLocationEmpty: true, }, { description: "path is only dir", location: "only_dir", - kubeconfig: "kubeconfig", + kubeconfig: newKubeConfig, isValid: false, isLocationDir: true, }, @@ -518,6 +560,20 @@ func TestWriteConfigFile(t *testing.T) { kubeconfig: "", isValid: false, }, + { + description: "kubeconfig bad content", + location: filepath.Join("empty", "config"), + existingKubeconfig: "hola", + kubeconfig: "kubeconfig", + isValid: false, + }, + { + description: "kubeconfig content", + location: filepath.Join("content", "config"), + kubeconfig: newKubeConfig, + existingKubeconfig: existingKubeConfig, + isValid: true, + }, } baseTestDir := "test_data/" @@ -527,27 +583,96 @@ func TestWriteConfigFile(t *testing.T) { // make sure empty case still works if tt.isLocationEmpty { testLocation = "" + } else if tt.existingKubeconfig != "" { + dir := filepath.Dir(testLocation) + + err := os.MkdirAll(dir, 0o700) + if err != nil { + t.Errorf("error create config directory: %s (%s)", dir, err.Error()) + } + + err = os.WriteFile(testLocation, []byte(tt.existingKubeconfig), 0o600) + if err != nil { + t.Errorf("could not write file: %s", tt.location) + } + defer func() { + err := os.Remove(testLocation) + if err != nil { + t.Errorf("could not deleete file: %s", tt.location) + } + }() } // filepath Join cleans trailing separators if tt.isLocationDir { testLocation += string(filepath.Separator) } - err := WriteConfigFile(testLocation, tt.kubeconfig) + + err := MergeKubeConfig(testLocation, tt.kubeconfig) if tt.isValid && err != nil { - t.Errorf("failed on valid input") + t.Errorf("failed on valid input %s", err) } + if !tt.isValid && err == nil { t.Errorf("did not fail on invalid input") } if tt.isValid { - data, err := os.ReadFile(testLocation) + kubeConfigFinal, err := clientcmd.LoadFromFile(testLocation) + if err != nil { + t.Errorf("error loading final kubeconfig: %s", err) + } + + kubeConfigNew, err := clientcmd.Load([]byte(tt.kubeconfig)) if err != nil { - t.Errorf("could not read file: %s", tt.location) + t.Errorf("error loading new kubeconfig: %s", err) } - if string(data) != tt.kubeconfig { - t.Errorf("expected file content to be %s, got %s", tt.kubeconfig, string(data)) + + // check new kubeconfig is still there + for name := range kubeConfigNew.AuthInfos { + _, exits := kubeConfigFinal.AuthInfos[name] + if !exits { + t.Errorf("the user %s does not exist in the final kubeconfig", name) + } + } + for name := range kubeConfigNew.Contexts { + _, exits := kubeConfigFinal.Contexts[name] + if !exits { + t.Errorf("the context %s does not exist in the final kubeconfig", name) + } + } + for name := range kubeConfigNew.Clusters { + _, exits := kubeConfigFinal.Clusters[name] + if !exits { + t.Errorf("the cluster %s does not exist in the final kubeconfig", name) + } + } + + if tt.existingKubeconfig != "" { + kubeConfigExisting, err := clientcmd.Load([]byte(tt.existingKubeconfig)) + if err != nil { + t.Errorf("error loading existing kubeconfig: %s", err) + } + + // check exiting kubeconfig is still there + for name := range kubeConfigExisting.AuthInfos { + _, exits := kubeConfigFinal.AuthInfos[name] + if !exits { + t.Errorf("the user %s does not exist in the final kubeconfig", name) + } + } + for name := range kubeConfigExisting.Contexts { + _, exits := kubeConfigFinal.Contexts[name] + if !exits { + t.Errorf("the context %s does not exist in the final kubeconfig", name) + } + } + for name := range kubeConfigExisting.Clusters { + _, exits := kubeConfigFinal.Clusters[name] + if !exits { + t.Errorf("the cluster %s does not exist in the final kubeconfig", name) + } + } } } }) From 198b3ed6f5d3476f271e2296a5de397703eb47d2 Mon Sep 17 00:00:00 2001 From: Javier Vela Date: Thu, 16 Jan 2025 18:27:17 +0100 Subject: [PATCH 2/3] feat: ske kubeconfig merge add flag overwrite Signed-off-by: Javier Vela --- docs/stackit_ske_kubeconfig_create.md | 6 +++- internal/cmd/ske/kubeconfig/create/create.go | 34 +++++++++++++------ .../cmd/ske/kubeconfig/create/create_test.go | 7 ++-- internal/pkg/services/ske/utils/utils.go | 13 +++---- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/stackit_ske_kubeconfig_create.md b/docs/stackit_ske_kubeconfig_create.md index d4b28df9b..a046c2905 100644 --- a/docs/stackit_ske_kubeconfig_create.md +++ b/docs/stackit_ske_kubeconfig_create.md @@ -20,7 +20,7 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] ### Examples ``` - Create a kubeconfig for the SKE cluster with name "my-cluster. If the config exits in the kubeconfig file the information will be updated." + Create or update a kubeconfig for the SKE cluster with name "my-cluster. If the config exits in the kubeconfig file the information will be updated." $ stackit ske kubeconfig create my-cluster Get a login kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command. @@ -37,6 +37,9 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] Get a kubeconfig for the SKE cluster with name "my-cluster" without writing it to a file and format the output as json $ stackit ske kubeconfig create my-cluster --disable-writing --output-format json + + Create a kubeconfig for the SKE cluster with name "my-cluster. It will OVERWRITE your current kubeconfig file." + $ stackit ske kubeconfig create my-cluster --overwrite true ``` ### Options @@ -47,6 +50,7 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] --filepath string Path to create the kubeconfig file. By default, the kubeconfig is created as 'config' in the .kube folder, in the user's home directory. -h, --help Help for "stackit ske kubeconfig create" -l, --login Create a login kubeconfig that obtains valid credentials via the STACKIT CLI. This flag is mutually exclusive with the expiration flag. + --overwrite Overwrite the kubeconfig file. ``` ### Options inherited from parent commands diff --git a/internal/cmd/ske/kubeconfig/create/create.go b/internal/cmd/ske/kubeconfig/create/create.go index 64a47d52d..846292aab 100644 --- a/internal/cmd/ske/kubeconfig/create/create.go +++ b/internal/cmd/ske/kubeconfig/create/create.go @@ -22,19 +22,21 @@ import ( const ( clusterNameArg = "CLUSTER_NAME" - loginFlag = "login" + disableWritingFlag = "disable-writing" expirationFlag = "expiration" filepathFlag = "filepath" - disableWritingFlag = "disable-writing" + loginFlag = "login" + overwrite = "overwrite" ) type inputModel struct { *globalflags.GlobalFlagModel ClusterName string - Filepath *string + DisableWriting bool ExpirationTime *string + Filepath *string Login bool - DisableWriting bool + Overwrite bool } func NewCmd(p *print.Printer) *cobra.Command { @@ -50,7 +52,7 @@ func NewCmd(p *print.Printer) *cobra.Command { Args: args.SingleArg(clusterNameArg, nil), Example: examples.Build( examples.NewExample( - `Create a kubeconfig for the SKE cluster with name "my-cluster. If the config exits in the kubeconfig file the information will be updated."`, + `Create or update a kubeconfig for the SKE cluster with name "my-cluster. If the config exits in the kubeconfig file the information will be updated."`, "$ stackit ske kubeconfig create my-cluster"), examples.NewExample( `Get a login kubeconfig for the SKE cluster with name "my-cluster". `+ @@ -68,6 +70,9 @@ func NewCmd(p *print.Printer) *cobra.Command { examples.NewExample( `Get a kubeconfig for the SKE cluster with name "my-cluster" without writing it to a file and format the output as json`, "$ stackit ske kubeconfig create my-cluster --disable-writing --output-format json"), + examples.NewExample( + `Create a kubeconfig for the SKE cluster with name "my-cluster. It will OVERWRITE your current kubeconfig file."`, + "$ stackit ske kubeconfig create my-cluster --overwrite true"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -83,7 +88,12 @@ func NewCmd(p *print.Printer) *cobra.Command { } if !model.AssumeYes && !model.DisableWriting { - prompt := fmt.Sprintf("Are you sure you want to update your kubeconfig for SKE cluster %q? This will update your kubeconfig file. \n If it the kubeconfig file doesn´t exists, it will create a new one.", model.ClusterName) + var prompt string + if model.Overwrite { + prompt = fmt.Sprintf("Are you sure you want to create a kubeconfig for SKE cluster %q? This will OVERWRITE your current kubeconfig file, if it exists.", model.ClusterName) + } else { + prompt = fmt.Sprintf("Are you sure you want to update your kubeconfig for SKE cluster %q? This will update your kubeconfig file. \n If it the kubeconfig file doesn't exists, it will create a new one.", model.ClusterName) + } err = p.PromptForConfirmation(prompt) if err != nil { return err @@ -137,7 +147,11 @@ func NewCmd(p *print.Printer) *cobra.Command { } if !model.DisableWriting { - err = skeUtils.MergeKubeConfig(kubeconfigPath, kubeconfig) + if model.Overwrite { + err = skeUtils.WriteConfigFile(kubeconfigPath, kubeconfig) + } else { + err = skeUtils.MergeKubeConfig(kubeconfigPath, kubeconfig) + } if err != nil { return fmt.Errorf("write kubeconfig file: %w", err) } @@ -152,11 +166,11 @@ func NewCmd(p *print.Printer) *cobra.Command { } func configureFlags(cmd *cobra.Command) { + cmd.Flags().Bool(disableWritingFlag, false, fmt.Sprintf("Disable the writing of kubeconfig. Set the output format to json or yaml using the --%s flag to display the kubeconfig.", globalflags.OutputFormatFlag)) cmd.Flags().BoolP(loginFlag, "l", false, "Create a login kubeconfig that obtains valid credentials via the STACKIT CLI. This flag is mutually exclusive with the expiration flag.") - cmd.Flags().StringP(expirationFlag, "e", "", "Expiration time for the kubeconfig in seconds(s), minutes(m), hours(h), days(d) or months(M). Example: 30d. By default, expiration time is 1h") cmd.Flags().String(filepathFlag, "", "Path to create the kubeconfig file. By default, the kubeconfig is created as 'config' in the .kube folder, in the user's home directory.") - cmd.Flags().Bool(disableWritingFlag, false, fmt.Sprintf("Disable the writing of kubeconfig. Set the output format to json or yaml using the --%s flag to display the kubeconfig.", globalflags.OutputFormatFlag)) - + cmd.Flags().StringP(expirationFlag, "e", "", "Expiration time for the kubeconfig in seconds(s), minutes(m), hours(h), days(d) or months(M). Example: 30d. By default, expiration time is 1h") + cmd.Flags().Bool(overwrite, false, "Overwrite the kubeconfig file.") cmd.MarkFlagsMutuallyExclusive(loginFlag, expirationFlag) } diff --git a/internal/cmd/ske/kubeconfig/create/create_test.go b/internal/cmd/ske/kubeconfig/create/create_test.go index bae59d3bb..c265f8ae8 100644 --- a/internal/cmd/ske/kubeconfig/create/create_test.go +++ b/internal/cmd/ske/kubeconfig/create/create_test.go @@ -4,13 +4,12 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-sdk-go/services/ske" ) diff --git a/internal/pkg/services/ske/utils/utils.go b/internal/pkg/services/ske/utils/utils.go index af433b647..8834a54a3 100644 --- a/internal/pkg/services/ske/utils/utils.go +++ b/internal/pkg/services/ske/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "context" "fmt" + "maps" "os" "path/filepath" "strconv" @@ -251,15 +252,9 @@ func MergeKubeConfig(pathDestionationKubeConfig, contentNewKubeConfig string) er return fmt.Errorf("error loading existing kubeconfig: %w", err) } - for name, authInfo := range newConfig.AuthInfos { - existingConfig.AuthInfos[name] = authInfo - } - for name, context := range newConfig.Contexts { - existingConfig.Contexts[name] = context - } - for name, cluster := range newConfig.Clusters { - existingConfig.Clusters[name] = cluster - } + maps.Copy(existingConfig.AuthInfos, newConfig.AuthInfos) + maps.Copy(existingConfig.Contexts, newConfig.Contexts) + maps.Copy(existingConfig.Clusters, newConfig.Clusters) err = clientcmd.WriteToFile(*existingConfig, pathDestionationKubeConfig) if err != nil { From c64874e7d89da259cd45483ab10feae36d061d77 Mon Sep 17 00:00:00 2001 From: Javier Vela Date: Fri, 17 Jan 2025 19:03:06 +0100 Subject: [PATCH 3/3] feat: ske kubeconfig merge. fix comments pr Signed-off-by: Javier Vela --- docs/stackit_ske_kubeconfig_create.md | 2 +- internal/cmd/ske/kubeconfig/create/create.go | 15 +++++++------ .../cmd/ske/kubeconfig/create/create_test.go | 22 +++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/docs/stackit_ske_kubeconfig_create.md b/docs/stackit_ske_kubeconfig_create.md index a046c2905..d3d0e5622 100644 --- a/docs/stackit_ske_kubeconfig_create.md +++ b/docs/stackit_ske_kubeconfig_create.md @@ -26,7 +26,7 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] Get a login kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command. $ stackit ske kubeconfig create my-cluster --login - Create o kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated. + Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated. $ stackit ske kubeconfig create my-cluster --expiration 30d Create or update a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months. If the config exits in the kubeconfig file the information will be updated. diff --git a/internal/cmd/ske/kubeconfig/create/create.go b/internal/cmd/ske/kubeconfig/create/create.go index 846292aab..4d2549e04 100644 --- a/internal/cmd/ske/kubeconfig/create/create.go +++ b/internal/cmd/ske/kubeconfig/create/create.go @@ -26,7 +26,7 @@ const ( expirationFlag = "expiration" filepathFlag = "filepath" loginFlag = "login" - overwrite = "overwrite" + overwriteFlag = "overwrite" ) type inputModel struct { @@ -59,7 +59,7 @@ func NewCmd(p *print.Printer) *cobra.Command { "This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command.", "$ stackit ske kubeconfig create my-cluster --login"), examples.NewExample( - `Create o kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated.`, + `Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated.`, "$ stackit ske kubeconfig create my-cluster --expiration 30d"), examples.NewExample( `Create or update a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months. If the config exits in the kubeconfig file the information will be updated.`, @@ -92,7 +92,7 @@ func NewCmd(p *print.Printer) *cobra.Command { if model.Overwrite { prompt = fmt.Sprintf("Are you sure you want to create a kubeconfig for SKE cluster %q? This will OVERWRITE your current kubeconfig file, if it exists.", model.ClusterName) } else { - prompt = fmt.Sprintf("Are you sure you want to update your kubeconfig for SKE cluster %q? This will update your kubeconfig file. \n If it the kubeconfig file doesn't exists, it will create a new one.", model.ClusterName) + prompt = fmt.Sprintf("Are you sure you want to update your kubeconfig for SKE cluster %q? This will update your kubeconfig file. \nIf it the kubeconfig file doesn't exists, it will create a new one.", model.ClusterName) } err = p.PromptForConfirmation(prompt) if err != nil { @@ -170,7 +170,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().BoolP(loginFlag, "l", false, "Create a login kubeconfig that obtains valid credentials via the STACKIT CLI. This flag is mutually exclusive with the expiration flag.") cmd.Flags().String(filepathFlag, "", "Path to create the kubeconfig file. By default, the kubeconfig is created as 'config' in the .kube folder, in the user's home directory.") cmd.Flags().StringP(expirationFlag, "e", "", "Expiration time for the kubeconfig in seconds(s), minutes(m), hours(h), days(d) or months(M). Example: 30d. By default, expiration time is 1h") - cmd.Flags().Bool(overwrite, false, "Overwrite the kubeconfig file.") + cmd.Flags().Bool(overwriteFlag, false, "Overwrite the kubeconfig file.") cmd.MarkFlagsMutuallyExclusive(loginFlag, expirationFlag) } @@ -204,12 +204,13 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } model := inputModel{ - GlobalFlagModel: globalFlags, ClusterName: clusterName, - Filepath: flags.FlagToStringPointer(p, cmd, filepathFlag), + DisableWriting: disableWriting, ExpirationTime: expTime, + Filepath: flags.FlagToStringPointer(p, cmd, filepathFlag), + GlobalFlagModel: globalFlags, Login: flags.FlagToBoolValue(p, cmd, loginFlag), - DisableWriting: disableWriting, + Overwrite: flags.FlagToBoolValue(p, cmd, overwriteFlag), } if p.IsVerbosityDebug() { diff --git a/internal/cmd/ske/kubeconfig/create/create_test.go b/internal/cmd/ske/kubeconfig/create/create_test.go index c265f8ae8..85cfe1560 100644 --- a/internal/cmd/ske/kubeconfig/create/create_test.go +++ b/internal/cmd/ske/kubeconfig/create/create_test.go @@ -176,6 +176,28 @@ func TestParseInput(t *testing.T) { }), isValid: true, }, + { + description: "enable overwrite", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[overwriteFlag] = "true" + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Overwrite = true + }), + isValid: true, + }, + { + description: "disable overwrite", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[overwriteFlag] = "false" + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Overwrite = false + }), + isValid: true, + }, } for _, tt := range tests {