Skip to content

Commit 01a8037

Browse files
Nemricbgilbert
authored andcommitted
Add support for user units
Allow users to create and enable user-level systemd services in ignition Fixes #1296
1 parent 191dc87 commit 01a8037

File tree

7 files changed

+245
-34
lines changed

7 files changed

+245
-34
lines changed

config/shared/errors/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ var (
8585
ErrInvalidSystemdDropinExt = errors.New("invalid systemd drop-in extension")
8686
ErrNoSystemdExt = errors.New("no systemd unit extension")
8787
ErrInvalidInstantiatedUnit = errors.New("invalid systemd instantiated unit")
88+
ErrInvalidUnitScope = errors.New("invalid unit scope (system, user)")
8889

8990
// Misc errors
9091
ErrSourceRequired = errors.New("source is required")

config/v3_4_experimental/types/unit.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package types
1717
import (
1818
"fmt"
1919
"path"
20+
"path/filepath"
2021
"strings"
2122

2223
"github.com/coreos/ignition/v2/config/shared/errors"
@@ -28,6 +29,13 @@ import (
2829
"github.com/coreos/vcontext/report"
2930
)
3031

32+
type UnitScope string
33+
34+
const (
35+
SystemUnit UnitScope = "system"
36+
UserUnit UnitScope = "user"
37+
)
38+
3139
func (u Unit) Key() string {
3240
return u.Name
3341
}
@@ -43,10 +51,22 @@ func (u Unit) Validate(c cpath.ContextPath) (r report.Report) {
4351
r.AddOnError(c, err)
4452

4553
r.AddOnWarn(c, validations.ValidateInstallSection(u.Name, util.IsTrue(u.Enabled), util.NilOrEmpty(u.Contents), opts))
46-
54+
r.AddOnError(c.Append("scope"), validateScope(u.Scope))
4755
return
4856
}
4957

58+
func validateScope(scope *string) error {
59+
if scope == nil {
60+
return nil
61+
}
62+
switch *scope {
63+
case "system", "user":
64+
return nil
65+
default:
66+
return errors.ErrInvalidUnitScope
67+
}
68+
}
69+
5070
func validateName(name string) error {
5171
switch path.Ext(name) {
5272
case ".service", ".socket", ".device", ".mount", ".automount", ".swap", ".target", ".path", ".timer", ".snapshot", ".slice", ".scope":
@@ -80,3 +100,22 @@ func validateUnitContent(content *string) ([]*unit.UnitOption, error) {
80100
}
81101
return opts, nil
82102
}
103+
104+
func (u Unit) GetScope() UnitScope {
105+
if u.Scope == nil {
106+
return UnitScope(*util.StrToPtr("system"))
107+
} else {
108+
return UnitScope(*u.Scope)
109+
}
110+
}
111+
112+
func (u Unit) GetBasePath() string {
113+
switch u.GetScope() {
114+
case UserUnit:
115+
return filepath.Join("etc", "systemd", "user")
116+
case SystemUnit:
117+
return filepath.Join("etc", "systemd", "system")
118+
default:
119+
return filepath.Join("etc", "systemd", "system")
120+
}
121+
}

internal/exec/stages/files/files.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ func (s *stage) relabeling() bool {
121121
func (s *stage) relabel(paths ...string) {
122122
if s.toRelabel != nil {
123123
for _, path := range paths {
124-
s.toRelabel = append(s.toRelabel, filepath.Join(s.DestDir, path))
124+
if !s.ToRelabelContains(path) {
125+
s.toRelabel = append(s.toRelabel, filepath.Join(s.DestDir, path))
126+
}
125127
}
126128
}
127129
}
@@ -141,3 +143,13 @@ func (s *stage) relabelFiles() error {
141143

142144
return s.RelabelFiles(s.toRelabel)
143145
}
146+
147+
//add a check for not relabelling path more than once
148+
func (s *stage) ToRelabelContains(value string) bool {
149+
for _, val := range s.toRelabel {
150+
if val == value {
151+
return true
152+
}
153+
}
154+
return false
155+
}

internal/exec/stages/files/units.go

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package files
1717
import (
1818
"fmt"
1919
"path/filepath"
20+
"sort"
2021
"strings"
2122

2223
"github.com/coreos/ignition/v2/config/shared/errors"
@@ -33,6 +34,7 @@ type Preset struct {
3334
enabled bool
3435
instantiatable bool
3536
instances []string
37+
path string
3638
}
3739

3840
// warnOnOldSystemdVersion checks the version of Systemd
@@ -73,12 +75,12 @@ func (s *stage) createUnits(config types.Config) error {
7375
if _, ok := presets[key]; ok {
7476
presets[key].instances = append(presets[key].instances, instance)
7577
} else {
76-
presets[key] = &Preset{unitName, *unit.Enabled, true, []string{instance}}
78+
presets[key] = &Preset{unitName, *unit.Enabled, true, []string{instance}, util.SystemdPresetPath(unit)}
7779
}
7880
} else {
7981
key := fmt.Sprintf("%s-%s", unit.Name, identifier)
8082
if _, ok := presets[unit.Name]; !ok {
81-
presets[key] = &Preset{unit.Name, *unit.Enabled, false, []string{}}
83+
presets[key] = &Preset{unit.Name, *unit.Enabled, false, []string{}, util.SystemdPresetPath(unit)}
8284
} else {
8385
return fmt.Errorf("%q key is already present in the presets map", key)
8486
}
@@ -118,10 +120,11 @@ func (s *stage) createUnits(config types.Config) error {
118120
}
119121
// if we have presets then create the systemd preset file.
120122
if len(presets) != 0 {
121-
if err := s.relabelPath(filepath.Join(s.DestDir, util.PresetPath)); err != nil {
122-
return err
123-
}
124-
if err := s.createSystemdPresetFile(presets); err != nil {
123+
// useless as it will be done later in createSystemdPresetFiles
124+
// if err := s.relabelPath(filepath.Join(s.DestDir, util.PresetPath)); err != nil {
125+
// return err
126+
// }
127+
if err := s.createSystemdPresetFiles(presets); err != nil {
125128
return err
126129
}
127130
}
@@ -148,27 +151,27 @@ func parseInstanceUnit(unit types.Unit) (string, string, error) {
148151

149152
// createSystemdPresetFile creates the presetfile for enabled/disabled
150153
// systemd units.
151-
func (s *stage) createSystemdPresetFile(presets map[string]*Preset) error {
154+
func (s *stage) createSystemdPresetFiles(presets map[string]*Preset) error {
152155
hasInstanceUnit := false
153-
for _, value := range presets {
154-
unitString := value.unit
155-
if value.instantiatable {
156+
for _, preset := range presets {
157+
unitString := preset.unit
158+
if preset.instantiatable {
156159
hasInstanceUnit = true
157160
// Let's say we have two instantiated enabled units listed under
158161
// the systemd units i.e. [email protected], [email protected]
159162
// then the unitString will look like "[email protected] foo bar"
160-
unitString = fmt.Sprintf("%s %s", unitString, strings.Join(value.instances, " "))
163+
unitString = fmt.Sprintf("%s %s", unitString, strings.Join(preset.instances, " "))
161164
}
162-
if value.enabled {
165+
if preset.enabled {
163166
if err := s.Logger.LogOp(
164-
func() error { return s.EnableUnit(unitString) },
167+
func() error { return s.EnableUnit(unitString, preset.path) },
165168
"setting preset to enabled for %q", unitString,
166169
); err != nil {
167170
return err
168171
}
169172
} else {
170173
if err := s.Logger.LogOp(
171-
func() error { return s.DisableUnit(unitString) },
174+
func() error { return s.DisableUnit(unitString, preset.path) },
172175
"setting preset to disabled for %q", unitString,
173176
); err != nil {
174177
return err
@@ -183,7 +186,26 @@ func (s *stage) createSystemdPresetFile(presets map[string]*Preset) error {
183186
return err
184187
}
185188
}
186-
s.relabel(util.PresetPath)
189+
190+
//getting all paths from presets
191+
var paths []string
192+
for _, preset := range presets {
193+
paths = append(paths, preset.path)
194+
}
195+
sort.Slice(paths, func(i, j int) bool {
196+
return paths[i] < paths[j]
197+
})
198+
//running accros differents paths not to apply them s.relabalpath more than once
199+
var tmppath string = ""
200+
for _, path := range paths {
201+
if path != tmppath {
202+
if err := s.relabelPath(filepath.Join(s.DestDir, path)); err != nil {
203+
return err
204+
}
205+
}
206+
tmppath = path
207+
}
208+
187209
return nil
188210
}
189211

internal/exec/stages/files/units_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515
package files
1616

1717
import (
18+
"fmt"
1819
"reflect"
1920
"testing"
2021

2122
"github.com/coreos/ignition/v2/config/shared/errors"
23+
cfgutil "github.com/coreos/ignition/v2/config/util"
24+
"github.com/coreos/ignition/v2/config/v3_4_experimental"
2225
"github.com/coreos/ignition/v2/config/v3_4_experimental/types"
26+
"github.com/coreos/ignition/v2/internal/exec/util"
27+
"github.com/coreos/ignition/v2/internal/log"
2328
)
2429

2530
func TestParseInstanceUnit(t *testing.T) {
@@ -74,3 +79,118 @@ func TestParseInstanceUnit(t *testing.T) {
7479
}
7580
}
7681
}
82+
83+
func TestSystemdUnitPath(t *testing.T) {
84+
85+
tests := []struct {
86+
in types.Unit
87+
out string
88+
}{
89+
{
90+
types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("system")},
91+
"etc/systemd/system",
92+
},
93+
{
94+
types.Unit{Name: "test.service"},
95+
"etc/systemd/system",
96+
},
97+
{
98+
types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("user")},
99+
"etc/systemd/user",
100+
},
101+
}
102+
103+
for i, test := range tests {
104+
path := util.SystemdUnitsPath(test.in)
105+
if path != test.out {
106+
t.Errorf("#%d: bad error: want %v, got %v", i, test.out, path)
107+
}
108+
}
109+
}
110+
111+
func TestSystemdDropinsPath(t *testing.T) {
112+
113+
tests := []struct {
114+
in types.Unit
115+
out string
116+
}{
117+
{
118+
types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("system")},
119+
"etc/systemd/system/test.service.d",
120+
},
121+
{
122+
types.Unit{Name: "test.service"},
123+
"etc/systemd/system/test.service.d",
124+
},
125+
{
126+
types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("user")},
127+
"etc/systemd/user/test.service.d",
128+
},
129+
}
130+
131+
for i, test := range tests {
132+
path := util.SystemdDropinsPath(test.in)
133+
if path != test.out {
134+
t.Errorf("#%d: bad error: want %v, got %v", i, test.out, path)
135+
}
136+
}
137+
}
138+
139+
func TestSystemdPresetPath(t *testing.T) {
140+
141+
tests := []struct {
142+
in types.Unit
143+
out string
144+
}{
145+
{
146+
types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("system")},
147+
"etc/systemd/system-preset/20-ignition.preset",
148+
},
149+
{
150+
types.Unit{Name: "test.service"},
151+
"etc/systemd/system-preset/20-ignition.preset",
152+
},
153+
{
154+
types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("user")},
155+
"etc/systemd/user-preset/20-ignition.preset",
156+
},
157+
}
158+
159+
for i, test := range tests {
160+
path := util.SystemdPresetPath(test.in)
161+
if path != test.out {
162+
t.Errorf("#%d: bad error: want %v, got %v", i, test.out, path)
163+
}
164+
}
165+
}
166+
167+
func TestCreateUnits(t *testing.T) {
168+
169+
config, report, err := v3_4_experimental.Parse([]byte(`{"ignition":{"version":"3.4.0-experimental"},"systemd":{"units":[{"contents":"[Unit]\nDescription=Prometheus node exporter\n[Install]\nWantedBy=multi-user.target\n","enabled":true,"name":"exporter.service"},{"contents":"[Unit]\nDescription=promtail.service\n[Install]\nWantedBy=multi-user.target default.target","enabled":true,"name":"promtail.service","scope":"user"},{"contents":"[Unit]\nDescription=promtail.service\n[Install]\nWantedBy=multi-user.target default.target","enabled":true,"name":"grafana.service","scope":"system"}]}}`))
170+
171+
if err != nil {
172+
print(report.Entries, err.Error())
173+
}
174+
175+
fmt.Printf("config: %v\n", config)
176+
177+
tests := []struct {
178+
in types.Config
179+
out error
180+
}{
181+
{
182+
config,
183+
nil,
184+
},
185+
}
186+
187+
for i, test := range tests {
188+
var logg log.Logger = log.New(true)
189+
var st stage
190+
st.Logger = &logg
191+
test.out = st.createUnits(test.in)
192+
if test.out != nil {
193+
t.Errorf("#%d: error occured: %v", i, test.out)
194+
}
195+
}
196+
}

internal/exec/util/path.go

100644100755
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@ package util
1616

1717
import (
1818
"path/filepath"
19+
20+
"github.com/coreos/ignition/v2/config/v3_4_experimental/types"
1921
)
2022

21-
func SystemdUnitsPath() string {
22-
return filepath.Join("etc", "systemd", "system")
23+
func SystemdUnitsPath(unit types.Unit) string {
24+
return unit.GetBasePath()
25+
}
26+
27+
func SystemdPresetPath(unit types.Unit) string {
28+
switch unit.GetScope() {
29+
case types.UserUnit:
30+
return filepath.Join("etc", "systemd", "user-preset", "20-ignition.preset")
31+
case types.SystemUnit:
32+
return filepath.Join("etc", "systemd", "system-preset", "20-ignition.preset")
33+
default:
34+
return filepath.Join("etc", "systemd", "system-preset", "20-ignition.preset")
35+
}
36+
}
37+
38+
func SystemdWantsPath(unit types.Unit) string {
39+
return filepath.Join(SystemdUnitsPath(unit), unit.Name+".wants")
2340
}
2441

25-
func SystemdDropinsPath(unitName string) string {
26-
return filepath.Join("etc", "systemd", "system", unitName+".d")
42+
func SystemdDropinsPath(unit types.Unit) string {
43+
return filepath.Join(SystemdUnitsPath(unit), unit.Name+".d")
2744
}

0 commit comments

Comments
 (0)