Skip to content

feat: ske kubeconfig create. merge kubeconfig into the default kubeconfig file #556 #557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 21, 2025
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 docs/stackit_ske_kubeconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

20 changes: 13 additions & 7 deletions docs/stackit_ske_kubeconfig_create.md
Original file line number Diff line number Diff line change
@@ -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 <value><unit>, e.g. 30d for 30 days and you can't combine units.

```
Expand All @@ -18,23 +20,26 @@ stackit ske kubeconfig create CLUSTER_NAME [flags]
### Examples

```
Create a kubeconfig for the SKE cluster with name "my-cluster"
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.
$ 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 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 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
$ 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
Expand All @@ -45,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
Expand Down
60 changes: 38 additions & 22 deletions internal/cmd/ske/kubeconfig/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,52 +22,57 @@ import (
const (
clusterNameArg = "CLUSTER_NAME"

loginFlag = "login"
disableWritingFlag = "disable-writing"
expirationFlag = "expiration"
filepathFlag = "filepath"
disableWritingFlag = "disable-writing"
loginFlag = "login"
overwriteFlag = "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 {
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 <value><unit>, 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 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". `+
"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 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 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`,
"$ 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()
Expand All @@ -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 create a kubeconfig for SKE cluster %q? This will OVERWRITE your current kubeconfig file, if it exists.", 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. \nIf it the kubeconfig file doesn't exists, it will create a new one.", model.ClusterName)
}
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
Expand Down Expand Up @@ -137,10 +147,15 @@ func NewCmd(p *print.Printer) *cobra.Command {
}

if !model.DisableWriting {
err = skeUtils.WriteConfigFile(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)
}
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)
Expand All @@ -151,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(overwriteFlag, false, "Overwrite the kubeconfig file.")
cmd.MarkFlagsMutuallyExclusive(loginFlag, expirationFlag)
}

Expand Down Expand Up @@ -189,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() {
Expand Down Expand Up @@ -260,7 +276,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
}
Expand Down
29 changes: 25 additions & 4 deletions internal/cmd/ske/kubeconfig/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -177,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 {
Expand Down
36 changes: 36 additions & 0 deletions internal/pkg/services/ske/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package utils
import (
"context"
"fmt"
"maps"
"os"
"path/filepath"
"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"
)
Expand Down Expand Up @@ -228,6 +231,39 @@ 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)
}

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 {
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 {
Expand Down
Loading
Loading