Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 048317a

Browse files
committed
feature: Implement independent API lifecycle
1 parent 2e806c5 commit 048317a

22 files changed

Lines changed: 1756 additions & 238 deletions

examples/configmap-deployment-controller/configdep_operator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ var _ = Describe("Configmap-deployment controller test:", Ordered, func() {
134134

135135
It("should create and start the operator controller", func() {
136136
setupLog.Info("setting up operator controller")
137-
c, err := controllers.NewOpController(cfg, ctrl.Options{
137+
c, err := controllers.NewOpController(cfg, nil, ctrl.Options{
138138
Scheme: scheme,
139139
LeaderElection: false, // disable leader-election
140140
HealthProbeBindAddress: "0", // disable health-check

examples/endpointslice-controller/ep_operator_test.go

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import (
3737
opv1a1 "github.com/l7mp/dcontroller/pkg/api/operator/v1alpha1"
3838
viewv1a1 "github.com/l7mp/dcontroller/pkg/api/view/v1alpha1"
3939
"github.com/l7mp/dcontroller/pkg/apiserver"
40-
"github.com/l7mp/dcontroller/pkg/manager"
40+
"github.com/l7mp/dcontroller/pkg/composite"
4141
"github.com/l7mp/dcontroller/pkg/object"
4242
doperator "github.com/l7mp/dcontroller/pkg/operator"
4343
dreconciler "github.com/l7mp/dcontroller/pkg/reconciler"
@@ -48,7 +48,7 @@ var (
4848
suite *testsuite.Suite
4949
// loglevel = 1
5050
// loglevel = -10
51-
loglevel int8 = -5
51+
loglevel int8 = -10
5252
port int
5353
epCtrl *testEpCtrl
5454
errorCh chan error
@@ -86,7 +86,7 @@ func (r *testEpCtrl) Reconcile(ctx context.Context, req dreconciler.Request) (re
8686

8787
var _ = Describe("EndpointSlice controller test:", Ordered, func() {
8888
Context("When creating an endpointslice controller w/o gather", Ordered, Label("operator"), func() {
89-
var mgr manager.Manager
89+
var api *composite.API
9090
var svc1, es1 object.Object
9191
var specs []map[string]any
9292
var epNames []string
@@ -124,17 +124,18 @@ var _ = Describe("EndpointSlice controller test:", Ordered, func() {
124124
AfterAll(func() { cancel() })
125125

126126
It("should create and start the API server", func() {
127-
suite.Log.Info("creating a manager")
127+
suite.Log.Info("creating an API server")
128128
var err error
129-
mgr, err = manager.New(suite.Cfg, ctrl.Options{
130-
Scheme: scheme,
131-
Logger: suite.Log,
129+
api, err = composite.NewAPI(suite.Cfg, composite.Options{
130+
CacheOptions: composite.CacheOptions{Logger: suite.Log},
132131
})
133132
Expect(err).NotTo(HaveOccurred())
133+
Expect(api.Client).NotTo(BeNil())
134+
Expect(api.Client.(*composite.CompositeClient).GetCache()).NotTo(BeNil())
134135

135136
suite.Log.Info("creating the API server")
136137
port = rand.IntN(5000) + (32768) //nolint:gosec
137-
config, err := apiserver.NewDefaultConfig("", port, mgr.GetClient(), true, false, suite.Log)
138+
config, err := apiserver.NewDefaultConfig("", port, api.Client, true, false, suite.Log)
138139
Expect(err).NotTo(HaveOccurred())
139140
server, err = apiserver.NewAPIServer(config)
140141
Expect(err).NotTo(HaveOccurred())
@@ -154,6 +155,7 @@ var _ = Describe("EndpointSlice controller test:", Ordered, func() {
154155
eventCh = make(chan testutils.ReconcileRequest, 16)
155156
errorCh = make(chan error, 16)
156157
opts := doperator.Options{
158+
Cache: api.GetCache(),
157159
ErrorChannel: errorCh,
158160
APIServer: server,
159161
Logger: suite.Log,
@@ -162,10 +164,11 @@ var _ = Describe("EndpointSlice controller test:", Ordered, func() {
162164
if _, err := os.Stat(specFile); errors.Is(err, os.ErrNotExist) {
163165
specFile = filepath.Base(specFile)
164166
}
165-
_, err := doperator.NewFromFile(OperatorName, mgr, specFile, opts)
167+
op, err := doperator.NewFromFile(OperatorName, suite.Cfg, specFile, opts)
166168
Expect(err).NotTo(HaveOccurred())
167169

168170
suite.Log.Info("creating the endpointslice controller")
171+
mgr := op.GetManager()
169172
epCtrl = &testEpCtrl{Client: mgr.GetClient(), log: suite.Log.WithName("test-ep-ctrl")}
170173
on := true
171174
c, err := controller.NewTyped("test-ep-ctrl", mgr, controller.TypedOptions[dreconciler.Request]{
@@ -204,7 +207,7 @@ var _ = Describe("EndpointSlice controller test:", Ordered, func() {
204207
suite.Log.Info("starting operator controller")
205208
go func() {
206209
defer GinkgoRecover()
207-
err := mgr.Start(ctx)
210+
err := op.Start(ctx)
208211
Expect(err).ToNot(HaveOccurred(), "failed to run controller")
209212
}()
210213
})
@@ -311,14 +314,13 @@ var _ = Describe("EndpointSlice controller test:", Ordered, func() {
311314
Group: viewv1a1.Group(OperatorName),
312315
Version: viewv1a1.Version,
313316
Resource: "endpointview",
314-
}).Namespace("default").List(context.TODO(), metav1.ListOptions{})
317+
}).Namespace("default").List(ctx, metav1.ListOptions{})
315318
Expect(err).NotTo(HaveOccurred())
316319
Expect(list.Items).To(BeEmpty())
317320
})
318321

319322
It("should allow a watcher to be created", func() {
320323
suite.Log.Info("creating a watch")
321-
322324
var err error
323325
watcher, err = dynamicClient.Resource(schema.GroupVersionResource{
324326
Group: viewv1a1.Group(OperatorName),
@@ -337,7 +339,7 @@ var _ = Describe("EndpointSlice controller test:", Ordered, func() {
337339
}
338340
specs = []map[string]any{}
339341

340-
for i := 0; i < 4; i++ {
342+
for i := range 4 {
341343
obj, err := dynamicClient.Resource(gvr).Namespace("testnamespace").
342344
Get(ctx, epNames[i], metav1.GetOptions{})
343345
Expect(err).NotTo(HaveOccurred())
@@ -489,6 +491,7 @@ var _ = Describe("EndpointSlice controller test:", Ordered, func() {
489491
})
490492

491493
Context("When creating an endpointslice controller w/ gather", Ordered, Label("operator"), func() {
494+
var api *composite.API
492495
var svc1, es1 object.Object
493496
var specs []map[string]any
494497
var ctx context.Context // context for the endpointslice controller
@@ -524,28 +527,47 @@ var _ = Describe("EndpointSlice controller test:", Ordered, func() {
524527
AfterAll(func() { cancel() })
525528

526529
It("should create and start the controller", func() {
527-
// Create a manager
528-
mgr, err := manager.New(suite.Cfg, ctrl.Options{
529-
Scheme: scheme,
530+
var err error
531+
api, err = composite.NewAPI(suite.Cfg, composite.Options{
532+
CacheOptions: composite.CacheOptions{Logger: suite.Log},
530533
})
531534
Expect(err).NotTo(HaveOccurred())
532535

533536
// Load the operator from file
534537
eventCh = make(chan testutils.ReconcileRequest, 16)
535538
errorCh = make(chan error, 16)
536539
opts := doperator.Options{
540+
Cache: api.GetCache(),
537541
ErrorChannel: errorCh,
538542
Logger: suite.Log,
539543
}
544+
545+
suite.Log.Info("creating the API server")
546+
port = rand.IntN(5000) + (32768) //nolint:gosec
547+
config, err := apiserver.NewDefaultConfig("", port, api.Client, true, false, suite.Log)
548+
Expect(err).NotTo(HaveOccurred())
549+
server, err = apiserver.NewAPIServer(config)
550+
Expect(err).NotTo(HaveOccurred())
551+
552+
go func() {
553+
defer GinkgoRecover()
554+
err := server.Start(ctx)
555+
Expect(err).NotTo(HaveOccurred())
556+
}()
557+
558+
// Give server a moment to start
559+
time.Sleep(20 * time.Millisecond)
560+
540561
specFile := OperatorGatherSpec
541562
if _, err := os.Stat(specFile); errors.Is(err, os.ErrNotExist) {
542563
specFile = filepath.Base(specFile)
543564
}
544-
_, err = doperator.NewFromFile(OperatorName, mgr, specFile, opts)
565+
op, err := doperator.NewFromFile(OperatorName, suite.Cfg, specFile, opts)
545566
Expect(err).NotTo(HaveOccurred())
546567

547568
// Create the endpointslice controller
548-
epCtrl = &testEpCtrl{Client: mgr.GetClient(), log: suite.Log.WithName("test-endpointslice-ctrl")}
569+
mgr := op.GetManager()
570+
epCtrl = &testEpCtrl{Client: api.GetClient(), log: suite.Log.WithName("test-endpointslice-ctrl")}
549571
on := true
550572
c, err := controller.NewTyped("test-ep-controller", mgr, controller.TypedOptions[dreconciler.Request]{
551573
SkipNameValidation: &on,
@@ -575,10 +597,17 @@ var _ = Describe("EndpointSlice controller test:", Ordered, func() {
575597
}
576598
}()
577599

600+
suite.Log.Info("starting API server cache (shared view storage)")
601+
go func() {
602+
defer GinkgoRecover()
603+
err := api.Cache.Start(ctx)
604+
Expect(err).NotTo(HaveOccurred(), "failed to start API server cache")
605+
}()
606+
578607
suite.Log.Info("starting operator controller")
579608
go func() {
580609
defer GinkgoRecover()
581-
err := mgr.Start(ctx)
610+
err := op.Start(ctx)
582611
Expect(err).ToNot(HaveOccurred(), "failed to run controller")
583612
}()
584613
})

examples/endpointslice-controller/main.go

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import (
2020
"sigs.k8s.io/controller-runtime/pkg/controller"
2121
"sigs.k8s.io/controller-runtime/pkg/log/zap"
2222
"sigs.k8s.io/controller-runtime/pkg/reconcile"
23-
"sigs.k8s.io/yaml"
2423

2524
opv1a1 "github.com/l7mp/dcontroller/pkg/api/operator/v1alpha1"
25+
"github.com/l7mp/dcontroller/pkg/composite"
2626
"github.com/l7mp/dcontroller/pkg/manager"
2727
dobject "github.com/l7mp/dcontroller/pkg/object"
2828
doperator "github.com/l7mp/dcontroller/pkg/operator"
@@ -70,10 +70,10 @@ func main() {
7070
specFile = OperatorSpec
7171
}
7272

73-
// Create a manager
74-
mgr, err := manager.New(ctrl.GetConfigOrDie(), ctrl.Options{
75-
Scheme: scheme,
76-
Logger: logger,
73+
// Create an api
74+
config := ctrl.GetConfigOrDie()
75+
api, err := composite.NewAPI(config, composite.Options{
76+
CacheOptions: composite.CacheOptions{Logger: logger},
7777
})
7878
if err != nil {
7979
log.Error(err, "unable to set up manager")
@@ -83,27 +83,20 @@ func main() {
8383
// Load the operator from file
8484
errorChan := make(chan error, 16)
8585
opts := doperator.Options{
86+
Cache: api.GetCache(),
8687
ErrorChannel: errorChan,
8788
Logger: logger,
8889
}
8990

9091
// Load the operator from file. Do not call NewFromFile as that would commit the operator.
91-
op := doperator.New(OperatorName, mgr, opts)
92-
b, err := os.ReadFile(specFile)
92+
op, err := doperator.NewFromFile(OperatorName, config, specFile, opts)
9393
if err != nil {
94-
log.Error(err, "failed to load spec")
94+
log.Error(err, "unable to set up operator")
9595
os.Exit(1)
9696
}
97-
var spec opv1a1.OperatorSpec
98-
if err := yaml.Unmarshal(b, &spec); err != nil {
99-
log.Error(err, "failed to parse spec")
100-
os.Exit(1)
101-
}
102-
103-
op.AddSpec(&spec)
10497

10598
// Create the endpointslice controller
106-
r, err := NewEndpointSliceController(mgr, logger)
99+
r, err := NewEndpointSliceController(op.GetManager(), logger)
107100
if err != nil {
108101
log.Error(err, "failed to create endpointslice controller")
109102
os.Exit(1)
@@ -129,7 +122,7 @@ func main() {
129122
}
130123
}()
131124

132-
if err := mgr.Start(ctx); err != nil {
125+
if err := op.Start(ctx); err != nil {
133126
log.Error(err, "problem running operator")
134127
os.Exit(1)
135128
}

examples/service-health-monitor/svc-health-operator.yaml

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ metadata:
44
name: svc-health-operator
55
spec:
66
controllers:
7-
# Controller 1: Pod HealthView (one object per pod)
7+
# Controller 1: Pod -> HealthView (one object per pod)
88
- name: pod-health-monitor
99
sources:
1010
- apiGroup: ""
@@ -13,39 +13,39 @@ spec:
1313
matchLabels:
1414
"dcontroller.io/health-monitor": "enabled"
1515
pipeline:
16-
"@project":
17-
metadata:
18-
name: "$.metadata.labels['app']"
19-
namespace: "$.metadata.namespace"
20-
pods:
21-
podName: "$.metadata.name"
22-
ready: "$.status.conditions[?(@.type=='Ready')].status"
23-
- "@gather":
24-
# Key: group by service (name + namespace)
25-
- "@concat":
26-
- "$.metadata.name"
27-
- "--"
28-
- "$.metadata.namespace"
29-
# Value: collapse the pods fields into a list (contains ready status)
30-
- "$.pods"
16+
- "@project":
17+
metadata:
18+
name: "$.metadata.labels['app']"
19+
namespace: "$.metadata.namespace"
20+
pods:
21+
podName: "$.metadata.name"
22+
ready: "$.status.conditions[?(@.type=='Ready')].status"
23+
- "@gather":
24+
# Key: group by service (name + namespace)
25+
- "@concat":
26+
- "$.metadata.name"
27+
- "--"
28+
- "$.metadata.namespace"
29+
# Value: collapse the pods fields into a list (contains ready status)
30+
- "$.pods"
3131
target:
3232
kind: HealthView
3333

34-
# Controller 2: HealthView Service (aggregated health annotations)
34+
# Controller 2: HealthView -> Service (aggregated health annotations)
3535
- name: svc-health-monitor
3636
sources:
3737
- kind: HealthView
3838
- apiGroup: ""
3939
kind: Service
4040
pipeline:
4141
- "@join":
42-
"@and":
43-
- "@eq":
44-
- "$.HealthView.metadata.name"
45-
- "$.Service.metadata.name"
46-
- "@eq":
47-
- "$.HealthView.metadata.namespace"
48-
- "$.Service.metadata.namespace"
42+
"@and":
43+
- "@eq":
44+
- "$.HealthView.metadata.name"
45+
- "$.Service.metadata.name"
46+
- "@eq":
47+
- "$.HealthView.metadata.namespace"
48+
- "$.Service.metadata.namespace"
4949
- "@project":
5050
metadata:
5151
name: "$.Service.metadata.name"

examples/service-health-monitor/svc_health_operator_test.go

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

2929
"github.com/l7mp/dcontroller/internal/testutils"
3030
opv1a1 "github.com/l7mp/dcontroller/pkg/api/operator/v1alpha1"
31+
"github.com/l7mp/dcontroller/pkg/composite"
3132
"github.com/l7mp/dcontroller/pkg/kubernetes/controllers"
3233
"github.com/l7mp/dcontroller/pkg/object"
3334
"github.com/l7mp/dcontroller/pkg/testsuite"
@@ -50,6 +51,7 @@ var (
5051
scheme = runtime.NewScheme()
5152
k8sClient, opClient client.Client
5253
logger, setupLog logr.Logger
54+
api *composite.API
5355
)
5456

5557
var _ = BeforeSuite(func() {
@@ -136,7 +138,12 @@ var _ = Describe("Service health monitor controller test:", Ordered, func() {
136138

137139
It("should create and start the operator controller", func() {
138140
setupLog.Info("setting up operator controller")
139-
c, err := controllers.NewOpController(cfg, ctrl.Options{
141+
var err error
142+
api, err = composite.NewAPI(suite.Cfg, composite.Options{
143+
CacheOptions: composite.CacheOptions{Logger: suite.Log},
144+
})
145+
Expect(err).NotTo(HaveOccurred())
146+
c, err := controllers.NewOpController(cfg, api.Cache, ctrl.Options{
140147
Scheme: scheme,
141148
LeaderElection: false, // disable leader-election
142149
HealthProbeBindAddress: "0", // disable health-check
@@ -151,6 +158,13 @@ var _ = Describe("Service health monitor controller test:", Ordered, func() {
151158
opClient = c.GetClient()
152159
Expect(opClient).NotTo(BeNil())
153160

161+
suite.Log.Info("starting shared view storage")
162+
go func() {
163+
defer GinkgoRecover()
164+
err := api.Cache.Start(ctx)
165+
Expect(err).NotTo(HaveOccurred(), "failed to start API server cache")
166+
}()
167+
154168
setupLog.Info("starting operator controller")
155169
go func() {
156170
defer GinkgoRecover()
@@ -159,7 +173,7 @@ var _ = Describe("Service health monitor controller test:", Ordered, func() {
159173
}()
160174
})
161175

162-
It("should let an operator to be attached to the manager", func() {
176+
It("should let an operator to be loaded", func() {
163177
setupLog.Info("reading YAML file")
164178
yamlData, err := os.ReadFile(OperatorSpecFile)
165179
Expect(err).NotTo(HaveOccurred())

0 commit comments

Comments
 (0)