diff --git a/assets/common/hypershift/controller_add_hypershift_desired_version_annotation.yaml b/assets/common/hypershift/controller_add_hypershift_desired_version_annotation.yaml new file mode 100644 index 000000000..d43171ec1 --- /dev/null +++ b/assets/common/hypershift/controller_add_hypershift_desired_version_annotation.yaml @@ -0,0 +1,3 @@ +metadata: + annotations: + release.openshift.io/desired-version: ${RELEASE_VERSION} diff --git a/assets/overlays/aws-ebs/generated/hypershift/controller.yaml b/assets/overlays/aws-ebs/generated/hypershift/controller.yaml index 771ec4155..d998ea80d 100644 --- a/assets/overlays/aws-ebs/generated/hypershift/controller.yaml +++ b/assets/overlays/aws-ebs/generated/hypershift/controller.yaml @@ -25,6 +25,7 @@ # Applied strategic merge patch common/hypershift/controller_add_affinity_tolerations.yaml # Applied JSON patch common/hypershift/controller_add_kubeconfig_volume.yaml.patch # Applied strategic merge patch common/hypershift/controller_add_hypershift_managed_by_label.yaml +# Applied strategic merge patch common/hypershift/controller_add_hypershift_desired_version_annotation.yaml # Applied strategic merge patch common/readOnlyRootFilesystem.yaml # Applied strategic merge patch overlays/aws-ebs/patches/controller_add_hypershift_controller_minter.yaml # @@ -36,6 +37,7 @@ metadata: annotations: config.openshift.io/inject-proxy: csi-driver config.openshift.io/inject-proxy-cabundle: csi-driver + release.openshift.io/desired-version: ${RELEASE_VERSION} labels: hypershift.openshift.io/managed-by: cluster-storage-operator name: aws-ebs-csi-driver-controller diff --git a/assets/overlays/azure-disk/generated/hypershift/controller.yaml b/assets/overlays/azure-disk/generated/hypershift/controller.yaml index 66c8d1bbb..d78844c64 100644 --- a/assets/overlays/azure-disk/generated/hypershift/controller.yaml +++ b/assets/overlays/azure-disk/generated/hypershift/controller.yaml @@ -25,6 +25,7 @@ # Applied strategic merge patch common/hypershift/controller_add_affinity_tolerations.yaml # Applied JSON patch common/hypershift/controller_add_kubeconfig_volume.yaml.patch # Applied strategic merge patch common/hypershift/controller_add_hypershift_managed_by_label.yaml +# Applied strategic merge patch common/hypershift/controller_add_hypershift_desired_version_annotation.yaml # Applied strategic merge patch common/readOnlyRootFilesystem.yaml # Applied strategic merge patch overlays/azure-disk/patches/controller_add_hypershift_controller.yaml # @@ -36,6 +37,7 @@ metadata: annotations: config.openshift.io/inject-proxy: csi-driver config.openshift.io/inject-proxy-cabundle: csi-driver + release.openshift.io/desired-version: ${RELEASE_VERSION} labels: hypershift.openshift.io/managed-by: cluster-storage-operator name: azure-disk-csi-driver-controller diff --git a/assets/overlays/azure-file/generated/hypershift/controller.yaml b/assets/overlays/azure-file/generated/hypershift/controller.yaml index 20090fc5d..990220da8 100644 --- a/assets/overlays/azure-file/generated/hypershift/controller.yaml +++ b/assets/overlays/azure-file/generated/hypershift/controller.yaml @@ -25,6 +25,7 @@ # Applied strategic merge patch common/hypershift/controller_add_affinity_tolerations.yaml # Applied JSON patch common/hypershift/controller_add_kubeconfig_volume.yaml.patch # Applied strategic merge patch common/hypershift/controller_add_hypershift_managed_by_label.yaml +# Applied strategic merge patch common/hypershift/controller_add_hypershift_desired_version_annotation.yaml # Applied strategic merge patch common/readOnlyRootFilesystem.yaml # Applied JSON patch common/hypershift/sidecar_add_kubeconfig.yaml.patch # Applied strategic merge patch overlays/azure-file/patches/controller_add_hypershift_controller.yaml @@ -37,6 +38,7 @@ metadata: annotations: config.openshift.io/inject-proxy: csi-driver config.openshift.io/inject-proxy-cabundle: csi-driver + release.openshift.io/desired-version: ${RELEASE_VERSION} labels: hypershift.openshift.io/managed-by: cluster-storage-operator name: azure-file-csi-driver-controller diff --git a/assets/overlays/openstack-cinder/generated/hypershift/controller.yaml b/assets/overlays/openstack-cinder/generated/hypershift/controller.yaml index 5469d033a..cc282c5b3 100644 --- a/assets/overlays/openstack-cinder/generated/hypershift/controller.yaml +++ b/assets/overlays/openstack-cinder/generated/hypershift/controller.yaml @@ -23,6 +23,7 @@ # Applied strategic merge patch common/hypershift/controller_add_affinity_tolerations.yaml # Applied JSON patch common/hypershift/controller_add_kubeconfig_volume.yaml.patch # Applied strategic merge patch common/hypershift/controller_add_hypershift_managed_by_label.yaml +# Applied strategic merge patch common/hypershift/controller_add_hypershift_desired_version_annotation.yaml # Applied strategic merge patch common/readOnlyRootFilesystem.yaml # Applied strategic merge patch overlays/openstack-cinder/patches/controller_add_hypershift_volumes.yaml # @@ -34,6 +35,7 @@ metadata: annotations: config.openshift.io/inject-proxy: csi-driver config.openshift.io/inject-proxy-cabundle: csi-driver + release.openshift.io/desired-version: ${RELEASE_VERSION} labels: hypershift.openshift.io/managed-by: cluster-storage-operator name: openstack-cinder-csi-driver-controller diff --git a/assets/overlays/openstack-manila/generated/hypershift/controller.yaml b/assets/overlays/openstack-manila/generated/hypershift/controller.yaml index 7ab727ad2..7a2ec586d 100644 --- a/assets/overlays/openstack-manila/generated/hypershift/controller.yaml +++ b/assets/overlays/openstack-manila/generated/hypershift/controller.yaml @@ -19,6 +19,7 @@ # Applied strategic merge patch common/hypershift/controller_add_affinity_tolerations.yaml # Applied JSON patch common/hypershift/controller_add_kubeconfig_volume.yaml.patch # Applied strategic merge patch common/hypershift/controller_add_hypershift_managed_by_label.yaml +# Applied strategic merge patch common/hypershift/controller_add_hypershift_desired_version_annotation.yaml # Applied strategic merge patch common/readOnlyRootFilesystem.yaml # Applied strategic merge patch overlays/openstack-manila/patches/controller_add_hypershift_volumes.yaml # Applied strategic merge patch overlays/openstack-manila/patches/controller_rename_config_map.yaml @@ -32,6 +33,7 @@ metadata: annotations: config.openshift.io/inject-proxy: csi-driver config.openshift.io/inject-proxy-cabundle: csi-driver + release.openshift.io/desired-version: ${RELEASE_VERSION} labels: hypershift.openshift.io/managed-by: cluster-storage-operator name: openstack-manila-csi-controllerplugin diff --git a/go.mod b/go.mod index 28f0e6507..c2a257167 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 github.com/openshift/hypershift/api v0.0.0-20251112122022-66a7017f8635 - github.com/openshift/library-go v0.0.0-20251112091634-ab97ebb73f0f + github.com/openshift/library-go v0.0.0-20251222131241-289839b3ffe8 github.com/prometheus/client_golang v1.23.2 github.com/spf13/cobra v1.10.1 gopkg.in/ini.v1 v1.67.0 @@ -121,7 +121,7 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect diff --git a/go.sum b/go.sum index 63299e2fb..a1ef02a18 100644 --- a/go.sum +++ b/go.sum @@ -208,8 +208,8 @@ github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 h1:9JBeIXmnHlp github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235/go.mod h1:L49W6pfrZkfOE5iC1PqEkuLkXG4W0BX4w8b+L2Bv7fM= github.com/openshift/hypershift/api v0.0.0-20251112122022-66a7017f8635 h1:7ZzaDMMgHMBo9qmsU7I5ySwDr7gQLBR/k5wluCxz8/4= github.com/openshift/hypershift/api v0.0.0-20251112122022-66a7017f8635/go.mod h1:JiaoBwTsYtBVKKPgHcajChZCu20KdM97W2xc0MeBCBA= -github.com/openshift/library-go v0.0.0-20251112091634-ab97ebb73f0f h1:r1pLosA7z3+t+lzW29FU54sg4/pAWu+lsKD0L5Gx3wg= -github.com/openshift/library-go v0.0.0-20251112091634-ab97ebb73f0f/go.mod h1:ErDfiIrPHH+menTP/B4LKd0nxFDdvCbTamAc6SWMIh8= +github.com/openshift/library-go v0.0.0-20251222131241-289839b3ffe8 h1:TWqbSjaYbZGgB6EmnEN6Hc8lQYYCgju2qORBX7Ix1LI= +github.com/openshift/library-go v0.0.0-20251222131241-289839b3ffe8/go.mod h1:nIzWQQE49XbiKizVnVOip9CEB7HJ0hoJwNi3g3YKnKc= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= @@ -313,8 +313,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/pkg/driver/aws-ebs/aws_ebs.go b/pkg/driver/aws-ebs/aws_ebs.go index e2c005294..40634c8aa 100644 --- a/pkg/driver/aws-ebs/aws_ebs.go +++ b/pkg/driver/aws-ebs/aws_ebs.go @@ -21,6 +21,7 @@ import ( "github.com/openshift/library-go/pkg/operator/csi/csidrivernodeservicecontroller" "github.com/openshift/library-go/pkg/operator/csi/csistorageclasscontroller" dc "github.com/openshift/library-go/pkg/operator/deploymentcontroller" + "github.com/openshift/library-go/pkg/operator/hypershift/deploymentversion" "github.com/openshift/library-go/pkg/operator/resourcesynccontroller" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -217,7 +218,15 @@ func GetAWSEBSOperatorControllerConfig(ctx context.Context, flavour generator.Cl if flavour == generator.FlavourHyperShift { volumeTagController := NewEBSVolumeTagsController(cfg.GetControllerName("EBSVolumeTagsController"), c, c.EventRecorder) - cfg.ExtraControlPlaneControllers = append(cfg.ExtraControlPlaneControllers, volumeTagController) + versionController := deploymentversioncontroller.NewDeploymentVersionController( + cfg.GetControllerName("DeploymentVersionController"), + c.ControlPlaneNamespace, + "aws-ebs-csi-driver-controller", + c.ControlPlaneKubeInformers.InformersFor(c.ControlPlaneNamespace).Apps().V1().Deployments(), + c.OperatorClient, + c.ControlPlaneKubeClient, + c.EventRecorder) + cfg.ExtraControlPlaneControllers = append(cfg.ExtraControlPlaneControllers, volumeTagController, versionController) cfg.DeploymentInformers = append(cfg.DeploymentInformers, c.KubeInformers.InformersFor("").Core().V1().PersistentVolumes().Informer()) cfg.DeploymentInformers = append(cfg.DeploymentInformers, c.KubeInformers.InformersFor(awsEBSSecretNamespace).Core().V1().Secrets().Informer()) } diff --git a/pkg/driver/azure-disk/azure_disk.go b/pkg/driver/azure-disk/azure_disk.go index f4b52c190..7989ed48a 100644 --- a/pkg/driver/azure-disk/azure_disk.go +++ b/pkg/driver/azure-disk/azure_disk.go @@ -18,6 +18,7 @@ import ( "github.com/openshift/library-go/pkg/operator/csi/csidrivercontrollerservicecontroller" "github.com/openshift/library-go/pkg/operator/csi/csidrivernodeservicecontroller" "github.com/openshift/library-go/pkg/operator/csi/csistorageclasscontroller" + "github.com/openshift/library-go/pkg/operator/hypershift/deploymentversion" "github.com/openshift/library-go/pkg/operator/resourcesynccontroller" dc "github.com/openshift/library-go/pkg/operator/deploymentcontroller" @@ -213,6 +214,15 @@ func GetAzureDiskOperatorControllerConfig(ctx context.Context, flavour generator if azureDiskSecretProviderClass != "" { cfg.DeploymentHooks = append(cfg.DeploymentHooks, withAROCSIVolume(azureDiskSecretProviderClass)) } + versionController := deploymentversioncontroller.NewDeploymentVersionController( + cfg.GetControllerName("DeploymentVersionController"), + c.ControlPlaneNamespace, + "azure-disk-csi-driver-controller", + c.ControlPlaneKubeInformers.InformersFor(c.ControlPlaneNamespace).Apps().V1().Deployments(), + c.OperatorClient, + c.ControlPlaneKubeClient, + c.EventRecorder) + cfg.ExtraControlPlaneControllers = append(cfg.ExtraControlPlaneControllers, versionController) } // add extra replacement for stuff diff --git a/pkg/driver/azure-file/azure_file.go b/pkg/driver/azure-file/azure_file.go index c75589935..e3ff198e2 100644 --- a/pkg/driver/azure-file/azure_file.go +++ b/pkg/driver/azure-file/azure_file.go @@ -15,6 +15,7 @@ import ( "github.com/openshift/library-go/pkg/controller/factory" "github.com/openshift/library-go/pkg/operator/csi/csidrivercontrollerservicecontroller" "github.com/openshift/library-go/pkg/operator/csi/csidrivernodeservicecontroller" + "github.com/openshift/library-go/pkg/operator/hypershift/deploymentversion" "github.com/openshift/library-go/pkg/operator/resourcesynccontroller" dc "github.com/openshift/library-go/pkg/operator/deploymentcontroller" @@ -178,6 +179,15 @@ func GetAzureFileOperatorControllerConfig(ctx context.Context, flavour generator if azureFileSecretProviderClass != "" { cfg.DeploymentHooks = append(cfg.DeploymentHooks, withAROCSIVolume(azureFileSecretProviderClass)) } + versionController := deploymentversioncontroller.NewDeploymentVersionController( + cfg.GetControllerName("DeploymentVersionController"), + c.ControlPlaneNamespace, + "azure-file-csi-driver-controller", + c.ControlPlaneKubeInformers.InformersFor(c.ControlPlaneNamespace).Apps().V1().Deployments(), + c.OperatorClient, + c.ControlPlaneKubeClient, + c.EventRecorder) + cfg.ExtraControlPlaneControllers = append(cfg.ExtraControlPlaneControllers, versionController) } // add extra replacement for stuff diff --git a/pkg/driver/common/generator/base_assets.go b/pkg/driver/common/generator/base_assets.go index 8b385381b..0a0742215 100644 --- a/pkg/driver/common/generator/base_assets.go +++ b/pkg/driver/common/generator/base_assets.go @@ -46,6 +46,7 @@ var ( "controller.yaml", "common/hypershift/controller_add_affinity_tolerations.yaml", "controller.yaml", "common/hypershift/controller_add_kubeconfig_volume.yaml.patch", "controller.yaml", "common/hypershift/controller_add_hypershift_managed_by_label.yaml", + "controller.yaml", "common/hypershift/controller_add_hypershift_desired_version_annotation.yaml", ).WithPatches(generator.AllFlavours, "controller.yaml", "common/readOnlyRootFilesystem.yaml", ) diff --git a/pkg/driver/common/operator/replacer.go b/pkg/driver/common/operator/replacer.go index e004e06c2..9b863c336 100644 --- a/pkg/driver/common/operator/replacer.go +++ b/pkg/driver/common/operator/replacer.go @@ -2,6 +2,8 @@ package operator import ( "os" + + "github.com/openshift/library-go/pkg/operator/status" ) const ( @@ -67,5 +69,6 @@ func DefaultReplacements(controlPlaneNamespace, guestNamespace string) []string pairs = append(pairs, []string{"${NAMESPACE}", controlPlaneNamespace}...) pairs = append(pairs, []string{"${NODE_NAMESPACE}", guestNamespace}...) + pairs = append(pairs, []string{"${RELEASE_VERSION}", status.VersionForOperatorFromEnv()}...) return pairs } diff --git a/pkg/driver/openstack-cinder/openstack_cinder.go b/pkg/driver/openstack-cinder/openstack_cinder.go index b244b5471..7f7efc837 100644 --- a/pkg/driver/openstack-cinder/openstack_cinder.go +++ b/pkg/driver/openstack-cinder/openstack_cinder.go @@ -15,6 +15,7 @@ import ( "github.com/openshift/library-go/pkg/operator/csi/csidrivercontrollerservicecontroller" "github.com/openshift/library-go/pkg/operator/csi/csidrivernodeservicecontroller" dc "github.com/openshift/library-go/pkg/operator/deploymentcontroller" + "github.com/openshift/library-go/pkg/operator/hypershift/deploymentversion" ) const ( @@ -122,6 +123,18 @@ func GetOpenStackCinderOperatorControllerConfig(ctx context.Context, flavour gen } cfg.ExtraControlPlaneControllers = append(cfg.ExtraControlPlaneControllers, configMapSyncer) + if flavour == generator.FlavourHyperShift { + versionController := deploymentversioncontroller.NewDeploymentVersionController( + cfg.GetControllerName("DeploymentVersionController"), + c.ControlPlaneNamespace, + "openstack-cinder-csi-driver-controller", + c.ControlPlaneKubeInformers.InformersFor(c.ControlPlaneNamespace).Apps().V1().Deployments(), + c.OperatorClient, + c.ControlPlaneKubeClient, + c.EventRecorder) + cfg.ExtraControlPlaneControllers = append(cfg.ExtraControlPlaneControllers, versionController) + } + return cfg, nil } diff --git a/pkg/driver/openstack-manila/openstack_manila.go b/pkg/driver/openstack-manila/openstack_manila.go index 6610f6e60..ca8dc2250 100644 --- a/pkg/driver/openstack-manila/openstack_manila.go +++ b/pkg/driver/openstack-manila/openstack_manila.go @@ -24,6 +24,7 @@ import ( "github.com/openshift/library-go/pkg/operator/csi/csidrivernodeservicecontroller" dc "github.com/openshift/library-go/pkg/operator/deploymentcontroller" "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/hypershift/deploymentversion" "github.com/openshift/library-go/pkg/operator/resource/resourceapply" "github.com/openshift/library-go/pkg/operator/resource/resourceread" "github.com/openshift/library-go/pkg/operator/resourcesynccontroller" @@ -158,6 +159,18 @@ func GetOpenStackManilaOperatorControllerConfig(ctx context.Context, flavour gen return nil, err } + if flavour == generator.FlavourHyperShift { + versionController := deploymentversioncontroller.NewDeploymentVersionController( + cfg.GetControllerName("DeploymentVersionController"), + c.ControlPlaneNamespace, + "openstack-manila-csi-controllerplugin", + c.ControlPlaneKubeInformers.InformersFor(c.ControlPlaneNamespace).Apps().V1().Deployments(), + c.OperatorClient, + c.ControlPlaneKubeClient, + c.EventRecorder) + cfg.ExtraControlPlaneControllers = append(cfg.ExtraControlPlaneControllers, versionController) + } + cfg.Precondition = func() (bool, error) { openstackClient, err := client.NewOpenStackClient(util.CloudConfigFilename) if err != nil { diff --git a/vendor/github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers/status.go b/vendor/github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers/status.go new file mode 100644 index 000000000..3898b9824 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers/status.go @@ -0,0 +1,143 @@ +package v1helpers + +import ( + "bytes" + "fmt" + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/utils/clock" + "strings" + + configv1 "github.com/openshift/api/config/v1" +) + +// SetStatusCondition sets the corresponding condition in conditions to newCondition. +func SetStatusCondition(conditions *[]configv1.ClusterOperatorStatusCondition, newCondition configv1.ClusterOperatorStatusCondition, clock clock.PassiveClock) { + if conditions == nil { + conditions = &[]configv1.ClusterOperatorStatusCondition{} + } + existingCondition := FindStatusCondition(*conditions, newCondition.Type) + if existingCondition == nil { + newCondition.LastTransitionTime = metav1.NewTime(clock.Now()) + *conditions = append(*conditions, newCondition) + return + } + + if existingCondition.Status != newCondition.Status { + existingCondition.Status = newCondition.Status + existingCondition.LastTransitionTime = metav1.NewTime(clock.Now()) + } + + existingCondition.Reason = newCondition.Reason + existingCondition.Message = newCondition.Message +} + +// RemoveStatusCondition removes the corresponding conditionType from conditions. +func RemoveStatusCondition(conditions *[]configv1.ClusterOperatorStatusCondition, conditionType configv1.ClusterStatusConditionType) { + if conditions == nil { + conditions = &[]configv1.ClusterOperatorStatusCondition{} + } + newConditions := []configv1.ClusterOperatorStatusCondition{} + for _, condition := range *conditions { + if condition.Type != conditionType { + newConditions = append(newConditions, condition) + } + } + + *conditions = newConditions +} + +// FindStatusCondition finds the conditionType in conditions. +func FindStatusCondition(conditions []configv1.ClusterOperatorStatusCondition, conditionType configv1.ClusterStatusConditionType) *configv1.ClusterOperatorStatusCondition { + for i := range conditions { + if conditions[i].Type == conditionType { + return &conditions[i] + } + } + + return nil +} + +// GetStatusDiff returns a string representing change in condition status in human readable form. +func GetStatusDiff(oldStatus configv1.ClusterOperatorStatus, newStatus configv1.ClusterOperatorStatus) string { + messages := []string{} + for _, newCondition := range newStatus.Conditions { + existingStatusCondition := FindStatusCondition(oldStatus.Conditions, newCondition.Type) + if existingStatusCondition == nil { + messages = append(messages, fmt.Sprintf("%s set to %s (%q)", newCondition.Type, newCondition.Status, newCondition.Message)) + continue + } + if existingStatusCondition.Status != newCondition.Status { + messages = append(messages, fmt.Sprintf("%s changed from %s to %s (%q)", existingStatusCondition.Type, existingStatusCondition.Status, newCondition.Status, newCondition.Message)) + continue + } + if existingStatusCondition.Message != newCondition.Message { + messages = append(messages, fmt.Sprintf("%s message changed from %q to %q", existingStatusCondition.Type, existingStatusCondition.Message, newCondition.Message)) + } + } + for _, oldCondition := range oldStatus.Conditions { + // This should not happen. It means we removed old condition entirely instead of just changing its status + if c := FindStatusCondition(newStatus.Conditions, oldCondition.Type); c == nil { + messages = append(messages, fmt.Sprintf("%s was removed", oldCondition.Type)) + } + } + + if !equality.Semantic.DeepEqual(oldStatus.RelatedObjects, newStatus.RelatedObjects) { + messages = append(messages, fmt.Sprintf("status.relatedObjects changed from %q to %q", oldStatus.RelatedObjects, newStatus.RelatedObjects)) + } + if !equality.Semantic.DeepEqual(oldStatus.Extension, newStatus.Extension) { + messages = append(messages, fmt.Sprintf("status.extension changed from %q to %q", oldStatus.Extension, newStatus.Extension)) + } + + if !equality.Semantic.DeepEqual(oldStatus.Versions, newStatus.Versions) { + messages = append(messages, fmt.Sprintf("status.versions changed from %q to %q", oldStatus.Versions, newStatus.Versions)) + } + + if len(messages) == 0 { + // ignore errors + originalJSON := &bytes.Buffer{} + json.NewEncoder(originalJSON).Encode(oldStatus) + newJSON := &bytes.Buffer{} + json.NewEncoder(newJSON).Encode(newStatus) + messages = append(messages, diff.Diff(originalJSON.String(), newJSON.String())) + } + + return strings.Join(messages, ",") +} + +// IsStatusConditionTrue returns true when the conditionType is present and set to `configv1.ConditionTrue` +func IsStatusConditionTrue(conditions []configv1.ClusterOperatorStatusCondition, conditionType configv1.ClusterStatusConditionType) bool { + return IsStatusConditionPresentAndEqual(conditions, conditionType, configv1.ConditionTrue) +} + +// IsStatusConditionFalse returns true when the conditionType is present and set to `configv1.ConditionFalse` +func IsStatusConditionFalse(conditions []configv1.ClusterOperatorStatusCondition, conditionType configv1.ClusterStatusConditionType) bool { + return IsStatusConditionPresentAndEqual(conditions, conditionType, configv1.ConditionFalse) +} + +// IsStatusConditionPresentAndEqual returns true when conditionType is present and equal to status. +func IsStatusConditionPresentAndEqual(conditions []configv1.ClusterOperatorStatusCondition, conditionType configv1.ClusterStatusConditionType, status configv1.ConditionStatus) bool { + for _, condition := range conditions { + if condition.Type == conditionType { + return condition.Status == status + } + } + return false +} + +// IsStatusConditionNotIn returns true when the conditionType does not match the status. +func IsStatusConditionNotIn(conditions []configv1.ClusterOperatorStatusCondition, conditionType configv1.ClusterStatusConditionType, status ...configv1.ConditionStatus) bool { + for _, condition := range conditions { + if condition.Type == conditionType { + for _, s := range status { + if s == condition.Status { + return false + } + } + return true + } + } + return true +} diff --git a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go index 33a09ae16..bff6155c2 100644 --- a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go +++ b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go @@ -242,35 +242,41 @@ func ValidCipherSuites() []string { sort.Strings(validCipherSuites) return validCipherSuites } + +// DefaultCiphers returns the default cipher suites for TLS connections. +// +// RECOMMENDATION: Instead of relying on this function directly, consumers should respect +// TLSSecurityProfile settings from one of the OpenShift API configuration resources: +// - For API servers: Use apiserver.config.openshift.io/cluster Spec.TLSSecurityProfile +// - For ingress controllers: Use operator.openshift.io/v1 IngressController Spec.TLSSecurityProfile +// - For kubelet: Use machineconfiguration.openshift.io/v1 KubeletConfig Spec.TLSSecurityProfile +// +// These API resources allow cluster administrators to choose between Old, Intermediate, +// Modern, or Custom TLS profiles. Components should observe these settings. func DefaultCiphers() []uint16 { - // HTTP/2 mandates TLS 1.2 or higher with an AEAD cipher - // suite (GCM, Poly1305) and ephemeral key exchange (ECDHE, DHE) for - // perfect forward secrecy. Servers may provide additional cipher - // suites for backwards compatibility with HTTP/1.1 clients. - // See RFC7540, section 9.2 (Use of TLS Features) and Appendix A - // (TLS 1.2 Cipher Suite Black List). + // Aligned with intermediate profile of the 5.7 version of the Mozilla Server + // Side TLS guidelines found at: https://ssl-config.mozilla.org/guidelines/5.7.json + // + // Latest guidelines: https://ssl-config.mozilla.org/guidelines/latest.json + // + // This profile provides strong security with wide compatibility. + // It requires TLS 1.2+ and uses only AEAD cipher suites (GCM, ChaCha20-Poly1305) + // with ECDHE key exchange for perfect forward secrecy. + // + // All CBC-mode ciphers have been removed due to padding oracle vulnerabilities. + // All RSA key exchange ciphers have been removed due to lack of perfect forward secrecy. + // + // HTTP/2 compliance: All ciphers are compliant with RFC7540, section 9.2. return []uint16{ + // TLS 1.2 cipher suites with ECDHE + AEAD tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // required by http/2 + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // required by HTTP/2 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // forbidden by http/2, not flagged by http2isBadCipher() in go1.8 - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, // forbidden by http/2, not flagged by http2isBadCipher() in go1.8 - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // forbidden by http/2 - tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // forbidden by http/2 - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // forbidden by http/2 - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // forbidden by http/2 - tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // forbidden by http/2 - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // forbidden by http/2 - // the next one is in the intermediate suite, but go1.8 http2isBadCipher() complains when it is included at the recommended index - // because it comes after ciphers forbidden by the http/2 spec - // tls.TLS_RSA_WITH_AES_128_CBC_SHA256, - // tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, // forbidden by http/2, disabled to mitigate SWEET32 attack - // tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // forbidden by http/2, disabled to mitigate SWEET32 attack - tls.TLS_RSA_WITH_AES_128_CBC_SHA, // forbidden by http/2 - tls.TLS_RSA_WITH_AES_256_CBC_SHA, // forbidden by http/2 + + // TLS 1.3 cipher suites (negotiated automatically, not configurable) tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_CHACHA20_POLY1305_SHA256, diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go index 1cb4e5554..c2c8b8368 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/signer.go @@ -188,7 +188,7 @@ func getValidityFromAnnotations(annotations map[string]string) (notBefore time.T return notBefore, notAfter, fmt.Sprintf("bad expiry: %q", notAfterString) } notBeforeString := annotations[CertificateNotBeforeAnnotation] - if len(notAfterString) == 0 { + if len(notBeforeString) == 0 { return notBefore, notAfter, "missing notBefore" } notBefore, err = time.Parse(time.RFC3339, notBeforeString) diff --git a/vendor/github.com/openshift/library-go/pkg/operator/hypershift/deploymentversion/version_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/hypershift/deploymentversion/version_controller.go new file mode 100644 index 000000000..0284ea1f5 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/hypershift/deploymentversion/version_controller.go @@ -0,0 +1,173 @@ +package deploymentversioncontroller + +import ( + "context" + "fmt" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + appsinformersv1 "k8s.io/client-go/informers/apps/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + + operatorapi "github.com/openshift/api/operator/v1" + "github.com/openshift/library-go/pkg/controller/factory" + "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/status" + "github.com/openshift/library-go/pkg/operator/v1helpers" +) + +const ( + defaultReSyncPeriod = 10 * time.Minute + + // This annotation is rendered as soon as the operator publishes Deployment object on API server + desiredVersionAnnotation = "release.openshift.io/desired-version" + + // This annotation is rendered when the operator verified that the Deployment is up and running + versionAnnotation = "release.openshift.io/version" +) + +// This controller updates versionAnnotation of the Deployment specified by controllerDeploymentName +// with the current version of the operator, read from the "OPERATOR_IMAGE_VERSION" environment variable. +// The controller expects this environment variable to be set to a reasonable value. +// The controller checks whether a rollout of the Deployment with the current version is in progress +// and updates versionAnnotation only when the deployment has completed. +// The controller relies on `.status.conditions` with the `Progressing` type to +// ensure that the deployment has completed, as explained in the Kubernetes docs: +// https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#complete-deployment +// The controller expects the operator to publish the Deployment with desiredVersionAnnotation +// equal to the current version of the operator. The lack of this annotation is treated as an old +// Deployment from some previous version of the operator that did not use this controller. +// Currently, this controller is supposed to be used only for Deployments residing in HyperShift +// control planes. +type DeploymentVersionController struct { + name string + controlPlaneNamespace string + controllerDeploymentName string + managementClusterDeploymentInformer appsinformersv1.DeploymentInformer + guestClusterOperatorClient v1helpers.OperatorClientWithFinalizers + managementClusterKubeClient kubernetes.Interface + eventRecorder events.Recorder +} + +func NewDeploymentVersionController( + name string, + controlPlaneNamespace string, + controllerDeploymentName string, + managementClusterDeploymentInformer appsinformersv1.DeploymentInformer, + guestClusterOperatorClient v1helpers.OperatorClientWithFinalizers, + managementClusterKubeClient kubernetes.Interface, + eventRecorder events.Recorder) factory.Controller { + + c := &DeploymentVersionController{ + name: name, + controlPlaneNamespace: controlPlaneNamespace, + controllerDeploymentName: controllerDeploymentName, + managementClusterDeploymentInformer: managementClusterDeploymentInformer, + guestClusterOperatorClient: guestClusterOperatorClient, + managementClusterKubeClient: managementClusterKubeClient, + eventRecorder: eventRecorder, + } + return factory.New().WithSync( + c.Sync, + ).WithSyncDegradedOnError( + guestClusterOperatorClient, + ).WithInformers( + c.managementClusterDeploymentInformer.Informer(), + ).ResyncEvery( + defaultReSyncPeriod, + ).ToController( + name, + eventRecorder, + ) +} + +func hasFinishedProgressing(deployment *appsv1.Deployment) bool { + if deployment.Status.ObservedGeneration != deployment.Generation { + // The Deployment controller did not act on the Deployment spec change yet. + // Any condition in the status may be stale. + return false + } + // Deployment whose rollout is complete gets Progressing condition with Reason NewReplicaSetAvailable condition. + // https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#complete-deployment + for _, cond := range deployment.Status.Conditions { + if cond.Type == appsv1.DeploymentProgressing { + return cond.Status == corev1.ConditionTrue && cond.Reason == "NewReplicaSetAvailable" + } + } + return false +} + +func (c *DeploymentVersionController) Sync(ctx context.Context, syncCtx factory.SyncContext) error { + klog.V(4).Infof("DeploymentVersionController sync started") + defer klog.V(4).Infof("DeploymentVersionController sync finished") + + opSpec, _, _, err := c.guestClusterOperatorClient.GetOperatorState() + if err != nil { + return err + } + if opSpec.ManagementState != operatorapi.Managed { + return nil + } + + deployment, err := c.managementClusterDeploymentInformer.Lister().Deployments(c.controlPlaneNamespace).Get(c.controllerDeploymentName) + if err != nil { + return fmt.Errorf("could not get Deployment %s in Namespace %s: %w", c.controllerDeploymentName, c.controlPlaneNamespace, err) + } + + desiredVersion := deployment.Annotations[desiredVersionAnnotation] + actualVersion := deployment.Annotations[versionAnnotation] + + // This operator adds desiredVersionAnnotation annotation with its version for sure; if the + // version from Annotations is not the same as operator version, we look at some outdated + // deployment generated by another (previous) operator instance. + if desiredVersion != status.VersionForOperatorFromEnv() { + klog.V(4).Infof("DeploymentVersionController: desiredVersion mismatch: \"%s\" vs \"%s\"", desiredVersion, status.VersionForOperatorFromEnv()) + return nil + } + + // If versions from versionAnnotation and desiredVersionAnnotation annotations are equal, + // we have already populated these annotations before, do nothing now. + if actualVersion == desiredVersion { + klog.V(4).Infof("DeploymentVersionController: version \"%s\" is already populated, nothing to do", desiredVersion) + return nil + } + + if hasFinishedProgressing(deployment) { + updatedDeployment := setVersionAnnotation(deployment, desiredVersion) + err := c.updateDeployment(ctx, updatedDeployment) + if err != nil { + return err + } + klog.V(2).Infof("DeploymentVersionController: deployment updated with version %s", desiredVersion) + } else { + klog.V(4).Infof("DeploymentVersionController: deployment update is in progress ...") + } + return nil +} + +func (c *DeploymentVersionController) updateDeployment(ctx context.Context, deployment *appsv1.Deployment) error { + _, err := c.managementClusterKubeClient.AppsV1().Deployments(c.controlPlaneNamespace).Update(ctx, deployment, metav1.UpdateOptions{}) + if err != nil { + klog.Errorf("error updating deployment object %s in namespace %s: %v", deployment.Name, c.controlPlaneNamespace, err) + return err + } + return nil +} + +func setVersionAnnotation(deployment *appsv1.Deployment, version string) *appsv1.Deployment { + // Create a deep copy of the PersistentVolume to avoid modifying the cached object + deploymentCopy := deployment.DeepCopy() + + // Ensure the deployment has an annotations map + if deploymentCopy.Annotations == nil { + deploymentCopy.Annotations = make(map[string]string) + } + + // Set or update the tag hash annotation + deploymentCopy.Annotations[versionAnnotation] = version + + return deploymentCopy +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/status/condition.go b/vendor/github.com/openshift/library-go/pkg/operator/status/condition.go new file mode 100644 index 000000000..56e1496b8 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/status/condition.go @@ -0,0 +1,169 @@ +package status + +import ( + "fmt" + "sort" + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + configv1 "github.com/openshift/api/config/v1" + operatorv1 "github.com/openshift/api/operator/v1" +) + +// UnionCondition returns a single operator condition that is the union of multiple operator conditions. +// +// defaultConditionStatus indicates whether you want to merge all Falses or merge all Trues. For instance, Failures merge +// on true, but Available merges on false. Thing of it like an anti-default. +// +// If inertia is non-nil, then resist returning a condition with a status opposite the defaultConditionStatus. +func UnionCondition(conditionType string, defaultConditionStatus operatorv1.ConditionStatus, inertia Inertia, allConditions ...operatorv1.OperatorCondition) operatorv1.OperatorCondition { + var oppositeConditionStatus operatorv1.ConditionStatus + if defaultConditionStatus == operatorv1.ConditionTrue { + oppositeConditionStatus = operatorv1.ConditionFalse + } else { + oppositeConditionStatus = operatorv1.ConditionTrue + } + + interestingConditions := []operatorv1.OperatorCondition{} + badConditions := []operatorv1.OperatorCondition{} + badConditionStatus := operatorv1.ConditionUnknown + for _, condition := range allConditions { + if strings.HasSuffix(condition.Type, conditionType) { + interestingConditions = append(interestingConditions, condition) + + if condition.Status != defaultConditionStatus { + badConditions = append(badConditions, condition) + if condition.Status == oppositeConditionStatus { + badConditionStatus = oppositeConditionStatus + } + } + } + } + sort.Sort(byConditionType(badConditions)) + + unionedCondition := operatorv1.OperatorCondition{Type: conditionType, Status: operatorv1.ConditionUnknown} + if len(interestingConditions) == 0 { + unionedCondition.Status = operatorv1.ConditionUnknown + unionedCondition.Reason = "NoData" + return unionedCondition + } + + var elderBadConditions []operatorv1.OperatorCondition + if inertia == nil { + elderBadConditions = badConditions + } else { + now := time.Now() + for _, condition := range badConditions { + if condition.LastTransitionTime.Time.Before(now.Add(-inertia(condition))) { + elderBadConditions = append(elderBadConditions, condition) + } + } + } + + if len(elderBadConditions) == 0 { + unionedCondition.Status = defaultConditionStatus + unionedCondition.Message = unionMessage(interestingConditions) + if unionedCondition.Message == "" { + unionedCondition.Message = "All is well" + } + unionedCondition.Reason = "AsExpected" + unionedCondition.LastTransitionTime = latestTransitionTime(interestingConditions) + + return unionedCondition + } + + // at this point we have bad conditions + unionedCondition.Status = badConditionStatus + unionedCondition.Message = unionMessage(badConditions) + unionedCondition.Reason = unionReason(conditionType, badConditions) + unionedCondition.LastTransitionTime = latestTransitionTime(badConditions) + + return unionedCondition +} + +// UnionClusterCondition returns a single cluster operator condition that is the union of multiple operator conditions. +// +// defaultConditionStatus indicates whether you want to merge all Falses or merge all Trues. For instance, Failures merge +// on true, but Available merges on false. Thing of it like an anti-default. +// +// If inertia is non-nil, then resist returning a condition with a status opposite the defaultConditionStatus. +func UnionClusterCondition(conditionType configv1.ClusterStatusConditionType, defaultConditionStatus operatorv1.ConditionStatus, inertia Inertia, allConditions ...operatorv1.OperatorCondition) configv1.ClusterOperatorStatusCondition { + cnd := UnionCondition(string(conditionType), defaultConditionStatus, inertia, allConditions...) + return OperatorConditionToClusterOperatorCondition(cnd) +} + +func OperatorConditionToClusterOperatorCondition(condition operatorv1.OperatorCondition) configv1.ClusterOperatorStatusCondition { + return configv1.ClusterOperatorStatusCondition{ + Type: configv1.ClusterStatusConditionType(condition.Type), + Status: configv1.ConditionStatus(condition.Status), + LastTransitionTime: condition.LastTransitionTime, + Reason: condition.Reason, + Message: condition.Message, + } +} +func latestTransitionTime(conditions []operatorv1.OperatorCondition) metav1.Time { + latestTransitionTime := metav1.Time{} + for _, condition := range conditions { + if latestTransitionTime.Before(&condition.LastTransitionTime) { + latestTransitionTime = condition.LastTransitionTime + } + } + return latestTransitionTime +} + +func uniq(s []string) []string { + seen := make(map[string]struct{}, len(s)) + j := 0 + for _, v := range s { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + s[j] = v + j++ + } + return s[:j] +} + +func unionMessage(conditions []operatorv1.OperatorCondition) string { + messages := []string{} + for _, condition := range conditions { + if len(condition.Message) == 0 { + continue + } + for _, message := range uniq(strings.Split(condition.Message, "\n")) { + messages = append(messages, fmt.Sprintf("%s: %s", condition.Type, message)) + } + } + return strings.Join(messages, "\n") +} + +func unionReason(unionConditionType string, conditions []operatorv1.OperatorCondition) string { + typeReasons := []string{} + for _, curr := range conditions { + currType := curr.Type[:len(curr.Type)-len(unionConditionType)] + if len(curr.Reason) > 0 { + typeReasons = append(typeReasons, currType+"_"+curr.Reason) + } else { + typeReasons = append(typeReasons, currType) + } + } + sort.Strings(typeReasons) + return strings.Join(typeReasons, "::") +} + +type byConditionType []operatorv1.OperatorCondition + +var _ sort.Interface = byConditionType{} + +func (s byConditionType) Len() int { + return len(s) +} +func (s byConditionType) Less(i, j int) bool { + return s[i].Type < s[j].Type +} +func (s byConditionType) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/status/inertia.go b/vendor/github.com/openshift/library-go/pkg/operator/status/inertia.go new file mode 100644 index 000000000..3ac29082e --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/status/inertia.go @@ -0,0 +1,66 @@ +package status + +import ( + "fmt" + "regexp" + "time" + + operatorv1 "github.com/openshift/api/operator/v1" +) + +// Inertia returns the inertial duration for the given condition. +type Inertia func(condition operatorv1.OperatorCondition) time.Duration + +// InertiaCondition configures an inertia duration for a given set of +// condition types. +type InertiaCondition struct { + // ConditionTypeMatcher is a regular expression selecting condition types + // with which this InertiaCondition is associated. + ConditionTypeMatcher *regexp.Regexp + + // Duration is the inertial duration for associated conditions. + Duration time.Duration +} + +// InertiaConfig holds configuration for an Inertia implementation. +type InertiaConfig struct { + defaultDuration time.Duration + conditions []InertiaCondition +} + +// NewInertia creates a new InertiaConfig object. Conditions are +// applied in the given order, so a condition type matching multiple +// regular expressions will have the duration associated with the first +// matching entry. +func NewInertia(defaultDuration time.Duration, conditions ...InertiaCondition) (*InertiaConfig, error) { + for i, condition := range conditions { + if condition.ConditionTypeMatcher == nil { + return nil, fmt.Errorf("condition %d has a nil ConditionTypeMatcher", i) + } + } + + return &InertiaConfig{ + defaultDuration: defaultDuration, + conditions: conditions, + }, nil +} + +// MustNewInertia is like NewInertia but panics on error. +func MustNewInertia(defaultDuration time.Duration, conditions ...InertiaCondition) *InertiaConfig { + inertia, err := NewInertia(defaultDuration, conditions...) + if err != nil { + panic(err) + } + + return inertia +} + +// Inertia returns the configured inertia for the given condition type. +func (c *InertiaConfig) Inertia(condition operatorv1.OperatorCondition) time.Duration { + for _, matcher := range c.conditions { + if matcher.ConditionTypeMatcher.MatchString(condition.Type) { + return matcher.Duration + } + } + return c.defaultDuration +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/status/status_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/status/status_controller.go new file mode 100644 index 000000000..de348e797 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/status/status_controller.go @@ -0,0 +1,293 @@ +package status + +import ( + "context" + "strings" + "time" + + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/klog/v2" + "k8s.io/utils/clock" + + configv1 "github.com/openshift/api/config/v1" + operatorv1 "github.com/openshift/api/operator/v1" + configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1" + configv1informers "github.com/openshift/client-go/config/informers/externalversions/config/v1" + configv1listers "github.com/openshift/client-go/config/listers/config/v1" + configv1helpers "github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers" + "github.com/openshift/library-go/pkg/controller/factory" + "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/management" + "github.com/openshift/library-go/pkg/operator/resource/resourceapply" + operatorv1helpers "github.com/openshift/library-go/pkg/operator/v1helpers" +) + +type VersionGetter interface { + // SetVersion is a way to set the version for an operand. It must be thread-safe + SetVersion(operandName, version string) + // UnsetVersion removes a version for an operand if it exists; it is a no-op otherwise. It must be thread-safe + UnsetVersion(operandName string) + // GetVersion is way to get the versions for all operands. It must be thread-safe and return an object that doesn't mutate + GetVersions() map[string]string + // VersionChangedChannel is a channel that will get an item whenever SetVersion has been called + VersionChangedChannel() <-chan struct{} +} + +type RelatedObjectsFunc func() (isset bool, objs []configv1.ObjectReference) + +type StatusSyncer struct { + clusterOperatorName string + relatedObjects []configv1.ObjectReference + relatedObjectsFunc RelatedObjectsFunc + clock clock.PassiveClock + + versionGetter VersionGetter + operatorClient operatorv1helpers.OperatorClient + clusterOperatorClient configv1client.ClusterOperatorsGetter + clusterOperatorLister configv1listers.ClusterOperatorLister + + controllerFactory *factory.Factory + recorder events.Recorder + degradedInertia Inertia + + removeUnusedVersions bool +} + +var _ factory.Controller = &StatusSyncer{} + +func (c *StatusSyncer) Name() string { + return c.clusterOperatorName +} + +func NewClusterOperatorStatusController( + name string, + relatedObjects []configv1.ObjectReference, + clusterOperatorClient configv1client.ClusterOperatorsGetter, + clusterOperatorInformer configv1informers.ClusterOperatorInformer, + operatorClient operatorv1helpers.OperatorClient, + versionGetter VersionGetter, + recorder events.Recorder, + clock clock.PassiveClock, +) *StatusSyncer { + return &StatusSyncer{ + clusterOperatorName: name, + relatedObjects: relatedObjects, + clock: clock, + versionGetter: versionGetter, + clusterOperatorClient: clusterOperatorClient, + clusterOperatorLister: clusterOperatorInformer.Lister(), + operatorClient: operatorClient, + degradedInertia: MustNewInertia(2 * time.Minute).Inertia, + controllerFactory: factory.New().ResyncEvery(time.Minute).WithInformers( + operatorClient.Informer(), + clusterOperatorInformer.Informer(), + ), + recorder: recorder.WithComponentSuffix("status-controller"), + } +} + +// WithRelatedObjectsFunc allows the set of related objects to be dynamically +// determined. +// +// The function returns (isset, objects) +// +// If isset is false, then the set of related objects is copied over from the +// existing ClusterOperator object. This is useful in cases where an operator +// has just restarted, and hasn't yet reconciled. +// +// Any statically-defined related objects (in NewClusterOperatorStatusController) +// will always be included in the result. +func (c *StatusSyncer) WithRelatedObjectsFunc(f RelatedObjectsFunc) { + c.relatedObjectsFunc = f +} + +func (c *StatusSyncer) Run(ctx context.Context, workers int) { + c.controllerFactory. + WithPostStartHooks(c.watchVersionGetterPostRunHook). + WithSync(c.Sync). + ToController( + "StatusSyncer_"+c.Name(), // don't change what is passed here unless you also remove the old FooDegraded condition + c.recorder, + ). + Run(ctx, workers) +} + +// WithDegradedInertia returns a copy of the StatusSyncer with the +// requested inertia function for degraded conditions. +func (c *StatusSyncer) WithDegradedInertia(inertia Inertia) *StatusSyncer { + output := *c + output.degradedInertia = inertia + return &output +} + +// WithVersionRemoval returns a copy of the StatusSyncer that will +// remove versions that are missing in VersionGetter from the status. +func (c *StatusSyncer) WithVersionRemoval() *StatusSyncer { + output := *c + output.removeUnusedVersions = true + return &output +} + +// sync reacts to a change in prereqs by finding information that is required to match another value in the cluster. This +// must be information that is logically "owned" by another component. +func (c StatusSyncer) Sync(ctx context.Context, syncCtx factory.SyncContext) error { + detailedSpec, currentDetailedStatus, _, err := c.operatorClient.GetOperatorState() + if apierrors.IsNotFound(err) { + syncCtx.Recorder().Warningf("StatusNotFound", "Unable to determine current operator status for clusteroperator/%s", c.clusterOperatorName) + if err := c.clusterOperatorClient.ClusterOperators().Delete(ctx, c.clusterOperatorName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { + return err + } + return nil + } + if err != nil { + return err + } + + originalClusterOperatorObj, err := c.clusterOperatorLister.Get(c.clusterOperatorName) + if err != nil && !apierrors.IsNotFound(err) { + syncCtx.Recorder().Warningf("StatusFailed", "Unable to get current operator status for clusteroperator/%s: %v", c.clusterOperatorName, err) + return err + } + + // ensure that we have a clusteroperator resource + if originalClusterOperatorObj == nil || apierrors.IsNotFound(err) { + klog.Infof("clusteroperator/%s not found", c.clusterOperatorName) + var createErr error + originalClusterOperatorObj, createErr = c.clusterOperatorClient.ClusterOperators().Create(ctx, &configv1.ClusterOperator{ + ObjectMeta: metav1.ObjectMeta{Name: c.clusterOperatorName}, + }, metav1.CreateOptions{}) + if apierrors.IsNotFound(createErr) { + // this means that the API isn't present. We did not fail. Try again later + klog.Infof("ClusterOperator API not created") + syncCtx.Queue().AddRateLimited(factory.DefaultQueueKey) + return nil + } + if createErr != nil { + syncCtx.Recorder().Warningf("StatusCreateFailed", "Failed to create operator status: %v", createErr) + return createErr + } + } + clusterOperatorObj := originalClusterOperatorObj.DeepCopy() + + if detailedSpec.ManagementState == operatorv1.Unmanaged && !management.IsOperatorAlwaysManaged() { + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.OperatorAvailable, Status: configv1.ConditionUnknown, Reason: "Unmanaged"}, c.clock) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.OperatorProgressing, Status: configv1.ConditionUnknown, Reason: "Unmanaged"}, c.clock) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.OperatorDegraded, Status: configv1.ConditionUnknown, Reason: "Unmanaged"}, c.clock) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.OperatorUpgradeable, Status: configv1.ConditionUnknown, Reason: "Unmanaged"}, c.clock) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.EvaluationConditionsDetected, Status: configv1.ConditionUnknown, Reason: "Unmanaged"}, c.clock) + + if equality.Semantic.DeepEqual(clusterOperatorObj, originalClusterOperatorObj) { + return nil + } + if _, err := c.clusterOperatorClient.ClusterOperators().UpdateStatus(ctx, clusterOperatorObj, metav1.UpdateOptions{}); err != nil { + return err + } + if !skipOperatorStatusChangedEvent(originalClusterOperatorObj.Status, clusterOperatorObj.Status) { + syncCtx.Recorder().Eventf("OperatorStatusChanged", "Status for operator %s changed: %s", c.clusterOperatorName, configv1helpers.GetStatusDiff(originalClusterOperatorObj.Status, clusterOperatorObj.Status)) + } + return nil + } + + if c.relatedObjectsFunc != nil { + isSet, ro := c.relatedObjectsFunc() + if !isSet { // temporarily unknown - copy over from existing object + ro = clusterOperatorObj.Status.RelatedObjects + } + + // merge in any static objects + for _, obj := range c.relatedObjects { + found := false + for _, existingObj := range ro { + if obj == existingObj { + found = true + break + } + } + if !found { + ro = append(ro, obj) + } + } + clusterOperatorObj.Status.RelatedObjects = ro + } else { + clusterOperatorObj.Status.RelatedObjects = c.relatedObjects + } + + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.OperatorDegraded, operatorv1.ConditionFalse, c.degradedInertia, currentDetailedStatus.Conditions...), c.clock) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.OperatorProgressing, operatorv1.ConditionFalse, nil, currentDetailedStatus.Conditions...), c.clock) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.OperatorAvailable, operatorv1.ConditionTrue, nil, currentDetailedStatus.Conditions...), c.clock) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.OperatorUpgradeable, operatorv1.ConditionTrue, nil, currentDetailedStatus.Conditions...), c.clock) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.EvaluationConditionsDetected, operatorv1.ConditionFalse, nil, currentDetailedStatus.Conditions...), c.clock) + + c.syncStatusVersions(clusterOperatorObj, syncCtx) + + // if we have no diff, just return + if equality.Semantic.DeepEqual(clusterOperatorObj, originalClusterOperatorObj) { + return nil + } + klog.V(2).Infof("clusteroperator/%s diff %v", c.clusterOperatorName, resourceapply.JSONPatchNoError(originalClusterOperatorObj, clusterOperatorObj)) + + if _, updateErr := c.clusterOperatorClient.ClusterOperators().UpdateStatus(ctx, clusterOperatorObj, metav1.UpdateOptions{}); updateErr != nil { + return updateErr + } + if !skipOperatorStatusChangedEvent(originalClusterOperatorObj.Status, clusterOperatorObj.Status) { + syncCtx.Recorder().Eventf("OperatorStatusChanged", "Status for clusteroperator/%s changed: %s", c.clusterOperatorName, configv1helpers.GetStatusDiff(originalClusterOperatorObj.Status, clusterOperatorObj.Status)) + } + return nil +} + +func skipOperatorStatusChangedEvent(originalStatus, newStatus configv1.ClusterOperatorStatus) bool { + originalCopy := *originalStatus.DeepCopy() + for i, condition := range originalCopy.Conditions { + switch condition.Type { + case configv1.OperatorAvailable, configv1.OperatorDegraded, configv1.OperatorProgressing, configv1.OperatorUpgradeable: + originalCopy.Conditions[i].Message = strings.TrimPrefix(condition.Message, "\ufeff") + } + } + return len(configv1helpers.GetStatusDiff(originalCopy, newStatus)) == 0 +} + +func (c *StatusSyncer) syncStatusVersions(clusterOperatorObj *configv1.ClusterOperator, syncCtx factory.SyncContext) { + versions := c.versionGetter.GetVersions() + // Add new versions from versionGetter to status + for operand, version := range versions { + previousVersion := operatorv1helpers.SetOperandVersion(&clusterOperatorObj.Status.Versions, configv1.OperandVersion{Name: operand, Version: version}) + if previousVersion != version { + // having this message will give us a marker in events when the operator updated compared to when the operand is updated + syncCtx.Recorder().Eventf("OperatorVersionChanged", "clusteroperator/%s version %q changed from %q to %q", c.clusterOperatorName, operand, previousVersion, version) + } + } + + if !c.removeUnusedVersions { + return + } + + // Filter out all versions from status that are not in versionGetter + filteredVersions := make([]configv1.OperandVersion, 0, len(clusterOperatorObj.Status.Versions)) + for _, version := range clusterOperatorObj.Status.Versions { + if _, found := versions[version.Name]; found { + filteredVersions = append(filteredVersions, version) + } + } + + clusterOperatorObj.Status.Versions = filteredVersions +} + +func (c *StatusSyncer) watchVersionGetterPostRunHook(ctx context.Context, syncCtx factory.SyncContext) error { + defer utilruntime.HandleCrash() + + versionCh := c.versionGetter.VersionChangedChannel() + // always kick at least once + syncCtx.Queue().Add(factory.DefaultQueueKey) + + for { + select { + case <-ctx.Done(): + return nil + case <-versionCh: + syncCtx.Queue().Add(factory.DefaultQueueKey) + } + } +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/status/version.go b/vendor/github.com/openshift/library-go/pkg/operator/status/version.go new file mode 100644 index 000000000..327bfa754 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/status/version.go @@ -0,0 +1,112 @@ +package status + +import ( + "context" + "os" + "sync" + + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/openshift/library-go/pkg/operator/events" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type versionGetter struct { + lock sync.Mutex + versions map[string]string + notificationChannels []chan struct{} +} + +const ( + operandImageEnvVarName = "IMAGE" + operandImageVersionEnvVarName = "OPERAND_IMAGE_VERSION" + operatorImageVersionEnvVarName = "OPERATOR_IMAGE_VERSION" +) + +func NewVersionGetter() VersionGetter { + return &versionGetter{ + versions: map[string]string{}, + } +} + +func (v *versionGetter) SetVersion(operandName, version string) { + v.lock.Lock() + defer v.lock.Unlock() + + v.versions[operandName] = version + v.notifyChannelsLocked() +} + +func (v *versionGetter) UnsetVersion(operandName string) { + v.lock.Lock() + defer v.lock.Unlock() + + if _, exists := v.versions[operandName]; !exists { + return + } + + delete(v.versions, operandName) + v.notifyChannelsLocked() +} + +func (v *versionGetter) GetVersions() map[string]string { + v.lock.Lock() + defer v.lock.Unlock() + + ret := map[string]string{} + for k, v := range v.versions { + ret[k] = v + } + return ret +} + +func (v *versionGetter) VersionChangedChannel() <-chan struct{} { + v.lock.Lock() + defer v.lock.Unlock() + + channel := make(chan struct{}, 50) + v.notificationChannels = append(v.notificationChannels, channel) + return channel +} + +// notifyChannelsLocked must be called under a locked v.lock +func (v *versionGetter) notifyChannelsLocked() { + for i := range v.notificationChannels { + ch := v.notificationChannels[i] + // don't let a slow consumer block the rest + go func() { + ch <- struct{}{} + }() + } +} + +func ImageForOperandFromEnv() string { + return os.Getenv(operandImageEnvVarName) +} + +func VersionForOperandFromEnv() string { + return os.Getenv(operandImageVersionEnvVarName) +} + +func VersionForOperatorFromEnv() string { + return os.Getenv(operatorImageVersionEnvVarName) +} + +func VersionForOperand(namespace, imagePullSpec string, configMapGetter corev1client.ConfigMapsGetter, eventRecorder events.Recorder) string { + versionMap := map[string]string{} + versionMapping, err := configMapGetter.ConfigMaps(namespace).Get(context.TODO(), "version-mapping", metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + eventRecorder.Warningf("VersionMappingFailure", "unable to get version mapping: %v", err) + return "" + } + if versionMapping != nil { + for version, image := range versionMapping.Data { + versionMap[image] = version + } + } + + // we have the actual daemonset and we need the pull spec + operandVersion := versionMap[imagePullSpec] + return operandVersion +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 97ea2bd94..964ae4939 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -475,12 +475,13 @@ github.com/openshift/client-go/operator/listers/operator/v1alpha1 github.com/openshift/hypershift/api/hypershift/v1beta1 github.com/openshift/hypershift/api/ibmcapi github.com/openshift/hypershift/api/util/ipnet -# github.com/openshift/library-go v0.0.0-20251112091634-ab97ebb73f0f +# github.com/openshift/library-go v0.0.0-20251222131241-289839b3ffe8 ## explicit; go 1.24.0 github.com/openshift/library-go/pkg/apiserver/jsonpatch github.com/openshift/library-go/pkg/authorization/hardcodedauthorizer github.com/openshift/library-go/pkg/certs github.com/openshift/library-go/pkg/config/client +github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers github.com/openshift/library-go/pkg/config/clusterstatus github.com/openshift/library-go/pkg/config/configdefaults github.com/openshift/library-go/pkg/config/leaderelection @@ -505,6 +506,7 @@ github.com/openshift/library-go/pkg/operator/csi/csistorageclasscontroller github.com/openshift/library-go/pkg/operator/deploymentcontroller github.com/openshift/library-go/pkg/operator/events github.com/openshift/library-go/pkg/operator/genericoperatorclient +github.com/openshift/library-go/pkg/operator/hypershift/deploymentversion github.com/openshift/library-go/pkg/operator/loglevel github.com/openshift/library-go/pkg/operator/management github.com/openshift/library-go/pkg/operator/managementstatecontroller @@ -516,6 +518,7 @@ github.com/openshift/library-go/pkg/operator/resource/resourceread github.com/openshift/library-go/pkg/operator/resourcesynccontroller github.com/openshift/library-go/pkg/operator/staleconditions github.com/openshift/library-go/pkg/operator/staticresourcecontroller +github.com/openshift/library-go/pkg/operator/status github.com/openshift/library-go/pkg/operator/v1helpers github.com/openshift/library-go/pkg/serviceability # github.com/pkg/profile v1.7.0 @@ -679,7 +682,7 @@ go.yaml.in/yaml/v2 # go.yaml.in/yaml/v3 v3.0.4 ## explicit; go 1.16 go.yaml.in/yaml/v3 -# golang.org/x/crypto v0.44.0 +# golang.org/x/crypto v0.45.0 ## explicit; go 1.24.0 golang.org/x/crypto/cryptobyte golang.org/x/crypto/cryptobyte/asn1