Skip to content

Commit

Permalink
convert Compose restart policies to systemd configs
Browse files Browse the repository at this point in the history
  • Loading branch information
aksiksi committed Nov 5, 2023
1 parent d95b39f commit dd04259
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 56 deletions.
122 changes: 111 additions & 11 deletions compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ import (
"context"
"fmt"
"log"
"regexp"
"slices"
"strconv"
"strings"
"time"

"github.com/compose-spec/compose-go/loader"
"github.com/compose-spec/compose-go/types"
"golang.org/x/exp/maps"
)

// Examples:
// nixose.systemd.service.RuntimeMaxSec=100
// nixose.systemd.unit.StartLimitBurst=10
var systemdLabelRegexp regexp.Regexp = *regexp.MustCompile(`nixose\.systemd\.(service|unit)\.(\w+)`)

func composeEnvironmentToMap(env types.MappingWithEquals) map[string]string {
m := make(map[string]string)
for k, v := range env {
Expand Down Expand Up @@ -85,6 +93,91 @@ func (g *Generator) Run(ctx context.Context) (*NixContainerConfig, error) {
}, nil
}

func parseRestartPolicyAndSystemdLabels(service *types.ServiceConfig) (*NixContainerSystemdConfig, error) {
p := &NixContainerSystemdConfig{
Service: make(map[string]any),
Unit: make(map[string]any),
}

// https://docs.docker.com/compose/compose-file/compose-file-v2/#restart
switch restart := service.Restart; restart {
case "":
p.Service["Restart"] = "no"
case "no", "always", "on-failure":
p.Service["Restart"] = restart
case "unless-stopped":
p.Service["Restart"] = "always"
default:
if strings.HasPrefix(restart, "on-failure") && strings.Contains(restart, ":") {
p.Service["Restart"] = "on-failure"
maxAttemptsString := strings.TrimSpace(strings.Split(restart, ":")[1])
if maxAttempts, err := strconv.ParseInt(maxAttemptsString, 10, 64); err != nil {
return nil, fmt.Errorf("failed to parse on-failure attempts: %q: %w", maxAttemptsString, err)
} else {
v := int(maxAttempts)
p.StartLimitBurst = &v
}
} else {
return nil, fmt.Errorf("unsupported restart: %q", restart)
}
}

if service.Deploy != nil {
// The newer "deploy" config will always override the legacy "restart" config.
// https://docs.docker.com/compose/compose-file/compose-file-v3/#restart_policy
if restartPolicy := service.Deploy.RestartPolicy; restartPolicy != nil {
switch condition := restartPolicy.Condition; condition {
case "none":
p.Service["Restart"] = "no"
case "any":
p.Service["Restart"] = "always"
case "on-failure":
p.Service["Restart"] = "on-failure"
default:
return nil, fmt.Errorf("unsupported condition: %q", condition)
}
if delay := restartPolicy.Delay; delay != nil {
p.Service["RestartSec"] = delay.String()
}
if maxAttempts := restartPolicy.MaxAttempts; maxAttempts != nil {
v := int(*maxAttempts)
p.StartLimitBurst = &v
}
if window := restartPolicy.Window; window != nil {
windowSecs := int(time.Duration(*window).Seconds())
p.StartLimitIntervalSec = &windowSecs
}
}
}

// Custom values provided via labels will override any explicit restart settings.
var labelsToDrop []string
for label, value := range service.Labels {
if !strings.HasPrefix(label, "nixose.") {
continue
}
m := systemdLabelRegexp.FindStringSubmatch(label)
if len(m) == 0 {
return nil, fmt.Errorf("invalid nixose label specified for service %q: %q", service.Name, label)
}
typ, key := m[1], m[2]
switch typ {
case "service":
p.Service[key] = parseSystemdValue(value)
case "unit":
p.Unit[key] = parseSystemdValue(value)
default:
return nil, fmt.Errorf(`invalid systemd type %q - must be "service" or "unit"`, typ)
}
labelsToDrop = append(labelsToDrop, label)
}
for _, label := range labelsToDrop {
delete(service.Labels, label)
}

return p, nil
}

func (g *Generator) buildNixContainer(service types.ServiceConfig) NixContainer {
dependsOn := service.GetDependencies()
if g.Project != nil {
Expand All @@ -103,18 +196,25 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig) NixContainer
name = service.Name
}

systemdConfig, err := parseRestartPolicyAndSystemdLabels(&service)
if err != nil {
// TODO(aksiksi): Return error here instead of panicing.
panic(err)
}

c := NixContainer{
Project: g.Project,
Runtime: g.Runtime,
Name: name,
Image: service.Image,
Labels: service.Labels,
Ports: portConfigsToPortStrings(service.Ports),
User: service.User,
Volumes: make(map[string]string),
Networks: maps.Keys(service.Networks),
DependsOn: dependsOn,
AutoStart: g.AutoStart,
Project: g.Project,
Runtime: g.Runtime,
Name: name,
Image: service.Image,
Labels: service.Labels,
Ports: portConfigsToPortStrings(service.Ports),
User: service.User,
Volumes: make(map[string]string),
Networks: maps.Keys(service.Networks),
SystemdConfig: systemdConfig,
DependsOn: dependsOn,
AutoStart: g.AutoStart,
}
slices.Sort(c.Networks)

Expand Down
15 changes: 15 additions & 0 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,24 @@ import (
"log"
"os"
"slices"
"strconv"
"strings"
)

// https://www.freedesktop.org/software/systemd/man/latest/systemd.syntax.html
func parseSystemdValue(v string) any {
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
return int(i)
}
switch v {
case "true", "yes", "on", "1":
return true
case "false", "no", "off", "0":
return false
}
return v
}

