Skip to content

feat(k8s): rework kubeconfig handling #4137

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

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
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 @@ -579,6 +579,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 @@ -611,6 +612,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{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this code commented?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to avoid possible leaking of a token

// 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
Loading