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
87 changes: 87 additions & 0 deletions 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 @@ -105,6 +108,11 @@ func (w *specWrapper) Requires() fmt.Stringer {
writeDep(b, "BuildRequires", name, constraints)
}

if len(w.Artifacts.Services) > 0 {
// We take advantage of the systemd-rpm-macros package to simplify the service file installation
writeDep(b, "BuildRequires", "systemd-rpm-macros", nil)
adamperlin marked this conversation as resolved.
Show resolved Hide resolved
}

if len(deps.Build) > 0 && len(deps.Runtime) > 0 {
b.WriteString("\n")
}
Expand Down Expand Up @@ -288,6 +296,54 @@ 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.Services)
for _, servicePath := range keys {
// must include '.service' suffix
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.Services)
for _, servicePath := range keys {
// must include '.service' suffix
serviceName := filepath.Base(servicePath)
fmt.Fprintf(b, "%%systemd_post %s\n", serviceName)
}

return b
}

func (w *specWrapper) PostUn() fmt.Stringer {
b := &strings.Builder{}
b.WriteString("%postun\n")
keys := dalec.SortMapKeys(w.Spec.Artifacts.Services)
for _, servicePath := range keys {
// must include '.service' suffix
cfg := w.Spec.Artifacts.Services[servicePath]
serviceName := filepath.Base(servicePath)

if cfg.NoRestart {
fmt.Fprintf(b, "%%systemd_postun %s\n", serviceName)
} else {
fmt.Fprintf(b, "%%systemd_postun_with_restart %s\n", serviceName)
}
}

return b
}

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

Expand Down Expand Up @@ -355,6 +411,27 @@ func (w *specWrapper) Install() fmt.Stringer {
cfg := w.Spec.Artifacts.ConfigFiles[c]
copyArtifact(`%{buildroot}/%{_sysconfdir}`, c, cfg)
}
serviceKeys := dalec.SortMapKeys(w.Spec.Artifacts.Services)
for _, p := range serviceKeys {
cfg := w.Spec.Artifacts.Services[p]
// must include '.service' suffix in name
adamperlin marked this conversation as resolved.
Show resolved Hide resolved
copyArtifact(`%{buildroot}/%{_unitdir}`, p, cfg.Artifact())

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

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

presetName := strings.TrimSuffix(serviceName, ".service") + ".preset"
fmt.Fprintf(b, "echo '%s %s' >> %s\n", verb, serviceName, presetName)
copyArtifact(`%{buildroot}/%{_presetdir}`, presetName, dalec.ArtifactConfig{})
}

return b
}

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

serviceKeys := dalec.SortMapKeys(w.Spec.Artifacts.Services)
for _, p := range serviceKeys {
serviceName := filepath.Base(p)
prefixName := strings.TrimSuffix(serviceName, ".service") + ".preset"
unitPath := filepath.Join(`%{_unitdir}/`, serviceName)
prefixPath := filepath.Join(`%{_presetdir}/`, prefixName)
fmt.Fprintln(b, unitPath)
fmt.Fprintln(b, prefixPath)
}

return b
}

Expand Down
23 changes: 23 additions & 0 deletions spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ type Artifacts struct {
// ConfigFiles is a list of files that should be marked as config files in the package.
ConfigFiles map[string]ArtifactConfig `yaml:"configFiles,omitempty" json:"configFiles,omitempty"`
// TODO: other types of artifacts (systtemd units, libexec, etc)
Services map[string]ServiceConfig `yaml:"services,omitempty" json:"services,omitempty"`
adamperlin marked this conversation as resolved.
Show resolved Hide resolved
}

// CreateArtifactDirectories describes various directories that should be created on install.
Expand Down Expand Up @@ -163,6 +164,23 @@ type ArtifactConfig struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
}

// ServiceConfig is the configuration for a service to include in the package.
type ServiceConfig struct {
adamperlin marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -177,6 +195,11 @@ func (a *Artifacts) IsEmpty() bool {
if len(a.ConfigFiles) > 0 {
return false
}

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

return true
}

Expand Down
132 changes: 132 additions & 0 deletions test/azlinux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
testLinuxDistro(ctx, t, "azlinux3/container", "azlinux3/rpm")
}

func mustParse(t *testing.T, spec string) *dalec.Spec {

Check failure on line 29 in test/azlinux_test.go

View workflow job for this annotation

GitHub Actions / lint

func `mustParse` is unused (unused)
t.Helper()
s, err := dalec.LoadSpec([]byte(spec))
if err != nil {
t.Fatalf("failed to parse spec: %v", err)
}
return s
}

