diff --git a/compose.go b/compose.go index 992cdb5..b507518 100644 --- a/compose.go +++ b/compose.go @@ -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 } diff --git a/nixos-test/docker-compose.nix b/nixos-test/docker-compose.nix index 76ae63c..9331597 100644 --- a/nixos-test/docker-compose.nix +++ b/nixos-test/docker-compose.nix @@ -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!"; }; @@ -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" diff --git a/nixos-test/docker-compose.yml b/nixos-test/docker-compose.yml index 4da4b24..5e0112c 100644 --- a/nixos-test/docker-compose.yml +++ b/nixos-test/docker-compose.yml @@ -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 @@ -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 diff --git a/nixos-test/podman-compose.nix b/nixos-test/podman-compose.nix index 70a6268..7315b52 100644 --- a/nixos-test/podman-compose.nix +++ b/nixos-test/podman-compose.nix @@ -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!"; }; @@ -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" diff --git a/systemd.go b/systemd.go index 4cf7cc2..3fef3b2 100644 --- a/systemd.go +++ b/systemd.go @@ -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 } @@ -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") } } diff --git a/testdata/TestDocker_EnvFilesOnly_out.nix b/testdata/TestDocker_EnvFilesOnly_out.nix index 3524fee..93f2f01 100644 --- a/testdata/TestDocker_EnvFilesOnly_out.nix +++ b/testdata/TestDocker_EnvFilesOnly_out.nix @@ -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; @@ -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!"; }; @@ -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; @@ -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; }; diff --git a/testdata/TestDocker_OverrideSystemdStopTimeout_out.nix b/testdata/TestDocker_OverrideSystemdStopTimeout_out.nix index b301823..5a68009 100644 --- a/testdata/TestDocker_OverrideSystemdStopTimeout_out.nix +++ b/testdata/TestDocker_OverrideSystemdStopTimeout_out.nix @@ -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; @@ -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!"; }; @@ -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; @@ -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; }; diff --git a/testdata/TestDocker_RemoveVolumes_out.nix b/testdata/TestDocker_RemoveVolumes_out.nix index f88caaf..3efa683 100644 --- a/testdata/TestDocker_RemoveVolumes_out.nix +++ b/testdata/TestDocker_RemoveVolumes_out.nix @@ -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; @@ -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!"; }; @@ -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; @@ -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; }; diff --git a/testdata/TestDocker_SystemdMount_out.nix b/testdata/TestDocker_SystemdMount_out.nix index b43371b..66aff16 100644 --- a/testdata/TestDocker_SystemdMount_out.nix +++ b/testdata/TestDocker_SystemdMount_out.nix @@ -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; @@ -102,8 +104,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!"; }; @@ -156,7 +162,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; @@ -293,8 +301,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; }; diff --git a/testdata/TestDocker_WithProject_out.nix b/testdata/TestDocker_WithProject_out.nix index 9eb1ace..e8d99c1 100644 --- a/testdata/TestDocker_WithProject_out.nix +++ b/testdata/TestDocker_WithProject_out.nix @@ -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; @@ -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!"; }; @@ -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; @@ -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; }; diff --git a/testdata/TestDocker_out.nix b/testdata/TestDocker_out.nix index a9d7f26..bac6d95 100644 --- a/testdata/TestDocker_out.nix +++ b/testdata/TestDocker_out.nix @@ -43,7 +43,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; @@ -98,8 +100,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!"; }; @@ -149,7 +155,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; @@ -280,8 +288,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; }; diff --git a/testdata/TestPodman_WithProject_out.nix b/testdata/TestPodman_WithProject_out.nix index 055ee15..b285836 100644 --- a/testdata/TestPodman_WithProject_out.nix +++ b/testdata/TestPodman_WithProject_out.nix @@ -101,6 +101,7 @@ Restart = lib.mkOverride 500 "always"; RuntimeMaxSec = lib.mkOverride 500 10; }; + startLimitIntervalSec = 0; unitConfig = { Description = lib.mkOverride 500 "This is the sabnzbd container!"; }; @@ -274,8 +275,9 @@ }; systemd.services."podman-traefik" = { serviceConfig = { - Restart = lib.mkOverride 500 "none"; + Restart = lib.mkOverride 500 "no"; }; + startLimitIntervalSec = 0; unitConfig = { AllowIsolate = lib.mkOverride 500 true; }; diff --git a/testdata/TestPodman_out.nix b/testdata/TestPodman_out.nix index e285309..d5304ed 100644 --- a/testdata/TestPodman_out.nix +++ b/testdata/TestPodman_out.nix @@ -99,6 +99,7 @@ Restart = lib.mkOverride 500 "always"; RuntimeMaxSec = lib.mkOverride 500 10; }; + startLimitIntervalSec = 0; unitConfig = { Description = lib.mkOverride 500 "This is the sabnzbd container!"; }; @@ -269,8 +270,9 @@ }; systemd.services."podman-traefik" = { serviceConfig = { - Restart = lib.mkOverride 500 "none"; + Restart = lib.mkOverride 500 "no"; }; + startLimitIntervalSec = 0; unitConfig = { AllowIsolate = lib.mkOverride 500 true; }; diff --git a/testdata/docker-compose.yml b/testdata/docker-compose.yml index 060d142..85fa110 100644 --- a/testdata/docker-compose.yml +++ b/testdata/docker-compose.yml @@ -184,7 +184,7 @@ services: - "traefik.http.routers.traefik.service=api@internal" - "traefik.http.routers.traefik.tls.certresolver=htpc" - "traefik.http.routers.traefik.middlewares=chain-authelia@file" - - "compose2nix.systemd.service.Restart=none" + - "compose2nix.systemd.service.Restart='no'" - "compose2nix.systemd.unit.AllowIsolate=true" network_mode: "container:sabnzbd" runtime: nvidia