Repository: aksiksi/compose2nix (go)

Issue:
serviceConfig Restart no with quoted strings
Hey, thanks for the great tool! With the following minimal `docker-compose.yml`
```
services:
  test:
    image: nginx:latest
```
and `compose2nix -project test` I get
```
  systemd.services."podman-test-test" = {
    serviceConfig = {
      Restart = lib.mkOverride 500 "\"no\"";
    };
  };
```
where nix complains about the [escaped quotes](https://github.com/NixOS/nixpkgs/blob/master/nixos/lib/systemd-unit-options.nix#L35) with something like this
```
       error: A definition for option `systemd.services.podman-test-test.serviceConfig' is not of type `attribute set of (systemd option)'. Definition values:
       - In `/nix/store/yv0aqwqsvns7rx164q6lvjgy1fpvfk0c-source/test.nix':
           {
             Restart = {
               _type = "override";
               content = "\"no\"";
               priority = 500;
           ...
```
please let me know if you would like to have a PR for that

Pull request description:
fix: handle Systemd `Restart=no` correctly
The root cause is that we had special handling for `yes/no` as Systemd booleans. This meant that the string `no` would be interpreted as `false`, which is not valid for the `Restart` setting.

To fix this and reduce ambiguity in general, we will now always treat yes/no as Systemd strings. And Nix supports both booleans and strings for boolean settings anyways, so no issues there.

Fixes #27.

Patch:
diff --git a/systemd.go b/systemd.go
index 56f6d0d..b3bdb14 100644
--- a/systemd.go
+++ b/systemd.go
@@ -19,9 +19,10 @@ const (
 )
 
 var (
-	// We purposefully do not support 0/1 for false/true.
-	systemdTrue  = []string{"true", "yes", "on"}
-	systemdFalse = []string{"false", "no", "off"}
+	// We purposefully do not support no/yes or 0/1 for false/true to avoid
+	// ambiguity. Nix supports strings and bools for boolean keys anyways.
+	systemdTrue  = []string{"true", "on"}
+	systemdFalse = []string{"false", "off"}
 
 	// Examples:
 	// compose2nix.systemd.service.RuntimeMaxSec=100
@@ -107,8 +108,7 @@ func (c *NixContainerSystemdConfig) ParseRestartPolicy(service *types.ServiceCon
 	// https://docs.docker.com/compose/compose-file/compose-file-v2/#restart
 	switch restart := service.Restart; restart {
 	case "", "no":
-		// Need to use string literal here to avoid parsing as a boolean.
-		c.Service.Set("Restart", `"no"`)
+		c.Service.Set("Restart", "no")
 	case "always", "on-failure":
 		// Both of these match the systemd restart options.
 		c.Service.Set("Restart", restart)
@@ -138,8 +138,7 @@ func (c *NixContainerSystemdConfig) ParseRestartPolicy(service *types.ServiceCon
 	if deploy := service.Deploy; deploy != nil && deploy.RestartPolicy != nil {
 		switch condition := deploy.RestartPolicy.Condition; condition {
 		case "none":
-			// Need to use string literal here to avoid parsing as a boolean.
-			c.Service.Set("Restart", `"no"`)
+			c.Service.Set("Restart", "no")
 		case "", "any":
 			// If unset, defaults to "any".
 			c.Service.Set("Restart", "always")


Test patch:
diff --git a/nix_test.go b/nix_test.go
index 6e0a52c..21c1b79 100644
--- a/nix_test.go
+++ b/nix_test.go
@@ -207,3 +207,12 @@ func TestRelativeServiceVolumes(t *testing.T) {
 	}
 	runSubtestsWithGenerator(t, g)
 }
+
+func TestNoRestart(t *testing.T) {
+	composePath, _ := getPaths(t, false)
+	g := &Generator{
+		Inputs:  []string{composePath},
+		Project: NewProject("test"),
+	}
+	runSubtestsWithGenerator(t, g)
+}
diff --git a/nixos-test/docker-compose.nix b/nixos-test/docker-compose.nix
index 24a3e7b..b1d000e 100644
--- a/nixos-test/docker-compose.nix
+++ b/nixos-test/docker-compose.nix
@@ -10,6 +10,31 @@
   virtualisation.oci-containers.backend = "docker";
 
   # Containers
+  virtualisation.oci-containers.containers."myproject-no-restart" = {
+    image = "docker.io/library/nginx:stable-alpine-slim";
+    log-driver = "journald";
+    extraOptions = [
+      "--network-alias=no-restart"
+      "--network=myproject_default"
+    ];
+  };
+  systemd.services."docker-myproject-no-restart" = {
+    serviceConfig = {
+      Restart = lib.mkOverride 500 "no";
+    };
+    after = [
+      "docker-network-myproject_default.service"
+    ];
+    requires = [
+      "docker-network-myproject_default.service"
+    ];
+    partOf = [
+      "docker-compose-myproject-root.target"
+    ];
+    wantedBy = [
+      "docker-compose-myproject-root.target"
+    ];
+  };
   virtualisation.oci-containers.containers."myproject-service-a" = {
     image = "docker.io/library/nginx:stable-alpine-slim";
     environment = {
@@ -83,7 +108,7 @@
     };
     startLimitBurst = 3;
     unitConfig = {
-      AllowIsolate = lib.mkOverride 500 false;
+      AllowIsolate = lib.mkOverride 500 "no";
       StartLimitIntervalSec = lib.mkOverride 500 "infinity";
     };
     after = [
diff --git a/nixos-test/docker-compose.yml b/nixos-test/docker-compose.yml
index 885f277..47c3fdb 100644
--- a/nixos-test/docker-compose.yml
+++ b/nixos-test/docker-compose.yml
@@ -12,7 +12,7 @@ services:
         reservations:
           cpus: "0.5"
     labels:
-      - 'compose2nix.systemd.service.Restart="no"'
+      - 'compose2nix.systemd.service.Restart=no'
       - "compose2nix.systemd.service.RuntimeMaxSec=360"
       - "compose2nix.systemd.unit.Description=This is the service-a container!"
     restart: unless-stopped
@@ -36,6 +36,8 @@ services:
       something:
         ipv4_address: 192.168.8.20
     restart: on-failure:3
+  no-restart:
+    image: docker.io/library/nginx:stable-alpine-slim
 
 networks:
   something:
diff --git a/nixos-test/podman-compose.nix b/nixos-test/podman-compose.nix
index 6997cf9..ac30f58 100644
--- a/nixos-test/podman-compose.nix
+++ b/nixos-test/podman-compose.nix
@@ -15,6 +15,31 @@
   virtualisation.oci-containers.backend = "podman";
 
   # Containers
+  virtualisation.oci-containers.containers."myproject-no-restart" = {
+    image = "docker.io/library/nginx:stable-alpine-slim";
+    log-driver = "journald";
+    extraOptions = [
+      "--network-alias=no-restart"
+      "--network=myproject_default"
+    ];
+  };
+  systemd.services."podman-myproject-no-restart" = {
+    serviceConfig = {
+      Restart = lib.mkOverride 500 "no";
+    };
+    after = [
+      "podman-network-myproject_default.service"
+    ];
+    requires = [
+      "podman-network-myproject_default.service"
+    ];
+    partOf = [
+      "podman-compose-myproject-root.target"
+    ];
+    wantedBy = [
+      "podman-compose-myproject-root.target"
+    ];
+  };
   virtualisation.oci-containers.containers."myproject-service-a" = {
     image = "docker.io/library/nginx:stable-alpine-slim";
     environment = {
@@ -85,7 +110,7 @@
     };
     startLimitBurst = 3;
     unitConfig = {
-      AllowIsolate = lib.mkOverride 500 false;
+      AllowIsolate = lib.mkOverride 500 "no";
       StartLimitIntervalSec = lib.mkOverride 500 "infinity";
     };
     after = [
diff --git a/nixos-test/test.nix b/nixos-test/test.nix
index b3bd5f5..b549602 100644
--- a/nixos-test/test.nix
+++ b/nixos-test/test.nix
@@ -15,6 +15,7 @@ let
     virtualisation.graphics = false;
     virtualisation.oci-containers.containers."myproject-service-a".imageFile = nginxImage;
     virtualisation.oci-containers.containers."service-b".imageFile = nginxImage;
+    virtualisation.oci-containers.containers."myproject-no-restart".imageFile = nginxImage;
     environment.systemPackages = [ pkgs.jq ];
     system.stateVersion = "23.05";
   };
@@ -52,6 +53,7 @@ in
       # Wait for container services.
       m.wait_for_unit(f"{runtime}-myproject-service-a.service")
       m.wait_for_unit(f"{runtime}-service-b.service")
+      m.wait_for_unit(f"{runtime}-myproject-no-restart.service")
 
       # Wait until the health check succeeds.
       m.wait_until_succeeds(f"{runtime} inspect service-b | jq .[0].State.Health.Status | grep healthy", timeout=30)
diff --git a/testdata/TestBasic.docker.nix b/testdata/TestBasic.docker.nix
index 5edbe4e..dc797c3 100644
--- a/testdata/TestBasic.docker.nix
+++ b/testdata/TestBasic.docker.nix
@@ -293,9 +293,6 @@
   systemd.services."docker-traefik" = {
     serviceConfig = {
       Restart = lib.mkOverride 500 "no";
-      RestartMaxDelaySec = lib.mkOverride 500 "1m";
-      RestartSec = lib.mkOverride 500 "100ms";
-      RestartSteps = lib.mkOverride 500 9;
     };
     unitConfig = {
       AllowIsolate = lib.mkOverride 500 true;
diff --git a/testdata/TestEnvFilesOnly.docker.nix b/testdata/TestEnvFilesOnly.docker.nix
index 4c064b6..4ff604d 100644
--- a/testdata/TestEnvFilesOnly.docker.nix
+++ b/testdata/TestEnvFilesOnly.docker.nix
@@ -272,9 +272,6 @@
   systemd.services."docker-traefik" = {
     serviceConfig = {
       Restart = lib.mkOverride 500 "no";
-      RestartMaxDelaySec = lib.mkOverride 500 "1m";
-      RestartSec = lib.mkOverride 500 "100ms";
-      RestartSteps = lib.mkOverride 500 9;
     };
     unitConfig = {
       AllowIsolate = lib.mkOverride 500 true;
diff --git a/testdata/TestNoRestart.compose.yml b/testdata/TestNoRestart.compose.yml
new file mode 100644
index 0000000..2804f52
--- /dev/null
+++ b/testdata/TestNoRestart.compose.yml
@@ -0,0 +1,4 @@
+services:
+  test:
+    image: nginx:latest
+
diff --git a/testdata/TestNoRestart.docker.nix b/testdata/TestNoRestart.docker.nix
new file mode 100644
index 0000000..dfe6fc3
--- /dev/null
+++ b/testdata/TestNoRestart.docker.nix
@@ -0,0 +1,62 @@
+{ pkgs, lib, ... }:
+
+{
+  # Runtime
+  virtualisation.docker = {
+    enable = true;
+    autoPrune.enable = true;
+  };
+  virtualisation.oci-containers.backend = "docker";
+
+  # Containers
+  virtualisation.oci-containers.containers."test-test" = {
+    image = "nginx:latest";
+    log-driver = "journald";
+    autoStart = false;
+    extraOptions = [
+      "--network-alias=test"
+      "--network=test_default"
+    ];
+  };
+  systemd.services."docker-test-test" = {
+    serviceConfig = {
+      Restart = lib.mkOverride 500 "no";
+    };
+    after = [
+      "docker-network-test_default.service"
+    ];
+    requires = [
+      "docker-network-test_default.service"
+    ];
+    partOf = [
+      "docker-compose-test-root.target"
+    ];
+    wantedBy = [
+      "docker-compose-test-root.target"
+    ];
+  };
+
+  # Networks
+  systemd.services."docker-network-test_default" = {
+    path = [ pkgs.docker ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStop = "${pkgs.docker}/bin/docker network rm -f test_default";
+    };
+    script = ''
+      docker network inspect test_default || docker network create test_default
+    '';
+    partOf = [ "docker-compose-test-root.target" ];
+    wantedBy = [ "docker-compose-test-root.target" ];
+  };
+
+  # Root service
+  # When started, this will automatically create all resources and start
+  # the containers. When stopped, this will teardown all resources.
+  systemd.targets."docker-compose-test-root" = {
+    unitConfig = {
+      Description = "Root target generated by compose2nix.";
+    };
+  };
+}
diff --git a/testdata/TestNoRestart.podman.nix b/testdata/TestNoRestart.podman.nix
new file mode 100644
index 0000000..1c9bb73
--- /dev/null
+++ b/testdata/TestNoRestart.podman.nix
@@ -0,0 +1,67 @@
+{ pkgs, lib, ... }:
+
+{
+  # Runtime
+  virtualisation.podman = {
+    enable = true;
+    autoPrune.enable = true;
+    dockerCompat = true;
+    defaultNetwork.settings = {
+      # Required for container networking to be able to use names.
+      dns_enabled = true;
+    };
+  };
+  virtualisation.oci-containers.backend = "podman";
+
+  # Containers
+  virtualisation.oci-containers.containers."test-test" = {
+    image = "nginx:latest";
+    log-driver = "journald";
+    autoStart = false;
+    extraOptions = [
+      "--network-alias=test"
+      "--network=test_default"
+    ];
+  };
+  systemd.services."podman-test-test" = {
+    serviceConfig = {
+      Restart = lib.mkOverride 500 "no";
+    };
+    after = [
+      "podman-network-test_default.service"
+    ];
+    requires = [
+      "podman-network-test_default.service"
+    ];
+    partOf = [
+      "podman-compose-test-root.target"
+    ];
+    wantedBy = [
+      "podman-compose-test-root.target"
+    ];
+  };
+
+  # Networks
+  systemd.services."podman-network-test_default" = {
+    path = [ pkgs.podman ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStop = "${pkgs.podman}/bin/podman network rm -f test_default";
+    };
+    script = ''
+      podman network inspect test_default || podman network create test_default
+    '';
+    partOf = [ "podman-compose-test-root.target" ];
+    wantedBy = [ "podman-compose-test-root.target" ];
+  };
+
+  # Root service
+  # When started, this will automatically create all resources and start
+  # the containers. When stopped, this will teardown all resources.
+  systemd.targets."podman-compose-test-root" = {
+    unitConfig = {
+      Description = "Root target generated by compose2nix.";
+    };
+  };
+}
diff --git a/testdata/TestNoWriteNixSetup.docker.nix b/testdata/TestNoWriteNixSetup.docker.nix
index 6392257..25941c1 100644
--- a/testdata/TestNoWriteNixSetup.docker.nix
+++ b/testdata/TestNoWriteNixSetup.docker.nix
@@ -292,9 +292,6 @@
   systemd.services."docker-traefik" = {
     serviceConfig = {
       Restart = lib.mkOverride 500 "no";
-      RestartMaxDelaySec = lib.mkOverride 500 "1m";
-      RestartSec = lib.mkOverride 500 "100ms";
-      RestartSteps = lib.mkOverride 500 9;
     };
     unitConfig = {
       AllowIsolate = lib.mkOverride 500 true;
diff --git a/testdata/TestOverrideSystemdStopTimeout.docker.nix b/testdata/TestOverrideSystemdStopTimeout.docker.nix
index f18f656..d978912 100644
--- a/testdata/TestOverrideSystemdStopTimeout.docker.nix
+++ b/testdata/TestOverrideSystemdStopTimeout.docker.nix
@@ -302,9 +302,6 @@
   systemd.services."docker-traefik" = {
     serviceConfig = {
       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;
     };
     unitConfig = {
diff --git a/testdata/TestProject.docker.nix b/testdata/TestProject.docker.nix
index d8c4de6..680954f 100644
--- a/testdata/TestProject.docker.nix
+++ b/testdata/TestProject.docker.nix
@@ -298,9 +298,6 @@
   systemd.services."docker-traefik" = {
     serviceConfig = {
       Restart = lib.mkOverride 500 "no";
-      RestartMaxDelaySec = lib.mkOverride 500 "1m";
-      RestartSec = lib.mkOverride 500 "100ms";
-      RestartSteps = lib.mkOverride 500 9;
     };
     unitConfig = {
       AllowIsolate = lib.mkOverride 500 true;
diff --git a/testdata/TestRemoveVolumes.docker.nix b/testdata/TestRemoveVolumes.docker.nix
index 6162746..f4f6667 100644
--- a/testdata/TestRemoveVolumes.docker.nix
+++ b/testdata/TestRemoveVolumes.docker.nix
@@ -298,9 +298,6 @@
   systemd.services."docker-traefik" = {
     serviceConfig = {
       Restart = lib.mkOverride 500 "no";
-      RestartMaxDelaySec = lib.mkOverride 500 "1m";
-      RestartSec = lib.mkOverride 500 "100ms";
-      RestartSteps = lib.mkOverride 500 9;
     };
     unitConfig = {
       AllowIsolate = lib.mkOverride 500 true;
diff --git a/testdata/TestSystemdMount.docker.nix b/testdata/TestSystemdMount.docker.nix
index 86d0100..cbb181c 100644
--- a/testdata/TestSystemdMount.docker.nix
+++ b/testdata/TestSystemdMount.docker.nix
@@ -312,9 +312,6 @@
   systemd.services."docker-traefik" = {
     serviceConfig = {
       Restart = lib.mkOverride 500 "no";
-      RestartMaxDelaySec = lib.mkOverride 500 "1m";
-      RestartSec = lib.mkOverride 500 "100ms";
-      RestartSteps = lib.mkOverride 500 9;
     };
     unitConfig = {
       AllowIsolate = lib.mkOverride 500 true;
diff --git a/testdata/TestUpheldBy.docker.nix b/testdata/TestUpheldBy.docker.nix
index 43817c9..1d7c69a 100644
--- a/testdata/TestUpheldBy.docker.nix
+++ b/testdata/TestUpheldBy.docker.nix
@@ -304,9 +304,6 @@
   systemd.services."docker-traefik" = {
     serviceConfig = {
       Restart = lib.mkOverride 500 "no";
-      RestartMaxDelaySec = lib.mkOverride 500 "1m";
-      RestartSec = lib.mkOverride 500 "100ms";
-      RestartSteps = lib.mkOverride 500 9;
     };
     unitConfig = {
       AllowIsolate = lib.mkOverride 500 true;
diff --git a/testdata/compose.yml b/testdata/compose.yml
index ba27ad0..8bc12b7 100644
--- a/testdata/compose.yml
+++ b/testdata/compose.yml
@@ -203,7 +203,6 @@ services:
         max-size: 10m
         max-file: "3"
         compress: "true"
-    restart: unless-stopped
 
 networks:
   something: