Skip to content

Commit 47600e4

Browse files
committed
feat: add static token authentication support to kubeconfig()
- Add token field to KubeconfgCluster struct for bearer token auth - Update kubeconfig template to support both token and exec-based auth - Exec-based auth takes priority when both are provided - Add comprehensive tests for token and exec authentication - Add TypeScript definitions for token field - Add example stack files demonstrating usage - Add Kubernetes integration guide to website docs - Update DSL quick reference with kubeconfig examples Useful for CI/CD pipelines, service accounts, and environments without cloud CLI tools installed.
1 parent 0d8d66c commit 47600e4

8 files changed

Lines changed: 700 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.7.1] - 2025-10-29
11+
12+
### Added
13+
- **`kubeconfig()` now supports static token authentication** - Added `token` field for bearer token auth
14+
- Use `token: "your-token"` instead of `exec_command` for simplified authentication
15+
- Mutually exclusive with exec-based authentication (exec_command takes priority if both provided)
16+
- Useful for CI/CD pipelines, service accounts, and environments without cloud CLI tools
17+
- Example: `kubeconfig({ clusters: [{ context: "ctx", host: "...", cert: "...", token: "..." }] })`
18+
1019
## [0.7.0] - 2025-10-29
1120

1221
### Fixed
@@ -204,7 +213,8 @@ bootstrap:
204213
- Support for Terraform and OpenTofu
205214
- CLI commands: plan, apply, destroy, list, output, clean
206215

207-
[Unreleased]: https://github.com/moonwalker/comet/compare/v0.7.0...HEAD
216+
[Unreleased]: https://github.com/moonwalker/comet/compare/v0.7.1...HEAD
217+
[0.7.1]: https://github.com/moonwalker/comet/compare/v0.7.0...v0.7.1
208218
[0.7.0]: https://github.com/moonwalker/comet/releases/tag/v0.7.0
209219
[0.6.8]: https://github.com/moonwalker/comet/releases/tag/v0.6.8
210220
[0.6.7]: https://github.com/moonwalker/comet/releases/tag/v0.6.7

docs/dsl-quick-reference.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,47 @@ secrets('sops://other-file.yaml#/special/secret')
4242
secrets('op://vault/item/field')
4343
```
4444

45+
## Kubernetes Config
46+
47+
Configure kubectl access for your clusters:
48+
49+
```javascript
50+
// Token-based authentication (simple, for CI/CD)
51+
kubeconfig({
52+
current: 0,
53+
clusters: [{
54+
context: 'my-cluster',
55+
host: 'https://kubernetes.example.com',
56+
cert: 'LS0tLS1CRUdJTi...', // Base64-encoded CA cert
57+
token: 'dop_v1_...' // Static bearer token
58+
}]
59+
})
60+
61+
// Exec-based authentication (more secure, uses cloud CLI)
62+
kubeconfig({
63+
current: 0,
64+
clusters: [{
65+
context: 'my-cluster',
66+
host: 'https://kubernetes.example.com',
67+
cert: 'LS0tLS1CRUdJTi...',
68+
exec_command: 'doctl',
69+
exec_args: ['kubernetes', 'cluster', 'kubeconfig', 'exec-credential']
70+
}]
71+
})
72+
73+
// Use with secrets (recommended)
74+
const cluster = secret('k8s/cluster')
75+
kubeconfig({
76+
current: 0,
77+
clusters: [{
78+
context: cluster.context,
79+
host: cluster.host,
80+
cert: cluster.cert,
81+
token: cluster.token
82+
}]
83+
})
84+
```
85+
4586
## Userland Patterns
4687

4788
Create your own helpers for your team's patterns:

internal/schema/kubeconfig.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type (
2525
Context string `json:"context"`
2626
Host string `json:"host"`
2727
Cert string `json:"cert"`
28+
Token string `json:"token"` // Static token authentication
2829
ExecApiVersion string `json:"exec_apiversion"`
2930
ExecCommand string `json:"exec_command"`
3031
ExecArgs interface{} `json:"exec_args"` // Can be []string or string (template)
@@ -208,6 +209,7 @@ users:
208209
{{- range .Clusters }}
209210
- name: {{ .Context }}
210211
user:
212+
{{- if .ExecCommand }}
211213
exec:
212214
apiVersion: {{ .ExecApiVersion }}
213215
command: {{ .ExecCommand }}
@@ -217,4 +219,7 @@ users:
217219
- {{ . }}
218220
{{- end }}
219221
{{- end }}
222+
{{- else if .Token }}
223+
token: {{ .Token }}
224+
{{- end }}
220225
{{- end }}`

