Skip to content

Commit edb1ba7

Browse files
author
Per G. da Silva
committed
Add 3 tier revision engine
Signed-off-by: Per G. da Silva <pegoncal@redhat.com>
1 parent db3ac18 commit edb1ba7

15 files changed

Lines changed: 446 additions & 90 deletions

File tree

‎api/v1/clusterobjectset_types.go‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ type ClusterObjectSetStatus struct {
518518
// different content. Each entry covers all fully-resolved object
519519
// manifests within a phase, making it source-agnostic.
520520
//
521-
// +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0",message="observedPhases is immutable"
521+
// +kubebuilder:validation:XValidation:rule="oldSelf.size() == 0 || (self.size() == oldSelf.size() && oldSelf.all(o, self.exists(n, n.name == o.name)))",message="observedPhases: phases cannot be added or removed once set"
522522
// +kubebuilder:validation:MaxItems=20
523523
// +listType=map
524524
// +listMapKey=name
@@ -527,22 +527,31 @@ type ClusterObjectSetStatus struct {
527527
}
528528

529529
// ObservedPhase records the observed content digest of a resolved phase.
530+
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.completedAt) || (has(self.completedAt) && self.completedAt == oldSelf.completedAt)",message="completedAt is immutable once set"
530531
type ObservedPhase struct {
531532
// name is the phase name matching a phase in spec.phases.
532533
//
533534
// +required
534535
// +kubebuilder:validation:MinLength=1
535536
// +kubebuilder:validation:MaxLength=63
536537
// +kubebuilder:validation:XValidation:rule=`!format.dns1123Label().validate(self).hasValue()`,message="the value must consist of only lowercase alphanumeric characters and hyphens, and must start and end with an alphanumeric character."
538+
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable"
537539
Name string `json:"name"`
538540

541+
// completedAt is the timestamp when this phase first became Complete.
542+
// Set once and never cleared. Zero value means the phase has never been
543+
// Complete.
544+
// +optional
545+
CompletedAt metav1.Time `json:"completedAt,omitzero"`
546+
539547
// digest is the digest of the phase's resolved object content
540548
// at first successful resolution, in the format "<algorithm>:<hex>".
541549
//
542550
// +required
543551
// +kubebuilder:validation:MinLength=1
544552
// +kubebuilder:validation:MaxLength=256
545553
// +kubebuilder:validation:XValidation:rule=`self.matches('^[a-z0-9]+:[a-f0-9]+$')`,message="digest must be in the format '<algorithm>:<hex>'"
554+
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="digest is immutable"
546555
Digest string `json:"digest"`
547556
}
548557