// mapToKeyValArray converts a map into a _sorted_ list of KEY=VAL entries.
func mapToKeyValArray(m map[string]string) []string {
var arr []string
Expand Down
75 changes: 31 additions & 44 deletions nixose.go
Original file line number Diff line number Diff line change
@@ -1,41 +1,11 @@
package nixose

import (
"embed"
"fmt"
"strings"
"text/template"

"github.com/Masterminds/sprig"
)

//go:embed templates/*.tmpl
var templateFS embed.FS
var nixTemplates = template.New("nix").Funcs(sprig.FuncMap()).Funcs(funcMap)

func labelMapToLabelFlags(l map[string]string) []string {
// https://docs.docker.com/engine/reference/commandline/run/#label
// https://docs.podman.io/en/latest/markdown/podman-run.1.html#label-l-key-value
labels := mapToKeyValArray(l)
for i, label := range labels {
labels[i] = fmt.Sprintf("--label=%s", label)
}
return labels
}

func execTemplate(t *template.Template) func(string, any) (string, error) {
return func(name string, v any) (string, error) {
var s strings.Builder
err := t.ExecuteTemplate(&s, name, v)
return s.String(), err
}
}

var funcMap template.FuncMap = template.FuncMap{
"labelMapToLabelFlags": labelMapToLabelFlags,
"mapToKeyValArray": mapToKeyValArray,
}

const DefaultProjectSeparator = "-"

type ContainerRuntime int
Expand Down Expand Up @@ -98,22 +68,39 @@ type NixVolume struct {
Containers []string
}

// NixContainerSystemdConfig configures the container's systemd config.
// In particular, this allows control of the container restart policy through systemd
// service and unit configs.
//
// Each key-value pair in a map represents a systemd key and its value (e.g., Restart=always).
// Users can provide custom config keys by setting the nixose.systemd.* label on the service.
type NixContainerSystemdConfig struct {
Service map[string]any
Unit map[string]any
// NixOS treats these differently, probably to fix the rename issue in
// earlier systemd versions.
// See: https://unix.stackexchange.com/a/464098
StartLimitBurst *int
StartLimitIntervalSec *int
}

// https://search.nixos.org/options?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=oci-container
type NixContainer struct {
Project *Project
Runtime ContainerRuntime
Name string
Image string
Environment map[string]string
EnvFiles []string
Volumes map[string]string
Ports []string
Labels map[string]string
Networks []string
DependsOn []string
ExtraOptions []string
User string
AutoStart bool
Project *Project
Runtime ContainerRuntime
Name string
Image string
Environment map[string]string
EnvFiles []string
Volumes map[string]string
Ports []string
Labels map[string]string
Networks []string
DependsOn []string
ExtraOptions []string
SystemdConfig *NixContainerSystemdConfig
User string
AutoStart bool
}

type NixContainerConfig struct {
Expand Down
52 changes: 52 additions & 0 deletions template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package nixose

import (
"embed"
"fmt"
"strings"
"text/template"

"github.com/Masterminds/sprig"
)

//go:embed templates/*.tmpl
var templateFS embed.FS
var nixTemplates = template.New("nix").Funcs(sprig.FuncMap()).Funcs(funcMap)

func labelMapToLabelFlags(l map[string]string) []string {
// https://docs.docker.com/engine/reference/commandline/run/#label
// https://docs.podman.io/en/latest/markdown/podman-run.1.html#label-l-key-value
labels := mapToKeyValArray(l)
for i, label := range labels {
labels[i] = fmt.Sprintf("--label=%s", label)
}
return labels
}

func execTemplate(t *template.Template) func(string, any) (string, error) {
return func(name string, v any) (string, error) {
var s strings.Builder
err := t.ExecuteTemplate(&s, name, v)
return s.String(), err
}
}

func derefInt(v *int) int {
return *v
}

func toNixValue(v any) any {
switch v.(type) {
case string:
return fmt.Sprintf("%q", v)
default:
return v
}
}

var funcMap template.FuncMap = template.FuncMap{
"derefInt": derefInt,
"labelMapToLabelFlags": labelMapToLabelFlags,
"mapToKeyValArray": mapToKeyValArray,
"toNixValue": toNixValue,
}
26 changes: 25 additions & 1 deletion templates/container.nix.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,28 @@ virtualisation.oci-containers.containers."{{$name}}" = {
{{- if not .AutoStart}}
autoStart = false;
{{- end}}
};
};
{{- if .SystemdConfig}}
systemd.services."{{$runtime}}-{{$name}}" = {
{{- if .SystemdConfig.Service}}
serviceConfig = {
{{- range $k, $v := .SystemdConfig.Service}}
{{$k}} = {{toNixValue $v}};
{{- end}}
};
{{- end}}
{{- if .SystemdConfig.Unit}}
unitConfig = {
{{- range $k, $v := .SystemdConfig.Unit}}
{{$k}} = {{toNixValue $v}};
{{- end}}
};
{{- end}}
{{- if .SystemdConfig.StartLimitBurst}}
startLimitBurst = {{derefInt .SystemdConfig.StartLimitBurst}};
{{- end}}
{{- if .SystemdConfig.StartLimitIntervalSec}}
startLimitIntervalSec = {{derefInt .SystemdConfig.StartLimitIntervalSec}};
{{- end}}
};
{{- end}}
Loading

0 comments on commit dd04259

Please sign in to comment.