diff --git a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go index a680bae52b8..4419f20d50e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go +++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go @@ -900,7 +900,9 @@ func (r *HostedControlPlaneReconciler) reconcile(ctx context.Context, hostedCont if err := ignitionserver.ReconcileIgnitionServer(ctx, r.Client, createOrUpdate, + releaseImageProvider.Version(), releaseImageProvider.GetImage(util.CPOImageName), + releaseImageProvider.GetImage("cluster-config-operator"), hostedControlPlane, r.DefaultIngressDomain, // The healthz handler was added before the CPO started to manage the ignition server, and it's the same binary, diff --git a/control-plane-operator/controllers/hostedcontrolplane/ignitionserver/ignitionserver.go b/control-plane-operator/controllers/hostedcontrolplane/ignitionserver/ignitionserver.go index 984952ce067..30bc2d1df7e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/ignitionserver/ignitionserver.go +++ b/control-plane-operator/controllers/hostedcontrolplane/ignitionserver/ignitionserver.go @@ -1,11 +1,14 @@ package ignitionserver import ( + "bytes" "context" "fmt" "net" + configv1 "github.com/openshift/api/config/v1" routev1 "github.com/openshift/api/route/v1" + "github.com/openshift/hypershift/api" hyperv1 "github.com/openshift/hypershift/api/v1beta1" "github.com/openshift/hypershift/hypershift-operator/controllers/manifests/controlplaneoperator" "github.com/openshift/hypershift/hypershift-operator/controllers/manifests/ignitionserver" @@ -29,7 +32,9 @@ import ( func ReconcileIgnitionServer(ctx context.Context, c client.Client, createOrUpdate upsert.CreateOrUpdateFN, + releaseVersion string, utilitiesImage string, + configOperatorImage string, hcp *hyperv1.HostedControlPlane, defaultIngressDomain string, hasHealthzHandler bool, @@ -252,6 +257,26 @@ func ReconcileIgnitionServer(ctx context.Context, } } + // Use the default FeatureSet unless otherwise specified. + featureGate := &configv1.FeatureGate{ + TypeMeta: metav1.TypeMeta{ + Kind: "FeatureGate", + APIVersion: configv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + } + if hcp.Spec.Configuration != nil && hcp.Spec.Configuration.FeatureGate != nil { + featureGate.Spec = *hcp.Spec.Configuration.FeatureGate + } + + featureGateBuffer := &bytes.Buffer{} + if err := api.YamlSerializer.Encode(featureGate, featureGateBuffer); err != nil { + return fmt.Errorf("failed to encode feature gates: %w", err) + } + featureGateYAML := featureGateBuffer.String() + // Reconcile deployment ignitionServerDeployment := ignitionserver.Deployment(controlPlaneNamespace) if result, err := createOrUpdate(ctx, c, ignitionServerDeployment, func() error { @@ -295,6 +320,38 @@ func ReconcileIgnitionServer(ctx context.Context, EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + { + Name: "shared", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "fetch-feature-gate", + Image: configOperatorImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{ + "/bin/bash", + }, + Args: []string{ + "-c", + invokeFeatureGateRenderScript("/shared", releaseVersion, string(featureGateYAML)), + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("40Mi"), + corev1.ResourceCPU: resource.MustParse("10m"), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "shared", + MountPath: "/shared", + }, + }, + }, }, Containers: []corev1.Container{ { @@ -322,6 +379,7 @@ func ReconcileIgnitionServer(ctx context.Context, "--key-file", "/var/run/secrets/ignition/serving-cert/tls.key", "--registry-overrides", util.ConvertRegistryOverridesToCommandLineFlag(registryOverrides), "--platform", string(hcp.Spec.Platform.Type), + "--feature-gate=/shared/99_feature-gate.yaml", }, LivenessProbe: &corev1.Probe{ ProbeHandler: probeHandler, @@ -364,6 +422,10 @@ func ReconcileIgnitionServer(ctx context.Context, Name: "payloads", MountPath: "/payloads", }, + { + Name: "shared", + MountPath: "/shared", + }, }, }, }, @@ -459,3 +521,25 @@ func reconcileInternalRoute(route *routev1.Route, ownerRef config.OwnerRef) erro // Assumes ownerRef is the HCP return util.ReconcileInternalRoute(route, ownerRef.Reference.Name, ignitionserver.Service(route.Namespace).Name) } + +func invokeFeatureGateRenderScript(workDir, payloadVersion, featureGateYAML string) string { + var script = `#!/bin/bash +set -e +cd /tmp +mkdir input output manifests + +touch /tmp/manifests/99_feature-gate.yaml +cat </tmp/manifests/99_feature-gate.yaml +%[3]s +EOF + +/usr/bin/cluster-config-operator render \ + --config-output-file config \ + --asset-input-dir /tmp/input \ + --asset-output-dir /tmp/output \ + --rendered-manifest-files=/tmp/manifests \ + --payload-version=%[2]s +cp /tmp/manifests/99_feature-gate.yaml %[1]s/99_feature-gate.yaml +` + return fmt.Sprintf(script, workDir, payloadVersion, featureGateYAML) +} diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go index 9e3799e5960..e0dacf4f76a 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go @@ -1500,10 +1500,23 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques // Reconcile the Ignition server if !controlplaneOperatorManagesIgnitionServer { + configOperatorImage, releaseVersion, err := func() (string, string, error) { + releaseInfo, err := r.ReleaseProvider.Lookup(ctx, hcluster.Spec.Release.Image, pullSecretBytes) + if err != nil { + return "", "", fmt.Errorf("failed to lookup release image: %w", err) + } + return releaseInfo.ComponentImages()["cluster-config-operator"], releaseInfo.Version(), nil + }() + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get cli image: %w", err) + } + if err := ignitionserverreconciliation.ReconcileIgnitionServer(ctx, r.Client, createOrUpdate, + releaseVersion, utilitiesImage, + configOperatorImage, hcp, defaultIngressDomain, ignitionServerHasHealthzHandler, diff --git a/ignition-server/cmd/run_local_ignitionprovider.go b/ignition-server/cmd/run_local_ignitionprovider.go index 9dd74488d05..2d494ad9a4d 100644 --- a/ignition-server/cmd/run_local_ignitionprovider.go +++ b/ignition-server/cmd/run_local_ignitionprovider.go @@ -21,10 +21,11 @@ import ( ) type RunLocalIgnitionProviderOptions struct { - Namespace string - Image string - TokenSecret string - WorkDir string + Namespace string + Image string + TokenSecret string + WorkDir string + FeatureGateManifest string } func NewRunLocalIgnitionProviderCommand() *cobra.Command { @@ -39,6 +40,7 @@ func NewRunLocalIgnitionProviderCommand() *cobra.Command { cmd.Flags().StringVar(&opts.Image, "image", opts.Image, "Release image") cmd.Flags().StringVar(&opts.TokenSecret, "token-secret", opts.TokenSecret, "Token secret name") cmd.Flags().StringVar(&opts.WorkDir, "dir", opts.WorkDir, "Working directory (default: temporary dir)") + cmd.Flags().StringVar(&opts.FeatureGateManifest, "feature-gate-manifest", opts.FeatureGateManifest, "Path to a rendered featuregates.config.openshift.io/v1 manifest") cmd.RunE = func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(context.Background()) @@ -94,13 +96,14 @@ func (o *RunLocalIgnitionProviderOptions) Run(ctx context.Context) error { } p := &controllers.LocalIgnitionProvider{ - Client: cl, - ReleaseProvider: &releaseinfo.RegistryClientProvider{}, - CloudProvider: "", - Namespace: o.Namespace, - WorkDir: o.WorkDir, - PreserveOutput: true, - ImageFileCache: imageFileCache, + Client: cl, + ReleaseProvider: &releaseinfo.RegistryClientProvider{}, + CloudProvider: "", + Namespace: o.Namespace, + WorkDir: o.WorkDir, + PreserveOutput: true, + ImageFileCache: imageFileCache, + FeatureGateManifest: o.FeatureGateManifest, } payload, err := p.GetPayload(ctx, o.Image, config.String(), "") diff --git a/ignition-server/cmd/start.go b/ignition-server/cmd/start.go index 5df69a86a78..124fd6530f7 100644 --- a/ignition-server/cmd/start.go +++ b/ignition-server/cmd/start.go @@ -56,6 +56,7 @@ type Options struct { Platform string WorkDir string MetricsAddr string + FeatureGate string } // This is a https server that enable us to satisfy @@ -88,6 +89,7 @@ func NewStartCommand() *cobra.Command { cmd.Flags().StringVar(&opts.Platform, "platform", "", "The cloud provider platform name") cmd.Flags().StringVar(&opts.WorkDir, "work-dir", opts.WorkDir, "Directory in which to store transient working data") cmd.Flags().StringVar(&opts.MetricsAddr, "metrics-addr", opts.MetricsAddr, "The address the metric endpoint binds to.") + cmd.Flags().StringVar(&opts.FeatureGate, "feature-gate", opts.FeatureGate, "Path to a rendered featuregates.config.openshift.io/v1 file") cmd.Run = func(cmd *cobra.Command, args []string) { ctx, cancel := context.WithCancel(context.Background()) @@ -110,7 +112,7 @@ func NewStartCommand() *cobra.Command { // setUpPayloadStoreReconciler sets up manager with a TokenSecretReconciler controller // to keep the PayloadStore up to date. -func setUpPayloadStoreReconciler(ctx context.Context, registryOverrides map[string]string, cloudProvider hyperv1.PlatformType, cacheDir string, metricsAddr string) (ctrl.Manager, error) { +func setUpPayloadStoreReconciler(ctx context.Context, registryOverrides map[string]string, cloudProvider hyperv1.PlatformType, cacheDir string, metricsAddr string, featureGateManifest string) (ctrl.Manager, error) { if os.Getenv(namespaceEnvVariableName) == "" { return nil, fmt.Errorf("environment variable %s is empty, this is not supported", namespaceEnvVariableName) } @@ -153,11 +155,12 @@ func setUpPayloadStoreReconciler(ctx context.Context, registryOverrides map[stri }, OpenShiftImageRegistryOverrides: util.ConvertImageRegistryOverrideStringToMap(os.Getenv("OPENSHIFT_IMG_OVERRIDES")), }, - Client: mgr.GetClient(), - Namespace: os.Getenv(namespaceEnvVariableName), - CloudProvider: cloudProvider, - WorkDir: cacheDir, - ImageFileCache: imageFileCache, + Client: mgr.GetClient(), + Namespace: os.Getenv(namespaceEnvVariableName), + CloudProvider: cloudProvider, + WorkDir: cacheDir, + ImageFileCache: imageFileCache, + FeatureGateManifest: featureGateManifest, }, }).SetupWithManager(ctx, mgr); err != nil { return nil, fmt.Errorf("unable to create controller: %w", err) @@ -178,7 +181,7 @@ func run(ctx context.Context, opts Options) error { return fmt.Errorf("failed to load serving cert: %w", err) } - mgr, err := setUpPayloadStoreReconciler(ctx, opts.RegistryOverrides, hyperv1.PlatformType(opts.Platform), opts.WorkDir, opts.MetricsAddr) + mgr, err := setUpPayloadStoreReconciler(ctx, opts.RegistryOverrides, hyperv1.PlatformType(opts.Platform), opts.WorkDir, opts.MetricsAddr, opts.FeatureGate) if err != nil { return fmt.Errorf("error setting up manager: %w", err) } diff --git a/ignition-server/controllers/local_ignitionprovider.go b/ignition-server/controllers/local_ignitionprovider.go index 256425ba36b..0f0620012a1 100644 --- a/ignition-server/controllers/local_ignitionprovider.go +++ b/ignition-server/controllers/local_ignitionprovider.go @@ -66,6 +66,11 @@ type LocalIgnitionProvider struct { // deleted after use. PreserveOutput bool + // FeatureGateManifest is the path to a rendered feature gate manifest. + // This must be copied into the MCC directory as it is required + // to render the ignition payload. + FeatureGateManifest string + ImageFileCache *imageFileCache lock sync.Mutex @@ -235,6 +240,23 @@ func (p *LocalIgnitionProvider) GetPayload(ctx context.Context, releaseImage str return nil, fmt.Errorf("failed to extract templates from image: %w", err) } + // Write out the feature gate manifest to the MCC dir. + // Use the feature gate from the hosted control plane which should reflect the feature gate of the cluster. + if err := func() error { + featureGateBytes, err := os.ReadFile(p.FeatureGateManifest) + if err != nil { + return fmt.Errorf("failed to read feature gate: %w", err) + } + + if err := os.WriteFile(filepath.Join(mccBaseDir, "99_feature-gate.yaml"), featureGateBytes, 0644); err != nil { + return fmt.Errorf("failed to write feature gate: %w", err) + } + + return nil + }(); err != nil { + return nil, fmt.Errorf("failed to extract feature gate: %w", err) + } + // Extract binaries from the MCO image into the bin directory err = func() error { start := time.Now()