Skip to content

Commit

Permalink
fix: enable escaping special Nix chars in template
Browse files Browse the repository at this point in the history
Fix an issue where special characters were not properly escaped in template,
leading to Nix parsing errors.

Tests have been checked and pass.

Test data has been written from the real world problem I faced using
Dovecot's labels containing shell scripts.
  • Loading branch information
Luka authored and aksiksi committed Aug 23, 2024
1 parent 3f3f499 commit 12e3f65
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 12 deletions.
8 changes: 8 additions & 0 deletions nix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,11 @@ func TestEmptyEnv(t *testing.T) {
}
runSubtestsWithGenerator(t, g)
}

func TestEscapeChars(t *testing.T) {
composePath, _ := getPaths(t, false)
g := &Generator{
Inputs: []string{composePath},
}
runSubtestsWithGenerator(t, g)
}
9 changes: 6 additions & 3 deletions nixos-test/docker-compose.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Auto-generated using compose2nix v0.2.1-pre.
# Auto-generated using compose2nix v0.2.2-pre.
{ pkgs, lib, ... }:

{
Expand Down Expand Up @@ -91,6 +91,9 @@
"myproject_books:/books:rw"
"storage:/storage:rw"
];
labels = {
"some-other-label" = "\"test\"";
};
dependsOn = [
"myproject-service-a"
];
Expand Down Expand Up @@ -158,7 +161,7 @@
ExecStop = "docker network rm -f myproject_something";
};
script = ''
docker network inspect myproject_something || docker network create myproject_something --subnet=192.168.8.0/24 --gateway=192.168.8.1 --label=test-label=okay
docker network inspect myproject_something || docker network create myproject_something --subnet=192.168.8.0/24 --gateway=192.168.8.1 --label=quoted="words" --label=test-label=okay
'';
partOf = [ "docker-compose-myproject-root.target" ];
wantedBy = [ "docker-compose-myproject-root.target" ];
Expand Down Expand Up @@ -190,7 +193,7 @@
"/mnt/media"
];
script = ''
docker volume inspect storage || docker volume create storage --opt=device=/mnt/media --opt=o=bind --opt=type=none
docker volume inspect storage || docker volume create storage --opt=device=/mnt/media --opt=o=bind --opt=type=none --label=quoted="words"
'';
partOf = [ "docker-compose-myproject-root.target" ];
wantedBy = [ "docker-compose-myproject-root.target" ];
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 @@ -29,6 +29,7 @@ services:
labels:
- "compose2nix.systemd.unit.AllowIsolate=no"
- "compose2nix.systemd.service.RuntimeMaxSec=360"
- "some-other-label=\"test\""
depends_on:
- service-a
healthcheck:
Expand All @@ -48,6 +49,7 @@ networks:
gateway: 192.168.8.1
labels:
- "test-label=okay"
- "quoted=\"words\""

volumes:
storage:
Expand All @@ -56,6 +58,8 @@ volumes:
type: none
device: /mnt/media
o: bind
labels:
- "quoted=\"words\""
books:
driver_opts:
type: none
Expand Down
9 changes: 6 additions & 3 deletions nixos-test/podman-compose.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Auto-generated using compose2nix v0.2.1-pre.
# Auto-generated using compose2nix v0.2.2-pre.
{ pkgs, lib, ... }:

{
Expand Down Expand Up @@ -93,6 +93,9 @@
"myproject_books:/books:rw"
"storage:/storage:rw"
];
labels = {
"some-other-label" = "\"test\"";
};
dependsOn = [
"myproject-service-a"
];
Expand Down Expand Up @@ -160,7 +163,7 @@
ExecStop = "podman network rm -f myproject_something";
};
script = ''
podman network inspect myproject_something || podman network create myproject_something --subnet=192.168.8.0/24 --gateway=192.168.8.1 --label=test-label=okay
podman network inspect myproject_something || podman network create myproject_something --subnet=192.168.8.0/24 --gateway=192.168.8.1 --label=quoted="words" --label=test-label=okay
'';
partOf = [ "podman-compose-myproject-root.target" ];
wantedBy = [ "podman-compose-myproject-root.target" ];
Expand Down Expand Up @@ -192,7 +195,7 @@
"/mnt/media"
];
script = ''
podman volume inspect storage || podman volume create storage --opt=device=/mnt/media --opt=o=bind --opt=type=none
podman volume inspect storage || podman volume create storage --opt=device=/mnt/media --opt=o=bind --opt=type=none --label=quoted="words"
'';
partOf = [ "podman-compose-myproject-root.target" ];
wantedBy = [ "podman-compose-myproject-root.target" ];
Expand Down
14 changes: 11 additions & 3 deletions template.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,16 @@ func toNixList(s []string) string {
return fmt.Sprintf("[ %s ]", b.String())
}

func escapeChars(s string) string {
s = strings.ReplaceAll(s, "\"", "\\\"")
s = strings.ReplaceAll(s, "$", "\\$")

return s
}

