Skip to content

Commit 6a29a07

Browse files
committed
fixes systemd unit creation for empty units
If a MachineConfig is applied with empty systemd unit contents, the MCD will degrade because it skips writing the file in that particular situation. For parity with the CoreOS Ignition implementation, we should not attempt to enable or disable any systemd units where the unit file does not have any contents.
1 parent b5688e7 commit 6a29a07

File tree

4 files changed

+251
-7
lines changed

4 files changed

+251
-7
lines changed

pkg/daemon/file_writers.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,8 @@ func writeUnit(u ign3types.Unit, systemdRoot string, isCoreOSVariant bool) error
290290
return nil
291291
}
292292

293-
if u.Contents != nil && *u.Contents != "" {
293+
switch {
294+
case unitHasContent(u):
294295
klog.Infof("Writing systemd unit %q", u.Name)
295296
if _, err := os.Stat(withUsrPath(fpath)); err == nil &&
296297
isCoreOSVariant {
@@ -315,7 +316,7 @@ func writeUnit(u ign3types.Unit, systemdRoot string, isCoreOSVariant bool) error
315316
}
316317

317318
klog.V(2).Infof("Successfully wrote systemd unit %q: ", u.Name)
318-
} else if u.Mask != nil && !*u.Mask {
319+
case u.Mask != nil && !*u.Mask:
319320
// if mask is explicitly set to false, make sure to remove a previous mask
320321
// see https://bugzilla.redhat.com/show_bug.cgi?id=1966445
321322
// Note that this does not catch all cleanup cases; for example, if the previous machine config specified
@@ -325,11 +326,20 @@ func writeUnit(u ign3types.Unit, systemdRoot string, isCoreOSVariant bool) error
325326
if err := os.RemoveAll(fpath); err != nil {
326327
return fmt.Errorf("failed to cleanup %s: %w", fpath, err)
327328
}
329+
default:
330+
klog.Infof("Unit %q has no content, skipping write", u.Name)
328331
}
329332

330333
return nil
331334
}
332335

