Skip to content

Commit e6c5e32

Browse files
mjudeikisclaude
andauthored
fix(infrastructure): don't retarget scoped provider kubeconfig; auto-discover workspace path (#336)
The operator rewrote the provider kubeconfig host to /clusters/<providerWorkspace> (a path). But the admin-portal provider kubeconfig is already scoped to the provider workspace by cluster ID, and front proxies that route by cluster ID 404 on the path form — so bootstrap failed with "the server could not find the requested resource" when creating CRDs. - RetargetHostToWorkspace now leaves an already-scoped (non-root) host untouched; it only retargets a root-scoped / unscoped host (the dev admin-kubeconfig case). - providerWorkspace is now optional: when unset, the operator discovers the path from the workspace's kcp.io/path annotation (LogicalCluster). It's only needed for stamping spec.export.path on the APIExportEndpointSlice. - CRD: providerWorkspace +optional (regenerated). Chart defaults it to "" and the CR omits it when empty. So a workspace-scoped provider kubeconfig needs no providerWorkspace and is used as-is. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 22ac514 commit e6c5e32

8 files changed

Lines changed: 97 additions & 13 deletions

File tree

providers/infrastructure/apis/v1alpha1/types_infrastructureprovider.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,14 @@ type InfrastructureProvider struct {
5050
// InfrastructureProviderSpec is the operator's input.
5151
type InfrastructureProviderSpec struct {
5252
// ProviderWorkspace is the kcp workspace path the provider is bootstrapped
53-
// into, e.g. "root:kedge:providers:infrastructure". The provider kubeconfig
54-
// is retargeted at this workspace.
55-
ProviderWorkspace string `json:"providerWorkspace"`
53+
// into, e.g. "root:kedge:providers:infrastructure". Optional: when the
54+
// provider kubeconfig is already scoped to the provider workspace (as the
55+
// admin portal issues it), the operator discovers the path from the
56+
// workspace's kcp.io/path annotation, so you don't need to set this. Set it
57+
// only when supplying a root-scoped (admin) kubeconfig that must be retargeted
58+
// at a workspace.
59+
// +optional
60+
ProviderWorkspace string `json:"providerWorkspace,omitempty"`
5661

5762
// ProviderKubeconfigSecret references a Secret holding the kcp provider
5863
// kubeconfig (the credential the admin portal issues for the provider

providers/infrastructure/config/crds/infrastructure.kedge.faros.sh_infrastructureproviders.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,12 @@ spec:
187187
providerWorkspace:
188188
description: |-
189189
ProviderWorkspace is the kcp workspace path the provider is bootstrapped
190-
into, e.g. "root:kedge:providers:infrastructure". The provider kubeconfig
191-
is retargeted at this workspace.
190+
into, e.g. "root:kedge:providers:infrastructure". Optional: when the
191+
provider kubeconfig is already scoped to the provider workspace (as the
192+
admin portal issues it), the operator discovers the path from the
193+
workspace's kcp.io/path annotation, so you don't need to set this. Set it
194+
only when supplying a root-scoped (admin) kubeconfig that must be retargeted
195+
at a workspace.
192196
type: string
193197
runtimeKubeconfigSecret:
194198
description: |-
@@ -212,7 +216,6 @@ spec:
212216
- kro
213217
- provider
214218
- providerKubeconfigSecret
215-
- providerWorkspace
216219
type: object
217220
status:
218221
description: InfrastructureProviderStatus reports the operator's reconcile

providers/infrastructure/deploy/chart/crds/infrastructure.kedge.faros.sh_infrastructureproviders.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,12 @@ spec:
187187
providerWorkspace:
188188
description: |-
189189
ProviderWorkspace is the kcp workspace path the provider is bootstrapped
190-
into, e.g. "root:kedge:providers:infrastructure". The provider kubeconfig
191-
is retargeted at this workspace.
190+
into, e.g. "root:kedge:providers:infrastructure". Optional: when the
191+
provider kubeconfig is already scoped to the provider workspace (as the
192+
admin portal issues it), the operator discovers the path from the
193+
workspace's kcp.io/path annotation, so you don't need to set this. Set it
194+
only when supplying a root-scoped (admin) kubeconfig that must be retargeted
195+
at a workspace.
192196
type: string
193197
runtimeKubeconfigSecret:
194198
description: |-
@@ -212,7 +216,6 @@ spec:
212216
- kro
213217
- provider
214218
- providerKubeconfigSecret
215-
- providerWorkspace
216219
type: object
217220
status:
218221
description: InfrastructureProviderStatus reports the operator's reconcile

providers/infrastructure/deploy/chart/templates/operator-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ metadata:
4040
labels:
4141
{{- include "infrastructure.labels" . | nindent 4 }}
4242
spec:
43+
{{- if .Values.operator.providerWorkspace }}
4344
providerWorkspace: {{ .Values.operator.providerWorkspace | quote }}
45+
{{- end }}
4446
providerKubeconfigSecret:
4547
name: {{ $providerSecret }}
4648
key: {{ .Values.operator.providerKubeconfigSecret.key | default "kubeconfig" }}

providers/infrastructure/deploy/chart/values.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,11 @@ operator:
157157
cpu: 50m
158158
memory: 64Mi
159159

160-
# kcp workspace the provider is bootstrapped into.
161-
providerWorkspace: root:kedge:providers:infrastructure
160+
# kcp workspace the provider is bootstrapped into. Leave empty when the
161+
# provider kubeconfig is already scoped to the provider workspace (the operator
162+
# discovers the path from the workspace's kcp.io/path annotation). Set it only
163+
# for a root-scoped (admin) kubeconfig that must be retargeted at a workspace.
164+
providerWorkspace: ""
162165

163166
# Provider (kcp) kubeconfig. Either set it inline via
164167
# --set-file operator.providerKubeconfig=./provider-infrastructure.kubeconfig

providers/infrastructure/install/operatorseed.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ func RetargetHostToWorkspace(host, workspacePath string) (string, error) {
4040
return "", fmt.Errorf("parse host %q: %w", host, err)
4141
}
4242
if idx := strings.Index(u.Path, "/clusters/"); idx >= 0 {
43+
// Already cluster-scoped. Leave a specific workspace (cluster ID, e.g.
44+
// the admin-portal-issued provider kubeconfig, or a workspace path)
45+
// untouched — only a "root"-scoped host is retargeted. Front proxies that
46+
// route by cluster ID 404 on a path form, so rewriting an already-scoped
47+
// kubeconfig breaks access.
48+
seg := strings.SplitN(strings.TrimPrefix(u.Path[idx:], "/clusters/"), "/", 2)[0]
49+
if seg != "" && seg != "root" {
50+
return host, nil
51+
}
4352
u.Path = u.Path[:idx]
4453
}
4554
u.Path = strings.TrimRight(u.Path, "/") + "/clusters/" + workspacePath

providers/infrastructure/operator/controller.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
7676
return r.fail(ctx, &cr, v1alpha1.ConditionBootstrapped, "ProviderKubeconfigInvalid", err)
7777
}
7878

79+
// The workspace path is only needed to stamp spec.export.path on the
80+
// APIExportEndpointSlice. When the CR doesn't set it, discover it from the
81+
// workspace the provider kubeconfig is scoped to (its kcp.io/path annotation)
82+
// — so a workspace-scoped provider kubeconfig needs no providerWorkspace.
83+
workspacePath := cr.Spec.ProviderWorkspace
84+
if workspacePath == "" {
85+
workspacePath, err = discoverWorkspacePath(ctx, providerCfg)
86+
if err != nil {
87+
return r.fail(ctx, &cr, v1alpha1.ConditionBootstrapped, "WorkspacePathUnknown", err)
88+
}
89+
}
90+
7991
// Runtime cluster: an explicit kubeconfig Secret, or — when omitted — the
8092
// operator's own cluster (in-cluster / current context). In the in-cluster
8193
// case runtimeKC stays nil: helm runs without a KUBECONFIG override (using
@@ -103,7 +115,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
103115

104116
// 1. Bootstrap the provider workspace.
105117
if err := Bootstrap(ctx, providerCfg, BootstrapOptions{
106-
WorkspacePath: cr.Spec.ProviderWorkspace,
118+
WorkspacePath: workspacePath,
107119
APIExportName: APIExportName,
108120
}); err != nil {
109121
return r.fail(ctx, &cr, v1alpha1.ConditionBootstrapped, "BootstrapFailed", err)
@@ -118,7 +130,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
118130
if err := ensureNamespace(ctx, cs, cr.Spec.Kro.Namespace); err != nil {
119131
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "KroNamespaceFailed", err)
120132
}
121-
if err := install.SeedKroClusterFromKubeconfig(ctx, runtimeCfg, providerKC, cr.Spec.ProviderWorkspace); err != nil {
133+
if err := install.SeedKroClusterFromKubeconfig(ctx, runtimeCfg, providerKC, workspacePath); err != nil {
122134
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "KroSeedFailed", err)
123135
}
124136
// helm needs a KUBECONFIG file only for an explicit runtime; for the
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright 2026 The Faros Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
*/
10+
11+
package operator
12+
13+
import (
14+
"context"
15+
"fmt"
16+
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
"k8s.io/apimachinery/pkg/runtime/schema"
19+
"k8s.io/client-go/dynamic"
20+
"k8s.io/client-go/rest"
21+
)
22+
23+
// logicalClusterGVR is kcp's per-workspace LogicalCluster singleton.
24+
var logicalClusterGVR = schema.GroupVersionResource{
25+
Group: "core.kcp.io",
26+
Version: "v1alpha1",
27+
Resource: "logicalclusters",
28+
}
29+
30+
// discoverWorkspacePath returns the kcp workspace path the supplied config is
31+
// scoped to, read from the LogicalCluster's kcp.io/path annotation. This lets a
32+
// workspace-scoped provider kubeconfig drive the operator without the caller
33+
// having to also pass spec.providerWorkspace.
34+
func discoverWorkspacePath(ctx context.Context, cfg *rest.Config) (string, error) {
35+
dyn, err := dynamic.NewForConfig(cfg)
36+
if err != nil {
37+
return "", err
38+
}
39+
obj, err := dyn.Resource(logicalClusterGVR).Get(ctx, "cluster", metav1.GetOptions{})
40+
if err != nil {
41+
return "", fmt.Errorf("get LogicalCluster to resolve workspace path: %w", err)
42+
}
43+
if path := obj.GetAnnotations()["kcp.io/path"]; path != "" {
44+
return path, nil
45+
}
46+
return "", fmt.Errorf("LogicalCluster has no kcp.io/path annotation; set spec.providerWorkspace explicitly")
47+
}

0 commit comments

Comments
 (0)