Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MCO-1331: Install extensions via Containerfile for OCL #4705

Merged
merged 3 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 && \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On RHCOS and SCOS one extension may resolve to multiple RPMs, or an RPM with a different name, which I don't think is currently accounted for, see

func getSupportedExtensions() map[string][]string {
// In future when list of extensions grow, it will make
// more sense to populate it in a dynamic way.
// These are RHCOS supported extensions.
// Each extension keeps a list of packages required to get enabled on host.
return map[string][]string{
"wasm": {"crun-wasm"},
"ipsec": {"NetworkManager-libreswan", "libreswan"},
"usbguard": {"usbguard"},
"kerberos": {"krb5-workstation", "libkadm5"},
"kernel-devel": {"kernel-devel", "kernel-headers"},
"sandboxed-containers": {"kata-containers"},
"sysstat": {"sysstat"},
}
}
and
extensions := getSupportedExtensions()
for _, ext := range added {
for _, pkg := range extensions[ext] {
extArgs = append(extArgs, "--install", pkg)
}
}

On FCOS, the relationship is 1:1, but I'm not sure whether we still need to handle that now that OKD is moving to SCOS.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened #4714 whose commits we could cherry-pick into this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cheesesashimi, I have cherry-picked those commits here

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"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usbguard is one of the more trivial extensions that contains only one RPM with the same name. Could we use e.g. the ipsec extension (which resolves to NetworkManager-libreswan and libreswan RPMs) for the test case instead?

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\"",
},
},
{
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",
"RUN --mount=type=bind,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
52 changes: 51 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,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("")
Expand All @@ -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, true)

t.Log(helpers.ExecCmdOnNode(t, cs, node, "chroot", "/rootfs", "cowsay", "Moo!"))

unlabelFunc()

assertNodeRevertsToNonLayered(t, cs, node)
assertExtensionInstalledOnNode(t, cs, node, false)
}

func assertNodeRevertsToNonLayered(t *testing.T, cs *framework.ClientSet, node corev1.Node) {
Expand All @@ -151,6 +157,22 @@ 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, shouldExist bool) {
foundPkg, err := helpers.ExecCmdOnNodeWithError(cs, node, "chroot", "/rootfs", "rpm", "-q", "usbguard")
if shouldExist {
require.NoError(t, err, "usbguard extension not found")
if strings.Contains(foundPkg, "package usbguard is not installed") {
t.Fatalf("usbguard package not installed on node %s, got %s", node.Name, foundPkg)
}
t.Logf("usbguard extension installed, got %s", foundPkg)
} else {
if !strings.Contains(foundPkg, "package usbguard is not installed") {
t.Fatalf("usbguard package is installed on node %s, got %s", node.Name, foundPkg)
}
t.Logf("usbguard extension not installed as expected, got %s", foundPkg)
}
}

// 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 +945,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)

Expand Down