Skip to content

Commit 62cda48

Browse files
committed
feat(k8s): rework kubeconfig handling
1 parent 8ef81e1 commit 62cda48

21 files changed

+9373
-5674
lines changed

cmd/scw/testdata/test-all-usage-k8s-kubeconfig-get-usage.golden

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ EXAMPLES:
1010
scw k8s kubeconfig get 11111111-1111-1111-1111-111111111111
1111

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

1617
FLAGS:
1718
-h, --help help for get

cmd/scw/testdata/test-all-usage-k8s-kubeconfig-install-usage.golden

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ EXAMPLES:
1313
ARGS:
1414
cluster-id Cluster ID from which to retrieve the kubeconfig
1515
[keep-current-context] Whether or not to keep the current kubeconfig context unmodified
16+
[auth-method=legacy] Which method to use to authenticate using kubelet (legacy | cli | copy-token)
1617
[region=fr-par] Region to target. If none is passed will use default region from the config
1718

1819
FLAGS:

docs/commands/k8s.md

+2
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,7 @@ scw k8s kubeconfig get <cluster-id ...> [arg=value ...]
610610
| Name | | Description |
611611
|------|---|-------------|
612612
| cluster-id | Required | Cluster ID from which to retrieve the kubeconfig |
613+
| auth-method | Default: `legacy`<br />One of: `legacy`, `cli`, `copy-token` | Which method to use to authenticate using kubelet |
613614
| region | Default: `fr-par` | Region to target. If none is passed will use default region from the config |
614615

615616

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

647649

internal/namespaces/k8s/v1/custom.go

+26
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package k8s
22

33
import (
4+
"context"
5+
"errors"
6+
47
"github.com/scaleway/scaleway-cli/v2/core"
58
"github.com/scaleway/scaleway-cli/v2/internal/human"
69
k8s "github.com/scaleway/scaleway-sdk-go/api/k8s/v1"
10+
"github.com/scaleway/scaleway-sdk-go/scw"
711
)
812

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

5054
return cmds
5155
}
56+
57+
func SecretKey(ctx context.Context) (string, error) {
58+
config, _ := scw.LoadConfigFromPath(core.ExtractConfigPath(ctx))
59+
profileName := core.ExtractProfileName(ctx)
60+
61+
switch {
62+
// Environment variable check
63+
case core.ExtractEnv(ctx, scw.ScwSecretKeyEnv) != "":
64+
return core.ExtractEnv(ctx, scw.ScwSecretKeyEnv), nil
65+
// There is no config file
66+
case config == nil:
67+
return "", errors.New("config not provided")
68+
// Config file with profile name
69+
case config.Profiles[profileName] != nil && config.Profiles[profileName].SecretKey != nil:
70+
return *config.Profiles[profileName].SecretKey, nil
71+
// Default config
72+
case config.Profile.SecretKey != nil:
73+
return *config.Profile.SecretKey, nil
74+
}
75+
76+
return "", errors.New("unable to find secret key")
77+
}

internal/namespaces/k8s/v1/custom_execcredentials.go

+3-21
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ package k8s
33
import (
44
"context"
55
"encoding/json"
6-
"errors"
76
"fmt"
87
"reflect"
98

109
"github.com/scaleway/scaleway-cli/v2/core"
11-
"github.com/scaleway/scaleway-sdk-go/scw"
1210
"github.com/scaleway/scaleway-sdk-go/validation"
1311
)
1412

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