func testLinuxDistro(ctx context.Context, t *testing.T, buildTarget string, signTarget string) {
t.Run("Fail when non-zero exit code during build", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -340,6 +349,128 @@

runTest(t, distroSigningTest(t, &spec, signTarget))
})

t.Run("test systemd unit", func(t *testing.T) {
t.Parallel()
spec := &dalec.Spec{
Name: "test-systemd-unit",
Description: "Test systemd unit",
Website: "https://www.github.com/Azure/dalec",
Version: "0.0.1",
Revision: "1",
Vendor: "Microsoft",
License: "Apache 2.0",
Packager: "Microsoft <[email protected]>",
Targets: map[string]dalec.Target{
"azlinux3": {
Image: &dalec.ImageConfig{
Base: "azurelinuxpreview.azurecr.io/public/azurelinux/base/core:3.0",
},
},
"mariner2": {
Image: &dalec.ImageConfig{
Base: "mcr.microsoft.com/cbl-mariner/base/core:2.0",
},
},
},
Dependencies: &dalec.PackageDependencies{
Build: map[string][]string{
"msft-golang": {},
adamperlin marked this conversation as resolved.
Show resolved Hide resolved
},
},
Sources: map[string]dalec.Source{
"src": {
Inline: &dalec.SourceInline{
Dir: &dalec.SourceInlineDir{

Files: map[string]*dalec.SourceInlineFile{
"simple.service": {
Contents: `,
[Unit]
Description=Phony Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/service
Restart=always

[Install]
WantedBy=multi-user.target
`},
},
},
},
},
},
Artifacts: dalec.Artifacts{
Services: map[string]dalec.ServiceConfig{
"src/simple.service": {},
},
},
Tests: []*dalec.TestSpec{
{
Name: "Check service files",
Files: map[string]dalec.FileCheckOutput{
"/usr/lib/systemd/system/simple.service": {
CheckOutput: dalec.CheckOutput{Contains: []string{"ExecStart=/usr/bin/service"}},
Permissions: 0644,
},
"/usr/lib/systemd/system-preset/simple.preset": {
CheckOutput: dalec.CheckOutput{Contains: []string{"enable simple.service"}},
Permissions: 0644,
},
},
},
},
}

/*
tests:
- name: Check service files
files:
/usr/lib/systemd/system/simple.service:
permissions: 0644
contains:
- "ExecStart=/usr/bin/service"
/usr/lib/systemd/system-preset/simple.preset:
permissions: 0644
contains:
- "enable simple.service"
*/
testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
req := newSolveRequest(withBuildTarget(buildTarget), withSpec(ctx, t, spec))
return client.Solve(ctx, req)
})

// Test to ensure disabling works
spec.Artifacts.Services["src/simple.service"] = dalec.ServiceConfig{
Disable: true,
}
spec.Tests = []*dalec.TestSpec{
{
Name: "Check service files",
Files: map[string]dalec.FileCheckOutput{
"/usr/lib/systemd/system/simple.service": {
CheckOutput: dalec.CheckOutput{Contains: []string{"ExecStart=/usr/bin/service"}},
Permissions: 0644,
},
"/usr/lib/systemd/system-preset/simple.preset": {
// This is the only change from the previous test, service should be
// disabled in preset
CheckOutput: dalec.CheckOutput{Contains: []string{"disable simple.service"}},
Permissions: 0644,
},
},
},
}

testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
req := newSolveRequest(withBuildTarget(buildTarget), withSpec(ctx, t, spec))
return client.Solve(ctx, req)
})
})

t.Run("go module", func(t *testing.T) {
t.Parallel()
ctx := startTestSpan(baseCtx, t)
Expand Down Expand Up @@ -394,6 +525,7 @@
req := newSolveRequest(withBuildTarget(buildTarget), withSpec(ctx, t, spec))
return client.Solve(ctx, req)
})

})

t.Run("test directory creation", func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/env-multiple-commands.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ license: Apache 2.0

targets: # Distro specific build requirements
mariner2:
base: mcr.microsoft.com/cbl-mariner/base/core:2.0
adamperlin marked this conversation as resolved.
Show resolved Hide resolved
dependencies:

build:
Expand Down
16 changes: 16 additions & 0 deletions test/fixtures/service/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

import (
"fmt"
"net/http"
)

func main() {
var mux = http.NewServeMux()
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
fmt.Fprintln(w, "Phony Service")
}))

http.ListenAndServe(":8080", mux)

Check failure on line 15 in test/fixtures/service/main.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `http.ListenAndServe` is not checked (errcheck)
}
11 changes: 11 additions & 0 deletions test/fixtures/service/simple.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[Unit]
Description=Phony Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/service
Restart=always

[Install]
WantedBy=multi-user.target
Loading
Loading