From abd2284efd1a4881c59c83d2c43560b49ea026bd Mon Sep 17 00:00:00 2001 From: Urvashi Date: Mon, 11 Nov 2024 20:25:18 -0500 Subject: [PATCH] Install extensions via Containerfile for OCL Add logic to the Containerfile used to build the new OS image to be able to install extensions when using OCL. Extensions are installed via rpm-ostree and commited to the container image. Signed-off-by: Urvashi --- .../Containerfile.on-cluster-build-template | 28 +++++---- .../build/buildrequest/buildrequest.go | 2 + .../build/buildrequest/buildrequest_test.go | 21 +++++-- .../build/buildrequest/buildrequestopts.go | 8 +++ .../e2e-techpreview/onclusterlayering_test.go | 60 ++++++++++++++++++- 5 files changed, 102 insertions(+), 17 deletions(-) diff --git a/pkg/controller/build/buildrequest/assets/Containerfile.on-cluster-build-template b/pkg/controller/build/buildrequest/assets/Containerfile.on-cluster-build-template index 5cdbb6f468..56961f770e 100644 --- a/pkg/controller/build/buildrequest/assets/Containerfile.on-cluster-build-template +++ b/pkg/controller/build/buildrequest/assets/Containerfile.on-cluster-build-template @@ -10,16 +10,6 @@ COPY ./machineconfig/machineconfig.json.gz /tmp/machineconfig.json.gz RUN mkdir -p /etc/machine-config-daemon && \ cat /tmp/machineconfig.json.gz | base64 -d | gunzip - > /etc/machine-config-daemon/currentconfig -{{if .ExtensionsImage}} -# Pull our extensions image. Not sure yet what / how this should be wired up -# though. Ideally, I'd like to use some Buildah tricks to have the extensions -# directory mounted into the container at build-time so that I don't have to -# copy the RPMs into the container, configure the repo, and do the -# installation. Alternatively, I'd have to start a pod with an HTTP server. -FROM {{.ExtensionsImage}} AS extensions -{{end}} - - FROM {{.BaseOSImage}} AS configs # Copy the extracted MachineConfig into the expected place in the image. COPY --from=extract /etc/machine-config-daemon/currentconfig /etc/machine-config-daemon/currentconfig @@ -28,6 +18,24 @@ COPY --from=extract /etc/machine-config-daemon/currentconfig /etc/machine-config # since it should be set by the container runtime / builder. RUN container="oci" exec -a ignition-apply /usr/lib/dracut/modules.d/30ignition/ignition --ignore-unsupported <(cat /etc/machine-config-daemon/currentconfig | jq '.spec.config') && \ ostree container commit +# Install any extensions specified +{{if and .ExtensionsImage .Extensions}} +# Mount the extensions image to use the content from it +# and add the extensions repo to /etc/yum.repos.d/coreos-extensions.repo +RUN --mount=type=bind,from={{.ExtensionsImage}},source=/,target=/tmp/mco-extensions/os-extensions-content,bind-propagation=rshared,rw,z \ + echo -e "[coreos-extensions]\n\ +enabled=1\n\ +metadata_expire=1m\n\ +baseurl=/tmp/mco-extensions/os-extensions-content/usr/share/rpm-ostree/extensions/\n\ +gpgcheck=0\n\ +skip_if_unavailable=False" > /etc/yum.repos.d/coreos-extensions.repo && \ + chmod 644 /etc/yum.repos.d/coreos-extensions.repo && \ + extensions="{{- range $index, $item := .Extensions }}{{- if $index }} {{ end }}{{$item}}{{- end }}" && \ + echo "Installing packages: $extensions" && \ + rpm-ostree install $extensions && \ + rm /etc/yum.repos.d/coreos-extensions.repo +RUN ostree container commit +{{end}} LABEL machineconfig={{.MachineOSBuild.Spec.DesiredConfig.Name}} LABEL machineconfigpool={{.MachineOSConfig.Spec.MachineConfigPool.Name}} diff --git a/pkg/controller/build/buildrequest/buildrequest.go b/pkg/controller/build/buildrequest/buildrequest.go index e682dfef12..9dc4aa0fe7 100644 --- a/pkg/controller/build/buildrequest/buildrequest.go +++ b/pkg/controller/build/buildrequest/buildrequest.go @@ -211,6 +211,7 @@ func (br buildRequestImpl) renderContainerfile() (string, error) { ReleaseVersion string BaseOSImage string ExtensionsImage string + Extensions []string }{ MachineOSBuild: br.opts.MachineOSBuild, MachineOSConfig: br.opts.MachineOSConfig, @@ -218,6 +219,7 @@ func (br buildRequestImpl) renderContainerfile() (string, error) { ReleaseVersion: br.opts.getReleaseVersion(), BaseOSImage: br.opts.getBaseOSImagePullspec(), ExtensionsImage: br.opts.getExtensionsImagePullspec(), + Extensions: br.opts.getExtensions(), } if err := tmpl.Execute(out, items); err != nil { diff --git a/pkg/controller/build/buildrequest/buildrequest_test.go b/pkg/controller/build/buildrequest/buildrequest_test.go index 557da56ec1..6b9c543d99 100644 --- a/pkg/controller/build/buildrequest/buildrequest_test.go +++ b/pkg/controller/build/buildrequest/buildrequest_test.go @@ -40,21 +40,28 @@ func TestBuildRequest(t *testing.T) { unexpectedContainerfileContents []string }{ { - name: "With extensions image", - optsFunc: getBuildRequestOpts, + name: "With extensions image and extensions", + optsFunc: func() BuildRequestOpts { + opts := getBuildRequestOpts() + opts.MachineConfig.Spec.Extensions = []string{"usbguard"} + return opts + }, expectedContainerfileContents: append(expectedContents(), []string{ - fmt.Sprintf("FROM %s AS extensions", osImageURLConfig.BaseOSExtensionsContainerImage), + fmt.Sprintf("RUN --mount=type=bind,from=%s", osImageURLConfig.BaseOSExtensionsContainerImage), + "extensions=\"usbguard\"", }...), }, { - name: "Missing extensions image", + name: "Missing extensions image and extensions", optsFunc: func() BuildRequestOpts { opts := getBuildRequestOpts() opts.OSImageURLConfig.BaseOSExtensionsContainerImage = "" + opts.MachineConfig.Spec.Extensions = []string{"usbguard"} return opts }, unexpectedContainerfileContents: []string{ - fmt.Sprintf("FROM %s AS extensions", osImageURLConfig.BaseOSContainerImage), + fmt.Sprintf("RUN --mount=type=bind,from=%s", osImageURLConfig.BaseOSContainerImage), + "extensions=\"usbguard\"", }, }, { @@ -98,12 +105,14 @@ func TestBuildRequest(t *testing.T) { opts.MachineOSConfig.Spec.BuildInputs.BaseOSImagePullspec = "base-os-image-from-machineosconfig" opts.MachineOSConfig.Spec.BuildInputs.BaseOSExtensionsImagePullspec = "base-ext-image-from-machineosconfig" opts.MachineOSConfig.Spec.BuildInputs.ReleaseVersion = "release-version-from-machineosconfig" + opts.MachineConfig.Spec.Extensions = []string{"usbguard"} return opts }, expectedContainerfileContents: []string{ "FROM base-os-image-from-machineosconfig AS extract", "FROM base-os-image-from-machineosconfig AS configs", - "FROM base-ext-image-from-machineosconfig AS extensions", + "RUN --mount=type=bind,from=base-ext-image-from-machineosconfig", + "extensions=\"usbguard\"", "LABEL releaseversion=release-version-from-machineosconfig", }, unexpectedContainerfileContents: expectedContents(), diff --git a/pkg/controller/build/buildrequest/buildrequestopts.go b/pkg/controller/build/buildrequest/buildrequestopts.go index c05d8af47c..2e86d6a723 100644 --- a/pkg/controller/build/buildrequest/buildrequestopts.go +++ b/pkg/controller/build/buildrequest/buildrequestopts.go @@ -68,6 +68,14 @@ func (b BuildRequestOpts) getReleaseVersion() string { return b.OSImageURLConfig.ReleaseVersion } +// Gets the extensions from the MachineConfig if available. +func (b BuildRequestOpts) getExtensions() []string { + if len(b.MachineConfig.Spec.Extensions) > 0 { + return b.MachineConfig.Spec.Extensions + } + return []string{} +} + // Gets all of the image build request opts from the Kube API server. func newBuildRequestOptsFromAPI(ctx context.Context, kubeclient clientset.Interface, mcfgclient mcfgclientset.Interface, mosb *mcfgv1alpha1.MachineOSBuild, mosc *mcfgv1alpha1.MachineOSConfig) (*BuildRequestOpts, error) { og := optsGetter{ diff --git a/test/e2e-techpreview/onclusterlayering_test.go b/test/e2e-techpreview/onclusterlayering_test.go index f8a781b9e9..3f9ec4d69f 100644 --- a/test/e2e-techpreview/onclusterlayering_test.go +++ b/test/e2e-techpreview/onclusterlayering_test.go @@ -88,6 +88,9 @@ type onClusterLayeringTestOpts struct { // Inject YUM repo information from a Centos 9 stream container useYumRepos bool + + // Add Extensions for testing + useExtensions bool } func TestOnClusterBuildsOnOKD(t *testing.T) { @@ -113,12 +116,13 @@ func TestOnClusterBuildsCustomPodBuilder(t *testing.T) { // Tests that an on-cluster build can be performed and that the resulting image // is rolled out to an opted-in node. -func TestOnClusterBuildRollsOutImage(t *testing.T) { +func TestOnClusterBuildRollsOutImageWithExtensionsInstalled(t *testing.T) { imagePullspec := runOnClusterLayeringTest(t, onClusterLayeringTestOpts{ poolName: layeredMCPName, customDockerfiles: map[string]string{ layeredMCPName: cowsayDockerfile, }, + useExtensions: true, }) cs := framework.NewClientSet("") @@ -129,12 +133,14 @@ func TestOnClusterBuildRollsOutImage(t *testing.T) { helpers.AssertNodeBootedIntoImage(t, cs, node, imagePullspec) t.Logf("Node %s is booted into image %q", node.Name, imagePullspec) + assertExtensionInstalledOnNode(t, cs, node) t.Log(helpers.ExecCmdOnNode(t, cs, node, "chroot", "/rootfs", "cowsay", "Moo!")) unlabelFunc() assertNodeRevertsToNonLayered(t, cs, node) + assertExtensionNotOnNode(t, cs, node) } func assertNodeRevertsToNonLayered(t *testing.T, cs *framework.ClientSet, node corev1.Node) { @@ -151,6 +157,30 @@ func assertNodeRevertsToNonLayered(t *testing.T, cs *framework.ClientSet, node c helpers.AssertFileNotOnNode(t, cs, node, runtimeassets.RevertServiceMachineConfigFile) } +func assertExtensionInstalledOnNode(t *testing.T, cs *framework.ClientSet, node corev1.Node) { + foundPkg, err := helpers.ExecCmdOnNodeWithError(cs, node, "rpm", "-q", "usbguard") + require.NoError(t, err, "usbguard extension not found") + t.Logf("usbguard extension installed, got %s", foundPkg) + + foundPkg, err = helpers.ExecCmdOnNodeWithError(cs, node, "rpm", "-q", "kerberos") + require.NoError(t, err, "kerberos extension not found") + t.Logf("kerberos extension installed, got %s", foundPkg) + + t.Logf("Node %s has both usbguard and kerberos extensions installed", node.Name) +} + +func assertExtensionNotOnNode(t *testing.T, cs *framework.ClientSet, node corev1.Node) { + foundPkg, err := helpers.ExecCmdOnNodeWithError(cs, node, "rpm", "-q", "usbguard") + require.Error(t, err, "usbguard extension is on node") + t.Logf("usbguard extension not installed as expected, got %s", foundPkg) + + foundPkg, err = helpers.ExecCmdOnNodeWithError(cs, node, "rpm", "-q", "kerberos") + require.Error(t, err, "kerberos extension is on node") + t.Logf("kerberos extension not installed as expected, got %s", foundPkg) + + t.Logf("Node %s does not have usbguard and kerberos extensions installed as expected", node.Name) +} + // This test extracts the /etc/yum.repos.d and /etc/pki/rpm-gpg content from a // Centos Stream 9 image and injects them into the MCO namespace. It then // performs a build with the expectation that these artifacts will be used, @@ -923,6 +953,34 @@ func prepareForOnClusterLayeringTest(t *testing.T, cs *framework.ClientSet, test t.Cleanup(makeIdempotentAndRegister(t, helpers.CreateMCP(t, cs, testOpts.poolName))) } + if testOpts.useExtensions { + extensionsMC := &mcfgv1.MachineConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "99-extensions", + Labels: helpers.MCLabelForRole(testOpts.poolName), + }, + Spec: mcfgv1.MachineConfigSpec{ + Config: runtime.RawExtension{ + Raw: helpers.MarshalOrDie(ctrlcommon.NewIgnConfig()), + }, + Extensions: []string{"usbguard"}, + }, + } + + helpers.SetMetadataOnObject(t, extensionsMC) + // Apply the extensions MC + mcCleanupFunc := helpers.ApplyMC(t, cs, extensionsMC) + t.Cleanup(func() { + mcCleanupFunc() + t.Logf("Deleted MachineConfig %s", extensionsMC.Name) + }) + t.Logf("Created new MachineConfig %q", extensionsMC.Name) + // Wait for rendered config to finish creating + renderedConfig, err := helpers.WaitForRenderedConfig(t, cs, testOpts.poolName, extensionsMC.Name) + require.NoError(t, err) + t.Logf("Finished rendering config %s", renderedConfig) + } + _, err := helpers.WaitForRenderedConfig(t, cs, testOpts.poolName, "00-worker") require.NoError(t, err)