Skip to content

Commit 4e4d95b

Browse files
authored
feat(k8s): add exec-credential (#4069)
1 parent b1f4347 commit 4e4d95b

9 files changed

+315
-7
lines changed

β€Žcmd/scw/testdata/test-all-usage-k8s-usage.goldenβ€Ž

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ USAGE:
66
scw k8s <command>
77

88
AVAILABLE COMMANDS:
9-
acl Access Control List (ACL) management commands
10-
cluster Kapsule cluster management commands
11-
cluster-type Cluster type management commands
12-
kubeconfig Manage your Kubernetes Kapsule cluster's kubeconfig files
13-
node Kapsule node management commands
14-
pool Kapsule pool management commands
15-
version Available Kubernetes versions commands
9+
acl Access Control List (ACL) management commands
10+
cluster Kapsule cluster management commands
11+
cluster-type Cluster type management commands
12+
kubeconfig Manage your Kubernetes Kapsule cluster's kubeconfig files
13+
node Kapsule node management commands
14+
pool Kapsule pool management commands
15+
version Available Kubernetes versions commands
1616

1717
FLAGS:
1818
-h, --help help for k8s

β€Žinternal/namespaces/k8s/v1/custom.goβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
func GetCommands() *core.Commands {
1616
cmds := GetGeneratedCommands()
1717
cmds.Merge(core.NewCommands(
18+
k8sExecCredentialCommand(),
1819
k8sKubeconfigCommand(),
1920
k8sKubeconfigGetCommand(),
2021
k8sKubeconfigInstallCommand(),
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package k8s
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"reflect"
9+
10+
"github.com/scaleway/scaleway-cli/v2/internal/core"
11+
"github.com/scaleway/scaleway-sdk-go/scw"
12+
"github.com/scaleway/scaleway-sdk-go/validation"
13+
)
14+
15+
func k8sExecCredentialCommand() *core.Command {
16+
return &core.Command{
17+
Hidden: true,
18+
Short: `exec-credential is a kubectl plugin to communicate credentials to HTTP transports.`,
19+
Namespace: "k8s",
20+
Resource: "exec-credential",
21+
ArgsType: reflect.TypeOf(struct{}{}),
22+
ArgSpecs: core.ArgSpecs{},
23+
Run: k8sExecCredentialRun,
24+
25+
// avoid calling checkAPIKey (Check if API Key is about to expire)
26+
DisableAfterChecks: true,
27+
}
28+
}
29+
30+
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")
50+
}
51+
52+
if !validation.IsSecretKey(token) {
53+
return nil, fmt.Errorf("invalid secret key format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", token)
54+
}
55+
56+
execCreds := ExecCredential{
57+
APIVersion: "client.authentication.k8s.io/v1",
58+
Kind: "ExecCredential",
59+
Status: &ExecCredentialStatus{
60+
Token: token,
61+
},
62+
}
63+
response, err := json.MarshalIndent(execCreds, "", " ")
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
return string(response), nil
69+
}
70+
71+
// ExecCredential is used by exec-based plugins to communicate credentials to HTTP transports.
72+
type ExecCredential struct {
73+
// APIVersion defines the versioned schema of this representation of an object.
74+
// Servers should convert recognized schemas to the latest internal value, and
75+
// may reject unrecognized values.
76+
APIVersion string `json:"apiVersion,omitempty"`
77+
78+
// Kind is a string value representing the REST resource this object represents.
79+
// Servers may infer this from the endpoint the client submits requests to.
80+
Kind string `json:"kind,omitempty"`
81+
82+
// Status is filled in by the plugin and holds the credentials that the transport
83+
// should use to contact the API.
84+
Status *ExecCredentialStatus `json:"status,omitempty"`
85+
}
86+
87+
// ExecCredentialStatus holds credentials for the transport to use.
88+
type ExecCredentialStatus struct {
89+
// Token is a bearer token used by the client for request authentication.
90+
Token string `json:"token,omitempty"`
91+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package k8s_test
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"path"
7+
"testing"
8+
9+
"github.com/scaleway/scaleway-cli/v2/internal/core"
10+
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/k8s/v1"
11+
"github.com/scaleway/scaleway-sdk-go/scw"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
const (
16+
p1Secret = "00000000-0000-0000-0000-111111111111"
17+
p2Secret = "00000000-0000-0000-0000-222222222222"
18+
p3Secret = "00000000-0000-0000-0000-333333333333"
19+
envSecret = "66666666-6666-6666-6666-666666666666"
20+
)
21+
22+
func Test_ExecCredential(t *testing.T) {
23+
// expect to return default secret_key
24+
t.Run("simple", core.Test(&core.TestConfig{
25+
Commands: k8s.GetCommands(),
26+
TmpHomeDir: true,
27+
BeforeFunc: beforeFuncCreateFullConfig(),
28+
Cmd: "scw k8s exec-credential",
29+
Check: core.TestCheckCombine(
30+
core.TestCheckExitCode(0),
31+
core.TestCheckGolden(),
32+
assertTokenInResponse(p1Secret),
33+
),
34+
}))
35+
36+
// expect to return 66666666-6666-6666-6666-666666666666
37+
t.Run("with scw_secret_key env", core.Test(&core.TestConfig{
38+
Commands: k8s.GetCommands(),
39+
TmpHomeDir: true,
40+
BeforeFunc: beforeFuncCreateFullConfig(),
41+
Cmd: "scw k8s exec-credential",
42+
OverrideEnv: map[string]string{
43+
scw.ScwSecretKeyEnv: envSecret,
44+
},
45+
Check: core.TestCheckCombine(
46+
core.TestCheckExitCode(0),
47+
core.TestCheckGolden(),
48+
assertTokenInResponse(envSecret),
49+
),
50+
}))
51+
52+
// expect to return p2 secret_key
53+
t.Run("with profile env", core.Test(&core.TestConfig{
54+
Commands: k8s.GetCommands(),
55+
TmpHomeDir: true,
56+
BeforeFunc: beforeFuncCreateFullConfig(),
57+
Cmd: "scw k8s exec-credential",
58+
OverrideEnv: map[string]string{
59+
scw.ScwActiveProfileEnv: "p2",
60+
},
61+
Check: core.TestCheckCombine(
62+
core.TestCheckExitCode(0),
63+
core.TestCheckGolden(),
64+
assertTokenInResponse(p2Secret),
65+
),
66+
}))
67+
68+
// expect to return p3 secret_key
69+
t.Run("with profile flag", core.Test(&core.TestConfig{
70+
Commands: k8s.GetCommands(),
71+
TmpHomeDir: true,
72+
BeforeFunc: beforeFuncCreateFullConfig(),
73+
Cmd: "scw --profile p3 k8s exec-credential",
74+
Check: core.TestCheckCombine(
75+
core.TestCheckExitCode(0),
76+
core.TestCheckGolden(),
77+
assertTokenInResponse(p3Secret),
78+
),
79+
}))
80+
81+
// expect to return p3 secret_key
82+
t.Run("with profile env and flag", core.Test(&core.TestConfig{
83+
Commands: k8s.GetCommands(),
84+
TmpHomeDir: true,
85+
BeforeFunc: beforeFuncCreateFullConfig(),
86+
Cmd: "scw --profile p3 k8s exec-credential",
87+
OverrideEnv: map[string]string{
88+
scw.ScwActiveProfileEnv: "p2",
89+
},
90+
Check: core.TestCheckCombine(
91+
core.TestCheckExitCode(0),
92+
core.TestCheckGolden(),
93+
assertTokenInResponse(p3Secret),
94+
),
95+
}))
96+
}
97+
98+
func beforeFuncCreateConfigFile(c *scw.Config) core.BeforeFunc {
99+
return func(ctx *core.BeforeFuncCtx) error {
100+
homeDir := ctx.OverrideEnv["HOME"]
101+
scwDir := path.Join(homeDir, ".config", "scw")
102+
err := os.MkdirAll(scwDir, 0o0755)
103+
if err != nil {
104+
return err
105+
}
106+
107+
return c.SaveTo(path.Join(scwDir, "config.yaml"))
108+
}
109+
}
110+
111+
func beforeFuncCreateFullConfig() core.BeforeFunc {
112+
return beforeFuncCreateConfigFile(&scw.Config{
113+
Profile: scw.Profile{
114+
AccessKey: scw.StringPtr("SCWXXXXXXXXXXXXXXXXX"),
115+
SecretKey: scw.StringPtr(p1Secret),
116+
APIURL: scw.StringPtr("https://mock-api-url.com"),
117+
Insecure: scw.BoolPtr(true),
118+
DefaultOrganizationID: scw.StringPtr("deadbeef-dead-dead-dead-deaddeafbeef"),
119+
DefaultProjectID: scw.StringPtr("deadbeef-dead-dead-dead-deaddeafbeef"),
120+
DefaultRegion: scw.StringPtr("fr-par"),
121+
DefaultZone: scw.StringPtr("fr-par-1"),
122+
SendTelemetry: scw.BoolPtr(true),
123+
},
124+
Profiles: map[string]*scw.Profile{
125+
"p2": {
126+
AccessKey: scw.StringPtr("SCWP2XXXXXXXXXXXXXXX"),
127+
SecretKey: scw.StringPtr(p2Secret),
128+
APIURL: scw.StringPtr("https://p2-mock-api-url.com"),
129+
Insecure: scw.BoolPtr(true),
130+
DefaultOrganizationID: scw.StringPtr("deadbeef-dead-dead-dead-deaddeafbeef"),
131+
DefaultProjectID: scw.StringPtr("deadbeef-dead-dead-dead-deaddeafbeef"),
132+
DefaultRegion: scw.StringPtr("fr-par"),
133+
DefaultZone: scw.StringPtr("fr-par-1"),
134+
SendTelemetry: scw.BoolPtr(true),
135+
},
136+
"p3": {
137+
AccessKey: scw.StringPtr("SCWP3XXXXXXXXXXXXXXX"),
138+
SecretKey: scw.StringPtr(p3Secret),
139+
APIURL: scw.StringPtr("https://p3-mock-api-url.com"),
140+
Insecure: scw.BoolPtr(true),
141+
DefaultOrganizationID: scw.StringPtr("deadbeef-dead-dead-dead-deaddeafbeef"),
142+
DefaultProjectID: scw.StringPtr("deadbeef-dead-dead-dead-deaddeafbeef"),
143+
DefaultRegion: scw.StringPtr("fr-par"),
144+
DefaultZone: scw.StringPtr("fr-par-1"),
145+
SendTelemetry: scw.BoolPtr(true),
146+
},
147+
},
148+
})
149+
}
150+
151+
func assertTokenInResponse(expectedToken string) core.TestCheck {
152+
return func(t *testing.T, ctx *core.CheckFuncCtx) {
153+
res := ctx.Result.(string)
154+
creds := k8s.ExecCredential{}
155+
err := json.Unmarshal([]byte(res), &creds)
156+
if err != nil {
157+
t.Fatal(err)
158+
}
159+
assert.Equal(t, expectedToken, creds.Status.Token)
160+
}
161+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟩🟩🟩 STDOUT️ 🟩🟩🟩️
3+
{
4+
"apiVersion": "client.authentication.k8s.io/v1",
5+
"kind": "ExecCredential",
6+
"status": {
7+
"token": "00000000-0000-0000-0000-111111111111"
8+
}
9+
}
10+
🟩🟩🟩 JSON STDOUT 🟩🟩🟩
11+
"{\n \"apiVersion\": \"client.authentication.k8s.io/v1\",\n \"kind\": \"ExecCredential\",\n \"status\": {\n \"token\": \"00000000-0000-0000-0000-111111111111\"\n }\n}"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟩🟩🟩 STDOUT️ 🟩🟩🟩️
3+
{
4+
"apiVersion": "client.authentication.k8s.io/v1",
5+
"kind": "ExecCredential",
6+
"status": {
7+
"token": "00000000-0000-0000-0000-333333333333"
8+
}
9+
}
10+
🟩🟩🟩 JSON STDOUT 🟩🟩🟩
11+
"{\n \"apiVersion\": \"client.authentication.k8s.io/v1\",\n \"kind\": \"ExecCredential\",\n \"status\": {\n \"token\": \"00000000-0000-0000-0000-333333333333\"\n }\n}"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟩🟩🟩 STDOUT️ 🟩🟩🟩️
3+
{
4+
"apiVersion": "client.authentication.k8s.io/v1",
5+
"kind": "ExecCredential",
6+
"status": {
7+
"token": "00000000-0000-0000-0000-222222222222"
8+
}
9+
}
10+
🟩🟩🟩 JSON STDOUT 🟩🟩🟩
11+
"{\n \"apiVersion\": \"client.authentication.k8s.io/v1\",\n \"kind\": \"ExecCredential\",\n \"status\": {\n \"token\": \"00000000-0000-0000-0000-222222222222\"\n }\n}"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟩🟩🟩 STDOUT️ 🟩🟩🟩️
3+
{
4+
"apiVersion": "client.authentication.k8s.io/v1",
5+
"kind": "ExecCredential",
6+
"status": {
7+
"token": "00000000-0000-0000-0000-333333333333"
8+
}
9+
}
10+
🟩🟩🟩 JSON STDOUT 🟩🟩🟩
11+
"{\n \"apiVersion\": \"client.authentication.k8s.io/v1\",\n \"kind\": \"ExecCredential\",\n \"status\": {\n \"token\": \"00000000-0000-0000-0000-333333333333\"\n }\n}"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟩🟩🟩 STDOUT️ 🟩🟩🟩️
3+
{
4+
"apiVersion": "client.authentication.k8s.io/v1",
5+
"kind": "ExecCredential",
6+
"status": {
7+
"token": "66666666-6666-6666-6666-666666666666"
8+
}
9+
}
10+
🟩🟩🟩 JSON STDOUT 🟩🟩🟩
11+
"{\n \"apiVersion\": \"client.authentication.k8s.io/v1\",\n \"kind\": \"ExecCredential\",\n \"status\": {\n \"token\": \"66666666-6666-6666-6666-666666666666\"\n }\n}"

0 commit comments

Comments
Β (0)