Skip to content

Commit

Permalink
Install extensions via Containerfile for OCL
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
umohnani8 committed Nov 20, 2024
1 parent 478aa75 commit 2581c8f
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}}
Expand Down
2 changes: 2 additions & 0 deletions pkg/controller/build/buildrequest/buildrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,15 @@ func (br buildRequestImpl) renderContainerfile() (string, error) {
ReleaseVersion string
BaseOSImage string
ExtensionsImage string
Extensions []string
}{
MachineOSBuild: br.opts.MachineOSBuild,
MachineOSConfig: br.opts.MachineOSConfig,
UserContainerfile: br.userContainerfile,
ReleaseVersion: br.opts.getReleaseVersion(),
BaseOSImage: br.opts.getBaseOSImagePullspec(),
ExtensionsImage: br.opts.getExtensionsImagePullspec(),
Extensions: br.opts.getExtensions(),
}

if err := tmpl.Execute(out, items); err != nil {
Expand Down
21 changes: 15 additions & 6 deletions pkg/controller/build/buildrequest/buildrequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("COPY --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("COPY --from=%s", osImageURLConfig.BaseOSContainerImage),
"extensions=\"usbguard\"",
},
},
{
Expand Down Expand Up @@ -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",
"COPY --from=base-ext-image-from-machineosconfig",
"extensions=\"usbguard\"",
"LABEL releaseversion=release-version-from-machineosconfig",
},
unexpectedContainerfileContents: expectedContents(),
Expand Down
8 changes: 8 additions & 0 deletions pkg/controller/build/buildrequest/buildrequestopts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
61 changes: 60 additions & 1 deletion test/e2e-techpreview/onclusterlayering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -113,12 +116,14 @@ 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,
},
useEtcPkiEntitlement: true,
useExtensions: true,
})

cs := framework.NewClientSet("")
Expand All @@ -129,12 +134,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) {
Expand All @@ -151,6 +158,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,
Expand Down Expand Up @@ -923,6 +954,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", "kerberos"},
},
}

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)

Expand Down

0 comments on commit 2581c8f

Please sign in to comment.