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

WIP Systemd Unit Artifacts #254

Merged
merged 15 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
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
35 changes: 35 additions & 0 deletions docs/spec.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@
},
"type": "object",
"description": "ConfigFiles is a list of files that should be marked as config files in the package."
},
"systemdUnits": {
"additionalProperties": {
"$ref": "#/$defs/SystemdUnitConfig"
},
"type": "object",
"description": "SystemdUnits is a list of systemd units to include in the package."
}
},
"additionalProperties": false,
Expand Down Expand Up @@ -900,6 +907,34 @@
],
"description": "SymlinkTarget specifies the properties of a symlink"
},
"SystemdUnitConfig": {
"properties": {
"name": {
"type": "string",
"description": "Name is the name systemd unit should be copied under.\nNested paths are not supported. It is the user's responsibility\nto name the service with the appropriate extension, i.e. .service, .timer, etc."
},
"enabled": {
"type": "boolean",
"description": "Enable is used to enable the systemd unit on install\nThis determines what will be written to a systemd preset file"
},
"Reload": {
"type": "boolean",
"description": "Should service be reloaded instead of restarted? (Mutually exclusive with `Restart`)"
},
"Restart": {
"type": "boolean",
"description": "Can service be restarted? (Mutually exclusive with Reload)"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"name",
"enabled",
"Reload",
"Restart"
]
},
"Target": {
"properties": {
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion frontend/azlinux/mariner2.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type mariner2 struct{}

func (w mariner2) Base(resolver llb.ImageMetaResolver, opts ...llb.ConstraintsOpt) llb.State {
return llb.Image(mariner2Ref, llb.WithMetaResolver(resolver), dalec.WithConstraints(opts...)).Run(
w.Install([]string{"rpm-build", "mariner-rpm-macros", "build-essential", "ca-certificates"}, installWithConstraints(opts)),
w.Install([]string{"rpm-build", "mariner-rpm-macros", "systemd-rpm-macros", "build-essential", "ca-certificates"}, installWithConstraints(opts)),
dalec.WithConstraints(opts...),
).Root()
}
Expand Down
90 changes: 89 additions & 1 deletion frontend/rpm/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ BuildArch: noarch
{{ .PrepareSources }}
{{ .BuildSteps }}
{{ .Install }}
{{ .Post }}
{{ .PreUn }}
{{ .PostUn }}
{{ .Files }}
{{ .Changelog }}
`)))
Expand Down Expand Up @@ -288,6 +291,55 @@ func (w *specWrapper) BuildSteps() fmt.Stringer {
return b
}

func (w *specWrapper) PreUn() fmt.Stringer {
b := &strings.Builder{}
b.WriteString("%preun\n")

keys := dalec.SortMapKeys(w.Spec.Artifacts.SystemdUnits)
for _, servicePath := range keys {
serviceName := filepath.Base(servicePath)
fmt.Fprintf(b, "%%systemd_preun %s\n", serviceName)
}

return b
}

func (w *specWrapper) Post() fmt.Stringer {
b := &strings.Builder{}
b.WriteString("%post\n")
// TODO: can inject other post install steps here in the future

keys := dalec.SortMapKeys(w.Spec.Artifacts.SystemdUnits)
for _, servicePath := range keys {
unitConf := w.Spec.Artifacts.SystemdUnits[servicePath].Artifact()
fmt.Fprintf(b, "%%systemd_post %s\n", unitConf.ResolveName(servicePath))
}

return b
}

func (w *specWrapper) PostUn() fmt.Stringer {
b := &strings.Builder{}
b.WriteString("%postun\n")
keys := dalec.SortMapKeys(w.Spec.Artifacts.SystemdUnits)
for _, servicePath := range keys {
cfg := w.Spec.Artifacts.SystemdUnits[servicePath]
a := cfg.Artifact()
serviceName := a.ResolveName(servicePath)

// TODO: verify mutual exclusivity of these options
if cfg.Restart {
fmt.Fprintf(b, "%%systemd_postun_with_restart %s\n", serviceName)
} else if cfg.Reload {
fmt.Fprintf(b, "%%systemd_postun_with_reload %s\n", serviceName)
} else {
fmt.Fprintf(b, "%%systemd_postun %s\n", serviceName)
}
}

return b
}

func (w *specWrapper) Install() fmt.Stringer {
b := &strings.Builder{}

Expand Down Expand Up @@ -355,6 +407,31 @@ func (w *specWrapper) Install() fmt.Stringer {
cfg := w.Spec.Artifacts.ConfigFiles[c]
copyArtifact(`%{buildroot}/%{_sysconfdir}`, c, cfg)
}

serviceKeys := dalec.SortMapKeys(w.Spec.Artifacts.SystemdUnits)
presetName := "%{name}.preset"
for _, p := range serviceKeys {
cfg := w.Spec.Artifacts.SystemdUnits[p]
// must include '.service' suffix in name
adamperlin marked this conversation as resolved.
Show resolved Hide resolved
copyArtifact(`%{buildroot}/%{_unitdir}`, p, cfg.Artifact())

verb := "disable"
if cfg.Enable {
verb = "enable"
}

unitName := filepath.Base(p)
if cfg.Name != "" {
unitName = cfg.Name
}

fmt.Fprintf(b, "echo '%s %s' >> '%s'\n", verb, unitName, presetName)
}

if len(serviceKeys) > 0 {
copyArtifact(`%{buildroot}/%{_presetdir}`, presetName, dalec.ArtifactConfig{})
}

return b
}

Expand All @@ -369,7 +446,7 @@ func (w *specWrapper) Files() fmt.Stringer {
binKeys := dalec.SortMapKeys(w.Spec.Artifacts.Binaries)
for _, p := range binKeys {
cfg := w.Spec.Artifacts.Binaries[p]
full := filepath.Join(`%{_bindir}/`, cfg.SubPath, filepath.Base(p))
full := filepath.Join(`%{_bindir}/`, cfg.SubPath, cfg.Name)
fmt.Fprintln(b, full)
}

Expand Down Expand Up @@ -399,6 +476,17 @@ func (w *specWrapper) Files() fmt.Stringer {
fmt.Fprintln(b, fullDirective)
}

serviceKeys := dalec.SortMapKeys(w.Spec.Artifacts.SystemdUnits)
for _, p := range serviceKeys {
serviceName := filepath.Base(p)
unitPath := filepath.Join(`%{_unitdir}/`, serviceName)
fmt.Fprintln(b, unitPath)
}

if len(serviceKeys) > 0 {
fmt.Fprintln(b, "%{_presetdir}/%{name}.preset")
}

return b
}

Expand Down
55 changes: 55 additions & 0 deletions frontend/rpm/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/Azure/dalec"
"gotest.tools/v3/assert"
)

func TestTemplateSources(t *testing.T) {
Expand Down Expand Up @@ -206,3 +207,57 @@ func TestTemplateSources(t *testing.T) {
}
})
}

func TestTemplate_Artifacts(t *testing.T) {
t.Run("systemd artifacts with restart", func(t *testing.T) {
w := &specWrapper{Spec: &dalec.Spec{
Artifacts: dalec.Artifacts{
SystemdUnits: map[string]dalec.SystemdUnitConfig{
"test.service": {
Restart: true,
},
},
},
}}

got := w.PostUn().String()
want := `%postun
%systemd_postun_with_restart test.service
`
assert.Equal(t, want, got)
})

t.Run("systemd artifacts with reload", func(t *testing.T) {
w := &specWrapper{Spec: &dalec.Spec{
Artifacts: dalec.Artifacts{
SystemdUnits: map[string]dalec.SystemdUnitConfig{
"test.service": {
Reload: true,
},
},
},
}}

got := w.PostUn().String()
want := `%postun
%systemd_postun_with_reload test.service
`
assert.Equal(t, want, got)
})

t.Run("systemd artifacts no reload/restart", func(t *testing.T) {
w := &specWrapper{Spec: &dalec.Spec{
Artifacts: dalec.Artifacts{
SystemdUnits: map[string]dalec.SystemdUnitConfig{
"test.service": {},
},
},
}}

got := w.PostUn().String()
want := `%postun
%systemd_postun test.service
`
assert.Equal(t, want, got)
})
}
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ require (
github.com/opencontainers/image-spec v1.1.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
adamperlin marked this conversation as resolved.
Show resolved Hide resolved
github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c
go.opentelemetry.io/otel v1.21.0
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81
golang.org/x/sys v0.18.0
google.golang.org/grpc v1.59.0
gotest.tools/v3 v3.5.0
)

require (
Expand All @@ -41,6 +43,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/ttrpc v1.2.2 // indirect
github.com/containerd/typeurl/v2 v2.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/docker v25.0.3+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
Expand All @@ -65,6 +68,7 @@ require (
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/signal v0.7.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
Expand Down Expand Up @@ -101,5 +105,4 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.0 // indirect
)
14 changes: 14 additions & 0 deletions load.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,20 @@ func (s Spec) Validate() error {
}
}

for p, art := range s.Artifacts.SystemdUnits {
if err := art.validate(); err != nil {
return errors.Wrapf(err, "error validating config for systemd unit with path %s", p)
}
}

return nil
}

func (s *SystemdUnitConfig) validate() error {
if s.Reload && s.Restart {
return fmt.Errorf("cannot specify both reload and restart")
}

return nil
}

Expand Down
18 changes: 18 additions & 0 deletions load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"os"
"reflect"
"testing"

"github.com/stretchr/testify/assert"
)

//go:embed test/fixtures/unmarshall/source-inline.yml
Expand Down Expand Up @@ -470,3 +472,19 @@ sources:
}
})
}

func TestArtifact_Validation(t *testing.T) {
spec := &Spec{
Artifacts: Artifacts{
SystemdUnits: map[string]SystemdUnitConfig{
"unit.service": {
Restart: true,
Reload: true,
},
},
},
}
want := errors.New("error validating config for systemd unit with path unit.service: cannot specify both reload and restart")
err := spec.Validate()
assert.Equal(t, want.Error(), err.Error())
}
Loading