Skip to content

Commit

Permalink
feat(k8s): rework kubeconfig handling
Browse files Browse the repository at this point in the history
  • Loading branch information
jtherin committed Sep 19, 2024
1 parent 8ef81e1 commit 710551c
Show file tree
Hide file tree
Showing 21 changed files with 9,374 additions and 5,671 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ EXAMPLES:
scw k8s kubeconfig get 11111111-1111-1111-1111-111111111111

ARGS:
cluster-id Cluster ID from which to retrieve the kubeconfig
[region=fr-par] Region to target. If none is passed will use default region from the config
cluster-id Cluster ID from which to retrieve the kubeconfig
[auth-method=legacy] Which method to use to authenticate using kubelet (legacy | cli | copy-token)
[region=fr-par] Region to target. If none is passed will use default region from the config

FLAGS:
-h, --help help for get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ EXAMPLES:
ARGS:
cluster-id Cluster ID from which to retrieve the kubeconfig
[keep-current-context] Whether or not to keep the current kubeconfig context unmodified
[auth-method=legacy] Which method to use to authenticate using kubelet (legacy | cli | copy-token)
[region=fr-par] Region to target. If none is passed will use default region from the config

FLAGS:
Expand Down
2 changes: 2 additions & 0 deletions docs/commands/k8s.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ scw k8s kubeconfig get <cluster-id ...> [arg=value ...]
| Name | | Description |
|------|---|-------------|
| cluster-id | Required | Cluster ID from which to retrieve the kubeconfig |
| auth-method | Default: `legacy`<br />One of: `legacy`, `cli`, `copy-token` | Which method to use to authenticate using kubelet |
| region | Default: `fr-par` | Region to target. If none is passed will use default region from the config |


Expand Down Expand Up @@ -642,6 +643,7 @@ scw k8s kubeconfig install <cluster-id ...> [arg=value ...]
|------|---|-------------|
| cluster-id | Required | Cluster ID from which to retrieve the kubeconfig |
| keep-current-context | | Whether or not to keep the current kubeconfig context unmodified |
| auth-method | Default: `legacy`<br />One of: `legacy`, `cli`, `copy-token` | Which method to use to authenticate using kubelet |
| region | Default: `fr-par` | Region to target. If none is passed will use default region from the config |


Expand Down
26 changes: 26 additions & 0 deletions internal/namespaces/k8s/v1/custom.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package k8s

import (
"context"
"errors"

"github.com/scaleway/scaleway-cli/v2/core"
"github.com/scaleway/scaleway-cli/v2/internal/human"
k8s "github.com/scaleway/scaleway-sdk-go/api/k8s/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

// GetCommands returns cluster commands.
Expand Down Expand Up @@ -49,3 +53,25 @@ func GetCommands() *core.Commands {

return cmds
}

func SecretKey(ctx context.Context) (string, error) {
config, _ := scw.LoadConfigFromPath(core.ExtractConfigPath(ctx))
profileName := core.ExtractProfileName(ctx)

switch {
// Environment variable check
case core.ExtractEnv(ctx, scw.ScwSecretKeyEnv) != "":
return core.ExtractEnv(ctx, scw.ScwSecretKeyEnv), nil
// There is no config file
case config == nil:
return "", errors.New("config not provided")
// Config file with profile name
case config.Profiles[profileName] != nil && config.Profiles[profileName].SecretKey != nil:
return *config.Profiles[profileName].SecretKey, nil
// Default config
case config.Profile.SecretKey != nil:
return *config.Profile.SecretKey, nil
}

return "", errors.New("unable to find secret key")
}
24 changes: 3 additions & 21 deletions internal/namespaces/k8s/v1/custom_execcredentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ package k8s
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"

"github.com/scaleway/scaleway-cli/v2/core"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/scaleway-sdk-go/validation"
)

Expand All @@ -28,25 +26,9 @@ func k8sExecCredentialCommand() *core.Command {
}