336+
// Determines if a systemd unit has contents by first checking whether the
337+
// contents field is nil and then checking if the contents field is an empty
338+
// string.
339+
func unitHasContent(u ign3types.Unit) bool {
340+
return u.Contents != nil && *u.Contents != ""
341+
}
342+
333343
// writeUnits writes systemd units and their dropins to disk
334344
func writeUnits(units []ign3types.Unit, systemdRoot string, isCoreOSVariant bool) error {
335345
for _, u := range units {

pkg/daemon/update.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,10 +2119,20 @@ func (dn *Daemon) writeUnits(units []ign3types.Unit) error {
21192119
// to not go through.
21202120

21212121
if u.Enabled != nil {
2122-
if *u.Enabled {
2123-
enabledUnits = append(enabledUnits, u.Name)
2122+
// Only when a unit has contents should we attempt to enable or disable it.
2123+
// See: https://issues.redhat.com/browse/OCPBUGS-56648
2124+
if unitHasContent(u) {
2125+
if *u.Enabled {
2126+
enabledUnits = append(enabledUnits, u.Name)
2127+
} else {
2128+
disabledUnits = append(disabledUnits, u.Name)
2129+
}
21242130
} else {
2125-
disabledUnits = append(disabledUnits, u.Name)
2131+
action := "disable"
2132+
if *u.Enabled {
2133+
action = "enable"
2134+
}
2135+
klog.Infof("Could not %s unit %q, because it has no contents, skipping", action, u.Name)
21262136
}
21272137
} else {
21282138
if err := dn.presetUnit(u); err != nil {

test/e2e-1of2/mcd_test.go

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ func TestDontDeleteRPMFiles(t *testing.T) {
620620
func TestIgn3Cfg(t *testing.T) {
621621
cs := framework.NewClientSet("")
622622

623-
delete := helpers.CreateMCP(t, cs, "infra")
623+
deleteFunc := helpers.CreateMCP(t, cs, "infra")
624624
workerOldMc := helpers.GetMcName(t, cs, "worker")
625625

626626
unlabelFunc := helpers.LabelRandomNodeFromPool(t, cs, "worker", "node-role.kubernetes.io/infra")
@@ -631,7 +631,7 @@ func TestIgn3Cfg(t *testing.T) {
631631
if err := helpers.WaitForPoolComplete(t, cs, "worker", workerOldMc); err != nil {
632632
t.Fatal(err)
633633
}
634-
delete()
634+
deleteFunc()
635635
})
636636
// create a dummy MC with an sshKey for user Core
637637
mcName := fmt.Sprintf("99-ign3cfg-infra-%s", uuid.NewUUID())
@@ -650,6 +650,59 @@ func TestIgn3Cfg(t *testing.T) {
650650
tempFile := ign3types.File{Node: ign3types.Node{Path: "/etc/testfileconfig"},
651651
FileEmbedded1: ign3types.FileEmbedded1{Contents: ign3types.Resource{Source: &testfiledata}, Mode: &mode}}
652652
testIgn3Config.Storage.Files = append(testIgn3Config.Storage.Files, tempFile)
653+
654+
overrideName := "override.conf"
655+
testIgn3Config.Systemd.Units = []ign3types.Unit{
656+
{
657+
Name: "new-svc.service",
658+
Enabled: helpers.BoolToPtr(true),
659+
Contents: getSystemdUnitContents("New Service"),
660+
},
661+
{
662+
Name: "new-svc-with-dropin.service",
663+
Enabled: helpers.BoolToPtr(true),
664+
Contents: getSystemdUnitContents("New Service With Dropin"),
665+
Dropins: []ign3types.Dropin{
666+
{
667+
Name: overrideName,
668+
Contents: helpers.StrToPtr("[Unit]\nDescription=Dropin Override"),
669+
},
670+
},
671+
},
672+
{
673+
Name: "new-empty-svc.service",
674+
Enabled: helpers.BoolToPtr(true),
675+
},
676+
{
677+
Name: "new-empty-svc-with-empty-dropin.service",
678+
Enabled: helpers.BoolToPtr(true),
679+
Dropins: []ign3types.Dropin{
680+
{
681+
Name: overrideName,
682+
},
683+
},
684+
},
685+
{
686+
Name: "new-empty-svc-with-dropin.service",
687+
Enabled: helpers.BoolToPtr(true),
688+
Dropins: []ign3types.Dropin{
689+
{
690+
Name: overrideName,
691+
Contents: getSystemdUnitContents("Empty Service With Dropin Override"),
692+
},
693+
},
694+
},
695+
{
696+
Name: "new-empty-disabled-svc.service",
697+
Enabled: helpers.BoolToPtr(false),
698+
},
699+
{
700+
Name: "new-disabled-svc.service",
701+
Enabled: helpers.BoolToPtr(false),
702+
Contents: getSystemdUnitContents("New Disabled Service"),
703+
},
704+
}
705+
653706
rawIgnConfig := helpers.MarshalOrDie(testIgn3Config)
654707
mcadd.Spec.Config.Raw = rawIgnConfig
655708

@@ -682,6 +735,46 @@ func TestIgn3Cfg(t *testing.T) {
682735
}
683736
t.Logf("Node %s has file", infraNode.Name)
684737

738+
systemdUnitAssertions := map[string]func(*testing.T, string){
739+
"new-svc.service": func(t *testing.T, name string) {
740+
assertSystemdUnitFileExists(t, cs, infraNode, name)
741+
assertSystemdUnitIsEnabled(t, cs, infraNode, name)
742+
},
743+
"new-svc-with-dropin.service": func(t *testing.T, name string) {
744+
assertSystemdUnitFileExists(t, cs, infraNode, name)
745+
assertSystemdUnitIsEnabled(t, cs, infraNode, name)
746+
assertSystemdUnitDropinFileExists(t, cs, infraNode, name, overrideName)
747+
assertSystemdUnitHasDropins(t, cs, infraNode, name, []string{overrideName})
748+
},
749+
"new-empty-svc.service": func(t *testing.T, name string) {
750+
assertSystemdUnitFileDoesNotExist(t, cs, infraNode, name)
751+
assertSystemdUnitDoesNotExist(t, cs, infraNode, name)
752+
},
753+
"new-empty-svc-with-empty-dropin.service": func(t *testing.T, name string) {
754+
assertSystemdUnitDoesNotExist(t, cs, infraNode, name)
755+
assertSystemdUnitFileDoesNotExist(t, cs, infraNode, name)
756+
assertSystemdUnitDropinFileDoesNotExist(t, cs, infraNode, name, overrideName)
757+
},
758+
"new-empty-svc-with-dropin.service": func(t *testing.T, name string) {
759+
assertSystemdUnitFileDoesNotExist(t, cs, infraNode, name)
760+
assertSystemdUnitDropinFileExists(t, cs, infraNode, name, overrideName)
761+
},
762+
"new-empty-disabled-svc.service": func(t *testing.T, name string) {
763+
assertSystemdUnitDoesNotExist(t, cs, infraNode, name)
764+
assertSystemdUnitFileDoesNotExist(t, cs, infraNode, name)
765+
},
766+
"new-disabled-svc.service": func(t *testing.T, name string) {
767+
assertSystemdUnitFileExists(t, cs, infraNode, name)
768+
assertSystemdUnitExists(t, cs, infraNode, name)
769+
assertSystemdUnitIsDisabled(t, cs, infraNode, name)
770+
},
771+
}
772+
773+
for name, assertionFunc := range systemdUnitAssertions {
774+
t.Logf("Running assertion(s) for %s", name)
775+
assertionFunc(t, name)
776+
}
777+
685778
unlabelFunc()
686779

687780
workerMCP, err := cs.MachineConfigPools().Get(context.TODO(), "worker", metav1.GetOptions{})
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package e2e_test
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
corev1 "k8s.io/api/core/v1"
11+
12+
"github.com/openshift/machine-config-operator/test/framework"
13+
"github.com/openshift/machine-config-operator/test/helpers"
14+
)
15+
16+
// Creates a dummy systemd unit file with a customizable description.
17+
func getSystemdUnitContents(desc string) *string {
18+
contents := []string{
19+
"[Unit]",
20+
fmt.Sprintf("Description=%s", desc),
21+
"",
22+
"[Service]",
23+
`ExecStart=/bin/bash -c "sleep infinity"`,
24+
"Restart=on-failure",
25+
"",
26+
"[Install]",
27+
"WantedBy=multi-user.target",
28+
}
29+
30+
return helpers.StrToPtr(strings.Join(contents, "\n"))
31+
}
32+
33+
// Asserts that a given systemd unit is enabled.
34+
func assertSystemdUnitIsEnabled(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
35+
t.Helper()
36+
37+
output := showSystemdUnit(t, cs, node, unitName)
38+
assert.Contains(t, output, "UnitFileState=enabled")
39+
assert.Contains(t, output, "ActiveState=active")
40+
assert.Contains(t, output, "LoadState=loaded")
41+
}
42+
43+
// Asserts that a given systemd unit is disabled.
44+
func assertSystemdUnitIsDisabled(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
45+
t.Helper()
46+
47+
output := showSystemdUnit(t, cs, node, unitName)
48+
assert.Contains(t, output, "UnitFileState=disabled")
49+
assert.Contains(t, output, "ActiveState=inactive")
50+
assert.Contains(t, output, "LoadState=loaded")
51+
}
52+
53+
// Asserts that a given systemd unit has the specified dropins.
54+
func assertSystemdUnitHasDropins(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string, dropins []string) {
55+
t.Helper()
56+
57+
output := showSystemdUnit(t, cs, node, unitName)
58+
dropinPaths := getSystemdLineFromOutput(output, "DropInPaths")
59+
for _, dropin := range dropins {
60+
assert.Contains(t, dropinPaths, dropin)
61+
}
62+
}
63+
64+
const (
65+
// The root path for where systemd stores its units, dropins, etc.
66+
systemdRootPath string = "/etc/systemd/system"
67+
)
68+
69+
// Asserts that a dropin file exists for a given systemd unit.
70+
func assertSystemdUnitDropinFileExists(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName, dropinName string) {
71+
t.Helper()
72+
helpers.AssertFileOnNode(t, cs, node, filepath.Join(systemdRootPath, unitName+".d", dropinName))
73+
}
74+
75+
// Asserts tha a dropin file does not exist for a given systemd unit.
76+
func assertSystemdUnitDropinFileDoesNotExist(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName, dropinName string) {
77+
t.Helper()
78+
helpers.AssertFileNotOnNode(t, cs, node, filepath.Join(systemdRootPath, unitName+".d", dropinName))
79+
}
80+
81+
// Asserts that the unit file for a given systemd unit exists.
82+
func assertSystemdUnitFileExists(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
83+
t.Helper()
84+
helpers.AssertFileOnNode(t, cs, node, filepath.Join(systemdRootPath, unitName))
85+
}
86+
87+
// Asserts that the given systemd unit exists by querying systemd.
88+
func assertSystemdUnitExists(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
89+
t.Helper()
90+
output := showSystemdUnit(t, cs, node, unitName)
91+
loadState := getSystemdLineFromOutput(output, "LoadState")
92+
assert.Equal(t, loadState, "LoadState=loaded")
93+
}
94+
95+
// Asserts that the given systemd unit does not exist by querying systemd.
96+
func assertSystemdUnitDoesNotExist(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
97+
t.Helper()
98+
99+
output := showSystemdUnit(t, cs, node, unitName)
100+
101+
loadState := getSystemdLineFromOutput(output, "LoadState")
102+
assert.Equal(t, "LoadState=not-found", loadState)
103+
104+
loadError := getSystemdLineFromOutput(output, "LoadError")
105+
assert.Contains(t, loadError, "NoSuchUnit")
106+
}
107+
108+
// Asserts that the given systemd unit file does not exist.
109+
func assertSystemdUnitFileDoesNotExist(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
110+
t.Helper()
111+
helpers.AssertFileNotOnNode(t, cs, node, filepath.Join("/rootfs", systemdRootPath, unitName))
112+
}
113+
114+
// Runs "systemctl show <unit>" on a given node. Note: This command should
115+
// always returns zero even when the unit does not exist. Assertions are done
116+
// by interrogating its output.
117+
func showSystemdUnit(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) string {
118+
return helpers.ExecCmdOnNode(t, cs, node, "chroot", "/rootfs", "systemctl", "show", unitName)
119+
}
120+
121+
// TODO: This does not handle the case where a key may appear multiple times;
122+
// e.g., multiple ExecStart lines.
123+
func getSystemdLineFromOutput(output, prefix string) string {
124+
for _, line := range strings.Split(output, "\n") {
125+
if strings.HasPrefix(line, prefix) {
126+
return line
127+
}
128+
}
129+
130+
return ""
131+
}

0 commit comments

Comments
 (0)