Skip to content

Commit 22ac514

Browse files
mjudeikisclaude
andauthored
feat(infrastructure): operator defaults runtime to in-cluster + grants SA RBAC (#334)
Make spec.runtimeKubeconfigSecret optional: when omitted the operator uses its own cluster (in-cluster config) as the runtime — kro + the serve Deployment land in the cluster the operator runs in. With it set, behavior is unchanged. - CRD: runtimeKubeconfigSecret +optional (regenerated CRD YAML + chart copy) - reconciler: fall back to the manager's rest.Config when no runtime Secret; run helm with no KUBECONFIG override (in-cluster creds) in that case - serve: in-cluster runtime drops the runtime-kubeconfig mount/KRO_KUBECONFIG and runs the pod under a ServiceAccount bound to cluster-admin (ensureServeRBAC) - controller_manager: kro backend falls back to in-cluster when KRO_KUBECONFIG is unset and running in a pod - chart: operator SA bound to cluster-admin (operator.clusterAdmin, default true) so it can helm-install kro (ClusterRoles/CRDs) + manage runtime workloads; operator-config CR omits runtimeKubeconfigSecret when none is provided Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f542cce commit 22ac514

10 files changed

Lines changed: 166 additions & 30 deletions

File tree

providers/infrastructure/apis/v1alpha1/types_infrastructureprovider.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,12 @@ type InfrastructureProviderSpec struct {
6161
ProviderKubeconfigSecret SecretKeyRef `json:"providerKubeconfigSecret"`
6262

6363
// RuntimeKubeconfigSecret references a Secret holding the kubeconfig of the
64-
// cluster where kro and the provider serve Deployment run.
65-
RuntimeKubeconfigSecret SecretKeyRef `json:"runtimeKubeconfigSecret"`
64+
// cluster where kro and the provider serve Deployment run. Optional: when
65+
// omitted the operator uses its own cluster (in-cluster config) as the
66+
// runtime — i.e. kro + the serve Deployment land in the cluster the operator
67+
// runs in.
68+
// +optional
69+
RuntimeKubeconfigSecret SecretKeyRef `json:"runtimeKubeconfigSecret,omitempty"`
6670

6771
// Hub configures the provider's heartbeat target. Optional.
6872
// +optional

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,10 @@ spec:
193193
runtimeKubeconfigSecret:
194194
description: |-
195195
RuntimeKubeconfigSecret references a Secret holding the kubeconfig of the
196-
cluster where kro and the provider serve Deployment run.
196+
cluster where kro and the provider serve Deployment run. Optional: when
197+
omitted the operator uses its own cluster (in-cluster config) as the
198+
runtime — i.e. kro + the serve Deployment land in the cluster the operator
199+
runs in.
197200
properties:
198201
key:
199202
default: kubeconfig
@@ -210,7 +213,6 @@ spec:
210213
- provider
211214
- providerKubeconfigSecret
212215
- providerWorkspace
213-
- runtimeKubeconfigSecret
214216
type: object
215217
status:
216218
description: InfrastructureProviderStatus reports the operator's reconcile

providers/infrastructure/controller_manager.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,32 @@ func startControllerManager(ctx context.Context, config *rest.Config) error {
107107
// kcp workspace. It needs a separate client; KRO_KUBECONFIG points at
108108
// that cluster (the same kubeconfig the legacy kro broker reads). When
109109
// unset we run stub-only so dev/REST-only flows still boot.
110+
// Resolve the kro runtime cluster: explicit KRO_KUBECONFIG, else the pod's
111+
// in-cluster config (the operator's in-cluster-runtime mode — serve runs in
112+
// the runtime cluster and authors RGDs against it via its pod SA). Falls
113+
// back to stub-only when neither is available (dev/REST-only).
114+
var kroCfg *rest.Config
115+
var kroSrc string
110116
if p := os.Getenv("KRO_KUBECONFIG"); p != "" {
111-
kroCfg, err := clientcmd.BuildConfigFromFlags("", p)
117+
c, err := clientcmd.BuildConfigFromFlags("", p)
112118
if err != nil {
113119
return fmt.Errorf("loading KRO_KUBECONFIG for kro backend: %w", err)
114120
}
121+
kroCfg, kroSrc = c, "KRO_KUBECONFIG="+p
122+
} else if c, err := rest.InClusterConfig(); err == nil {
123+
kroCfg, kroSrc = c, "in-cluster"
124+
}
125+
if kroCfg != nil {
115126
kroDyn, err := dynamic.NewForConfig(kroCfg)
116127
if err != nil {
117128
return fmt.Errorf("kro backend dynamic client: %w", err)
118129
}
119130
if err := registry.Register(krobackend.New(kroDyn)); err != nil {
120131
return fmt.Errorf("register kro backend: %w", err)
121132
}
122-
log.Printf("controller manager: kro backend registered (RGD runtime cluster from KRO_KUBECONFIG=%s)", p)
133+
log.Printf("controller manager: kro backend registered (RGD runtime cluster: %s)", kroSrc)
123134
} else {
124-
log.Printf("controller manager: KRO_KUBECONFIG unset — kro backend not registered (stub-only)")
135+
log.Printf("controller manager: no kro runtime config (KRO_KUBECONFIG unset, not in a pod) — kro backend not registered (stub-only)")
125136
}
126137

127138
dyn, err := dynamic.NewForConfig(config)

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,10 @@ spec:
193193
runtimeKubeconfigSecret:
194194
description: |-
195195
RuntimeKubeconfigSecret references a Secret holding the kubeconfig of the
196-
cluster where kro and the provider serve Deployment run.
196+
cluster where kro and the provider serve Deployment run. Optional: when
197+
omitted the operator uses its own cluster (in-cluster config) as the
198+
runtime — i.e. kro + the serve Deployment land in the cluster the operator
199+
runs in.
197200
properties:
198201
key:
199202
default: kubeconfig
@@ -210,7 +213,6 @@ spec:
210213
- provider
211214
- providerKubeconfigSecret
212215
- providerWorkspace
213-
- runtimeKubeconfigSecret
214216
type: object
215217
status:
216218
description: InfrastructureProviderStatus reports the operator's reconcile

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,14 @@ spec:
4444
providerKubeconfigSecret:
4545
name: {{ $providerSecret }}
4646
key: {{ .Values.operator.providerKubeconfigSecret.key | default "kubeconfig" }}
47+
{{- if or .Values.operator.runtimeKubeconfig .Values.operator.runtimeKubeconfigSecret.name }}
4748
runtimeKubeconfigSecret:
4849
name: {{ $runtimeSecret }}
4950
key: {{ .Values.operator.runtimeKubeconfigSecret.key | default "kubeconfig" }}
51+
{{- else }}
52+
# No runtimeKubeconfigSecret → the operator uses its own (in-cluster) cluster
53+
# as the runtime (kro + serve land in the cluster the operator runs in).
54+
{{- end }}
5055
{{- with .Values.hub }}
5156
hub:
5257
url: {{ .url | quote }}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,28 @@ subjects:
4444
- kind: ServiceAccount
4545
name: {{ include "infrastructure.fullname" . }}-operator
4646
namespace: {{ .Release.Namespace }}
47+
{{- if .Values.operator.clusterAdmin }}
48+
---
49+
# The operator helm-installs the kro chart (which creates ClusterRoles, CRDs and
50+
# cluster-scoped objects) and, for the in-cluster runtime, manages namespaces,
51+
# Secrets, Deployments + the serve SA/binding on its own cluster — so its SA
52+
# needs cluster-admin. Set operator.clusterAdmin=false to bind a narrower role
53+
# yourself (e.g. when an explicit runtimeKubeconfig with its own creds is used).
54+
apiVersion: rbac.authorization.k8s.io/v1
55+
kind: ClusterRoleBinding
56+
metadata:
57+
name: {{ include "infrastructure.fullname" . }}-operator-admin
58+
labels:
59+
{{- include "infrastructure.labels" . | nindent 4 }}
60+
roleRef:
61+
apiGroup: rbac.authorization.k8s.io
62+
kind: ClusterRole
63+
name: cluster-admin
64+
subjects:
65+
- kind: ServiceAccount
66+
name: {{ include "infrastructure.fullname" . }}-operator
67+
namespace: {{ .Release.Namespace }}
68+
{{- end }}
4769
---
4870
apiVersion: apps/v1
4971
kind: Deployment

providers/infrastructure/deploy/chart/values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ affinity: {}
138138
operator:
139139
enabled: false
140140

141+
# Bind the operator ServiceAccount to cluster-admin. Required for the operator
142+
# to helm-install the kro chart (which creates ClusterRoles/CRDs) and to manage
143+
# runtime workloads in its own cluster (in-cluster runtime). Set false to wire
144+
# a narrower role yourself.
145+
clusterAdmin: true
146+
141147
# Operator (controller) image. Defaults to the provider image above.
142148
image:
143149
repository: ""

providers/infrastructure/operator/controller.go

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ type Reconciler struct {
4444
// Client reads CRs + referenced Secrets from the cluster the operator runs
4545
// in (where the CRs live).
4646
Client client.Client
47+
// RestConfig is the operator's own cluster config (what the manager was
48+
// built with). Used as the runtime cluster when a CR omits
49+
// spec.runtimeKubeconfigSecret — i.e. "use the current context".
50+
RestConfig *rest.Config
4751
}
4852

4953
// Reconcile drives one CR to its desired state.
@@ -60,10 +64,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
6064
if err != nil {
6165
return r.fail(ctx, &cr, v1alpha1.ConditionBootstrapped, "ProviderKubeconfigMissing", err)
6266
}
63-
runtimeKC, err := r.secretValue(ctx, cr.Namespace, cr.Spec.RuntimeKubeconfigSecret)
64-
if err != nil {
65-
return r.fail(ctx, &cr, v1alpha1.ConditionBootstrapped, "RuntimeKubeconfigMissing", err)
66-
}
6767
var hubToken []byte
6868
if cr.Spec.Hub.TokenSecret != nil {
6969
if hubToken, err = r.secretValue(ctx, cr.Namespace, *cr.Spec.Hub.TokenSecret); err != nil {
@@ -75,9 +75,30 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
7575
if err != nil {
7676
return r.fail(ctx, &cr, v1alpha1.ConditionBootstrapped, "ProviderKubeconfigInvalid", err)
7777
}
78-
runtimeCfg, err := clientcmd.RESTConfigFromKubeConfig(runtimeKC)
79-
if err != nil {
80-
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "RuntimeKubeconfigInvalid", err)
78+
79+
// Runtime cluster: an explicit kubeconfig Secret, or — when omitted — the
80+
// operator's own cluster (in-cluster / current context). In the in-cluster
81+
// case runtimeKC stays nil: helm runs without a KUBECONFIG override (using
82+
// its in-cluster credentials) and the serve Deployment uses its pod SA.
83+
var (
84+
runtimeCfg *rest.Config
85+
runtimeKC []byte
86+
)
87+
if cr.Spec.RuntimeKubeconfigSecret.Name != "" {
88+
runtimeKC, err = r.secretValue(ctx, cr.Namespace, cr.Spec.RuntimeKubeconfigSecret)
89+
if err != nil {
90+
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "RuntimeKubeconfigMissing", err)
91+
}
92+
runtimeCfg, err = clientcmd.RESTConfigFromKubeConfig(runtimeKC)
93+
if err != nil {
94+
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "RuntimeKubeconfigInvalid", err)
95+
}
96+
} else {
97+
if r.RestConfig == nil {
98+
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "RuntimeKubeconfigMissing",
99+
fmt.Errorf("no runtimeKubeconfigSecret and operator has no in-cluster config"))
100+
}
101+
runtimeCfg = r.RestConfig
81102
}
82103

83104
// 1. Bootstrap the provider workspace.
@@ -100,12 +121,19 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
100121
if err := install.SeedKroClusterFromKubeconfig(ctx, runtimeCfg, providerKC, cr.Spec.ProviderWorkspace); err != nil {
101122
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "KroSeedFailed", err)
102123
}
103-
tmp, cleanup, err := writeTempKubeconfig(runtimeKC)
104-
if err != nil {
105-
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "RuntimeKubeconfigWriteFailed", err)
124+
// helm needs a KUBECONFIG file only for an explicit runtime; for the
125+
// in-cluster runtime (runtimeKC nil) we run helm with no override so it uses
126+
// its in-cluster service account.
127+
helmKubeconfig := ""
128+
if runtimeKC != nil {
129+
tmp, cleanup, werr := writeTempKubeconfig(runtimeKC)
130+
if werr != nil {
131+
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "RuntimeKubeconfigWriteFailed", werr)
132+
}
133+
defer cleanup()
134+
helmKubeconfig = tmp
106135
}
107-
defer cleanup()
108-
if err := EnsureKroRelease(ctx, tmp, cr.Spec.Kro); err != nil {
136+
if err := EnsureKroRelease(ctx, helmKubeconfig, cr.Spec.Kro); err != nil {
109137
return r.fail(ctx, &cr, v1alpha1.ConditionKroReleased, "HelmFailed", err)
110138
}
111139
// Dev-only kind networking patches (no-op in prod; gated by env). Lets the
@@ -190,7 +218,7 @@ func Run(ctx context.Context, cfg *rest.Config) error {
190218
if err != nil {
191219
return fmt.Errorf("manager.New: %w", err)
192220
}
193-
if err := (&Reconciler{Client: mgr.GetClient()}).SetupWithManager(mgr); err != nil {
221+
if err := (&Reconciler{Client: mgr.GetClient(), RestConfig: cfg}).SetupWithManager(mgr); err != nil {
194222
return fmt.Errorf("setup reconciler: %w", err)
195223
}
196224
klog.FromContext(ctx).Info("infrastructure operator manager starting")

providers/infrastructure/operator/helm.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ func EnsureKroRelease(ctx context.Context, runtimeKubeconfigPath string, kro v1a
5252
}
5353

5454
cmd := exec.CommandContext(ctx, "helm", args...)
55-
cmd.Env = append(os.Environ(), "KUBECONFIG="+runtimeKubeconfigPath)
55+
// An empty path means "in-cluster runtime": run helm with no KUBECONFIG
56+
// override so it uses the pod's in-cluster service account.
57+
if runtimeKubeconfigPath != "" {
58+
cmd.Env = append(os.Environ(), "KUBECONFIG="+runtimeKubeconfigPath)
59+
}
5660
if out, err := cmd.CombinedOutput(); err != nil {
5761
return fmt.Errorf("helm upgrade --install %s: %w\n%s", kro.ReleaseName, err, string(out))
5862
}

providers/infrastructure/operator/serve.go

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
appsv1 "k8s.io/api/apps/v1"
1818
corev1 "k8s.io/api/core/v1"
19+
rbacv1 "k8s.io/api/rbac/v1"
1920
apierrors "k8s.io/apimachinery/pkg/api/errors"
2021
"k8s.io/apimachinery/pkg/api/resource"
2122
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -52,13 +53,14 @@ func EnsureProviderServe(
5253

5354
name := cr.Name
5455
providerSecret := name + "-provider-kubeconfig"
55-
runtimeSecret := name + "-runtime-kubeconfig"
5656
if err := upsertOpaqueSecret(ctx, cs, ServeNamespace, providerSecret, "kubeconfig", providerKubeconfig); err != nil {
5757
return fmt.Errorf("replicate provider kubeconfig: %w", err)
5858
}
59-
if err := upsertOpaqueSecret(ctx, cs, ServeNamespace, runtimeSecret, "kubeconfig", runtimeKubeconfig); err != nil {
60-
return fmt.Errorf("replicate runtime kubeconfig: %w", err)
61-
}
59+
60+
// inCluster: the runtime is the operator's own cluster (no runtime
61+
// kubeconfig). The serve pod then runs the kro backend with its pod
62+
// ServiceAccount (in-cluster) instead of a mounted runtime kubeconfig.
63+
inCluster := len(runtimeKubeconfig) == 0
6264

6365
port := cr.Spec.Provider.Port
6466
if port == 0 {
@@ -73,7 +75,6 @@ func EnsureProviderServe(
7375
{Name: "PORT", Value: fmt.Sprintf("%d", port)},
7476
{Name: "KEDGE_PROVIDER_NAME", Value: "infrastructure"},
7577
{Name: "INFRASTRUCTURE_KUBECONFIG", Value: providerKubeconfigMount},
76-
{Name: "KRO_KUBECONFIG", Value: runtimeKubeconfigMount},
7778
}
7879
if cr.Spec.Hub.URL != "" {
7980
env = append(env, corev1.EnvVar{Name: "KEDGE_HUB_URL", Value: cr.Spec.Hub.URL})
@@ -83,11 +84,30 @@ func EnsureProviderServe(
8384
}
8485
volMounts := []corev1.VolumeMount{
8586
{Name: "provider-kubeconfig", MountPath: "/var/run/secrets/kedge/provider", ReadOnly: true},
86-
{Name: "runtime-kubeconfig", MountPath: "/var/run/secrets/kedge/runtime", ReadOnly: true},
8787
}
8888
volumes := []corev1.Volume{
8989
{Name: "provider-kubeconfig", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: providerSecret}}},
90-
{Name: "runtime-kubeconfig", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: runtimeSecret}}},
90+
}
91+
// Serve's kro backend reaches the runtime cluster either via a mounted
92+
// runtime kubeconfig (explicit runtime) or its in-cluster SA (in-cluster
93+
// runtime — KRO_KUBECONFIG left unset; controller_manager falls back to
94+
// in-cluster).
95+
serveSA := ""
96+
if !inCluster {
97+
runtimeSecret := name + "-runtime-kubeconfig"
98+
if err := upsertOpaqueSecret(ctx, cs, ServeNamespace, runtimeSecret, "kubeconfig", runtimeKubeconfig); err != nil {
99+
return fmt.Errorf("replicate runtime kubeconfig: %w", err)
100+
}
101+
env = append(env, corev1.EnvVar{Name: "KRO_KUBECONFIG", Value: runtimeKubeconfigMount})
102+
volMounts = append(volMounts, corev1.VolumeMount{Name: "runtime-kubeconfig", MountPath: "/var/run/secrets/kedge/runtime", ReadOnly: true})
103+
volumes = append(volumes, corev1.Volume{Name: "runtime-kubeconfig", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: runtimeSecret}}})
104+
} else {
105+
// Give the serve pod an SA bound to the access its kro backend needs on
106+
// the (operator's own) runtime cluster.
107+
serveSA = name
108+
if err := ensureServeRBAC(ctx, cs, serveSA); err != nil {
109+
return fmt.Errorf("serve RBAC: %w", err)
110+
}
91111
}
92112
if cr.Spec.Hub.TokenSecret != nil && len(hubToken) > 0 {
93113
hubSecret := name + "-hub-token"
@@ -117,6 +137,7 @@ func EnsureProviderServe(
117137
Template: corev1.PodTemplateSpec{
118138
ObjectMeta: metav1.ObjectMeta{Labels: labels},
119139
Spec: corev1.PodSpec{
140+
ServiceAccountName: serveSA,
120141
Containers: []corev1.Container{{
121142
Name: "provider",
122143
Image: image,
@@ -166,6 +187,37 @@ func EnsureProviderServe(
166187
return ensureServeService(ctx, cs, name, labels, port)
167188
}
168189

190+
// ensureServeRBAC creates the serve pod's ServiceAccount (in ServeNamespace)
191+
// and binds it to cluster-admin so its in-cluster kro backend can author
192+
// RGD-defined instances, namespaces, and secrets on the runtime cluster. Used
193+
// only for the in-cluster runtime (no runtime kubeconfig). Scope down for
194+
// least privilege in hardened environments.
195+
func ensureServeRBAC(ctx context.Context, cs kubernetes.Interface, saName string) error {
196+
sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: saName, Namespace: ServeNamespace}}
197+
if _, err := cs.CoreV1().ServiceAccounts(ServeNamespace).Get(ctx, saName, metav1.GetOptions{}); apierrors.IsNotFound(err) {
198+
if _, cerr := cs.CoreV1().ServiceAccounts(ServeNamespace).Create(ctx, sa, metav1.CreateOptions{}); cerr != nil && !apierrors.IsAlreadyExists(cerr) {
199+
return fmt.Errorf("create serve ServiceAccount: %w", cerr)
200+
}
201+
} else if err != nil {
202+
return fmt.Errorf("get serve ServiceAccount: %w", err)
203+
}
204+
205+
crbName := "kedge-infrastructure-serve-" + saName
206+
crb := &rbacv1.ClusterRoleBinding{
207+
ObjectMeta: metav1.ObjectMeta{Name: crbName},
208+
RoleRef: rbacv1.RoleRef{APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: "cluster-admin"},
209+
Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: saName, Namespace: ServeNamespace}},
210+
}
211+
if _, err := cs.RbacV1().ClusterRoleBindings().Get(ctx, crbName, metav1.GetOptions{}); apierrors.IsNotFound(err) {
212+
if _, cerr := cs.RbacV1().ClusterRoleBindings().Create(ctx, crb, metav1.CreateOptions{}); cerr != nil && !apierrors.IsAlreadyExists(cerr) {
213+
return fmt.Errorf("create serve ClusterRoleBinding: %w", cerr)
214+
}
215+
} else if err != nil {
216+
return fmt.Errorf("get serve ClusterRoleBinding: %w", err)
217+
}
218+
return nil
219+
}
220+
169221
func ensureServeService(ctx context.Context, cs kubernetes.Interface, name string, labels map[string]string, port int32) error {
170222
want := &corev1.Service{
171223
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ServeNamespace, Labels: labels},

0 commit comments

Comments
 (0)