Skip to content

Commit f844c6c

Browse files
authored
Merge pull request #221 from datum-cloud/feat/update-kubeconfig-exec-interactive-mode
feat: add --exec-interactive-mode to auth update-kubeconfig
2 parents 2232ec3 + af4d338 commit f844c6c

2 files changed

Lines changed: 88 additions & 3 deletions

File tree

internal/cmd/auth/update-kubeconfig.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
func updateKubeconfigCmd() *cobra.Command {
16-
var kubeconfig, projectName, organizationName, hostname string
16+
var kubeconfig, projectName, organizationName, hostname, execInteractiveMode string
1717

1818
cmd := &cobra.Command{
1919
Use: "update-kubeconfig",
@@ -47,8 +47,20 @@ environments where the hostname cannot be derived from stored credentials).`,
4747
datumctl auth update-kubeconfig --project my-project-id
4848
4949
# Write to a custom kubeconfig file
50-
datumctl auth update-kubeconfig --organization my-org-id --kubeconfig ~/.kube/datum-config`,
50+
datumctl auth update-kubeconfig --organization my-org-id --kubeconfig ~/.kube/datum-config
51+
52+
# Non-interactive exec credential (CI / scripts)
53+
datumctl auth update-kubeconfig --project my-project-id --exec-interactive-mode Never`,
5154
RunE: func(cmd *cobra.Command, args []string) error {
55+
interactiveMode := api.ExecInteractiveMode(execInteractiveMode)
56+
switch interactiveMode {
57+
case api.NeverExecInteractiveMode, api.IfAvailableExecInteractiveMode, api.AlwaysExecInteractiveMode:
58+
default:
59+
return fmt.Errorf("invalid --exec-interactive-mode %q: must be one of %s, %s, %s",
60+
execInteractiveMode,
61+
api.NeverExecInteractiveMode, api.IfAvailableExecInteractiveMode, api.AlwaysExecInteractiveMode)
62+
}
63+
5264
// Determine kubeconfig path
5365
var kubeconfigPath string
5466
if kubeconfig != "" {
@@ -143,7 +155,7 @@ environments where the hostname cannot be derived from stored credentials).`,
143155
Args: execArgs,
144156
APIVersion: "client.authentication.k8s.io/v1",
145157
ProvideClusterInfo: false,
146-
InteractiveMode: "IfAvailable",
158+
InteractiveMode: interactiveMode,
147159
},
148160
}
149161

@@ -166,6 +178,9 @@ environments where the hostname cannot be derived from stored credentials).`,
166178
cmd.Flags().StringVar(&projectName, "project", "", "Configure kubectl to access a specific project's control plane instead of the core control plane.")
167179
cmd.Flags().StringVar(&organizationName, "organization", "", "The organization name that is being connected to.")
168180
cmd.Flags().StringVar(&hostname, "hostname", "", "Override the hostname for the API server")
181+
cmd.Flags().StringVar(&execInteractiveMode, "exec-interactive-mode", string(api.IfAvailableExecInteractiveMode),
182+
fmt.Sprintf("Interactive mode for the kubeconfig exec credential plugin (%s, %s, %s). Use %s for non-interactive contexts such as CI.",
183+
api.NeverExecInteractiveMode, api.IfAvailableExecInteractiveMode, api.AlwaysExecInteractiveMode, api.NeverExecInteractiveMode))
169184

170185
cmd.MarkFlagsOneRequired("project", "organization")
171186
cmd.MarkFlagsMutuallyExclusive("project", "organization")
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package auth
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"k8s.io/client-go/tools/clientcmd"
8+
"k8s.io/client-go/tools/clientcmd/api"
9+
)
10+
11+
func TestUpdateKubeconfigExecInteractiveMode(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
args []string
15+
want api.ExecInteractiveMode
16+
wantErr bool
17+
}{
18+
{
19+
name: "default is IfAvailable",
20+
args: []string{"--project", "p", "--hostname", "https://api.example.test"},
21+
want: api.IfAvailableExecInteractiveMode,
22+
},
23+
{
24+
name: "Never",
25+
args: []string{"--project", "p", "--hostname", "https://api.example.test", "--exec-interactive-mode", "Never"},
26+
want: api.NeverExecInteractiveMode,
27+
},
28+
{
29+
name: "Always",
30+
args: []string{"--project", "p", "--hostname", "https://api.example.test", "--exec-interactive-mode", "Always"},
31+
want: api.AlwaysExecInteractiveMode,
32+
},
33+
{
34+
name: "invalid value rejected",
35+
args: []string{"--project", "p", "--hostname", "https://api.example.test", "--exec-interactive-mode", "Bogus"},
36+
wantErr: true,
37+
},
38+
}
39+
40+
for _, tc := range tests {
41+
t.Run(tc.name, func(t *testing.T) {
42+
kubeconfigPath := filepath.Join(t.TempDir(), "config")
43+
cmd := updateKubeconfigCmd()
44+
cmd.SetArgs(append(tc.args, "--kubeconfig", kubeconfigPath))
45+
46+
err := cmd.Execute()
47+
if tc.wantErr {
48+
if err == nil {
49+
t.Fatal("expected error, got nil")
50+
}
51+
return
52+
}
53+
if err != nil {
54+
t.Fatalf("unexpected error: %v", err)
55+
}
56+
57+
cfg, err := clientcmd.LoadFromFile(kubeconfigPath)
58+
if err != nil {
59+
t.Fatalf("failed to load written kubeconfig: %v", err)
60+
}
61+
user, ok := cfg.AuthInfos["datum-user"]
62+
if !ok || user.Exec == nil {
63+
t.Fatal("datum-user exec auth info not written")
64+
}
65+
if user.Exec.InteractiveMode != tc.want {
66+
t.Errorf("InteractiveMode = %q, want %q", user.Exec.InteractiveMode, tc.want)
67+
}
68+
})
69+
}
70+
}

0 commit comments

Comments
 (0)