Repository: aksiksi/compose2nix (go)

Issue:
Macvlan Network Support in Compose File
I am currently using compose2nix lift and shift containers over to NixOS and I have a specific use case where I need support for macvlan networks.
The Service relies on using :443 and I quite like my Nginx/smalstep setup.
Below is an example of my compose file:

```yaml
version: '3.7'
services:
  teddycloud:
    container_name: teddycloud
    mac_address: 10:50:02:01:00:02
    dns: 192.168.8.1
    hostname: tc
    image: ghcr.io/toniebox-reverse-engineering/teddycloud:latest
    ports:
     - 80:80
     - 443:443  
    volumes:
      - /opt/tony/certs:/teddycloud/certs
      - /opt/tony/config:/teddycloud/config
      - /opt/tony/content:/teddycloud/data/content
      - /opt/tony/library:/teddycloud/data/library
      - /opt/tony/firmware:/teddycloud/data/firmware
    restart: unless-stopped
    networks:
      homenet:
        ipv4_address: 192.168.8.10

networks:
  homenet:
    driver: macvlan
    driver_opts:
      parent: enp2s0 
    ipam:
      config:
        - subnet: 192.168.8.0/24
          gateway: 192.168.8.1 
```



Thank you for considering and for your awesome effort in maintaining compose2nix!

Pull request:
Network `macvlan` and `ipam` support
Closes #15.

