diff --git a/control-plane-operator/hostedclusterconfigoperator/cmd.go b/control-plane-operator/hostedclusterconfigoperator/cmd.go index 32409ca40f..f0ea47c869 100644 --- a/control-plane-operator/hostedclusterconfigoperator/cmd.go +++ b/control-plane-operator/hostedclusterconfigoperator/cmd.go @@ -23,6 +23,7 @@ import ( "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/configmetrics" "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/cmca" "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/drainer" + "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/featuregate" "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/hcpstatus" "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/inplaceupgrader" "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/machine" @@ -36,14 +37,12 @@ import ( "github.com/openshift/hypershift/support/releaseinfo" "github.com/openshift/hypershift/support/upsert" "github.com/openshift/hypershift/support/util" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/spf13/cobra" "go.uber.org/zap/zapcore" - "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/cluster" "sigs.k8s.io/controller-runtime/pkg/log/zap" ) @@ -58,14 +57,15 @@ func NewCommand() *cobra.Command { } var controllerFuncs = map[string]operator.ControllerSetupFunc{ - "controller-manager-ca": cmca.Setup, - resources.ControllerName: resources.Setup, - "inplaceupgrader": inplaceupgrader.Setup, - "node": node.Setup, - nodecount.ControllerName: nodecount.Setup, - "machine": machine.Setup, - "drainer": drainer.Setup, - hcpstatus.ControllerName: hcpstatus.Setup, + "controller-manager-ca": cmca.Setup, + resources.ControllerName: resources.Setup, + "inplaceupgrader": inplaceupgrader.Setup, + "node": node.Setup, + nodecount.ControllerName: nodecount.Setup, + featuregate.ControllerName: featuregate.Setup, + "machine": machine.Setup, + "drainer": drainer.Setup, + hcpstatus.ControllerName: hcpstatus.Setup, } type HostedClusterConfigOperator struct { diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/featuregate/controller.go b/control-plane-operator/hostedclusterconfigoperator/controllers/featuregate/controller.go new file mode 100644 index 0000000000..6d43465a32 --- /dev/null +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/featuregate/controller.go @@ -0,0 +1,81 @@ +package featuregate + +import ( + "context" + "fmt" + + "github.com/blang/semver/v4" + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "github.com/openshift/hypershift/support/util" + nodelib "github.com/openshift/library-go/pkg/apiserver/node" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + hypershiftv1beta1applyconfigurations "github.com/openshift/hypershift/client/applyconfiguration/hypershift/v1beta1" + hypershiftclient "github.com/openshift/hypershift/client/clientset/clientset" +) + +type minimumKubeletVersionReconciler struct { + hcpName, hcpNamespace string + client hypershiftclient.Interface + lister client.Client + + guestClusterClient client.Client +} + +func (r *minimumKubeletVersionReconciler) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) { + log := ctrl.LoggerFrom(ctx) + log.Info("Reconciling") + + var hcp hypershiftv1beta1.HostedControlPlane + if err := r.lister.Get(ctx, client.ObjectKey{ + Namespace: r.hcpNamespace, + Name: r.hcpName, + }, &hcp); err != nil { + return reconcile.Result{}, err + } + if isPaused, duration := util.IsReconciliationPaused(log, hcp.Spec.PausedUntil); isPaused { + log.Info("Reconciliation paused", "pausedUntil", *hcp.Spec.PausedUntil) + return ctrl.Result{ + RequeueAfter: duration, + }, nil + } + if hcp.ObjectMeta.DeletionTimestamp != nil { + return reconcile.Result{}, nil + } + + nodes := &corev1.NodeList{} + if err := r.guestClusterClient.List(ctx, nodes); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get Nodes: %w", err) + } + + currentOldestKubelet := getOldestKubeletVersion(nodes.Items) + if currentOldestKubelet == nil { + // no valid nodes, leave the field unset + return reconcile.Result{}, nil + } + + cfg := hypershiftv1beta1applyconfigurations.HostedControlPlane(r.hcpName, r.hcpNamespace) + cfg.Status = hypershiftv1beta1applyconfigurations.HostedControlPlaneStatus().WithOldestKubeletVersion(currentOldestKubelet.String()) + _, err := r.client.HypershiftV1beta1().HostedControlPlanes(r.hcpNamespace).ApplyStatus(ctx, cfg, metav1.ApplyOptions{FieldManager: ControllerName}) + return reconcile.Result{}, err +} + +func getOldestKubeletVersion(nodes []corev1.Node) *semver.Version { + var oldestVersion *semver.Version + for _, node := range nodes { + vStr := node.Status.NodeInfo.KubeletVersion + v, err := nodelib.ParseKubeletVersion(vStr) + if err != nil { + continue + } + if oldestVersion == nil || v.LT(*oldestVersion) { + oldestVersion = v + } + } + return oldestVersion +} diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/featuregate/setup.go b/control-plane-operator/hostedclusterconfigoperator/controllers/featuregate/setup.go new file mode 100644 index 0000000000..a949dee707 --- /dev/null +++ b/control-plane-operator/hostedclusterconfigoperator/controllers/featuregate/setup.go @@ -0,0 +1,41 @@ +package featuregate + +import ( + "context" + "time" + + hypershiftclient "github.com/openshift/hypershift/client/clientset/clientset" + "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/operator" + featuregate "github.com/openshift/hypershift/hypershift-operator/featuregate" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ControllerName = "featuregate" + +func Setup(ctx context.Context, opts *operator.HostedClusterConfigOperatorConfig) error { + if !featuregate.Gates.Enabled(featuregate.MinimumKubeletVersion) { + return nil + } + + hypershiftClient, err := hypershiftclient.NewForConfig(opts.CPCluster.GetConfig()) + if err != nil { + return err + } + + return ctrl.NewControllerManagedBy(opts.Manager). + Named(ControllerName). + For(&corev1.Node{}). + WithOptions(controller.Options{ + RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](1*time.Second, 10*time.Second), + }).Complete(&minimumKubeletVersionReconciler{ + hcpName: opts.HCPName, + hcpNamespace: opts.Namespace, + client: hypershiftClient, + lister: opts.CPCluster.GetClient(), + guestClusterClient: opts.Manager.GetClient(), + }) +}