diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..266b61d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Test + +on: + push: + paths-ignore: + - '**/*.md' + branches: + - main + pull_request: + paths-ignore: + - '**/*.md' + +jobs: + linux-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: '>=1.21' + - run: go version + - name: Test + run: go test -v -covermode=count -coverprofile=coverage.out + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + fail_ci_if_error: true + files: coverage.out diff --git a/compose.go b/compose.go index ff84144..6562107 100644 --- a/compose.go +++ b/compose.go @@ -92,10 +92,21 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig) NixContainer dependsOn[i] = g.Project.With(dependsOn[i]) } } + + var name string + if service.ContainerName != "" { + name = service.ContainerName + } else { + // TODO(aksiksi): We should try to use the same convention as Docker Compose + // when container_name is not set. + // See: https://github.com/docker/compose/issues/6316 + name = service.Name + } + c := NixContainer{ Project: g.Project, Runtime: g.Runtime, - Name: service.Name, + Name: name, Image: service.Image, Labels: service.Labels, Ports: portConfigsToPortStrings(service.Ports), @@ -120,6 +131,13 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig) NixContainer c.ExtraOptions = append(c.ExtraOptions, fmt.Sprintf("--network-alias=%s", service.Name)) } + // TODO(aksiksi): Handle the service's "network_mode" + // We can only parse the network mode at this point if it points to host or container. + // If it points to a service, we'll need to do a scan when we've finished parsing all + // containers. + // Compose: https://docs.docker.com/compose/compose-file/compose-file-v3/#network_mode + // Podman: https://docs.podman.io/en/latest/markdown/podman-run.1.html#network-mode-net + for _, v := range service.Volumes { c.Volumes[v.Source] = v.String() } diff --git a/go.mod b/go.mod index 8758244..043f7d7 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,11 @@ require ( golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 ) +// Test +require ( + github.com/google/go-cmp v0.5.9 +) + require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect @@ -17,6 +22,7 @@ require ( github.com/google/uuid v1.1.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -30,5 +36,6 @@ require ( golang.org/x/crypto v0.3.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.2.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 183f633..3d633d2 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,11 @@ github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4 github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= @@ -59,8 +64,9 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/nixose.go b/nixose.go index c5e5426..1ae3653 100644 --- a/nixose.go +++ b/nixose.go @@ -14,6 +14,8 @@ var templateFS embed.FS var nixTemplates = template.New("nix").Funcs(sprig.FuncMap()).Funcs(funcMap) func labelMapToLabelFlags(l map[string]string) []string { + // https://docs.docker.com/engine/reference/commandline/run/#label + // https://docs.podman.io/en/latest/markdown/podman-run.1.html#label-l-key-value labels := mapToKeyValArray(l) for i, label := range labels { labels[i] = fmt.Sprintf("--label=%s", label) @@ -21,11 +23,11 @@ func labelMapToLabelFlags(l map[string]string) []string { return labels } -func execTemplate(t *template.Template) func(string, interface{}) (string, error) { - return func(name string, v interface{}) (string, error) { - var buf strings.Builder - err := t.ExecuteTemplate(&buf, name, v) - return buf.String(), err +func execTemplate(t *template.Template) func(string, any) (string, error) { + return func(name string, v any) (string, error) { + var s strings.Builder + err := t.ExecuteTemplate(&s, name, v) + return s.String(), err } } diff --git a/nixose_test.go b/nixose_test.go new file mode 100644 index 0000000..4d7eb60 --- /dev/null +++ b/nixose_test.go @@ -0,0 +1,63 @@ +package nixose + +import ( + "context" + "fmt" + "os" + "path" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func getPaths(t *testing.T) (string, string, string) { + outFileName := fmt.Sprintf("%s_out.nix", t.Name()) + composePath := path.Join("testdata", "docker-compose.yml") + envFilePath := path.Join("testdata", "input.env") + outFilePath := path.Join("testdata", outFileName) + return composePath, envFilePath, outFilePath +} + +func TestDocker(t *testing.T) { + ctx := context.Background() + composePath, envFilePath, outFilePath := getPaths(t) + g := Generator{ + Runtime: ContainerRuntimeDocker, + Paths: []string{composePath}, + EnvFiles: []string{envFilePath}, + } + c, err := g.Run(ctx) + if err != nil { + t.Fatal(err) + } + wantOutput, err := os.ReadFile(outFilePath) + if err != nil { + t.Fatal(err) + } + got, want := c.String(), string(wantOutput) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("output diff: %s\n", diff) + } +} + +func TestPodman(t *testing.T) { + ctx := context.Background() + composePath, envFilePath, outFilePath := getPaths(t) + g := Generator{ + Runtime: ContainerRuntimePodman, + Paths: []string{composePath}, + EnvFiles: []string{envFilePath}, + } + c, err := g.Run(ctx) + if err != nil { + t.Fatal(err) + } + wantOutput, err := os.ReadFile(outFilePath) + if err != nil { + t.Fatal(err) + } + got, want := c.String(), string(wantOutput) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("output diff: %s\n", diff) + } +} diff --git a/templates/container.tmpl b/templates/container.tmpl index d3be00a..6168b65 100644 --- a/templates/container.tmpl +++ b/templates/container.tmpl @@ -1,6 +1,6 @@ -{{- $name := .Project.With .Name}} -{{- $runtime := .Runtime | printf "%s"}} -{{- $labels := labelMapToLabelFlags .Labels}} +{{- $name := .Project.With .Name -}} +{{- $runtime := .Runtime | printf "%s" -}} +{{- $labels := labelMapToLabelFlags .Labels -}} virtualisation.oci-containers.containers."{{$name}}" = { image = "{{.Image}}"; diff --git a/templates/main.tmpl b/templates/main.tmpl index 26645af..315ab0b 100644 --- a/templates/main.tmpl +++ b/templates/main.tmpl @@ -1,4 +1,4 @@ -{{- $runtime := .Runtime | printf "%s"}} +{{- $runtime := .Runtime | printf "%s" -}} { pkgs, ... }: { @@ -16,20 +16,35 @@ }; virtualisation.oci-containers.backend = "{{$runtime}}"; + {{- if .Containers}} + # Containers - {{- range .Containers}}{{execTemplate "container.tmpl" . | indent 2}}{{end}} + {{- range .Containers}} +{{execTemplate "container.tmpl" . | indent 2}} + {{- end}} + {{- end}} + + {{- if .Networks}} # Networks - {{- range .Networks}}{{execTemplate "network.tmpl" . | indent 2}}{{end}} + {{- range .Networks}} +{{execTemplate "network.tmpl" . | indent 2}} + {{- end}} + {{- end}} + + {{- if .Volumes}} # Volumes - {{- range .Volumes}}{{execTemplate "volume.tmpl" . | indent 2}}{{end}} + {{- range .Volumes}} +{{execTemplate "volume.tmpl" . | indent 2}} + {{- end}} + {{- end}} # Scripts - up = writeShellScript "compose-{{.Project.Name}}-up.sh" '' + up = writeShellScript "compose-{{.Project.With "up"}}.sh" '' echo "TODO: Create resources." ''; - down = writeShellScript "compose-{{.Project.Name}}-down.sh" '' + down = writeShellScript "compose-{{.Project.With "down"}}.sh" '' echo "TODO: Remove resources." ''; } diff --git a/templates/network.tmpl b/templates/network.tmpl index ac58882..02be119 100644 --- a/templates/network.tmpl +++ b/templates/network.tmpl @@ -1,6 +1,6 @@ -{{- $name := .Project.With .Name}} -{{- $runtime := .Runtime | printf "%s"}} -{{- $labels := labelMapToLabelFlags .Labels}} +{{- $name := .Project.With .Name -}} +{{- $runtime := .Runtime | printf "%s" -}} +{{- $labels := labelMapToLabelFlags .Labels -}} systemd.services."create-{{$runtime}}-network-{{$name}}" = { serviceConfig.Type = "oneshot"; path = [ pkgs.{{$runtime}} ]; diff --git a/templates/volume.tmpl b/templates/volume.tmpl index 4d35e11..ecafbe5 100644 --- a/templates/volume.tmpl +++ b/templates/volume.tmpl @@ -1,7 +1,7 @@ -{{/* NOTE(aksiksi): Volume name is _not_ project scoped to match Compose semantics. */}} -{{- $name := .Name}} -{{- $runtime := .Runtime | printf "%s"}} -{{- $driverOptsString := mapToKeyValArray .DriverOpts | join ","}} +{{- /* NOTE(aksiksi): Volume name is _not_ project scoped to match Compose semantics. */ -}} +{{- $name := .Name -}} +{{- $runtime := .Runtime | printf "%s" -}} +{{- $driverOptsString := mapToKeyValArray .DriverOpts | join "," -}} systemd.services."create-{{$runtime}}-volume-{{$name}}" = { serviceConfig.Type = "oneshot"; path = [ pkgs.{{$runtime}} ]; diff --git a/testdata/TestDocker_out.nix b/testdata/TestDocker_out.nix new file mode 100644 index 0000000..738624b --- /dev/null +++ b/testdata/TestDocker_out.nix @@ -0,0 +1,200 @@ +{ pkgs, ... }: + +{ + # Runtime + virtualisation.docker = { + enable = true; + autoPrune.enable = true; + }; + virtualisation.oci-containers.backend = "docker"; + + # Containers + virtualisation.oci-containers.containers."jellyseerr" = { + image = "docker.io/fallenbagel/jellyseerr:latest"; + environment = { + PGID = "1000"; + PUID = "1000"; + TZ = "America/New_York"; + }; + volumes = [ + "/var/volumes/jellyseerr:/app/config:rw" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.jellyseerr.middlewares" = "chain-authelia@file"; + "traefik.http.routers.jellyseerr.rule" = "Host(`requests.hello.us`)"; + "traefik.http.routers.jellyseerr.tls.certresolver" = "htpc"; + }; + extraOptions = [ + "--network=default" + "--network-alias=jellyseerr" + ]; + autoStart = false; + }; + virtualisation.oci-containers.containers."photoprism-mariadb" = { + image = "docker.io/library/mariadb:10.9"; + environment = { + MARIADB_AUTO_UPGRADE = "1"; + MARIADB_DATABASE = "photoprism"; + MARIADB_INITDB_SKIP_TZINFO = "1"; + MARIADB_PASSWORD = "insecure"; + MARIADB_ROOT_PASSWORD = "insecure"; + MARIADB_USER = "photoprism"; + }; + volumes = [ + "/var/volumes/photoprism-mariadb:/var/lib/mysql:rw" + ]; + extraOptions = [ + "--network=default" + "--network-alias=photoprism-mariadb" + ]; + user = "1000:1000"; + autoStart = false; + }; + virtualisation.oci-containers.containers."sabnzbd" = { + image = "lscr.io/linuxserver/sabnzbd"; + environment = { + DOCKER_MODS = "ghcr.io/gilbn/theme.park:sabnzbd"; + PGID = "1000"; + PUID = "1000"; + TP_DOMAIN = "hey.hello.us\/themepark"; + TP_HOTIO = "false"; + TP_THEME = "potato"; + TZ = "America/New_York"; + }; + volumes = [ + "/var/volumes/sabnzbd:/config:rw" + "storage:/storage:rw" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.sabnzbd.middlewares" = "chain-authelia@file"; + "traefik.http.routers.sabnzbd.rule" = "Host(`hey.hello.us`) && PathPrefix(`/sabnzbd`)"; + "traefik.http.routers.sabnzbd.tls.certresolver" = "htpc"; + }; + extraOptions = [ + "--network=default" + "--network-alias=sabnzbd" + ]; + autoStart = false; + }; + virtualisation.oci-containers.containers."traefik" = { + image = "docker.io/library/traefik"; + environment = { + CLOUDFLARE_API_KEY = "yomama"; + CLOUDFLARE_EMAIL = "aaa@aaa.com"; + }; + volumes = [ + "/var/run/podman/podman.sock:/var/run/docker.sock:ro" + "/var/volumes/traefik:/etc/traefik:rw" + ]; + ports = [ + "80:80/tcp" + "443:443/tcp" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.traefik.entrypoints" = "https"; + "traefik.http.routers.traefik.middlewares" = "chain-authelia@file"; + "traefik.http.routers.traefik.rule" = "Host(`hey.hello.us`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"; + "traefik.http.routers.traefik.service" = "api@internal"; + "traefik.http.routers.traefik.tls.certresolver" = "htpc"; + }; + extraOptions = [ + "--network=default" + "--network-alias=traefik" + ]; + autoStart = false; + }; + virtualisation.oci-containers.containers."transmission" = { + image = "docker.io/haugene/transmission-openvpn"; + environment = { + GLOBAL_APPLY_PERMISSIONS = "false"; + LOCAL_NETWORK = "192.168.0.0/16"; + PGID = "1000"; + PUID = "1000"; + TRANSMISSION_DHT_ENABLED = "false"; + TRANSMISSION_DOWNLOAD_DIR = "/storage/Downloads/transmission"; + TRANSMISSION_HOME = "/config/transmission-home"; + TRANSMISSION_INCOMPLETE_DIR = "/storage/Downloads/transmission/incomplete"; + TRANSMISSION_INCOMPLETE_DIR_ENABLED = "true"; + TRANSMISSION_PEX_ENABLED = "false"; + TRANSMISSION_SCRIPT_TORRENT_DONE_ENABLED = "true"; + TRANSMISSION_SCRIPT_TORRENT_DONE_FILENAME = "/config/transmission-unpack.sh"; + TZ = "America/New_York"; + }; + volumes = [ + "/etc/localtime:/etc/localtime:ro" + "/var/volumes/transmission/config:/config:rw" + "/var/volumes/transmission/scripts:/scripts:rw" + "storage:/storage:rw" + ]; + ports = [ + "9091:9091/tcp" + ]; + labels = { + "autoheal" = "true"; + "traefik.enable" = "true"; + "traefik.http.routers.transmission.middlewares" = "chain-authelia@file"; + "traefik.http.routers.transmission.rule" = "Host(`hey.hello.us`) && PathPrefix(`/transmission`)"; + "traefik.http.routers.transmission.tls.certresolver" = "htpc"; + "traefik.http.services.transmission.loadbalancer.server.port" = "9091"; + }; + extraOptions = [ + "--network=default" + "--network-alias=transmission" + ]; + autoStart = false; + }; + + # Networks + systemd.services."create-docker-network-default" = { + serviceConfig.Type = "oneshot"; + path = [ pkgs.docker ]; + script = '' + docker network inspect default || docker network create default + ''; + wantedBy = [ + "docker-jellyseerr.service" + "docker-photoprism-mariadb.service" + "docker-sabnzbd.service" + "docker-traefik.service" + "docker-transmission.service" + ]; + }; + + # Volumes + systemd.services."create-docker-volume-books" = { + serviceConfig.Type = "oneshot"; + path = [ pkgs.docker ]; + script = '' + docker volume inspect books || docker volume create books --opt device=/mnt/media/Books,o=bind,type=none + ''; + }; + systemd.services."create-docker-volume-photos" = { + serviceConfig.Type = "oneshot"; + path = [ pkgs.docker ]; + script = '' + docker volume inspect photos || docker volume create photos --opt device=/mnt/photos,o=bind,type=none + ''; + }; + systemd.services."create-docker-volume-storage" = { + serviceConfig.Type = "oneshot"; + path = [ pkgs.docker ]; + script = '' + docker volume inspect storage || docker volume create storage --opt device=/mnt/media,o=bind,type=none + ''; + wantedBy = [ + "docker-sabnzbd.service" + "docker-transmission.service" + ]; + }; + + # Scripts + up = writeShellScript "compose-up.sh" '' + echo "TODO: Create resources." + ''; + down = writeShellScript "compose-down.sh" '' + echo "TODO: Remove resources." + ''; +} diff --git a/testdata/TestPodman_out.nix b/testdata/TestPodman_out.nix new file mode 100644 index 0000000..171979a --- /dev/null +++ b/testdata/TestPodman_out.nix @@ -0,0 +1,178 @@ +{ pkgs, ... }: + +{ + # 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."jellyseerr" = { + image = "docker.io/fallenbagel/jellyseerr:latest"; + environment = { + PGID = "1000"; + PUID = "1000"; + TZ = "America/New_York"; + }; + volumes = [ + "/var/volumes/jellyseerr:/app/config:rw" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.jellyseerr.middlewares" = "chain-authelia@file"; + "traefik.http.routers.jellyseerr.rule" = "Host(`requests.hello.us`)"; + "traefik.http.routers.jellyseerr.tls.certresolver" = "htpc"; + }; + extraOptions = [ + "--network=default" + "--network-alias=jellyseerr" + ]; + autoStart = false; + }; + virtualisation.oci-containers.containers."photoprism-mariadb" = { + image = "docker.io/library/mariadb:10.9"; + environment = { + MARIADB_AUTO_UPGRADE = "1"; + MARIADB_DATABASE = "photoprism"; + MARIADB_INITDB_SKIP_TZINFO = "1"; + MARIADB_PASSWORD = "insecure"; + MARIADB_ROOT_PASSWORD = "insecure"; + MARIADB_USER = "photoprism"; + }; + volumes = [ + "/var/volumes/photoprism-mariadb:/var/lib/mysql:rw" + ]; + extraOptions = [ + "--network=default" + "--network-alias=photoprism-mariadb" + ]; + user = "1000:1000"; + autoStart = false; + }; + virtualisation.oci-containers.containers."sabnzbd" = { + image = "lscr.io/linuxserver/sabnzbd"; + environment = { + DOCKER_MODS = "ghcr.io/gilbn/theme.park:sabnzbd"; + PGID = "1000"; + PUID = "1000"; + TP_DOMAIN = "hey.hello.us\/themepark"; + TP_HOTIO = "false"; + TP_THEME = "potato"; + TZ = "America/New_York"; + }; + volumes = [ + "/var/volumes/sabnzbd:/config:rw" + "/mnt/media:/storage:rw" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.sabnzbd.middlewares" = "chain-authelia@file"; + "traefik.http.routers.sabnzbd.rule" = "Host(`hey.hello.us`) && PathPrefix(`/sabnzbd`)"; + "traefik.http.routers.sabnzbd.tls.certresolver" = "htpc"; + }; + extraOptions = [ + "--network=default" + "--network-alias=sabnzbd" + ]; + autoStart = false; + }; + virtualisation.oci-containers.containers."traefik" = { + image = "docker.io/library/traefik"; + environment = { + CLOUDFLARE_API_KEY = "yomama"; + CLOUDFLARE_EMAIL = "aaa@aaa.com"; + }; + volumes = [ + "/var/run/podman/podman.sock:/var/run/docker.sock:ro" + "/var/volumes/traefik:/etc/traefik:rw" + ]; + ports = [ + "80:80/tcp" + "443:443/tcp" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.traefik.entrypoints" = "https"; + "traefik.http.routers.traefik.middlewares" = "chain-authelia@file"; + "traefik.http.routers.traefik.rule" = "Host(`hey.hello.us`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"; + "traefik.http.routers.traefik.service" = "api@internal"; + "traefik.http.routers.traefik.tls.certresolver" = "htpc"; + }; + extraOptions = [ + "--network=default" + "--network-alias=traefik" + ]; + autoStart = false; + }; + virtualisation.oci-containers.containers."transmission" = { + image = "docker.io/haugene/transmission-openvpn"; + environment = { + GLOBAL_APPLY_PERMISSIONS = "false"; + LOCAL_NETWORK = "192.168.0.0/16"; + PGID = "1000"; + PUID = "1000"; + TRANSMISSION_DHT_ENABLED = "false"; + TRANSMISSION_DOWNLOAD_DIR = "/storage/Downloads/transmission"; + TRANSMISSION_HOME = "/config/transmission-home"; + TRANSMISSION_INCOMPLETE_DIR = "/storage/Downloads/transmission/incomplete"; + TRANSMISSION_INCOMPLETE_DIR_ENABLED = "true"; + TRANSMISSION_PEX_ENABLED = "false"; + TRANSMISSION_SCRIPT_TORRENT_DONE_ENABLED = "true"; + TRANSMISSION_SCRIPT_TORRENT_DONE_FILENAME = "/config/transmission-unpack.sh"; + TZ = "America/New_York"; + }; + volumes = [ + "/etc/localtime:/etc/localtime:ro" + "/var/volumes/transmission/config:/config:rw" + "/var/volumes/transmission/scripts:/scripts:rw" + "/mnt/media:/storage:rw" + ]; + ports = [ + "9091:9091/tcp" + ]; + labels = { + "autoheal" = "true"; + "traefik.enable" = "true"; + "traefik.http.routers.transmission.middlewares" = "chain-authelia@file"; + "traefik.http.routers.transmission.rule" = "Host(`hey.hello.us`) && PathPrefix(`/transmission`)"; + "traefik.http.routers.transmission.tls.certresolver" = "htpc"; + "traefik.http.services.transmission.loadbalancer.server.port" = "9091"; + }; + extraOptions = [ + "--network=default" + "--network-alias=transmission" + ]; + autoStart = false; + }; + + # Networks + systemd.services."create-podman-network-default" = { + serviceConfig.Type = "oneshot"; + path = [ pkgs.podman ]; + script = '' + podman network create default --opt isolate=true --ignore + ''; + wantedBy = [ + "podman-jellyseerr.service" + "podman-photoprism-mariadb.service" + "podman-sabnzbd.service" + "podman-traefik.service" + "podman-transmission.service" + ]; + }; + + # Scripts + up = writeShellScript "compose-up.sh" '' + echo "TODO: Create resources." + ''; + down = writeShellScript "compose-down.sh" '' + echo "TODO: Remove resources." + ''; +} diff --git a/testdata/docker-compose.yml b/testdata/docker-compose.yml new file mode 100644 index 0000000..db12a07 --- /dev/null +++ b/testdata/docker-compose.yml @@ -0,0 +1,176 @@ +version: "3.7" +services: + sabnzbd: + image: lscr.io/linuxserver/sabnzbd + container_name: sabnzbd + environment: + PUID: ${PUID} + PGID: ${PGID} + TZ: ${TIMEZONE} + DOCKER_MODS: ghcr.io/gilbn/theme.park:sabnzbd + TP_THEME: ${THEMEPARK_THEME} + TP_DOMAIN: ${HOME_DOMAIN}\/themepark + TP_HOTIO: "false" + volumes: + - /var/volumes/sabnzbd:/config + - storage:/storage + labels: + - "traefik.enable=true" + - "traefik.http.routers.sabnzbd.rule=Host(`${HOME_DOMAIN}`) && PathPrefix(`/sabnzbd`)" + - "traefik.http.routers.sabnzbd.tls.certresolver=htpc" + - "traefik.http.routers.sabnzbd.middlewares=chain-authelia@file" + logging: + driver: "json-file" + options: + max-size: 10m + max-file: "3" + compress: "true" + restart: unless-stopped + transmission: + image: docker.io/haugene/transmission-openvpn + container_name: transmission + privileged: true + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + dns: + - 8.8.8.8 + - 8.8.4.4 + sysctls: + net.ipv6.conf.all.disable_ipv6: 0 + ports: + - "9091:9091" + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/volumes/transmission/config:/config + - /var/volumes/transmission/scripts:/scripts + - storage:/storage + environment: + TZ: ${TIMEZONE} + PUID: ${PUID} + PGID: ${PGID} + + # Do not try to chown the download directories. + GLOBAL_APPLY_PERMISSIONS: "false" + + TRANSMISSION_HOME: /config/transmission-home + + LOCAL_NETWORK: 192.168.0.0/16 + + # Disable DHT and PEX for private trackers. + TRANSMISSION_DHT_ENABLED: "false" + TRANSMISSION_PEX_ENABLED: "false" + + # Directories + TRANSMISSION_DOWNLOAD_DIR: /storage/Downloads/transmission + TRANSMISSION_INCOMPLETE_DIR: /storage/Downloads/transmission/incomplete + TRANSMISSION_INCOMPLETE_DIR_ENABLED: "true" + + # Script to automatically unrar downloads in Transmission. + # Make sure to set perms to 655. + TRANSMISSION_SCRIPT_TORRENT_DONE_ENABLED: "true" + TRANSMISSION_SCRIPT_TORRENT_DONE_FILENAME: /config/transmission-unpack.sh + labels: + - "traefik.enable=true" + - "traefik.http.services.transmission.loadbalancer.server.port=9091" + - "traefik.http.routers.transmission.rule=Host(`${HOME_DOMAIN}`) && PathPrefix(`/transmission`)" + - "traefik.http.routers.transmission.tls.certresolver=htpc" + - "traefik.http.routers.transmission.middlewares=chain-authelia@file" + - "autoheal=true" + logging: + driver: "json-file" + options: + max-size: 10m + max-file: "3" + compress: "true" + # Restart 3 times on failure. This can happen when the VPN sub expires. + restart: on-failure:3 + jellyseerr: + image: docker.io/fallenbagel/jellyseerr:latest + container_name: jellyseerr + environment: + PUID: ${PUID} + PGID: ${PGID} + TZ: ${TIMEZONE} + dns: + - 1.1.1.1 + volumes: + - /var/volumes/jellyseerr:/app/config + labels: + - "traefik.enable=true" + - "traefik.http.routers.jellyseerr.rule=Host(`requests.${DOMAIN}`)" + - "traefik.http.routers.jellyseerr.tls.certresolver=htpc" + - "traefik.http.routers.jellyseerr.middlewares=chain-authelia@file" + logging: + driver: "json-file" + options: + max-size: 10m + max-file: "3" + compress: "true" + restart: unless-stopped + photoprism-mariadb: + image: docker.io/library/mariadb:10.9 + container_name: photoprism-mariadb + environment: + MARIADB_AUTO_UPGRADE: "1" + MARIADB_INITDB_SKIP_TZINFO: "1" + MARIADB_DATABASE: "photoprism" + MARIADB_USER: "photoprism" + MARIADB_PASSWORD: "insecure" + MARIADB_ROOT_PASSWORD: "insecure" + user: "${PUID}:${PGID}" + volumes: + - /var/volumes/photoprism-mariadb:/var/lib/mysql + logging: + driver: "json-file" + options: + max-size: 10m + max-file: "3" + compress: "true" + restart: unless-stopped + traefik: + container_name: traefik + image: docker.io/library/traefik + ports: + - "80:80" + - "443:443" + environment: + CLOUDFLARE_EMAIL: ${CLOUDFLARE_EMAIL} + CLOUDFLARE_API_KEY: ${CLOUDFLARE_API_KEY} + volumes: + - /var/run/podman/podman.sock:/var/run/docker.sock:ro + - /var/volumes/traefik:/etc/traefik + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik.rule=Host(`${HOME_DOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" + - "traefik.http.routers.traefik.entrypoints=https" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.routers.traefik.tls.certresolver=htpc" + - "traefik.http.routers.traefik.middlewares=chain-authelia@file" + logging: + driver: "json-file" + options: + max-size: 10m + max-file: "3" + compress: "true" + restart: unless-stopped +volumes: + storage: + name: storage + driver_opts: + type: none + device: /mnt/media + o: bind + books: + name: books + driver_opts: + type: none + device: /mnt/media/Books + o: bind + photos: + name: photos + driver_opts: + type: none + device: /mnt/photos + o: bind diff --git a/testdata/input.env b/testdata/input.env new file mode 100644 index 0000000..820b9af --- /dev/null +++ b/testdata/input.env @@ -0,0 +1,8 @@ +DOMAIN=hello.us +HOME_DOMAIN=hey.hello.us +PUID=1000 +PGID=1000 +TIMEZONE=America/New_York +THEMEPARK_THEME=potato +CLOUDFLARE_EMAIL=aaa@aaa.com +CLOUDFLARE_API_KEY=yomama