Skip to content

Commit

Permalink
fix: properly escape Nix strings everywhere (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
aksiksi authored Sep 28, 2024
1 parent 7c53c2f commit 75eac4e
Show file tree
Hide file tree
Showing 28 changed files with 258 additions and 40 deletions.
10 changes: 0 additions & 10 deletions compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,16 +576,6 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig, networkMap ma
if err != nil {
return nil, fmt.Errorf("failed to convert healthcheck command: %w", err)
}

// We need to escape double-quotes for Nix.
//
// We also need to escape the special "${" sequence as it is possible that this is
// passed in to evaluate a Bash env variable as part of the command.
//
// See: https://nixos.org/manual/nix/stable/language/values
cmd = strings.ReplaceAll(cmd, `"`, `\"`)
cmd = strings.ReplaceAll(cmd, "${", `\${`)

c.ExtraOptions = append(c.ExtraOptions, fmt.Sprintf("--health-cmd=%s", cmd))
}
if timeout := healthCheck.Timeout; timeout != nil {
Expand Down
8 changes: 8 additions & 0 deletions nix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,14 @@ func TestDeployDevices(t *testing.T) {
runSubtestsWithGenerator(t, g)
}

func TestEscapeChars(t *testing.T) {
composePath, _ := getPaths(t, false)
g := &Generator{
Inputs: []string{composePath},
}
runSubtestsWithGenerator(t, g)
}

func TestNoCreateRootTarget(t *testing.T) {
composePath, _ := getPaths(t, false)
g := &Generator{
Expand Down
5 changes: 3 additions & 2 deletions nixos-test/docker-compose.nix
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"compose2nix.systemd.service.Restart" = "no";
"compose2nix.systemd.service.RuntimeMaxSec" = "360";
"compose2nix.systemd.unit.Description" = "This is the service-a container!";
"escape-me" = "\"hello\"";
};
log-driver = "journald";
extraOptions = [
Expand Down Expand Up @@ -170,7 +171,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=escape-me='''hello''' --label=test-label=okay
'';
partOf = [ "docker-compose-myproject-root.target" ];
wantedBy = [ "docker-compose-myproject-root.target" ];
Expand Down Expand Up @@ -202,7 +203,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=escape-me='''hello'''
'';
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 @@ -19,6 +19,7 @@ services:
- 'compose2nix.systemd.service.Restart=no'
- "compose2nix.systemd.service.RuntimeMaxSec=360"
- "compose2nix.systemd.unit.Description=This is the service-a container!"
- "escape-me=\"hello\""
restart: unless-stopped
service-b:
image: docker.io/library/nginx:stable-alpine-slim
Expand Down Expand Up @@ -51,6 +52,7 @@ networks:
gateway: 192.168.8.1
labels:
- "test-label=okay"
- "escape-me=''hello''"

volumes:
storage:
Expand All @@ -59,6 +61,8 @@ volumes:
type: none
device: /mnt/media
o: bind
labels:
- "escape-me=''hello''"
books:
driver_opts:
type: none
Expand Down
5 changes: 3 additions & 2 deletions nixos-test/podman-compose.nix
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"compose2nix.systemd.service.Restart" = "no";
"compose2nix.systemd.service.RuntimeMaxSec" = "360";
"compose2nix.systemd.unit.Description" = "This is the service-a container!";
"escape-me" = "\"hello\"";
};
log-driver = "journald";
extraOptions = [
Expand Down Expand Up @@ -172,7 +173,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=escape-me='''hello''' --label=test-label=okay
'';
partOf = [ "podman-compose-myproject-root.target" ];
wantedBy = [ "podman-compose-myproject-root.target" ];
Expand Down Expand Up @@ -204,7 +205,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=escape-me='''hello'''
'';
partOf = [ "podman-compose-myproject-root.target" ];
wantedBy = [ "podman-compose-myproject-root.target" ];
Expand Down
27 changes: 22 additions & 5 deletions template.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func derefInt(v *int) int {
func toNixValue(v any) any {
switch v := v.(type) {
case string:
return fmt.Sprintf("%q", v)
return fmt.Sprintf("%q", escapeNixString(v))
default:
return v
}
Expand All @@ -37,16 +37,33 @@ func toNixValue(v any) any {
func toNixList(s []string) string {
b := strings.Builder{}
for i, e := range s {
b.WriteString(fmt.Sprintf("%q", e))
b.WriteString(fmt.Sprintf("%q", escapeNixString(e)))
if i < len(s)-1 {
b.WriteString(" ")
}
}
return fmt.Sprintf("[ %s ]", b.String())
}

func escapeNixString(s string) string {
// https://nix.dev/manual/nix/latest/language/syntax#string-literal
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `"`, `\"`)
s = strings.ReplaceAll(s, `${`, `\${`)
return s
}

func escapeIndentedNixString(s string) string {
// https://nix.dev/manual/nix/latest/language/syntax#string-literal
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,
"escapeNixString": escapeNixString,
"escapeIndentedNixString": escapeIndentedNixString,
}
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}}" = "{{escapeNixString $v}}";
{{- end}}
};
{{- end}}
Expand Down Expand Up @@ -40,7 +40,7 @@ virtualisation.oci-containers.containers."{{.Name}}" = {
{{- if .Labels}}
labels = {
{{- range $k, $v := .Labels}}
"{{$k}}" = "{{$v}}";
"{{$k}}" = "{{escapeNixString $v}}";
{{- end}}
};
{{- end}}
Expand Down Expand Up @@ -68,7 +68,7 @@ virtualisation.oci-containers.containers."{{.Name}}" = {
{{- if .ExtraOptions}}
extraOptions = [
{{- range .ExtraOptions}}
"{{.}}"
"{{escapeNixString .}}"
{{- end}}
];
{{- end}}
Expand Down
2 changes: 1 addition & 1 deletion templates/network.nix.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ systemd.services."{{.Runtime}}-network-{{.Name}}" = {
ExecStop = "{{.Runtime}} network rm -f {{.Name}}";
};
script = ''
{{ .Command }}
{{escapeIndentedNixString .Command }}
'';
{{- if rootTarget}}
{{- /* PartOf for stop/restart of root, WantedBy for start of root. */}}
Expand Down
2 changes: 1 addition & 1 deletion templates/volume.nix.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ systemd.services."{{.Runtime}}-volume-{{.Name}}" = {
];
{{- end}}
script = ''
{{ .Command }}
{{escapeIndentedNixString .Command }}
'';
{{- if rootTarget}}
{{- /* PartOf for stop/restart of root, WantedBy for start of root. */}}
Expand Down
2 changes: 1 addition & 1 deletion testdata/TestBasic.docker.nix
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
"PGID" = "1000";
"PUID" = "1000";
"TP_DOMAIN" = "hey.hello.us\/themepark";
"TP_DOMAIN" = "hey.hello.us\\/themepark";
"TP_HOTIO" = "false";
"TP_THEME" = "potato";
"TZ" = "America/New_York";
Expand Down
2 changes: 1 addition & 1 deletion testdata/TestBasic.podman.nix
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
"PGID" = "1000";
"PUID" = "1000";
"TP_DOMAIN" = "hey.hello.us\/themepark";
"TP_DOMAIN" = "hey.hello.us\\/themepark";
"TP_HOTIO" = "false";
"TP_THEME" = "potato";
"TZ" = "America/New_York";
Expand Down
2 changes: 1 addition & 1 deletion testdata/TestBasicAutoFormat.docker.nix
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
"PGID" = "1000";
"PUID" = "1000";
"TP_DOMAIN" = "hey.hello.us\/themepark";
"TP_DOMAIN" = "hey.hello.us\\/themepark";
"TP_HOTIO" = "false";
"TP_THEME" = "potato";
"TZ" = "America/New_York";
Expand Down
2 changes: 1 addition & 1 deletion testdata/TestBasicAutoFormat.podman.nix
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
"PGID" = "1000";
"PUID" = "1000";
"TP_DOMAIN" = "hey.hello.us\/themepark";
"TP_DOMAIN" = "hey.hello.us\\/themepark";
"TP_HOTIO" = "false";
"TP_THEME" = "potato";
"TZ" = "America/New_York";
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\""
83 changes: 83 additions & 0 deletions testdata/TestEscapeChars.docker.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{ 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 90 "no";
};
after = [
"docker-network-dovecot_abc.service"
"docker-volume-dovecot_def.service"
];
requires = [
"docker-network-dovecot_abc.service"
"docker-volume-dovecot_def.service"
];
};

# 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.";
};
};
}
Loading

0 comments on commit 75eac4e

Please sign in to comment.