internal/schema/kubeconfig_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package schema
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
"text/template"
8+
)
9+
10+
func TestKubeconfigTemplate(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
config *Kubeconfig
14+
wantAuth string // Expected auth section in output
15+
}{
16+
{
17+
name: "token authentication",
18+
config: &Kubeconfig{
19+
Current: 0,
20+
Clusters: []*KubeconfgCluster{
21+
{
22+
Context: "test-cluster",
23+
Host: "https://kubernetes.example.com",
24+
Cert: "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t",
25+
Token: "test-token-12345",
26+
ExecApiVersion: "client.authentication.k8s.io/v1beta1",
27+
},
28+
},
29+
},
30+
wantAuth: "token: test-token-12345",
31+
},
32+
{
33+
name: "exec authentication",
34+
config: &Kubeconfig{
35+
Current: 0,
36+
Clusters: []*KubeconfgCluster{
37+
{
38+
Context: "test-cluster",
39+
Host: "https://kubernetes.example.com",
40+
Cert: "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t",
41+
ExecCommand: "kubectl",
42+
ExecArgs: []string{"get-token"},
43+
ExecApiVersion: "client.authentication.k8s.io/v1beta1",
44+
},
45+
},
46+
},
47+
wantAuth: "command: kubectl",
48+
},
49+
{
50+
name: "exec takes priority over token",
51+
config: &Kubeconfig{
52+
Current: 0,
53+
Clusters: []*KubeconfgCluster{
54+
{
55+
Context: "test-cluster",
56+
Host: "https://kubernetes.example.com",
57+
Cert: "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t",
58+
Token: "test-token-12345",
59+
ExecCommand: "kubectl",
60+
ExecArgs: []string{"get-token"},
61+
ExecApiVersion: "client.authentication.k8s.io/v1beta1",
62+
},
63+
},
64+
},
65+
wantAuth: "command: kubectl",
66+
},
67+
{
68+
name: "no authentication",
69+
config: &Kubeconfig{
70+
Current: 0,
71+
Clusters: []*KubeconfgCluster{
72+
{
73+
Context: "test-cluster",
74+
Host: "https://kubernetes.example.com",
75+
Cert: "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t",
76+
ExecApiVersion: "client.authentication.k8s.io/v1beta1",
77+
},
78+
},
79+
},
80+
wantAuth: "", // No auth expected
81+
},
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
// Normalize exec args
87+
for _, c := range tt.config.Clusters {
88+
c.ExecArgs = normalizeExecArgs(c.ExecArgs)
89+
}
90+
91+
// Render template
92+
tmpl, err := template.New("k").Parse(kubeconfigTemplate)
93+
if err != nil {
94+
t.Fatalf("template.Parse() error = %v", err)
95+
}
96+
97+
var buf bytes.Buffer
98+
err = tmpl.Execute(&buf, tt.config)
99+
if err != nil {
100+
t.Fatalf("template.Execute() error = %v", err)
101+
}
102+
103+
output := buf.String()
104+
105+
// Check for expected auth if specified
106+
if tt.wantAuth != "" {
107+
if !strings.Contains(output, tt.wantAuth) {
108+
t.Errorf("output missing expected auth: %q\nGot:\n%s", tt.wantAuth, output)
109+
}
110+
}
111+
112+
// Ensure valid YAML structure
113+
if !strings.Contains(output, "apiVersion: v1") {
114+
t.Errorf("output missing apiVersion")
115+
}
116+
if !strings.Contains(output, "kind: Config") {
117+
t.Errorf("output missing kind")
118+
}
119+
if !strings.Contains(output, "current-context: test-cluster") {
120+
t.Errorf("output missing current-context")
121+
}
122+
})
123+
}
124+
}
125+
126+
func TestNormalizeExecArgs(t *testing.T) {
127+
tests := []struct {
128+
name string
129+
args interface{}
130+
want []string
131+
}{
132+
{
133+
name: "nil args",
134+
args: nil,
135+
want: nil,
136+
},
137+
{
138+
name: "string slice",
139+
args: []string{"arg1", "arg2"},
140+
want: []string{"arg1", "arg2"},
141+
},
142+
{
143+
name: "interface slice",
144+
args: []interface{}{"arg1", "arg2", 123},
145+
want: []string{"arg1", "arg2", "123"},
146+
},
147+
{
148+
name: "JSON array string",
149+
args: `["arg1", "arg2"]`,
150+
want: []string{"arg1", "arg2"},
151+
},
152+
{
153+
name: "Go slice string",
154+
args: "[arg1 arg2 arg3]",
155+
want: []string{"arg1", "arg2", "arg3"},
156+
},
157+
{
158+
name: "single string",
159+
args: "single-arg",
160+
want: []string{"single-arg"},
161+
},
162+
{
163+
name: "empty string",
164+
args: "",
165+
want: nil,
166+
},
167+
}
168+
169+
for _, tt := range tests {
170+
t.Run(tt.name, func(t *testing.T) {
171+
got := normalizeExecArgs(tt.args)
172+
if len(got) != len(tt.want) {
173+
t.Errorf("normalizeExecArgs() = %v, want %v", got, tt.want)
174+
return
175+
}
176+
for i := range got {
177+
if got[i] != tt.want[i] {
178+
t.Errorf("normalizeExecArgs()[%d] = %v, want %v", i, got[i], tt.want[i])
179+
}
180+
}
181+
})
182+
}
183+
}

