Skip to content

Commit 85d35ea

Browse files
committed
feat(k8s): add exec-credentials
1 parent 44f37c5 commit 85d35ea

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed

internal/namespaces/k8s/v1/custom.go

+1
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(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package k8s
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"reflect"
8+
9+
"github.com/scaleway/scaleway-cli/v2/internal/core"
10+
"github.com/scaleway/scaleway-sdk-go/scw"
11+
"github.com/scaleway/scaleway-sdk-go/validation"
12+
)
13+
14+
func k8sExecCredentialCommand() *core.Command {
15+
return &core.Command{
16+
Hidden: true,
17+
Short: `exec-credential is a kubectl plugin to communicate credentials to HTTP transports.`,
18+
Namespace: "k8s",
19+
Resource: "exec-credential",
20+
ArgsType: reflect.TypeOf(struct{}{}),
21+
ArgSpecs: core.ArgSpecs{},
22+
Run: k8sExecCredentialRun,
23+
}
24+
}
25+
26+
func k8sExecCredentialRun(ctx context.Context, argsI interface{}) (i interface{}, e error) {
27+
config, _ := scw.LoadConfigFromPath(core.ExtractConfigPath(ctx))
28+
profileName := core.ExtractProfileName(ctx)
29+
30+
var token string
31+
switch {
32+
// Environment variable check
33+
case core.ExtractEnv(ctx, scw.ScwSecretKeyEnv) != "":
34+
token = core.ExtractEnv(ctx, scw.ScwSecretKeyEnv)
35+
// There is no config file
36+
case config == nil:
37+
return nil, fmt.Errorf("config not provided")
38+
// Config file with profile name
39+
case config.Profiles[profileName] != nil && config.Profiles[profileName].SecretKey != nil:
40+
token = *config.Profiles[profileName].SecretKey
41+
// Default config
42+
case config.Profile.SecretKey != nil:
43+
token = *config.Profile.SecretKey
44+
default:
45+
return nil, fmt.Errorf("unable to find secret key")
46+
}
47+
48+
if !validation.IsSecretKey(token) {
49+
return nil, fmt.Errorf("invalid secret key format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", token)
50+
}
51+
52+
execCreds := ExecCredential{
53+
APIVersion: "client.authentication.k8s.io/v1",
54+
Kind: "ExecCredential",
55+
Status: &ExecCredentialStatus{
56+
Token: token,
57+
},
58+
}
59+
response, err := json.MarshalIndent(execCreds, "", " ")
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
return string(response), nil
65+
}
66+
67+
// ExecCredential is used by exec-based plugins to communicate credentials to HTTP transports.
68+
type ExecCredential struct {
69+
// APIVersion defines the versioned schema of this representation of an object.
70+
// Servers should convert recognized schemas to the latest internal value, and
71+
// may reject unrecognized values.
72+
APIVersion string `json:"apiVersion,omitempty"`
73+
74+
// Kind is a string value representing the REST resource this object represents.
75+
// Servers may infer this from the endpoint the client submits requests to.
76+
Kind string `json:"kind,omitempty"`
77+
78+
// Status is filled in by the plugin and holds the credentials that the transport
79+
// should use to contact the API.
80+
Status *ExecCredentialStatus `json:"status,omitempty"`
81+
}
82+
83+
// ExecCredentialStatus holds credentials for the transport to use.
84+
type ExecCredentialStatus struct {
85+
// Token is a bearer token used by the client for request authentication.
86+
Token string `json:"token,omitempty"`
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package k8s
2+
3+
import (
4+
"os"
5+
"path"
6+
"testing"
7+
8+
"github.com/scaleway/scaleway-cli/v2/internal/core"
9+
"github.com/scaleway/scaleway-sdk-go/scw"
10+
)
11+
12+
func Test_ExecCredential(t *testing.T) {
13+
// expect to return default secret_key
14+
t.Run("simple", core.Test(&core.TestConfig{
15+
Commands: GetCommands(),
16+
TmpHomeDir: true,
17+
BeforeFunc: beforeFuncCreateFullConfig(),
18+
Cmd: "scw k8s exec-credential",
19+
Check: core.TestCheckCombine(
20+
core.TestCheckExitCode(0),
21+
core.TestCheckGolden(),
22+
),
23+
}))
24+
25+
// expect to return 66666666-6666-6666-6666-666666666666
26+
t.Run("with scw_secret_key env", core.Test(&core.TestConfig{
27+
Commands: GetCommands(),
28+
TmpHomeDir: true,
29+
BeforeFunc: beforeFuncCreateFullConfig(),
30+
Cmd: "scw k8s exec-credential",
31+
OverrideEnv: map[string]string{
32+
scw.ScwSecretKeyEnv: "66666666-6666-6666-6666-666666666666",
33+
},
34+
Check: core.TestCheckCombine(
35+
core.TestCheckExitCode(0),
36+
core.TestCheckGolden(),
37+
),
38+
}))
39+
40+
// expect to return p2 secret_key
41+
t.Run("with profile env", core.Test(&core.TestConfig{
42+
Commands: GetCommands(),
43+
TmpHomeDir: true,
44+
BeforeFunc: beforeFuncCreateFullConfig(),
45+
Cmd: "scw k8s exec-credential",
46+
OverrideEnv: map[string]string{
47+
scw.ScwActiveProfileEnv: "p2",
48+
},
49+
Check: core.TestCheckCombine(
50+
core.TestCheckExitCode(0),
51+
core.TestCheckGolden(),
52+
),
53+
}))
54+
55+
// expect to return p3 secret_key
56+
t.Run("with profile flag", core.Test(&core.TestConfig{
57+
Commands: GetCommands(),
58+
TmpHomeDir: true,
59+
BeforeFunc: beforeFuncCreateFullConfig(),
60+
Cmd: "scw --profile p3 k8s exec-credential",
61+
Check: core.TestCheckCombine(
62+
core.TestCheckExitCode(0),
63+
core.TestCheckGolden(),
64+
),
65+
}))
66+
67+
// expect to return p3 secret_key
68+
t.Run("with profile env and flag", core.Test(&core.TestConfig{
69+
Commands: GetCommands(),
70+
TmpHomeDir: true,
71+
BeforeFunc: beforeFuncCreateFullConfig(),
72+
Cmd: "scw --profile p3 k8s exec-credential",
73+
OverrideEnv: map[string]string{
74+
scw.ScwActiveProfileEnv: "p2",
75+
},
76+
Check: core.TestCheckCombine(
77+
core.TestCheckExitCode(0),
78+
core.TestCheckGolden(),
79+
),
80+
}))
81+
}
82+
83+
func beforeFuncCreateConfigFile(c *scw.Config) core.BeforeFunc {
84+
return func(ctx *core.BeforeFuncCtx) error {
85+
homeDir := ctx.OverrideEnv["HOME"]
86+
scwDir := path.Join(homeDir, ".config", "scw")
87+
err := os.MkdirAll(scwDir, 0755)
88+
if err != nil {
89+
return err
90+
}
91+
92+
return c.SaveTo(path.Join(scwDir, "config.yaml"))
93+
}
94+
}
95+
96+
func beforeFuncCreateFullConfig() core.BeforeFunc {
97+
return beforeFuncCreateConfigFile(&scw.Config{
98+
Profile: scw.Profile{
99+
AccessKey: scw.StringPtr("SCWXXXXXXXXXXXXXXXXX"),
100+
SecretKey: scw.StringPtr("00000000-0000-0000-0000-111111111111"),
101+
APIURL: scw.StringPtr("https://mock-api-url.com"),
102+
Insecure: scw.BoolPtr(true),
103+
DefaultOrganizationID: scw.StringPtr("deadbeef-dead-dead-dead-deaddeafbeef"),
104+
DefaultRegion: scw.StringPtr("fr-par"),
105+
DefaultZone: scw.StringPtr("fr-par-1"),
106+
SendTelemetry: scw.BoolPtr(true),
107+
},
108+
Profiles: map[string]*scw.Profile{
109+
"p2": {
110+
AccessKey: scw.StringPtr("SCWP2XXXXXXXXXXXXXXX"),
111+
SecretKey: scw.StringPtr("00000000-0000-0000-0000-222222222222"),
112+
APIURL: scw.StringPtr("https://p2-mock-api-url.com"),
113+
Insecure: scw.BoolPtr(true),
114+
DefaultOrganizationID: scw.StringPtr("deadbeef-dead-dead-dead-deaddeafbeef"),
115+
DefaultRegion: scw.StringPtr("fr-par"),
116+
DefaultZone: scw.StringPtr("fr-par-1"),
117+
SendTelemetry: scw.BoolPtr(true),
118+
},
119+
"p3": {
120+
AccessKey: scw.StringPtr("SCWP3XXXXXXXXXXXXXXX"),
121+
SecretKey: scw.StringPtr("00000000-0000-0000-0000-333333333333"),
122+
APIURL: scw.StringPtr("https://p3-mock-api-url.com"),
123+
Insecure: scw.BoolPtr(true),
124+
DefaultOrganizationID: scw.StringPtr("deadbeef-dead-dead-dead-deaddeafbeef"),
125+
DefaultRegion: scw.StringPtr("fr-par"),
126+
DefaultZone: scw.StringPtr("fr-par-1"),
127+
SendTelemetry: scw.BoolPtr(true),
128+
},
129+
},
130+
})
131+
}

0 commit comments

Comments
 (0)