‎api/v1/zz_generated.deepcopy.go‎

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎applyconfigurations/api/v1/observedphase.go‎

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎applyconfigurations/internal/internal.go‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cmd/operator-controller/main.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import (
6262

6363
ocv1 "github.com/operator-framework/operator-controller/api/v1"
6464
clusterobjctrl "github.com/operator-framework/operator-controller/internal/object-controller/controllers"
65+
"github.com/operator-framework/operator-controller/internal/object-controller/revision"
6566
"github.com/operator-framework/operator-controller/internal/operator-controller/action"
6667
"github.com/operator-framework/operator-controller/internal/operator-controller/applier"
6768
"github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/cache"
@@ -673,7 +674,7 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
673674
// Wrap the discovery client with caching to reduce memory usage from repeated OpenAPI schema fetches
674675
discoveryClient := memory.NewMemCacheClient(baseDiscoveryClient)
675676

676-
revisionEngineFactory, err := clusterobjctrl.NewDefaultRevisionEngineFactory(
677+
revisionEngineFactory, err := revision.NewDefaultRevisionEngineFactory(
677678
c.mgr.GetScheme(),
678679
c.trackingCache,
679680
discoveryClient,

‎helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.20.1
6+
controller-gen.kubebuilder.io/version: v0.21.0
77
olm.operatorframework.io/generator: experimental
88
name: clusterobjectsets.olm.operatorframework.io
99
spec:
@@ -632,6 +632,13 @@ spec:
632632
description: ObservedPhase records the observed content digest of
633633
a resolved phase.
634634
properties:
635+
completedAt:
636+
description: |-
637+
completedAt is the timestamp when this phase first became Complete.
638+
Set once and never cleared. Zero value means the phase has never been
639+
Complete.
640+
format: date-time
641+
type: string
635642
digest:
636643
description: |-
637644
digest is the digest of the phase's resolved object content
@@ -642,6 +649,8 @@ spec:
642649
x-kubernetes-validations:
643650
- message: digest must be in the format '<algorithm>:<hex>'
644651
rule: self.matches('^[a-z0-9]+:[a-f0-9]+$')
652+
- message: digest is immutable
653+
rule: self == oldSelf
645654
name:
646655
description: name is the phase name matching a phase in spec.phases.
647656
maxLength: 63
@@ -652,18 +661,26 @@ spec:
652661
characters and hyphens, and must start and end with an alphanumeric
653662
character.
654663
rule: '!format.dns1123Label().validate(self).hasValue()'
664+
- message: name is immutable
665+
rule: self == oldSelf
655666
required:
656667
- digest
657668
- name
658669
type: object
670+
x-kubernetes-validations:
671+
- message: completedAt is immutable once set
672+
rule: '!has(oldSelf.completedAt) || (has(self.completedAt) &&
673+
self.completedAt == oldSelf.completedAt)'
659674
maxItems: 20
660675
type: array
661676
x-kubernetes-list-map-keys:
662677
- name
663678
x-kubernetes-list-type: map
664679
x-kubernetes-validations:
665-
- message: observedPhases is immutable
666-
rule: self == oldSelf || oldSelf.size() == 0
680+
- message: 'observedPhases: phases cannot be added or removed once
681+
set'
682+
rule: oldSelf.size() == 0 || (self.size() == oldSelf.size() && oldSelf.all(o,
683+
self.exists(n, n.name == o.name)))
667684
type: object
668685
type: object
669686
served: true

‎internal/object-controller/controllers/clusterobjectset_controller.go‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"sigs.k8s.io/controller-runtime/pkg/source"
4343

4444
ocv1 "github.com/operator-framework/operator-controller/api/v1"
45+
"github.com/operator-framework/operator-controller/internal/object-controller/revision"
4546
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
4647
)
4748

@@ -53,7 +54,7 @@ const (
5354
// as part of the boxcutter integration.
5455
type ClusterObjectSetReconciler struct {
5556
Client client.Client
56-
RevisionEngineFactory RevisionEngineFactory
57+
RevisionEngineFactory revision.EngineFactory
5758
TrackingCache trackingCache
5859
Clock clock.Clock
5960
}
@@ -150,13 +151,13 @@ func (c *ClusterObjectSetReconciler) reconcile(ctx context.Context, cos *ocv1.Cl
150151
return ctrl.Result{}, nil
151152
}
152153

153-
revisionEngine, err := c.RevisionEngineFactory.CreateRevisionEngine(ctx, cos)
154+
revisionEngine, err := c.RevisionEngineFactory.New(ctx, cos)
154155
if err != nil {
155156
setRetryingConditions(l, cos, err.Error(), isDeadlineExceeded)
156157
return ctrl.Result{}, fmt.Errorf("failed to create revision engine: %v", err)
157158
}
158159

159-
revision := boxcutter.NewRevisionWithOwner(
160+
bcRevision := boxcutter.NewRevisionWithOwner(
160161
cos.Name,
161162
cos.Spec.Revision,
162163
phases,
@@ -169,20 +170,20 @@ func (c *ClusterObjectSetReconciler) reconcile(ctx context.Context, cos *ocv1.Cl
169170
markAsAvailableUnknown(cos, ocv1.ClusterObjectSetReasonReconciling, err.Error())
170171
return ctrl.Result{}, fmt.Errorf("error stopping informers: %v", err)
171172
}
172-
return c.archive(ctx, revisionEngine, cos, revision)
173+
return c.archive(ctx, revisionEngine, cos, bcRevision)
173174
}
174175

175176
if err := c.ensureFinalizer(ctx, cos, clusterObjectSetTeardownFinalizer); err != nil {
176177
return ctrl.Result{}, fmt.Errorf("error ensuring teardown finalizer: %v", err)
177178
}
178179

179-
if err := c.establishWatch(ctx, cos, revision); err != nil {
180+
if err := c.establishWatch(ctx, cos, bcRevision); err != nil {
180181
werr := fmt.Errorf("establish watch: %v", err)
181182
setRetryingConditions(l, cos, werr.Error(), isDeadlineExceeded)
182183
return ctrl.Result{}, werr
183184
}
184185

185-
rres, err := revisionEngine.Reconcile(ctx, revision, opts...)
186+
rres, err := revisionEngine.Reconcile(ctx, bcRevision, opts...)
186187
if err != nil {
187188
if rres != nil {
188189
// Log detailed reconcile reports only in debug mode (V(1)) to reduce verbosity.
@@ -200,6 +201,14 @@ func (c *ClusterObjectSetReconciler) reconcile(ctx context.Context, cos *ocv1.Cl
200201
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
201202
}
202203

204+
// Set phase completedAt
205+
now := metav1.NewTime(time.Now())
206+
for i, pres := range rres.GetPhases() {
207+
if pres.IsComplete() && cos.Status.ObservedPhases[i].CompletedAt.IsZero() {
208+
cos.Status.ObservedPhases[i].CompletedAt = now
209+
}
210+
}
211+
203212
for i, pres := range rres.GetPhases() {
204213
if verr := pres.GetValidationError(); verr != nil {
205214
l.Error(fmt.Errorf("%w", verr), "phase preflight validation failed, retrying after 10s", "phase", i)
@@ -308,7 +317,7 @@ func (c *ClusterObjectSetReconciler) delete(ctx context.Context, cos *ocv1.Clust
308317
return ctrl.Result{}, nil
309318
}
310319

311-
func (c *ClusterObjectSetReconciler) archive(ctx context.Context, revisionEngine RevisionEngine, cos *ocv1.ClusterObjectSet, revision boxcutter.RevisionBuilder) (ctrl.Result, error) {
320+
func (c *ClusterObjectSetReconciler) archive(ctx context.Context, revisionEngine revision.Engine, cos *ocv1.ClusterObjectSet, revision boxcutter.RevisionBuilder) (ctrl.Result, error) {
312321
l := log.FromContext(ctx)
313322
tdres, err := revisionEngine.Teardown(ctx, revision)
314323
if err != nil {

‎internal/object-controller/controllers/clusterobjectset_controller_test.go‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
ocv1 "github.com/operator-framework/operator-controller/api/v1"
3131
"github.com/operator-framework/operator-controller/internal/object-controller/controllers"
32+
"github.com/operator-framework/operator-controller/internal/object-controller/revision"
3233
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
3334
mockcontrollers "github.com/operator-framework/operator-controller/internal/testutil/mock/controllers"
3435
mockmachinery "github.com/operator-framework/operator-controller/internal/testutil/mock/machinery"
@@ -1269,8 +1270,8 @@ func newMockTrackingCache(ctrl *gomock.Controller, cl client.Client, freeFn func
12691270

12701271
// newNoopMockRevisionEngine creates a MockRevisionEngine with no expectations set.
12711272
// Useful for tests where the engine is never called (e.g., error paths that fail before reaching the engine).
1272-
func newNoopMockRevisionEngine(ctrl *gomock.Controller) *mockcontrollers.MockRevisionEngine {
1273-
return mockcontrollers.NewMockRevisionEngine(ctrl)
1273+
func newNoopMockRevisionEngine(ctrl *gomock.Controller) *mockcontrollers.MockEngine {
1274+
return mockcontrollers.NewMockEngine(ctrl)
12741275
}
12751276

12761277
// newMockRevisionEngineWithReconcile creates a MockRevisionEngine with a Reconcile expectation.
@@ -1279,8 +1280,8 @@ func newMockRevisionEngineWithReconcile(
12791280
ctrl *gomock.Controller,
12801281
reconcileFn func(context.Context, machinerytypes.Revision, ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error),
12811282
teardownFn func(context.Context, machinerytypes.Revision, ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error),
1282-
) *mockcontrollers.MockRevisionEngine {
1283-
m := mockcontrollers.NewMockRevisionEngine(ctrl)
1283+
) *mockcontrollers.MockEngine {
1284+
m := mockcontrollers.NewMockEngine(ctrl)
12841285
m.EXPECT().Reconcile(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(reconcileFn).AnyTimes()
12851286
if teardownFn != nil {
12861287
m.EXPECT().Teardown(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(teardownFn).AnyTimes()
@@ -1292,12 +1293,12 @@ func newMockRevisionEngineWithReconcile(
12921293
// that returns the given engine and error.
12931294
func newMockRevisionEngineFactoryWithEngine(
12941295
ctrl *gomock.Controller,
1295-
engine controllers.RevisionEngine,
1296+
engine revision.Engine,
12961297
createErr error,
1297-
) *mockcontrollers.MockRevisionEngineFactory {
1298-
m := mockcontrollers.NewMockRevisionEngineFactory(ctrl)
1299-
m.EXPECT().CreateRevisionEngine(gomock.Any(), gomock.Any()).DoAndReturn(
1300-
func(ctx context.Context, rev *ocv1.ClusterObjectSet) (controllers.RevisionEngine, error) {
1298+
) *mockcontrollers.MockEngineFactory {
1299+
m := mockcontrollers.NewMockEngineFactory(ctrl)
1300+
m.EXPECT().New(gomock.Any(), gomock.Any()).DoAndReturn(
1301+
func(ctx context.Context, rev *ocv1.ClusterObjectSet) (revision.Engine, error) {
13011302
if createErr != nil {
13021303
return nil, createErr
13031304
}

0 commit comments

Comments
 (0)