internal/types/index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ export interface KubeconfigCluster {
103103
host: string;
104104
/** Base64-encoded cluster CA certificate */
105105
cert: string;
106-
/** Exec plugin command */
106+
/** Static bearer token for authentication (mutually exclusive with exec_command) */
107+
token?: string;
108+
/** Exec plugin command (mutually exclusive with token) */
107109
exec_command?: string;
108110
/** Exec plugin arguments */
109111
exec_args?: string[];
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Example: Using static token authentication in kubeconfig()
2+
//
3+
// This demonstrates how to use token-based authentication instead of
4+
// exec-based credential providers. Useful for CI/CD pipelines and
5+
// simplified environments.
6+
7+
stack({
8+
name: 'token-auth-example',
9+
backend: 'local'
10+
})
11+
12+
// Example 1: Token-based authentication
13+
kubeconfig({
14+
current: 0,
15+
clusters: [
16+
{
17+
context: 'my-cluster-token',
18+
host: 'https://kubernetes.example.com',
19+
cert: 'LS0tLS1CRUdJTi...', // Base64-encoded CA cert
20+
token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6Ij...' // Static bearer token
21+
}
22+
]
23+
})
24+
25+
// Example 2: Exec-based authentication (existing behavior)
26+
kubeconfig({
27+
current: 0,
28+
clusters: [
29+
{
30+
context: 'my-cluster-exec',
31+
host: 'https://kubernetes.example.com',
32+
cert: 'LS0tLS1CRUdJTi...',
33+
exec_command: 'doctl',
34+
exec_args: ['kubernetes', 'cluster', 'kubeconfig', 'exec-credential', '--version=v1beta1', 'my-cluster']
35+
}
36+
]
37+
})
38+
39+
// Example 3: Using with secrets (recommended for tokens)
40+
// const cluster = secret('op://vault/k8s-cluster/config')
41+
//
42+
// kubeconfig({
43+
// current: 0,
44+
// clusters: [
45+
// {
46+
// context: cluster.context,
47+
// host: cluster.host,
48+
// cert: cluster.cert,
49+
// token: cluster.token // Token from secrets provider
50+
// }
51+
// ]
52+
// })
53+
54+
// Example 4: Multiple clusters with mixed authentication
55+
kubeconfig({
56+
current: 0,
57+
clusters: [
58+
{
59+
context: 'prod-cluster',
60+
host: 'https://prod.k8s.example.com',
61+
cert: 'LS0tLS1CRUdJTi...',
62+
exec_command: 'gcloud',
63+
exec_args: ['container', 'clusters', 'get-credentials', 'prod-cluster']
64+
},
65+
{
66+
context: 'ci-cluster',
67+
host: 'https://ci.k8s.example.com',
68+
cert: 'LS0tLS1CRUdJTi...',
69+
token: 'service-account-token-here' // Static token for CI
70+
}
71+
]
72+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Test stack for token authentication feature
2+
stack({
3+
name: 'test-token-auth',
4+
backend: 'local'
5+
})
6+
7+
// Using token authentication
8+
kubeconfig({
9+
current: 0,
10+
clusters: [
11+
{
12+
context: 'test-cluster',
13+
host: 'https://kubernetes.example.com',
14+
cert: 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTXpBME16azRPRFF3SGhjTk1qUXhNREkwTVRrd056STBXaGNOTXpReE1ESXlNVGt3TnpJMApXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTXpBME16azRPRFF3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSMWlLK05qV0h1YzY1WVZXK3pjNVhOcmcrMGRiQWNwZDVBekE3cW9aNXoKdVdqNVlTOWFaOVdMSlg3dHh5ME5kZUlOQmFtZ3hHQWNxWGFLUWJmbmlvMEl3UURBT0JnTlZIUThCQWY4RUJBTUNBY0l3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVeG92OFJCUXJwYjRvK1hOYlRaNm9vCnN3RDdnVVF3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQU9NMkFrdkpPa2lqQytGdTRnVWVBT1pUeTBxdTdOcnYKY0lFRXRVTmpWQW5wQWlFQTE1ZEoxY3k4SlUwSUlSOUdDT2RqK0ZUVTJyVUhEYlhab0I0MXBOZitTVjg9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K',
15+
token: 'dop_v1_1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab'
16+
}
17+
]
18+
})

0 commit comments

Comments
 (0)