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 7b85279
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 26 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 buildContainerRestartPolicy(service *types.ServiceConfig) (*NixContainerRestartPolicy, error) {
p := &NixContainerRestartPolicy{
Service: make(map[string]string),
Unit: make(map[string]string),
}

// 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"] = condition
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] = value
case "unit":
p.Unit[key] = 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
}

restartPolicy, err := buildContainerRestartPolicy(&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),
RestartPolicy: restartPolicy,
DependsOn: dependsOn,
AutoStart: g.AutoStart,
}
slices.Sort(c.Networks)

Expand Down
47 changes: 33 additions & 14 deletions nixose.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ func execTemplate(t *template.Template) func(string, any) (string, error) {
}
}

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

var funcMap template.FuncMap = template.FuncMap{
"derefInt": derefInt,
"labelMapToLabelFlags": labelMapToLabelFlags,
"mapToKeyValArray": mapToKeyValArray,
}
Expand Down Expand Up @@ -98,22 +103,36 @@ type NixVolume struct {
Containers []string
}

// NixContainerRestartPolicy configures the container restart policy
// through systemd service and unit configs. Each key-value pair in the map
// represents a systemd key and its value (e.g., Restart=always).
type NixContainerRestartPolicy struct {
Service map[string]string
Unit map[string]string
// 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
RestartPolicy *NixContainerRestartPolicy
User string
AutoStart bool
}

type NixContainerConfig struct {
Expand Down
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 .RestartPolicy}}
systemd.services."{{$runtime}}-{{$name}}" = {
{{- if .RestartPolicy.Service}}
serviceConfig = {
{{- range $k, $v := .RestartPolicy.Service}}
{{$k}} = "{{$v}}";
{{- end}}
};
{{- end}}
{{- if .RestartPolicy.Unit}}
unitConfig = {
{{- range $k, $v := .RestartPolicy.Unit}}
{{$k}} = "{{$v}}";
{{- end}}
};
{{- end}}
{{- if .RestartPolicy.StartLimitBurst}}
startLimitBurst = {{derefInt .RestartPolicy.StartLimitBurst}};
{{- end}}
{{- if .RestartPolicy.StartLimitIntervalSec}}
startLimitIntervalSec = {{derefInt .RestartPolicy.StartLimitIntervalSec}};
{{- end}}
};
{{- end}}
27 changes: 27 additions & 0 deletions testdata/TestDocker_out.nix
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
];
autoStart = false;
};
systemd.services."docker-jellyseerr" = {
serviceConfig = {
Restart = "always";
};
};
virtualisation.oci-containers.containers."photoprism-mariadb" = {
image = "docker.io/library/mariadb:10.9";
environment = {
Expand All @@ -51,6 +56,11 @@
user = "1000:1000";
autoStart = false;
};
systemd.services."docker-photoprism-mariadb" = {
serviceConfig = {
Restart = "always";
};
};
virtualisation.oci-containers.containers."sabnzbd" = {
image = "lscr.io/linuxserver/sabnzbd";
environment = {
Expand Down Expand Up @@ -78,6 +88,12 @@
];
autoStart = false;
};
systemd.services."docker-sabnzbd" = {
serviceConfig = {
Restart = "always";
RuntimeMaxSec = "10";
};
};
virtualisation.oci-containers.containers."traefik" = {
image = "docker.io/library/traefik";
environment = {
Expand Down Expand Up @@ -106,6 +122,11 @@
];
autoStart = false;
};
systemd.services."docker-traefik" = {
serviceConfig = {
Restart = "always";
};
};
virtualisation.oci-containers.containers."transmission" = {
image = "docker.io/haugene/transmission-openvpn";
environment = {
Expand Down Expand Up @@ -146,6 +167,12 @@
];
autoStart = false;
};
systemd.services."docker-transmission" = {
serviceConfig = {
Restart = "on-failure";
};
startLimitBurst = 3;
};

# Networks
systemd.services."create-docker-network-default" = {
Expand Down
27 changes: 27 additions & 0 deletions testdata/TestPodman_out.nix
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
];
autoStart = false;
};
systemd.services."podman-jellyseerr" = {
serviceConfig = {
Restart = "always";
};
};
virtualisation.oci-containers.containers."photoprism-mariadb" = {
image = "docker.io/library/mariadb:10.9";
environment = {
Expand All @@ -56,6 +61,11 @@
user = "1000:1000";
autoStart = false;
};
systemd.services."podman-photoprism-mariadb" = {
serviceConfig = {
Restart = "always";
};
};
virtualisation.oci-containers.containers."sabnzbd" = {
image = "lscr.io/linuxserver/sabnzbd";
environment = {
Expand Down Expand Up @@ -83,6 +93,12 @@
];
autoStart = false;
};
systemd.services."podman-sabnzbd" = {
serviceConfig = {
Restart = "always";
RuntimeMaxSec = "10";
};
};
virtualisation.oci-containers.containers."traefik" = {
image = "docker.io/library/traefik";
environment = {
Expand Down Expand Up @@ -111,6 +127,11 @@
];
autoStart = false;
};
systemd.services."podman-traefik" = {
serviceConfig = {
Restart = "always";
};
};
virtualisation.oci-containers.containers."transmission" = {
image = "docker.io/haugene/transmission-openvpn";
environment = {
Expand Down Expand Up @@ -151,6 +172,12 @@
];
autoStart = false;
};
systemd.services."podman-transmission" = {
serviceConfig = {
Restart = "on-failure";
};
startLimitBurst = 3;
};

# Networks
systemd.services."create-podman-network-default" = {
Expand Down
1 change: 1 addition & 0 deletions testdata/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ services:
- "traefik.http.routers.sabnzbd.rule=Host(`${HOME_DOMAIN}`) && PathPrefix(`/sabnzbd`)"
- "traefik.http.routers.sabnzbd.tls.certresolver=htpc"
- "traefik.http.routers.sabnzbd.middlewares=chain-authelia@file"
- "nixose.systemd.service.RuntimeMaxSec=10"
logging:
driver: "json-file"
options:
Expand Down

0 comments on commit 7b85279

Please sign in to comment.