Skip to content

Commit 6f3dd06

Browse files
mjudeikisclaude
andauthored
feat(infrastructure): operator self-registers the CatalogEntry (#337)
The operator bootstrapped the workspace (APIExport + Templates) but never created the CatalogEntry, so the provider never appeared in the catalog/portal. Now, after deploying serve, the operator applies the provider's CatalogEntry (embedded manifest.yaml) into the provider workspace with ui/backend URLs rewritten to the in-cluster serve Service it owns, and stamps spec.version from KEDGE_PROVIDER_VERSION (chart .Chart.AppVersion). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0d729fd commit 6f3dd06

6 files changed

Lines changed: 126 additions & 4 deletions

File tree

providers/infrastructure/apis/v1alpha1/types_infrastructureprovider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,5 @@ const (
193193
ConditionBootstrapped = "Bootstrapped"
194194
ConditionKroReleased = "KroReleased"
195195
ConditionProviderDeployed = "ProviderDeployed"
196+
ConditionRegistered = "Registered"
196197
)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ spec:
9292
image: "{{ .Values.operator.image.repository | default .Values.image.repository }}:{{ .Values.operator.image.tag | default .Values.image.tag | default .Chart.AppVersion }}"
9393
imagePullPolicy: {{ .Values.image.pullPolicy }}
9494
args: ["controller"]
95+
env:
96+
# Stamps spec.version on the CatalogEntry the operator applies, so
97+
# the portal shows the release version (the embedded manifest.yaml
98+
# only carries a 0.1.0 placeholder). Mirrors the chart-templated
99+
# catalogentry.yaml's {{ "{{ .Chart.AppVersion }}" }}.
100+
- name: KEDGE_PROVIDER_VERSION
101+
value: {{ .Chart.AppVersion | quote }}
95102
resources:
96103
{{- toYaml .Values.operator.resources | nindent 12 }}
97104
{{- end }}

providers/infrastructure/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
k8s.io/klog/v2 v2.140.0
1515
sigs.k8s.io/controller-runtime v0.24.1
1616
sigs.k8s.io/multicluster-runtime v0.24.1
17+
sigs.k8s.io/yaml v1.6.0
1718
)
1819

