diff --git a/config/samples/featuregates.yaml b/config/samples/featuregates.yaml index 79e83d92..eb8e1c8c 100644 --- a/config/samples/featuregates.yaml +++ b/config/samples/featuregates.yaml @@ -4,7 +4,6 @@ metadata: name: osc-feature-gates namespace: openshift-sandboxed-containers-operator data: - # timeTravel allows navigating through cluster states across time. - # It is useful for scenarios where you want to view historical data or - # predict future states based on current trends. Default is "false". - # timeTravel: "false" + # layeredImageDeployment allows deploying Kata using RHCOS layered image + # This feature gate needs a ConfigMap named layered-image-deploy-cm + layeredImageDeployment: "false" diff --git a/config/samples/layered-image-deploy-cm.yaml b/config/samples/layered-image-deploy-cm.yaml new file mode 100644 index 00000000..a7f33ffc --- /dev/null +++ b/config/samples/layered-image-deploy-cm.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +data: + osImageURL: "quay.io/openshift_sandboxed_containers/kata-ocp415:tdx" + kernelArguments: "kvm_intel.tdx=1" + +kind: ConfigMap +metadata: + name: layered-image-deploy-cm + namespace: openshift-sandboxed-containers-operator diff --git a/controllers/fg_handler.go b/controllers/fg_handler.go index c6b83747..fffeb2f8 100644 --- a/controllers/fg_handler.go +++ b/controllers/fg_handler.go @@ -11,10 +11,12 @@ import ( const ( FgConfigMapName = "osc-feature-gates" ConfidentialFeatureGate = "confidential" + LayeredImageDeployment = "layeredImageDeployment" ) var DefaultFeatureGates = map[string]bool{ ConfidentialFeatureGate: false, + LayeredImageDeployment: false, } type FeatureGateStatus struct { @@ -95,6 +97,15 @@ func (r *KataConfigOpenShiftReconciler) processFeatureGates() error { } } - return err + // Check layered Image deployment FG + if IsEnabled(fgStatus, LayeredImageDeployment) { + r.Log.Info("Feature gate is enabled", "featuregate", LayeredImageDeployment) + // Perform the necessary actions + return r.handleLayeredImageDeploymentFeature(Enabled) + } else { + r.Log.Info("Feature gate is disabled", "featuregate", LayeredImageDeployment) + // Perform the necessary actions + return r.handleLayeredImageDeploymentFeature(Disabled) + } } diff --git a/controllers/layered_image_handler.go b/controllers/layered_image_handler.go new file mode 100644 index 00000000..d512ea0a --- /dev/null +++ b/controllers/layered_image_handler.go @@ -0,0 +1,164 @@ +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + ignTypes "github.com/coreos/ignition/v2/config/v3_2/types" + mcfgv1 "github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +const ( + LayeredImageDeployCm = "layered-image-deploy-cm" + image_mc_name = "50-enable-sandboxed-containers-image" +) + +// Process the LayeredImageDeployment feature gate (FG) +// This method will be called by the processFeatureGates method during the beginning of the reconcile loop +// The method will check for any existing MachineConfig related to KataConfig and return to the caller +// by setting r.ImgMc to the image MachineConfig if present. This will allow the remainder of the reconcile loop +// to use the image MachineConfig as needed. +// If neither extension or image MachineConfig exists, and layeredImageDeployment feature is enabled, +// then this method will create the image MachineConfig from the fg ConfigMap and set r.ImgMc to the +// newly created image MachineConfig. +// If layeredImageDeployment feature is disabled, then the method will reset r.ImgMc to nil +// The key design aspect of this FG is that it has effect only during the creation of the KataConfig. +// After creation of the KataConfig this FG has no effect. +func (r *KataConfigOpenShiftReconciler) handleLayeredImageDeploymentFeature(state FeatureGateState) error { + + // Check if MachineConfig exists and return the same without changing anything + mc, err := r.getExistingMachineConfig() + if err != nil { + r.Log.Info("Error in getting existing MachineConfig", "err", err) + return err + } + + if mc != nil { + r.Log.Info("MachineConfig is already present. No changes will be done") + // If the MachineConfig is imageMachineConfig, then set r.ImgMc to the same + if mc.Name == image_mc_name { + r.ImgMc = mc + } + return nil + } + + if state == Enabled { + r.Log.Info("LayeredImageDeployment feature is enabled") + + cm := &corev1.ConfigMap{} + err := r.Client.Get(context.Background(), types.NamespacedName{ + Name: LayeredImageDeployCm, + Namespace: OperatorNamespace, + }, cm) + if err != nil { + r.Log.Info("Error in retrieving LayeredImageDeployment ConfigMap", "err", err) + return err + } + + // Set the ImgMc here + r.ImgMc, err = r.createMachineConfigFromConfigMap(cm) + if err != nil { + r.Log.Info("Error in creating MachineConfig for LayeredImageDeployment from ConfigMap", "err", err) + return err + } + + } else { + r.Log.Info("LayeredImageDeployment feature is disabled. Resetting ImgMc") + // Reset ImgMc + r.ImgMc = nil + + } + + return nil +} + +func (r *KataConfigOpenShiftReconciler) getExistingMachineConfig() (*mcfgv1.MachineConfig, error) { + r.Log.Info("Getting any existing MachineConfigs related to KataConfig") + + // Retrieve the existing MachineConfig for Kata - either extension or image + // Check for label "app":r.kataConfig.Name + // and name "50-enable-sandboxed-containers-extension" or name "50-enable-sandboxed-containers-image" + mcList := &mcfgv1.MachineConfigList{} + err := r.Client.List(context.Background(), mcList) + if err != nil { + r.Log.Info("Error in listing MachineConfigs", "err", err) + return nil, err + } + + for _, mc := range mcList.Items { + if mc.Labels["app"] == r.kataConfig.Name && + (mc.Name == extension_mc_name || mc.Name == image_mc_name) { + return &mc, nil + } + } + + r.Log.Info("No existing MachineConfigs related to KataConfig found") + + return nil, nil +} + +// Method to create a new MachineConfig object from configMap data +// The configMap data will have two keys: "osImageURL" and "kernelArgs" +func (r *KataConfigOpenShiftReconciler) createMachineConfigFromConfigMap(cm *corev1.ConfigMap) (*mcfgv1.MachineConfig, error) { + + // Get the osImageURL from the ConfigMap + // osImageURL is mandatory for creating a MachineConfig + osImageURL, exists := cm.Data["osImageURL"] + if !exists { + return nil, fmt.Errorf("osImageURL not found in ConfigMap") + } + + ic := ignTypes.Config{ + Ignition: ignTypes.Ignition{ + Version: "3.2.0", + }, + } + + icb, err := json.Marshal(ic) + if err != nil { + return nil, err + } + mc := &mcfgv1.MachineConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "machineconfiguration.openshift.io/v1", + Kind: "MachineConfig", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: image_mc_name, + Namespace: OperatorNamespace, + }, + Spec: mcfgv1.MachineConfigSpec{ + Config: runtime.RawExtension{ + Raw: icb, + }, + OSImageURL: osImageURL, + }, + } + + if kernelArguments, ok := cm.Data["kernelArguments"]; ok { + // Parse the kernel arguments and set them in the MachineConfig + // Note that in the configmap the kernel arguments are stored as a single string + // eg. "a=b c=d ..." and we need to split them into individual arguments + // eg ["a=b", "c=d", ...] + // Split the kernel arguments string into individual arguments + mc.Spec.KernelArguments = strings.Fields(kernelArguments) + } + + // Set the required labels + mcp, err := r.getMcpName() + if err != nil { + return nil, err + } + mc.Labels = map[string]string{ + "machineconfiguration.openshift.io/role": mcp, + "app": r.kataConfig.Name, + } + + return mc, nil +} diff --git a/controllers/openshift_controller.go b/controllers/openshift_controller.go index 2d23834c..11ee5051 100644 --- a/controllers/openshift_controller.go +++ b/controllers/openshift_controller.go @@ -61,6 +61,8 @@ type KataConfigOpenShiftReconciler struct { Scheme *runtime.Scheme kataConfig *kataconfigurationv1.KataConfig + + ImgMc *mcfgv1.MachineConfig } const ( @@ -494,6 +496,12 @@ func getExtensionName() string { func (r *KataConfigOpenShiftReconciler) newMCForCR(machinePool string) (*mcfgv1.MachineConfig, error) { r.Log.Info("Creating MachineConfig for Custom Resource") + if r.ImgMc != nil { + r.Log.Info("Image based MachineConfig", "MachineConfig", r.ImgMc) + return r.ImgMc, nil + } + + // Create extension MachineConfig ic := ignTypes.Config{ Ignition: ignTypes.Ignition{ Version: "3.2.0", @@ -528,6 +536,8 @@ func (r *KataConfigOpenShiftReconciler) newMCForCR(machinePool string) (*mcfgv1. }, } + r.Log.Info("Extension based MachineConfig", "MachineConfig", mc) + return &mc, nil } @@ -843,6 +853,8 @@ func (r *KataConfigOpenShiftReconciler) processKataConfigDeleteRequest() (ctrl.R err = r.Client.Get(context.TODO(), types.NamespacedName{Name: mc.Name}, mc) if err != nil && k8serrors.IsNotFound(err) { isMcDeleted = true + // Reset ImgMc + r.ImgMc = nil } else if err != nil { return ctrl.Result{}, err } @@ -1053,7 +1065,7 @@ func (r *KataConfigOpenShiftReconciler) processKataConfigInstallRequest() (ctrl. r.Log.Info("SCNodeRole is: " + machinePool) } - wasMcJustCreated, err := r.createExtensionMc(machinePool) + wasMcJustCreated, err := r.createMc(machinePool) if err != nil { return ctrl.Result{Requeue: true}, nil } @@ -1281,7 +1293,7 @@ func (r *KataConfigOpenShiftReconciler) processKataConfigInstallRequest() (ctrl. // If the first return value is 'true' it means that the MC was just created // by this call, 'false' means that it's already existed. As usual, the first // return value is only valid if the second one is nil. -func (r *KataConfigOpenShiftReconciler) createExtensionMc(machinePool string) (bool, error) { +func (r *KataConfigOpenShiftReconciler) createMc(machinePool string) (bool, error) { // In case we're returning an error we want to make it explicit that // the first return value is "not care". Unfortunately golang seems @@ -1289,16 +1301,16 @@ func (r *KataConfigOpenShiftReconciler) createExtensionMc(machinePool string) (b // hence this work-around. var dummy bool - /* Create Machine Config object to enable sandboxed containers RHCOS extension */ - mc := &mcfgv1.MachineConfig{} - err := r.Client.Get(context.TODO(), types.NamespacedName{Name: extension_mc_name}, mc) - if err != nil && (k8serrors.IsNotFound(err) || k8serrors.IsGone(err)) { + /* Create Machine Config object to install sandboxed containers */ - r.Log.Info("creating RHCOS extension MachineConfig") - mc, err = r.newMCForCR(machinePool) - if err != nil { - return dummy, err - } + r.Log.Info("creating RHCOS MachineConfig") + mc, err := r.newMCForCR(machinePool) + if err != nil { + return dummy, err + } + + err = r.Client.Get(context.TODO(), types.NamespacedName{Name: mc.Name}, mc) + if err != nil && (k8serrors.IsNotFound(err) || k8serrors.IsGone(err)) { err = r.Client.Create(context.TODO(), mc) if err != nil { @@ -1308,12 +1320,13 @@ func (r *KataConfigOpenShiftReconciler) createExtensionMc(machinePool string) (b r.Log.Info("MachineConfig successfully created", "mc.Name", mc.Name) return true, nil } else if err != nil { - r.Log.Info("failed to retrieve extension MachineConfig", "err", err) + r.Log.Info("failed to retrieve MachineConfig", "err", err) return dummy, err } else { - r.Log.Info("extension MachineConfig already exists") + r.Log.Info("MachineConfig already exists") return false, nil } + } func (r *KataConfigOpenShiftReconciler) makeReconcileRequest() reconcile.Request {