Skip to content
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
26 changes: 26 additions & 0 deletions agent/internal/container/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package container

import (
"fmt"
"net/netip"
"strings"
)

// StableMACAddress derives a locally administered unicast MAC address from an
// IPv4 address. Containers that keep the same static IP must also keep the same
// MAC so hosts do not retain a stale neighbor entry across stop/recreate cycles.
func StableMACAddress(ipAddress string) string {
ip, err := netip.ParseAddr(strings.TrimSpace(ipAddress))
if err != nil || !ip.Is4() {
return ""
}

octets := ip.As4()
return fmt.Sprintf(
"02:42:%02x:%02x:%02x:%02x",
octets[0],
octets[1],
octets[2],
octets[3],
)
}
6 changes: 5 additions & 1 deletion agent/internal/container/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ func Deploy(config *DeployConfig) (*DeployResult, error) {
}

func buildPodmanRunArgs(config *DeployConfig, image string) []string {
networkMAC := StableMACAddress(config.IPAddress)

args := []string{
"run", "-d",
"--name", config.Name,
Expand All @@ -155,9 +157,11 @@ func buildPodmanRunArgs(config *DeployConfig, image string) []string {
"--label", fmt.Sprintf("techulus.service.name=%s", config.ServiceName),
"--label", fmt.Sprintf("techulus.deployment.id=%s", config.DeploymentID),
)

if config.IPAddress != "" {
args = append(args, "--network", NetworkName, "--ip", config.IPAddress)
if networkMAC != "" {
args = append(args, "--mac-address", networkMAC)
}
if config.PublishLocalPorts {
for _, pm := range config.PortMappings {
portMapping := fmt.Sprintf("127.0.0.1:%d:%d", pm.HostPort, pm.ContainerPort)
Expand Down
32 changes: 31 additions & 1 deletion agent/internal/container/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,43 @@ func TestBuildPodmanRunArgsPublishesLoopbackPortsWithStaticIP(t *testing.T) {
},
}, "docker.io/library/nginx:latest")

for _, want := range []string{"--network", NetworkName, "--ip", "10.200.1.2", "-p", "127.0.0.1:30080:80"} {
for _, want := range []string{
"--network",
NetworkName,
"--ip",
"10.200.1.2",
"--mac-address",
"02:42:0a:c8:01:02",
"-p",
"127.0.0.1:30080:80",
} {
if !slices.Contains(args, want) {
t.Fatalf("args missing %q: %+v", want, args)
}
}
}

func TestStableMACAddress(t *testing.T) {
tests := []struct {
name string
ipAddress string
want string
}{
{name: "private IPv4", ipAddress: "10.200.7.4", want: "02:42:0a:c8:07:04"},
{name: "trims whitespace", ipAddress: " 10.200.1.2 ", want: "02:42:0a:c8:01:02"},
{name: "invalid", ipAddress: "not-an-ip", want: ""},
{name: "IPv6", ipAddress: "fd00::1", want: ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := StableMACAddress(tt.ipAddress); got != tt.want {
t.Fatalf("StableMACAddress(%q) = %q, want %q", tt.ipAddress, got, tt.want)
}
})
}
}

func TestBuildPodmanRunArgsDoesNotPublishStaticIPPortsByDefault(t *testing.T) {
args := buildPodmanRunArgs(&DeployConfig{
Name: "svc-dep",
Expand Down
Loading