diff --git a/pkg/controller/bootstrap/bootstrap.go b/pkg/controller/bootstrap/bootstrap.go index b99ad49859..cd256b5e9c 100644 --- a/pkg/controller/bootstrap/bootstrap.go +++ b/pkg/controller/bootstrap/bootstrap.go @@ -102,6 +102,7 @@ func (b *Bootstrap) Run(destDir string) error { imgCfg *apicfgv1.Image apiServer *apicfgv1.APIServer iri *mcfgv1alpha1.InternalReleaseImage + iriTLSCert *corev1.Secret ) for _, info := range infos { if info.IsDir() { @@ -171,6 +172,10 @@ func (b *Bootstrap) Run(destDir string) error { if obj.GetName() == ctrlcommon.InternalReleaseImageInstanceName { iri = obj } + case *corev1.Secret: + if obj.GetName() == ctrlcommon.InternalReleaseImageTLSSecretName { + iriTLSCert = obj + } default: klog.Infof("skipping %q [%d] manifest because of unhandled %T", file.Name(), idx+1, obji) } @@ -253,11 +258,11 @@ func (b *Bootstrap) Run(destDir string) error { if fgHandler != nil && fgHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) { if iri != nil { - iriConfig, err := internalreleaseimage.RunInternalReleaseImageBootstrap(iri, cconfig) + iriConfigs, err := internalreleaseimage.RunInternalReleaseImageBootstrap(iri, iriTLSCert, cconfig) if err != nil { return err } - configs = append(configs, iriConfig) + configs = append(configs, iriConfigs...) klog.Infof("Successfully generated MachineConfig from InternalReleaseImage.") } } diff --git a/pkg/controller/common/constants.go b/pkg/controller/common/constants.go index d3e4c7578c..15d773dc0a 100644 --- a/pkg/controller/common/constants.go +++ b/pkg/controller/common/constants.go @@ -66,6 +66,9 @@ const ( // InternalReleaseImageInstanceName is a singleton name for InternalReleaseImage InternalReleaseImageInstanceName = "cluster" + // InternalReleaseImageTLSSecretName is the name of the secret manifest containing the InternalReleaseImage TLS certificate. + InternalReleaseImageTLSSecretName = "internal-release-image-tls" + // APIServerInstanceName is a singleton name for APIServer configuration APIServerBootstrapFileLocation = "/etc/mcs/bootstrap/api-server/api-server.yaml" diff --git a/pkg/controller/internalreleaseimage/OWNERS b/pkg/controller/internalreleaseimage/OWNERS new file mode 100644 index 0000000000..74e8b7eee0 --- /dev/null +++ b/pkg/controller/internalreleaseimage/OWNERS @@ -0,0 +1,14 @@ +# See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md + +approvers: + - andfasano + - bfournie + - pawanpinjarkar + - rwsu + - zaneb +reviewers: + - andfasano + - bfournie + - pawanpinjarkar + - rwsu + - zaneb \ No newline at end of file diff --git a/pkg/controller/internalreleaseimage/internalreleaseimage_bootstrap.go b/pkg/controller/internalreleaseimage/internalreleaseimage_bootstrap.go index 700e02ced2..6d2a243eab 100644 --- a/pkg/controller/internalreleaseimage/internalreleaseimage_bootstrap.go +++ b/pkg/controller/internalreleaseimage/internalreleaseimage_bootstrap.go @@ -1,11 +1,28 @@ package internalreleaseimage import ( + corev1 "k8s.io/api/core/v1" + mcfgv1 "github.com/openshift/api/machineconfiguration/v1" mcfgv1alpha1 "github.com/openshift/api/machineconfiguration/v1alpha1" ) -// RunInternalReleaseImageBootstrap generates a MachineConfig object for InternalReleaseImage that would have been generated by syncInternalReleaseImage -func RunInternalReleaseImageBootstrap(iri *mcfgv1alpha1.InternalReleaseImage, controllerConfig *mcfgv1.ControllerConfig) (*mcfgv1.MachineConfig, error) { - return generateInternalReleaseImageMachineConfig(iri, controllerConfig) +// RunInternalReleaseImageBootstrap generates the MachineConfig objects for InternalReleaseImage that would have been generated by syncInternalReleaseImage. +func RunInternalReleaseImageBootstrap(iri *mcfgv1alpha1.InternalReleaseImage, iriSecret *corev1.Secret, cconfig *mcfgv1.ControllerConfig) ([]*mcfgv1.MachineConfig, error) { + configs := []*mcfgv1.MachineConfig{} + + for _, role := range SupportedRoles { + r := NewRendererByRole(role, iri, iriSecret, cconfig) + mc, err := r.CreateEmptyMachineConfig() + if err != nil { + return nil, err + } + err = r.RenderAndSetIgnition(mc) + if err != nil { + return nil, err + } + configs = append(configs, mc) + } + + return configs, nil } diff --git a/pkg/controller/internalreleaseimage/internalreleaseimage_bootstrap_test.go b/pkg/controller/internalreleaseimage/internalreleaseimage_bootstrap_test.go index 0aaf2a1180..be62296f9f 100644 --- a/pkg/controller/internalreleaseimage/internalreleaseimage_bootstrap_test.go +++ b/pkg/controller/internalreleaseimage/internalreleaseimage_bootstrap_test.go @@ -3,25 +3,12 @@ package internalreleaseimage import ( "testing" - mcfgv1 "github.com/openshift/api/machineconfiguration/v1" mcfgv1alpha1 "github.com/openshift/api/machineconfiguration/v1alpha1" - templatectrl "github.com/openshift/machine-config-operator/pkg/controller/template" "github.com/stretchr/testify/assert" ) func TestRunInternalReleaseImageBootstrap(t *testing.T) { - cc := &mcfgv1.ControllerConfig{ - Spec: mcfgv1.ControllerConfigSpec{ - Images: map[string]string{ - templatectrl.DockerRegistryKey: "docker-registry-image-pullspec", - }, - }, - } - - mc, err := RunInternalReleaseImageBootstrap(&mcfgv1alpha1.InternalReleaseImage{}, cc) + configs, err := RunInternalReleaseImageBootstrap(&mcfgv1alpha1.InternalReleaseImage{}, iriCertSecret().obj, cconfig().obj) assert.NoError(t, err) - assert.Equal(t, mc.Name, iriMachineConfigName) - assert.Equal(t, mc.Labels[mcfgv1.MachineConfigRoleLabelKey], "master") - assert.Equal(t, mc.OwnerReferences[0].Kind, "InternalReleaseImage") - assert.Contains(t, string(mc.Spec.Config.Raw), "docker-registry-image-pullspec") + verifyAllInternalReleaseImageMachineConfigs(t, configs) } diff --git a/pkg/controller/internalreleaseimage/internalreleaseimage_controller.go b/pkg/controller/internalreleaseimage/internalreleaseimage_controller.go index 4c64455855..c148dfa2f9 100644 --- a/pkg/controller/internalreleaseimage/internalreleaseimage_controller.go +++ b/pkg/controller/internalreleaseimage/internalreleaseimage_controller.go @@ -1,16 +1,11 @@ package internalreleaseimage import ( - "bytes" "context" - _ "embed" "fmt" "reflect" - "text/template" "time" - "github.com/clarketm/json" - ign3types "github.com/coreos/ignition/v2/config/v3_5/types" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -31,17 +26,13 @@ import ( mcfginformersv1alpha1 "github.com/openshift/client-go/machineconfiguration/informers/externalversions/machineconfiguration/v1alpha1" mcfglistersv1 "github.com/openshift/client-go/machineconfiguration/listers/machineconfiguration/v1" mcfglistersv1alpha1 "github.com/openshift/client-go/machineconfiguration/listers/machineconfiguration/v1alpha1" - "github.com/openshift/machine-config-operator/pkg/controller/common" ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" templatectrl "github.com/openshift/machine-config-operator/pkg/controller/template" - "github.com/openshift/machine-config-operator/pkg/version" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( maxRetries = 15 - - iriMachineConfigName = "02-master-internalreleaseimage" ) var ( @@ -53,14 +44,12 @@ var ( Duration: 100 * time.Millisecond, Jitter: 1.0, } - - //go:embed templates/iri-registry.service.yaml - iriRegistryServiceTemplate string ) // Controller defines the InternalReleaseImage controller. type Controller struct { client mcfgclientset.Interface + kubeClient clientset.Interface eventRecorder record.EventRecorder syncHandler func(mcp string) error @@ -91,8 +80,8 @@ func New( eventBroadcaster.StartRecordingToSink(&corev1client.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")}) ctrl := &Controller{ - client: mcfgClient, - + client: mcfgClient, + kubeClient: kubeClient, eventRecorder: ctrlcommon.NamespacedEventRecorder(eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "machineconfigcontroller-internalreleaseimagecontroller"})), queue: workqueue.NewTypedRateLimitingQueueWithConfig( workqueue.DefaultTypedControllerRateLimiter[string](), @@ -255,7 +244,8 @@ func (ctrl *Controller) deleteMachineConfig(obj interface{}) { func (ctrl *Controller) processMachineConfigEvent(obj interface{}, logMsg string) { mc := obj.(*mcfgv1.MachineConfig) - if mc.Name != iriMachineConfigName { + // Skip any event not related to the InternalReleaseImage machine configs + if len(mc.OwnerReferences) == 0 || mc.OwnerReferences[0].Kind != controllerKind.Kind { return } @@ -300,7 +290,7 @@ func (ctrl *Controller) syncInternalReleaseImage(key string) error { // Deep-copy otherwise we are mutating our cache. iri = iri.DeepCopy() - // Check for Deleted InternalReleaseImage and optionally delete finalizers + // Check for Deleted InternalReleaseImage and optionally delete finalizers. if !iri.DeletionTimestamp.IsZero() { if len(iri.GetFinalizers()) > 0 { return ctrl.cascadeDelete(iri) @@ -308,129 +298,77 @@ func (ctrl *Controller) syncInternalReleaseImage(key string) error { return nil } - // Create or update InternalReleaseImage MachineConfig - mc, err := ctrl.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), iriMachineConfigName, metav1.GetOptions{}) - isNotFound := errors.IsNotFound(err) - if err != nil && !isNotFound { - return err // syncStatus, could not find MachineConfig - } - cconfig, err := ctrl.client.MachineconfigurationV1().ControllerConfigs().Get(context.TODO(), ctrlcommon.ControllerConfigName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("could not get ControllerConfig %w", err) } - if isNotFound { - mc, err = generateInternalReleaseImageMachineConfig(iri, cconfig) - } else { - err = updateInternalReleaseImageMachineConfig(mc, cconfig) - } + iriSecret, err := ctrl.kubeClient.CoreV1().Secrets(ctrlcommon.MCONamespace).Get(context.TODO(), ctrlcommon.InternalReleaseImageTLSSecretName, metav1.GetOptions{}) if err != nil { - return err // syncStatus, could not create/update MachineConfig + return fmt.Errorf("could not get Secret %s: %w", ctrlcommon.InternalReleaseImageTLSSecretName, err) } - if err := retry.RetryOnConflict(updateBackoff, func() error { - var err error + for _, role := range SupportedRoles { + r := NewRendererByRole(role, iri, iriSecret, cconfig) + + mc, err := ctrl.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), r.GetMachineConfigName(), metav1.GetOptions{}) + isNotFound := errors.IsNotFound(err) + if err != nil && !isNotFound { + return err // syncStatus, could not find MachineConfig + } if isNotFound { - _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Create(context.TODO(), mc, metav1.CreateOptions{}) - } else { - _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Update(context.TODO(), mc, metav1.UpdateOptions{}) + mc, err = r.CreateEmptyMachineConfig() + if err != nil { + return err // syncStatusOnly, could not create MachineConfig + } } - return err - }); err != nil { - return err // syncStatus, could not Create/Update MachineConfig - } - // Add finalizer to the InternalReleaseImage - if err := ctrl.addFinalizerToInternalReleaseImage(iri, mc); err != nil { - return err // syncStatus , could not add finalizers + err = r.RenderAndSetIgnition(mc) + if err != nil { + return err // syncStatus, could not generate IRI configs + } + err = ctrl.createOrUpdateMachineConfig(isNotFound, mc) + if err != nil { + return err // syncStatus, could not Create/Update MachineConfig + } + if err := ctrl.addFinalizerToInternalReleaseImage(iri, mc); err != nil { + return err // syncStatus , could not add finalizers + } } return nil } -func updateInternalReleaseImageMachineConfig(mc *mcfgv1.MachineConfig, controllerConfig *mcfgv1.ControllerConfig) error { - ignCfg, err := generateIgnitionFromTemplate(controllerConfig) - if err != nil { - return err - } - - rawIgn, err := json.Marshal(ignCfg) - if err != nil { +func (ctrl *Controller) createOrUpdateMachineConfig(isNotFound bool, mc *mcfgv1.MachineConfig) error { + return retry.RetryOnConflict(updateBackoff, func() error { + var err error + if isNotFound { + _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Create(context.TODO(), mc, metav1.CreateOptions{}) + } else { + _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Update(context.TODO(), mc, metav1.UpdateOptions{}) + } return err - } - - mc.Spec.Config.Raw = rawIgn - return nil + }) } func (ctrl *Controller) addFinalizerToInternalReleaseImage(iri *mcfgv1alpha1.InternalReleaseImage, mc *mcfgv1.MachineConfig) error { - if len(iri.GetFinalizers()) > 0 { + if ctrlcommon.InSlice(mc.Name, iri.Finalizers) { return nil } - return ctrl.updateInternalReleaseImageFinalizers(iri, []string{mc.Name}) + iri.Finalizers = append(iri.Finalizers, mc.Name) + _, err := ctrl.client.MachineconfigurationV1alpha1().InternalReleaseImages().Update(context.TODO(), iri, metav1.UpdateOptions{}) + return err } func (ctrl *Controller) cascadeDelete(iri *mcfgv1alpha1.InternalReleaseImage) error { - // Delete the InternalReleaseImage machine config - err := ctrl.client.MachineconfigurationV1().MachineConfigs().Delete(context.TODO(), iriMachineConfigName, metav1.DeleteOptions{}) + mcName := iri.GetFinalizers()[0] + err := ctrl.client.MachineconfigurationV1().MachineConfigs().Delete(context.TODO(), mcName, metav1.DeleteOptions{}) if err != nil && !errors.IsNotFound(err) { return err } - // Remove the InternalRelaseImage finalizer - return ctrl.updateInternalReleaseImageFinalizers(iri, []string{}) -} - -func (ctrl *Controller) updateInternalReleaseImageFinalizers(iri *mcfgv1alpha1.InternalReleaseImage, finalizers []string) error { - iri.SetFinalizers(finalizers) - _, err := ctrl.client.MachineconfigurationV1alpha1().InternalReleaseImages().Update(context.TODO(), iri, metav1.UpdateOptions{}) + iri.Finalizers = append([]string{}, iri.Finalizers[1:]...) + _, err = ctrl.client.MachineconfigurationV1alpha1().InternalReleaseImages().Update(context.TODO(), iri, metav1.UpdateOptions{}) return err } - -func generateInternalReleaseImageMachineConfig(iri *mcfgv1alpha1.InternalReleaseImage, controllerConfig *mcfgv1.ControllerConfig) (*mcfgv1.MachineConfig, error) { - ignCfg, err := generateIgnitionFromTemplate(controllerConfig) - if err != nil { - return nil, err - } - - mcfg, err := ctrlcommon.MachineConfigFromIgnConfig(common.MachineConfigPoolMaster, iriMachineConfigName, ignCfg) - if err != nil { - return nil, fmt.Errorf("error creating MachineConfig from Ignition config: %w", err) - } - - cref := metav1.NewControllerRef(iri, controllerKind) - mcfg.SetOwnerReferences([]metav1.OwnerReference{*cref}) - mcfg.SetAnnotations(map[string]string{ - ctrlcommon.GeneratedByControllerVersionAnnotationKey: version.Hash, - }) - - return mcfg, nil -} - -func generateIgnitionFromTemplate(controllerConfig *mcfgv1.ControllerConfig) (*ign3types.Config, error) { - // Parse the iri template - tmpl, err := template.New("iri-template").Parse(iriRegistryServiceTemplate) - if err != nil { - return nil, fmt.Errorf("failed to parse iri-template : %w", err) - } - - type iriRenderConfig struct { - DockerRegistryImage string - } - - buf := new(bytes.Buffer) - if err := tmpl.Execute(buf, iriRenderConfig{ - DockerRegistryImage: controllerConfig.Spec.Images[templatectrl.DockerRegistryKey], - }); err != nil { - return nil, fmt.Errorf("failed to execute template: %w", err) - } - - // Generate the iri ignition - ignCfg, err := ctrlcommon.TranspileCoreOSConfigToIgn(nil, []string{buf.String()}) - if err != nil { - return nil, fmt.Errorf("error transpiling CoreOS config to Ignition config: %w", err) - } - return ignCfg, nil -} diff --git a/pkg/controller/internalreleaseimage/internalreleaseimage_controller_test.go b/pkg/controller/internalreleaseimage/internalreleaseimage_controller_test.go index 5e59f6461c..3fd94603a0 100644 --- a/pkg/controller/internalreleaseimage/internalreleaseimage_controller_test.go +++ b/pkg/controller/internalreleaseimage/internalreleaseimage_controller_test.go @@ -9,10 +9,10 @@ import ( mcfgv1alpha1 "github.com/openshift/api/machineconfiguration/v1alpha1" "github.com/openshift/client-go/machineconfiguration/clientset/versioned/fake" informers "github.com/openshift/client-go/machineconfiguration/informers/externalversions" - "github.com/openshift/machine-config-operator/pkg/controller/common" ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" - templatectrl "github.com/openshift/machine-config-operator/pkg/controller/template" "github.com/stretchr/testify/assert" + + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -24,56 +24,79 @@ func TestInternalReleaseImageCreate(t *testing.T) { cases := []struct { name string initialObjects func() []runtime.Object - verify func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMC *mcfgv1.MachineConfig) + verify func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) }{ { name: "feature inactive", initialObjects: objs(), - verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMC *mcfgv1.MachineConfig) { + verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) { assert.Nil(t, actualIRI) - assert.Nil(t, actualMC) + assert.Nil(t, actualMasterMC) + assert.Nil(t, actualWorkerMC) }, }, { name: "add finalizer if not present", - initialObjects: objs(iri(), cconfig()), - verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMC *mcfgv1.MachineConfig) { - assert.Equal(t, iri().finalizer(iriMachineConfigName).build(), actualIRI) + initialObjects: objs(iri(), cconfig(), iriCertSecret()), + verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) { + assert.Equal(t, iri().finalizer(masterName(), workerName()).build(), actualIRI) }, }, { name: "generate iri machine-config if not present", - initialObjects: objs(iri(), cconfig()), - verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMC *mcfgv1.MachineConfig) { - assert.Equal(t, iriMachineConfigName, actualMC.Name) - assert.Equal(t, actualMC.Labels[mcfgv1.MachineConfigRoleLabelKey], common.MachineConfigPoolMaster) - assert.Equal(t, actualMC.OwnerReferences[0].Kind, "InternalReleaseImage") - // Check that the templating code replaced the docker-registry image - assert.Contains(t, string(actualMC.Spec.Config.Raw), "docker-registry-image-pullspec") + initialObjects: objs(iri(), cconfig(), iriCertSecret()), + verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) { + verifyInternalReleaseMasterMachineConfig(t, actualMasterMC) + verifyInternalReleaseWorkerMachineConfig(t, actualWorkerMC) + }, + }, + { + name: "avoid machine-config drifting", + initialObjects: objs( + iri().finalizer(masterName(), workerName()), + cconfig(), iriCertSecret(), + machineconfigmaster().ignition("some garbage"), + machineconfigworker().ignition("other garbage")), + verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) { + verifyInternalReleaseMasterMachineConfig(t, actualMasterMC) + verifyInternalReleaseWorkerMachineConfig(t, actualWorkerMC) }, }, { - name: "avoid machine-config drifting", - initialObjects: objs(iri().finalizer(iriMachineConfigName), cconfig(), machineconfig().ignition("some garbage")), - verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMC *mcfgv1.MachineConfig) { - assert.Contains(t, string(actualMC.Spec.Config.Raw), "docker-registry-image-pullspec") + name: "refresh machine-config on controllerConfig update", + initialObjects: objs( + iri().finalizer(masterName(), workerName()), + cconfig().dockerRegistryImage("a-new-docker-registry-image-pullspec"), iriCertSecret(), + machineconfigmaster(), machineconfigworker()), + verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) { + verifyInternalReleaseMasterMachineConfig(t, actualMasterMC) + verifyInternalReleaseWorkerMachineConfig(t, actualWorkerMC) }, }, { - name: "refresh machine-config on controllerConfig update", - initialObjects: objs(iri().finalizer(iriMachineConfigName), cconfig().dockerRegistryImage("a-new-docker-registry-image-pullspec"), machineconfig()), - verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMC *mcfgv1.MachineConfig) { - assert.Contains(t, string(actualMC.Spec.Config.Raw), "a-new-docker-registry-image-pullspec") + name: "machine-config cascade delete on iri removal - removes the first machineconfig", + initialObjects: objs( + iri().finalizer(masterName(), workerName()).setDeletionTimestamp(), + cconfig(), iriCertSecret(), + machineconfigmaster(), machineconfigworker()), + verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) { + assert.NotNil(t, iri) + assert.Equal(t, []string{workerName()}, actualIRI.Finalizers) + assert.Nil(t, actualMasterMC) + assert.NotNil(t, actualWorkerMC) }, }, { - name: "machine-config cascade delete on iri removal", + name: "machine-config cascade delete on iri removal - then removes the remaining machineconfig", initialObjects: objs( - iri().finalizer(iriMachineConfigName).setDeletionTimestamp(), - cconfig(), machineconfig()), - verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMC *mcfgv1.MachineConfig) { + iri().finalizer(workerName()).setDeletionTimestamp(), + cconfig(), iriCertSecret(), + machineconfigworker()), + verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) { + assert.NotNil(t, iri) assert.Empty(t, actualIRI.Finalizers) - assert.Nil(t, actualMC) + assert.Nil(t, actualMasterMC) + assert.Nil(t, actualWorkerMC) }, }, } @@ -103,153 +126,72 @@ func TestInternalReleaseImageCreate(t *testing.T) { actualIRI = nil } } - actualMC, err := f.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), iriMachineConfigName, v1.GetOptions{}) + actualMasterMC, err := f.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), masterName(), v1.GetOptions{}) + if err != nil { + if !errors.IsNotFound(err) { + t.Errorf("Error while running sync step: %v", err) + } else { + actualMasterMC = nil + } + } + actualWorkerMC, err := f.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), workerName(), v1.GetOptions{}) if err != nil { if !errors.IsNotFound(err) { t.Errorf("Error while running sync step: %v", err) } else { - actualMC = nil + actualWorkerMC = nil } } - tc.verify(t, actualIRI, actualMC) + tc.verify(t, actualIRI, actualMasterMC, actualWorkerMC) } }) } } -// objs is an helper func to improve the test readability. -func objs(builders ...objBuilder) func() []runtime.Object { - return func() []runtime.Object { - objects := []runtime.Object{} - for _, b := range builders { - objects = append(objects, b.build()) - } - return objects - } -} - -type objBuilder interface { - build() runtime.Object -} - -// iriBuilder simplifies the creation of an InternalReleaseImage resource in the test. -type iriBuilder struct { - obj *mcfgv1alpha1.InternalReleaseImage -} - -func iri() *iriBuilder { - return &iriBuilder{ - obj: &mcfgv1alpha1.InternalReleaseImage{ - ObjectMeta: v1.ObjectMeta{ - Name: ctrlcommon.InternalReleaseImageInstanceName, - }, - }, - } -} - -func (ib *iriBuilder) finalizer(f ...string) *iriBuilder { - ib.obj.SetFinalizers(f) - return ib -} - -func (ib *iriBuilder) setDeletionTimestamp() *iriBuilder { - now := v1.Now() - ib.obj.SetDeletionTimestamp(&now) - return ib -} - -func (ib *iriBuilder) build() runtime.Object { - return ib.obj -} - -// controllerConfigBuilder simplifies the creation of a ControllerConfig resource in the test. -type controllerConfigBuilder struct { - obj *mcfgv1.ControllerConfig -} - -func cconfig() *controllerConfigBuilder { - return &controllerConfigBuilder{ - obj: &mcfgv1.ControllerConfig{ - ObjectMeta: v1.ObjectMeta{ - Name: ctrlcommon.ControllerConfigName, - }, - Spec: mcfgv1.ControllerConfigSpec{ - Images: map[string]string{ - templatectrl.DockerRegistryKey: "docker-registry-image-pullspec", - }, - }, - }, - } -} - -func (ccb *controllerConfigBuilder) dockerRegistryImage(image string) *controllerConfigBuilder { - ccb.obj.Spec.Images[templatectrl.DockerRegistryKey] = image - return ccb -} - -func (ccb *controllerConfigBuilder) build() runtime.Object { - return ccb.obj -} - -// machineConfigBuilder simplifies the creation of a MachineConfig resource in the test. -type machineConfigBuilder struct { - obj *mcfgv1.MachineConfig -} - -func machineconfig() *machineConfigBuilder { - return &machineConfigBuilder{ - obj: &mcfgv1.MachineConfig{ - ObjectMeta: v1.ObjectMeta{ - Name: iriMachineConfigName, - }, - Spec: mcfgv1.MachineConfigSpec{ - Config: runtime.RawExtension{}, - }, - }, - } -} - -func (mcb *machineConfigBuilder) ignition(ign string) *machineConfigBuilder { - mcb.obj.Spec.Config.Raw = []byte(ign) - return mcb -} - -func (mcb *machineConfigBuilder) build() runtime.Object { - return mcb.obj -} - // The fixture used to setup and run the controller. type fixture struct { t *testing.T client *fake.Clientset + k8sClient *k8sfake.Clientset iriLister []*mcfgv1alpha1.InternalReleaseImage ccLister []*mcfgv1.ControllerConfig mcLister []*mcfgv1.MachineConfig controller *Controller objects []runtime.Object + k8sObjects []runtime.Object } func newFixture(t *testing.T, objects []runtime.Object) *fixture { - f := &fixture{ - t: t, - objects: objects, - } + f := &fixture{t: t} + f.setupObjects(objects) f.controller = f.newController() return f } +func (f *fixture) setupObjects(objs []runtime.Object) { + for _, o := range objs { + switch o.(type) { + case *corev1.Secret, *corev1.ConfigMap, *corev1.Pod: + f.k8sObjects = append(f.k8sObjects, o) + default: + f.objects = append(f.objects, o) + } + } +} + func (f *fixture) newController() *Controller { f.client = fake.NewSimpleClientset(f.objects...) + f.k8sClient = k8sfake.NewSimpleClientset(f.k8sObjects...) i := informers.NewSharedInformerFactory(f.client, func() time.Duration { return 0 }()) c := New( i.Machineconfiguration().V1alpha1().InternalReleaseImages(), i.Machineconfiguration().V1().ControllerConfigs(), i.Machineconfiguration().V1().MachineConfigs(), - k8sfake.NewSimpleClientset(), + f.k8sClient, f.client, ) @@ -282,7 +224,6 @@ func (f *fixture) run(key string) { } func (f *fixture) runController(key string, expectError bool) { - err := f.controller.syncHandler(key) if !expectError && err != nil { f.t.Errorf("error syncing internalreleaseimage: %v", err) diff --git a/pkg/controller/internalreleaseimage/internalreleaseimage_helpers_test.go b/pkg/controller/internalreleaseimage/internalreleaseimage_helpers_test.go new file mode 100644 index 0000000000..271c38f209 --- /dev/null +++ b/pkg/controller/internalreleaseimage/internalreleaseimage_helpers_test.go @@ -0,0 +1,210 @@ +package internalreleaseimage + +// Test builders and helper methods. + +import ( + "fmt" + "testing" + + ign3types "github.com/coreos/ignition/v2/config/v3_5/types" + mcfgv1 "github.com/openshift/api/machineconfiguration/v1" + mcfgv1alpha1 "github.com/openshift/api/machineconfiguration/v1alpha1" + ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" + templatectrl "github.com/openshift/machine-config-operator/pkg/controller/template" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func verifyAllInternalReleaseImageMachineConfigs(t *testing.T, configs []*mcfgv1.MachineConfig) { + assert.Len(t, configs, 2) + verifyInternalReleaseMasterMachineConfig(t, configs[0]) + verifyInternalReleaseWorkerMachineConfig(t, configs[1]) +} + +func verifyInternalReleaseMasterMachineConfig(t *testing.T, mc *mcfgv1.MachineConfig) { + assert.Equal(t, masterName(), mc.Name) + assert.Equal(t, ctrlcommon.MachineConfigPoolMaster, mc.Labels[mcfgv1.MachineConfigRoleLabelKey]) + assert.Equal(t, controllerKind.Kind, mc.OwnerReferences[0].Kind) + + ignCfg, err := ctrlcommon.ParseAndConvertConfig(mc.Spec.Config.Raw) + assert.NoError(t, err, mc.Name) + + assert.Len(t, ignCfg.Systemd.Units, 1) + assert.Contains(t, *ignCfg.Systemd.Units[0].Contents, "docker-registry-image-pullspec") + + assert.Len(t, ignCfg.Storage.Files, 3) + verifyIgnitionFile(t, &ignCfg, "/etc/pki/ca-trust/source/anchors/root-ca.crt", "root-ca-data") + verifyIgnitionFile(t, &ignCfg, "/etc/iri-registry/certs/tls.key", "iri-tls-key") + verifyIgnitionFile(t, &ignCfg, "/etc/iri-registry/certs/tls.crt", "iri-tls-crt") +} + +func verifyInternalReleaseWorkerMachineConfig(t *testing.T, mc *mcfgv1.MachineConfig) { + assert.Equal(t, workerName(), mc.Name) + assert.Equal(t, ctrlcommon.MachineConfigPoolWorker, mc.Labels[mcfgv1.MachineConfigRoleLabelKey]) + assert.Equal(t, controllerKind.Kind, mc.OwnerReferences[0].Kind) + + ignCfg, err := ctrlcommon.ParseAndConvertConfig(mc.Spec.Config.Raw) + assert.NoError(t, err) + + assert.Len(t, ignCfg.Systemd.Units, 0) + assert.Len(t, ignCfg.Storage.Files, 1) + verifyIgnitionFile(t, &ignCfg, "/etc/pki/ca-trust/source/anchors/root-ca.crt", "root-ca-data") +} + +func verifyIgnitionFile(t *testing.T, ignCfg *ign3types.Config, path string, expectedContent string) { + data, err := ctrlcommon.GetIgnitionFileDataByPath(ignCfg, path) + assert.NoError(t, err) + assert.Equal(t, expectedContent, string(data), path) +} + +// objs is an helper func to improve the test readability. +func objs(builders ...objBuilder) func() []runtime.Object { + return func() []runtime.Object { + objects := []runtime.Object{} + for _, b := range builders { + objects = append(objects, b.build()) + } + return objects + } +} + +type objBuilder interface { + build() runtime.Object +} + +// iriBuilder simplifies the creation of an InternalReleaseImage resource in the test. +type iriBuilder struct { + obj *mcfgv1alpha1.InternalReleaseImage +} + +func iri() *iriBuilder { + return &iriBuilder{ + obj: &mcfgv1alpha1.InternalReleaseImage{ + ObjectMeta: v1.ObjectMeta{ + Name: ctrlcommon.InternalReleaseImageInstanceName, + }, + }, + } +} + +func (ib *iriBuilder) finalizer(f ...string) *iriBuilder { + ib.obj.SetFinalizers(f) + return ib +} + +func (ib *iriBuilder) setDeletionTimestamp() *iriBuilder { + now := v1.Now() + ib.obj.SetDeletionTimestamp(&now) + return ib +} + +func (ib *iriBuilder) build() runtime.Object { + return ib.obj +} + +// controllerConfigBuilder simplifies the creation of a ControllerConfig resource in the test. +type controllerConfigBuilder struct { + obj *mcfgv1.ControllerConfig +} + +func cconfig() *controllerConfigBuilder { + return &controllerConfigBuilder{ + obj: &mcfgv1.ControllerConfig{ + ObjectMeta: v1.ObjectMeta{ + Name: ctrlcommon.ControllerConfigName, + }, + Spec: mcfgv1.ControllerConfigSpec{ + Images: map[string]string{ + templatectrl.DockerRegistryKey: "docker-registry-image-pullspec", + }, + RootCAData: []byte("root-ca-data"), + }, + }, + } +} + +func (ccb *controllerConfigBuilder) dockerRegistryImage(image string) *controllerConfigBuilder { + ccb.obj.Spec.Images[templatectrl.DockerRegistryKey] = image + return ccb +} + +func (ccb *controllerConfigBuilder) build() runtime.Object { + return ccb.obj +} + +// machineConfigBuilder simplifies the creation of a MachineConfig resource in the test. +type machineConfigBuilder struct { + obj *mcfgv1.MachineConfig +} + +func machineconfigmaster() *machineConfigBuilder { + return machineconfig("master") +} + +func machineconfigworker() *machineConfigBuilder { + return machineconfig("worker") +} + +func masterName() string { + return fmt.Sprintf(machineConfigNameFmt, "master") +} + +func workerName() string { + return fmt.Sprintf(machineConfigNameFmt, "worker") +} + +func machineconfig(role string) *machineConfigBuilder { + return &machineConfigBuilder{ + obj: &mcfgv1.MachineConfig{ + ObjectMeta: v1.ObjectMeta{ + Name: fmt.Sprintf(machineConfigNameFmt, role), + Labels: map[string]string{ + mcfgv1.MachineConfigRoleLabelKey: role, + }, + OwnerReferences: []v1.OwnerReference{ + { + Kind: "InternalReleaseImage", + }, + }, + }, + Spec: mcfgv1.MachineConfigSpec{ + Config: runtime.RawExtension{}, + }, + }, + } +} + +func (mcb *machineConfigBuilder) ignition(ign string) *machineConfigBuilder { + mcb.obj.Spec.Config.Raw = []byte(ign) + return mcb +} + +func (mcb *machineConfigBuilder) build() runtime.Object { + return mcb.obj +} + +// secretBuilder simplifies the creation of a Secret resource in the test. +type secretBuilder struct { + obj *corev1.Secret +} + +func iriCertSecret() *secretBuilder { + return &secretBuilder{ + obj: &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Namespace: ctrlcommon.MCONamespace, + Name: ctrlcommon.InternalReleaseImageTLSSecretName, + }, + Data: map[string][]byte{ + "tls.key": []byte("iri-tls-key"), + "tls.crt": []byte("iri-tls-crt"), + }, + }, + } +} + +func (sb *secretBuilder) build() runtime.Object { + return sb.obj +} diff --git a/pkg/controller/internalreleaseimage/internalreleaseimage_renderer.go b/pkg/controller/internalreleaseimage/internalreleaseimage_renderer.go new file mode 100644 index 0000000000..36806638fc --- /dev/null +++ b/pkg/controller/internalreleaseimage/internalreleaseimage_renderer.go @@ -0,0 +1,195 @@ +package internalreleaseimage + +import ( + "bytes" + "embed" + "errors" + "fmt" + "io/fs" + "path/filepath" + "text/template" + + "github.com/clarketm/json" + ign3types "github.com/coreos/ignition/v2/config/v3_5/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + mcfgv1 "github.com/openshift/api/machineconfiguration/v1" + mcfgv1alpha1 "github.com/openshift/api/machineconfiguration/v1alpha1" + ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" + templatectrl "github.com/openshift/machine-config-operator/pkg/controller/template" + "github.com/openshift/machine-config-operator/pkg/version" +) + +var ( + //go:embed templates/* + templatesFS embed.FS + + // List of supported roles for generating the machine configs. + // Templates folders are organized by those roles. + SupportedRoles = []string{"master", "worker"} + + // Format of the name for the InternalReleaseImage machine configs. + machineConfigNameFmt = "02-%s-internalreleaseimage" +) + +// Renderer takes care of generating the required ignition (by role) for +// the InternalReleaseImage machine config resources. It can also create +// a MachineConfig instance when required. +type Renderer struct { + role string + iri *mcfgv1alpha1.InternalReleaseImage + iriSecret *corev1.Secret + cconfig *mcfgv1.ControllerConfig +} + +// NewRendererByRole creates a new Renderer instance for generating +// the machine config for the given role. +func NewRendererByRole(role string, iri *mcfgv1alpha1.InternalReleaseImage, iriSecret *corev1.Secret, cconfig *mcfgv1.ControllerConfig) *Renderer { + return &Renderer{ + role: role, + iri: iri, + iriSecret: iriSecret, + cconfig: cconfig, + } +} + +// GetMachineConfigName returns the name of the MachineConfig instance. +func (r *Renderer) GetMachineConfigName() string { + return fmt.Sprintf(machineConfigNameFmt, r.role) +} + +// CreateEmptyMachineConfig creates an empty MachineConfig (without any ignition configured) owned by InternalReleaseImage. +func (r *Renderer) CreateEmptyMachineConfig() (*mcfgv1.MachineConfig, error) { + mc, err := ctrlcommon.MachineConfigFromIgnConfig(r.role, r.GetMachineConfigName(), ctrlcommon.NewIgnConfig()) + if err != nil { + return nil, err + } + + cref := metav1.NewControllerRef(r.iri, controllerKind) + mc.SetOwnerReferences([]metav1.OwnerReference{*cref}) + mc.SetAnnotations(map[string]string{ + ctrlcommon.GeneratedByControllerVersionAnnotationKey: version.Hash, + }) + return mc, nil +} + +// RenderAndSetIgnition generates the required ignition for the given role, +// and sets it on the specified MachineConfig. +func (r *Renderer) RenderAndSetIgnition(mc *mcfgv1.MachineConfig) error { + rc, err := r.newRenderContext() + if err != nil { + return err + } + + ignCfg, err := r.generateIgnitionFromTemplates(rc) + if err != nil { + return err + } + + rawIgn, err := json.Marshal(ignCfg) + if err != nil { + return err + } + + mc.Spec.Config.Raw = rawIgn + return nil +} + +// renderContext is a type used to hold the configuration required +// for current the template rendering. +type renderContext struct { + DockerRegistryImage string + IriTLSKey string + IriTLSCert string + RootCA string +} + +// newRenderContext creates a new renderContext instance. +func (r *Renderer) newRenderContext() (*renderContext, error) { + iriTLSKey, err := r.extractTLSCertFieldFromSecret(r.iriSecret, "tls.key") + if err != nil { + return nil, err + } + iriTLSCert, err := r.extractTLSCertFieldFromSecret(r.iriSecret, "tls.crt") + if err != nil { + return nil, err + } + return &renderContext{ + DockerRegistryImage: r.cconfig.Spec.Images[templatectrl.DockerRegistryKey], + IriTLSKey: iriTLSKey, + IriTLSCert: iriTLSCert, + RootCA: string(r.cconfig.Spec.RootCAData), + }, nil +} + +// extractTLSCertFieldFromSecret is an helper func to get the specified secret field data. +func (r *Renderer) extractTLSCertFieldFromSecret(secret *corev1.Secret, fieldName string) (string, error) { + raw, found := secret.Data[fieldName] + if !found { + return "", fmt.Errorf("cannot find %s in secret %s", fieldName, secret.Name) + } + return string(raw), nil +} + +// generateIgnitionFromTemplates creates the required ignition for the given roles +// using the InternalReleaseImage templates. +func (r *Renderer) generateIgnitionFromTemplates(rc *renderContext) (*ign3types.Config, error) { + // Render template subfolders, if defined. + units, err := r.renderTemplateFolder(rc, filepath.Join(r.role, "units")) + if err != nil { + return nil, err + } + files, err := r.renderTemplateFolder(rc, filepath.Join(r.role, "files")) + if err != nil { + return nil, err + } + + ignCfg, err := ctrlcommon.TranspileCoreOSConfigToIgn(files, units) + if err != nil { + return nil, fmt.Errorf("error transpiling CoreOS config to Ignition config: %w", err) + } + return ignCfg, nil +} + +// renderTemplateFolder renders all the templates found in the specified folder. +func (r *Renderer) renderTemplateFolder(rc any, folder string) ([]string, error) { + tmplFolder := filepath.Join("templates", folder) + + files := []string{} + entries, err := templatesFS.ReadDir(tmplFolder) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + + for _, e := range entries { + data, err := templatesFS.ReadFile(filepath.Join(tmplFolder, e.Name())) + if err != nil { + return nil, err + } + + rendered, err := r.applyTemplate(rc, data) + if err != nil { + return nil, err + } + files = append(files, rendered) + } + + return files, nil +} + +// applyTemplate applies the current template to the specified render context. +func (r *Renderer) applyTemplate(rc any, iriTemplate []byte) (string, error) { + funcs := ctrlcommon.GetTemplateFuncMap() + tmpl, err := template.New("internalreleaseimage").Funcs(funcs).Parse(string(iriTemplate)) + if err != nil { + return "", fmt.Errorf("failed to parse template : %w", err) + } + + buf := new(bytes.Buffer) + if err := tmpl.Execute(buf, rc); err != nil { + return "", fmt.Errorf("failed to execute template: %w", err) + } + + return buf.String(), nil +} diff --git a/pkg/controller/internalreleaseimage/templates/master/files/root-ca.yaml b/pkg/controller/internalreleaseimage/templates/master/files/root-ca.yaml new file mode 100644 index 0000000000..3ca105ab4d --- /dev/null +++ b/pkg/controller/internalreleaseimage/templates/master/files/root-ca.yaml @@ -0,0 +1,5 @@ +mode: 0644 +path: "/etc/pki/ca-trust/source/anchors/root-ca.crt" +contents: + inline: |- +{{indent 4 .RootCA}} diff --git a/pkg/controller/internalreleaseimage/templates/master/files/tls-crt.yaml b/pkg/controller/internalreleaseimage/templates/master/files/tls-crt.yaml new file mode 100644 index 0000000000..bbf4963758 --- /dev/null +++ b/pkg/controller/internalreleaseimage/templates/master/files/tls-crt.yaml @@ -0,0 +1,5 @@ +mode: 0644 +path: "/etc/iri-registry/certs/tls.crt" +contents: + inline: |- +{{indent 4 .IriTLSCert}} diff --git a/pkg/controller/internalreleaseimage/templates/master/files/tls-key.yaml b/pkg/controller/internalreleaseimage/templates/master/files/tls-key.yaml new file mode 100644 index 0000000000..1c3020762d --- /dev/null +++ b/pkg/controller/internalreleaseimage/templates/master/files/tls-key.yaml @@ -0,0 +1,5 @@ +mode: 0600 +path: "/etc/iri-registry/certs/tls.key" +contents: + inline: |- +{{indent 4 .IriTLSKey}} diff --git a/pkg/controller/internalreleaseimage/templates/iri-registry.service.yaml b/pkg/controller/internalreleaseimage/templates/master/units/iri-registry.service.yaml similarity index 64% rename from pkg/controller/internalreleaseimage/templates/iri-registry.service.yaml rename to pkg/controller/internalreleaseimage/templates/master/units/iri-registry.service.yaml index c109b32a75..dac56bb40d 100644 --- a/pkg/controller/internalreleaseimage/templates/iri-registry.service.yaml +++ b/pkg/controller/internalreleaseimage/templates/master/units/iri-registry.service.yaml @@ -7,9 +7,9 @@ contents: | [Service] Environment=PODMAN_SYSTEMD_UNIT=%n + ExecStartPre=mkdir -p /var/lib/iri-registry ExecStartPre=/bin/rm -f %t/%n.ctr-id - ExecStartPre=/usr/bin/mkdir -p /var/lib/iri-registry - ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --log-driver=journald --replace --name=iri-registry -v /var/lib/iri-registry:/var/lib/registry:ro -e REGISTRY_HTTP_ADDR=0.0.0.0:22625 -u 0 --entrypoint=/usr/bin/distribution {{ .DockerRegistryImage }} serve /etc/registry/config.yaml + ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --log-driver=journald --replace --name=iri-registry -v /var/lib/iri-registry:/var/lib/registry:ro -v /etc/iri-registry/certs:/certs:ro -e REGISTRY_HTTP_ADDR=0.0.0.0:22625 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/tls.crt -e REGISTRY_HTTP_TLS_KEY=/certs/tls.key -u 0 --entrypoint=/usr/bin/distribution {{ .DockerRegistryImage }} serve /etc/registry/config.yaml ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id diff --git a/pkg/controller/internalreleaseimage/templates/worker/files/root-ca.yaml b/pkg/controller/internalreleaseimage/templates/worker/files/root-ca.yaml new file mode 100644 index 0000000000..3ca105ab4d --- /dev/null +++ b/pkg/controller/internalreleaseimage/templates/worker/files/root-ca.yaml @@ -0,0 +1,5 @@ +mode: 0644 +path: "/etc/pki/ca-trust/source/anchors/root-ca.crt" +contents: + inline: |- +{{indent 4 .RootCA}}