Patch:
diff --git a/README.md b/README.md
index 7a8e0da..c34c022 100644
--- a/README.md
+++ b/README.md
@@ -236,6 +236,7 @@ If a feature is missing, please feel free to [create an issue](https://github.co
 | [`deploy.resources`](https://docs.docker.com/compose/compose-file/deploy/#resources) | ✅ | |
 | [`devices`](https://docs.docker.com/compose/compose-file/05-services/#devices) | ✅ | |
 | [`networks.aliases`](https://docs.docker.com/compose/compose-file/05-services/#aliases) | ✅ | |
+| [`networks.ipv*_address`](https://docs.docker.com/compose/compose-file/05-services/#ipv4_address-ipv6_address) | ✅ | |
 | [`network_mode`](https://docs.docker.com/compose/compose-file/05-services/#network_mode) | ✅ | |
 | [`privileged`](https://docs.docker.com/compose/compose-file/05-services/#privileged) | ✅ | |
 | [`extra_hosts`](https://docs.docker.com/compose/compose-file/05-services/#extra_hosts) | ✅ | |
@@ -246,15 +247,17 @@ If a feature is missing, please feel free to [create an issue](https://github.co
 | [`command`](https://docs.docker.com/compose/compose-file/05-services/#command) | ✅ | |
 | [`healthcheck`](https://docs.docker.com/compose/compose-file/05-services/#healthcheck) | ✅ | |
 | [`hostname`](https://docs.docker.com/compose/compose-file/05-services/#hostname) | ✅ | |
+| [`mac_address`](https://docs.docker.com/compose/compose-file/05-services/#mac_address) | ✅ | |
 
 #### [`networks`](https://docs.docker.com/compose/compose-file/06-networks/)
 
 |   |     |
 |---|:---:|
 | [`labels`](https://docs.docker.com/compose/compose-file/06-networks/#labels) | ✅ |
-| [`name`](https://docs.docker.com/compose/compose-file/06-networks/#name) | ❌ |
-| [`driver`](https://docs.docker.com/compose/compose-file/06-networks/#driver) | ❌ |
-| [`driver_opts`](https://docs.docker.com/compose/compose-file/06-networks/#driver_opts) | ❌ |
+| [`name`](https://docs.docker.com/compose/compose-file/06-networks/#name) | ✅ |
+| [`driver`](https://docs.docker.com/compose/compose-file/06-networks/#driver) | ✅ |
+| [`driver_opts`](https://docs.docker.com/compose/compose-file/06-networks/#driver_opts) | ✅ |
+| [`ipam`](https://docs.docker.com/compose/compose-file/06-networks/#ipam) | ✅ |
 | [`external`](https://docs.docker.com/compose/compose-file/06-networks/#external) | ❌ |
 | [`internal`](https://docs.docker.com/compose/compose-file/06-networks/#internal) | ❌ |
 
diff --git a/compose.go b/compose.go
index 9729c16..f0e07a3 100644
--- a/compose.go
+++ b/compose.go
@@ -270,38 +270,6 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig) (*NixContaine
 	// We need to know this to be able to determine if we can configure a network alias.
 	inBridgeNetwork := len(service.Networks) > 0
 
-	for name, net := range service.Networks {
-		networkName := g.Project.With(name)
-		c.Networks = append(c.Networks, networkName)
-
-		// Network-scoped aliases.
-		var aliases []string
-		if net != nil {
-			aliases = append(aliases, net.Aliases...)
-		}
-
-		networkFlag := fmt.Sprintf("--network=%s", networkName)
-		switch g.Runtime {
-		case ContainerRuntimeDocker:
-			// Aliases are scoped to this (single) network.
-			for _, alias := range aliases {
-				c.ExtraOptions = append(c.ExtraOptions, "--network-alias="+alias)
-			}
-		case ContainerRuntimePodman:
-			// Aliases are scoped to the current network.
-			// https://docs.podman.io/en/latest/markdown/podman-run.1.html#network-mode-net
-			var networkOpts []string
-			for _, alias := range aliases {
-				networkOpts = append(networkOpts, "alias="+alias)
-			}
-			if len(networkOpts) > 0 {
-				networkFlag += fmt.Sprintf(":%s", strings.Join(networkOpts, ","))
-			}
-		}
-
-		c.ExtraOptions = append(c.ExtraOptions, networkFlag)
-	}
-
 	// https://docs.docker.com/compose/compose-file/05-services/#network_mode
 	// https://docs.podman.io/en/latest/markdown/podman-run.1.html#network-mode-net
 	if networkMode := strings.TrimSpace(service.NetworkMode); networkMode != "" {
@@ -340,6 +308,74 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig) (*NixContaine
 		}
 	}
 
+	var firstNetworkName string
+	for name, net := range service.Networks {
+		if firstNetworkName == "" {
+			firstNetworkName = name
+		}
+
+		networkName := g.Project.With(name)
+		c.Networks = append(c.Networks, networkName)
+
+		networkFlag := fmt.Sprintf("--network=%s", networkName)
+
+		// If we don't have any additional config set on this network, stop here.
+		if net == nil {
+			c.ExtraOptions = append(c.ExtraOptions, networkFlag)
+			continue
+		}
+
+		switch g.Runtime {
+		case ContainerRuntimeDocker:
+			// Aliases are scoped to this (single) network.
+			for _, alias := range net.Aliases {
+				c.ExtraOptions = append(c.ExtraOptions, "--network-alias="+alias)
+			}
+			if len(service.Networks) > 1 {
+				// TODO(aksiksi): Improve logging.
+				log.Printf("WARNING: Docker only supports a single network per container")
+				break
+			}
+		case ContainerRuntimePodman:
+			// Aliases are scoped to the current network.
+			// https://docs.podman.io/en/latest/markdown/podman-run.1.html#network-mode-net
+			var networkOpts []string
+			for _, alias := range net.Aliases {
+				networkOpts = append(networkOpts, "alias="+alias)
+			}
+
+			// Below, we fallback to using --ip/--ip6 if a single network is
+			// specified. This aligns with Docker behavior.
+			if len(service.Networks) > 1 {
+				if net.Ipv4Address != "" {
+					networkOpts = append(networkOpts, "ip="+net.Ipv4Address)
+				}
+				if net.Ipv6Address != "" {
+					networkOpts = append(networkOpts, "ip="+net.Ipv6Address)
+				}
+			}
+
+			if len(networkOpts) > 0 {
+				networkFlag += fmt.Sprintf(":%s", strings.Join(networkOpts, ","))
+			}
+		}
+
+		c.ExtraOptions = append(c.ExtraOptions, networkFlag)
+	}
+
+	if net := service.Networks[firstNetworkName]; len(service.Networks) == 1 && net != nil {
+		if net.Ipv4Address != "" {
+			c.ExtraOptions = append(c.ExtraOptions, "--ip="+net.Ipv4Address)
+		}
+		if net.Ipv6Address != "" {
+			c.ExtraOptions = append(c.ExtraOptions, "--ip6="+net.Ipv6Address)
+		}
+	}
+
+	if service.MacAddress != "" {
+		c.ExtraOptions = append(c.ExtraOptions, "--mac-address="+service.MacAddress)
+	}
+
 	if inBridgeNetwork {
 		// Allow other containers to use service name as an alias.
 		//
@@ -571,11 +607,43 @@ func (g *Generator) buildNixNetworks(composeProject *types.Project, containers [
 	var networks []*NixNetwork
 	for name, network := range composeProject.Networks {
 		n := &NixNetwork{
-			Runtime: g.Runtime,
-			Name:    g.Project.With(name),
-			Labels:  network.Labels,
+			Runtime:    g.Runtime,
+			Name:       g.Project.With(name),
+			Driver:     network.Driver,
+			DriverOpts: network.DriverOpts,
+			Labels:     network.Labels,
+		}
+		if g.Runtime == ContainerRuntimePodman {
+			if n.DriverOpts == nil {
+				n.DriverOpts = make(map[string]string)
+			}
+			n.DriverOpts["isolate"] = "true"
 		}
+
+		// IPAM configuration.
+		// https://docs.docker.com/compose/compose-file/06-networks/#ipam
+		// https://docs.docker.com/reference/cli/docker/network/create/
+		// https://docs.podman.io/en/latest/markdown/podman-network-create.1.html
+		if network.Ipam.Driver != "" {
+			n.IpamDriver = network.Ipam.Driver
+		}
+		for _, ipamConfig := range network.Ipam.Config {
+			cfg := IpamConfig{
+				Subnet:  ipamConfig.Subnet,
+				IPRange: ipamConfig.IPRange,
+				Gateway: ipamConfig.Gateway,
+			}
+			if g.Runtime == ContainerRuntimeDocker {
+				for k, v := range ipamConfig.AuxiliaryAddresses {
+					cfg.AuxAddresses = append(cfg.AuxAddresses, fmt.Sprintf("%s=%s", k, v))
+				}
+				slices.Sort(cfg.AuxAddresses)
+			}
+			n.IpamConfigs = append(n.IpamConfigs, cfg)
+		}
+
 		// Keep track of all container services that are in this network.
+		// If a network is unused, we don't need to generate it.
 		used := false
 		for _, c := range containers {
 			if slices.Contains(c.Networks, n.Name) {
@@ -583,10 +651,10 @@ func (g *Generator) buildNixNetworks(composeProject *types.Project, containers [
 				break
 			}
 		}
-		// If a network is unused, we don't need to generate it.
 		if !used && !g.GenerateUnusedResoures {
 			continue
 		}
+
 		networks = append(networks, n)
 	}
 	slices.SortFunc(networks, func(n1, n2 *NixNetwork) int {
diff --git a/helpers.go b/helpers.go
index a39b548..22e5c8d 100644
--- a/helpers.go
+++ b/helpers.go
@@ -27,15 +27,6 @@ func mapToRepeatedKeyValFlag(flagName string, m map[string]string) []string {
 	return arr
 }
 
-func mapToRepeatedFlag(flagName string, m map[string]string) []string {
-	var flags []string
-	for k, v := range m {
-		flags = append(flags, fmt.Sprintf("%s=%s=%s", flagName, k, v))
-	}
-	slices.Sort(flags)
-	return flags
-}
-
 // ReadEnvFiles reads the given set of env files into a list of KEY=VAL entries.
 //
 // If mergeWithEnv is set, the running env is merged with the provided env files. Any
diff --git a/nix.go b/nix.go
index 45ec84d..c22d47f 100644
--- a/nix.go
+++ b/nix.go
@@ -51,16 +51,62 @@ func (p *Project) With(name string) string {
 	return fmt.Sprintf("%s%s%s", p.Name, p.separator, name)
 }
 
+type IpamConfig struct {
+	Subnet       string
+	IPRange      string
+	Gateway      string
+	AuxAddresses []string
+}
+
 type NixNetwork struct {
-	Runtime ContainerRuntime
-	Name    string
-	Labels  map[string]string
+	Runtime     ContainerRuntime
+	Name        string
+	Driver      string
+	DriverOpts  map[string]string
+	Labels      map[string]string
+	IpamDriver  string
+	IpamConfigs []IpamConfig
 }
 
 func (n *NixNetwork) Unit() string {
 	return fmt.Sprintf("%s-network-%s.service", n.Runtime, n.Name)
 }
 
+func (n *NixNetwork) Command() string {
+	cmd := fmt.Sprintf("%[1]s network inspect %[2]s || %[1]s network create %[2]s", n.Runtime, n.Name)
+	if n.Driver != "" {
+		cmd += fmt.Sprintf(" --driver=%s", n.Driver)
+	}
+	if len(n.DriverOpts) > 0 {
+		driverOpts := mapToRepeatedKeyValFlag("--opt", n.DriverOpts)
+		cmd += " " + strings.Join(driverOpts, " ")
+	}
+
+	if n.IpamDriver != "" {
+		cmd += fmt.Sprintf(" --ipam-driver=%s", n.IpamDriver)
+	}
+	for _, cfg := range n.IpamConfigs {
+		if cfg.Subnet != "" {
+			cmd += fmt.Sprintf(" --subnet=%s", cfg.Subnet)
+		}
+		if cfg.IPRange != "" {
+			cmd += fmt.Sprintf(" --ip-range=%s", cfg.IPRange)
+		}
+		if cfg.Gateway != "" {
+			cmd += fmt.Sprintf(" --gateway=%s", cfg.Gateway)
+		}
+		for _, addr := range cfg.AuxAddresses {
+			cmd += fmt.Sprintf(` --aux-address="%s"`, addr)
+		}
+	}
+
+	if len(n.Labels) > 0 {
+		labels := mapToRepeatedKeyValFlag("--label", n.Labels)
+		cmd += " " + strings.Join(labels, " ")
+	}
+	return cmd
+}
+
 type NixVolume struct {
 	Runtime           ContainerRuntime
 	Name              string
@@ -79,6 +125,22 @@ func (v *NixVolume) Unit() string {
 	return fmt.Sprintf("%s-volume-%s.service", v.Runtime, v.Name)
 }
 
+func (v *NixVolume) Command() string {
+	cmd := fmt.Sprintf("%[1]s volume inspect %[2]s || %[1]s volume create %[2]s", v.Runtime, v.Name)
+	if v.Driver != "" {
+		cmd += fmt.Sprintf(" --driver=%s", v.Driver)
+	}
+	if len(v.DriverOpts) > 0 {
+		driverOpts := mapToRepeatedKeyValFlag("--opt", v.DriverOpts)
+		cmd += " " + strings.Join(driverOpts, " ")
+	}
+	if len(v.Labels) > 0 {
+		labels := mapToRepeatedKeyValFlag("--label", v.Labels)
+		cmd += " " + strings.Join(labels, " ")
+	}
+	return cmd
+}
+
 // NixContainerSystemdConfig configures the container's systemd config.
 // In particular, this allows control of the container restart policy through systemd
 // service and unit configs.
diff --git a/template.go b/template.go
index c64f79d..a11bdcd 100644
--- a/template.go
+++ b/template.go
@@ -46,10 +46,7 @@ func toNixList(s []string) string {
 }
 
 var funcMap template.FuncMap = template.FuncMap{
-	"derefInt":                derefInt,
-	"mapToKeyValArray":        mapToKeyValArray,
-	"mapToRepeatedFlag":       mapToRepeatedFlag,
-	"mapToRepeatedKeyValFlag": mapToRepeatedKeyValFlag,
-	"toNixValue":              toNixValue,
-	"toNixList":               toNixList,
+	"derefInt":   derefInt,
+	"toNixValue": toNixValue,
+	"toNixList":  toNixList,
 }
diff --git a/templates/network.nix.tmpl b/templates/network.nix.tmpl
index de7b25e..ac1f6d7 100644
--- a/templates/network.nix.tmpl
+++ b/templates/network.nix.tmpl
@@ -1,4 +1,3 @@
-{{- $labels := mapToRepeatedKeyValFlag "--label" .Labels -}}
 systemd.services."{{.Runtime}}-network-{{.Name}}" = {
   path = [ pkgs.{{.Runtime}} ];
   serviceConfig = {
@@ -6,15 +5,9 @@ systemd.services."{{.Runtime}}-network-{{.Name}}" = {
     RemainAfterExit = true;
     ExecStop = "${pkgs.{{.Runtime}}}/bin/{{.Runtime}} network rm -f {{.Name}}";
   };
-  {{- if eq (.Runtime | printf "%s") "docker"}}
   script = ''
-    {{.Runtime}} network inspect {{.Name}} || {{.Runtime}} network create {{.Name}}{{- if $labels}} {{ $labels | join " "}}{{- end}}
+    {{ .Command }}
   '';
-  {{- else}}
-  script = ''
-    {{.Runtime}} network inspect {{.Name}} || {{.Runtime}} network create {{.Name}} --opt isolate=true{{- if $labels}} {{ $labels | join " "}}{{- end}}
-  '';
-  {{- end}}
   {{- if rootTarget}}
   {{- /* PartOf for stop/restart of root, WantedBy for start of root. */}}
   partOf = [ "{{rootTarget}}.target" ];
diff --git a/templates/volume.nix.tmpl b/templates/volume.nix.tmpl
index bc8378c..deef52c 100644
--- a/templates/volume.nix.tmpl
+++ b/templates/volume.nix.tmpl
@@ -1,5 +1,3 @@
-{{- $driverOpts := mapToRepeatedFlag "--opt" .DriverOpts | join " " -}}
-{{- $labels := mapToRepeatedKeyValFlag "--label" .Labels -}}
 systemd.services."{{.Runtime}}-volume-{{.Name}}" = {
   path = [ pkgs.{{.Runtime}} ];
   serviceConfig = {
@@ -17,11 +15,7 @@ systemd.services."{{.Runtime}}-volume-{{.Name}}" = {
   ];
   {{- end}}
   script = ''
-    {{- if eq .Driver ""}}
-    {{.Runtime}} volume inspect {{.Name}} || {{.Runtime}} volume create {{.Name}}{{- if $driverOpts}} {{$driverOpts}}{{- end}}{{- if $labels}} {{ $labels | join " "}}{{- end}}
-    {{- else}}
-    {{.Runtime}} volume inspect {{.Name}} || {{.Runtime}} volume create {{.Name}} --driver {{.Driver}}{{- if $driverOpts}} {{$driverOpts}}{{- end}}{{- if $labels}} {{ $labels | join " "}}{{- end}}
-    {{- end}}
+    {{ .Command }}
   '';
   {{- if rootTarget}}
   {{- /* PartOf for stop/restart of root, WantedBy for start of root. */}}


Test patch:
diff --git a/nix_test.go b/nix_test.go
index 12b4920..dba375a 100644
--- a/nix_test.go
+++ b/nix_test.go
@@ -377,3 +377,34 @@ func TestDocker_NoWriteNixSetup(t *testing.T) {
 		t.Errorf("output diff: %s\n", diff)
 	}
 }
+
+func TestMacvlanSupport(t *testing.T) {
+	ctx := context.Background()
+	testName := t.Name()
+	inputPath := fmt.Sprintf("testdata/%s.compose.yml", testName)
+	for _, runtime := range []ContainerRuntime{ContainerRuntimeDocker, ContainerRuntimePodman} {
+		t.Run(runtime.String(), func(t *testing.T) {
+			outFilePath := fmt.Sprintf("testdata/%s_%s.nix", testName, runtime)
+			g := Generator{
+				Runtime: runtime,
+				Inputs:  []string{inputPath},
+			}
+			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 *update {
+				if err := os.WriteFile(outFilePath, []byte(got), 0644); err != nil {
+					t.Fatal(err)
+				}
+			} else if diff := cmp.Diff(want, got); diff != "" {
+				t.Errorf("output diff: %s\n", diff)
+			}
+		})
+	}
+}
diff --git a/nixos-test/docker-compose.nix b/nixos-test/docker-compose.nix
index 6ad4029..35939c3 100644
--- a/nixos-test/docker-compose.nix
+++ b/nixos-test/docker-compose.nix
@@ -69,8 +69,9 @@
     ];
     log-driver = "journald";
     extraOptions = [
+      "--ip=192.168.8.20"
       "--network-alias=service-b"
-      "--network=myproject-default"
+      "--network=myproject-something"
     ];
   };
   systemd.services."docker-service-b" = {
@@ -84,11 +85,11 @@
       StartLimitIntervalSec = lib.mkOverride 500 "infinity";
     };
     after = [
-      "docker-network-myproject-default.service"
+      "docker-network-myproject-something.service"
       "docker-volume-storage.service"
     ];
     requires = [
-      "docker-network-myproject-default.service"
+      "docker-network-myproject-something.service"
       "docker-volume-storage.service"
     ];
     partOf = [
@@ -119,6 +120,19 @@
     partOf = [ "docker-compose-myproject-root.target" ];
     wantedBy = [ "docker-compose-myproject-root.target" ];
   };
+  systemd.services."docker-network-myproject-something" = {
+    path = [ pkgs.docker ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStop = "${pkgs.docker}/bin/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
+    '';
+    partOf = [ "docker-compose-myproject-root.target" ];
+    wantedBy = [ "docker-compose-myproject-root.target" ];
+  };
 
   # Volumes
   systemd.services."docker-volume-storage" = {
diff --git a/nixos-test/docker-compose.yml b/nixos-test/docker-compose.yml
index 8f51205..c8fa291 100644
--- a/nixos-test/docker-compose.yml
+++ b/nixos-test/docker-compose.yml
@@ -30,10 +30,17 @@ services:
       - "compose2nix.systemd.service.RuntimeMaxSec=360"
     depends_on:
       - service-a
+    networks:
+      something:
+        ipv4_address: 192.168.8.20
     restart: on-failure:3
 
 networks:
   something:
+    ipam:
+      config:
+        - subnet: 192.168.8.0/24
+          gateway: 192.168.8.1
     labels:
       - "test-label=okay"
 
diff --git a/nixos-test/podman-compose.nix b/nixos-test/podman-compose.nix
index 60a4d67..f344fb9 100644
--- a/nixos-test/podman-compose.nix
+++ b/nixos-test/podman-compose.nix
@@ -71,8 +71,9 @@
     ];
     log-driver = "journald";
     extraOptions = [
+      "--ip=192.168.8.20"
       "--network-alias=service-b"
-      "--network=myproject-default"
+      "--network=myproject-something"
     ];
   };
   systemd.services."podman-service-b" = {
@@ -86,11 +87,11 @@
       StartLimitIntervalSec = lib.mkOverride 500 "infinity";
     };
     after = [
-      "podman-network-myproject-default.service"
+      "podman-network-myproject-something.service"
       "podman-volume-storage.service"
     ];
     requires = [
-      "podman-network-myproject-default.service"
+      "podman-network-myproject-something.service"
       "podman-volume-storage.service"
     ];
     partOf = [
@@ -116,7 +117,20 @@
       ExecStop = "${pkgs.podman}/bin/podman network rm -f myproject-default";
     };
     script = ''
-      podman network inspect myproject-default || podman network create myproject-default --opt isolate=true
+      podman network inspect myproject-default || podman network create myproject-default --opt=isolate=true
+    '';
+    partOf = [ "podman-compose-myproject-root.target" ];
+    wantedBy = [ "podman-compose-myproject-root.target" ];
+  };
+  systemd.services."podman-network-myproject-something" = {
+    path = [ pkgs.podman ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStop = "${pkgs.podman}/bin/podman network rm -f myproject-something";
+    };
+    script = ''
+      podman network inspect myproject-something || podman network create myproject-something --opt=isolate=true --subnet=192.168.8.0/24 --gateway=192.168.8.1 --label=test-label=okay
     '';
     partOf = [ "podman-compose-myproject-root.target" ];
     wantedBy = [ "podman-compose-myproject-root.target" ];
diff --git a/nixos-test/update.sh b/nixos-test/update.sh
index 543352d..cec2ff6 100755
--- a/nixos-test/update.sh
+++ b/nixos-test/update.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 
 export TIMEZONE="America/New_York"
 
diff --git a/testdata/TestMacvlanSupport.compose.yml b/testdata/TestMacvlanSupport.compose.yml
new file mode 100644
index 0000000..1f33c17
--- /dev/null
+++ b/testdata/TestMacvlanSupport.compose.yml
@@ -0,0 +1,31 @@
+version: '3.7'
+name: "myproject"
+services:
+  teddycloud:
+    container_name: container
+    mac_address: 10:50:02:01:00:02
+    dns: 192.168.8.1
+    hostname: tc
+    image: ghcr.io/container/container:latest
+    ports:
+     - 80:80
+     - 443:443
+    volumes:
+      - /opt/container/certs:/container/certs
+    restart: unless-stopped
+    networks:
+      homenet:
+        ipv4_address: 192.168.8.10
+
+networks:
+  homenet:
+    driver: macvlan
+    driver_opts:
+      parent: enp2s0
+    ipam:
+      config:
+        - subnet: 192.168.8.0/24
+          gateway: 192.168.8.1
+          aux_addresses:
+            host1: 192.168.8.5
+
diff --git a/testdata/TestMacvlanSupport_docker.nix b/testdata/TestMacvlanSupport_docker.nix
new file mode 100644
index 0000000..d2ea6e5
--- /dev/null
+++ b/testdata/TestMacvlanSupport_docker.nix
@@ -0,0 +1,76 @@
+{ pkgs, lib, ... }:
+
+{
+  # Runtime
+  virtualisation.docker = {
+    enable = true;
+    autoPrune.enable = true;
+  };
+  virtualisation.oci-containers.backend = "docker";
+
+  # Containers
+  virtualisation.oci-containers.containers."container" = {
+    image = "ghcr.io/container/container:latest";
+    volumes = [
+      "/opt/container/certs:/container/certs:rw"
+    ];
+    ports = [
+      "80:80/tcp"
+      "443:443/tcp"
+    ];
+    log-driver = "journald";
+    autoStart = false;
+    extraOptions = [
+      "--dns=192.168.8.1"
+      "--hostname=tc"
+      "--ip=192.168.8.10"
+      "--mac-address=10:50:02:01:00:02"
+      "--network-alias=teddycloud"
+      "--network=myproject-homenet"
+    ];
+  };
+  systemd.services."docker-container" = {
+    serviceConfig = {
+      Restart = lib.mkOverride 500 "always";
+      RestartMaxDelaySec = lib.mkOverride 500 "1m";
+      RestartSec = lib.mkOverride 500 "100ms";
+      RestartSteps = lib.mkOverride 500 9;
+    };
+    after = [
+      "docker-network-myproject-homenet.service"
+    ];
+    requires = [
+      "docker-network-myproject-homenet.service"
+    ];
+    partOf = [
+      "docker-compose-myproject-root.target"
+    ];
+    wantedBy = [
+      "docker-compose-myproject-root.target"
+    ];
+  };
+
+  # Networks
+  systemd.services."docker-network-myproject-homenet" = {
+    path = [ pkgs.docker ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStop = "${pkgs.docker}/bin/docker network rm -f myproject-homenet";
+    };
+    script = ''
+      docker network inspect myproject-homenet || docker network create myproject-homenet --driver=macvlan --opt=parent=enp2s0 --subnet=192.168.8.0/24 --gateway=192.168.8.1 --aux-address="host1=192.168.8.5"
+    '';
+    partOf = [ "docker-compose-myproject-root.target" ];
+    wantedBy = [ "docker-compose-myproject-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-myproject-root" = {
+    unitConfig = {
+      Description = "Root target generated by compose2nix.";
+    };
+  };
+}
diff --git a/testdata/TestMacvlanSupport_podman.nix b/testdata/TestMacvlanSupport_podman.nix
new file mode 100644
index 0000000..f892921
--- /dev/null
+++ b/testdata/TestMacvlanSupport_podman.nix
@@ -0,0 +1,78 @@
+{ 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."container" = {
+    image = "ghcr.io/container/container:latest";
+    volumes = [
+      "/opt/container/certs:/container/certs:rw"
+    ];
+    ports = [
+      "80:80/tcp"
+      "443:443/tcp"
+    ];
+    log-driver = "journald";
+    autoStart = false;
+    extraOptions = [
+      "--dns=192.168.8.1"
+      "--hostname=tc"
+      "--ip=192.168.8.10"
+      "--mac-address=10:50:02:01:00:02"
+      "--network-alias=teddycloud"
+      "--network=myproject-homenet"
+    ];
+  };
+  systemd.services."podman-container" = {
+    serviceConfig = {
+      Restart = lib.mkOverride 500 "always";
+    };
+    after = [
+      "podman-network-myproject-homenet.service"
+    ];
+    requires = [
+      "podman-network-myproject-homenet.service"
+    ];
+    partOf = [
+      "podman-compose-myproject-root.target"
+    ];
+    wantedBy = [
+      "podman-compose-myproject-root.target"
+    ];
+  };
+
+  # Networks
+  systemd.services."podman-network-myproject-homenet" = {
+    path = [ pkgs.podman ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStop = "${pkgs.podman}/bin/podman network rm -f myproject-homenet";
+    };
+    script = ''
+      podman network inspect myproject-homenet || podman network create myproject-homenet --driver=macvlan --opt=isolate=true --opt=parent=enp2s0 --subnet=192.168.8.0/24 --gateway=192.168.8.1
+    '';
+    partOf = [ "podman-compose-myproject-root.target" ];
+    wantedBy = [ "podman-compose-myproject-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-myproject-root" = {
+    unitConfig = {
+      Description = "Root target generated by compose2nix.";
+    };
+  };
+}
diff --git a/testdata/TestPodman_SystemdMount_out.nix b/testdata/TestPodman_SystemdMount_out.nix
index 8e9eb0f..14fd541 100644
--- a/testdata/TestPodman_SystemdMount_out.nix
+++ b/testdata/TestPodman_SystemdMount_out.nix
@@ -343,7 +343,7 @@
       ExecStop = "${pkgs.podman}/bin/podman network rm -f myproject-default";
     };
     script = ''
-      podman network inspect myproject-default || podman network create myproject-default --opt isolate=true
+      podman network inspect myproject-default || podman network create myproject-default --opt=isolate=true
     '';
     partOf = [ "podman-compose-myproject-root.target" ];
     wantedBy = [ "podman-compose-myproject-root.target" ];
@@ -356,7 +356,7 @@
       ExecStop = "${pkgs.podman}/bin/podman network rm -f myproject-something";
     };
     script = ''
-      podman network inspect myproject-something || podman network create myproject-something --opt isolate=true --label=test-label=okay
+      podman network inspect myproject-something || podman network create myproject-something --opt=isolate=true --label=test-label=okay
     '';
     partOf = [ "podman-compose-myproject-root.target" ];
     wantedBy = [ "podman-compose-myproject-root.target" ];
diff --git a/testdata/TestPodman_WithProject_out.nix b/testdata/TestPodman_WithProject_out.nix
index ce81f86..3123da5 100644
--- a/testdata/TestPodman_WithProject_out.nix
+++ b/testdata/TestPodman_WithProject_out.nix
@@ -325,7 +325,7 @@
       ExecStop = "${pkgs.podman}/bin/podman network rm -f myproject-default";
     };
     script = ''
-      podman network inspect myproject-default || podman network create myproject-default --opt isolate=true
+      podman network inspect myproject-default || podman network create myproject-default --opt=isolate=true
     '';
     partOf = [ "podman-compose-myproject-root.target" ];
     wantedBy = [ "podman-compose-myproject-root.target" ];
@@ -338,7 +338,7 @@
       ExecStop = "${pkgs.podman}/bin/podman network rm -f myproject-something";
     };
     script = ''
-      podman network inspect myproject-something || podman network create myproject-something --opt isolate=true --label=test-label=okay
+      podman network inspect myproject-something || podman network create myproject-something --opt=isolate=true --label=test-label=okay
     '';
     partOf = [ "podman-compose-myproject-root.target" ];
     wantedBy = [ "podman-compose-myproject-root.target" ];
diff --git a/testdata/TestPodman_out.nix b/testdata/TestPodman_out.nix
index fadc404..8aabcc7 100644
--- a/testdata/TestPodman_out.nix
+++ b/testdata/TestPodman_out.nix
@@ -320,7 +320,7 @@
       ExecStop = "${pkgs.podman}/bin/podman network rm -f myproject-default";
     };
     script = ''
-      podman network inspect myproject-default || podman network create myproject-default --opt isolate=true
+      podman network inspect myproject-default || podman network create myproject-default --opt=isolate=true
     '';
     partOf = [ "podman-compose-myproject-root.target" ];
     wantedBy = [ "podman-compose-myproject-root.target" ];
@@ -333,7 +333,7 @@
       ExecStop = "${pkgs.podman}/bin/podman network rm -f myproject-something";
     };
     script = ''
-      podman network inspect myproject-something || podman network create myproject-something --opt isolate=true --label=test-label=okay
+      podman network inspect myproject-something || podman network create myproject-something --opt=isolate=true --label=test-label=okay
     '';
     partOf = [ "podman-compose-myproject-root.target" ];
     wantedBy = [ "podman-compose-myproject-root.target" ];