Skip to content

Commit

Permalink
match systemd restart with Compose behavior (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
aksiksi authored Mar 14, 2024
1 parent d1d383c commit 7d5e84b
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 47 deletions.
2 changes: 1 addition & 1 deletion compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig) (*NixContaine
}

// Restart policy.
if err := c.SystemdConfig.ParseRestartPolicy(&service); err != nil {
if err := c.SystemdConfig.ParseRestartPolicy(&service, g.Runtime); err != nil {
return nil, err
}

Expand Down
14 changes: 13 additions & 1 deletion nixos-test/docker-compose.nix
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@
};
systemd.services."docker-myproject-sabnzbd" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
Restart = lib.mkOverride 500 "no";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
RuntimeMaxSec = lib.mkOverride 500 360;
};
startLimitIntervalSec = 0;
unitConfig = {
Description = lib.mkOverride 500 "This is the sabnzbd container!";
};
Expand Down Expand Up @@ -69,6 +73,14 @@
systemd.services."docker-radarr" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
RuntimeMaxSec = lib.mkOverride 500 360;
};
startLimitIntervalSec = 0;
unitConfig = {
AllowIsolate = lib.mkOverride 500 false;
};
after = [
"docker-network-myproject-default.service"
Expand Down
4 changes: 4 additions & 0 deletions nixos-test/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
- /var/volumes/sabnzbd:/config
- storage:/storage
labels:
- 'compose2nix.systemd.service.Restart="no"'
- "compose2nix.systemd.service.RuntimeMaxSec=360"
- "compose2nix.systemd.unit.Description=This is the sabnzbd container!"
restart: unless-stopped
Expand All @@ -20,6 +21,9 @@ services:
volumes:
- /var/volumes/radarr:/config
- storage:/storage
labels:
- 'compose2nix.systemd.unit.AllowIsolate=no'
- "compose2nix.systemd.service.RuntimeMaxSec=360"
depends_on:
- sabnzbd
restart: unless-stopped
Expand Down
8 changes: 7 additions & 1 deletion nixos-test/podman-compose.nix
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
};
systemd.services."podman-myproject-sabnzbd" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
Restart = lib.mkOverride 500 "no";
RuntimeMaxSec = lib.mkOverride 500 360;
};
startLimitIntervalSec = 0;
unitConfig = {
Description = lib.mkOverride 500 "This is the sabnzbd container!";
};
Expand Down Expand Up @@ -72,6 +73,11 @@
systemd.services."podman-radarr" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
RuntimeMaxSec = lib.mkOverride 500 360;
};
startLimitIntervalSec = 0;
unitConfig = {
AllowIsolate = lib.mkOverride 500 false;
};
after = [
"podman-network-myproject-default.service"
Expand Down
76 changes: 53 additions & 23 deletions systemd.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ func parseSystemdValue(v string) any {
}

// String
// Remove all quotes from the string.
v = strings.ReplaceAll(v, `"`, "")
v = strings.ReplaceAll(v, `'`, "")

return v
}

Expand Down Expand Up @@ -135,65 +139,91 @@ func (s *SystemdCLI) FindMountForPath(path string) (string, error) {
return "", nil
}

