diff --git a/go.mod b/go.mod index ad57958151..76ef4e4d78 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/IBM/go-sdk-core/v5 v5.17.4 github.com/IBM/networking-go-sdk v0.26.0 github.com/aws/aws-sdk-go v1.38.49 + github.com/blang/semver/v4 v4.0.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/florianl/go-nfqueue v1.3.2 github.com/go-logr/logr v1.4.2 @@ -61,7 +62,6 @@ require ( github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect diff --git a/manifests/00-cluster-role.yaml b/manifests/00-cluster-role.yaml index e29ed4bd76..8c5b28712f 100644 --- a/manifests/00-cluster-role.yaml +++ b/manifests/00-cluster-role.yaml @@ -180,6 +180,7 @@ rules: resources: - subscriptions - installplans + - clusterserviceversions verbs: - '*' diff --git a/manifests/02-deployment.yaml b/manifests/02-deployment.yaml index f36ee6340a..07a6ed3c34 100644 --- a/manifests/02-deployment.yaml +++ b/manifests/02-deployment.yaml @@ -87,6 +87,12 @@ spec: value: redhat-operators - name: GATEWAY_API_OPERATOR_CHANNEL value: stable + # NOTE: + # Use an operator version that exists in the catalog. + # If the specified version is not found, the latest available + # OSSM operator version will be installed as of the upgrade date. + # Downgrades are not supported; the new version must be + # semantically greater than the previous one. - name: GATEWAY_API_OPERATOR_VERSION value: servicemeshoperator3.v3.1.0 - name: ISTIO_VERSION diff --git a/pkg/operator/controller/gatewayclass/controller.go b/pkg/operator/controller/gatewayclass/controller.go index e625c32ae1..03b1c216d7 100644 --- a/pkg/operator/controller/gatewayclass/controller.go +++ b/pkg/operator/controller/gatewayclass/controller.go @@ -39,38 +39,6 @@ const ( // inferencepoolExperimentalCrdName is the name of the experimental // (alpha version) InferencePool CRD. inferencepoolExperimentalCrdName = "inferencepools.inference.networking.x-k8s.io" - - // subscriptionCatalogOverrideAnnotationKey is the key for an - // unsupported annotation on the gatewayclass using which a custom - // catalog source can be specified for the OSSM subscription. This - // annotation is only intended for use by OpenShift developers. Note - // that this annotation is intended to be used only when initially - // creating the gatewayclass and subscription; changing the catalog - // source on an existing subscription will likely have no effect or - // cause errors. - subscriptionCatalogOverrideAnnotationKey = "unsupported.do-not-use.openshift.io/ossm-catalog" - // subscriptionChannelOverrideAnnotationKey is the key for an - // unsupported annotation on the gatewayclass using which a custom - // channel can be specified for the OSSM subscription. This annotation - // is only intended for use by OpenShift developers. Note that this - // annotation is intended to be used only when initially creating the - // gatewayclass and subscription; changing the channel on an existing - // subscription will likely have no effect or cause errors. - subscriptionChannelOverrideAnnotationKey = "unsupported.do-not-use.openshift.io/ossm-channel" - // subscriptionVersionOverrideAnnotationKey is the key for an - // unsupported annotation on the gatewayclass using which a custom - // version of OSSM can be specified. This annotation is only intended - // for use by OpenShift developers. Note that this annotation is - // intended to be used only when initially creating the gatewayclass and - // subscription; OLM will not allow downgrades, and upgrades are - // generally restricted to the next version after the currently - // installed version. - subscriptionVersionOverrideAnnotationKey = "unsupported.do-not-use.openshift.io/ossm-version" - // istioVersionOverrideAnnotationKey is the key for an unsupported - // annotation on the gatewayclass using which a custom version of Istio - // can be specified. This annotation is only intended for use by - // OpenShift developers. - istioVersionOverrideAnnotationKey = "unsupported.do-not-use.openshift.io/istio-version" ) var log = logf.Logger.WithName(controllerName) @@ -237,15 +205,15 @@ func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( var errs []error ossmCatalog := r.config.GatewayAPIOperatorCatalog - if v, ok := gatewayclass.Annotations[subscriptionCatalogOverrideAnnotationKey]; ok { + if v, ok := gatewayclass.Annotations[operatorcontroller.SubscriptionCatalogOverrideAnnotationKey]; ok { ossmCatalog = v } ossmChannel := r.config.GatewayAPIOperatorChannel - if v, ok := gatewayclass.Annotations[subscriptionChannelOverrideAnnotationKey]; ok { + if v, ok := gatewayclass.Annotations[operatorcontroller.SubscriptionChannelOverrideAnnotationKey]; ok { ossmChannel = v } ossmVersion := r.config.GatewayAPIOperatorVersion - if v, ok := gatewayclass.Annotations[subscriptionVersionOverrideAnnotationKey]; ok { + if v, ok := gatewayclass.Annotations[operatorcontroller.SubscriptionVersionOverrideAnnotationKey]; ok { ossmVersion = v } if _, _, err := r.ensureServiceMeshOperatorSubscription(ctx, ossmCatalog, ossmChannel, ossmVersion); err != nil { @@ -255,7 +223,7 @@ func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( errs = append(errs, err) } istioVersion := r.config.IstioVersion - if v, ok := gatewayclass.Annotations[istioVersionOverrideAnnotationKey]; ok { + if v, ok := gatewayclass.Annotations[operatorcontroller.IstioVersionOverrideAnnotationKey]; ok { istioVersion = v } if _, _, err := r.ensureIstio(ctx, &gatewayclass, istioVersion); err != nil { diff --git a/pkg/operator/controller/gatewayclass/subscription.go b/pkg/operator/controller/gatewayclass/subscription.go index 927032bc32..1570e19466 100644 --- a/pkg/operator/controller/gatewayclass/subscription.go +++ b/pkg/operator/controller/gatewayclass/subscription.go @@ -3,7 +3,9 @@ package gatewayclass import ( "context" "fmt" + "regexp" + "github.com/blang/semver/v4" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "sigs.k8s.io/controller-runtime/pkg/client" @@ -31,6 +33,11 @@ const ( WorkloadPartitioningManagementPreferredScheduling = `{"effect": "PreferredDuringScheduling"}` ) +var ( + // csvSemVerRegexp is a RegExp to extract semantic version from OLM CSV name. + csvSemVerRegexp = regexp.MustCompile(`v(\d+\.\d+\.\d+)`) +) + // ensureServiceMeshOperatorSubscription attempts to ensure that a subscription // for servicemeshoperator is present and returns a Boolean indicating whether // it exists, the subscription if it exists, and an error value. @@ -176,7 +183,9 @@ func (r *reconciler) ensureServiceMeshOperatorInstallPlan(ctx context.Context, v } // currentInstallPlan returns the InstallPlan that describes installing the expected version of the GatewayAPI -// implementation, if one exists. +// implementation. If no InstallPlan exists for the expected version, return the InstallPlan which replaces the currently installed one. +// This InstallPlan is expected to advance the OSSM operator toward the next CSV in the upgrade graph, +// assuming that the configured version is available further up the graph. func (r *reconciler) currentInstallPlan(ctx context.Context, version string) (bool, *operatorsv1alpha1.InstallPlan, error) { _, subscription, err := r.currentSubscription(ctx, operatorcontroller.ServiceMeshOperatorSubscriptionName()) if err != nil { @@ -189,8 +198,9 @@ func (r *reconciler) currentInstallPlan(ctx context.Context, version string) (bo if installPlans == nil || len(installPlans.Items) == 0 { return false, nil, nil } - var currentInstallPlan *operatorsv1alpha1.InstallPlan + var currentInstallPlan, nextInstallPlan *operatorsv1alpha1.InstallPlan multipleInstallPlans := false + for _, installPlan := range installPlans.Items { if len(installPlan.OwnerReferences) == 0 || len(installPlan.Spec.ClusterServiceVersionNames) == 0 { continue @@ -209,6 +219,7 @@ func (r *reconciler) currentInstallPlan(ctx context.Context, version string) (bo if installPlan.Status.Phase != operatorsv1alpha1.InstallPlanPhaseRequiresApproval { continue } + // Check whether InstallPlan implements the expected operator version. for _, csvName := range installPlan.Spec.ClusterServiceVersionNames { if csvName == version { // Keep the newest InstallPlan to return at the end of the loop. @@ -223,10 +234,62 @@ func (r *reconciler) currentInstallPlan(ctx context.Context, version string) (bo } } } + // Check whether InstallPlan implements the next operator version in the upgrade graph. + for _, csvName := range installPlan.Spec.ClusterServiceVersionNames { + // The definitions of InstalledCSV and CurrentCSV are non-trivial: + // + // - InstalledCSV represents the currently running CSV. + // - CurrentCSV represents the version that "subscription is progressing to" + // which practically means "the next CSV in the upgrade graph." + // + // - If InstalledCSV < CurrentCSV: + // No CSV replacement is ongoing. InstalledCSV is the current version, + // and CurrentCSV is the next one in the upgrade graph. + // - If InstalledCSV == CurrentCSV: + // One of the following scenarios is possible: + // 1. CSV replacement is in progress "InstalledCSV-1" is being replaced with CurrentCSV. + // 2. Installation of the first CSV is in progress. + // 3. There is no "next CSV" in the upgrade graph, so CurrentCSV + // cannot point to a future version. + // CurrentCSV only becomes the next version once the replacement finishes, + // and the next InstallPlan appears around the same time. + // + // The first condition (below) prevents setting the next InstallPlan while a replacement + // or installation is ongoing, or when the end of the upgrade graph is reached. + if subscription.Status.InstalledCSV != subscription.Status.CurrentCSV && csvName == subscription.Status.CurrentCSV { + // CurrentCSV should not be greater (semver-wise) than desiredCSV to avoid upgrading past the desired version, + // in case the desired one was skipped. + // Note: since we use the "stable" channel, the upgrade graph allows transitions between minor releases. + // For example, after installing 3.0.3, we may see 3.1.0 in currentCSV. + desiredCSVSemVer, currentCSVSemVer := extractSemVerFromCSV(version), extractSemVerFromCSV(subscription.Status.CurrentCSV) + if currentCSVSemVer != nil && desiredCSVSemVer != nil && currentCSVSemVer.Compare(*desiredCSVSemVer) != 1 { + if nextInstallPlan == nil { + nextInstallPlan = &installPlan + break + } + } + } + } } if multipleInstallPlans { log.Info(fmt.Sprintf("found multiple valid InstallPlans. using %s because it's the newest", currentInstallPlan.Name)) } + // No InstallPlan with the expected operator version was found, + // but the next one in the upgrade graph exists. + // Return the next InstallPlan to continue the upgrade. + if currentInstallPlan == nil && nextInstallPlan != nil { + // The condition below prevents approving an InstallPlan + // that targets a version beyond the expected operator version. + // This can happen when: + // - InstallPlan with the expected version is complete (no approval needed). + // - Newer versions exist in the upgrade graph. + // The check ensures that the currently running CSV is different + // from the expected version. Once they match, no further action is needed. + if subscription.Status.InstalledCSV != version { + log.Info("installplan with expected operator version was not found; proceedng with an intermedite installplan", "name", nextInstallPlan.Name, "csv", subscription.Status.CurrentCSV) + currentInstallPlan = nextInstallPlan + } + } return (currentInstallPlan != nil), currentInstallPlan, nil } @@ -263,3 +326,18 @@ func installPlanChanged(current, expected *operatorsv1alpha1.InstallPlan) (bool, return true, updated } + +// extractSemVerFromCSV exctracts the semantic version from an OLM CSV name. +// It returns a semver.Version pointer, or nil if the CSV name does not contain +// a valid semantic version. +func extractSemVerFromCSV(csv string) *semver.Version { + match := csvSemVerRegexp.FindStringSubmatch(csv) + if len(match) == 0 { + return nil + } + version, err := semver.Make(match[1]) + if err != nil { + return nil + } + return &version +} diff --git a/pkg/operator/controller/gatewayclass/subscription_test.go b/pkg/operator/controller/gatewayclass/subscription_test.go index 8913dac6ec..df3073c4f2 100644 --- a/pkg/operator/controller/gatewayclass/subscription_test.go +++ b/pkg/operator/controller/gatewayclass/subscription_test.go @@ -145,6 +145,10 @@ func Test_ensureServiceMeshOperatorInstallPlan(t *testing.T) { CatalogSourceNamespace: "openshift-marketplace", StartingCSV: "servicemeshoperator.v1.0.0", }, + Status: operatorsv1alpha1.SubscriptionStatus{ + InstalledCSV: "servicemeshoperator.v1.0.0", + CurrentCSV: "servicemeshoperator.v1.0.0", + }, }, &operatorsv1alpha1.InstallPlan{ ObjectMeta: metav1.ObjectMeta{ @@ -214,6 +218,10 @@ func Test_ensureServiceMeshOperatorInstallPlan(t *testing.T) { CatalogSourceNamespace: "openshift-marketplace", StartingCSV: "servicemeshoperator.v1.0.0", }, + Status: operatorsv1alpha1.SubscriptionStatus{ + InstalledCSV: "servicemeshoperator.v1.0.0", + CurrentCSV: "servicemeshoperator.v1.0.0", + }, }, &operatorsv1alpha1.InstallPlan{ ObjectMeta: metav1.ObjectMeta{ @@ -398,6 +406,10 @@ func Test_ensureServiceMeshOperatorInstallPlan(t *testing.T) { CatalogSourceNamespace: "openshift-marketplace", StartingCSV: "servicemeshoperator.v1.0.0", }, + Status: operatorsv1alpha1.SubscriptionStatus{ + InstalledCSV: "servicemeshoperator.v1.0.0", + CurrentCSV: "servicemeshoperator.v1.0.0", + }, }, &operatorsv1alpha1.InstallPlan{ ObjectMeta: metav1.ObjectMeta{ @@ -515,6 +527,10 @@ func Test_ensureServiceMeshOperatorInstallPlan(t *testing.T) { CatalogSourceNamespace: "openshift-marketplace", StartingCSV: "servicemeshoperator.v1.0.0", }, + Status: operatorsv1alpha1.SubscriptionStatus{ + InstalledCSV: "servicemeshoperator.v1.0.0", + CurrentCSV: "servicemeshoperator.v1.0.0", + }, }, &operatorsv1alpha1.InstallPlan{ ObjectMeta: metav1.ObjectMeta{ @@ -603,6 +619,444 @@ func Test_ensureServiceMeshOperatorInstallPlan(t *testing.T) { }, expectDelete: []client.Object{}, }, + { + name: "Upgrade to next version", + channel: "stable", + version: "servicemeshoperator.v1.0.1", + existingObjects: []runtime.Object{ + &operatorsv1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: operatorcontroller.ServiceMeshOperatorSubscriptionName().Namespace, + Name: operatorcontroller.ServiceMeshOperatorSubscriptionName().Name, + UID: "foobar", + }, + Spec: &operatorsv1alpha1.SubscriptionSpec{ + Channel: "stable", + InstallPlanApproval: operatorsv1alpha1.ApprovalManual, + Package: "servicemeshoperator", + CatalogSource: "redhat-operators", + CatalogSourceNamespace: "openshift-marketplace", + StartingCSV: "servicemeshoperator.v1.0.0", + }, + Status: operatorsv1alpha1.SubscriptionStatus{ + InstalledCSV: "servicemeshoperator.v1.0.0", + CurrentCSV: "servicemeshoperator.v1.0.1", + }, + }, + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-foo", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.0", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseComplete, + }, + }, + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-bar", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.1", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: false, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseRequiresApproval, + }, + }, + }, + expectUpdate: []client.Object{ + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-bar", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.1", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseRequiresApproval, + }, + }, + }, + }, + { + name: "Upgrade through intermediate version", + channel: "stable", + version: "servicemeshoperator.v1.0.2", // N+2 from the current + existingObjects: []runtime.Object{ + &operatorsv1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: operatorcontroller.ServiceMeshOperatorSubscriptionName().Namespace, + Name: operatorcontroller.ServiceMeshOperatorSubscriptionName().Name, + UID: "foobar", + }, + Spec: &operatorsv1alpha1.SubscriptionSpec{ + Channel: "stable", + InstallPlanApproval: operatorsv1alpha1.ApprovalManual, + Package: "servicemeshoperator", + CatalogSource: "redhat-operators", + CatalogSourceNamespace: "openshift-marketplace", + StartingCSV: "servicemeshoperator.v1.0.2", + }, + Status: operatorsv1alpha1.SubscriptionStatus{ + InstalledCSV: "servicemeshoperator.v1.0.0", + CurrentCSV: "servicemeshoperator.v1.0.1", + }, + }, + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-foo", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.0", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseComplete, + }, + }, + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-bar", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.1", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: false, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseRequiresApproval, + }, + }, + }, + expectUpdate: []client.Object{ + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-bar", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.1", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseRequiresApproval, + }, + }, + }, + }, + { + name: "Upgrade to next version when previous installplan is complete", + channel: "stable", + version: "servicemeshoperator.v1.0.2", + existingObjects: []runtime.Object{ + &operatorsv1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: operatorcontroller.ServiceMeshOperatorSubscriptionName().Namespace, + Name: operatorcontroller.ServiceMeshOperatorSubscriptionName().Name, + UID: "foobar", + }, + Spec: &operatorsv1alpha1.SubscriptionSpec{ + Channel: "stable", + InstallPlanApproval: operatorsv1alpha1.ApprovalManual, + Package: "servicemeshoperator", + CatalogSource: "redhat-operators", + CatalogSourceNamespace: "openshift-marketplace", + StartingCSV: "servicemeshoperator.v1.0.2", + }, + Status: operatorsv1alpha1.SubscriptionStatus{ + InstalledCSV: "servicemeshoperator.v1.0.1", + CurrentCSV: "servicemeshoperator.v1.0.2", + }, + }, + // N-1 + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-foo", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.0", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseComplete, + }, + }, + // Current + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-bar", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.1", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseComplete, + }, + }, + // N+1 (and expected). + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-baz", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.2", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: false, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseRequiresApproval, + }, + }, + }, + expectUpdate: []client.Object{ + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-baz", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.2", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseRequiresApproval, + }, + }, + }, + }, + { + name: "Upgrade reached end", + channel: "stable", + version: "servicemeshoperator.v1.0.2", + existingObjects: []runtime.Object{ + &operatorsv1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: operatorcontroller.ServiceMeshOperatorSubscriptionName().Namespace, + Name: operatorcontroller.ServiceMeshOperatorSubscriptionName().Name, + UID: "foobar", + }, + Spec: &operatorsv1alpha1.SubscriptionSpec{ + Channel: "stable", + InstallPlanApproval: operatorsv1alpha1.ApprovalManual, + Package: "servicemeshoperator", + CatalogSource: "redhat-operators", + CatalogSourceNamespace: "openshift-marketplace", + StartingCSV: "servicemeshoperator.v1.0.2", + }, + Status: operatorsv1alpha1.SubscriptionStatus{ + InstalledCSV: "servicemeshoperator.v1.0.2", + CurrentCSV: "servicemeshoperator.v1.0.2", + }, + }, + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-foo", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.2", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseComplete, + }, + }, + // Current + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-bar", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.2", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseComplete, + }, + }, + }, + expectUpdate: []client.Object{}, + }, + { + name: "Upgrade to non-existing desired version", + channel: "stable", + version: "servicemeshoperator.v1.0.2", + existingObjects: []runtime.Object{ + &operatorsv1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: operatorcontroller.ServiceMeshOperatorSubscriptionName().Namespace, + Name: operatorcontroller.ServiceMeshOperatorSubscriptionName().Name, + UID: "foobar", + }, + Spec: &operatorsv1alpha1.SubscriptionSpec{ + Channel: "stable", + InstallPlanApproval: operatorsv1alpha1.ApprovalManual, + Package: "servicemeshoperator", + CatalogSource: "redhat-operators", + CatalogSourceNamespace: "openshift-marketplace", + StartingCSV: "servicemeshoperator.v1.0.2", + }, + Status: operatorsv1alpha1.SubscriptionStatus{ + InstalledCSV: "servicemeshoperator.v1.0.1", + CurrentCSV: "servicemeshoperator.v1.0.3", // 1.0.2 was skipped from upgrade graph + }, + }, + // Current + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-bar", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.1", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: true, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseComplete, + }, + }, + // N+2 (and expected is in between but skipped). + &operatorsv1alpha1.InstallPlan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Install-baz", + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + OwnerReferences: []metav1.OwnerReference{ + { + UID: "foobar", + }, + }, + }, + Spec: operatorsv1alpha1.InstallPlanSpec{ + ClusterServiceVersionNames: []string{ + "servicemeshoperator.v1.0.3", + }, + Approval: operatorsv1alpha1.ApprovalManual, + Approved: false, + }, + Status: operatorsv1alpha1.InstallPlanStatus{ + Phase: operatorsv1alpha1.InstallPlanPhaseRequiresApproval, + }, + }, + }, + expectUpdate: []client.Object{}, + }, } scheme := runtime.NewScheme() diff --git a/pkg/operator/controller/names.go b/pkg/operator/controller/names.go index 749bbf8a23..0ae24be5f0 100644 --- a/pkg/operator/controller/names.go +++ b/pkg/operator/controller/names.go @@ -78,6 +78,38 @@ const ( IstioRevLabelKey = "istio.io/rev" GatewayClassIndexFieldName = "gatewayclassController" + + // SubscriptionCatalogOverrideAnnotationKey is the key for an + // unsupported annotation on the gatewayclass using which a custom + // catalog source can be specified for the OSSM subscription. This + // annotation is only intended for use by OpenShift developers. Note + // that this annotation is intended to be used only when initially + // creating the gatewayclass and subscription; changing the catalog + // source on an existing subscription will likely have no effect or + // cause errors. + SubscriptionCatalogOverrideAnnotationKey = "unsupported.do-not-use.openshift.io/ossm-catalog" + // SubscriptionChannelOverrideAnnotationKey is the key for an + // unsupported annotation on the gatewayclass using which a custom + // channel can be specified for the OSSM subscription. This annotation + // is only intended for use by OpenShift developers. Note that this + // annotation is intended to be used only when initially creating the + // gatewayclass and subscription; changing the channel on an existing + // subscription will likely have no effect or cause errors. + SubscriptionChannelOverrideAnnotationKey = "unsupported.do-not-use.openshift.io/ossm-channel" + // SubscriptionVersionOverrideAnnotationKey is the key for an + // unsupported annotation on the gatewayclass using which a custom + // version of OSSM can be specified. This annotation is only intended + // for use by OpenShift developers. Note that this annotation is + // intended to be used only when initially creating the gatewayclass and + // subscription; OLM will not allow downgrades, and upgrades are + // generally restricted to the next version after the currently + // installed version. + SubscriptionVersionOverrideAnnotationKey = "unsupported.do-not-use.openshift.io/ossm-version" + // istioVersionOverrideAnnotationKey is the key for an unsupported + // annotation on the gatewayclass using which a custom version of Istio + // can be specified. This annotation is only intended for use by + // OpenShift developers. + IstioVersionOverrideAnnotationKey = "unsupported.do-not-use.openshift.io/istio-version" ) // IngressClusterOperatorName returns the namespaced name of the ClusterOperator diff --git a/pkg/operator/controller/status/controller.go b/pkg/operator/controller/status/controller.go index fc1d94e3b9..bebf16d7be 100644 --- a/pkg/operator/controller/status/controller.go +++ b/pkg/operator/controller/status/controller.go @@ -119,6 +119,9 @@ func New(mgr manager.Manager, config Config) (controller.Controller, error) { isIngressClusterOperator := func(o client.Object) bool { return o.GetName() == operatorcontroller.IngressClusterOperatorName().Name } + isOpenshiftOperatorNamespace := func(o client.Object) bool { + return o.GetNamespace() == operatorcontroller.OpenshiftOperatorNamespace + } toDefaultIngressController := func(ctx context.Context, _ client.Object) []reconcile.Request { return []reconcile.Request{{ NamespacedName: types.NamespacedName{ @@ -148,13 +151,13 @@ func New(mgr manager.Manager, config Config) (controller.Controller, error) { return e.Object.GetNamespace() == operatorcontroller.OpenshiftOperatorNamespace }, UpdateFunc: func(e event.UpdateEvent) bool { - return false + return e.ObjectNew.GetNamespace() == operatorcontroller.OpenshiftOperatorNamespace }, DeleteFunc: func(e event.DeleteEvent) bool { return e.Object.GetNamespace() == operatorcontroller.OpenshiftOperatorNamespace }, GenericFunc: func(e event.GenericEvent) bool { - return false + return e.Object.GetNamespace() == operatorcontroller.OpenshiftOperatorNamespace }, })); err != nil { return nil, err @@ -186,6 +189,10 @@ func New(mgr manager.Manager, config Config) (controller.Controller, error) { if err := c.Watch(source.Kind[client.Object](reconciler.subscriptionCache, &operatorsv1alpha1.Subscription{}, handler.EnqueueRequestsFromMapFunc(toDefaultIngressController), isOSSMSubscription)); err != nil { return nil, err } + + if err := c.Watch(source.Kind[client.Object](operatorCache, &operatorsv1alpha1.ClusterServiceVersion{}, handler.EnqueueRequestsFromMapFunc(toDefaultIngressController), predicate.NewPredicateFuncs(isOpenshiftOperatorNamespace))); err != nil { + return nil, err + } } return c, nil @@ -327,13 +334,11 @@ func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( co.Status.Conditions = mergeConditions(co.Status.Conditions, computeOperatorAvailableCondition(state.IngressControllers), computeOperatorProgressingCondition( - state.IngressControllers, + state, + r.config, allIngressesAvailable, oldStatus.Versions, co.Status.Versions, - r.config.OperatorReleaseVersion, - r.config.IngressControllerImage, - r.config.CanaryImage, ), computeOperatorDegradedCondition(state), computeOperatorUpgradeableCondition(state.IngressControllers), @@ -406,6 +411,14 @@ type operatorState struct { // shouldInstallOSSM reflects whether the ingress operator should install OSSM. Currently, this happens when a // gateway class with Spec.ControllerName=operatorcontroller.OpenShiftGatewayClassControllerName is created. shouldInstallOSSM bool + // installedGatewayAPIOperatorVersion contains the currently installed + // version of the OSSM operator. + installedGatewayAPIOperatorVersion string + // currentGatewayAPIOperatorVersion contains the OSSM operator version + // which is currently being installed or the next in the upgrade graph. + currentGatewayAPIOperatorVersion string + // installedGatewayAPIOperatorVersionPhase contains the phase of the installed OSSM operator version. + installedGatewayAPIOperatorVersionPhase operatorsv1alpha1.ClusterServiceVersionPhase } // getOperatorState gets and returns the resources necessary to compute the @@ -456,7 +469,6 @@ func (r *reconciler) getOperatorState(ctx context.Context, ingressNamespace, can } } else { state.haveOSSMSubscription = true - } var ( @@ -508,6 +520,31 @@ func (r *reconciler) getOperatorState(ctx context.Context, ingressNamespace, can // If one or more gateway classes have ControllerName=operatorcontroller.OpenShiftGatewayClassControllerName, // the ingress operator should try to install OSSM. state.shouldInstallOSSM = (len(gatewayClassList.Items) > 0) + // Apply the OSSM override if present. + // We assume that all GatewayClasses managed by the OpenShift controller + // share the same override. + if state.shouldInstallOSSM { + if v, ok := gatewayClassList.Items[0].Annotations[operatorcontroller.SubscriptionVersionOverrideAnnotationKey]; ok { + state.expectedGatewayAPIOperatorVersion = v + } + state.installedGatewayAPIOperatorVersion = subscription.Status.InstalledCSV + state.currentGatewayAPIOperatorVersion = subscription.Status.CurrentCSV + // To compute the OSSM operator's progressing status, + // we need to inspect the CSV level to determine whether + // the installedCSV of the subscription succeeded. + var installedCSV operatorsv1alpha1.ClusterServiceVersion + installedCSVName := types.NamespacedName{ + Name: subscription.Status.InstalledCSV, + Namespace: operatorcontroller.OpenshiftOperatorNamespace, + } + if err := r.cache.Get(ctx, installedCSVName, &installedCSV); err != nil { + if !errors.IsNotFound(err) { + return state, fmt.Errorf("failed to get installed CSV %q: %v", installedCSVName.Name, err) + } + } else { + state.installedGatewayAPIOperatorVersionPhase = installedCSV.Status.Phase + } + } } } @@ -566,6 +603,7 @@ func computeOperatorDegradedCondition(state operatorState) configv1.ClusterOpera computeIngressControllerDegradedCondition, computeGatewayAPICRDsDegradedCondition, computeGatewayAPIInstallDegradedCondition, + computeOSSMOperatorDegradedCondition, } { degradedCondition = joinConditions(degradedCondition, fn(state)) } @@ -690,6 +728,21 @@ func computeGatewayAPIInstallDegradedCondition(state operatorState) configv1.Clu return degradedCondition } +// computeOSSMOperatorDegradedCondition computes the degraded condition for OSSM operator. +func computeOSSMOperatorDegradedCondition(state operatorState) configv1.ClusterOperatorStatusCondition { + degradedCondition := configv1.ClusterOperatorStatusCondition{} + + if state.shouldInstallOSSM { + if state.installedGatewayAPIOperatorVersionPhase == operatorsv1alpha1.CSVPhaseFailed { + degradedCondition.Status = configv1.ConditionTrue + degradedCondition.Reason = "GatewayAPIOperatorDegraded" + degradedCondition.Message = fmt.Sprintf("GatewayAPI operator failed to install version %q", state.installedGatewayAPIOperatorVersion) + } + } + + return degradedCondition +} + // computeOperatorUpgradeableCondition computes the operator's Upgradeable // status condition. func computeOperatorUpgradeableCondition(ingresses []operatorv1.IngressController) configv1.ClusterOperatorStatusCondition { @@ -768,7 +821,19 @@ func computeOperatorEvaluationConditionsDetectedCondition(ingresses []operatorv1 } // computeOperatorProgressingCondition computes the operator's current Progressing status state. -func computeOperatorProgressingCondition(ingresscontrollers []operatorv1.IngressController, allIngressesAvailable bool, oldVersions, curVersions []configv1.OperandVersion, operatorReleaseVersion, ingressControllerImage string, canaryImage string) configv1.ClusterOperatorStatusCondition { +func computeOperatorProgressingCondition(state operatorState, config Config, allIngressesAvailable bool, oldVersions, curVersions []configv1.OperandVersion) configv1.ClusterOperatorStatusCondition { + progressingCondition := configv1.ClusterOperatorStatusCondition{ + Type: configv1.OperatorProgressing, + } + + icCondition := computeIngressControllersProgressingCondition(state.IngressControllers, allIngressesAvailable, oldVersions, curVersions, config.OperatorReleaseVersion, config.IngressControllerImage, config.CanaryImage) + ossmCondition := computeOSSMOperatorProgressingCondition(state) + + return joinConditions(joinConditions(progressingCondition, icCondition), ossmCondition) +} + +// computeIngressControllersProgressingCondition computes the IngressControllers' current Progressing status state. +func computeIngressControllersProgressingCondition(ingresscontrollers []operatorv1.IngressController, allIngressesAvailable bool, oldVersions, curVersions []configv1.OperandVersion, operatorReleaseVersion, ingressControllerImage string, canaryImage string) configv1.ClusterOperatorStatusCondition { progressingCondition := configv1.ClusterOperatorStatusCondition{ Type: configv1.OperatorProgressing, } @@ -835,6 +900,57 @@ func computeOperatorProgressingCondition(ingresscontrollers []operatorv1.Ingress return progressingCondition } +// computeOSSMOperatorProgressingCondition computes the OSSM operator's current Progressing status state. +func computeOSSMOperatorProgressingCondition(state operatorState) configv1.ClusterOperatorStatusCondition { + progressingCondition := configv1.ClusterOperatorStatusCondition{} + + // Skip updating the progressing status if OpenShift managed GatewayClass doesn't exist. + if !state.shouldInstallOSSM { + return progressingCondition + } + + progressing, failed := false, false + + // Set the progressing to true if the desired version is different + // from the currently installed. + if state.expectedGatewayAPIOperatorVersion != state.installedGatewayAPIOperatorVersion { + // Note that the progressing state remains "true" if the desired version is + // beyond the latest available in the upgrade graph. + // There is no reliable way of knowing that the end of the upgrade graph + // is reached and no next version is available. None of OLM resources + // can provide such information. + progressing = true + } else { + // The desired version was reached. However the CSV may still be + // in "Replacing" or "Installing" state, stop setting progressing + // when the installed CSV succeeded. + if state.installedGatewayAPIOperatorVersionPhase != operatorsv1alpha1.CSVPhaseSucceeded { + progressing = true + } + } + + // Set Progressing=False if the installation failed. + if state.installedGatewayAPIOperatorVersionPhase == operatorsv1alpha1.CSVPhaseFailed { + failed = true + progressing = false + } + + if progressing { + progressingCondition.Status = configv1.ConditionTrue + progressingCondition.Reason = "GatewayAPIOperatorUpgrading" + progressingCondition.Message = fmt.Sprintf("GatewayAPI operator is upgrading to version %q", state.expectedGatewayAPIOperatorVersion) + if len(state.installedGatewayAPIOperatorVersion) > 0 && len(state.currentGatewayAPIOperatorVersion) > 0 { + progressingCondition.Message += fmt.Sprintf(" (installed(-ing):%q,next:%q)", state.installedGatewayAPIOperatorVersion, state.currentGatewayAPIOperatorVersion) + } + } else if !failed { + progressingCondition.Status = configv1.ConditionFalse + progressingCondition.Reason = "GatewayAPIOperatorUpToDate" + progressingCondition.Message = fmt.Sprintf("GatewayAPI operator is running version %q", state.expectedGatewayAPIOperatorVersion) + } + + return progressingCondition +} + // computeOperatorAvailableCondition computes the operator's current Available status state. func computeOperatorAvailableCondition(ingresses []operatorv1.IngressController) configv1.ClusterOperatorStatusCondition { availableCondition := configv1.ClusterOperatorStatusCondition{ @@ -945,7 +1061,7 @@ func operatorStatusesEqual(a, b configv1.ClusterOperatorStatus) bool { // joinConditions merges two cluster operator conditions into one. // If both conditions have the same status: -// - Reason becomes: "MultipleComponents[Not]" + condType. +// - Reasons are concatenated using "And" as a delimiter. // - Messages are concatenated. // If the new condition has a higher-priority status, it overrides the current one. // Status priority (highest to lowest): True, Unknown, False, empty. diff --git a/pkg/operator/controller/status/controller_test.go b/pkg/operator/controller/status/controller_test.go index 9cf01f900c..29fa1a838c 100644 --- a/pkg/operator/controller/status/controller_test.go +++ b/pkg/operator/controller/status/controller_test.go @@ -21,29 +21,38 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { } testCases := []struct { - description string - noNamespace bool - allIngressesAvailable bool - someIngressProgressing bool - reportedVersions versions - oldVersions versions - curVersions versions - expectProgressing configv1.ConditionStatus + description string + noNamespace bool + allIngressesAvailable bool + someIngressProgressing bool + reportedVersions versions + oldVersions versions + curVersions versions + shouldInstallOSSM bool + desiredOSSMVersion string + installedOSSMVersion string + currentOSSMVersion string + installedOSSMVersionPhase operatorsv1alpha1.ClusterServiceVersionPhase + expectProgressing configv1.ConditionStatus + expectReason string }{ { description: "all ingress controllers are available", allIngressesAvailable: true, expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpected", }, { description: "some ingress controller is progressing", allIngressesAvailable: true, someIngressProgressing: true, expectProgressing: configv1.ConditionTrue, + expectReason: "Reconciling", }, { description: "all ingress controllers are not available", expectProgressing: configv1.ConditionTrue, + expectReason: "Reconciling", }, { description: "versions match", @@ -52,6 +61,7 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { oldVersions: versions{"v1", "ic-v1", "c-v1"}, curVersions: versions{"v1", "ic-v1", "c-v1"}, expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpected", }, { description: "operator upgrade in progress", @@ -60,6 +70,7 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { oldVersions: versions{"v1", "ic-v1", "c-v1"}, curVersions: versions{"v2", "ic-v1", "c-v1"}, expectProgressing: configv1.ConditionTrue, + expectReason: "Reconciling", }, { description: "operand upgrade in progress", @@ -68,6 +79,7 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { oldVersions: versions{"v1", "ic-v1", "c-v1"}, curVersions: versions{"v1", "ic-v2", "c-v2"}, expectProgressing: configv1.ConditionTrue, + expectReason: "Reconciling", }, { description: "operator and operand upgrade in progress", @@ -76,6 +88,7 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { oldVersions: versions{"v1", "ic-v1", "c-v1"}, curVersions: versions{"v2", "ic-v2", "c-v2"}, expectProgressing: configv1.ConditionTrue, + expectReason: "Reconciling", }, { description: "operator upgrade done", @@ -84,6 +97,7 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { oldVersions: versions{"v1", "ic-v1", "c-v1"}, curVersions: versions{"v2", "ic-v1", "c-v1"}, expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpected", }, { description: "operand upgrade done", @@ -92,6 +106,7 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { oldVersions: versions{"v1", "ic-v1", "c-v1"}, curVersions: versions{"v1", "ic-v2", "c-v2"}, expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpected", }, { description: "operator and operand upgrade done", @@ -100,6 +115,7 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { oldVersions: versions{"v1", "ic-v1", "c-v1"}, curVersions: versions{"v2", "ic-v2", "c-v2"}, expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpected", }, { description: "operator upgrade in progress, operand upgrade done", @@ -108,6 +124,140 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { oldVersions: versions{"v1", "ic-v1", "c-v1"}, curVersions: versions{"v2", "ic-v2", "c-v2"}, expectProgressing: configv1.ConditionTrue, + expectReason: "Reconciling", + }, + { + description: "OSSM operator not requested", + allIngressesAvailable: true, + shouldInstallOSSM: false, + desiredOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersion: "servicemeshoperator3.v3.0.1", + currentOSSMVersion: "servicemeshoperator3.v3.0.2", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseSucceeded, + expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpected", + }, + { + description: "OSSM operator is installing", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.0", + installedOSSMVersion: "servicemeshoperator3.v3.0.0", + currentOSSMVersion: "servicemeshoperator3.v3.0.0", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseInstalling, + expectProgressing: configv1.ConditionTrue, + expectReason: "GatewayAPIOperatorUpgrading", + }, + { + description: "OSSM operator installation succeeded", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.0", + installedOSSMVersion: "servicemeshoperator3.v3.0.0", + currentOSSMVersion: "servicemeshoperator3.v3.0.0", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseSucceeded, + expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpectedAndGatewayAPIOperatorUpToDate", + }, + { + description: "OSSM operator installation failed", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.0", + installedOSSMVersion: "servicemeshoperator3.v3.0.0", + currentOSSMVersion: "servicemeshoperator3.v3.0.0", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseFailed, + expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpected", + }, + { + description: "OSSM operator got next version", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.0", + installedOSSMVersion: "servicemeshoperator3.v3.0.0", + currentOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseSucceeded, + expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpectedAndGatewayAPIOperatorUpToDate", + }, + { + description: "OSSM operator desired version changed", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersion: "servicemeshoperator3.v3.0.0", + currentOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseSucceeded, + expectProgressing: configv1.ConditionTrue, + expectReason: "GatewayAPIOperatorUpgrading", + }, + { + description: "OSSM operator desired version is rolling out", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersion: "servicemeshoperator3.v3.0.1", + currentOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseInstalling, + expectProgressing: configv1.ConditionTrue, + expectReason: "GatewayAPIOperatorUpgrading", + }, + { + description: "OSSM operator desired version succeeded", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersion: "servicemeshoperator3.v3.0.1", + currentOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseSucceeded, + expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpectedAndGatewayAPIOperatorUpToDate", + }, + { + description: "OSSM operator desired version is beyong graph", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.5", + installedOSSMVersion: "servicemeshoperator3.v3.0.3", + currentOSSMVersion: "servicemeshoperator3.v3.0.3", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseSucceeded, + expectProgressing: configv1.ConditionTrue, + expectReason: "GatewayAPIOperatorUpgrading", + }, + { + description: "OSSM operator desired version is back", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.3", + installedOSSMVersion: "servicemeshoperator3.v3.0.3", + currentOSSMVersion: "servicemeshoperator3.v3.0.3", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseSucceeded, + expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpectedAndGatewayAPIOperatorUpToDate", + }, + { + description: "some ingress controller is progressing and OSSM operator is upgrading", + allIngressesAvailable: true, + someIngressProgressing: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersion: "servicemeshoperator3.v3.0.1", + currentOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseInstalling, + expectProgressing: configv1.ConditionTrue, + expectReason: "ReconcilingAndGatewayAPIOperatorUpgrading", + }, + { + description: "OSSM operator upgrading failed", + allIngressesAvailable: true, + shouldInstallOSSM: true, + desiredOSSMVersion: "servicemeshoperator3.v3.0.2", + installedOSSMVersion: "servicemeshoperator3.v3.0.1", + currentOSSMVersion: "servicemeshoperator3.v3.0.1", + installedOSSMVersionPhase: operatorsv1alpha1.CSVPhaseFailed, + expectProgressing: configv1.ConditionFalse, + expectReason: "AsExpected", }, } @@ -145,6 +295,7 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { expected := configv1.ClusterOperatorStatusCondition{ Type: configv1.OperatorProgressing, Status: tc.expectProgressing, + Reason: tc.expectReason, } var ingresscontrollers []operatorv1.IngressController @@ -160,10 +311,23 @@ func Test_computeOperatorProgressingCondition(t *testing.T) { if tc.someIngressProgressing { ingresscontrollers[0].Status.Conditions[0].Status = operatorv1.ConditionTrue } + state := operatorState{ + IngressControllers: ingresscontrollers, + shouldInstallOSSM: tc.shouldInstallOSSM, + expectedGatewayAPIOperatorVersion: tc.desiredOSSMVersion, + installedGatewayAPIOperatorVersion: tc.installedOSSMVersion, + currentGatewayAPIOperatorVersion: tc.currentOSSMVersion, + installedGatewayAPIOperatorVersionPhase: tc.installedOSSMVersionPhase, + } + config := Config{ + OperatorReleaseVersion: tc.curVersions.operator, + IngressControllerImage: tc.curVersions.operand1, + CanaryImage: tc.curVersions.operand2, + } - actual := computeOperatorProgressingCondition(ingresscontrollers, tc.allIngressesAvailable, oldVersions, reportedVersions, tc.curVersions.operator, tc.curVersions.operand1, tc.curVersions.operand2) + actual := computeOperatorProgressingCondition(state, config, tc.allIngressesAvailable, oldVersions, reportedVersions) conditionsCmpOpts := []cmp.Option{ - cmpopts.IgnoreFields(configv1.ClusterOperatorStatusCondition{}, "LastTransitionTime", "Reason", "Message"), + cmpopts.IgnoreFields(configv1.ClusterOperatorStatusCondition{}, "LastTransitionTime", "Message"), } if !cmp.Equal(actual, expected, conditionsCmpOpts...) { t.Fatalf("expected %#v, got %#v", expected, actual) @@ -897,6 +1061,60 @@ func Test_computeOperatorDegradedCondition(t *testing.T) { Message: "The \"default\" ingress controller reports Degraded=False.\nfailed to compare installed OSSM version to expected: \"servicemeshoperator3.v3.5-beta\" does not match expected format", }, }, + { + description: "OSSM operator degraded", + state: operatorState{ + IngressControllers: []operatorv1.IngressController{ + icWithStatus("default", false), + icWithStatus("test2", true), + }, + shouldInstallOSSM: true, + installedGatewayAPIOperatorVersion: "servicemeshoperator3.v3.0.1", + installedGatewayAPIOperatorVersionPhase: operatorsv1alpha1.CSVPhaseFailed, + }, + expectCondition: configv1.ClusterOperatorStatusCondition{ + Type: configv1.OperatorDegraded, + Status: configv1.ConditionTrue, + Reason: "GatewayAPIOperatorDegraded", + Message: `GatewayAPI operator failed to install version "servicemeshoperator3.v3.0.1"`, + }, + }, + { + description: "default ingresscontroller not degraded but unmanaged gateway api crds exist and OSSM operator degraded", + state: operatorState{ + IngressControllers: []operatorv1.IngressController{ + icWithStatus("default", false), + }, + unmanagedGatewayAPICRDNames: "listenersets.gateway.networking.x-k8s.io", + shouldInstallOSSM: true, + installedGatewayAPIOperatorVersion: "servicemeshoperator3.v3.0.1", + installedGatewayAPIOperatorVersionPhase: operatorsv1alpha1.CSVPhaseFailed, + }, + expectCondition: configv1.ClusterOperatorStatusCondition{ + Type: configv1.OperatorDegraded, + Status: configv1.ConditionTrue, + Reason: "GatewayAPICRDsDegradedAndGatewayAPIOperatorDegraded", + Message: "Unmanaged Gateway API CRDs found: listenersets.gateway.networking.x-k8s.io.\nGatewayAPI operator failed to install version \"servicemeshoperator3.v3.0.1\"", + }, + }, + { + description: "OSSM operator not degraded", + state: operatorState{ + IngressControllers: []operatorv1.IngressController{ + icWithStatus("default", false), + icWithStatus("test2", true), + }, + shouldInstallOSSM: true, + installedGatewayAPIOperatorVersion: "servicemeshoperator3.v3.0.1", + installedGatewayAPIOperatorVersionPhase: operatorsv1alpha1.CSVPhaseSucceeded, + }, + expectCondition: configv1.ClusterOperatorStatusCondition{ + Type: configv1.OperatorDegraded, + Status: configv1.ConditionFalse, + Reason: "IngressNotDegraded", + Message: `The "default" ingress controller reports Degraded=False.`, + }, + }, } for _, tc := range testCases { diff --git a/test/e2e/all_test.go b/test/e2e/all_test.go index 2d499afd3b..43f160d66f 100644 --- a/test/e2e/all_test.go +++ b/test/e2e/all_test.go @@ -136,5 +136,6 @@ func TestAll(t *testing.T) { // Serializing the test ensures it runs in isolation with other tests, // preventing any impact of the mutating webhook on pod creation in the cluster t.Run("TestGatewayAPI", TestGatewayAPI) + t.Run("TestOSSMOperatorUpgradeViaIntermediateVersions", TestOSSMOperatorUpgradeViaIntermediateVersions) }) } diff --git a/test/e2e/gateway_api_test.go b/test/e2e/gateway_api_test.go index d88ea03dbe..14d28056a2 100644 --- a/test/e2e/gateway_api_test.go +++ b/test/e2e/gateway_api_test.go @@ -83,14 +83,16 @@ func TestGatewayAPI(t *testing.T) { // Defer the cleanup of the test gateway. t.Cleanup(func() { - testGateway := gatewayapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: testGatewayName, Namespace: operatorcontroller.DefaultOperandNamespace}} - if err := kclient.Delete(context.TODO(), &testGateway); err != nil { - if errors.IsNotFound(err) { - return - } - t.Errorf("failed to delete gateway %q: %v", testGateway.Name, err) + t.Logf("Cleaning Gateway, GatewayClass and OSSM operator...") + if err := cleanupGateway(t, "openshift-ingress", "test-gateway", "openshift-default"); err != nil { + t.Errorf("Failed to cleanup gateway: %v", err) + } + if err := cleanupGatewayClass(t, "openshift-default"); err != nil { + t.Errorf("Failed to cleanup gatewayclass: %v", err) + } + if err := cleanupOLMOperator(t, "openshift-operators", "servicemeshoperator3.openshift-operators"); err != nil { + t.Errorf("Failed to cleanup OSSM operator: %v", err) } - // TODO: Uninstall OSSM after test is completed. }) t.Run("testGatewayAPIResources", testGatewayAPIResources) @@ -547,10 +549,10 @@ func testGatewayAPIDNS(t *testing.T) { // Create gateways for _, gateway := range tc.createGateways { createdGateway, err := createGatewayWithListeners(t, gatewayClass, gateway.gatewayName, gateway.namespace, gateway.listeners) - gateways = append(gateways, createdGateway) if err != nil { t.Fatalf("failed to create gateway %s: %v", gateway.gatewayName, err) } + gateways = append(gateways, createdGateway) } t.Cleanup(func() { diff --git a/test/e2e/gateway_api_upgrade_test.go b/test/e2e/gateway_api_upgrade_test.go new file mode 100644 index 0000000000..ae3bcd6816 --- /dev/null +++ b/test/e2e/gateway_api_upgrade_test.go @@ -0,0 +1,170 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "context" + "testing" + "time" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/api/features" +) + +// TestOSSMOperatorUpgradeViaIntermediateVersions verifies that the OSSM operator +// correctly upgrades through intermediate versions. It ensures the upgrade logic automatically approves +// each InstallPlan along the upgrade graph until the desired version is reached. +func TestOSSMOperatorUpgradeViaIntermediateVersions(t *testing.T) { + gatewayAPIEnabled, err := isFeatureGateEnabled(features.FeatureGateGatewayAPI) + if err != nil { + t.Fatalf("Error checking feature gate enabled status: %v", err) + } + + gatewayAPIControllerEnabled, err := isFeatureGateEnabled(features.FeatureGateGatewayAPIController) + if err != nil { + t.Fatalf("Error checking controller feature gate enabled status: %v", err) + } + + if !gatewayAPIEnabled || !gatewayAPIControllerEnabled { + t.Skip("Gateway API featuregates are not enabled, skipping TestOSSMOperatorUpgradeViaIntermediateVersions") + } + + t.Cleanup(func() { + t.Logf("Cleaning GatewayClass and OSSM operator...") + if err := cleanupGatewayClass(t, "openshift-default"); err != nil { + t.Errorf("Failed to cleanup gatewayclass: %v", err) + } + if err := cleanupOLMOperator(t, "openshift-operators", "servicemeshoperator3.openshift-operators"); err != nil { + t.Errorf("Failed to cleanup OSSM operator: %v", err) + } + }) + + var ( + // CatalogSource image saved before the release of servicemeshoperator 3.1.0. + // This image is useful because it includes intermediate versions in the + // servicemeshoperator3 upgrade graph. After 3.1.0 was released, all + // upgrades from 3.0.z were changed to jump directly to 3.1.0, skipping + // intermediate versions. + customCatalogSourceImage = "registry.redhat.io/redhat/redhat-operator-index@sha256:069050dbf2970f0762b816a054342e551802c45fb77417c216aed70cec5e843c" + customCatalogSourceName = "custom-redhat-operators-4-19" + initialOSSMVersion = "servicemeshoperator3.v3.0.0" + initialIstioVersion = "v1.24.3" + upgradeOSSMVersion = "servicemeshoperator3.v3.0.3" + upgradeIstioVersion = "v1.24.6" + ) + + // Create custom catalog source with expected upgrade graph for servicemeshoperator3. + t.Log("Creating custom CatalogSource...") + if err := createCatalogSource(t, "openshift-marketplace", customCatalogSourceName, customCatalogSourceImage); err != nil { + t.Fatalf("Failed to create CatalogSource %q: %v", customCatalogSourceName, err) + } + t.Log("Checking for the CatalogSource...") + if err := assertCatalogSourceWithConfig(t, "openshift-marketplace", customCatalogSourceName, 2*time.Second, 2*time.Minute); err != nil { + t.Fatalf("Failed to find expected CatalogSource %q: %v", customCatalogSourceName, err) + } + + // Installation. + t.Logf("Creating GatewayClass with OSSMversion %q and Istio version %q...", initialOSSMVersion, initialIstioVersion) + gatewayClass := buildGatewayClass("openshift-default", "openshift.io/gateway-controller/v1") + gatewayClass.Annotations = map[string]string{ + "unsupported.do-not-use.openshift.io/ossm-catalog": customCatalogSourceName, + "unsupported.do-not-use.openshift.io/ossm-version": initialOSSMVersion, + "unsupported.do-not-use.openshift.io/istio-version": initialIstioVersion, + } + if err := kclient.Create(context.TODO(), gatewayClass); err != nil { + t.Fatalf("Failed to create gatewayclass %s: %v", gatewayClass.Name, err) + } + t.Log("Checking for the Subscription...") + if err := assertSubscription(t, openshiftOperatorsNamespace, expectedSubscriptionName); err != nil { + t.Fatalf("Failed to find expected Subscription %s: %v", expectedSubscriptionName, err) + } + t.Log("Checking for the InstallPlan...") + if err := assertInstallPlan(t, openshiftOperatorsNamespace, initialOSSMVersion); err != nil { + t.Fatalf("Failed to find expected InstallPlan %s: %v", initialOSSMVersion, err) + } + t.Log("Checking for the OSSM operator deployment...") + if err := assertOSSMOperatorWithConfig(t, initialOSSMVersion, 2*time.Second, 2*time.Minute); err != nil { + logClusterServiceVersion(t, openshiftOperatorsNamespace, initialOSSMVersion) + t.Fatalf("Failed to find expected Istio operator: %v", err) + } + t.Log("Checking for the Istio CR...") + if err := assertIstioWithConfig(t, initialIstioVersion); err != nil { + t.Fatalf("Failed to find expected Istio: %v", err) + } + t.Log("Checking for the GatewayClass readiness...") + if _, err := assertGatewayClassSuccessful(t, "openshift-default"); err != nil { + t.Fatalf("Failed to find successful GatewayClass: %v", err) + } + + // Upgrade. + t.Logf("Upgrading GatewayClass to version %q and Istio version %q...", upgradeOSSMVersion, upgradeIstioVersion) + if err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 30*time.Second, false, func(context context.Context) (bool, error) { + gc := &gatewayapiv1.GatewayClass{} + if err := kclient.Get(context, types.NamespacedName{Name: gatewayClass.Name}, gc); err != nil { + t.Logf("Failed to get GatewayClass %q: %v, retrying...", gatewayClass.Name, err) + return false, nil + } + gc.Annotations = map[string]string{ + "unsupported.do-not-use.openshift.io/ossm-catalog": customCatalogSourceName, + "unsupported.do-not-use.openshift.io/ossm-version": upgradeOSSMVersion, + "unsupported.do-not-use.openshift.io/istio-version": upgradeIstioVersion, + } + if err := kclient.Update(context, gc); err != nil { + t.Logf("Failed to update GatewayClass %q: %v, retrying...", gc.Name, err) + return false, nil + } + t.Logf("GatewayClass %q has been updated to version %q", gc.Name, upgradeOSSMVersion) + return true, nil + }); err != nil { + t.Fatalf("Failed to update GatewayClass to next version: %v", err) + } + t.Log("Checking for the status...") + progressingNotDegraded := []configv1.ClusterOperatorStatusCondition{ + { + Type: configv1.OperatorProgressing, + Status: configv1.ConditionTrue, + Reason: "GatewayAPIOperatorUpgrading", + }, + { + Type: configv1.OperatorDegraded, + Status: configv1.ConditionFalse, + Reason: "IngressNotDegraded", + }, + } + if err := waitForClusterOperatorConditions(t, kclient, progressingNotDegraded...); err != nil { + t.Fatalf("Operator should be Progressing=True and Degraded=False while upgrading: %v", err) + } + t.Log("Checking for the OSSM operator deployment...") + if err := assertOSSMOperatorWithConfig(t, upgradeOSSMVersion, 2*time.Second, 4*time.Minute); err != nil { + t.Fatalf("failed to find expected Istio operator: %v", err) + } + t.Log("Re-checking for the status...") + notProgressingNotDegraded := []configv1.ClusterOperatorStatusCondition{ + { + Type: configv1.OperatorProgressing, + Status: configv1.ConditionFalse, + Reason: "AsExpectedAndGatewayAPIOperatorUpToDate", + }, + { + Type: configv1.OperatorDegraded, + Status: configv1.ConditionFalse, + Reason: "IngressNotDegraded", + }, + } + if err := waitForClusterOperatorConditions(t, kclient, notProgressingNotDegraded...); err != nil { + t.Fatalf("Operator should be Progressing=False and Degraded=False once upgrade reached desired version: %v", err) + } + t.Log("Checking for the GatewayClass readiness...") + if _, err := assertGatewayClassSuccessful(t, "openshift-default"); err != nil { + t.Fatalf("Failed to find successful GatewayClass: %v", err) + } + t.Log("Checking for the Istio CR...") + if err := assertIstioWithConfig(t, upgradeIstioVersion); err != nil { + t.Fatalf("Failed to find expected Istio: %v", err) + } +} diff --git a/test/e2e/operator_test.go b/test/e2e/operator_test.go index 3ac4c01c96..4b245ae686 100644 --- a/test/e2e/operator_test.go +++ b/test/e2e/operator_test.go @@ -31,6 +31,7 @@ import ( operatorv1 "github.com/openshift/api/operator/v1" iov1 "github.com/openshift/api/operatoringress/v1" routev1 "github.com/openshift/api/route/v1" + olmv1 "github.com/operator-framework/api/pkg/operators/v1" configclientset "github.com/openshift/client-go/config/clientset/versioned" "github.com/openshift/cluster-ingress-operator/pkg/manifests" @@ -181,6 +182,12 @@ func TestMain(m *testing.M) { } kclient = kubeClient + // GatewayAPI tests need to clean up the OSSM operator. + if olmv1.AddToScheme(operatorclient.GetScheme()); err != nil { + fmt.Printf("failed to install olmv1 scheme: %s\n", err) + os.Exit(1) + } + configClient, err = configclientset.NewForConfig(kubeConfig) if err != nil { fmt.Printf("failed to create config client: %s\n", err) diff --git a/test/e2e/util_gatewayapi_test.go b/test/e2e/util_gatewayapi_test.go index 355f12cc70..d1ae6f3eaa 100644 --- a/test/e2e/util_gatewayapi_test.go +++ b/test/e2e/util_gatewayapi_test.go @@ -20,6 +20,7 @@ import ( v1 "github.com/openshift/api/operatoringress/v1" operatorcontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller" util "github.com/openshift/cluster-ingress-operator/pkg/util" + olmv1 "github.com/operator-framework/api/pkg/operators/v1" operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/google/go-cmp/cmp" @@ -30,6 +31,7 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" kerrors "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/types" "k8s.io/apimachinery/pkg/util/wait" @@ -180,6 +182,106 @@ func deleteExistingVAP(t *testing.T, vapName string) error { return nil } +// cleanupGateway deletes the given gateway and waits for the corresponding Istio proxy deployment to disappear. +func cleanupGateway(t *testing.T, namespace, name, gcname string) error { + t.Helper() + + gw := &gatewayapiv1.Gateway{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}} + if err := kclient.Delete(context.Background(), gw); err != nil && !kerrors.IsNotFound(err) { + return fmt.Errorf(`failed to delete gateway "%s/%s": %w`, namespace, name, err) + } + t.Logf(`Deleted gateway "%s/%s"`, namespace, name) + + depl := &appsv1.Deployment{} + istioName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: name + "-" + gcname} + if err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 1*time.Minute, false, func(context context.Context) (bool, error) { + if err := kclient.Get(context, istioName, depl); err != nil { + if !kerrors.IsNotFound(err) { + t.Logf("Failed to get deployment %q, retrying...", istioName) + return false, nil + } + } else { + t.Logf("Deployment %q still exists, retrying...", istioName) + return false, nil + } + return true, nil + }); err != nil { + return fmt.Errorf("timed out waiting for deployment %v to disappear", istioName) + } + t.Logf("Deleted deployment %q", istioName) + + return nil +} + +// cleanupGatewayClass deletes the given gatewayclass and waits for the corresponding Istiod deployment to disappear. +func cleanupGatewayClass(t *testing.T, name string) error { + t.Helper() + + gc := &gatewayapiv1.GatewayClass{ObjectMeta: metav1.ObjectMeta{Name: name}} + if err := kclient.Delete(context.Background(), gc); err != nil && !kerrors.IsNotFound(err) { + return fmt.Errorf("failed to delete gatewayclass %q: %w", name, err) + } + t.Logf("Deleted gatewayclass %q", name) + + depl := &appsv1.Deployment{} + istiodName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: "istiod-" + name} + if err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 1*time.Minute, false, func(context context.Context) (bool, error) { + if err := kclient.Get(context, istiodName, depl); err != nil { + if !kerrors.IsNotFound(err) { + t.Logf("Failed to get deployment %q, retrying...", istiodName) + return false, nil + } + } else { + t.Logf("Deployment %q still exists, retrying...", istiodName) + return false, nil + } + return true, nil + }); err != nil { + return fmt.Errorf("timed out waiting for deployment %v to disappear", istiodName) + } + t.Logf("Deleted deployment %q", istiodName) + + return nil +} + +// cleanupOLMOperator deletes all components associated with the given OLM operator. +func cleanupOLMOperator(t *testing.T, namespace, name string) error { + operatorName := types.NamespacedName{Namespace: namespace, Name: name} + operator := &olmv1.Operator{} + if err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 1*time.Minute, false, func(context context.Context) (bool, error) { + if err := kclient.Get(context, operatorName, operator); err != nil { + t.Logf("Failed to get operator %q, retrying...", operatorName) + return false, nil + } + return true, nil + }); err != nil { + return fmt.Errorf("timed out getting operator %v", operatorName) + } + + if operator.Status.Components != nil { + for _, compRef := range operator.Status.Components.Refs { + // OperatorHUB's uninstall action doesn't touch operator's CRDs. + if compRef.Kind != "CustomResourceDefinition" { + unstructuredObj := &unstructured.Unstructured{} + unstructuredObj.SetAPIVersion(compRef.APIVersion) + unstructuredObj.SetKind(compRef.Kind) + unstructuredObj.SetName(compRef.Name) + if len(compRef.Namespace) > 0 { + unstructuredObj.SetNamespace(compRef.Namespace) + } + if err := kclient.Delete(context.Background(), unstructuredObj); err != nil && !kerrors.IsNotFound(err) { + return fmt.Errorf(`failed to delete operator component "%s/%s/%s/%s": %w`, compRef.APIVersion, compRef.Kind, compRef.Namespace, compRef.Name, err) + } + t.Logf(`Deleted operator component "%s/%s/%s/%s"`, compRef.APIVersion, compRef.Kind, compRef.Namespace, compRef.Name) + } + } + } else { + t.Logf("No components found for operator %q", operatorName) + } + + return nil +} + // createHttpRoute checks if the HTTPRoute can be created. // If it can't an error is returned. func createHttpRoute(t *testing.T, namespace, routeName, parentNamespace, hostname, backendRefname string, gateway *gatewayapiv1.Gateway) (*gatewayapiv1.HTTPRoute, error) { @@ -318,6 +420,33 @@ func createCRD(name string) (*apiextensionsv1.CustomResourceDefinition, error) { return crd, nil } +// createCatalogSource creates a CatalogSource with the given name and image. +// It returns an error if creation fails after retries. +func createCatalogSource(t *testing.T, namespace, name, image string) error { + t.Helper() + + cs := &operatorsv1alpha1.CatalogSource{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: operatorsv1alpha1.CatalogSourceSpec{ + Image: image, + SourceType: operatorsv1alpha1.SourceTypeGrpc, + DisplayName: name, + Publisher: "Custom Red Hat", + }, + } + + return wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 30*time.Second, false, func(context context.Context) (bool, error) { + if err := kclient.Create(context, cs); err != nil && !kerrors.IsAlreadyExists(err) { + t.Logf("Failed to create CatalogSource %q: %v, retrying...", name, err) + return false, nil + } + return true, nil + }) +} + // buildGatewayClass initializes the GatewayClass and returns its address. func buildGatewayClass(name, controllerName string) *gatewayapiv1.GatewayClass { return &gatewayapiv1.GatewayClass{ @@ -445,12 +574,22 @@ func assertSubscription(t *testing.T, namespace, subName string) error { subscription := &operatorsv1alpha1.Subscription{} nsName := types.NamespacedName{Namespace: namespace, Name: subName} - err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 30*time.Second, false, func(context context.Context) (bool, error) { + err := wait.PollUntilContextTimeout(context.Background(), 2*time.Second, 2*time.Minute, false, func(context context.Context) (bool, error) { if err := kclient.Get(context, nsName, subscription); err != nil { t.Logf("failed to get subscription %s, retrying...", subName) return false, nil } t.Logf("found subscription %s at installed version %s", subscription.Name, subscription.Status.InstalledCSV) + for _, c := range subscription.Status.Conditions { + if c.Type == operatorsv1alpha1.SubscriptionCatalogSourcesUnhealthy { + if c.Status == corev1.ConditionTrue { + t.Logf("catalog sources unhealthy for subscription %s, retrying...", subName) + return false, nil + } + break + } + } + t.Logf("all catalog sources healthy for subscription %s", subscription.Name) return true, nil }) return err @@ -501,39 +640,119 @@ func deleteExistingSubscription(t *testing.T, namespace, subName string) error { } -// assertOSSMOperator checks if the OSSM Istio operator gets successfully installed -// and returns an error if not. +// assertInstallPlan checks if the InstallPlan with given ClusterServiceVersion exists and installed. +func assertInstallPlan(t *testing.T, namespace, csvName string) error { + t.Helper() + + ips := &operatorsv1alpha1.InstallPlanList{} + var ip *operatorsv1alpha1.InstallPlan + ipHasCSV := func(ip operatorsv1alpha1.InstallPlan) bool { + for _, c := range ip.Spec.ClusterServiceVersionNames { + if c == csvName { + return true + } + } + return false + } + + err := wait.PollUntilContextTimeout(context.Background(), 2*time.Second, 2*time.Minute, false, func(context context.Context) (bool, error) { + if err := kclient.List(context, ips); err != nil { + t.Logf("Failed to list install plans: %v, retrying...", err) + return false, nil + } + + for i := range ips.Items { + if ipHasCSV(ips.Items[i]) { + ip = &ips.Items[i] + break + } + } + + if ip == nil { + t.Logf("Failed to find install plan for clusterserviceversion %q, retrying...", csvName) + return false, nil + } + + t.Logf("Found install plan %q", ip.Name) + for _, c := range ip.Status.Conditions { + if c.Type == operatorsv1alpha1.InstallPlanInstalled { + if c.Status == corev1.ConditionTrue { + t.Logf("Installed condition is false for install plan %q, retrying...", ip.Name) + return false, nil + } + break + } + } + t.Logf("Install plan %q successfully installed", ip.Name) + return true, nil + }) + if err != nil { + if ip != nil { + t.Logf("Last observed state of install plan %q:\n%s", ip.Name, util.ToYaml(ip)) + } else { + t.Logf("Last observed state of all install plans:\n%s", util.ToYaml(ips)) + } + } + + return err +} + +// logClusterServiceVersion logs the status of the given cluster service version. +func logClusterServiceVersion(t *testing.T, namespace, csvName string) error { + t.Helper() + + csv := &operatorsv1alpha1.ClusterServiceVersion{} + nsName := types.NamespacedName{Namespace: namespace, Name: csvName} + + return wait.PollUntilContextTimeout(context.Background(), 2*time.Second, 2*time.Minute, false, func(context context.Context) (bool, error) { + if err := kclient.Get(context, nsName, csv); err != nil { + t.Logf("Failed to get cluster service version %q: %v, retrying...", nsName.Name, err) + return false, nil + } + t.Logf("Found cluster service version %q:\n%s", nsName.Name, util.ToYaml(csv.Status)) + return true, nil + }) +} + +// assertOSSMOperator checks if the OSSM operator gets successfully installed +// and returns an error if not. It uses configurable parameters such as the expected OSSM version, polling interval, and timeout. func assertOSSMOperator(t *testing.T) error { + return assertOSSMOperatorWithConfig(t, "", 1*time.Second, 60*time.Second) +} + +// assertOSSMOperatorWithConfig checks if the OSSM operator gets successfully installed +// and returns an error if not. It uses configurable parameters such as +// the expected OSSM version, polling interval, and timeout. +func assertOSSMOperatorWithConfig(t *testing.T, version string, interval, timeout time.Duration) error { t.Helper() dep := &appsv1.Deployment{} ns := types.NamespacedName{Namespace: openshiftOperatorsNamespace, Name: openshiftIstioOperatorDeploymentName} - // Get the Istio operator deployment. - err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 30*time.Second, false, func(context context.Context) (bool, error) { + // Get the OSSM operator deployment. + if err := wait.PollUntilContextTimeout(context.Background(), interval, timeout, false, func(context context.Context) (bool, error) { if err := kclient.Get(context, ns, dep); err != nil { t.Logf("failed to get deployment %v, retrying...", ns) return false, nil } + if len(version) > 0 { + if csv, found := dep.Labels["olm.owner"]; found { + if csv == version { + t.Logf("Found OSSM deployment %q with expected version %q", ns, version) + } else { + t.Logf("OSSM deployment %q expected to have version %q but got %q, retrying...", ns, version, csv) + return false, nil + } + } + } + if dep.Status.AvailableReplicas < *dep.Spec.Replicas { + t.Logf("OSSM deployment %q expected to have %d available replica(s) but got %d, retrying...", ns, *dep.Spec.Replicas, dep.Status.AvailableReplicas) + return false, nil + } + t.Logf("found OSSM operator deployment %q with %d available replica(s)", ns, dep.Status.AvailableReplicas) return true, nil - }) - if err != nil { + }); err != nil { return fmt.Errorf("error finding deployment %v: %v", ns, err) } - - // Get the istio-operator pod. - podlist, err := getPods(t, kclient, dep) - if err != nil { - return fmt.Errorf("error finding pod for deployment %v: %v", ns, err) - } - if len(podlist.Items) > 1 { - return fmt.Errorf("too many pods for deployment %v: %d", ns, len(podlist.Items)) - } - pod := podlist.Items[0] - if pod.Status.Phase != corev1.PodRunning { - return fmt.Errorf("OSSM operator failure: pod %s is not running, it is %v", pod.Name, pod.Status.Phase) - } - - t.Logf("found OSSM operator pod %s/%s to be %s", pod.Namespace, pod.Name, pod.Status.Phase) return nil } @@ -620,8 +839,9 @@ func assertGatewaySuccessful(t *testing.T, namespace, name string) (*gatewayapiv // Wait for the gateway to be accepted and programmed. // Load balancer provisioning can take several minutes on some platforms. - // Therefore, a timeout of 3 minutes is set to accommodate potential delays. - err := wait.PollUntilContextTimeout(context.Background(), 3*time.Second, 3*time.Minute, false, func(context context.Context) (bool, error) { + // CCM leader election can also be slow - up to 8 minutes has been observed. + // Therefore, a timeout of 10 minutes is set to accommodate potential delays. + err := wait.PollUntilContextTimeout(context.Background(), 5*time.Second, 10*time.Minute, false, func(context context.Context) (bool, error) { if err := kclient.Get(context, nsName, gw); err != nil { t.Logf("Failed to get gateway %v: %v; retrying...", nsName, err) return false, nil @@ -659,7 +879,7 @@ func assertGatewaySuccessful(t *testing.T, namespace, name string) (*gatewayapiv t.Logf("Failed to get gateway service %v: %v; retrying...", svcNsName, err) return false, nil } - t.Logf("[%s] Found gateway service: %+v", time.Now().Format(time.DateTime), svc) + t.Logf("[%s] Found gateway service with status: %+v", time.Now().Format(time.DateTime), svc.Status) return false, nil }) if err != nil { @@ -977,11 +1197,18 @@ func getHTTPResponse(client *http.Client, name string) (int, http.Header, string // assertCatalogSource checks if the CatalogSource of the given name exists, // and returns an error if not. func assertCatalogSource(t *testing.T, namespace, csName string) error { + return assertCatalogSourceWithConfig(t, namespace, csName, 1*time.Second, 30*time.Second) +} + +// assertCatalogSourceWithConfig checks if the CatalogSource of the given name exists, +// and returns an error if not. It uses configurable parameters such as polling interval +// and timeout. +func assertCatalogSourceWithConfig(t *testing.T, namespace, csName string, interval, timeout time.Duration) error { t.Helper() catalogSource := &operatorsv1alpha1.CatalogSource{} nsName := types.NamespacedName{Namespace: namespace, Name: csName} - err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 30*time.Second, false, func(context context.Context) (bool, error) { + return wait.PollUntilContextTimeout(context.Background(), interval, timeout, false, func(context context.Context) (bool, error) { if err := kclient.Get(context, nsName, catalogSource); err != nil { t.Logf("Failed to get CatalogSource %s: %v. Retrying...", csName, err) return false, nil @@ -993,12 +1220,18 @@ func assertCatalogSource(t *testing.T, namespace, csName string) error { t.Logf("Found CatalogSource %s but could not determine last observed state. Retrying...", catalogSource.Name) return false, nil }) - return err } // assertIstio checks if the Istio exists in a ready state, // and returns an error if not. func assertIstio(t *testing.T) error { + return assertIstioWithConfig(t, "") +} + +// assertIstio checks if the Istio exists in a ready state, +// and returns an error if not.It uses configurable parameters such as +// the expected version. +func assertIstioWithConfig(t *testing.T, version string) error { t.Helper() istio := &sailv1.Istio{} nsName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: openshiftIstioName} @@ -1008,6 +1241,14 @@ func assertIstio(t *testing.T) error { t.Logf("Failed to get Istio %s/%s: %v. Retrying...", nsName.Namespace, nsName.Name, err) return false, nil } + if len(version) > 0 { + if version == istio.Spec.Version { + t.Logf("Found Istio %s/%s with expected version %q", istio.Namespace, istio.Name, version) + } else { + t.Logf("Istio %s/%s expected to have version %q but got %q, retrying...", istio.Namespace, istio.Name, version, istio.Spec.Version) + return false, nil + } + } if istio.Status.GetCondition(sailv1.IstioConditionReady).Status == metav1.ConditionTrue { t.Logf("Found Istio %s/%s, and it reports ready", istio.Namespace, istio.Name) return true, nil diff --git a/vendor/github.com/operator-framework/api/pkg/operators/v1/doc.go b/vendor/github.com/operator-framework/api/pkg/operators/v1/doc.go new file mode 100644 index 0000000000..dec83277bb --- /dev/null +++ b/vendor/github.com/operator-framework/api/pkg/operators/v1/doc.go @@ -0,0 +1,4 @@ +// +groupName=operators.coreos.com + +// Package v1 contains resources types for version v1 of the operators.coreos.com API group. +package v1 diff --git a/vendor/github.com/operator-framework/api/pkg/operators/v1/groupversion_info.go b/vendor/github.com/operator-framework/api/pkg/operators/v1/groupversion_info.go new file mode 100644 index 0000000000..089ec87839 --- /dev/null +++ b/vendor/github.com/operator-framework/api/pkg/operators/v1/groupversion_info.go @@ -0,0 +1,28 @@ +// +kubebuilder:object:generate=true + +// Package v1 contains API Schema definitions for the operator v1 API group. +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "operators.coreos.com", Version: "v1"} + + // SchemeGroupVersion is required for compatibility with client generation. + SchemeGroupVersion = GroupVersion + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return GroupVersion.WithResource(resource).GroupResource() +} diff --git a/vendor/github.com/operator-framework/api/pkg/operators/v1/olmconfig_types.go b/vendor/github.com/operator-framework/api/pkg/operators/v1/olmconfig_types.go new file mode 100644 index 0000000000..c15b5114fc --- /dev/null +++ b/vendor/github.com/operator-framework/api/pkg/operators/v1/olmconfig_types.go @@ -0,0 +1,90 @@ +package v1 + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DisabledCopiedCSVsConditionType = "DisabledCopiedCSVs" +) + +// OLMConfigSpec is the spec for an OLMConfig resource. +type OLMConfigSpec struct { + Features *Features `json:"features,omitempty"` +} + +// Features contains the list of configurable OLM features. +type Features struct { + + // DisableCopiedCSVs is used to disable OLM's "Copied CSV" feature + // for operators installed at the cluster scope, where a cluster + // scoped operator is one that has been installed in an + // OperatorGroup that targets all namespaces. + // When reenabled, OLM will recreate the "Copied CSVs" for each + // cluster scoped operator. + DisableCopiedCSVs *bool `json:"disableCopiedCSVs,omitempty"` + // PackageServerSyncInterval is used to define the sync interval for + // packagerserver pods. Packageserver pods periodically check the + // status of CatalogSources; this specifies the period using duration + // format (e.g. "60m"). For this parameter, only hours ("h"), minutes + // ("m"), and seconds ("s") may be specified. When not specified, the + // period defaults to the value specified within the packageserver. + // +optional + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(s|m|h))+$" + PackageServerSyncInterval *metav1.Duration `json:"packageServerSyncInterval,omitempty"` +} + +// OLMConfigStatus is the status for an OLMConfig resource. +type OLMConfigStatus struct { + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +genclient +// +genclient:nonNamespaced +// +kubebuilder:storageversion +// +kubebuilder:resource:categories=olm,scope=Cluster +// +kubebuilder:subresource:status + +// OLMConfig is a resource responsible for configuring OLM. +type OLMConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + Spec OLMConfigSpec `json:"spec,omitempty"` + Status OLMConfigStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// OLMConfigList is a list of OLMConfig resources. +type OLMConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + // +listType=set + Items []OLMConfig `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OLMConfig{}, &OLMConfigList{}) +} + +// CopiedCSVsAreEnabled returns true if and only if the olmConfigs DisableCopiedCSVs is set and true, +// otherwise false is returned +func (config *OLMConfig) CopiedCSVsAreEnabled() bool { + if config == nil || config.Spec.Features == nil || config.Spec.Features.DisableCopiedCSVs == nil { + return true + } + + return !*config.Spec.Features.DisableCopiedCSVs +} + +func (config *OLMConfig) PackageServerSyncInterval() *time.Duration { + if config == nil || config.Spec.Features == nil || config.Spec.Features.PackageServerSyncInterval == nil { + return nil + } + return &config.Spec.Features.PackageServerSyncInterval.Duration +} diff --git a/vendor/github.com/operator-framework/api/pkg/operators/v1/operator_types.go b/vendor/github.com/operator-framework/api/pkg/operators/v1/operator_types.go new file mode 100644 index 0000000000..af735950f5 --- /dev/null +++ b/vendor/github.com/operator-framework/api/pkg/operators/v1/operator_types.go @@ -0,0 +1,88 @@ +package v1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// OperatorSpec defines the desired state of Operator +type OperatorSpec struct{} + +// OperatorStatus defines the observed state of an Operator and its components +type OperatorStatus struct { + // Components describes resources that compose the operator. + // +optional + Components *Components `json:"components,omitempty"` +} + +// ConditionType codifies a condition's type. +type ConditionType string + +// Condition represent the latest available observations of an component's state. +type Condition struct { + // Type of condition. + Type ConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status"` + // The reason for the condition's last transition. + // +optional + Reason string `json:"reason,omitempty"` + // A human readable message indicating details about the transition. + // +optional + Message string `json:"message,omitempty"` + // Last time the condition was probed + // +optional + LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + // Last time the condition transitioned from one status to another. + // +optional + LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"` +} + +// Components tracks the resources that compose an operator. +type Components struct { + // LabelSelector is a label query over a set of resources used to select the operator's components + LabelSelector *metav1.LabelSelector `json:"labelSelector"` + // Refs are a set of references to the operator's component resources, selected with LabelSelector. + // +optional + Refs []RichReference `json:"refs,omitempty"` +} + +// RichReference is a reference to a resource, enriched with its status conditions. +type RichReference struct { + *corev1.ObjectReference `json:",inline"` + // Conditions represents the latest state of the component. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// +genclient +// +genclient:nonNamespaced +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:resource:categories=olm,scope=Cluster +// +kubebuilder:subresource:status + +// Operator represents a cluster operator. +type Operator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OperatorSpec `json:"spec,omitempty"` + Status OperatorStatus `json:"status,omitempty"` +} + +// +genclient:nonNamespaced +// +kubebuilder:object:root=true + +// OperatorList contains a list of Operators. +type OperatorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Operator `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Operator{}, &OperatorList{}) +} diff --git a/vendor/github.com/operator-framework/api/pkg/operators/v1/operatorcondition_types.go b/vendor/github.com/operator-framework/api/pkg/operators/v1/operatorcondition_types.go new file mode 100644 index 0000000000..8647b227e5 --- /dev/null +++ b/vendor/github.com/operator-framework/api/pkg/operators/v1/operatorcondition_types.go @@ -0,0 +1,49 @@ +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // Upgradeable indicates that the operator is upgradeable + Upgradeable string = "Upgradeable" +) + +// OperatorConditionSpec allows a cluster admin to convey information about the state of an operator to OLM, potentially overriding state reported by the operator. +type OperatorConditionSpec struct { + ServiceAccounts []string `json:"serviceAccounts,omitempty"` + Deployments []string `json:"deployments,omitempty"` + Overrides []metav1.Condition `json:"overrides,omitempty"` +} + +// OperatorConditionStatus allows an operator to convey information its state to OLM. The status may trail the actual +// state of a system. +type OperatorConditionStatus struct { + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +genclient +// +kubebuilder:resource:shortName=condition,categories=olm +// +kubebuilder:subresource:status +// OperatorCondition is a Custom Resource of type `OperatorCondition` which is used to convey information to OLM about the state of an operator. +type OperatorCondition struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + Spec OperatorConditionSpec `json:"spec,omitempty"` + Status OperatorConditionStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// OperatorConditionList represents a list of Conditions. +type OperatorConditionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []OperatorCondition `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OperatorCondition{}, &OperatorConditionList{}) +} diff --git a/vendor/github.com/operator-framework/api/pkg/operators/v1/operatorgroup_types.go b/vendor/github.com/operator-framework/api/pkg/operators/v1/operatorgroup_types.go new file mode 100644 index 0000000000..81ad352d4e --- /dev/null +++ b/vendor/github.com/operator-framework/api/pkg/operators/v1/operatorgroup_types.go @@ -0,0 +1,214 @@ +package v1 + +import ( + "fmt" + "sort" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + OperatorGroupAnnotationKey = "olm.operatorGroup" + OperatorGroupNamespaceAnnotationKey = "olm.operatorNamespace" + OperatorGroupTargetsAnnotationKey = "olm.targetNamespaces" + OperatorGroupProvidedAPIsAnnotationKey = "olm.providedAPIs" + + OperatorGroupKind = "OperatorGroup" + + OperatorGroupLabelPrefix = "olm.operatorgroup.uid/" + OperatorGroupLabelTemplate = OperatorGroupLabelPrefix + "%s" + + OperatorGroupServiceAccountCondition = "OperatorGroupServiceAccount" + MutlipleOperatorGroupCondition = "MultipleOperatorGroup" + MultipleOperatorGroupsReason = "MultipleOperatorGroupsFound" + OperatorGroupServiceAccountReason = "ServiceAccountNotFound" + + // UpgradeStrategyDefault configures OLM such that it will only allow + // clusterServiceVersions to move to the replacing phase to the succeeded + // phase. This effectively means that OLM will not allow operators to move + // to the next version if an installation or upgrade has failed. + UpgradeStrategyDefault UpgradeStrategy = "Default" + + // UpgradeStrategyUnsafeFailForward configures OLM such that it will allow + // clusterServiceVersions to move to the replacing phase from the succeeded + // phase or from the failed phase. Additionally, OLM will generate new + // installPlans when a subscription references a failed installPlan and the + // catalog has been updated with a new upgrade for the existing set of + // operators. + // + // WARNING: The UpgradeStrategyUnsafeFailForward upgrade strategy is unsafe + // and may result in unexpected behavior or unrecoverable data loss unless + // you have deep understanding of the set of operators being managed in the + // namespace. + UpgradeStrategyUnsafeFailForward UpgradeStrategy = "TechPreviewUnsafeFailForward" +) + +type UpgradeStrategy string + +// OperatorGroupSpec is the spec for an OperatorGroup resource. +type OperatorGroupSpec struct { + // Selector selects the OperatorGroup's target namespaces. + // +optional + Selector *metav1.LabelSelector `json:"selector,omitempty"` + + // TargetNamespaces is an explicit set of namespaces to target. + // If it is set, Selector is ignored. + // +optional + // +listType=set + TargetNamespaces []string `json:"targetNamespaces,omitempty"` + + // ServiceAccountName is the admin specified service account which will be + // used to deploy operator(s) in this operator group. + ServiceAccountName string `json:"serviceAccountName,omitempty"` + + // Static tells OLM not to update the OperatorGroup's providedAPIs annotation + // +optional + StaticProvidedAPIs bool `json:"staticProvidedAPIs,omitempty"` + + // UpgradeStrategy defines the upgrade strategy for operators in the namespace. + // There are currently two supported upgrade strategies: + // + // Default: OLM will only allow clusterServiceVersions to move to the replacing + // phase from the succeeded phase. This effectively means that OLM will not + // allow operators to move to the next version if an installation or upgrade + // has failed. + // + // TechPreviewUnsafeFailForward: OLM will allow clusterServiceVersions to move to the + // replacing phase from the succeeded phase or from the failed phase. + // Additionally, OLM will generate new installPlans when a subscription references + // a failed installPlan and the catalog has been updated with a new upgrade for + // the existing set of operators. + // + // WARNING: The TechPreviewUnsafeFailForward upgrade strategy is unsafe and may result + // in unexpected behavior or unrecoverable data loss unless you have deep + // understanding of the set of operators being managed in the namespace. + // + // +kubebuilder:validation:Enum=Default;TechPreviewUnsafeFailForward + // +kubebuilder:default=Default + // +optional + UpgradeStrategy UpgradeStrategy `json:"upgradeStrategy,omitempty"` +} + +// OperatorGroupStatus is the status for an OperatorGroupResource. +type OperatorGroupStatus struct { + // Namespaces is the set of target namespaces for the OperatorGroup. + // +listType=set + Namespaces []string `json:"namespaces,omitempty"` + + // ServiceAccountRef references the service account object specified. + ServiceAccountRef *corev1.ObjectReference `json:"serviceAccountRef,omitempty"` + + // LastUpdated is a timestamp of the last time the OperatorGroup's status was Updated. + LastUpdated *metav1.Time `json:"lastUpdated"` + + // Conditions is an array of the OperatorGroup's conditions. + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +genclient +// +kubebuilder:storageversion +// +kubebuilder:resource:shortName=og,categories=olm +// +kubebuilder:subresource:status + +// OperatorGroup is the unit of multitenancy for OLM managed operators. +// It constrains the installation of operators in its namespace to a specified set of target namespaces. +type OperatorGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + // +optional + // +kubebuilder:default={upgradeStrategy:Default} + Spec OperatorGroupSpec `json:"spec"` + Status OperatorGroupStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// OperatorGroupList is a list of OperatorGroup resources. +type OperatorGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + // +listType=set + Items []OperatorGroup `json:"items"` +} + +// BuildTargetNamespaces returns the set of target namespaces as a sorted, comma-delimited string +func (o *OperatorGroup) BuildTargetNamespaces() string { + ns := make([]string, len(o.Status.Namespaces)) + copy(ns, o.Status.Namespaces) + sort.Strings(ns) + return strings.Join(ns, ",") +} + +// UpgradeStrategy returns the UpgradeStrategy specified or the default value otherwise. +func (o *OperatorGroup) UpgradeStrategy() UpgradeStrategy { + strategyName := o.Spec.UpgradeStrategy + switch { + case strategyName == UpgradeStrategyUnsafeFailForward: + return strategyName + default: + return UpgradeStrategyDefault + } +} + +// IsServiceAccountSpecified returns true if the spec has a service account name specified. +func (o *OperatorGroup) IsServiceAccountSpecified() bool { + if o.Spec.ServiceAccountName == "" { + return false + } + + return true +} + +// HasServiceAccountSynced returns true if the service account specified has been synced. +func (o *OperatorGroup) HasServiceAccountSynced() bool { + if o.IsServiceAccountSpecified() && o.Status.ServiceAccountRef != nil { + return true + } + + return false +} + +// OGLabelKeyAndValue returns a key and value that should be applied to namespaces listed in the OperatorGroup. +// If the UID is not set an error is returned. +func (o *OperatorGroup) OGLabelKeyAndValue() (string, string, error) { + if string(o.GetUID()) == "" { + return "", "", fmt.Errorf("Missing UID") + } + return fmt.Sprintf(OperatorGroupLabelTemplate, o.GetUID()), "", nil +} + +// NamespaceLabelSelector provides a selector that can be used to filter namespaces that belong to the OperatorGroup. +func (o *OperatorGroup) NamespaceLabelSelector() (*metav1.LabelSelector, error) { + if len(o.Spec.TargetNamespaces) == 0 { + // If no target namespaces are set, check if a selector exists. + if o.Spec.Selector != nil { + return o.Spec.Selector, nil + } + // No selector exists, return nil which should be used to select EVERYTHING. + return nil, nil + } + // Return a label that should be present on all namespaces defined in the OperatorGroup.Spec.TargetNamespaces field. + ogKey, ogValue, err := o.OGLabelKeyAndValue() + if err != nil { + return nil, err + } + + return &metav1.LabelSelector{ + MatchLabels: map[string]string{ + ogKey: ogValue, + }, + }, nil +} + +// IsOperatorGroupLabel returns true if the label is an OperatorGroup label. +func IsOperatorGroupLabel(label string) bool { + return strings.HasPrefix(label, OperatorGroupLabelPrefix) +} + +func init() { + SchemeBuilder.Register(&OperatorGroup{}, &OperatorGroupList{}) +} diff --git a/vendor/github.com/operator-framework/api/pkg/operators/v1/zz_generated.deepcopy.go b/vendor/github.com/operator-framework/api/pkg/operators/v1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..d6f89ba401 --- /dev/null +++ b/vendor/github.com/operator-framework/api/pkg/operators/v1/zz_generated.deepcopy.go @@ -0,0 +1,556 @@ +//go:build !ignore_autogenerated + +/* + +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. DO NOT EDIT. + +package v1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Components) DeepCopyInto(out *Components) { + *out = *in + if in.LabelSelector != nil { + in, out := &in.LabelSelector, &out.LabelSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.Refs != nil { + in, out := &in.Refs, &out.Refs + *out = make([]RichReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Components. +func (in *Components) DeepCopy() *Components { + if in == nil { + return nil + } + out := new(Components) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + if in.LastUpdateTime != nil { + in, out := &in.LastUpdateTime, &out.LastUpdateTime + *out = (*in).DeepCopy() + } + if in.LastTransitionTime != nil { + in, out := &in.LastTransitionTime, &out.LastTransitionTime + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Features) DeepCopyInto(out *Features) { + *out = *in + if in.DisableCopiedCSVs != nil { + in, out := &in.DisableCopiedCSVs, &out.DisableCopiedCSVs + *out = new(bool) + **out = **in + } + if in.PackageServerSyncInterval != nil { + in, out := &in.PackageServerSyncInterval, &out.PackageServerSyncInterval + *out = new(metav1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Features. +func (in *Features) DeepCopy() *Features { + if in == nil { + return nil + } + out := new(Features) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OLMConfig) DeepCopyInto(out *OLMConfig) { + *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 OLMConfig. +func (in *OLMConfig) DeepCopy() *OLMConfig { + if in == nil { + return nil + } + out := new(OLMConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OLMConfig) 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 *OLMConfigList) DeepCopyInto(out *OLMConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OLMConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OLMConfigList. +func (in *OLMConfigList) DeepCopy() *OLMConfigList { + if in == nil { + return nil + } + out := new(OLMConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OLMConfigList) 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 *OLMConfigSpec) DeepCopyInto(out *OLMConfigSpec) { + *out = *in + if in.Features != nil { + in, out := &in.Features, &out.Features + *out = new(Features) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OLMConfigSpec. +func (in *OLMConfigSpec) DeepCopy() *OLMConfigSpec { + if in == nil { + return nil + } + out := new(OLMConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OLMConfigStatus) DeepCopyInto(out *OLMConfigStatus) { + *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 OLMConfigStatus. +func (in *OLMConfigStatus) DeepCopy() *OLMConfigStatus { + if in == nil { + return nil + } + out := new(OLMConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Operator) DeepCopyInto(out *Operator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Operator. +func (in *Operator) DeepCopy() *Operator { + if in == nil { + return nil + } + out := new(Operator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Operator) 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 *OperatorCondition) DeepCopyInto(out *OperatorCondition) { + *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 OperatorCondition. +func (in *OperatorCondition) DeepCopy() *OperatorCondition { + if in == nil { + return nil + } + out := new(OperatorCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperatorCondition) 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 *OperatorConditionList) DeepCopyInto(out *OperatorConditionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OperatorCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConditionList. +func (in *OperatorConditionList) DeepCopy() *OperatorConditionList { + if in == nil { + return nil + } + out := new(OperatorConditionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperatorConditionList) 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 *OperatorConditionSpec) DeepCopyInto(out *OperatorConditionSpec) { + *out = *in + if in.ServiceAccounts != nil { + in, out := &in.ServiceAccounts, &out.ServiceAccounts + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Deployments != nil { + in, out := &in.Deployments, &out.Deployments + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Overrides != nil { + in, out := &in.Overrides, &out.Overrides + *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 OperatorConditionSpec. +func (in *OperatorConditionSpec) DeepCopy() *OperatorConditionSpec { + if in == nil { + return nil + } + out := new(OperatorConditionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConditionStatus) DeepCopyInto(out *OperatorConditionStatus) { + *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 OperatorConditionStatus. +func (in *OperatorConditionStatus) DeepCopy() *OperatorConditionStatus { + if in == nil { + return nil + } + out := new(OperatorConditionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorGroup) DeepCopyInto(out *OperatorGroup) { + *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 OperatorGroup. +func (in *OperatorGroup) DeepCopy() *OperatorGroup { + if in == nil { + return nil + } + out := new(OperatorGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperatorGroup) 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 *OperatorGroupList) DeepCopyInto(out *OperatorGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OperatorGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorGroupList. +func (in *OperatorGroupList) DeepCopy() *OperatorGroupList { + if in == nil { + return nil + } + out := new(OperatorGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperatorGroupList) 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 *OperatorGroupSpec) DeepCopyInto(out *OperatorGroupSpec) { + *out = *in + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.TargetNamespaces != nil { + in, out := &in.TargetNamespaces, &out.TargetNamespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorGroupSpec. +func (in *OperatorGroupSpec) DeepCopy() *OperatorGroupSpec { + if in == nil { + return nil + } + out := new(OperatorGroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorGroupStatus) DeepCopyInto(out *OperatorGroupStatus) { + *out = *in + if in.Namespaces != nil { + in, out := &in.Namespaces, &out.Namespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ServiceAccountRef != nil { + in, out := &in.ServiceAccountRef, &out.ServiceAccountRef + *out = new(corev1.ObjectReference) + **out = **in + } + if in.LastUpdated != nil { + in, out := &in.LastUpdated, &out.LastUpdated + *out = (*in).DeepCopy() + } + 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 OperatorGroupStatus. +func (in *OperatorGroupStatus) DeepCopy() *OperatorGroupStatus { + if in == nil { + return nil + } + out := new(OperatorGroupStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorList) DeepCopyInto(out *OperatorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Operator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorList. +func (in *OperatorList) DeepCopy() *OperatorList { + if in == nil { + return nil + } + out := new(OperatorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperatorList) 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 *OperatorSpec) DeepCopyInto(out *OperatorSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorSpec. +func (in *OperatorSpec) DeepCopy() *OperatorSpec { + if in == nil { + return nil + } + out := new(OperatorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorStatus) DeepCopyInto(out *OperatorStatus) { + *out = *in + if in.Components != nil { + in, out := &in.Components, &out.Components + *out = new(Components) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorStatus. +func (in *OperatorStatus) DeepCopy() *OperatorStatus { + if in == nil { + return nil + } + out := new(OperatorStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RichReference) DeepCopyInto(out *RichReference) { + *out = *in + if in.ObjectReference != nil { + in, out := &in.ObjectReference, &out.ObjectReference + *out = new(corev1.ObjectReference) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RichReference. +func (in *RichReference) DeepCopy() *RichReference { + if in == nil { + return nil + } + out := new(RichReference) + in.DeepCopyInto(out) + return out +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 507c3cda9a..63a79cefc3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -517,6 +517,7 @@ github.com/openshift/library-go/test/library/metrics ## explicit; go 1.23.0 github.com/operator-framework/api/pkg/lib/version github.com/operator-framework/api/pkg/operators +github.com/operator-framework/api/pkg/operators/v1 github.com/operator-framework/api/pkg/operators/v1alpha1 # github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c ## explicit; go 1.14