1920
require (
@@ -80,7 +81,6 @@ require (
8081
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
8182
sigs.k8s.io/randfill v1.0.0 // indirect
8283
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect
83-
sigs.k8s.io/yaml v1.6.0 // indirect
8484
)
8585

8686
replace github.com/kcp-dev/multicluster-provider/client => github.com/kcp-dev/multicluster-provider/client v0.8.0

providers/infrastructure/operator.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ package main
3232

3333
import (
3434
"context"
35+
_ "embed"
3536
"fmt"
3637
"log"
3738
"os"
@@ -46,6 +47,13 @@ import (
4647
"github.com/faroshq/provider-infrastructure/operator"
4748
)
4849

50+
// catalogEntryManifest is the provider's CatalogEntry, embedded so the operator
51+
// can self-register it into the provider workspace (ui/backend URLs rewritten to
52+
// the serve Service it owns).
53+
//
54+
//go:embed manifest.yaml
55+
var catalogEntryManifest []byte
56+
4957
// operatorReconcileInterval is how often the bootstrap reconcile re-runs.
5058
// Every step is idempotent, so this is purely a self-healing cadence —
5159
// e.g. it re-seeds kro if the Secret is deleted, or finishes APIExport
@@ -152,7 +160,7 @@ func runController() error {
152160
if err != nil {
153161
return fmt.Errorf("operator manager kubeconfig: %w", err)
154162
}
155-
return operator.Run(ctx, cfg)
163+
return operator.Run(ctx, cfg, catalogEntryManifest)
156164
}
157165

158166
// loadOperatorManagerConfig resolves the cluster the operator watches CRs in:
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
"os"
17+
18+
"k8s.io/client-go/dynamic"
19+
"k8s.io/client-go/rest"
20+
"sigs.k8s.io/yaml"
21+
22+
sdkinstall "github.com/faroshq/provider-sdk/install"
23+
)
24+
25+
// EnsureCatalogEntry registers the provider with the hub by applying its
26+
// CatalogEntry (the embedded manifest) into the provider workspace, rewriting
27+
// the ui/backend URLs to the serve Service the operator owns. This is what makes
28+
// the provider appear in the catalog/portal — without it the workspace is
29+
// bootstrapped but the provider is never listed.
30+
func EnsureCatalogEntry(ctx context.Context, providerCfg *rest.Config, manifest []byte, serveURL string) error {
31+
if len(manifest) == 0 {
32+
return fmt.Errorf("no embedded CatalogEntry manifest")
33+
}
34+
35+
var obj map[string]any
36+
if err := yaml.Unmarshal(manifest, &obj); err != nil {
37+
return fmt.Errorf("parse CatalogEntry manifest: %w", err)
38+
}
39+
if spec, ok := obj["spec"].(map[string]any); ok {
40+
// Point ui + backend at the in-cluster serve Service the operator manages.
41+
if serveURL != "" {
42+
if ui, ok := spec["ui"].(map[string]any); ok {
43+
ui["url"] = serveURL
44+
}
45+
if be, ok := spec["backend"].(map[string]any); ok {
46+
be["url"] = serveURL
47+
}
48+
}
49+
// Stamp the release version. The embedded manifest.yaml carries a
50+
// placeholder (spec.version: 0.1.0); the chart injects the real
51+
// .Chart.AppVersion (stamped by `helm package --app-version` at
52+
// release) via KEDGE_PROVIDER_VERSION so the portal shows the same
53+
// version as the chart-templated providers.
54+
if v := os.Getenv("KEDGE_PROVIDER_VERSION"); v != "" {
55+
spec["version"] = v
56+
}
57+
}
58+
out, err := yaml.Marshal(obj)
59+
if err != nil {
60+
return fmt.Errorf("encode CatalogEntry: %w", err)
61+
}
62+
63+
// sdkinstall.ApplyCatalogEntry reads a file; round-trip through a temp file.
64+
f, err := os.CreateTemp("", "kedge-catalogentry-*.yaml")
65+
if err != nil {
66+
return err
67+
}
68+
defer os.Remove(f.Name())
69+
if _, err := f.Write(out); err != nil {
70+
f.Close()
71+
return err
72+
}
73+
if err := f.Close(); err != nil {
74+
return err
75+
}
76+
77+
dyn, err := dynamic.NewForConfig(providerCfg)
78+
if err != nil {
79+
return err
80+
}
81+
return sdkinstall.ApplyCatalogEntry(ctx, dyn, f.Name())
82+
}
83+
84+
// ServeServiceURL is the in-cluster URL of the serve Service the operator owns.
85+
func ServeServiceURL(name string, port int32) string {
86+
if port == 0 {
87+
port = 8081
88+
}
89+
return fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", name, ServeNamespace, port)
90+
}

providers/infrastructure/operator/controller.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ type Reconciler struct {
4848
// built with). Used as the runtime cluster when a CR omits
4949
// spec.runtimeKubeconfigSecret — i.e. "use the current context".
5050
RestConfig *rest.Config
51+
// CatalogEntryManifest is the embedded provider CatalogEntry (manifest.yaml).
52+
// The operator applies it to the provider workspace (ui/backend URLs pointed
53+
// at the serve Service) so the provider self-registers in the catalog.
54+
CatalogEntryManifest []byte
5155
}
5256

5357
// Reconcile drives one CR to its desired state.
@@ -159,11 +163,21 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
159163
// where serve runs as a host binary for fast iteration.
160164
if os.Getenv("INFRASTRUCTURE_OPERATOR_SKIP_SERVE") == "true" {
161165
setCond(&cr, v1alpha1.ConditionProviderDeployed, metav1.ConditionTrue, "Skipped", "serve managed out-of-band (INFRASTRUCTURE_OPERATOR_SKIP_SERVE)")
166+
setCond(&cr, v1alpha1.ConditionRegistered, metav1.ConditionTrue, "Skipped", "CatalogEntry managed out-of-band (skip-serve)")
162167
} else {
163168
if err := EnsureProviderServe(ctx, cs, &cr, providerKC, runtimeKC, hubToken); err != nil {
164169
return r.fail(ctx, &cr, v1alpha1.ConditionProviderDeployed, "ServeDeployFailed", err)
165170
}
166171
setCond(&cr, v1alpha1.ConditionProviderDeployed, metav1.ConditionTrue, "Deployed", "provider serve Deployment reconciled")
172+
173+
// 4. Register the provider with the hub by applying its CatalogEntry,
174+
// ui/backend pointed at the serve Service the operator owns. This is what
175+
// lists the provider in the catalog/portal.
176+
serveURL := ServeServiceURL(cr.Name, cr.Spec.Provider.Port)
177+
if err := EnsureCatalogEntry(ctx, providerCfg, r.CatalogEntryManifest, serveURL); err != nil {
178+
return r.fail(ctx, &cr, v1alpha1.ConditionRegistered, "CatalogEntryFailed", err)
179+
}
180+
setCond(&cr, v1alpha1.ConditionRegistered, metav1.ConditionTrue, "Registered", "CatalogEntry applied ("+serveURL+")")
167181
}
168182

169183
cr.Status.Phase = "Ready"
@@ -212,7 +226,9 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
212226

213227
// Run builds a manager on the supplied config (the cluster the CRs live in) and
214228
// runs the InfrastructureProvider reconciler until ctx is cancelled.
215-
func Run(ctx context.Context, cfg *rest.Config) error {
229+
// catalogEntryManifest is the embedded provider CatalogEntry the operator
230+
// applies to self-register; may be nil to skip registration.
231+
func Run(ctx context.Context, cfg *rest.Config, catalogEntryManifest []byte) error {
216232
ctrl.SetLogger(klog.NewKlogr())
217233

218234
scheme := runtime.NewScheme()
@@ -230,7 +246,7 @@ func Run(ctx context.Context, cfg *rest.Config) error {
230246
if err != nil {
231247
return fmt.Errorf("manager.New: %w", err)
232248
}
233-
if err := (&Reconciler{Client: mgr.GetClient(), RestConfig: cfg}).SetupWithManager(mgr); err != nil {
249+
if err := (&Reconciler{Client: mgr.GetClient(), RestConfig: cfg, CatalogEntryManifest: catalogEntryManifest}).SetupWithManager(mgr); err != nil {
234250
return fmt.Errorf("setup reconciler: %w", err)
235251
}
236252
klog.FromContext(ctx).Info("infrastructure operator manager starting")

0 commit comments

Comments
 (0)