Skip to content

Commit c761b7c

Browse files
Merge pull request #4705 from umohnani8/extensions
MCO-1331: Install extensions via Containerfile for OCL
2 parents 66bf8b7 + fee1b99 commit c761b7c

File tree

9 files changed

+291
-65
lines changed

9 files changed

+291
-65
lines changed

pkg/controller/build/buildrequest/assets/Containerfile.on-cluster-build-template

+18-10
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,6 @@ COPY ./machineconfig/machineconfig.json.gz /tmp/machineconfig.json.gz
1010
RUN mkdir -p /etc/machine-config-daemon && \
1111
cat /tmp/machineconfig.json.gz | base64 -d | gunzip - > /etc/machine-config-daemon/currentconfig
1212

13-
{{if .ExtensionsImage}}
14-
# Pull our extensions image. Not sure yet what / how this should be wired up
15-
# though. Ideally, I'd like to use some Buildah tricks to have the extensions
16-
# directory mounted into the container at build-time so that I don't have to
17-
# copy the RPMs into the container, configure the repo, and do the
18-
# installation. Alternatively, I'd have to start a pod with an HTTP server.
19-
FROM {{.ExtensionsImage}} AS extensions
20-
{{end}}
21-
22-
2313
FROM {{.BaseOSImage}} AS configs
2414
# Copy the extracted MachineConfig into the expected place in the image.
2515
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
2818
# since it should be set by the container runtime / builder.
2919
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') && \
3020
ostree container commit
21+
# Install any extensions specified
22+
{{if and .ExtensionsImage .ExtensionsPackages}}
23+
# Mount the extensions image to use the content from it
24+
# and add the extensions repo to /etc/yum.repos.d/coreos-extensions.repo
25+
RUN --mount=type=bind,from={{.ExtensionsImage}},source=/,target=/tmp/mco-extensions/os-extensions-content,bind-propagation=rshared,rw,z \
26+
echo -e "[coreos-extensions]\n\
27+
enabled=1\n\
28+
metadata_expire=1m\n\
29+
baseurl=/tmp/mco-extensions/os-extensions-content/usr/share/rpm-ostree/extensions/\n\
30+
gpgcheck=0\n\
31+
skip_if_unavailable=False" > /etc/yum.repos.d/coreos-extensions.repo && \
32+
chmod 644 /etc/yum.repos.d/coreos-extensions.repo && \
33+
extensions="{{- range $index, $item := .ExtensionsPackages }}{{- if $index }} {{ end }}{{$item}}{{- end }}" && \
34+
echo "Installing extension packages: $extensions" && \
35+
rpm-ostree install $extensions && \
36+
rm /etc/yum.repos.d/coreos-extensions.repo
37+
RUN ostree container commit
38+
{{end}}
3139

3240
COPY ./openshift-config-user-ca-bundle.crt /etc/pki/ca-trust/source/anchors/openshift-config-user-ca-bundle.crt
3341
RUN update-ca-trust

pkg/controller/build/buildrequest/buildrequest.go

+19-12
Original file line numberDiff line numberDiff line change
@@ -214,26 +214,33 @@ func (br buildRequestImpl) renderContainerfile() (string, error) {
214214
return "", fmt.Errorf("could not parse containerfile template: %w", err)
215215
}
216216

217+
extPkgs, err := br.opts.getExtensionsPackages()
218+
if err != nil {
219+
return "", err
220+
}
221+
217222
out := &strings.Builder{}
218223