func k8sExecCredentialRun(ctx context.Context, _ interface{}) (i interface{}, e error) {
config, _ := scw.LoadConfigFromPath(core.ExtractConfigPath(ctx))
profileName := core.ExtractProfileName(ctx)

var token string
switch {
// Environment variable check
case core.ExtractEnv(ctx, scw.ScwSecretKeyEnv) != "":
token = core.ExtractEnv(ctx, scw.ScwSecretKeyEnv)
// There is no config file
case config == nil:
return nil, errors.New("config not provided")
// Config file with profile name
case config.Profiles[profileName] != nil && config.Profiles[profileName].SecretKey != nil:
token = *config.Profiles[profileName].SecretKey
// Default config
case config.Profile.SecretKey != nil:
token = *config.Profile.SecretKey
default:
return nil, errors.New("unable to find secret key")
token, err := SecretKey(ctx)
if err != nil {
return nil, err
}

if !validation.IsSecretKey(token) {
Expand Down
106 changes: 95 additions & 11 deletions internal/namespaces/k8s/v1/custom_kubeconfig_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package k8s

import (
"context"
"errors"
"fmt"
"hash/crc32"
"reflect"

"github.com/ghodss/yaml"
Expand All @@ -12,8 +15,9 @@ import (
)

type k8sKubeconfigGetRequest struct {
ClusterID string
Region scw.Region
ClusterID string
Region scw.Region
AuthMethod authMethods
}

func k8sKubeconfigGetCommand() *core.Command {
Expand All @@ -31,6 +35,16 @@ func k8sKubeconfigGetCommand() *core.Command {
Required: true,
Positional: true,
},
{
Name: "auth-method",
Short: `Which method to use to authenticate using kubelet`,
Default: core.DefaultValueSetter(string(authMethodLegacy)),
EnumValues: []string{
string(authMethodLegacy),
string(authMethodCLI),
string(authMethodCopyToken),
},
},
core.RegionArgSpec(),
},
Run: k8sKubeconfigGetRun,
Expand All @@ -52,27 +66,97 @@ func k8sKubeconfigGetCommand() *core.Command {
func k8sKubeconfigGetRun(ctx context.Context, argsI interface{}) (i interface{}, e error) {
request := argsI.(*k8sKubeconfigGetRequest)

kubeconfigRequest := &k8s.GetClusterKubeConfigRequest{
apiKubeconfig, err := k8s.NewAPI(core.ExtractClient(ctx)).GetClusterKubeConfig(&k8s.GetClusterKubeConfigRequest{
Region: request.Region,
ClusterID: request.ClusterID,
}

client := core.ExtractClient(ctx)
apiK8s := k8s.NewAPI(client)

apiKubeconfig, err := apiK8s.GetClusterKubeConfig(kubeconfigRequest)
})
if err != nil {
return nil, err
}

var kubeconfig api.Config

err = yaml.Unmarshal(apiKubeconfig.GetRaw(), &kubeconfig)
if err != nil {
return nil, err
}

config, err := yaml.Marshal(kubeconfig)
namedClusterInfo := api.NamedCluster{
Name: fmt.Sprintf("%s-%s", kubeconfig.Clusters[0].Name, request.ClusterID),
Cluster: kubeconfig.Clusters[0].Cluster,
}

var namedAuthInfo api.NamedAuthInfo
switch request.AuthMethod {
case authMethodLegacy:
if kubeconfig.AuthInfos[0].AuthInfo.Token == RedactedAuthInfoToken {
return nil, errors.New("this cluster does not support legacy authentication")
}

namedAuthInfo.Name = fmt.Sprintf("%s-%s", kubeconfig.Clusters[0].Name, request.ClusterID)
namedAuthInfo.AuthInfo.Token = kubeconfig.AuthInfos[0].AuthInfo.Token
case authMethodCLI:
args := []string{}
profileName := core.ExtractProfileName(ctx)
if profileName != scw.DefaultProfileName {
args = append(args, "--profile", profileName)
}

var configPath string
switch {
case core.ExtractConfigPathFlag(ctx) != "":
configPath = core.ExtractConfigPathFlag(ctx)
args = append(args, "--config", configPath)
case core.ExtractEnv(ctx, scw.ScwConfigPathEnv) != "":
configPath = core.ExtractEnv(ctx, scw.ScwConfigPathEnv)
args = append(args, "--config", configPath)
}

configPathSum := crc32.ChecksumIEEE([]byte(configPath))
namedAuthInfo.Name = fmt.Sprintf("cli-%s-%08x", profileName, configPathSum)
namedAuthInfo.AuthInfo = api.AuthInfo{
Exec: &api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1",
Command: core.ExtractBinaryName(ctx),
Args: append(args,
"k8s",
"exec-credential",
),
InstallHint: installHint,
},
}
case authMethodCopyToken:
token, err := SecretKey(ctx)
if err != nil {
return nil, err
}

tokenSum := crc32.ChecksumIEEE([]byte(token))
namedAuthInfo.Name = fmt.Sprintf("token-cli-%08x", tokenSum)
namedAuthInfo.AuthInfo = api.AuthInfo{
Token: token,
}
default:
return nil, errors.New("unknown auth method")
}

namedContext := api.NamedContext{
Name: fmt.Sprintf("%s-%s", kubeconfig.Clusters[0].Name, request.ClusterID),
Context: api.Context{
Cluster: namedClusterInfo.Name,
AuthInfo: namedAuthInfo.Name,
},
}

resultingKubeconfig := api.Config{
APIVersion: KubeconfigAPIVersion,
Kind: KubeconfigKind,
Clusters: []api.NamedCluster{namedClusterInfo},
AuthInfos: []api.NamedAuthInfo{namedAuthInfo},
Contexts: []api.NamedContext{namedContext},
CurrentContext: namedContext.Name,
}

config, err := yaml.Marshal(resultingKubeconfig)
if err != nil {
return nil, err
}
Expand Down
71 changes: 61 additions & 10 deletions internal/namespaces/k8s/v1/custom_kubeconfig_get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,82 @@ package k8s_test
import (
"testing"

"github.com/alecthomas/assert"
"github.com/ghodss/yaml"
"github.com/scaleway/scaleway-cli/v2/core"
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/k8s/v1"
api "github.com/scaleway/scaleway-cli/v2/internal/namespaces/k8s/v1/types"
)

func Test_GetKubeconfig(t *testing.T) {
////
// Simple use cases
////
// simple, auth-mode= not provided
t.Run("simple", core.Test(&core.TestConfig{
Commands: k8s.GetCommands(),
BeforeFunc: createClusterAndWaitAndKubeconfig("get-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
Cmd: "scw k8s kubeconfig get {{ .Cluster.ID }}",
Check: core.TestCheckCombine(
core.TestCheckGolden(),
func(t *testing.T, ctx *core.CheckFuncCtx) {
func(t *testing.T, _ *core.CheckFuncCtx) {
t.Helper()
config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
assert.Equal(t, err, nil)
assert.Equal(t, ctx.Result.(string), string(config))
// config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
// assert.Equal(t, err, nil)
// assert.Equal(t, ctx.Result.(string), string(config))
},
core.TestCheckExitCode(0),
),
AfterFunc: deleteCluster("Cluster"),
}))

t.Run("legacy", core.Test(&core.TestConfig{
Commands: k8s.GetCommands(),
BeforeFunc: createClusterAndWaitAndKubeconfig("get-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
Cmd: "scw k8s kubeconfig get {{ .Cluster.ID }} auth-method=legacy",
Check: core.TestCheckCombine(
core.TestCheckGolden(),
func(t *testing.T, _ *core.CheckFuncCtx) {
t.Helper()
// config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
// assert.Equal(t, err, nil)
// assert.Equal(t, ctx.Result.(string), string(config))
},
core.TestCheckExitCode(0),
),
AfterFunc: deleteCluster("Cluster"),
}))

t.Run("cli", core.Test(&core.TestConfig{
Commands: k8s.GetCommands(),
BeforeFunc: createClusterAndWaitAndKubeconfig("get-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
Cmd: "scw k8s kubeconfig get {{ .Cluster.ID }} auth-method=cli",
Check: core.TestCheckCombine(
core.TestCheckGolden(),
func(t *testing.T, _ *core.CheckFuncCtx) {
t.Helper()
// config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
// assert.Equal(t, err, nil)
// assert.Equal(t, ctx.Result.(string), string(config))
},
core.TestCheckExitCode(0),
),
AfterFunc: deleteCluster("Cluster"),
}))

// t.Run("copy-token", core.Test(&core.TestConfig{
// Commands: k8s.GetCommands(),
// BeforeFunc: createClusterAndWaitAndKubeconfig("get-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
// Cmd: "scw k8s kubeconfig get {{ .Cluster.ID }} auth-method=copy-token",
// Check: core.TestCheckCombine(
// core.TestCheckGoldenAndReplacePatterns(
// core.GoldenReplacement{
// Pattern: regexp.MustCompile("token: [a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}"),
// Replacement: "token: 11111111-1111-1111-1111-111111111111",
// OptionalMatch: false,
// },
// ),
// func(t *testing.T, _ *core.CheckFuncCtx) {
// // config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
// // assert.Equal(t, err, nil)
// // assert.Equal(t, ctx.Result.(string), string(config))
// },
// core.TestCheckExitCode(0),
// ),
// AfterFunc: deleteCluster("Cluster"),
// }))
}
Loading

0 comments on commit 710551c

Please sign in to comment.