From a1eb50044a57c9888bb4fa1ee4399b3986ae43a8 Mon Sep 17 00:00:00 2001 From: Assil Ksiksi Date: Mon, 18 Dec 2023 22:11:03 -0500 Subject: [PATCH] use systemd Upholds with depends_on By default, systemd does not start a service if its dependencies started succesfully after failing. Upholds (reverse: UpheldBy) will handle starting dependent containers for us to mimic Compose behavior. --- README.md | 49 ++++++++++++----------- compose.go | 38 +++++++++++++----- nixos-test/docker-compose.nix | 6 +++ nixos-test/docker-compose.yml | 2 + nixos-test/podman-compose.nix | 6 +++ systemd.go | 3 ++ templates/container.nix.tmpl | 7 ++++ testdata/TestDocker_EnvFilesOnly_out.nix | 9 +++++ testdata/TestDocker_RemoveVolumes_out.nix | 9 +++++ testdata/TestDocker_SystemdMount_out.nix | 9 +++++ testdata/TestDocker_WithProject_out.nix | 9 +++++ testdata/TestDocker_out.nix | 9 +++++ testdata/TestPodman_WithProject_out.nix | 9 +++++ testdata/TestPodman_out.nix | 9 +++++ 14 files changed, 139 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index ccfe9dd..dd3aabc 100644 --- a/README.md +++ b/README.md @@ -184,30 +184,31 @@ If a feature is missing, please feel free to [create an issue](https://github.co #### [`services`](https://docs.docker.com/compose/compose-file/05-services/) -| | | -|---|:---:| -| [`image`](https://docs.docker.com/compose/compose-file/05-services/#image) | ✅ | -| [`container_name`](https://docs.docker.com/compose/compose-file/05-services/#container_name) | ✅ | -| [`environment`](https://docs.docker.com/compose/compose-file/05-services/#environment) | ✅ | -| [`volumes`](https://docs.docker.com/compose/compose-file/05-services/#volumes) | ✅ | -| [`labels`](https://docs.docker.com/compose/compose-file/05-services/#labels) | ✅ | -| [`ports`](https://docs.docker.com/compose/compose-file/05-services/#ports) | ✅ | -| [`dns`](https://docs.docker.com/compose/compose-file/05-services/#dns) | ✅ | -| [`cap_add/cap_drop`](https://docs.docker.com/compose/compose-file/05-services/#cap_add) | ✅ | -| [`logging`](https://docs.docker.com/compose/compose-file/05-services/#logging) | ✅ | -| [`restart`](https://docs.docker.com/compose/compose-file/05-services/#restart) | ✅ | -| [`deploy.restart_policy`](https://docs.docker.com/compose/compose-file/deploy/#restart_policy) | ✅ | -| [`devices`](https://docs.docker.com/compose/compose-file/05-services/#devices) | ✅ | -| [`networks.aliases`](https://docs.docker.com/compose/compose-file/05-services/#aliases) | ✅ | -| [`network_mode`](https://docs.docker.com/compose/compose-file/05-services/#network_mode) | ✅ | -| [`privileged`](https://docs.docker.com/compose/compose-file/05-services/#privileged) | ✅ | -| [`extra_hosts`](https://docs.docker.com/compose/compose-file/05-services/#extra_hosts) | ✅ | -| [`sysctls`](https://docs.docker.com/compose/compose-file/05-services/#sysctls) | ✅ | -| [`shm_size`](https://docs.docker.com/compose/compose-file/05-services/#shm_size) | ✅ | -| [`runtime`](https://docs.docker.com/compose/compose-file/05-services/#runtime) | ✅ | -| [`security_opt`](https://docs.docker.com/compose/compose-file/05-services/#security_opt) | ✅ | -| [`command`](https://docs.docker.com/compose/compose-file/05-services/#command) | ✅ | -| [`healthcheck`](https://docs.docker.com/compose/compose-file/05-services/#healthcheck) | ✅ | +| | | Notes | +|---|:---:|-------| +| [`image`](https://docs.docker.com/compose/compose-file/05-services/#image) | ✅ | | +| [`container_name`](https://docs.docker.com/compose/compose-file/05-services/#container_name) | ✅ | | +| [`environment`](https://docs.docker.com/compose/compose-file/05-services/#environment) | ✅ | | +| [`volumes`](https://docs.docker.com/compose/compose-file/05-services/#volumes) | ✅ | | +| [`labels`](https://docs.docker.com/compose/compose-file/05-services/#labels) | ✅ | | +| [`ports`](https://docs.docker.com/compose/compose-file/05-services/#ports) | ✅ | | +| [`dns`](https://docs.docker.com/compose/compose-file/05-services/#dns) | ✅ | | +| [`cap_add/cap_drop`](https://docs.docker.com/compose/compose-file/05-services/#cap_add) | ✅ | | +| [`logging`](https://docs.docker.com/compose/compose-file/05-services/#logging) | ✅ | | +| [`depends_on`](https://docs.docker.com/compose/compose-file/05-services/#depends_on) | ⚠️ | Only short syntax is supported. | +| [`restart`](https://docs.docker.com/compose/compose-file/05-services/#restart) | ✅ | | +| [`deploy.restart_policy`](https://docs.docker.com/compose/compose-file/deploy/#restart_policy) | ✅ | | +| [`devices`](https://docs.docker.com/compose/compose-file/05-services/#devices) | ✅ | | +| [`networks.aliases`](https://docs.docker.com/compose/compose-file/05-services/#aliases) | ✅ | | +| [`network_mode`](https://docs.docker.com/compose/compose-file/05-services/#network_mode) | ✅ | | +| [`privileged`](https://docs.docker.com/compose/compose-file/05-services/#privileged) | ✅ | | +| [`extra_hosts`](https://docs.docker.com/compose/compose-file/05-services/#extra_hosts) | ✅ | | +| [`sysctls`](https://docs.docker.com/compose/compose-file/05-services/#sysctls) | ✅ | | +| [`shm_size`](https://docs.docker.com/compose/compose-file/05-services/#shm_size) | ✅ | | +| [`runtime`](https://docs.docker.com/compose/compose-file/05-services/#runtime) | ✅ | | +| [`security_opt`](https://docs.docker.com/compose/compose-file/05-services/#security_opt) | ✅ | | +| [`command`](https://docs.docker.com/compose/compose-file/05-services/#command) | ✅ | | +| [`healthcheck`](https://docs.docker.com/compose/compose-file/05-services/#healthcheck) | ✅ | | #### [`networks`](https://docs.docker.com/compose/compose-file/06-networks/) diff --git a/compose.go b/compose.go index f5a8d98..f652183 100644 --- a/compose.go +++ b/compose.go @@ -266,6 +266,9 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig) (*NixContaine } // Figure out explicit dependencies for this container. + // + // TODO(aksiksi): Support the long syntax. + // https://docs.docker.com/compose/compose-file/05-services/#long-syntax-1 for _, s := range service.GetDependencies() { targetContainerName, ok := g.serviceToContainerName[s] if !ok { @@ -429,17 +432,6 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig) (*NixContaine c.ExtraOptions = append(c.ExtraOptions, mapToRepeatedKeyValFlag("--log-opt", logging.Options)...) } - // Add systemd dependencies on network(s). - for _, networkName := range c.Networks { - c.SystemdConfig.Unit.After = append(c.SystemdConfig.Unit.After, g.networkNameToService(networkName)) - c.SystemdConfig.Unit.Requires = append(c.SystemdConfig.Unit.Requires, g.networkNameToService(networkName)) - } - // Add systemd dependency on root target. - if !g.NoCreateRootTarget { - c.SystemdConfig.Unit.PartOf = append(c.SystemdConfig.Unit.PartOf, fmt.Sprintf("%s.target", rootTarget(g.Runtime, g.Project))) - c.SystemdConfig.Unit.WantedBy = append(c.SystemdConfig.Unit.WantedBy, fmt.Sprintf("%s.target", rootTarget(g.Runtime, g.Project))) - } - // Health check. // https://docs.docker.com/compose/compose-file/05-services/#healthcheck if healthCheck := service.HealthCheck; healthCheck != nil { @@ -489,6 +481,30 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig) (*NixContaine slices.Sort(c.ExtraOptions) slices.Sort(c.Networks) + // Add systemd dependencies on network(s). + for _, networkName := range c.Networks { + c.SystemdConfig.Unit.After = append(c.SystemdConfig.Unit.After, g.networkNameToService(networkName)) + c.SystemdConfig.Unit.Requires = append(c.SystemdConfig.Unit.Requires, g.networkNameToService(networkName)) + } + // Add systemd dependency on root target. + if !g.NoCreateRootTarget { + c.SystemdConfig.Unit.PartOf = append(c.SystemdConfig.Unit.PartOf, fmt.Sprintf("%s.target", rootTarget(g.Runtime, g.Project))) + c.SystemdConfig.Unit.WantedBy = append(c.SystemdConfig.Unit.WantedBy, fmt.Sprintf("%s.target", rootTarget(g.Runtime, g.Project))) + } + + // Set UpheldBy for this service's dependencies. This ensures that, when the dependency comes up, + // this container will also be started - and continuously restarted with backoff - until it comes up. + // See: https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#Upholds= + // + // Why do we need to do this? Because, by default, systemd does not attempt to start failed dependent units + // when the parent (dependency) comes up. See: https://github.com/systemd/systemd/issues/1312. + // + // Note 2: Upholds is supported in version 249+. NixOS 23.05 uses 253.6, so this should always be supported. + // See: https://github.com/NixOS/nixpkgs/blob/nixos-23.05/pkgs/os-specific/linux/systemd/default.nix#L148. + for _, containerName := range c.DependsOn { + c.SystemdConfig.Unit.UpheldBy = append(c.SystemdConfig.Unit.UpheldBy, fmt.Sprintf("%s-%s.service", g.Runtime, containerName)) + } + return c, nil } diff --git a/nixos-test/docker-compose.nix b/nixos-test/docker-compose.nix index dd952a1..44fe33b 100644 --- a/nixos-test/docker-compose.nix +++ b/nixos-test/docker-compose.nix @@ -57,6 +57,9 @@ "/var/volumes/radarr:/config:rw" "storage:/storage:rw" ]; + dependsOn = [ + "myproject-sabnzbd" + ]; log-driver = "journald"; extraOptions = [ "--network-alias=radarr" @@ -78,6 +81,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; diff --git a/nixos-test/docker-compose.yml b/nixos-test/docker-compose.yml index 402efdf..4da4b24 100644 --- a/nixos-test/docker-compose.yml +++ b/nixos-test/docker-compose.yml @@ -20,6 +20,8 @@ services: volumes: - /var/volumes/radarr:/config - storage:/storage + depends_on: + - sabnzbd restart: unless-stopped networks: diff --git a/nixos-test/podman-compose.nix b/nixos-test/podman-compose.nix index da68ea9..febe084 100644 --- a/nixos-test/podman-compose.nix +++ b/nixos-test/podman-compose.nix @@ -60,6 +60,9 @@ "/mnt/media:/storage:rw" "/var/volumes/radarr:/config:rw" ]; + dependsOn = [ + "myproject-sabnzbd" + ]; log-driver = "journald"; extraOptions = [ "--network-alias=radarr" @@ -79,6 +82,9 @@ partOf = [ "podman-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "podman-myproject-sabnzbd.service" + ]; wantedBy = [ "podman-compose-myproject-root.target" ]; diff --git a/systemd.go b/systemd.go index 6e9de12..f85e481 100644 --- a/systemd.go +++ b/systemd.go @@ -70,6 +70,7 @@ type UnitConfig struct { After []string Requires []string PartOf []string + UpheldBy []string WantedBy []string // Map for generic options. Options map[string]any @@ -86,6 +87,8 @@ func (u *UnitConfig) Set(key string, value any) { u.Requires = append(u.Requires, value.(string)) case "PartOf": u.PartOf = append(u.PartOf, value.(string)) + case "UpheldBy": + u.UpheldBy = append(u.UpheldBy, value.(string)) case "WantedBy": u.WantedBy = append(u.WantedBy, value.(string)) default: diff --git a/templates/container.nix.tmpl b/templates/container.nix.tmpl index 7c75947..0932a28 100644 --- a/templates/container.nix.tmpl +++ b/templates/container.nix.tmpl @@ -116,6 +116,13 @@ systemd.services."{{.Runtime}}-{{.Name}}" = { {{- end}} ]; {{- end}} + {{- if .SystemdConfig.Unit.UpheldBy}} + unitConfig.UpheldBy = [ + {{- range .SystemdConfig.Unit.UpheldBy}} + "{{.}}" + {{- end}} + ]; + {{- end}} {{- if .SystemdConfig.Unit.WantedBy}} wantedBy = [ {{- range .SystemdConfig.Unit.WantedBy}} diff --git a/testdata/TestDocker_EnvFilesOnly_out.nix b/testdata/TestDocker_EnvFilesOnly_out.nix index e525967..493c047 100644 --- a/testdata/TestDocker_EnvFilesOnly_out.nix +++ b/testdata/TestDocker_EnvFilesOnly_out.nix @@ -55,6 +55,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -211,6 +214,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -260,6 +266,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; diff --git a/testdata/TestDocker_RemoveVolumes_out.nix b/testdata/TestDocker_RemoveVolumes_out.nix index 896dfae..10bcbee 100644 --- a/testdata/TestDocker_RemoveVolumes_out.nix +++ b/testdata/TestDocker_RemoveVolumes_out.nix @@ -57,6 +57,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -236,6 +239,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -286,6 +292,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; diff --git a/testdata/TestDocker_SystemdMount_out.nix b/testdata/TestDocker_SystemdMount_out.nix index 865130b..c280115 100644 --- a/testdata/TestDocker_SystemdMount_out.nix +++ b/testdata/TestDocker_SystemdMount_out.nix @@ -59,6 +59,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -244,6 +247,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -294,6 +300,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; diff --git a/testdata/TestDocker_WithProject_out.nix b/testdata/TestDocker_WithProject_out.nix index fe5d820..e07dd91 100644 --- a/testdata/TestDocker_WithProject_out.nix +++ b/testdata/TestDocker_WithProject_out.nix @@ -57,6 +57,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -236,6 +239,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -286,6 +292,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; diff --git a/testdata/TestDocker_out.nix b/testdata/TestDocker_out.nix index e8f150f..9805522 100644 --- a/testdata/TestDocker_out.nix +++ b/testdata/TestDocker_out.nix @@ -56,6 +56,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -232,6 +235,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-myproject-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; @@ -281,6 +287,9 @@ partOf = [ "docker-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "docker-sabnzbd.service" + ]; wantedBy = [ "docker-compose-myproject-root.target" ]; diff --git a/testdata/TestPodman_WithProject_out.nix b/testdata/TestPodman_WithProject_out.nix index 77b2217..314f724 100644 --- a/testdata/TestPodman_WithProject_out.nix +++ b/testdata/TestPodman_WithProject_out.nix @@ -56,6 +56,9 @@ partOf = [ "podman-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "podman-myproject-sabnzbd.service" + ]; wantedBy = [ "podman-compose-myproject-root.target" ]; @@ -225,6 +228,9 @@ partOf = [ "podman-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "podman-myproject-sabnzbd.service" + ]; wantedBy = [ "podman-compose-myproject-root.target" ]; @@ -275,6 +281,9 @@ partOf = [ "podman-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "podman-sabnzbd.service" + ]; wantedBy = [ "podman-compose-myproject-root.target" ]; diff --git a/testdata/TestPodman_out.nix b/testdata/TestPodman_out.nix index e77012e..2af245a 100644 --- a/testdata/TestPodman_out.nix +++ b/testdata/TestPodman_out.nix @@ -55,6 +55,9 @@ partOf = [ "podman-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "podman-myproject-sabnzbd.service" + ]; wantedBy = [ "podman-compose-myproject-root.target" ]; @@ -221,6 +224,9 @@ partOf = [ "podman-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "podman-myproject-sabnzbd.service" + ]; wantedBy = [ "podman-compose-myproject-root.target" ]; @@ -270,6 +276,9 @@ partOf = [ "podman-compose-myproject-root.target" ]; + unitConfig.UpheldBy = [ + "podman-sabnzbd.service" + ]; wantedBy = [ "podman-compose-myproject-root.target" ];