3028
func k8sExecCredentialRun(ctx context.Context, _ interface{}) (i interface{}, e error) {
31-
config, _ := scw.LoadConfigFromPath(core.ExtractConfigPath(ctx))
32-
profileName := core.ExtractProfileName(ctx)
33-
34-
var token string
35-
switch {
36-
// Environment variable check
37-
case core.ExtractEnv(ctx, scw.ScwSecretKeyEnv) != "":
38-
token = core.ExtractEnv(ctx, scw.ScwSecretKeyEnv)
39-
// There is no config file
40-
case config == nil:
41-
return nil, errors.New("config not provided")
42-
// Config file with profile name
43-
case config.Profiles[profileName] != nil && config.Profiles[profileName].SecretKey != nil:
44-
token = *config.Profiles[profileName].SecretKey
45-
// Default config
46-
case config.Profile.SecretKey != nil:
47-
token = *config.Profile.SecretKey
48-
default:
49-
return nil, errors.New("unable to find secret key")
29+
token, err := SecretKey(ctx)
30+
if err != nil {
31+
return nil, err
5032
}
5133

5234
if !validation.IsSecretKey(token) {

internal/namespaces/k8s/v1/custom_kubeconfig_get.go

+95-11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package k8s
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
7+
"hash/crc32"
58
"reflect"
69

710
"github.com/ghodss/yaml"
@@ -12,8 +15,9 @@ import (
1215
)
1316

1417
type k8sKubeconfigGetRequest struct {
15-
ClusterID string
16-
Region scw.Region
18+
ClusterID string
19+
Region scw.Region
20+
AuthMethod authMethods
1721
}
1822

1923
func k8sKubeconfigGetCommand() *core.Command {
@@ -31,6 +35,16 @@ func k8sKubeconfigGetCommand() *core.Command {
3135
Required: true,
3236
Positional: true,
3337
},
38+
{
39+
Name: "auth-method",
40+
Short: `Which method to use to authenticate using kubelet`,
41+
Default: core.DefaultValueSetter(string(authMethodLegacy)),
42+
EnumValues: []string{
43+
string(authMethodLegacy),
44+
string(authMethodCLI),
45+
string(authMethodCopyToken),
46+
},
47+
},
3448
core.RegionArgSpec(),
3549
},
3650
Run: k8sKubeconfigGetRun,
@@ -52,27 +66,97 @@ func k8sKubeconfigGetCommand() *core.Command {
5266
func k8sKubeconfigGetRun(ctx context.Context, argsI interface{}) (i interface{}, e error) {
5367
request := argsI.(*k8sKubeconfigGetRequest)
5468

55-
kubeconfigRequest := &k8s.GetClusterKubeConfigRequest{
69+
apiKubeconfig, err := k8s.NewAPI(core.ExtractClient(ctx)).GetClusterKubeConfig(&k8s.GetClusterKubeConfigRequest{
5670
Region: request.Region,
5771
ClusterID: request.ClusterID,
58-
}
59-
60-
client := core.ExtractClient(ctx)
61-
apiK8s := k8s.NewAPI(client)
62-
63-
apiKubeconfig, err := apiK8s.GetClusterKubeConfig(kubeconfigRequest)
72+
})
6473
if err != nil {
6574
return nil, err
6675
}
6776

6877
var kubeconfig api.Config
69-
7078
err = yaml.Unmarshal(apiKubeconfig.GetRaw(), &kubeconfig)
7179
if err != nil {
7280
return nil, err
7381
}
7482

75-
config, err := yaml.Marshal(kubeconfig)
83+
namedClusterInfo := api.NamedCluster{
84+
Name: fmt.Sprintf("%s-%s", kubeconfig.Clusters[0].Name, request.ClusterID),
85+
Cluster: kubeconfig.Clusters[0].Cluster,
86+
}
87+
88+
var namedAuthInfo api.NamedAuthInfo
89+
switch request.AuthMethod {
90+
case authMethodLegacy:
91+
if kubeconfig.AuthInfos[0].AuthInfo.Token == RedactedAuthInfoToken {
92+
return nil, errors.New("this cluster does not support legacy authentication")
93+
}
94+
95+
namedAuthInfo.Name = fmt.Sprintf("%s-%s", kubeconfig.Clusters[0].Name, request.ClusterID)
96+
namedAuthInfo.AuthInfo.Token = kubeconfig.AuthInfos[0].AuthInfo.Token
97+
case authMethodCLI:
98+
args := []string{}
99+
profileName := core.ExtractProfileName(ctx)
100+
if profileName != scw.DefaultProfileName {
101+
args = append(args, "--profile", profileName)
102+
}
103+
104+
var configPath string
105+
switch {
106+
case core.ExtractConfigPathFlag(ctx) != "":
107+
configPath = core.ExtractConfigPathFlag(ctx)
108+
args = append(args, "--config", configPath)
109+
case core.ExtractEnv(ctx, scw.ScwConfigPathEnv) != "":
110+
configPath = core.ExtractEnv(ctx, scw.ScwConfigPathEnv)
111+
args = append(args, "--config", configPath)
112+
}
113+
114+
configPathSum := crc32.ChecksumIEEE([]byte(configPath))
115+
namedAuthInfo.Name = fmt.Sprintf("cli-%s-%08x", profileName, configPathSum)
116+
namedAuthInfo.AuthInfo = api.AuthInfo{
117+
Exec: &api.ExecConfig{
118+
APIVersion: "client.authentication.k8s.io/v1",
119+
Command: core.ExtractBinaryName(ctx),
120+
Args: append(args,
121+
"k8s",
122+
"exec-credential",
123+
),
124+
InstallHint: installHint,
125+
},
126+
}
127+
case authMethodCopyToken:
128+
token, err := SecretKey(ctx)
129+
if err != nil {
130+
return nil, err
131+
}
132+
133+
tokenSum := crc32.ChecksumIEEE([]byte(token))
134+
namedAuthInfo.Name = fmt.Sprintf("token-cli-%08x", tokenSum)
135+
namedAuthInfo.AuthInfo = api.AuthInfo{
136+
Token: token,
137+
}
138+
default:
139+
return nil, errors.New("unknown auth method")
140+
}
141+
142+
namedContext := api.NamedContext{
143+
Name: fmt.Sprintf("%s-%s", kubeconfig.Clusters[0].Name, request.ClusterID),
144+
Context: api.Context{
145+
Cluster: namedClusterInfo.Name,
146+
AuthInfo: namedAuthInfo.Name,
147+
},
148+
}
149+
150+
resultingKubeconfig := api.Config{
151+
APIVersion: KubeconfigAPIVersion,
152+
Kind: KubeconfigKind,
153+
Clusters: []api.NamedCluster{namedClusterInfo},
154+
AuthInfos: []api.NamedAuthInfo{namedAuthInfo},
155+
Contexts: []api.NamedContext{namedContext},
156+
CurrentContext: namedContext.Name,
157+
}
158+
159+
config, err := yaml.Marshal(resultingKubeconfig)
76160
if err != nil {
77161
return nil, err
78162
}

internal/namespaces/k8s/v1/custom_kubeconfig_get_test.go

+61-10
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,82 @@ package k8s_test
33
import (
44
"testing"
55

6-
"github.com/alecthomas/assert"
7-
"github.com/ghodss/yaml"
86
"github.com/scaleway/scaleway-cli/v2/core"
97
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/k8s/v1"
10-
api "github.com/scaleway/scaleway-cli/v2/internal/namespaces/k8s/v1/types"
118
)
129

1310
func Test_GetKubeconfig(t *testing.T) {
14-
////
15-
// Simple use cases
16-
////
11+
// simple, auth-mode= not provided
1712
t.Run("simple", core.Test(&core.TestConfig{
1813
Commands: k8s.GetCommands(),
1914
BeforeFunc: createClusterAndWaitAndKubeconfig("get-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
2015
Cmd: "scw k8s kubeconfig get {{ .Cluster.ID }}",
2116
Check: core.TestCheckCombine(
2217
core.TestCheckGolden(),
23-
func(t *testing.T, ctx *core.CheckFuncCtx) {
18+
func(t *testing.T, _ *core.CheckFuncCtx) {
2419
t.Helper()
25-
config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
26-
assert.Equal(t, err, nil)
27-
assert.Equal(t, ctx.Result.(string), string(config))
20+
// config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
21+
// assert.Equal(t, err, nil)
22+
// assert.Equal(t, ctx.Result.(string), string(config))
2823
},
2924
core.TestCheckExitCode(0),
3025
),
3126
AfterFunc: deleteCluster("Cluster"),
3227
}))
28+
29+
t.Run("legacy", core.Test(&core.TestConfig{
30+
Commands: k8s.GetCommands(),
31+
BeforeFunc: createClusterAndWaitAndKubeconfig("get-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
32+
Cmd: "scw k8s kubeconfig get {{ .Cluster.ID }} auth-method=legacy",
33+
Check: core.TestCheckCombine(
34+
core.TestCheckGolden(),
35+
func(t *testing.T, _ *core.CheckFuncCtx) {
36+
t.Helper()
37+
// config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
38+
// assert.Equal(t, err, nil)
39+
// assert.Equal(t, ctx.Result.(string), string(config))
40+
},
41+
core.TestCheckExitCode(0),
42+
),
43+
AfterFunc: deleteCluster("Cluster"),
44+
}))
45+
46+
t.Run("cli", core.Test(&core.TestConfig{
47+
Commands: k8s.GetCommands(),
48+
BeforeFunc: createClusterAndWaitAndKubeconfig("get-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
49+
Cmd: "scw k8s kubeconfig get {{ .Cluster.ID }} auth-method=cli",
50+
Check: core.TestCheckCombine(
51+
core.TestCheckGolden(),
52+
func(t *testing.T, _ *core.CheckFuncCtx) {
53+
t.Helper()
54+
// config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
55+
// assert.Equal(t, err, nil)
56+
// assert.Equal(t, ctx.Result.(string), string(config))
57+
},
58+
core.TestCheckExitCode(0),
59+
),
60+
AfterFunc: deleteCluster("Cluster"),
61+
}))
62+
63+
// t.Run("copy-token", core.Test(&core.TestConfig{
64+
// Commands: k8s.GetCommands(),
65+
// BeforeFunc: createClusterAndWaitAndKubeconfig("get-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
66+
// Cmd: "scw k8s kubeconfig get {{ .Cluster.ID }} auth-method=copy-token",
67+
// Check: core.TestCheckCombine(
68+
// core.TestCheckGoldenAndReplacePatterns(
69+
// core.GoldenReplacement{
70+
// 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}"),
71+
// Replacement: "token: 11111111-1111-1111-1111-111111111111",
72+
// OptionalMatch: false,
73+
// },
74+
// ),
75+
// func(t *testing.T, _ *core.CheckFuncCtx) {
76+
// // config, err := yaml.Marshal(ctx.Meta["Kubeconfig"].(api.Config))
77+
// // assert.Equal(t, err, nil)
78+
// // assert.Equal(t, ctx.Result.(string), string(config))
79+
// },
80+
// core.TestCheckExitCode(0),
81+
// ),
82+
// AfterFunc: deleteCluster("Cluster"),
83+
// }))
3384
}

0 commit comments

Comments
 (0)