From b230589252b08d4da2ac6eed9964d4f91698be16 Mon Sep 17 00:00:00 2001 From: Daniel Franz Date: Tue, 10 Mar 2026 22:08:29 +0900 Subject: [PATCH] ProgressionProbes Provides an API which allows custom probe definitions to determine readiness of the CER phases. Objects can be selected for in one of two ways: by GroupKind, or by Label (matchLabels and matchExpressions). They can then be tested via any of: Condition, FieldsEqual, and FieldValue. Condition checks that the object has a condition matching the type and status provided. FieldsEqual uses two provided field paths and checks for equality. FieldValue uses a provided field path and checks that the value is equal to the provided expected value. Signed-off-by: Daniel Franz --- api/v1/clusterextensionrevision_types.go | 179 ++++++++++++++ api/v1/zz_generated.deepcopy.go | 128 ++++++++++ .../api/v1/clusterextensionrevisionspec.go | 20 ++ applyconfigurations/api/v1/conditionprobe.go | 51 ++++ .../api/v1/fieldsequalprobe.go | 53 +++++ applyconfigurations/api/v1/fieldvalueprobe.go | 52 +++++ applyconfigurations/api/v1/probeassertion.go | 81 +++++++ applyconfigurations/api/v1/probeselector.go | 82 +++++++ .../api/v1/progressionprobe.go | 63 +++++ applyconfigurations/utils.go | 12 + docs/api-reference/olmv1-api-reference.md | 125 ++++++++++ ...ramework.io_clusterextensionrevisions.yaml | 218 ++++++++++++++++++ internal/operator-controller/applier/phase.go | 57 +++++ .../operator-controller/applier/phase_test.go | 84 +++++++ .../clusterextensionrevision_controller.go | 113 ++++++--- manifests/experimental-e2e.yaml | 218 ++++++++++++++++++ manifests/experimental.yaml | 218 ++++++++++++++++++ test/e2e/features/revision.feature | 183 ++++++++++++++- .../pvc-probe-sa-boxcutter-rbac-template.yaml | 5 +- 19 files changed, 1908 insertions(+), 34 deletions(-) create mode 100644 applyconfigurations/api/v1/conditionprobe.go create mode 100644 applyconfigurations/api/v1/fieldsequalprobe.go create mode 100644 applyconfigurations/api/v1/fieldvalueprobe.go create mode 100644 applyconfigurations/api/v1/probeassertion.go create mode 100644 applyconfigurations/api/v1/probeselector.go create mode 100644 applyconfigurations/api/v1/progressionprobe.go diff --git a/api/v1/clusterextensionrevision_types.go b/api/v1/clusterextensionrevision_types.go index f7e90db899..2a5d4f8584 100644 --- a/api/v1/clusterextensionrevision_types.go +++ b/api/v1/clusterextensionrevision_types.go @@ -106,6 +106,18 @@ type ClusterExtensionRevisionSpec struct { // ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"` + // progressionProbes is an optional field which provides the ability to define custom readiness probes + // for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + // within the phases already have some kind of readiness check built-in, but this field allows for checks + // which are tailored to the objects being rolled out - particularly custom resources. + // + // The maximum number of probes is 20. + // + // +kubebuilder:validation:MaxItems=20 + // +listType=atomic + // +optional + ProgressionProbes []ProgressionProbe `json:"progressionProbes,omitempty"` + // collisionProtection specifies the default collision protection strategy for all objects // in this revision. Individual phases or objects can override this value. // @@ -120,6 +132,173 @@ type ClusterExtensionRevisionSpec struct { CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` } +// ProgressionProbe provides a custom probe definition, consisting of an object selection method and assertions. +type ProgressionProbe struct { + // 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. + // + // +required + Selector ProbeSelector `json:"selector,omitzero"` + + // 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. + // + // +kubebuilder:validation:MaxItems=20 + // +listType=atomic + // +required + Assertions []ProbeAssertion `json:"assertions,omitempty"` +} + +// ProbeSelector is a discriminated union which defines the method by which we select objects to make assertions against. +// +union +// +kubebuilder:validation:XValidation:rule="self.type == 'GroupKind' ?has(self.groupKind) : !has(self.groupKind)",message="groupKind is required when type is GroupKind, and forbidden otherwise" +// +kubebuilder:validation:XValidation:rule="self.type == 'Label' ?has(self.label) : !has(self.label)",message="label is required when type is Label, and forbidden otherwise" +type ProbeSelector struct { + // 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. + // + // +unionDiscriminator + // +kubebuilder:validation:Enum=GroupKind;Label + // +required + SelectorType SelectorType `json:"type,omitempty"` + + // 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 + // + // +optional + // +unionMember + GroupKind *metav1.GroupKind `json:"groupKind,omitempty"` + + // label is the label selector definition. + // + // Required when type is "Label". + // + // Uses the kubernetes format specified here: + // https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#LabelSelector + // + // +optional + // +unionMember + Label *metav1.LabelSelector `json:"label,omitempty"` +} + +// SelectorType defines the type of selector used for progressionProbes. +// +enum +type SelectorType string + +const ( + SelectorTypeGroupKind SelectorType = "GroupKind" + SelectorTypeLabel SelectorType = "Label" +) + +// ProbeType defines the type of probe used as an assertion. +// +enum +type ProbeType string + +const ( + ProbeTypeFieldCondition ProbeType = "Condition" + ProbeTypeFieldEqual ProbeType = "FieldsEqual" + ProbeTypeFieldValue ProbeType = "FieldValue" +) + +// ProbeAssertion is a discriminated union which defines the probe type and definition used as an assertion. +// +union +// +kubebuilder:validation:XValidation:rule="self.type == 'Condition' ?has(self.condition) : !has(self.condition)",message="condition is required when type is Condition, and forbidden otherwise" +// +kubebuilder:validation:XValidation:rule="self.type == 'FieldsEqual' ?has(self.fieldsEqual) : !has(self.fieldsEqual)",message="fieldsEqual is required when type is FieldsEqual, and forbidden otherwise" +// +kubebuilder:validation:XValidation:rule="self.type == 'FieldValue' ?has(self.fieldValue) : !has(self.fieldValue)",message="fieldValue is required when type is FieldValue, and forbidden otherwise" +type ProbeAssertion struct { + // type is a required field which specifies the type of probe to use. + // + // The allowed probe types are "Condition", "FieldsEqual", and "FieldValue". + // + // When set to "Condition", 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. + // + // +unionDiscriminator + // +kubebuilder:validation:Enum=Condition;FieldsEqual;FieldValue + // +required + ProbeType ProbeType `json:"type,omitempty"` + + // condition contains the expected condition type and status. + // + // +unionMember + // +optional + Condition *ConditionProbe `json:"condition,omitempty"` + + // fieldsEqual contains the two field paths whose values are expected to match. + // + // +unionMember + // +optional + FieldsEqual *FieldsEqualProbe `json:"fieldsEqual,omitempty"` + + // fieldValue contains the expected field path and value found within. + // + // +unionMember + // +optional + FieldValue *FieldValueProbe `json:"fieldValue,omitempty"` +} + +// ConditionProbe defines the condition type and status required for the probe to succeed. +type ConditionProbe struct { + // type sets the expected condition type, i.e. "Ready". + // + // +kubebuilder:validation:MinLength=1 + // +required + Type string `json:"type,omitempty"` + + // status sets the expected condition status, i.e. "True". + // + // +kubebuilder:validation:MinLength=1 + // +required + Status string `json:"status,omitempty"` +} + +// FieldsEqualProbe defines the paths of the two fields required to match for the probe to succeed. +type FieldsEqualProbe struct { + // fieldA sets the field path for the first field, i.e. "spec.replicas". The probe will fail + // if the path does not exist. + // + // +kubebuilder:validation:MinLength=1 + // +required + FieldA string `json:"fieldA,omitempty"` + + // fieldB sets the field path for the second field, i.e. "status.readyReplicas". The probe will fail + // if the path does not exist. + // + // +kubebuilder:validation:MinLength=1 + // +required + FieldB string `json:"fieldB,omitempty"` +} + +// FieldValueProbe defines the path and value expected within for the probe to succeed. +type FieldValueProbe struct { + // fieldPath sets the field path for the field to check, i.e. "status.phase". The probe will fail + // if the path does not exist. + // + // +kubebuilder:validation:MinLength=1 + // +required + FieldPath string `json:"fieldPath,omitempty"` + + // value sets the expected value found at fieldPath, i.e. "Bound". + // + // +kubebuilder:validation:MinLength=1 + // +required + Value string `json:"value,omitempty"` +} + // ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision. type ClusterExtensionRevisionLifecycleState string diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 0c5c67c164..abb7073785 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -446,6 +446,13 @@ func (in *ClusterExtensionRevisionSpec) DeepCopyInto(out *ClusterExtensionRevisi (*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 ClusterExtensionRevisionSpec. @@ -541,6 +548,51 @@ 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 *ConditionProbe) DeepCopyInto(out *ConditionProbe) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionProbe. +func (in *ConditionProbe) DeepCopy() *ConditionProbe { + if in == nil { + return nil + } + out := new(ConditionProbe) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FieldValueProbe) DeepCopyInto(out *FieldValueProbe) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FieldValueProbe. +func (in *FieldValueProbe) DeepCopy() *FieldValueProbe { + if in == nil { + return nil + } + out := new(FieldValueProbe) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FieldsEqualProbe) DeepCopyInto(out *FieldsEqualProbe) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FieldsEqualProbe. +func (in *FieldsEqualProbe) DeepCopy() *FieldsEqualProbe { + if in == nil { + return nil + } + out := new(FieldsEqualProbe) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImageSource) DeepCopyInto(out *ImageSource) { *out = *in @@ -581,6 +633,82 @@ func (in *PreflightConfig) DeepCopy() *PreflightConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProbeAssertion) DeepCopyInto(out *ProbeAssertion) { + *out = *in + if in.Condition != nil { + in, out := &in.Condition, &out.Condition + *out = new(ConditionProbe) + **out = **in + } + if in.FieldsEqual != nil { + in, out := &in.FieldsEqual, &out.FieldsEqual + *out = new(FieldsEqualProbe) + **out = **in + } + if in.FieldValue != nil { + in, out := &in.FieldValue, &out.FieldValue + *out = new(FieldValueProbe) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProbeAssertion. +func (in *ProbeAssertion) DeepCopy() *ProbeAssertion { + if in == nil { + return nil + } + out := new(ProbeAssertion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProbeSelector) DeepCopyInto(out *ProbeSelector) { + *out = *in + if in.GroupKind != nil { + in, out := &in.GroupKind, &out.GroupKind + *out = (*in).DeepCopy() + } + if in.Label != nil { + in, out := &in.Label, &out.Label + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProbeSelector. +func (in *ProbeSelector) DeepCopy() *ProbeSelector { + if in == nil { + return nil + } + out := new(ProbeSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProgressionProbe) DeepCopyInto(out *ProgressionProbe) { + *out = *in + in.Selector.DeepCopyInto(&out.Selector) + if in.Assertions != nil { + in, out := &in.Assertions, &out.Assertions + *out = make([]ProbeAssertion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProgressionProbe. +func (in *ProgressionProbe) DeepCopy() *ProgressionProbe { + if in == nil { + return nil + } + out := new(ProgressionProbe) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResolvedCatalogSource) DeepCopyInto(out *ResolvedCatalogSource) { *out = *in diff --git a/applyconfigurations/api/v1/clusterextensionrevisionspec.go b/applyconfigurations/api/v1/clusterextensionrevisionspec.go index 8a3a7ee081..f80009be8e 100644 --- a/applyconfigurations/api/v1/clusterextensionrevisionspec.go +++ b/applyconfigurations/api/v1/clusterextensionrevisionspec.go @@ -71,6 +71,13 @@ type ClusterExtensionRevisionSpecApplyConfiguration struct { // // ProgressDeadlineMinutes *int32 `json:"progressDeadlineMinutes,omitempty"` + // progressionProbes is an optional field which provides the ability to define custom readiness probes + // for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + // within the phases already have some kind of readiness check built-in, but this field allows for checks + // which are tailored to the objects being rolled out - particularly custom resources. + // + // The maximum number of probes is 20. + ProgressionProbes []ProgressionProbeApplyConfiguration `json:"progressionProbes,omitempty"` // collisionProtection specifies the default collision protection strategy for all objects // in this revision. Individual phases or objects can override this value. // @@ -124,6 +131,19 @@ func (b *ClusterExtensionRevisionSpecApplyConfiguration) WithProgressDeadlineMin 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 *ClusterExtensionRevisionSpecApplyConfiguration) WithProgressionProbes(values ...*ProgressionProbeApplyConfiguration) *ClusterExtensionRevisionSpecApplyConfiguration { + 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. diff --git a/applyconfigurations/api/v1/conditionprobe.go b/applyconfigurations/api/v1/conditionprobe.go new file mode 100644 index 0000000000..993348f2e0 --- /dev/null +++ b/applyconfigurations/api/v1/conditionprobe.go @@ -0,0 +1,51 @@ +/* +Copyright 2022. + +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.20. DO NOT EDIT. + +package v1 + +// ConditionProbeApplyConfiguration represents a declarative configuration of the ConditionProbe type for use +// with apply. +// +// ConditionProbe defines the condition type and status required for the probe to succeed. +type ConditionProbeApplyConfiguration struct { + // type sets the expected condition type, i.e. "Ready". + Type *string `json:"type,omitempty"` + // status sets the expected condition status, i.e. "True". + Status *string `json:"status,omitempty"` +} + +// ConditionProbeApplyConfiguration constructs a declarative configuration of the ConditionProbe type for use with +// apply. +func ConditionProbe() *ConditionProbeApplyConfiguration { + return &ConditionProbeApplyConfiguration{} +} + +// WithType sets the Type 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 Type field is set to the value of the last call. +func (b *ConditionProbeApplyConfiguration) WithType(value string) *ConditionProbeApplyConfiguration { + b.Type = &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 *ConditionProbeApplyConfiguration) WithStatus(value string) *ConditionProbeApplyConfiguration { + b.Status = &value + return b +} diff --git a/applyconfigurations/api/v1/fieldsequalprobe.go b/applyconfigurations/api/v1/fieldsequalprobe.go new file mode 100644 index 0000000000..5b1f24ebce --- /dev/null +++ b/applyconfigurations/api/v1/fieldsequalprobe.go @@ -0,0 +1,53 @@ +/* +Copyright 2022. + +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.20. DO NOT EDIT. + +package v1 + +// FieldsEqualProbeApplyConfiguration represents a declarative configuration of the FieldsEqualProbe type for use +// with apply. +// +// FieldsEqualProbe defines the paths of the two fields required to match for the probe to succeed. +type FieldsEqualProbeApplyConfiguration struct { + // fieldA sets the field path for the first field, i.e. "spec.replicas". The probe will fail + // if the path does not exist. + FieldA *string `json:"fieldA,omitempty"` + // fieldB sets the field path for the second field, i.e. "status.readyReplicas". The probe will fail + // if the path does not exist. + FieldB *string `json:"fieldB,omitempty"` +} + +// FieldsEqualProbeApplyConfiguration constructs a declarative configuration of the FieldsEqualProbe type for use with +// apply. +func FieldsEqualProbe() *FieldsEqualProbeApplyConfiguration { + return &FieldsEqualProbeApplyConfiguration{} +} + +// WithFieldA sets the FieldA 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 FieldA field is set to the value of the last call. +func (b *FieldsEqualProbeApplyConfiguration) WithFieldA(value string) *FieldsEqualProbeApplyConfiguration { + b.FieldA = &value + return b +} + +// WithFieldB sets the FieldB 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 FieldB field is set to the value of the last call. +func (b *FieldsEqualProbeApplyConfiguration) WithFieldB(value string) *FieldsEqualProbeApplyConfiguration { + b.FieldB = &value + return b +} diff --git a/applyconfigurations/api/v1/fieldvalueprobe.go b/applyconfigurations/api/v1/fieldvalueprobe.go new file mode 100644 index 0000000000..66a84065db --- /dev/null +++ b/applyconfigurations/api/v1/fieldvalueprobe.go @@ -0,0 +1,52 @@ +/* +Copyright 2022. + +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.20. DO NOT EDIT. + +package v1 + +// FieldValueProbeApplyConfiguration represents a declarative configuration of the FieldValueProbe type for use +// with apply. +// +// FieldValueProbe defines the path and value expected within for the probe to succeed. +type FieldValueProbeApplyConfiguration struct { + // fieldPath sets the field path for the field to check, i.e. "status.phase". The probe will fail + // if the path does not exist. + FieldPath *string `json:"fieldPath,omitempty"` + // value sets the expected value found at fieldPath, i.e. "Bound". + Value *string `json:"value,omitempty"` +} + +// FieldValueProbeApplyConfiguration constructs a declarative configuration of the FieldValueProbe type for use with +// apply. +func FieldValueProbe() *FieldValueProbeApplyConfiguration { + return &FieldValueProbeApplyConfiguration{} +} + +// WithFieldPath sets the FieldPath 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 FieldPath field is set to the value of the last call. +func (b *FieldValueProbeApplyConfiguration) WithFieldPath(value string) *FieldValueProbeApplyConfiguration { + b.FieldPath = &value + return b +} + +// WithValue sets the Value 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 Value field is set to the value of the last call. +func (b *FieldValueProbeApplyConfiguration) WithValue(value string) *FieldValueProbeApplyConfiguration { + b.Value = &value + return b +} diff --git a/applyconfigurations/api/v1/probeassertion.go b/applyconfigurations/api/v1/probeassertion.go new file mode 100644 index 0000000000..5ed6e660ea --- /dev/null +++ b/applyconfigurations/api/v1/probeassertion.go @@ -0,0 +1,81 @@ +/* +Copyright 2022. + +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.20. DO NOT EDIT. + +package v1 + +import ( + apiv1 "github.com/operator-framework/operator-controller/api/v1" +) + +// ProbeAssertionApplyConfiguration represents a declarative configuration of the ProbeAssertion type for use +// with apply. +// +// ProbeAssertion is a discriminated union which defines the probe type and definition used as an assertion. +type ProbeAssertionApplyConfiguration struct { + // type is a required field which specifies the type of probe to use. + // + // The allowed probe types are "Condition", "FieldsEqual", and "FieldValue". + // + // When set to "Condition", 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. + ProbeType *apiv1.ProbeType `json:"type,omitempty"` + // condition contains the expected condition type and status. + Condition *ConditionProbeApplyConfiguration `json:"condition,omitempty"` + // fieldsEqual contains the two field paths whose values are expected to match. + FieldsEqual *FieldsEqualProbeApplyConfiguration `json:"fieldsEqual,omitempty"` + // fieldValue contains the expected field path and value found within. + FieldValue *FieldValueProbeApplyConfiguration `json:"fieldValue,omitempty"` +} + +// ProbeAssertionApplyConfiguration constructs a declarative configuration of the ProbeAssertion type for use with +// apply. +func ProbeAssertion() *ProbeAssertionApplyConfiguration { + return &ProbeAssertionApplyConfiguration{} +} + +// WithProbeType sets the ProbeType 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 ProbeType field is set to the value of the last call. +func (b *ProbeAssertionApplyConfiguration) WithProbeType(value apiv1.ProbeType) *ProbeAssertionApplyConfiguration { + b.ProbeType = &value + return b +} + +// WithCondition sets the Condition 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 Condition field is set to the value of the last call. +func (b *ProbeAssertionApplyConfiguration) WithCondition(value *ConditionProbeApplyConfiguration) *ProbeAssertionApplyConfiguration { + b.Condition = value + return b +} + +// WithFieldsEqual sets the FieldsEqual 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 FieldsEqual field is set to the value of the last call. +func (b *ProbeAssertionApplyConfiguration) WithFieldsEqual(value *FieldsEqualProbeApplyConfiguration) *ProbeAssertionApplyConfiguration { + b.FieldsEqual = value + return b +} + +// WithFieldValue sets the FieldValue 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 FieldValue field is set to the value of the last call. +func (b *ProbeAssertionApplyConfiguration) WithFieldValue(value *FieldValueProbeApplyConfiguration) *ProbeAssertionApplyConfiguration { + b.FieldValue = value + return b +} diff --git a/applyconfigurations/api/v1/probeselector.go b/applyconfigurations/api/v1/probeselector.go new file mode 100644 index 0000000000..339626b0de --- /dev/null +++ b/applyconfigurations/api/v1/probeselector.go @@ -0,0 +1,82 @@ +/* +Copyright 2022. + +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.20. DO NOT EDIT. + +package v1 + +import ( + apiv1 "github.com/operator-framework/operator-controller/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + applyconfigurationsmetav1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// ProbeSelectorApplyConfiguration represents a declarative configuration of the ProbeSelector type for use +// with apply. +// +// ProbeSelector is a discriminated union which defines the method by which we select objects to make assertions against. +type ProbeSelectorApplyConfiguration struct { + // 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. + SelectorType *apiv1.SelectorType `json:"type,omitempty"` + // 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 + GroupKind *metav1.GroupKind `json:"groupKind,omitempty"` + // label is the label selector definition. + // + // Required when type is "Label". + // + // Uses the kubernetes format specified here: + // https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#LabelSelector + Label *applyconfigurationsmetav1.LabelSelectorApplyConfiguration `json:"label,omitempty"` +} + +// ProbeSelectorApplyConfiguration constructs a declarative configuration of the ProbeSelector type for use with +// apply. +func ProbeSelector() *ProbeSelectorApplyConfiguration { + return &ProbeSelectorApplyConfiguration{} +} + +// WithSelectorType sets the SelectorType 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 SelectorType field is set to the value of the last call. +func (b *ProbeSelectorApplyConfiguration) WithSelectorType(value apiv1.SelectorType) *ProbeSelectorApplyConfiguration { + b.SelectorType = &value + return b +} + +// WithGroupKind sets the GroupKind 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 GroupKind field is set to the value of the last call. +func (b *ProbeSelectorApplyConfiguration) WithGroupKind(value metav1.GroupKind) *ProbeSelectorApplyConfiguration { + b.GroupKind = &value + return b +} + +// WithLabel sets the Label 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 Label field is set to the value of the last call. +func (b *ProbeSelectorApplyConfiguration) WithLabel(value *applyconfigurationsmetav1.LabelSelectorApplyConfiguration) *ProbeSelectorApplyConfiguration { + b.Label = value + return b +} diff --git a/applyconfigurations/api/v1/progressionprobe.go b/applyconfigurations/api/v1/progressionprobe.go new file mode 100644 index 0000000000..f019f8e2e0 --- /dev/null +++ b/applyconfigurations/api/v1/progressionprobe.go @@ -0,0 +1,63 @@ +/* +Copyright 2022. + +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.20. DO NOT EDIT. + +package v1 + +// ProgressionProbeApplyConfiguration represents a declarative configuration of the ProgressionProbe type for use +// with apply. +// +// ProgressionProbe provides a custom probe definition, consisting of an object selection method and assertions. +type ProgressionProbeApplyConfiguration struct { + // 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. + Selector *ProbeSelectorApplyConfiguration `json:"selector,omitempty"` + // 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. + Assertions []ProbeAssertionApplyConfiguration `json:"assertions,omitempty"` +} + +// ProgressionProbeApplyConfiguration constructs a declarative configuration of the ProgressionProbe type for use with +// apply. +func ProgressionProbe() *ProgressionProbeApplyConfiguration { + return &ProgressionProbeApplyConfiguration{} +} + +// WithSelector sets the Selector 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 Selector field is set to the value of the last call. +func (b *ProgressionProbeApplyConfiguration) WithSelector(value *ProbeSelectorApplyConfiguration) *ProgressionProbeApplyConfiguration { + b.Selector = value + return b +} + +// WithAssertions adds the given value to the Assertions 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 Assertions field. +func (b *ProgressionProbeApplyConfiguration) WithAssertions(values ...*ProbeAssertionApplyConfiguration) *ProgressionProbeApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithAssertions") + } + b.Assertions = append(b.Assertions, *values[i]) + } + return b +} diff --git a/applyconfigurations/utils.go b/applyconfigurations/utils.go index 4c4a80e9f6..4319935225 100644 --- a/applyconfigurations/utils.go +++ b/applyconfigurations/utils.go @@ -67,12 +67,24 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1.ClusterExtensionSpecApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ClusterExtensionStatus"): return &apiv1.ClusterExtensionStatusApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ConditionProbe"): + return &apiv1.ConditionProbeApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("CRDUpgradeSafetyPreflightConfig"): return &apiv1.CRDUpgradeSafetyPreflightConfigApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("FieldsEqualProbe"): + return &apiv1.FieldsEqualProbeApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("FieldValueProbe"): + return &apiv1.FieldValueProbeApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ImageSource"): return &apiv1.ImageSourceApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("PreflightConfig"): return &apiv1.PreflightConfigApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ProbeAssertion"): + return &apiv1.ProbeAssertionApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ProbeSelector"): + return &apiv1.ProbeSelectorApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("ProgressionProbe"): + return &apiv1.ProgressionProbeApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ResolvedCatalogSource"): return &apiv1.ResolvedCatalogSourceApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ResolvedImageSource"): diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index 3ee7f19386..3bf6689515 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -367,6 +367,57 @@ _Appears in:_ +#### ConditionProbe + + + +ConditionProbe defines the condition type and status required for the probe to succeed. + + + +_Appears in:_ +- [ProbeAssertion](#probeassertion) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `type` _string_ | type sets the expected condition type, i.e. "Ready". | | MinLength: 1
Required: \{\}
| +| `status` _string_ | status sets the expected condition status, i.e. "True". | | MinLength: 1
Required: \{\}
| + + +#### FieldValueProbe + + + +FieldValueProbe defines the path and value expected within for the probe to succeed. + + + +_Appears in:_ +- [ProbeAssertion](#probeassertion) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `fieldPath` _string_ | fieldPath sets the field path for the field to check, i.e. "status.phase". The probe will fail
if the path does not exist. | | MinLength: 1
Required: \{\}
| +| `value` _string_ | value sets the expected value found at fieldPath, i.e. "Bound". | | MinLength: 1
Required: \{\}
| + + +#### FieldsEqualProbe + + + +FieldsEqualProbe defines the paths of the two fields required to match for the probe to succeed. + + + +_Appears in:_ +- [ProbeAssertion](#probeassertion) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `fieldA` _string_ | fieldA sets the field path for the first field, i.e. "spec.replicas". The probe will fail
if the path does not exist. | | MinLength: 1
Required: \{\}
| +| `fieldB` _string_ | fieldB sets the field path for the second field, i.e. "status.readyReplicas". The probe will fail
if the path does not exist. | | MinLength: 1
Required: \{\}
| + + #### ImageSource @@ -403,6 +454,63 @@ _Appears in:_ | `crdUpgradeSafety` _[CRDUpgradeSafetyPreflightConfig](#crdupgradesafetypreflightconfig)_ | crdUpgradeSafety configures the CRD Upgrade Safety pre-flight checks that run
before upgrades of installed content.
The CRD Upgrade Safety pre-flight check safeguards from unintended consequences of upgrading a CRD,
such as data loss. | | | +#### ProbeAssertion + + + +ProbeAssertion is a discriminated union which defines the probe type and definition used as an assertion. + + + +_Appears in:_ +- [ProgressionProbe](#progressionprobe) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `type` _[ProbeType](#probetype)_ | type is a required field which specifies the type of probe to use.
The allowed probe types are "Condition", "FieldsEqual", and "FieldValue".
When set to "Condition", 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: [Condition FieldsEqual FieldValue]
Required: \{\}
| +| `condition` _[ConditionProbe](#conditionprobe)_ | condition contains the expected condition type and status. | | Optional: \{\}
| +| `fieldsEqual` _[FieldsEqualProbe](#fieldsequalprobe)_ | fieldsEqual contains the two field paths whose values are expected to match. | | Optional: \{\}
| +| `fieldValue` _[FieldValueProbe](#fieldvalueprobe)_ | fieldValue contains the expected field path and value found within. | | Optional: \{\}
| + + +#### ProbeSelector + + + +ProbeSelector is a discriminated union which defines the method by which we select objects to make assertions against. + + + +_Appears in:_ +- [ProgressionProbe](#progressionprobe) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `type` _[SelectorType](#selectortype)_ | 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]
Required: \{\}
| +| `groupKind` _[GroupKind](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#groupkind-v1-meta)_ | 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 | | Optional: \{\}
| +| `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".
Uses the kubernetes format specified here:
https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#LabelSelector | | Optional: \{\}
| + + +#### ProbeType + +_Underlying type:_ _string_ + +ProbeType defines the type of probe used as an assertion. + + + +_Appears in:_ +- [ProbeAssertion](#probeassertion) + +| Field | Description | +| --- | --- | +| `Condition` | | +| `FieldsEqual` | | +| `FieldValue` | | + + + + #### ResolvedCatalogSource @@ -454,6 +562,23 @@ _Appears in:_ | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | conditions optionally expose Progressing and Available condition of the revision,
in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
Given that a ClusterExtension should remain available during upgrades, an observer may use these conditions
to get more insights about reasons for its current state. | | Optional: \{\}
| +#### SelectorType + +_Underlying type:_ _string_ + +SelectorType defines the type of selector used for progressionProbes. + + + +_Appears in:_ +- [ProbeSelector](#probeselector) + +| Field | Description | +| --- | --- | +| `GroupKind` | | +| `Label` | | + + #### ServiceAccountReference diff --git a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml index 0ff49e01b0..0406460286 100644 --- a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml +++ b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml @@ -223,6 +223,224 @@ spec: maximum: 720 minimum: 10 type: integer + progressionProbes: + description: |- + progressionProbes is an optional field which provides the ability to define custom readiness probes + for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + within the phases already have some kind of readiness check built-in, but this field allows for checks + which are tailored to the objects being rolled out - particularly custom resources. + + The maximum number of probes is 20. + 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: ProbeAssertion is a discriminated union which + defines the probe type and definition used as an assertion. + properties: + condition: + description: condition contains the expected condition + type and status. + properties: + status: + description: status sets the expected condition status, + i.e. "True". + minLength: 1 + type: string + type: + description: type sets the expected condition type, + i.e. "Ready". + 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. + minLength: 1 + type: string + value: + description: value sets the expected value found at + fieldPath, i.e. "Bound". + 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. + minLength: 1 + type: string + 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. + minLength: 1 + type: string + 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 "Condition", "FieldsEqual", and "FieldValue". + + When set to "Condition", 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: + - Condition + - FieldsEqual + - FieldValue + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: condition is required when type is Condition, and + forbidden otherwise + rule: 'self.type == ''Condition'' ?has(self.condition) : + !has(self.condition)' + - 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 + 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". + + Uses the kubernetes format specified here: + https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#LabelSelector + 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 + 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 + type: array + x-kubernetes-list-type: atomic revision: description: |- revision is a required, immutable sequence number representing a specific revision diff --git a/internal/operator-controller/applier/phase.go b/internal/operator-controller/applier/phase.go index 7675265902..859a778f4c 100644 --- a/internal/operator-controller/applier/phase.go +++ b/internal/operator-controller/applier/phase.go @@ -2,11 +2,19 @@ package applier import ( "cmp" + "encoding/json" + "fmt" "slices" + "strings" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "pkg.package-operator.run/boxcutter/probing" + "sigs.k8s.io/controller-runtime/pkg/client" ocv1ac "github.com/operator-framework/operator-controller/applyconfigurations/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" ) // The following, with modifications, is taken from: @@ -198,3 +206,52 @@ func PhaseSort(unsortedObjs []ocv1ac.ClusterExtensionRevisionObjectApplyConfigur return phasesSorted } + +// FieldValueProbe checks if the value found at FieldPath matches the provided Value +type FieldValueProbe struct { + FieldPath, Value string +} + +var _ probing.Prober = (*FieldValueProbe)(nil) + +// Probe executes the probe. +func (fe *FieldValueProbe) Probe(obj client.Object) probing.Result { + uMap, err := util.ToUnstructured(obj) + if err != nil { + return probing.UnknownResult(fmt.Sprintf("failed to convert to unstructured: %v", err)) + } + return fe.probe(uMap) +} + +func (fv *FieldValueProbe) probe(obj *unstructured.Unstructured) probing.Result { + fieldPath := strings.Split(strings.Trim(fv.FieldPath, "."), ".") + + fieldVal, ok, err := unstructured.NestedFieldCopy(obj.Object, fieldPath...) + if err != nil || !ok { + return probing.Result{ + Status: probing.StatusFalse, + Messages: []string{fmt.Sprintf(`missing key: %q`, fv.FieldPath)}, + } + } + + if !equality.Semantic.DeepEqual(fieldVal, fv.Value) { + foundJSON, err := json.Marshal(fieldVal) + if err != nil { + foundJSON = []byte("") + } + expectedJSON, err := json.Marshal(fv.Value) + if err != nil { + expectedJSON = []byte("") + } + + return probing.Result{ + Status: probing.StatusFalse, + Messages: []string{fmt.Sprintf(`value at key %q != %q; expected: %s got: %s`, fv.FieldPath, fv.Value, expectedJSON, foundJSON)}, + } + } + + return probing.Result{ + Status: probing.StatusTrue, + Messages: []string{fmt.Sprintf(`value at key %q == %q`, fv.FieldPath, fv.Value)}, + } +} diff --git a/internal/operator-controller/applier/phase_test.go b/internal/operator-controller/applier/phase_test.go index 25e2916705..8a4554be0e 100644 --- a/internal/operator-controller/applier/phase_test.go +++ b/internal/operator-controller/applier/phase_test.go @@ -3,9 +3,14 @@ package applier_test import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/utils/ptr" + "pkg.package-operator.run/boxcutter/probing" + "sigs.k8s.io/controller-runtime/pkg/client" ocv1ac "github.com/operator-framework/operator-controller/applyconfigurations/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/applier" @@ -1031,3 +1036,82 @@ func Test_PhaseSort(t *testing.T) { }) } } + +func Test_FieldValueProbe(t *testing.T) { + for _, tc := range []struct { + name string + obj client.Object + probe applier.FieldValueProbe + expectedResult probing.Result + }{ + { + name: "True result with found key and equal value", + obj: &corev1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "my-service", Namespace: "my-namespace"}, + }, + probe: applier.FieldValueProbe{ + FieldPath: "metadata.name", + Value: "my-service", + }, + expectedResult: probing.Result{ + Status: probing.StatusTrue, + Messages: []string{ + `value at key "metadata.name" == "my-service"`, + }, + }, + }, + { + name: "False result with unfound key", + obj: &corev1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "my-service", Namespace: "my-namespace"}, + }, + probe: applier.FieldValueProbe{ + FieldPath: "spec.foo", + Value: "my-service", + }, + expectedResult: probing.Result{ + Status: probing.StatusFalse, + Messages: []string{ + `missing key: "spec.foo"`, + }, + }, + }, + { + name: "False result with found key and unequal value", + obj: &corev1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "my-service", Namespace: "my-namespace"}, + }, + probe: applier.FieldValueProbe{ + FieldPath: "metadata.namespace", + Value: "bar", + }, + expectedResult: probing.Result{ + Status: probing.StatusFalse, + Messages: []string{ + `value at key "metadata.namespace" != "bar"; expected: "bar" got: "my-namespace"`, + }, + }, + }, + { + name: "Unknown result unstructured conversion failure", + obj: nil, + probe: applier.FieldValueProbe{ + FieldPath: "metadata.name", + Value: "my-service", + }, + expectedResult: probing.Result{ + Status: probing.StatusUnknown, + Messages: []string{ + "failed to convert to unstructured: object is nil", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expectedResult, tc.probe.Probe(tc.obj)) + }) + } +} diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go index aa757b7f9a..0efdf7314d 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "strings" - "sync" "time" appsv1 "k8s.io/api/apps/v1" @@ -36,6 +35,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/applier" "github.com/operator-framework/operator-controller/internal/operator-controller/labels" ) @@ -463,13 +463,15 @@ func (c *ClusterExtensionRevisionReconciler) toBoxcutterRevision(ctx context.Con previousObjs[i] = rev } - if err = initializeProbes(); err != nil { + progressionProbes, err := buildProgressionProbes(cer.Spec.ProgressionProbes) + if err != nil { return nil, nil, err } + opts := []boxcutter.RevisionReconcileOption{ boxcutter.WithPreviousOwners(previousObjs), boxcutter.WithProbe(boxcutter.ProgressProbeType, probing.And{ - &namespaceActiveProbe, deploymentProbe, statefulSetProbe, crdProbe, issuerProbe, certProbe, &pvcBoundProbe, + &namespaceActiveProbe, deploymentProbe, statefulSetProbe, crdProbe, issuerProbe, certProbe, &pvcBoundProbe, progressionProbes, }), } @@ -515,27 +517,70 @@ func EffectiveCollisionProtection(cp ...ocv1.CollisionProtection) ocv1.Collision return ecp } -// initializeProbes is used to initialize CEL probes once, so we don't recreate them on every reconcile -var initializeProbes = sync.OnceValue(func() error { - nsCEL, err := probing.NewCELProbe(namespaceActiveCEL, `namespace phase must be "Active"`) - if err != nil { - return fmt.Errorf("initializing namespace CEL probe: %w", err) - } - pvcCEL, err := probing.NewCELProbe(pvcBoundCEL, `persistentvolumeclaim phase must be "Bound"`) - if err != nil { - return fmt.Errorf("initializing PVC CEL probe: %w", err) - } - namespaceActiveProbe = probing.GroupKindSelector{ - GroupKind: schema.GroupKind{Group: corev1.GroupName, Kind: "Namespace"}, - Prober: nsCEL, - } - pvcBoundProbe = probing.GroupKindSelector{ - GroupKind: schema.GroupKind{Group: corev1.GroupName, Kind: "PersistentVolumeClaim"}, - Prober: pvcCEL, - } +// buildProgressionProbes creates a set of boxcutter probes from the fields provided in the CER's spec.progressionProbes. +// Returns nil and an error if encountered while attempting to build the probes. +func buildProgressionProbes(progressionProbes []ocv1.ProgressionProbe) (probing.And, error) { + userProbes := probing.And{} + if len(progressionProbes) < 1 { + return userProbes, nil + } + for _, progressionProbe := range progressionProbes { + // Collect all user assertions into a single 'And' + assertions := probing.And{} + for _, probe := range progressionProbe.Assertions { + switch probe.ProbeType { + // Switch based on the union discriminator; nil entries are skipped + case ocv1.ProbeTypeFieldCondition: + if probe.Condition != nil { + conditionProbe := probing.ConditionProbe(*probe.Condition) + assertions = append(assertions, &conditionProbe) + } + case ocv1.ProbeTypeFieldEqual: + if probe.FieldsEqual != nil { + fieldsEqualProbe := probing.FieldsEqualProbe(*probe.FieldsEqual) + assertions = append(assertions, &fieldsEqualProbe) + } + case ocv1.ProbeTypeFieldValue: + if probe.FieldValue != nil { + fieldValueProbe := applier.FieldValueProbe(*probe.FieldValue) + assertions = append(assertions, &fieldValueProbe) + } + default: + return nil, fmt.Errorf("unknown progressionProbe assertion probe type: %s", probe.ProbeType) + } + } - return nil -}) + // Create the selector probe based on user-requested type and provide the assertions + var selectorProbe probing.Prober + switch progressionProbe.Selector.SelectorType { + // Switch based on the union discriminator; nil entries are skipped + case ocv1.SelectorTypeGroupKind: + if progressionProbe.Selector.GroupKind == nil { + continue + } + selectorProbe = &probing.GroupKindSelector{ + GroupKind: schema.GroupKind(*progressionProbe.Selector.GroupKind), + Prober: assertions, + } + case ocv1.SelectorTypeLabel: + if progressionProbe.Selector.Label == nil { + continue + } + selector, err := metav1.LabelSelectorAsSelector(progressionProbe.Selector.Label) + if err != nil { + return nil, fmt.Errorf("invalid label selector in progressionProbe (%v): %w", progressionProbe.Selector.Label, err) + } + selectorProbe = &probing.LabelSelector{ + Selector: selector, + Prober: assertions, + } + default: + return nil, fmt.Errorf("unknown progressionProbe selector type: %s", progressionProbe.Selector.SelectorType) + } + userProbes = append(userProbes, selectorProbe) + } + return userProbes, nil +} var ( deploymentProbe = &probing.GroupKindSelector{ @@ -568,13 +613,23 @@ var ( }, } - // namespaceActiveCEL is a CEL rule which asserts that the namespace is in "Active" phase - namespaceActiveCEL = `self.status.phase == "Active"` - namespaceActiveProbe probing.GroupKindSelector + // namespaceActiveProbe is a probe which asserts that the namespace is in "Active" phase + namespaceActiveProbe = probing.GroupKindSelector{ + GroupKind: schema.GroupKind{Group: corev1.GroupName, Kind: "Namespace"}, + Prober: &applier.FieldValueProbe{ + FieldPath: "status.phase", + Value: "Active", + }, + } - // pvcBoundCEL is a CEL rule which asserts that the PVC is in "Bound" phase - pvcBoundCEL = `self.status.phase == "Bound"` - pvcBoundProbe probing.GroupKindSelector + // pvcBoundCEL is a probe which asserts that the PVC is in "Bound" phase + pvcBoundProbe = probing.GroupKindSelector{ + GroupKind: schema.GroupKind{Group: corev1.GroupName, Kind: "PersistentVolumeClaim"}, + Prober: &applier.FieldValueProbe{ + FieldPath: "status.phase", + Value: "Bound", + }, + } // deplStaefulSetProbe probes Deployment, StatefulSet objects. deplStatefulSetProbe = &probing.ObservedGenerationProbe{ diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index 74fcc399f9..9930999eaa 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -835,6 +835,224 @@ spec: maximum: 720 minimum: 10 type: integer + progressionProbes: + description: |- + progressionProbes is an optional field which provides the ability to define custom readiness probes + for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + within the phases already have some kind of readiness check built-in, but this field allows for checks + which are tailored to the objects being rolled out - particularly custom resources. + + The maximum number of probes is 20. + 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: ProbeAssertion is a discriminated union which + defines the probe type and definition used as an assertion. + properties: + condition: + description: condition contains the expected condition + type and status. + properties: + status: + description: status sets the expected condition status, + i.e. "True". + minLength: 1 + type: string + type: + description: type sets the expected condition type, + i.e. "Ready". + 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. + minLength: 1 + type: string + value: + description: value sets the expected value found at + fieldPath, i.e. "Bound". + 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. + minLength: 1 + type: string + 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. + minLength: 1 + type: string + 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 "Condition", "FieldsEqual", and "FieldValue". + + When set to "Condition", 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: + - Condition + - FieldsEqual + - FieldValue + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: condition is required when type is Condition, and + forbidden otherwise + rule: 'self.type == ''Condition'' ?has(self.condition) : + !has(self.condition)' + - 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 + 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". + + Uses the kubernetes format specified here: + https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#LabelSelector + 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 + 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 + type: array + x-kubernetes-list-type: atomic revision: description: |- revision is a required, immutable sequence number representing a specific revision diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index 12ae10d9bc..0b1314b76f 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -796,6 +796,224 @@ spec: maximum: 720 minimum: 10 type: integer + progressionProbes: + description: |- + progressionProbes is an optional field which provides the ability to define custom readiness probes + for objects defined within spec.phases. As documented in that field, most kubernetes-native objects + within the phases already have some kind of readiness check built-in, but this field allows for checks + which are tailored to the objects being rolled out - particularly custom resources. + + The maximum number of probes is 20. + 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: ProbeAssertion is a discriminated union which + defines the probe type and definition used as an assertion. + properties: + condition: + description: condition contains the expected condition + type and status. + properties: + status: + description: status sets the expected condition status, + i.e. "True". + minLength: 1 + type: string + type: + description: type sets the expected condition type, + i.e. "Ready". + 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. + minLength: 1 + type: string + value: + description: value sets the expected value found at + fieldPath, i.e. "Bound". + 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. + minLength: 1 + type: string + 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. + minLength: 1 + type: string + 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 "Condition", "FieldsEqual", and "FieldValue". + + When set to "Condition", 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: + - Condition + - FieldsEqual + - FieldValue + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: condition is required when type is Condition, and + forbidden otherwise + rule: 'self.type == ''Condition'' ?has(self.condition) : + !has(self.condition)' + - 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 + 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". + + Uses the kubernetes format specified here: + https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#LabelSelector + 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 + 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 + type: array + x-kubernetes-list-type: atomic revision: description: |- revision is a required, immutable sequence number representing a specific revision diff --git a/test/e2e/features/revision.feature b/test/e2e/features/revision.feature index 2d91266301..221cc32091 100644 --- a/test/e2e/features/revision.feature +++ b/test/e2e/features/revision.feature @@ -54,7 +54,7 @@ Feature: Install ClusterExtensionRevision Then resource "persistentvolumeclaim/test-pvc" is installed And ClusterExtensionRevision "${CER_NAME}" reports Available as False with Reason ProbeFailure and Message: """ - Object PersistentVolumeClaim.v1 ${TEST_NAMESPACE}/test-pvc: persistentvolumeclaim phase must be "Bound" + Object PersistentVolumeClaim.v1 ${TEST_NAMESPACE}/test-pvc: value at key "status.phase" != "Bound"; expected: "Bound" got: "Pending" """ And resource "configmap/test-configmap" is not installed @@ -136,3 +136,184 @@ Feature: Install ClusterExtensionRevision And resource "persistentvolume/test-pv" is installed And resource "persistentvolumeclaim/test-pvc" is installed And resource "configmap/test-configmap" is installed + + Scenario: Phases does not progress when user-provided progressionProbes do not pass + Given ServiceAccount "pvc-probe-sa" with needed permissions is available in test namespace + When ClusterExtensionRevision is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtensionRevision + metadata: + annotations: + olm.operatorframework.io/service-account-name: pvc-probe-sa + olm.operatorframework.io/service-account-namespace: ${TEST_NAMESPACE} + name: ${CER_NAME} + spec: + lifecycleState: Active + collisionProtection: Prevent + progressionProbes: + - selector: + type: Label + label: + matchLabels: + test-label: foo + assertions: + - type: FieldValue + fieldValue: + fieldPath: data.foo + value: bar + phases: + - name: cm-1 + objects: + - object: + apiVersion: v1 + kind: ConfigMap + metadata: + name: test-configmap-1 + namespace: ${TEST_NAMESPACE} + labels: + test-label: foo + data: + foo: foo + - name: cm-2 + objects: + - object: + apiVersion: v1 + kind: ConfigMap + metadata: + name: test-configmap-2 + namespace: ${TEST_NAMESPACE} + labels: + test-label: bar + data: + name: test-configmap + version: v1.2.0 + revision: 1 + """ + + Then resource "configmap/test-configmap-1" is installed + And ClusterExtensionRevision "${CER_NAME}" reports Available as False with Reason ProbeFailure and Message: + """ + Object ConfigMap.v1 ${TEST_NAMESPACE}/test-configmap-1: value at key "data.foo" != "bar"; expected: "bar" got: "foo" + """ + And resource "configmap/test-configmap-2" is not installed + + Scenario: Phases progresses when user-provided progressionProbes pass + Given ServiceAccount "pvc-probe-sa" with needed permissions is available in test namespace + When ClusterExtensionRevision is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtensionRevision + metadata: + annotations: + olm.operatorframework.io/service-account-name: pvc-probe-sa + olm.operatorframework.io/service-account-namespace: ${TEST_NAMESPACE} + name: ${CER_NAME} + spec: + lifecycleState: Active + collisionProtection: Prevent + progressionProbes: + - selector: + type: GroupKind + groupKind: + group: "" + kind: ConfigMap + assertions: + - type: FieldValue + fieldValue: + fieldPath: data.foo + value: bar + - selector: + type: GroupKind + groupKind: + group: "" + kind: ServiceAccount + assertions: + - type: FieldsEqual + fieldsEqual: + fieldA: "metadata.labels.foo" + fieldB: "metadata.labels.bar" + - selector: + type: Label + label: + matchExpressions: + - { key: expkey, operator: In, values: [exercise-label-selector-matchexpressions] } + assertions: + - type: Condition + condition: + type: "Ready" + status: "True" + phases: + - name: phase1 + objects: + - object: + apiVersion: v1 + kind: ConfigMap + metadata: + name: test-configmap-1 + namespace: ${TEST_NAMESPACE} + labels: + test-label: foo + data: + foo: bar + - name: phase2 + objects: + - object: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: test-serviceaccount + namespace: ${TEST_NAMESPACE} + labels: + foo: exercise-fieldsEqual-probe + bar: exercise-fieldsEqual-probe + - name: phase3 + objects: + - object: + apiVersion: v1 + kind: Pod + metadata: + name: test-pod + namespace: ${TEST_NAMESPACE} + labels: + expkey: exercise-label-selector-matchexpressions + spec: + containers: + - command: + - "sleep" + args: + - "1000" + image: busybox:1.36 + imagePullPolicy: IfNotPresent + name: busybox + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + - name: phase4 + objects: + - object: + apiVersion: v1 + kind: ConfigMap + metadata: + name: test-configmap-3 + namespace: ${TEST_NAMESPACE} + labels: + test-label: bar + data: + if-this-configmap-is-installed: all-prior-phase-probes-have-succeeded + foo: bar + revision: 1 + """ + + Then resource "configmap/test-configmap-1" is installed + And resource "serviceaccount/test-serviceaccount" is installed + And resource "pod/test-pod" is installed + And resource "configmap/test-configmap-3" is installed + And ClusterExtensionRevision "${CER_NAME}" reports Progressing as True with Reason Succeeded + And ClusterExtensionRevision "${CER_NAME}" reports Available as True with Reason ProbesSucceeded \ No newline at end of file diff --git a/test/e2e/steps/testdata/pvc-probe-sa-boxcutter-rbac-template.yaml b/test/e2e/steps/testdata/pvc-probe-sa-boxcutter-rbac-template.yaml index 8dceb24782..526b9845a0 100644 --- a/test/e2e/steps/testdata/pvc-probe-sa-boxcutter-rbac-template.yaml +++ b/test/e2e/steps/testdata/pvc-probe-sa-boxcutter-rbac-template.yaml @@ -17,10 +17,7 @@ metadata: namespace: ${TEST_NAMESPACE} rules: - apiGroups: [""] - resources: [persistentvolumeclaims] - verbs: [create, update, get, delete, patch] - - apiGroups: [""] - resources: [configmaps] + resources: [configmaps, persistentvolumeclaims, pods, serviceaccounts] verbs: [create, update, get, delete, patch] --- apiVersion: rbac.authorization.k8s.io/v1