Skip to content

Commit e604e33

Browse files
mjudeikis-botOpenClaw Botmjudeikis
authored
fix: allow join token in kedge agent join / kedge install for kubernetes-type (#118)
Two bugs prevented the token-based onboarding flow from working: 1. agentJoinKubernetes required --hub-kubeconfig even when --token was provided, failing immediately with 'hub-kubeconfig is required for kubernetes-type join'. Fix: accept --token as an alternative auth mode. When token is used, the Deployment passes --token directly to the agent binary (no hub-kubeconfig Secret needed). When --hub-kubeconfig is used, the existing Secret+volume-mount flow is preserved. 2. kedge install --type kubernetes created a Deployment that stored the join token in a KEDGE_TOKEN env var but never passed --token to the agent args, so the agent called registerEdge (which requires a kcp kubeconfig) and received 'Unauthorized'. Fix: pass --token={{ .Token }} directly in the container args; remove the now-unused kedge-agent-join-token Secret from the manifest; fix agent command from 'agent join' to 'agent run' for foreground use. Also fixes printAgentRunCmd to use 'agent run' (not 'agent join'). Co-authored-by: OpenClaw Bot <bot@openclaw.ai> Co-authored-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
1 parent 16412e9 commit e604e33

2 files changed

Lines changed: 102 additions & 59 deletions

File tree

pkg/cli/cmd/agent.go

Lines changed: 98 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -260,22 +260,16 @@ func agentJoinServer(opts *agent.Options) error {
260260

261261
// agentJoinKubernetes applies a Deployment + RBAC to the target cluster so the
262262
// agent runs as a persistent in-cluster workload.
263+
//
264+
// Two auth modes are supported:
265+
// - Join-token mode (--token): Deployment runs with --token flag; no kubeconfig Secret needed.
266+
// - Kubeconfig mode (--hub-kubeconfig): Deployment mounts a Secret containing the hub kubeconfig.
263267
func agentJoinKubernetes(opts *agent.Options) error {
264-
if opts.HubKubeconfig == "" {
265-
return fmt.Errorf("--hub-kubeconfig is required for kubernetes-type join")
268+
if opts.HubKubeconfig == "" && opts.Token == "" {
269+
return fmt.Errorf("--hub-kubeconfig or --token is required for kubernetes-type join")
266270
}
267271

268-
// Resolve the hub kubeconfig to absolute path so the mounted Secret is portable.
269-
absKubeconfig, err := filepath.Abs(opts.HubKubeconfig)
270-
if err != nil {
271-
return fmt.Errorf("resolving hub kubeconfig path: %w", err)
272-
}
273-
274-
// Read kubeconfig content to embed as a Secret.
275-
kubeconfigData, err := os.ReadFile(absKubeconfig)
276-
if err != nil {
277-
return fmt.Errorf("reading hub kubeconfig: %w", err)
278-
}
272+
usingToken := opts.Token != ""
279273

280274
// Determine the target cluster kubeconfig for kubectl.
281275
kubectlArgs := []string{}
@@ -296,21 +290,6 @@ metadata:
296290
return fmt.Errorf("creating kedge-system namespace: %w", err)
297291
}
298292

299-
// Create Secret with hub kubeconfig.
300-
secretName := "kedge-agent-" + opts.EdgeName + "-hub-kubeconfig"
301-
secretManifest := fmt.Sprintf(`apiVersion: v1
302-
kind: Secret
303-
metadata:
304-
name: %s
305-
namespace: kedge-system
306-
type: Opaque
307-
stringData:
308-
hub.kubeconfig: |
309-
%s`, secretName, indentLines(string(kubeconfigData), " "))
310-
if err := kubectlApplyManifest(kubectlArgs, secretManifest); err != nil {
311-
return fmt.Errorf("creating hub kubeconfig secret: %w", err)
312-
}
313-
314293
// ServiceAccount.
315294
saManifest := fmt.Sprintf(`apiVersion: v1
316295
kind: ServiceAccount
@@ -322,13 +301,88 @@ metadata:
322301
return fmt.Errorf("creating ServiceAccount: %w", err)
323302
}
324303

325-
// Deployment.
326304
image := "ghcr.io/faroshq/kedge-agent:latest"
327-
deployArgs := fmt.Sprintf("agent run --hub-kubeconfig=/etc/kedge/hub.kubeconfig --edge-name=%s --type=kubernetes --hub-insecure-skip-tls-verify", opts.EdgeName)
328-
if opts.Cluster != "" {
329-
deployArgs += " --cluster=" + opts.Cluster
330-
}
331-
deployManifest := fmt.Sprintf(`apiVersion: apps/v1
305+
306+
var deployManifest string
307+
308+
if usingToken {
309+
// Token-based bootstrap: pass --token directly in Deployment args.
310+
// The agent exchanges the join token for a kubeconfig on first connect;
311+
// no hub kubeconfig Secret is required.
312+
hubURL := opts.HubURL
313+
if hubURL == "" {
314+
return fmt.Errorf("--hub-url is required when using --token for kubernetes-type join")
315+
}
316+
deployArgs := fmt.Sprintf("agent run --hub-url=%s --edge-name=%s --type=kubernetes --token=%s",
317+
hubURL, opts.EdgeName, opts.Token)
318+
if opts.InsecureSkipTLSVerify {
319+
deployArgs += " --hub-insecure-skip-tls-verify"
320+
}
321+
if opts.Cluster != "" {
322+
deployArgs += " --cluster=" + opts.Cluster
323+
}
324+
deployManifest = fmt.Sprintf(`apiVersion: apps/v1
325+
kind: Deployment
326+
metadata:
327+
name: kedge-agent-%s
328+
namespace: kedge-system
329+
labels:
330+
app: kedge-agent
331+
kedge.faros.sh/edge-name: %s
332+
spec:
333+
replicas: 1
334+
selector:
335+
matchLabels:
336+
app: kedge-agent
337+
kedge.faros.sh/edge-name: %s
338+
template:
339+
metadata:
340+
labels:
341+
app: kedge-agent
342+
kedge.faros.sh/edge-name: %s
343+
spec:
344+
serviceAccountName: kedge-agent-%s
345+
containers:
346+
- name: agent
347+
image: %s
348+
command: ["/kedge"]
349+
args: [%s]
350+
`,
351+
opts.EdgeName, opts.EdgeName, opts.EdgeName, opts.EdgeName,
352+
opts.EdgeName, image,
353+
formatDeployArgs(deployArgs))
354+
} else {
355+
// Kubeconfig-based: mount a Secret containing the hub kubeconfig.
356+
absKubeconfig, err := filepath.Abs(opts.HubKubeconfig)
357+
if err != nil {
358+
return fmt.Errorf("resolving hub kubeconfig path: %w", err)
359+
}
360+
kubeconfigData, err := os.ReadFile(absKubeconfig)
361+
if err != nil {
362+
return fmt.Errorf("reading hub kubeconfig: %w", err)
363+
}
364+
secretName := "kedge-agent-" + opts.EdgeName + "-hub-kubeconfig"
365+
secretManifest := fmt.Sprintf(`apiVersion: v1
366+
kind: Secret
367+
metadata:
368+
name: %s
369+
namespace: kedge-system
370+
type: Opaque
371+
stringData:
372+
hub.kubeconfig: |
373+
%s`, secretName, indentLines(string(kubeconfigData), " "))
374+
if err := kubectlApplyManifest(kubectlArgs, secretManifest); err != nil {
375+
return fmt.Errorf("creating hub kubeconfig secret: %w", err)
376+
}
377+
378+
deployArgs := fmt.Sprintf("agent run --hub-kubeconfig=/etc/kedge/hub.kubeconfig --edge-name=%s --type=kubernetes", opts.EdgeName)
379+
if opts.InsecureSkipTLSVerify {
380+
deployArgs += " --hub-insecure-skip-tls-verify"
381+
}
382+
if opts.Cluster != "" {
383+
deployArgs += " --cluster=" + opts.Cluster
384+
}
385+
deployManifest = fmt.Sprintf(`apiVersion: apps/v1
332386
kind: Deployment
333387
metadata:
334388
name: kedge-agent-%s
@@ -363,17 +417,20 @@ spec:
363417
secret:
364418
secretName: %s
365419
`,
366-
opts.EdgeName, opts.EdgeName, opts.EdgeName, opts.EdgeName,
367-
opts.EdgeName, image,
368-
formatDeployArgs(deployArgs),
369-
secretName)
420+
opts.EdgeName, opts.EdgeName, opts.EdgeName, opts.EdgeName,
421+
opts.EdgeName, image,
422+
formatDeployArgs(deployArgs),
423+
secretName)
424+
}
425+
370426
if err := kubectlApplyManifest(kubectlArgs, deployManifest); err != nil {
371427
return fmt.Errorf("creating Deployment: %w", err)
372428
}
373429

374-
fmt.Printf("Agent installed as Kubernetes Deployment in kedge-system.\n")
375-
fmt.Printf(" Check status: kubectl -n kedge-system get deployment kedge-agent-%s\n", opts.EdgeName)
376-
fmt.Printf(" View logs: kubectl -n kedge-system logs -l kedge.faros.sh/edge-name=%s -f\n", opts.EdgeName)
430+
fmt.Printf("✓ kedge-agent deployed to Kubernetes\n")
431+
fmt.Printf(" Namespace: kedge-system\n")
432+
fmt.Printf(" Check status: kubectl get pods -n kedge-system\n")
433+
fmt.Printf(" Logs: kubectl logs -n kedge-system deploy/kedge-agent-%s -f\n", opts.EdgeName)
377434
return nil
378435
}
379436

pkg/cli/cmd/install.go

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,10 @@ WantedBy=multi-user.target
166166

167167
// printAgentRunCmd prints the foreground agent run command.
168168
func printAgentRunCmd(opts *installOptions) {
169-
fmt.Printf(" kedge agent join \\\n")
169+
fmt.Printf(" kedge agent run \\\n")
170170
fmt.Printf(" --hub-url %s \\\n", opts.hubURL)
171171
fmt.Printf(" --edge-name %s \\\n", opts.edgeName)
172-
fmt.Printf(" --type server \\\n")
172+
fmt.Printf(" --type %s \\\n", opts.installType)
173173
fmt.Printf(" --token %s\n", opts.token)
174174
}
175175

@@ -195,15 +195,6 @@ metadata:
195195
name: kedge-agent
196196
namespace: kedge-system
197197
---
198-
apiVersion: v1
199-
kind: Secret
200-
metadata:
201-
name: kedge-agent-join-token
202-
namespace: kedge-system
203-
type: Opaque
204-
stringData:
205-
token: "{{ .Token }}"
206-
---
207198
apiVersion: apps/v1
208199
kind: Deployment
209200
metadata:
@@ -227,16 +218,11 @@ spec:
227218
image: ghcr.io/faroshq/kedge-agent:latest
228219
args:
229220
- agent
230-
- join
221+
- run
231222
- --hub-url={{ .HubURL }}
232223
- --edge-name={{ .EdgeName }}
233224
- --type=kubernetes
234-
env:
235-
- name: KEDGE_TOKEN
236-
valueFrom:
237-
secretKeyRef:
238-
name: kedge-agent-join-token
239-
key: token
225+
- --token={{ .Token }}
240226
envFrom: []
241227
resources:
242228
requests:

0 commit comments

Comments
 (0)