From ea08d1bf1db1ada0b41db6d33787a45c55a6ba93 Mon Sep 17 00:00:00 2001 From: Fabricio Aguiar Date: Thu, 15 Oct 2026 17:51:07 +0100 Subject: [PATCH 1/2] feat(api): add ClusterObjectDeployment types and admission validation Define mutable deployment templates using existing ClusterObjectSet content types, without exposing controller-assigned revision fields or inheriting snapshot immutability. Keep the MVP status focused on Progressing. Validate names, phase content, and collision protection through the API server. Preserve omitted collision protection and verify that deferred template metadata, deadlines, and revision summary fields are pruned. Narrow the API reference type exclusions so required template phases and their content types remain documented. Generated artifacts follow in a separate commit. Controller and Helm installation wiring remain in their dedicated follow-up tasks. Refs: OPRUN-4786 Signed-off-by: Fabricio Aguiar Commit-Message-Assisted-by: Codex rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- api/v1/clusterobjectdeployment_types.go | 143 +++++++ api/v1/clusterobjectdeployment_types_test.go | 366 ++++++++++++++++++ .../crd-ref-docs-gen-config.yaml | 2 +- hack/tools/update-crds.sh | 5 +- 4 files changed, 513 insertions(+), 3 deletions(-) create mode 100644 api/v1/clusterobjectdeployment_types.go create mode 100644 api/v1/clusterobjectdeployment_types_test.go diff --git a/api/v1/clusterobjectdeployment_types.go b/api/v1/clusterobjectdeployment_types.go new file mode 100644 index 0000000000..7596b94fb7 --- /dev/null +++ b/api/v1/clusterobjectdeployment_types.go @@ -0,0 +1,143 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +const ( + ClusterObjectDeploymentKind = "ClusterObjectDeployment" + + // Condition types. + ClusterObjectDeploymentTypeProgressing = "Progressing" + + // Condition reasons. + ClusterObjectDeploymentReasonNewClusterObjectSetProgressed = "NewClusterObjectSetProgressed" + ClusterObjectDeploymentReasonNewClusterObjectSetProgressing = "NewClusterObjectSetProgressing" +) + +// ClusterObjectDeploymentSpec defines the mutable desired state of a deployment. +type ClusterObjectDeploymentSpec struct { + // template is the desired content of newly created ClusterObjectSets. + // Changes to template create a new revision; existing revisions remain immutable. + // +required + Template ClusterObjectDeploymentTemplate `json:"template,omitzero"` +} + +// ClusterObjectDeploymentTemplate describes the caller-controlled part of a revision. +type ClusterObjectDeploymentTemplate struct { + // spec contains the desired revision content. Group, revision, lifecycle state, + // and ownership are assigned by the controller and cannot be set here. + // +required + Spec ClusterObjectDeploymentTemplateSpec `json:"spec,omitzero"` +} + +// ClusterObjectDeploymentTemplateSpec contains mutable ClusterObjectSet content. +type ClusterObjectDeploymentTemplateSpec struct { + // phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains + // at most 50 objects using the existing inline object or immutable Secret reference + // representation. Phase, object, and probe order participate in template identity. + // +required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=20 + // +listType=map + // +listMapKey=name + Phases []ClusterObjectSetPhase `json:"phases"` //nolint:kubeapilinter // These are ordered groups of objects, not an enum describing lifecycle state. + + // progressionProbes supplies custom readiness probes for objects in every phase. + // The existing ClusterObjectSet selector and assertion semantics apply. + // +optional + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=20 + // +listType=atomic + ProgressionProbes []ProgressionProbe `json:"progressionProbes,omitempty"` + + // collisionProtection is the default strategy for objects in the revision. + // Objects and phases may override it. When omitted the controller uses Prevent + // in the generated ClusterObjectSet. Omission remains distinct from explicit + // Prevent for template identity; admission does not default this field. + // +optional + // +kubebuilder:validation:Enum=Prevent;IfNoController;None + CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` +} + +// ClusterObjectDeploymentStatus reports rollout progress. +type ClusterObjectDeploymentStatus struct { + // conditions describes rollout progress for the observed generation. + // Progressing is True with reason NewClusterObjectSetProgressing while the desired + // rollout is incomplete, or False with reason NewClusterObjectSetProgressed after + // completion. Completion remains recorded even if the completed revision later + // becomes unhealthy. + // +optional + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` //nolint:kubeapilinter // This CRD uses structural list-map validation, not protobuf serialization. +} + +// +genclient +// +genclient:nonNamespaced +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster,shortName=cod +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Progressing,type=string,JSONPath=`.status.conditions[?(@.type=='Progressing')].status` +// +kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 52 && self.metadata.name.matches('^[a-z]([a-z0-9-]*[a-z0-9])?$')",message="name must be 1 to 52 lowercase alphanumeric or hyphen characters, starting with a letter and ending with an alphanumeric character" + +// ClusterObjectDeployment manages mutable desired content through immutable +// ClusterObjectSet revisions. It is an experimental, cluster-scoped API. +type ClusterObjectDeployment struct { + metav1.TypeMeta `json:",inline"` + + // metadata is the standard object's metadata. Names are valid COS groups of at + // most 52 characters; generated child names also include an int64 revision suffix. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec defines the desired deployment template. + // +required + Spec ClusterObjectDeploymentSpec `json:"spec,omitzero"` + + // status defines the observed state and is written through the status subresource. + // +optional + Status ClusterObjectDeploymentStatus `json:"status,omitzero"` //nolint:kubeapilinter // OpenShift APIs use value structs with omitzero for optional status. +} + +// +kubebuilder:object:root=true + +// ClusterObjectDeploymentList contains ClusterObjectDeployment objects. +type ClusterObjectDeploymentList struct { + metav1.TypeMeta `json:",inline"` + + // metadata is the standard list metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // items is the list of deployments. + // +required + Items []ClusterObjectDeployment `json:"items"` +} + +func init() { + SchemeBuilder.Register(func(s *runtime.Scheme) error { + s.AddKnownTypes(GroupVersion, &ClusterObjectDeployment{}, &ClusterObjectDeploymentList{}) + return nil + }) +} diff --git a/api/v1/clusterobjectdeployment_types_test.go b/api/v1/clusterobjectdeployment_types_test.go new file mode 100644 index 0000000000..138048d696 --- /dev/null +++ b/api/v1/clusterobjectdeployment_types_test.go @@ -0,0 +1,366 @@ +package v1 + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + apiequality "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestClusterObjectDeploymentValidity(t *testing.T) { + c := newClient(t) + for i, tc := range []struct { + name string + mutate func(*ClusterObjectDeployment) + valid bool + }{ + {name: "minimal template", valid: true}, + {name: "one character name", mutate: func(cod *ClusterObjectDeployment) { cod.Name = "a" }, valid: true}, + {name: "52 character name", mutate: func(cod *ClusterObjectDeployment) { cod.Name = strings.Repeat("a", 52) }, valid: true}, + {name: "53 character name", mutate: func(cod *ClusterObjectDeployment) { cod.Name = strings.Repeat("a", 53) }}, + {name: "name starts with a digit", mutate: func(cod *ClusterObjectDeployment) { cod.Name = "1-deployment" }}, + {name: "name contains a dot", mutate: func(cod *ClusterObjectDeployment) { cod.Name = "deployment.example" }}, + {name: "name contains uppercase", mutate: func(cod *ClusterObjectDeployment) { cod.Name = "Deployment" }}, + {name: "name ends with a hyphen", mutate: func(cod *ClusterObjectDeployment) { cod.Name = "deployment-" }}, + {name: "collision protection Prevent", mutate: codCollisionProtection(CollisionProtectionPrevent), valid: true}, + {name: "collision protection IfNoController", mutate: codCollisionProtection(CollisionProtectionIfNoController), valid: true}, + {name: "collision protection None", mutate: codCollisionProtection(CollisionProtectionNone), valid: true}, + {name: "collision protection invalid", mutate: codCollisionProtection("Invalid")}, + {name: "phases omitted", mutate: func(cod *ClusterObjectDeployment) { cod.Spec.Template.Spec.Phases = nil }}, + {name: "phases empty", mutate: func(cod *ClusterObjectDeployment) { cod.Spec.Template.Spec.Phases = []ClusterObjectSetPhase{} }}, + {name: "20 phases", mutate: codPhases(20), valid: true}, + {name: "21 phases", mutate: codPhases(21)}, + {name: "duplicate phase names", mutate: func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.Phases = append(cod.Spec.Template.Spec.Phases, cod.Spec.Template.Spec.Phases[0]) + }}, + {name: "phase name empty", mutate: codPhaseName("")}, + {name: "phase name uppercase", mutate: codPhaseName("Deploy")}, + {name: "phase name contains dot", mutate: codPhaseName("deploy.example")}, + {name: "phase name ends with hyphen", mutate: codPhaseName("deploy-")}, + {name: "phase name 63 characters", mutate: codPhaseName(strings.Repeat("a", 63)), valid: true}, + {name: "phase name 64 characters", mutate: codPhaseName(strings.Repeat("a", 64))}, + {name: "phase with 50 objects", mutate: codPhaseObjects(50), valid: true}, + {name: "phase with 51 objects", mutate: codPhaseObjects(51)}, + {name: "phase collision protection invalid", mutate: func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.Phases[0].CollisionProtection = "Invalid" + }}, + {name: "object collision protection invalid", mutate: func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.Phases[0].Objects[0].CollisionProtection = "Invalid" + }}, + {name: "object reference", mutate: func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.Phases[0].Objects[0] = ClusterObjectSetObject{Ref: ObjectSourceRef{Name: "objects", Key: "configmap"}} + }, valid: true}, + {name: "both inline object and reference", mutate: func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.Phases[0].Objects[0].Ref = ObjectSourceRef{Name: "objects", Key: "configmap"} + }}, + {name: "neither inline object nor reference", mutate: func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.Phases[0].Objects[0] = ClusterObjectSetObject{} + }}, + {name: "reference key missing", mutate: func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.Phases[0].Objects[0] = ClusterObjectSetObject{Ref: ObjectSourceRef{Name: "objects"}} + }}, + {name: "inline object kind missing", mutate: func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.Phases[0].Objects[0].Object.SetKind("") + }}, + {name: "progression probe", mutate: func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.ProgressionProbes = []ProgressionProbe{codProbe("ready")} + }, valid: true}, + {name: "progression probe selector union invalid", mutate: func(cod *ClusterObjectDeployment) { + probe := codProbe("ready") + probe.Selector.Label = metav1.LabelSelector{MatchLabels: map[string]string{"app": "example"}} + cod.Spec.Template.Spec.ProgressionProbes = []ProgressionProbe{probe} + }}, + {name: "progression probe assertion union invalid", mutate: func(cod *ClusterObjectDeployment) { + probe := codProbe("ready") + probe.Assertions[0].ConditionEqual = ConditionEqualProbe{Type: "Ready", Status: "True"} + cod.Spec.Template.Spec.ProgressionProbes = []ProgressionProbe{probe} + }}, + {name: "progression probe assertions missing", mutate: func(cod *ClusterObjectDeployment) { + probe := codProbe("ready") + probe.Assertions = nil + cod.Spec.Template.Spec.ProgressionProbes = []ProgressionProbe{probe} + }}, + } { + t.Run(tc.name, func(t *testing.T) { + cod := newTestClusterObjectDeployment(fmt.Sprintf("cod-validity-%d", i)) + if tc.mutate != nil { + tc.mutate(cod) + } + err := c.Create(t.Context(), cod) + if tc.valid { + require.NoError(t, err) + } else { + require.True(t, apierrors.IsInvalid(err), "expected invalid deployment, got: %v", err) + } + }) + } +} + +func TestClusterObjectDeploymentDoesNotDefaultTemplate(t *testing.T) { + c := newClient(t) + cod := newTestClusterObjectDeployment("cod-no-defaults") + require.NoError(t, c.Create(t.Context(), cod)) + cod = getTestClusterObjectDeployment(t, c, cod.Name) + require.Empty(t, cod.Spec.Template.Spec.CollisionProtection) + require.Empty(t, cod.Spec.Template.Spec.Phases[0].CollisionProtection) + require.Empty(t, cod.Spec.Template.Spec.Phases[0].Objects[0].CollisionProtection) + + // Omitted and explicitly supplied Prevent remain distinguishable templates. + cod.Spec.Template.Spec.CollisionProtection = CollisionProtectionPrevent + require.NoError(t, c.Update(t.Context(), cod)) + cod = getTestClusterObjectDeployment(t, c, cod.Name) + require.Equal(t, CollisionProtectionPrevent, cod.Spec.Template.Spec.CollisionProtection) + cod.Spec.Template.Spec.CollisionProtection = "" + require.NoError(t, c.Update(t.Context(), cod)) + cod = getTestClusterObjectDeployment(t, c, cod.Name) + require.Empty(t, cod.Spec.Template.Spec.CollisionProtection) +} + +func TestClusterObjectDeploymentTemplateIsMutable(t *testing.T) { + c := newClient(t) + cod := newTestClusterObjectDeployment("cod-mutable-template") + require.NoError(t, c.Create(t.Context(), cod)) + initialGeneration := cod.Generation + + secondObject := configMap() + secondObject.SetName("second-cm") + cod.Spec.Template.Spec.Phases = append(cod.Spec.Template.Spec.Phases, ClusterObjectSetPhase{ + Name: "second", Objects: []ClusterObjectSetObject{{Object: secondObject}}, + }) + cod.Spec.Template.Spec.CollisionProtection = CollisionProtectionIfNoController + cod.Spec.Template.Spec.ProgressionProbes = []ProgressionProbe{codProbe("first"), codProbe("second")} + require.NoError(t, c.Update(t.Context(), cod)) + require.Greater(t, cod.Generation, initialGeneration) + + // COS immutability rules must not apply to the mutable deployment template. + cod.Spec.Template.Spec.Phases[0], cod.Spec.Template.Spec.Phases[1] = cod.Spec.Template.Spec.Phases[1], cod.Spec.Template.Spec.Phases[0] + cod.Spec.Template.Spec.Phases[0].Name = "renamed" + cod.Spec.Template.Spec.Phases[0].Objects[0].Object.Object["data"] = map[string]interface{}{"mode": "updated"} + cod.Spec.Template.Spec.CollisionProtection = CollisionProtectionNone + cod.Spec.Template.Spec.ProgressionProbes[0], cod.Spec.Template.Spec.ProgressionProbes[1] = cod.Spec.Template.Spec.ProgressionProbes[1], cod.Spec.Template.Spec.ProgressionProbes[0] + cod.Spec.Template.Spec.ProgressionProbes[0].Assertions[0].FieldValue.Value = "updated" + expected := cod.Spec.DeepCopy() + require.NoError(t, c.Update(t.Context(), cod)) + cod = getTestClusterObjectDeployment(t, c, cod.Name) + require.Equal(t, *expected, cod.Spec) + + // Optional fields can also be removed after being set. + cod.Spec.Template.Spec.ProgressionProbes = nil + cod.Spec.Template.Spec.CollisionProtection = "" + require.NoError(t, c.Update(t.Context(), cod)) +} + +func TestClusterObjectDeploymentUpdateValidation(t *testing.T) { + c := newClient(t) + for i, tc := range []struct { + name string + mutate func(*ClusterObjectDeployment) + }{ + {name: "no phases", mutate: func(cod *ClusterObjectDeployment) { cod.Spec.Template.Spec.Phases = nil }}, + {name: "invalid collision protection", mutate: codCollisionProtection("Invalid")}, + } { + t.Run(tc.name, func(t *testing.T) { + cod := newTestClusterObjectDeployment(fmt.Sprintf("cod-update-validity-%d", i)) + require.NoError(t, c.Create(t.Context(), cod)) + expected := cod.Spec.DeepCopy() + tc.mutate(cod) + err := c.Update(t.Context(), cod) + require.True(t, apierrors.IsInvalid(err), "expected invalid deployment update, got: %v", err) + cod = getTestClusterObjectDeployment(t, c, cod.Name) + require.Equal(t, *expected, cod.Spec) + }) + } +} + +func TestClusterObjectDeploymentStatusSubresource(t *testing.T) { + c := newClient(t) + cod := newTestClusterObjectDeployment("cod-status-subresource") + cod.Status = codTestStatus() + require.NoError(t, c.Create(t.Context(), cod)) + cod = getTestClusterObjectDeployment(t, c, cod.Name) + require.Equal(t, ClusterObjectDeploymentStatus{}, cod.Status, "create must discard status") + + cod.Status = codTestStatus() + require.NoError(t, c.Update(t.Context(), cod)) + cod = getTestClusterObjectDeployment(t, c, cod.Name) + require.Equal(t, ClusterObjectDeploymentStatus{}, cod.Status, "ordinary update must preserve existing status") + + expectedSpec := cod.Spec.DeepCopy() + cod.Spec.Template.Spec.CollisionProtection = CollisionProtectionNone + cod.Status = codTestStatus() + expectedStatus := cod.Status.DeepCopy() + require.NoError(t, c.Status().Update(t.Context(), cod)) + cod = getTestClusterObjectDeployment(t, c, cod.Name) + require.Equal(t, *expectedSpec, cod.Spec, "status update must preserve spec") + require.True(t, apiequality.Semantic.DeepEqual(*expectedStatus, cod.Status), "status must survive storage") + + cod.Status = ClusterObjectDeploymentStatus{} + cod.Spec.Template.Spec.CollisionProtection = CollisionProtectionPrevent + require.NoError(t, c.Update(t.Context(), cod)) + cod = getTestClusterObjectDeployment(t, c, cod.Name) + require.True(t, apiequality.Semantic.DeepEqual(*expectedStatus, cod.Status), "ordinary update cannot clear status") + require.Equal(t, CollisionProtectionPrevent, cod.Spec.Template.Spec.CollisionProtection) +} + +func TestClusterObjectDeploymentTemplateSchema(t *testing.T) { + c := newClient(t) + for i, path := range [][]string{ + {"spec"}, + {"spec", "template"}, + {"spec", "template", "spec"}, + {"spec", "template", "spec", "phases"}, + } { + t.Run("required "+strings.Join(path, "."), func(t *testing.T) { + cod := newTestClusterObjectDeployment(fmt.Sprintf("cod-required-%d", i)) + raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cod) + require.NoError(t, err) + unstructured.RemoveNestedField(raw, path...) + err = c.Create(t.Context(), &unstructured.Unstructured{Object: raw}) + require.True(t, apierrors.IsInvalid(err), "expected missing required field to be invalid, got: %v", err) + }) + } + + t.Run("controller assigned fields are absent", func(t *testing.T) { + cod := newTestClusterObjectDeployment("cod-template-pruning") + raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cod) + require.NoError(t, err) + for field, value := range map[string]interface{}{ + "group": "example", "revision": int64(42), "lifecycleState": "Active", "progressDeadlineMinutes": int64(10), + } { + require.NoError(t, unstructured.SetNestedField(raw, value, "spec", "template", "spec", field)) + } + obj := &unstructured.Unstructured{Object: raw} + require.NoError(t, c.Create(t.Context(), obj)) + require.NoError(t, c.Get(t.Context(), client.ObjectKeyFromObject(obj), obj)) + for _, field := range []string{"group", "revision", "lifecycleState", "progressDeadlineMinutes"} { + _, found, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "template", "spec", field) + require.NoError(t, err) + require.False(t, found, "template must not expose %s", field) + } + }) +} + +func TestClusterObjectDeploymentStatusValidation(t *testing.T) { + c := newClient(t) + cod := newTestClusterObjectDeployment("cod-status-validity") + require.NoError(t, c.Create(t.Context(), cod)) + cod.Status = codTestStatus() + cod.Status.Conditions = append(cod.Status.Conditions, cod.Status.Conditions[0]) + err := c.Status().Update(t.Context(), cod) + require.True(t, apierrors.IsInvalid(err), "expected duplicate deployment conditions to be invalid, got: %v", err) +} + +func TestClusterObjectDeploymentDeferredFieldsArePruned(t *testing.T) { + c := newClient(t) + cod := newTestClusterObjectDeployment("cod-deferred-fields") + raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cod) + require.NoError(t, err) + require.NoError(t, unstructured.SetNestedField(raw, int64(10), "spec", "progressDeadlineMinutes")) + require.NoError(t, unstructured.SetNestedField(raw, map[string]interface{}{ + "labels": map[string]interface{}{"app": "example"}, + "annotations": map[string]interface{}{"note": "deferred"}, + }, "spec", "template", "metadata")) + obj := &unstructured.Unstructured{Object: raw} + require.NoError(t, c.Create(t.Context(), obj)) + require.NoError(t, c.Get(t.Context(), client.ObjectKeyFromObject(obj), obj)) + for _, path := range [][]string{{"spec", "progressDeadlineMinutes"}, {"spec", "template", "metadata"}} { + _, found, err := unstructured.NestedFieldNoCopy(obj.Object, path...) + require.NoError(t, err) + require.False(t, found, "deployment must not expose %s", strings.Join(path, ".")) + } + + status := codTestStatus() + rawStatus, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&status) + require.NoError(t, err) + rawStatus["activeRevisions"] = []interface{}{map[string]interface{}{"name": "cod-deferred-fields-1"}} + rawStatus["objectCounts"] = map[string]interface{}{"total": int64(0)} + obj.Object["status"] = rawStatus + require.NoError(t, c.Status().Update(t.Context(), obj)) + require.NoError(t, c.Get(t.Context(), client.ObjectKeyFromObject(obj), obj)) + for _, field := range []string{"activeRevisions", "objectCounts"} { + _, found, err := unstructured.NestedFieldNoCopy(obj.Object, "status", field) + require.NoError(t, err) + require.False(t, found, "deployment status must not expose %s", field) + } +} + +func getTestClusterObjectDeployment(t *testing.T, c client.Client, name string) *ClusterObjectDeployment { + t.Helper() + cod := &ClusterObjectDeployment{} + require.NoError(t, c.Get(t.Context(), client.ObjectKey{Name: name}, cod)) + return cod +} + +func newTestClusterObjectDeployment(name string) *ClusterObjectDeployment { + return &ClusterObjectDeployment{ + TypeMeta: metav1.TypeMeta{APIVersion: GroupVersion.String(), Kind: "ClusterObjectDeployment"}, + ObjectMeta: metav1.ObjectMeta{Name: name}, + Spec: ClusterObjectDeploymentSpec{ + Template: ClusterObjectDeploymentTemplate{ + Spec: ClusterObjectDeploymentTemplateSpec{ + Phases: []ClusterObjectSetPhase{{Name: "deploy", Objects: []ClusterObjectSetObject{{Object: configMap()}}}}, + }, + }, + }, + } +} + +func codCollisionProtection(value CollisionProtection) func(*ClusterObjectDeployment) { + return func(cod *ClusterObjectDeployment) { cod.Spec.Template.Spec.CollisionProtection = value } +} + +func codPhaseName(name string) func(*ClusterObjectDeployment) { + return func(cod *ClusterObjectDeployment) { cod.Spec.Template.Spec.Phases[0].Name = name } +} + +func codPhases(count int) func(*ClusterObjectDeployment) { + return func(cod *ClusterObjectDeployment) { + cod.Spec.Template.Spec.Phases = make([]ClusterObjectSetPhase, count) + for i := range cod.Spec.Template.Spec.Phases { + cod.Spec.Template.Spec.Phases[i] = ClusterObjectSetPhase{ + Name: fmt.Sprintf("phase-%d", i), Objects: []ClusterObjectSetObject{{Object: configMap()}}, + } + } + } +} + +func codPhaseObjects(count int) func(*ClusterObjectDeployment) { + return func(cod *ClusterObjectDeployment) { + objects := make([]ClusterObjectSetObject, count) + for i := range objects { + obj := configMap() + obj.SetName(fmt.Sprintf("configmap-%d", i)) + objects[i] = ClusterObjectSetObject{Object: obj} + } + cod.Spec.Template.Spec.Phases[0].Objects = objects + } +} + +func codProbe(value string) ProgressionProbe { + return ProgressionProbe{ + Selector: ObjectSelector{Type: SelectorTypeGroupKind, GroupKind: metav1.GroupKind{Kind: "ConfigMap"}}, + Assertions: []Assertion{{ + Type: ProbeTypeFieldValue, + FieldValue: FieldValueProbe{FieldPath: "data.mode", Value: value}, + }}, + } +} + +func codTestStatus() ClusterObjectDeploymentStatus { + condition := metav1.Condition{ + Type: ClusterObjectDeploymentTypeProgressing, Status: metav1.ConditionTrue, + Reason: ClusterObjectDeploymentReasonNewClusterObjectSetProgressing, Message: "revision is progressing", + ObservedGeneration: 1, LastTransitionTime: metav1.NewTime(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)), + } + return ClusterObjectDeploymentStatus{ + Conditions: []metav1.Condition{condition}, + } +} diff --git a/docs/api-reference/crd-ref-docs-gen-config.yaml b/docs/api-reference/crd-ref-docs-gen-config.yaml index c42240a46c..fcd840fab3 100644 --- a/docs/api-reference/crd-ref-docs-gen-config.yaml +++ b/docs/api-reference/crd-ref-docs-gen-config.yaml @@ -1,5 +1,5 @@ processor: - ignoreTypes: [ClusterObjectSet, ClusterObjectSetList, ObservedPhase] + ignoreTypes: ['\.ClusterObjectSet(List|Spec|Status)?$', '\.ObservedPhase$'] ignoreFields: [] render: diff --git a/hack/tools/update-crds.sh b/hack/tools/update-crds.sh index c68dba4c15..175abe0fdd 100755 --- a/hack/tools/update-crds.sh +++ b/hack/tools/update-crds.sh @@ -8,11 +8,12 @@ set -e CE="olm.operatorframework.io_clusterextensions.yaml" CC="olm.operatorframework.io_clustercatalogs.yaml" CR="olm.operatorframework.io_clusterobjectsets.yaml" +COD="olm.operatorframework.io_clusterobjectdeployments.yaml" # order for modules and crds must match # each item in crds must be unique, and should be associated with a module -modules=("operator-controller" "catalogd" "object-controller") -crds=("${CE}" "${CC}" "${CR}") +modules=("operator-controller" "catalogd" "object-controller" "object-controller") +crds=("${CE}" "${CC}" "${CR}" "${COD}") # Channels must much those in the generator channels=("standard" "experimental") From 1c356e5d0dc34dce9244c51ca4edc1c0b3dda1a0 Mon Sep 17 00:00:00 2001 From: Fabricio Aguiar Date: Thu, 15 Oct 2026 17:51:22 +0100 Subject: [PATCH 2/2] chore(api): generate ClusterObjectDeployment artifacts Regenerate DeepCopy methods, apply configurations, the experimental base CRD, and API reference documentation from the ClusterObjectDeployment types. Document required phases and remove deferred template metadata, deadline, revision summary, object count, and Available condition artifacts. Existing rendered install manifests retain their current API exposure; COD packaging belongs to OPRUN-4793. Refs: OPRUN-4786 Signed-off-by: Fabricio Aguiar Commit-Message-Assisted-by: Codex rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- api/v1/zz_generated.deepcopy.go | 142 +++++ .../api/v1/clusterobjectdeployment.go | 292 +++++++++ .../api/v1/clusterobjectdeploymentspec.go | 43 ++ .../api/v1/clusterobjectdeploymentstatus.go | 55 ++ .../api/v1/clusterobjectdeploymenttemplate.go | 43 ++ .../v1/clusterobjectdeploymenttemplatespec.go | 82 +++ applyconfigurations/internal/internal.go | 61 ++ applyconfigurations/utils.go | 10 + docs/api-reference/olmv1-api-reference.md | 204 +++++++ ...framework.io_clusterobjectdeployments.yaml | 560 ++++++++++++++++++ 10 files changed, 1492 insertions(+) create mode 100644 applyconfigurations/api/v1/clusterobjectdeployment.go create mode 100644 applyconfigurations/api/v1/clusterobjectdeploymentspec.go create mode 100644 applyconfigurations/api/v1/clusterobjectdeploymentstatus.go create mode 100644 applyconfigurations/api/v1/clusterobjectdeploymenttemplate.go create mode 100644 applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go create mode 100644 helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 2ddfb0da6a..0f679a1384 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -423,6 +423,148 @@ func (in *ClusterExtensionStatus) DeepCopy() *ClusterExtensionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterObjectDeployment) DeepCopyInto(out *ClusterObjectDeployment) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectDeployment. +func (in *ClusterObjectDeployment) DeepCopy() *ClusterObjectDeployment { + if in == nil { + return nil + } + out := new(ClusterObjectDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterObjectDeployment) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterObjectDeploymentList) DeepCopyInto(out *ClusterObjectDeploymentList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterObjectDeployment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectDeploymentList. +func (in *ClusterObjectDeploymentList) DeepCopy() *ClusterObjectDeploymentList { + if in == nil { + return nil + } + out := new(ClusterObjectDeploymentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterObjectDeploymentList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterObjectDeploymentSpec) DeepCopyInto(out *ClusterObjectDeploymentSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectDeploymentSpec. +func (in *ClusterObjectDeploymentSpec) DeepCopy() *ClusterObjectDeploymentSpec { + if in == nil { + return nil + } + out := new(ClusterObjectDeploymentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterObjectDeploymentStatus) DeepCopyInto(out *ClusterObjectDeploymentStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectDeploymentStatus. +func (in *ClusterObjectDeploymentStatus) DeepCopy() *ClusterObjectDeploymentStatus { + if in == nil { + return nil + } + out := new(ClusterObjectDeploymentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterObjectDeploymentTemplate) DeepCopyInto(out *ClusterObjectDeploymentTemplate) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectDeploymentTemplate. +func (in *ClusterObjectDeploymentTemplate) DeepCopy() *ClusterObjectDeploymentTemplate { + if in == nil { + return nil + } + out := new(ClusterObjectDeploymentTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterObjectDeploymentTemplateSpec) DeepCopyInto(out *ClusterObjectDeploymentTemplateSpec) { + *out = *in + if in.Phases != nil { + in, out := &in.Phases, &out.Phases + *out = make([]ClusterObjectSetPhase, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProgressionProbes != nil { + in, out := &in.ProgressionProbes, &out.ProgressionProbes + *out = make([]ProgressionProbe, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterObjectDeploymentTemplateSpec. +func (in *ClusterObjectDeploymentTemplateSpec) DeepCopy() *ClusterObjectDeploymentTemplateSpec { + if in == nil { + return nil + } + out := new(ClusterObjectDeploymentTemplateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterObjectSet) DeepCopyInto(out *ClusterObjectSet) { *out = *in diff --git a/applyconfigurations/api/v1/clusterobjectdeployment.go b/applyconfigurations/api/v1/clusterobjectdeployment.go new file mode 100644 index 0000000000..e967f68fa4 --- /dev/null +++ b/applyconfigurations/api/v1/clusterobjectdeployment.go @@ -0,0 +1,292 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen-v0.21. DO NOT EDIT. + +package v1 + +import ( + apiv1 "github.com/operator-framework/operator-controller/api/v1" + internal "github.com/operator-framework/operator-controller/applyconfigurations/internal" + apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" + metav1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// ClusterObjectDeploymentApplyConfiguration represents a declarative configuration of the ClusterObjectDeployment type for use +// with apply. +// +// ClusterObjectDeployment manages mutable desired content through immutable +// ClusterObjectSet revisions. It is an experimental, cluster-scoped API. +type ClusterObjectDeploymentApplyConfiguration struct { + metav1.TypeMetaApplyConfiguration `json:",inline"` + // metadata is the standard object's metadata. Names are valid COS groups of at + // most 52 characters; generated child names also include an int64 revision suffix. + *metav1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + // spec defines the desired deployment template. + Spec *ClusterObjectDeploymentSpecApplyConfiguration `json:"spec,omitempty"` + // status defines the observed state and is written through the status subresource. + Status *ClusterObjectDeploymentStatusApplyConfiguration `json:"status,omitempty"` +} + +// ClusterObjectDeployment constructs a declarative configuration of the ClusterObjectDeployment type for use with +// apply. +func ClusterObjectDeployment(name string) *ClusterObjectDeploymentApplyConfiguration { + b := &ClusterObjectDeploymentApplyConfiguration{} + b.WithName(name) + b.WithKind("ClusterObjectDeployment") + b.WithAPIVersion("olm.operatorframework.io/v1") + return b +} + +// ExtractClusterObjectDeploymentFrom extracts the applied configuration owned by fieldManager from +// clusterObjectDeployment for the specified subresource. Pass an empty string for subresource to extract +// the main resource. Common subresources include "status", "scale", etc. +// clusterObjectDeployment must be a unmodified ClusterObjectDeployment API object that was retrieved from the Kubernetes API. +// ExtractClusterObjectDeploymentFrom provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +func ExtractClusterObjectDeploymentFrom(clusterObjectDeployment *apiv1.ClusterObjectDeployment, fieldManager string, subresource string) (*ClusterObjectDeploymentApplyConfiguration, error) { + b := &ClusterObjectDeploymentApplyConfiguration{} + err := managedfields.ExtractInto(clusterObjectDeployment, internal.Parser().Type("com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeployment"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(clusterObjectDeployment.Name) + + b.WithKind("ClusterObjectDeployment") + b.WithAPIVersion("olm.operatorframework.io/v1") + return b, nil +} + +// ExtractClusterObjectDeployment extracts the applied configuration owned by fieldManager from +// clusterObjectDeployment. If no managedFields are found in clusterObjectDeployment for fieldManager, a +// ClusterObjectDeploymentApplyConfiguration is returned with only the Name, Namespace (if applicable), +// APIVersion and Kind populated. It is possible that no managed fields were found for because other +// field managers have taken ownership of all the fields previously owned by fieldManager, or because +// the fieldManager never owned fields any fields. +// clusterObjectDeployment must be a unmodified ClusterObjectDeployment API object that was retrieved from the Kubernetes API. +// ExtractClusterObjectDeployment provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +func ExtractClusterObjectDeployment(clusterObjectDeployment *apiv1.ClusterObjectDeployment, fieldManager string) (*ClusterObjectDeploymentApplyConfiguration, error) { + return ExtractClusterObjectDeploymentFrom(clusterObjectDeployment, fieldManager, "") +} + +// ExtractClusterObjectDeploymentStatus extracts the applied configuration owned by fieldManager from +// clusterObjectDeployment for the status subresource. +func ExtractClusterObjectDeploymentStatus(clusterObjectDeployment *apiv1.ClusterObjectDeployment, fieldManager string) (*ClusterObjectDeploymentApplyConfiguration, error) { + return ExtractClusterObjectDeploymentFrom(clusterObjectDeployment, fieldManager, "status") +} + +func (b ClusterObjectDeploymentApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithKind(value string) *ClusterObjectDeploymentApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithAPIVersion(value string) *ClusterObjectDeploymentApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithName(value string) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithGenerateName(value string) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithNamespace(value string) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithUID(value types.UID) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithResourceVersion(value string) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithGeneration(value int64) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithCreationTimestamp(value apismetav1.Time) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithDeletionTimestamp(value apismetav1.Time) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *ClusterObjectDeploymentApplyConfiguration) WithLabels(entries map[string]string) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *ClusterObjectDeploymentApplyConfiguration) WithAnnotations(entries map[string]string) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *ClusterObjectDeploymentApplyConfiguration) WithOwnerReferences(values ...*metav1.OwnerReferenceApplyConfiguration) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *ClusterObjectDeploymentApplyConfiguration) WithFinalizers(values ...string) *ClusterObjectDeploymentApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *ClusterObjectDeploymentApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &metav1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithSpec(value *ClusterObjectDeploymentSpecApplyConfiguration) *ClusterObjectDeploymentApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *ClusterObjectDeploymentApplyConfiguration) WithStatus(value *ClusterObjectDeploymentStatusApplyConfiguration) *ClusterObjectDeploymentApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *ClusterObjectDeploymentApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *ClusterObjectDeploymentApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *ClusterObjectDeploymentApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *ClusterObjectDeploymentApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/applyconfigurations/api/v1/clusterobjectdeploymentspec.go b/applyconfigurations/api/v1/clusterobjectdeploymentspec.go new file mode 100644 index 0000000000..8064d0c598 --- /dev/null +++ b/applyconfigurations/api/v1/clusterobjectdeploymentspec.go @@ -0,0 +1,43 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen-v0.21. DO NOT EDIT. + +package v1 + +// ClusterObjectDeploymentSpecApplyConfiguration represents a declarative configuration of the ClusterObjectDeploymentSpec type for use +// with apply. +// +// ClusterObjectDeploymentSpec defines the mutable desired state of a deployment. +type ClusterObjectDeploymentSpecApplyConfiguration struct { + // template is the desired content of newly created ClusterObjectSets. + // Changes to template create a new revision; existing revisions remain immutable. + Template *ClusterObjectDeploymentTemplateApplyConfiguration `json:"template,omitempty"` +} + +// ClusterObjectDeploymentSpecApplyConfiguration constructs a declarative configuration of the ClusterObjectDeploymentSpec type for use with +// apply. +func ClusterObjectDeploymentSpec() *ClusterObjectDeploymentSpecApplyConfiguration { + return &ClusterObjectDeploymentSpecApplyConfiguration{} +} + +// WithTemplate sets the Template field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Template field is set to the value of the last call. +func (b *ClusterObjectDeploymentSpecApplyConfiguration) WithTemplate(value *ClusterObjectDeploymentTemplateApplyConfiguration) *ClusterObjectDeploymentSpecApplyConfiguration { + b.Template = value + return b +} diff --git a/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go b/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go new file mode 100644 index 0000000000..183dda5203 --- /dev/null +++ b/applyconfigurations/api/v1/clusterobjectdeploymentstatus.go @@ -0,0 +1,55 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen-v0.21. DO NOT EDIT. + +package v1 + +import ( + metav1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// ClusterObjectDeploymentStatusApplyConfiguration represents a declarative configuration of the ClusterObjectDeploymentStatus type for use +// with apply. +// +// ClusterObjectDeploymentStatus reports rollout progress. +type ClusterObjectDeploymentStatusApplyConfiguration struct { + // conditions describes rollout progress for the observed generation. + // Progressing is True with reason NewClusterObjectSetProgressing while the desired + // rollout is incomplete, or False with reason NewClusterObjectSetProgressed after + // completion. Completion remains recorded even if the completed revision later + // becomes unhealthy. + Conditions []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"` +} + +// ClusterObjectDeploymentStatusApplyConfiguration constructs a declarative configuration of the ClusterObjectDeploymentStatus type for use with +// apply. +func ClusterObjectDeploymentStatus() *ClusterObjectDeploymentStatusApplyConfiguration { + return &ClusterObjectDeploymentStatusApplyConfiguration{} +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *ClusterObjectDeploymentStatusApplyConfiguration) WithConditions(values ...*metav1.ConditionApplyConfiguration) *ClusterObjectDeploymentStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/applyconfigurations/api/v1/clusterobjectdeploymenttemplate.go b/applyconfigurations/api/v1/clusterobjectdeploymenttemplate.go new file mode 100644 index 0000000000..c9dd55352c --- /dev/null +++ b/applyconfigurations/api/v1/clusterobjectdeploymenttemplate.go @@ -0,0 +1,43 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen-v0.21. DO NOT EDIT. + +package v1 + +// ClusterObjectDeploymentTemplateApplyConfiguration represents a declarative configuration of the ClusterObjectDeploymentTemplate type for use +// with apply. +// +// ClusterObjectDeploymentTemplate describes the caller-controlled part of a revision. +type ClusterObjectDeploymentTemplateApplyConfiguration struct { + // spec contains the desired revision content. Group, revision, lifecycle state, + // and ownership are assigned by the controller and cannot be set here. + Spec *ClusterObjectDeploymentTemplateSpecApplyConfiguration `json:"spec,omitempty"` +} + +// ClusterObjectDeploymentTemplateApplyConfiguration constructs a declarative configuration of the ClusterObjectDeploymentTemplate type for use with +// apply. +func ClusterObjectDeploymentTemplate() *ClusterObjectDeploymentTemplateApplyConfiguration { + return &ClusterObjectDeploymentTemplateApplyConfiguration{} +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *ClusterObjectDeploymentTemplateApplyConfiguration) WithSpec(value *ClusterObjectDeploymentTemplateSpecApplyConfiguration) *ClusterObjectDeploymentTemplateApplyConfiguration { + b.Spec = value + return b +} diff --git a/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go b/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go new file mode 100644 index 0000000000..745f2b128e --- /dev/null +++ b/applyconfigurations/api/v1/clusterobjectdeploymenttemplatespec.go @@ -0,0 +1,82 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen-v0.21. DO NOT EDIT. + +package v1 + +import ( + apiv1 "github.com/operator-framework/operator-controller/api/v1" +) + +// ClusterObjectDeploymentTemplateSpecApplyConfiguration represents a declarative configuration of the ClusterObjectDeploymentTemplateSpec type for use +// with apply. +// +// ClusterObjectDeploymentTemplateSpec contains mutable ClusterObjectSet content. +type ClusterObjectDeploymentTemplateSpecApplyConfiguration struct { + // phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains + // at most 50 objects using the existing inline object or immutable Secret reference + // representation. Phase, object, and probe order participate in template identity. + Phases []ClusterObjectSetPhaseApplyConfiguration `json:"phases,omitempty"` + // progressionProbes supplies custom readiness probes for objects in every phase. + // The existing ClusterObjectSet selector and assertion semantics apply. + ProgressionProbes []ProgressionProbeApplyConfiguration `json:"progressionProbes,omitempty"` + // collisionProtection is the default strategy for objects in the revision. + // Objects and phases may override it. When omitted the controller uses Prevent + // in the generated ClusterObjectSet. Omission remains distinct from explicit + // Prevent for template identity; admission does not default this field. + CollisionProtection *apiv1.CollisionProtection `json:"collisionProtection,omitempty"` +} + +// ClusterObjectDeploymentTemplateSpecApplyConfiguration constructs a declarative configuration of the ClusterObjectDeploymentTemplateSpec type for use with +// apply. +func ClusterObjectDeploymentTemplateSpec() *ClusterObjectDeploymentTemplateSpecApplyConfiguration { + return &ClusterObjectDeploymentTemplateSpecApplyConfiguration{} +} + +// WithPhases adds the given value to the Phases field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Phases field. +func (b *ClusterObjectDeploymentTemplateSpecApplyConfiguration) WithPhases(values ...*ClusterObjectSetPhaseApplyConfiguration) *ClusterObjectDeploymentTemplateSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithPhases") + } + b.Phases = append(b.Phases, *values[i]) + } + return b +} + +// WithProgressionProbes adds the given value to the ProgressionProbes field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the ProgressionProbes field. +func (b *ClusterObjectDeploymentTemplateSpecApplyConfiguration) WithProgressionProbes(values ...*ProgressionProbeApplyConfiguration) *ClusterObjectDeploymentTemplateSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithProgressionProbes") + } + b.ProgressionProbes = append(b.ProgressionProbes, *values[i]) + } + return b +} + +// WithCollisionProtection sets the CollisionProtection field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CollisionProtection field is set to the value of the last call. +func (b *ClusterObjectDeploymentTemplateSpecApplyConfiguration) WithCollisionProtection(value apiv1.CollisionProtection) *ClusterObjectDeploymentTemplateSpecApplyConfiguration { + b.CollisionProtection = &value + return b +} diff --git a/applyconfigurations/internal/internal.go b/applyconfigurations/internal/internal.go index 12d01d336f..53177e12af 100644 --- a/applyconfigurations/internal/internal.go +++ b/applyconfigurations/internal/internal.go @@ -249,6 +249,67 @@ var schemaYAML = typed.YAMLObject(`types: - name: install type: namedType: com.github.operator-framework.operator-controller.api.v1.ClusterExtensionInstallStatus +- name: com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeployment + map: + fields: + - name: apiVersion + type: + scalar: string + - name: kind + type: + scalar: string + - name: metadata + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta + - name: spec + type: + namedType: com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeploymentSpec + - name: status + type: + namedType: com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeploymentStatus +- name: com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeploymentSpec + map: + fields: + - name: template + type: + namedType: com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeploymentTemplate +- name: com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeploymentStatus + map: + fields: + - name: conditions + type: + list: + elementType: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Condition + elementRelationship: associative + keys: + - type +- name: com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeploymentTemplate + map: + fields: + - name: spec + type: + namedType: com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeploymentTemplateSpec +- name: com.github.operator-framework.operator-controller.api.v1.ClusterObjectDeploymentTemplateSpec + map: + fields: + - name: collisionProtection + type: + namedType: com.github.operator-framework.operator-controller.api.v1.CollisionProtection + - name: phases + type: + list: + elementType: + namedType: com.github.operator-framework.operator-controller.api.v1.ClusterObjectSetPhase + elementRelationship: associative + keys: + - name + - name: progressionProbes + type: + list: + elementType: + namedType: com.github.operator-framework.operator-controller.api.v1.ProgressionProbe + elementRelationship: atomic - name: com.github.operator-framework.operator-controller.api.v1.ClusterObjectSet map: fields: diff --git a/applyconfigurations/utils.go b/applyconfigurations/utils.go index b82ea41164..11d86eb61a 100644 --- a/applyconfigurations/utils.go +++ b/applyconfigurations/utils.go @@ -60,6 +60,16 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1.ClusterExtensionSpecApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ClusterExtensionStatus"): return &apiv1.ClusterExtensionStatusApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ClusterObjectDeployment"): + return &apiv1.ClusterObjectDeploymentApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ClusterObjectDeploymentSpec"): + return &apiv1.ClusterObjectDeploymentSpecApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ClusterObjectDeploymentStatus"): + return &apiv1.ClusterObjectDeploymentStatusApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ClusterObjectDeploymentTemplate"): + return &apiv1.ClusterObjectDeploymentTemplateApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ClusterObjectDeploymentTemplateSpec"): + return &apiv1.ClusterObjectDeploymentTemplateSpecApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ClusterObjectSet"): return &apiv1.ClusterObjectSetApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ClusterObjectSetObject"): diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index 5a87485a8e..2a46ebfa25 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -13,6 +13,8 @@ Package v1 contains API Schema definitions for the olm v1 API group - [ClusterCatalogList](#clustercataloglist) - [ClusterExtension](#clusterextension) - [ClusterExtensionList](#clusterextensionlist) +- [ClusterObjectDeployment](#clusterobjectdeployment) +- [ClusterObjectDeploymentList](#clusterobjectdeploymentlist) @@ -384,6 +386,176 @@ _Appears in:_ | `activeRevisions` _[RevisionStatus](#revisionstatus) array_ | activeRevisions holds a list of currently active (non-archived) ClusterObjectSets,
including both installed and rolling out revisions.
**Experimental channel:** | | Optional: \{\}
| +#### ClusterObjectDeployment + + + +ClusterObjectDeployment manages mutable desired content through immutable +ClusterObjectSet revisions. It is an experimental, cluster-scoped API. + + + +_Appears in:_ +- [ClusterObjectDeploymentList](#clusterobjectdeploymentlist) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `olm.operatorframework.io/v1` | | | +| `kind` _string_ | `ClusterObjectDeployment` | | | +| `kind` _string_ | Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | | Optional: \{\}
| +| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | | Optional: \{\}
| +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | Optional: \{\}
| +| `spec` _[ClusterObjectDeploymentSpec](#clusterobjectdeploymentspec)_ | spec defines the desired deployment template. | | Required: \{\}
| +| `status` _[ClusterObjectDeploymentStatus](#clusterobjectdeploymentstatus)_ | status defines the observed state and is written through the status subresource. | | Optional: \{\}
| + + +#### ClusterObjectDeploymentList + + + +ClusterObjectDeploymentList contains ClusterObjectDeployment objects. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `olm.operatorframework.io/v1` | | | +| `kind` _string_ | `ClusterObjectDeploymentList` | | | +| `kind` _string_ | Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | | Optional: \{\}
| +| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | | Optional: \{\}
| +| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | Optional: \{\}
| +| `items` _[ClusterObjectDeployment](#clusterobjectdeployment) array_ | items is the list of deployments. | | Required: \{\}
| + + +#### ClusterObjectDeploymentSpec + + + +ClusterObjectDeploymentSpec defines the mutable desired state of a deployment. + + + +_Appears in:_ +- [ClusterObjectDeployment](#clusterobjectdeployment) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `template` _[ClusterObjectDeploymentTemplate](#clusterobjectdeploymenttemplate)_ | template is the desired content of newly created ClusterObjectSets.
Changes to template create a new revision; existing revisions remain immutable. | | Required: \{\}
| + + +#### ClusterObjectDeploymentStatus + + + +ClusterObjectDeploymentStatus reports rollout progress. + + + +_Appears in:_ +- [ClusterObjectDeployment](#clusterobjectdeployment) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | conditions describes rollout progress for the observed generation.
Progressing is True with reason NewClusterObjectSetProgressing while the desired
rollout is incomplete, or False with reason NewClusterObjectSetProgressed after
completion. Completion remains recorded even if the completed revision later
becomes unhealthy. | | Optional: \{\}
| + + +#### ClusterObjectDeploymentTemplate + + + +ClusterObjectDeploymentTemplate describes the caller-controlled part of a revision. + + + +_Appears in:_ +- [ClusterObjectDeploymentSpec](#clusterobjectdeploymentspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `spec` _[ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec)_ | spec contains the desired revision content. Group, revision, lifecycle state,
and ownership are assigned by the controller and cannot be set here. | | Required: \{\}
| + + +#### ClusterObjectDeploymentTemplateSpec + + + +ClusterObjectDeploymentTemplateSpec contains mutable ClusterObjectSet content. + + + +_Appears in:_ +- [ClusterObjectDeploymentTemplate](#clusterobjectdeploymenttemplate) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `phases` _[ClusterObjectSetPhase](#clusterobjectsetphase) array_ | phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains
at most 50 objects using the existing inline object or immutable Secret reference
representation. Phase, object, and probe order participate in template identity. | | MaxItems: 20
MinItems: 1
Required: \{\}
| +| `progressionProbes` _[ProgressionProbe](#progressionprobe) array_ | progressionProbes supplies custom readiness probes for objects in every phase.
The existing ClusterObjectSet selector and assertion semantics apply. | | MaxItems: 20
MinItems: 1
Optional: \{\}
| +| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection is the default strategy for objects in the revision.
Objects and phases may override it. When omitted the controller uses Prevent
in the generated ClusterObjectSet. Omission remains distinct from explicit
Prevent for template identity; admission does not default this field. | | Enum: [Prevent IfNoController None]
Optional: \{\}
| + + + + +#### ClusterObjectSetObject + + + +ClusterObjectSetObject represents a Kubernetes object to be applied as part +of a phase, along with its collision protection settings. + +Exactly one of object or ref must be set. + +_Validation:_ +- ExactlyOneOf: [object ref] + +_Appears in:_ +- [ClusterObjectSetPhase](#clusterobjectsetphase) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `object` _[Unstructured](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#unstructured-unstructured-v1)_ | object is an optional embedded Kubernetes object to be applied.
Exactly one of object or ref must be set.
This object must be a valid Kubernetes resource with apiVersion, kind, and metadata fields. | | EmbeddedResource: \{\}
Optional: \{\}
| +| `ref` _[ObjectSourceRef](#objectsourceref)_ | ref is an optional reference to a Secret that holds the serialized
object manifest.
Exactly one of object or ref must be set. | | Optional: \{\}
| +| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection controls whether the operator can adopt and modify objects
that already exist on the cluster.
Allowed values are: "Prevent", "IfNoController", and "None".
When set to "Prevent", the operator only manages objects it created itself.
This prevents ownership collisions.
When set to "IfNoController", the operator can adopt and modify pre-existing objects
that are not owned by another controller.
This is useful for taking over management of manually-created resources.
When set to "None", the operator can adopt and modify any pre-existing object, even if
owned by another controller.
Use this setting with extreme caution as it may cause multiple controllers to fight over
the same resource, resulting in increased load on the API server and etcd.
When omitted, the value is inherited from the phase, then spec. | | Enum: [Prevent IfNoController None]
Optional: \{\}
| + + +#### ClusterObjectSetPhase + + + +ClusterObjectSetPhase represents a group of objects that are applied together. The phase is considered +complete only after all objects pass their status probes. + + + +_Appears in:_ +- [ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | name is a required identifier for this phase.
phase names must follow the DNS label standard as defined in [RFC 1123].
They must contain only lowercase alphanumeric characters or hyphens (-),
start and end with an alphanumeric character, and be no longer than 63 characters.
Common phase names include: namespaces, policies, rbac, crds, storage, deploy, publish.
[RFC 1123]: https://tools.ietf.org/html/rfc1123 | | MaxLength: 63
MinLength: 1
Required: \{\}
| +| `objects` _[ClusterObjectSetObject](#clusterobjectsetobject) array_ | objects is a required list of all Kubernetes objects that belong to this phase.
All objects in this list are applied to the cluster in no particular order. The maximum number of objects per phase is 50. | | ExactlyOneOf: [object ref]
MaxItems: 50
Required: \{\}
| +| `collisionProtection` _[CollisionProtection](#collisionprotection)_ | collisionProtection specifies the default collision protection strategy for all objects
in this phase. Individual objects can override this value.
When set, this value is used as the default for any object in this phase that does not
explicitly specify its own collisionProtection.
When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not
explicitly specify its own collisionProtection. | | Enum: [Prevent IfNoController None]
Optional: \{\}
| + + +#### CollisionProtection + +_Underlying type:_ _string_ + +CollisionProtection specifies if and how ownership collisions are prevented. + + + +_Appears in:_ +- [ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec) +- [ClusterObjectSetObject](#clusterobjectsetobject) +- [ClusterObjectSetPhase](#clusterobjectsetphase) + +| Field | Description | +| --- | --- | +| `Prevent` | CollisionProtectionPrevent prevents owner collisions entirely
by only allowing to work with objects itself has created.
| +| `IfNoController` | CollisionProtectionIfNoController allows to patch and override
objects already present if they are not owned by another controller.
| +| `None` | CollisionProtectionNone allows to patch and override objects
already present and owned by other controllers.
Be careful! This setting may cause multiple controllers to fight over a resource,
causing load on the API server and etcd.
| #### ConditionEqualProbe @@ -477,6 +649,23 @@ _Appears in:_ | `label` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#labelselector-v1-meta)_ | label is the label selector definition.
Required when type is "Label".
A probe using a Label selector will be executed against every object matching the labels or expressions; you must use care
when using this type of selector. For example, if multiple Kind objects are selected via labels then the probe is
likely to fail because the values of different Kind objects rarely share the same schema.
The LabelSelector field uses the following Kubernetes format:
https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#LabelSelector
Requires exactly one of matchLabels or matchExpressions.
**Experimental channel:** | | Optional: \{\}
| +#### ObjectSourceRef + + + +ObjectSourceRef references content within a Secret that contains a +serialized object manifest. + + + +_Appears in:_ +- [ClusterObjectSetObject](#clusterobjectsetobject) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | name is the name of the referenced Secret. | | MaxLength: 253
MinLength: 1
Required: \{\}
| +| `namespace` _string_ | namespace is the namespace of the referenced Secret.
When empty, defaults to the OLM system namespace during ref resolution. | | MaxLength: 63
Optional: \{\}
| +| `key` _string_ | key is the data key within the referenced Secret containing the
object manifest content. The value at this key must be a
JSON-serialized Kubernetes object manifest. | | MaxLength: 253
MinLength: 1
Required: \{\}
| #### PreflightConfig @@ -513,6 +702,21 @@ _Appears in:_ | `FieldValue` | | +#### ProgressionProbe + + + +ProgressionProbe provides a custom probe definition, consisting of an object selection method and assertions. + + + +_Appears in:_ +- [ClusterObjectDeploymentTemplateSpec](#clusterobjectdeploymenttemplatespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `selector` _[ObjectSelector](#objectselector)_ | selector is a required field which defines the method by which we select objects to apply the below
assertions to. Any object which matches the defined selector will have all the associated assertions
applied against it.
If no objects within a phase are selected by the provided selector, then all assertions defined here
are considered to have succeeded.
**Experimental channel:** | | Required: \{\}
| +| `assertions` _[Assertion](#assertion) array_ | assertions is a required list of checks which will run against the objects selected by the selector. If
one or more assertions fail then the phase within which the object lives will be not be considered
'Ready', blocking rollout of all subsequent phases.
**Experimental channel:** | | MaxItems: 20
MinItems: 1
Required: \{\}
| #### ResolvedCatalogSource diff --git a/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml new file mode 100644 index 0000000000..8ecf889a6c --- /dev/null +++ b/helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectdeployments.yaml @@ -0,0 +1,560 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + olm.operatorframework.io/generator: experimental + name: clusterobjectdeployments.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterObjectDeployment + listKind: ClusterObjectDeploymentList + plural: clusterobjectdeployments + shortNames: + - cod + singular: clusterobjectdeployment + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Progressing')].status + name: Progressing + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ClusterObjectDeployment manages mutable desired content through immutable + ClusterObjectSet revisions. It is an experimental, cluster-scoped API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec defines the desired deployment template. + properties: + template: + description: |- + template is the desired content of newly created ClusterObjectSets. + Changes to template create a new revision; existing revisions remain immutable. + properties: + spec: + description: |- + spec contains the desired revision content. Group, revision, lifecycle state, + and ownership are assigned by the controller and cannot be set here. + properties: + collisionProtection: + description: |- + collisionProtection is the default strategy for objects in the revision. + Objects and phases may override it. When omitted the controller uses Prevent + in the generated ClusterObjectSet. Omission remains distinct from explicit + Prevent for template identity; admission does not default this field. + enum: + - Prevent + - IfNoController + - None + type: string + phases: + description: |- + phases is an ordered list of 1 to 20 uniquely named phases. Each phase contains + at most 50 objects using the existing inline object or immutable Secret reference + representation. Phase, object, and probe order participate in template identity. + items: + description: |- + ClusterObjectSetPhase represents a group of objects that are applied together. The phase is considered + complete only after all objects pass their status probes. + properties: + collisionProtection: + description: |- + collisionProtection specifies the default collision protection strategy for all objects + in this phase. Individual objects can override this value. + + When set, this value is used as the default for any object in this phase that does not + explicitly specify its own collisionProtection. + + When omitted, we use .spec.collistionProtection as the default for any object in this phase that does not + explicitly specify its own collisionProtection. + enum: + - Prevent + - IfNoController + - None + type: string + name: + description: |- + name is a required identifier for this phase. + + phase names must follow the DNS label standard as defined in [RFC 1123]. + They must contain only lowercase alphanumeric characters or hyphens (-), + start and end with an alphanumeric character, and be no longer than 63 characters. + + Common phase names include: namespaces, policies, rbac, crds, storage, deploy, publish. + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: the value must consist of only lowercase + alphanumeric characters and hyphens, and must start + and end with an alphanumeric character. + rule: '!format.dns1123Label().validate(self).hasValue()' + objects: + description: |- + objects is a required list of all Kubernetes objects that belong to this phase. + + All objects in this list are applied to the cluster in no particular order. The maximum number of objects per phase is 50. + items: + description: |- + ClusterObjectSetObject represents a Kubernetes object to be applied as part + of a phase, along with its collision protection settings. + + Exactly one of object or ref must be set. + properties: + collisionProtection: + description: |- + collisionProtection controls whether the operator can adopt and modify objects + that already exist on the cluster. + + Allowed values are: "Prevent", "IfNoController", and "None". + + When set to "Prevent", the operator only manages objects it created itself. + This prevents ownership collisions. + + When set to "IfNoController", the operator can adopt and modify pre-existing objects + that are not owned by another controller. + This is useful for taking over management of manually-created resources. + + When set to "None", the operator can adopt and modify any pre-existing object, even if + owned by another controller. + Use this setting with extreme caution as it may cause multiple controllers to fight over + the same resource, resulting in increased load on the API server and etcd. + + When omitted, the value is inherited from the phase, then spec. + enum: + - Prevent + - IfNoController + - None + type: string + object: + description: |- + object is an optional embedded Kubernetes object to be applied. + + Exactly one of object or ref must be set. + + This object must be a valid Kubernetes resource with apiVersion, kind, and metadata fields. + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + ref: + description: |- + ref is an optional reference to a Secret that holds the serialized + object manifest. + + Exactly one of object or ref must be set. + properties: + key: + description: |- + key is the data key within the referenced Secret containing the + object manifest content. The value at this key must be a + JSON-serialized Kubernetes object manifest. + maxLength: 253 + minLength: 1 + type: string + name: + description: name is the name of the referenced + Secret. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + namespace is the namespace of the referenced Secret. + When empty, defaults to the OLM system namespace during ref resolution. + maxLength: 63 + type: string + required: + - key + - name + type: object + type: object + x-kubernetes-validations: + - message: exactly one of the fields in [object ref] + must be set + rule: '[has(self.object),has(self.ref)].filter(x,x==true).size() + == 1' + maxItems: 50 + type: array + required: + - name + - objects + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + progressionProbes: + description: |- + progressionProbes supplies custom readiness probes for objects in every phase. + The existing ClusterObjectSet selector and assertion semantics apply. + items: + description: ProgressionProbe provides a custom probe definition, + consisting of an object selection method and assertions. + properties: + assertions: + description: |- + assertions is a required list of checks which will run against the objects selected by the selector. If + one or more assertions fail then the phase within which the object lives will be not be considered + 'Ready', blocking rollout of all subsequent phases. + items: + description: Assertion is a discriminated union which + defines the probe type and definition used as an + assertion. + properties: + conditionEqual: + description: conditionEqual contains the expected + condition type and status. + properties: + status: + description: |- + status sets the expected condition status. + + Allowed values are "True" and "False". + enum: + - "True" + - "False" + type: string + type: + description: type sets the expected condition + type, i.e. "Ready". + maxLength: 200 + minLength: 1 + type: string + required: + - status + - type + type: object + fieldValue: + description: fieldValue contains the expected + field path and value found within. + properties: + fieldPath: + description: |- + fieldPath sets the field path for the field to check, i.e. "status.phase". The probe will fail + if the path does not exist. + maxLength: 200 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must contain a valid field path. + valid fields contain upper or lower-case + alphanumeric characters separated by the + "." character. + rule: self.matches('^[a-zA-Z0-9]+(?:\\.[a-zA-Z0-9]+)*$') + value: + description: value sets the expected value + found at fieldPath, i.e. "Bound". + maxLength: 200 + minLength: 1 + type: string + required: + - fieldPath + - value + type: object + fieldsEqual: + description: fieldsEqual contains the two field + paths whose values are expected to match. + properties: + fieldA: + description: |- + fieldA sets the field path for the first field, i.e. "spec.replicas". The probe will fail + if the path does not exist. + maxLength: 200 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must contain a valid field path. + valid fields contain upper or lower-case + alphanumeric characters separated by the + "." character. + rule: self.matches('^[a-zA-Z0-9]+(?:\\.[a-zA-Z0-9]+)*$') + fieldB: + description: |- + fieldB sets the field path for the second field, i.e. "status.readyReplicas". The probe will fail + if the path does not exist. + maxLength: 200 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must contain a valid field path. + valid fields contain upper or lower-case + alphanumeric characters separated by the + "." character. + rule: self.matches('^[a-zA-Z0-9]+(?:\\.[a-zA-Z0-9]+)*$') + required: + - fieldA + - fieldB + type: object + type: + description: |- + type is a required field which specifies the type of probe to use. + + The allowed probe types are "ConditionEqual", "FieldsEqual", and "FieldValue". + + When set to "ConditionEqual", the probe checks objects that have reached a condition of specified type and status. + When set to "FieldsEqual", the probe checks that the values found at two provided field paths are matching. + When set to "FieldValue", the probe checks that the value found at the provided field path matches what was specified. + enum: + - ConditionEqual + - FieldsEqual + - FieldValue + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: conditionEqual is required when type is + ConditionEqual, and forbidden otherwise + rule: 'self.type == ''ConditionEqual'' ?has(self.conditionEqual) + : !has(self.conditionEqual)' + - message: fieldsEqual is required when type is FieldsEqual, + and forbidden otherwise + rule: 'self.type == ''FieldsEqual'' ?has(self.fieldsEqual) + : !has(self.fieldsEqual)' + - message: fieldValue is required when type is FieldValue, + and forbidden otherwise + rule: 'self.type == ''FieldValue'' ?has(self.fieldValue) + : !has(self.fieldValue)' + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + selector: + description: |- + selector is a required field which defines the method by which we select objects to apply the below + assertions to. Any object which matches the defined selector will have all the associated assertions + applied against it. + + If no objects within a phase are selected by the provided selector, then all assertions defined here + are considered to have succeeded. + properties: + groupKind: + description: |- + groupKind specifies the group and kind of objects to select. + + Required when type is "GroupKind". + + Uses the Kubernetes format specified here: + https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#GroupKind + properties: + group: + type: string + kind: + type: string + required: + - group + - kind + type: object + label: + description: |- + label is the label selector definition. + + Required when type is "Label". + + A probe using a Label selector will be executed against every object matching the labels or expressions; you must use care + when using this type of selector. For example, if multiple Kind objects are selected via labels then the probe is + likely to fail because the values of different Kind objects rarely share the same schema. + + The LabelSelector field uses the following Kubernetes format: + https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#LabelSelector + Requires exactly one of matchLabels or matchExpressions. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: exactly one of matchLabels or matchExpressions + must be set + rule: (has(self.matchExpressions) && !has(self.matchLabels)) + || (!has(self.matchExpressions) && has(self.matchLabels)) + type: + description: |- + type is a required field which specifies the type of selector to use. + + The allowed selector types are "GroupKind" and "Label". + + When set to "GroupKind", all objects which match the specified group and kind will be selected. + When set to "Label", all objects which match the specified labels and/or expressions will be selected. + enum: + - GroupKind + - Label + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupKind is required when type is GroupKind, + and forbidden otherwise + rule: 'self.type == ''GroupKind'' ?has(self.groupKind) + : !has(self.groupKind)' + - message: label is required when type is Label, and + forbidden otherwise + rule: 'self.type == ''Label'' ?has(self.label) : !has(self.label)' + required: + - assertions + - selector + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - phases + type: object + required: + - spec + type: object + required: + - template + type: object + status: + description: status defines the observed state and is written through + the status subresource. + properties: + conditions: + description: |- + conditions describes rollout progress for the observed generation. + Progressing is True with reason NewClusterObjectSetProgressing while the desired + rollout is incomplete, or False with reason NewClusterObjectSetProgressed after + completion. Completion remains recorded even if the completed revision later + becomes unhealthy. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: name must be 1 to 52 lowercase alphanumeric or hyphen characters, + starting with a letter and ending with an alphanumeric character + rule: size(self.metadata.name) <= 52 && self.metadata.name.matches('^[a-z]([a-z0-9-]*[a-z0-9])?$') + served: true + storage: true + subresources: + status: {}