Skip to content

Commit

Permalink
support MAC and IP address on service
Browse files Browse the repository at this point in the history
  • Loading branch information
aksiksi committed Mar 30, 2024
1 parent 7fdd01c commit f334638
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 32 deletions.
100 changes: 68 additions & 32 deletions compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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.
//
Expand Down
31 changes: 31 additions & 0 deletions nix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
29 changes: 29 additions & 0 deletions testdata/TestMacvlanSupport.compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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

76 changes: 76 additions & 0 deletions testdata/TestMacvlanSupport_docker.nix
Original file line number Diff line number Diff line change
@@ -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
'';
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.";
};
};
}
78 changes: 78 additions & 0 deletions testdata/TestMacvlanSupport_podman.nix
Original file line number Diff line number Diff line change
@@ -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 --opt isolate=true
'';
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.";
};
};
}

0 comments on commit f334638

Please sign in to comment.