var funcMap template.FuncMap = template.FuncMap{
"derefInt": derefInt,
"toNixValue": toNixValue,
"toNixList": toNixList,
"derefInt": derefInt,
"toNixValue": toNixValue,
"toNixList": toNixList,
"escapeChars": escapeChars,
}
6 changes: 3 additions & 3 deletions templates/container.nix.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ virtualisation.oci-containers.containers."{{.Name}}" = {
{{- if .Environment}}
environment = {
{{- range $k, $v := .Environment}}
"{{$k}}" = "{{$v}}";
"{{$k}}" = "{{escapeChars $v}}";
{{- end}}
};
{{- end}}
Expand All @@ -20,7 +20,7 @@ virtualisation.oci-containers.containers."{{.Name}}" = {
{{- if .Volumes}}
volumes = [
{{- range $k, $v := .Volumes}}
"{{$v}}"
"{{escapeChars $v}}"
{{- end}}
];
{{- end}}
Expand All @@ -40,7 +40,7 @@ virtualisation.oci-containers.containers."{{.Name}}" = {
{{- if .Labels}}
labels = {
{{- range $k, $v := .Labels}}
"{{$k}}" = "{{$v}}";
"{{escapeChars $k}}" = "{{escapeChars $v}}";
{{- end}}
};
{{- end}}
Expand Down
26 changes: 26 additions & 0 deletions testdata/TestEscapeChars.compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: "dovecot"
services:
dovecot:
container_name: dovecot
image: dovecot
labels:
ofelia.enabled: "true"
ofelia.job-exec.dovecot_imapsync_runner.schedule: "@every 1m"
ofelia.job-exec.dovecot_imapsync_runner.no-overlap: "true"
ofelia.job-exec.dovecot_imapsync_runner.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\""
ofelia.job-exec.dovecot_trim_logs.schedule: "@every 1m"
ofelia.job-exec.dovecot_trim_logs.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\""
networks:
- abc
volumes:
- def:/path/to/path

networks:
abc:
labels:
my-label: "\"some quoted string\""

volumes:
def:
labels:
other-label: "\"another quota string\""
89 changes: 89 additions & 0 deletions testdata/TestEscapeChars.docker.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{ pkgs, lib, ... }:

{
# Runtime
virtualisation.docker = {
enable = true;
autoPrune.enable = true;
};
virtualisation.oci-containers.backend = "docker";

# Containers
virtualisation.oci-containers.containers."dovecot" = {
image = "dovecot";
volumes = [
"dovecot_def:/path/to/path:rw"
];
labels = {
"ofelia.enabled" = "true";
"ofelia.job-exec.dovecot_imapsync_runner.command" = "/bin/bash -c \"[[ \${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\"";
"ofelia.job-exec.dovecot_imapsync_runner.no-overlap" = "true";
"ofelia.job-exec.dovecot_imapsync_runner.schedule" = "@every 1m";
"ofelia.job-exec.dovecot_trim_logs.command" = "/bin/bash -c \"[[ \${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\"";
"ofelia.job-exec.dovecot_trim_logs.schedule" = "@every 1m";
};
log-driver = "journald";
autoStart = false;
extraOptions = [
"--network-alias=dovecot"
"--network=dovecot_abc"
];
};
systemd.services."docker-dovecot" = {
serviceConfig = {
Restart = lib.mkOverride 500 "no";
};
after = [
"docker-network-dovecot_abc.service"
"docker-volume-dovecot_def.service"
];
requires = [
"docker-network-dovecot_abc.service"
"docker-volume-dovecot_def.service"
];
partOf = [
"docker-compose-dovecot-root.target"
];
wantedBy = [
"docker-compose-dovecot-root.target"
];
};

# Networks
systemd.services."docker-network-dovecot_abc" = {
path = [ pkgs.docker ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "docker network rm -f dovecot_abc";
};
script = ''
docker network inspect dovecot_abc || docker network create dovecot_abc --label=my-label="some quoted string"
'';
partOf = [ "docker-compose-dovecot-root.target" ];
wantedBy = [ "docker-compose-dovecot-root.target" ];
};

# Volumes
systemd.services."docker-volume-dovecot_def" = {
path = [ pkgs.docker ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
docker volume inspect dovecot_def || docker volume create dovecot_def --label=other-label="another quota string"
'';
partOf = [ "docker-compose-dovecot-root.target" ];
wantedBy = [ "docker-compose-dovecot-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-dovecot-root" = {
unitConfig = {
Description = "Root target generated by compose2nix.";
};
};
}
94 changes: 94 additions & 0 deletions testdata/TestEscapeChars.podman.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{ 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."dovecot" = {
image = "dovecot";
volumes = [
"dovecot_def:/path/to/path:rw"
];
labels = {
"ofelia.enabled" = "true";
"ofelia.job-exec.dovecot_imapsync_runner.command" = "/bin/bash -c \"[[ \${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\"";
"ofelia.job-exec.dovecot_imapsync_runner.no-overlap" = "true";
"ofelia.job-exec.dovecot_imapsync_runner.schedule" = "@every 1m";
"ofelia.job-exec.dovecot_trim_logs.command" = "/bin/bash -c \"[[ \${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\"";
"ofelia.job-exec.dovecot_trim_logs.schedule" = "@every 1m";
};
log-driver = "journald";
autoStart = false;
extraOptions = [
"--network-alias=dovecot"
"--network=dovecot_abc"
];
};
systemd.services."podman-dovecot" = {
serviceConfig = {
Restart = lib.mkOverride 500 "no";
};
after = [
"podman-network-dovecot_abc.service"
"podman-volume-dovecot_def.service"
];
requires = [
"podman-network-dovecot_abc.service"
"podman-volume-dovecot_def.service"
];
partOf = [
"podman-compose-dovecot-root.target"
];
wantedBy = [
"podman-compose-dovecot-root.target"
];
};

# Networks
systemd.services."podman-network-dovecot_abc" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "podman network rm -f dovecot_abc";
};
script = ''
podman network inspect dovecot_abc || podman network create dovecot_abc --label=my-label="some quoted string"
'';
partOf = [ "podman-compose-dovecot-root.target" ];
wantedBy = [ "podman-compose-dovecot-root.target" ];
};

# Volumes
systemd.services."podman-volume-dovecot_def" = {
path = [ pkgs.podman ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
podman volume inspect dovecot_def || podman volume create dovecot_def --label=other-label="another quota string"
'';
partOf = [ "podman-compose-dovecot-root.target" ];
wantedBy = [ "podman-compose-dovecot-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-dovecot-root" = {
unitConfig = {
Description = "Root target generated by compose2nix.";
};
};
}

0 comments on commit 12e3f65

Please sign in to comment.