219224
// This anonymous struct is necessary because templates cannot access
220225
// lowercase fields. Additionally, since there are a few fields where we
221226
// default to a value from a different location, it makes more sense for us
222227
// to implement that logic in Go as opposed to the Go template language.
223228
items := struct {
224-
MachineOSBuild *mcfgv1alpha1.MachineOSBuild
225-
MachineOSConfig *mcfgv1alpha1.MachineOSConfig
226-
UserContainerfile string
227-
ReleaseVersion string
228-
BaseOSImage string
229-
ExtensionsImage string
229+
MachineOSBuild *mcfgv1alpha1.MachineOSBuild
230+
MachineOSConfig *mcfgv1alpha1.MachineOSConfig
231+
UserContainerfile string
232+
ReleaseVersion string
233+
BaseOSImage string
234+
ExtensionsImage string
235+
ExtensionsPackages []string
230236
}{
231-
MachineOSBuild: br.opts.MachineOSBuild,
232-
MachineOSConfig: br.opts.MachineOSConfig,
233-
UserContainerfile: br.userContainerfile,
234-
ReleaseVersion: br.opts.getReleaseVersion(),
235-
BaseOSImage: br.opts.getBaseOSImagePullspec(),
236-
ExtensionsImage: br.opts.getExtensionsImagePullspec(),
237+
MachineOSBuild: br.opts.MachineOSBuild,
238+
MachineOSConfig: br.opts.MachineOSConfig,
239+
UserContainerfile: br.userContainerfile,
240+
ReleaseVersion: br.opts.getReleaseVersion(),
241+
BaseOSImage: br.opts.getBaseOSImagePullspec(),
242+
ExtensionsImage: br.opts.getExtensionsImagePullspec(),
243+
ExtensionsPackages: extPkgs,
237244
}
238245

239246
if err := tmpl.Execute(out, items); err != nil {

pkg/controller/build/buildrequest/buildrequest_test.go

+45-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ const (
2121
mcoImagePullspec = "registry.hostname.com/org/repo@sha256:87980e0edfc86d01182f70c53527f74b5b01df00fe6d47668763d228d4de43a9"
2222
)
2323

24+
// Validates that if an invalid extension is provided that the ConfigMap
25+
// generation fails and the error contains the names of the invalid extensions.
26+
func TestBuildRequestInvalidExtensions(t *testing.T) {
27+
t.Parallel()
28+
29+
opts := getBuildRequestOpts()
30+
opts.MachineConfig.Spec.Extensions = []string{"invalid-ext1", "invalid-ext2"}
31+
32+
br := newBuildRequest(opts)
33+
34+
_, err := br.ConfigMaps()
35+
assert.Error(t, err)
36+
37+
for _, ext := range opts.MachineConfig.Spec.Extensions {
38+
assert.Contains(t, err.Error(), ext)
39+
}
40+
}
41+
2442
// Tests that the BuildRequest is constructed as expected.
2543
func TestBuildRequest(t *testing.T) {
2644
t.Parallel()
@@ -42,21 +60,40 @@ func TestBuildRequest(t *testing.T) {
4260
unexpectedContainerfileContents []string
4361
}{
4462
{
45-
name: "With extensions image",
46-
optsFunc: getBuildRequestOpts,
63+
name: "With extensions image and extensions",
64+
optsFunc: func() BuildRequestOpts {
65+
opts := getBuildRequestOpts()
66+
opts.MachineConfig.Spec.Extensions = []string{"usbguard"}
67+
return opts
68+
},
69+
expectedContainerfileContents: append(expectedContents(), []string{
70+
fmt.Sprintf("RUN --mount=type=bind,from=%s", osImageURLConfig.BaseOSExtensionsContainerImage),
71+
`extensions="usbguard"`,
72+
}...),
73+
},
74+
{
75+
name: "With extensions image and resolved extensions packages",
76+
optsFunc: func() BuildRequestOpts {
77+
opts := getBuildRequestOpts()
78+
opts.MachineConfig.Spec.Extensions = []string{"kerberos", "usbguard"}
79+
return opts
80+
},
4781
expectedContainerfileContents: append(expectedContents(), []string{
48-
fmt.Sprintf("FROM %s AS extensions", osImageURLConfig.BaseOSExtensionsContainerImage),
82+
fmt.Sprintf("RUN --mount=type=bind,from=%s", osImageURLConfig.BaseOSExtensionsContainerImage),
83+
`extensions="krb5-workstation libkadm5 usbguard"`,
4984
}...),
5085
},
5186
{
52-
name: "Missing extensions image",
87+
name: "Missing extensions image and extensions",
5388
optsFunc: func() BuildRequestOpts {
5489
opts := getBuildRequestOpts()
5590
opts.OSImageURLConfig.BaseOSExtensionsContainerImage = ""
91+
opts.MachineConfig.Spec.Extensions = []string{"usbguard"}
5692
return opts
5793
},
5894
unexpectedContainerfileContents: []string{
59-
fmt.Sprintf("FROM %s AS extensions", osImageURLConfig.BaseOSContainerImage),
95+
fmt.Sprintf("RUN --mount=type=bind,from=%s", osImageURLConfig.BaseOSContainerImage),
96+
"extensions=\"usbguard\"",
6097
},
6198
},
6299
{
@@ -100,12 +137,14 @@ func TestBuildRequest(t *testing.T) {
100137
opts.MachineOSConfig.Spec.BuildInputs.BaseOSImagePullspec = "base-os-image-from-machineosconfig"
101138
opts.MachineOSConfig.Spec.BuildInputs.BaseOSExtensionsImagePullspec = "base-ext-image-from-machineosconfig"
102139
opts.MachineOSConfig.Spec.BuildInputs.ReleaseVersion = "release-version-from-machineosconfig"
140+
opts.MachineConfig.Spec.Extensions = []string{"usbguard"}
103141
return opts
104142
},
105143
expectedContainerfileContents: []string{
106144
"FROM base-os-image-from-machineosconfig AS extract",
107145
"FROM base-os-image-from-machineosconfig AS configs",
108-
"FROM base-ext-image-from-machineosconfig AS extensions",
146+
"RUN --mount=type=bind,from=base-ext-image-from-machineosconfig",
147+
"extensions=\"usbguard\"",
109148
"LABEL releaseversion=release-version-from-machineosconfig",
110149
},
111150
unexpectedContainerfileContents: expectedContents(),

pkg/controller/build/buildrequest/buildrequestopts.go

+9
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ func (b BuildRequestOpts) getReleaseVersion() string {
7474
return b.OSImageURLConfig.ReleaseVersion
7575
}
7676

77+
// Gets the packages for the extensions from the MachineConfig, if available.
78+
func (b BuildRequestOpts) getExtensionsPackages() ([]string, error) {
79+
if len(b.MachineConfig.Spec.Extensions) == 0 {
80+
return nil, nil
81+
}
82+
83+
return ctrlcommon.GetPackagesForSupportedExtensions(b.MachineConfig.Spec.Extensions)
84+
}
85+
7786
// Gets all of the image build request opts from the Kube API server.
7887
func newBuildRequestOptsFromAPI(ctx context.Context, kubeclient clientset.Interface, mcfgclient mcfgclientset.Interface, mosb *mcfgv1alpha1.MachineOSBuild, mosc *mcfgv1alpha1.MachineOSConfig) (*BuildRequestOpts, error) {
7988
og := optsGetter{

pkg/controller/common/helpers.go

+57
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,63 @@ func ValidateMachineConfig(cfg mcfgv1.MachineConfigSpec) error {
612612
return nil
613613
}
614614

615+
// Validates that a given MachineConfig's extensions are supported.
616+
func ValidateMachineConfigExtensions(cfg mcfgv1.MachineConfigSpec) error {
617+
return validateExtensions(cfg.Extensions)
618+
}
619+
620+
func validateExtensions(exts []string) error {
621+
supportedExtensions := SupportedExtensions()
622+
invalidExts := []string{}
623+
for _, ext := range exts {
624+
if _, ok := supportedExtensions[ext]; !ok {
625+
invalidExts = append(invalidExts, ext)
626+
}
627+
}
628+
if len(invalidExts) != 0 {
629+
return fmt.Errorf("invalid extensions found: %v", invalidExts)
630+
}
631+
return nil
632+
}
633+
634+
// Resolves a list of supported extensions to the individual packages required
635+
// for each of those extensions. Returns an error is any of the supplied
636+
// extensions is invalid.
637+
func GetPackagesForSupportedExtensions(exts []string) ([]string, error) {
638+
if err := validateExtensions(exts); err != nil {
639+
return nil, err
640+
}
641+
642+
pkgs := []string{}
643+
644+
supported := SupportedExtensions()
645+
for _, ext := range exts {
646+
for _, pkg := range supported[ext] {
647+
pkgs = append(pkgs, pkg)
648+
}
649+
}
650+
651+
return pkgs, nil
652+
}
653+
654+
// Returns list of extensions possible to install on a CoreOS based system.
655+
func SupportedExtensions() map[string][]string {
656+
// In future when list of extensions grow, it will make
657+
// more sense to populate it in a dynamic way.
658+
659+
// These are RHCOS supported extensions.
660+
// Each extension keeps a list of packages required to get enabled on host.
661+
return map[string][]string{
662+
"wasm": {"crun-wasm"},
663+
"ipsec": {"NetworkManager-libreswan", "libreswan"},
664+
"usbguard": {"usbguard"},
665+
"kerberos": {"krb5-workstation", "libkadm5"},
666+
"kernel-devel": {"kernel-devel", "kernel-headers"},
667+
"sandboxed-containers": {"kata-containers"},
668+
"sysstat": {"sysstat"},
669+
}
670+
}
671+
615672
// IgnParseWrapper parses rawIgn for both V2 and V3 ignition configs and returns
616673
// a V2 or V3 Config or an error. This wrapper is necessary since V2 and V3 use different parsers.
617674
func IgnParseWrapper(rawIgn []byte) (interface{}, error) {

pkg/controller/common/helpers_test.go

+89
Original file line numberDiff line numberDiff line change
@@ -793,3 +793,92 @@ func TestParseAndConvertGzippedConfig(t *testing.T) {
793793
})
794794
}
795795
}
796+
797+
func TestValidateMachineConfigExtensions(t *testing.T) {
798+
t.Parallel()
799+
800+
testCases := []struct {
801+
name string
802+
extensions []string
803+
errExpected bool
804+
}{
805+
{
806+
name: "Supported",
807+
extensions: []string{"sysstat", "sandboxed-containers"},
808+
},
809+
{
810+
name: "Unsupported",
811+
extensions: []string{"unsupported1", "unsupported2"},
812+
errExpected: true,
813+
},
814+
}
815+
816+
for _, testCase := range testCases {
817+
testCase := testCase
818+
t.Run(testCase.name, func(t *testing.T) {
819+
t.Parallel()
820+
mcfgSpec := mcfgv1.MachineConfigSpec{
821+
Extensions: testCase.extensions,
822+
}
823+
824+
err := ValidateMachineConfigExtensions(mcfgSpec)
825+
826+
if testCase.errExpected {
827+
assert.Error(t, err)
828+
for _, ext := range testCase.extensions {
829+
assert.Contains(t, err.Error(), ext)
830+
}
831+
} else {
832+
assert.NoError(t, err)
833+
}
834+
})
835+
}
836+
}
837+
838+
func TestGetPackagesForSupportedExtensions(t *testing.T) {
839+
t.Parallel()
840+
841+
testCases := []struct {
842+
name string
843+
extensions []string
844+
expectedPackages []string
845+
errExpected bool
846+
}{
847+
{
848+
name: "Unsupported extension",
849+
extensions: []string{"unsupported"},
850+
errExpected: true,
851+
},
852+
{
853+
name: "Supported single package extension",
854+
extensions: []string{"wasm"},
855+
expectedPackages: []string{"crun-wasm"},
856+
},
857+
{
858+
name: "Supported single multi-package extension",
859+
extensions: []string{"kerberos"},
860+
expectedPackages: []string{"krb5-workstation", "libkadm5"},
861+
},
862+
{
863+
name: "Supported multiple multi-package extensions",
864+
extensions: []string{"ipsec", "kerberos"},
865+
expectedPackages: []string{"NetworkManager-libreswan", "libreswan", "krb5-workstation", "libkadm5"},
866+
},
867+
}
868+
869+
for _, testCase := range testCases {
870+
testCase := testCase
871+
t.Run(testCase.name, func(t *testing.T) {
872+
t.Parallel()
873+
874+
pkgs, err := GetPackagesForSupportedExtensions(testCase.extensions)
875+
if testCase.errExpected {
876+
assert.Error(t, err)
877+
return
878+
}
879+
880+
assert.NoError(t, err)
881+
assert.Equal(t, testCase.expectedPackages, pkgs)
882+
})
883+
}
884+
}

pkg/daemon/daemon.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -2862,7 +2862,7 @@ func (dn *Daemon) getUnsupportedPackages() {
28622862
}
28632863

28642864
supportedPackages := make(map[string]bool)
2865-
for _, packages := range getSupportedExtensions() {
2865+
for _, packages := range ctrlcommon.SupportedExtensions() {
28662866
for _, pkg := range packages {
28672867
supportedPackages[pkg] = true
28682868
}

0 commit comments

Comments
 (0)