func (c *NixContainerSystemdConfig) ParseRestartPolicy(service *types.ServiceConfig) error {
func (c *NixContainerSystemdConfig) ParseRestartPolicy(service *types.ServiceConfig, runtime ContainerRuntime) error {
indefiniteRestart := false

// https://docs.docker.com/compose/compose-file/compose-file-v2/#restart
switch restart := service.Restart; restart {
case "":
c.Service.Set("Restart", "no")
case "no", "always", "on-failure":
// All of these match the systemd restart options.
case "", "no":
// Need to use string literal here to avoid parsing as a boolean.
c.Service.Set("Restart", `"no"`)
case "always", "on-failure":
// Both of these match the systemd restart options.
c.Service.Set("Restart", restart)
indefiniteRestart = true
case "unless-stopped":
// We don't have an equivalent in systemd. Podman does the same thing.
c.Service.Set("Restart", "always")
indefiniteRestart = true
default:
if strings.HasPrefix(restart, "on-failure") && strings.Contains(restart, ":") {
if strings.HasPrefix(restart, "on-failure:") && len(strings.Split(restart, ":")) == 2 {
c.Service.Set("Restart", "on-failure")
maxAttemptsString := strings.TrimSpace(strings.Split(restart, ":")[1])
if maxAttempts, err := strconv.ParseInt(maxAttemptsString, 10, 64); err != nil {
maxAttempts, err := strconv.ParseInt(maxAttemptsString, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse on-failure attempts: %q: %w", maxAttemptsString, err)
} else {
burst := int(maxAttempts)
c.StartLimitBurst = &burst
// Retry limit resets once per day.
c.StartLimitIntervalSec = &defaultStartLimitIntervalSec
}
burst := int(maxAttempts)
c.StartLimitBurst = &burst
// Retry limit resets once per day to simulate "giving up" in systemd.
c.StartLimitIntervalSec = &defaultStartLimitIntervalSec
} else {
return fmt.Errorf("unsupported restart: %q", restart)
}
}

if service.Deploy == nil {
return 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 {
if deploy := service.Deploy; deploy != nil && deploy.RestartPolicy != nil {
switch condition := deploy.RestartPolicy.Condition; condition {
case "none":
c.Service.Set("Restart", "no")
// Need to use string literal here to avoid parsing as a boolean.
c.Service.Set("Restart", `"no"`)
case "", "any":
// If unset, defaults to "any".
c.Service.Set("Restart", "always")
indefiniteRestart = true
case "on-failure":
c.Service.Set("Restart", "on-failure")
default:
return fmt.Errorf("unsupported condition: %q", condition)
}
if delay := restartPolicy.Delay; delay != nil {
if delay := deploy.RestartPolicy.Delay; delay != nil {
c.Service.Set("RestartSec", delay.String())
} else {
c.Service.Set("RestartSec", 0)
}
if maxAttempts := restartPolicy.MaxAttempts; maxAttempts != nil {
if maxAttempts := deploy.RestartPolicy.MaxAttempts; maxAttempts != nil {
v := int(*maxAttempts)
c.StartLimitBurst = &v
}
if window := restartPolicy.Window; window != nil {
if window := deploy.RestartPolicy.Window; window != nil {
// TODO(aksiksi): Investigate if StartLimitIntervalSec lines up with Compose's "window".
windowSecs := int(time.Duration(*window).Seconds())
c.StartLimitIntervalSec = &windowSecs
} else if c.StartLimitBurst != nil {
// Retry limit resets once per day by default.
// Retry limit resets once per day to simulate "giving up" in systemd.
c.StartLimitIntervalSec = &defaultStartLimitIntervalSec
} else {
// Defaults to zero (i.e., indefinite).
s := 0
c.StartLimitIntervalSec = &s
}
}

if indefiniteRestart {
if c.StartLimitIntervalSec == nil {
s := 0
c.StartLimitIntervalSec = &s
}
if runtime == ContainerRuntimeDocker {
// This simulates the default behavior of Docker. Basically, Docker will restart
// the container with a sleep period of 100ms. This sleep period is doubled until a
// maximum of 1 minute.
// See: https://docs.docker.com/reference/cli/docker/container/run/#restart
c.Service.Set("RestartSec", "100ms")
c.Service.Set("RestartSteps", 9) // 2^(9 attempts) = 512 (* 100ms) ~= 1 minute
c.Service.Set("RestartMaxDelaySec", "1m")
}
}

Expand Down
18 changes: 15 additions & 3 deletions testdata/TestDocker_EnvFilesOnly_out.nix
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
systemd.services."docker-jellyseerr" = {
serviceConfig = {
Restart = lib.mkOverride 500 "on-failure";
RestartSec = lib.mkOverride 500 "5s";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
};
startLimitBurst = 3;
startLimitIntervalSec = 120;
Expand Down Expand Up @@ -92,8 +94,12 @@
systemd.services."docker-myproject-sabnzbd" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
RuntimeMaxSec = lib.mkOverride 500 10;
};
startLimitIntervalSec = 0;
unitConfig = {
Description = lib.mkOverride 500 "This is the sabnzbd container!";
};
Expand Down Expand Up @@ -139,7 +145,9 @@
systemd.services."docker-photoprism-mariadb" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
RestartSec = lib.mkOverride 500 "3m0s";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
};
startLimitBurst = 10;
startLimitIntervalSec = 86400;
Expand Down Expand Up @@ -259,8 +267,12 @@
};
systemd.services."docker-traefik" = {
serviceConfig = {
Restart = lib.mkOverride 500 "none";
Restart = lib.mkOverride 500 "no";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
};
startLimitIntervalSec = 0;
unitConfig = {
AllowIsolate = lib.mkOverride 500 true;
};
Expand Down
18 changes: 15 additions & 3 deletions testdata/TestDocker_OverrideSystemdStopTimeout_out.nix
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
systemd.services."docker-jellyseerr" = {
serviceConfig = {
Restart = lib.mkOverride 500 "on-failure";
RestartSec = lib.mkOverride 500 "5s";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
TimeoutStopSec = lib.mkOverride 500 10;
};
startLimitBurst = 3;
Expand Down Expand Up @@ -101,9 +103,13 @@
systemd.services."docker-myproject-sabnzbd" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
RuntimeMaxSec = lib.mkOverride 500 10;
TimeoutStopSec = lib.mkOverride 500 10;
};
startLimitIntervalSec = 0;
unitConfig = {
Description = lib.mkOverride 500 "This is the sabnzbd container!";
};
Expand Down Expand Up @@ -154,7 +160,9 @@
systemd.services."docker-photoprism-mariadb" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
RestartSec = lib.mkOverride 500 "3m0s";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
TimeoutStopSec = lib.mkOverride 500 10;
};
startLimitBurst = 10;
Expand Down Expand Up @@ -289,9 +297,13 @@
};
systemd.services."docker-traefik" = {
serviceConfig = {
Restart = lib.mkOverride 500 "none";
Restart = lib.mkOverride 500 "no";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
TimeoutStopSec = lib.mkOverride 500 10;
};
startLimitIntervalSec = 0;
unitConfig = {
AllowIsolate = lib.mkOverride 500 true;
};
Expand Down
18 changes: 15 additions & 3 deletions testdata/TestDocker_RemoveVolumes_out.nix
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
systemd.services."docker-jellyseerr" = {
serviceConfig = {
Restart = lib.mkOverride 500 "on-failure";
RestartSec = lib.mkOverride 500 "5s";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
};
startLimitBurst = 3;
startLimitIntervalSec = 120;
Expand Down Expand Up @@ -100,8 +102,12 @@
systemd.services."docker-myproject-sabnzbd" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
RuntimeMaxSec = lib.mkOverride 500 10;
};
startLimitIntervalSec = 0;
unitConfig = {
Description = lib.mkOverride 500 "This is the sabnzbd container!";
};
Expand Down Expand Up @@ -152,7 +158,9 @@
systemd.services."docker-photoprism-mariadb" = {
serviceConfig = {
Restart = lib.mkOverride 500 "always";
RestartSec = lib.mkOverride 500 "3m0s";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
};
startLimitBurst = 10;
startLimitIntervalSec = 86400;
Expand Down Expand Up @@ -285,8 +293,12 @@
};
systemd.services."docker-traefik" = {
serviceConfig = {
Restart = lib.mkOverride 500 "none";
Restart = lib.mkOverride 500 "no";
RestartMaxDelaySec = lib.mkOverride 500 "1m";
RestartSec = lib.mkOverride 500 "100ms";
RestartSteps = lib.mkOverride 500 9;
};
startLimitIntervalSec = 0;
unitConfig = {
AllowIsolate = lib.mkOverride 500 true;
};
Expand Down
Loading

0 comments on commit 7d5e84b

Please sign in to comment.