Skip to content

Commit

Permalink
Add support for Systemd Unit Artifacts
Browse files Browse the repository at this point in the history
This allows systemd units (.service, .sock, etc) to be added to packages.
  • Loading branch information
adamperlin committed May 23, 2024
1 parent 51c8486 commit b8a0e9d
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 4 deletions.
25 changes: 25 additions & 0 deletions docs/spec.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@
},
"type": "object",
"description": "Licenses is a list of doc files included 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 @@ -914,6 +921,24 @@
],
"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."
},
"enable": {
"type": "boolean",
"description": "Enable is used to enable the systemd unit on install\nThis determines what will be written to a systemd preset file"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"name",
"enable"
]
},
"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
81 changes: 80 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,47 @@ 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)
fmt.Fprintf(b, "%%systemd_postun %s\n", serviceName)
}

return b
}

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

Expand Down Expand Up @@ -356,6 +400,30 @@ func (w *specWrapper) Install() fmt.Stringer {
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 systemd unit extension (.service, .socket, .timer, etc.) in name
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{})
}

docKeys := dalec.SortMapKeys(w.Spec.Artifacts.Docs)
for _, d := range docKeys {
cfg := w.Spec.Artifacts.Docs[d]
Expand Down Expand Up @@ -392,7 +460,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 @@ -422,6 +490,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")
}

docKeys := dalec.SortMapKeys(w.Spec.Artifacts.Docs)
for _, d := range docKeys {
cfg := w.Spec.Artifacts.Docs[d]
Expand Down
18 changes: 18 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,20 @@ func TestTemplateSources(t *testing.T) {
}
})
}

func TestTemplate_Artifacts(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)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +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/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c
github.com/stretchr/testify v1.8.4
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 Down Expand Up @@ -104,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
)
50 changes: 50 additions & 0 deletions spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package dalec
import (
"fmt"
"io/fs"
"path/filepath"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -135,9 +136,29 @@ type Artifacts struct {
Docs map[string]ArtifactConfig `yaml:"docs,omitempty" json:"docs,omitempty"`
// Licenses is a list of doc files included in the package
Licenses map[string]ArtifactConfig `yaml:"licenses,omitempty" json:"licenses,omitempty"`
// SystemdUnits is a list of systemd units to include in the package.
SystemdUnits map[string]SystemdUnitConfig `yaml:"systemdUnits,omitempty" json:"systemdUnits,omitempty"`
// TODO: other types of artifacts (systtemd units, libexec, etc)
}

type SystemdUnitConfig struct {
// Name is the name systemd unit should be copied under.
// Nested paths are not supported. It is the user's responsibility
// to name the service with the appropriate extension, i.e. .service, .timer, etc.
Name string `yaml:"name,omitempty" json:"name"`

// Enable is used to enable the systemd unit on install
// This determines what will be written to a systemd preset file
Enable bool `yaml:"enable,omitempty" json:"enable"`
}

func (s SystemdUnitConfig) Artifact() ArtifactConfig {
return ArtifactConfig{
SubPath: "",
Name: s.Name,
}
}

// CreateArtifactDirectories describes various directories that should be created on install.
// CreateArtifactDirectories represents different directory paths that are common to RPM systems.
type CreateArtifactDirectories struct {
Expand Down Expand Up @@ -167,6 +188,30 @@ type ArtifactConfig struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
}

func (a *ArtifactConfig) ResolveName(path string) string {
if a.Name != "" {
return a.Name
}
return filepath.Base(path)
}

// ServiceConfig is the configuration for a service to include in the package.
type ServiceConfig struct {
Name string `yaml:"name" json:"name" jsonschema:"omitempty"`

// Some services don't support restarting, in which case this should be set to true
NoRestart bool `yaml:"noRestart,omitempty" json:"noRestart,omitempty"`

Disable bool `yaml:"disable,omitempty" json:"disable,omitempty"`
}

func (s ServiceConfig) Artifact() ArtifactConfig {
return ArtifactConfig{
SubPath: "",
Name: s.Name,
}
}

// IsEmpty is used to determine if there are any artifacts to include in the package.
func (a *Artifacts) IsEmpty() bool {
if len(a.Binaries) > 0 {
Expand All @@ -181,6 +226,11 @@ func (a *Artifacts) IsEmpty() bool {
if len(a.ConfigFiles) > 0 {
return false
}

if len(a.SystemdUnits) > 0 {
return false
}

if len(a.Docs) > 0 {
return false
}
Expand Down
Loading

0 comments on commit b8a0e9d

Please sign in to comment.