Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: properly escape Nix strings everywhere #35

Merged
merged 1 commit into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading