diff --git a/config/config_docker.go b/config/config_docker.go index f3e846b0..f0184985 100644 --- a/config/config_docker.go +++ b/config/config_docker.go @@ -3,6 +3,7 @@ package config import ( "encoding/base64" "sort" + "strings" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/registry" @@ -42,6 +43,15 @@ type DockerNetworkConfiguration struct { Interfaces dockerNetworkInterfaces `yaml:"interfaces"` } +// IsContainerNetworkMode returns true if the network mode shares another container's network namespace. +// When using "container:" mode, the container inherits the target container's network stack, +// including hostname, DNS, and network interfaces. +func (c DockerNetworkConfiguration) IsContainerNetworkMode() bool { + // Must have "container:" prefix and at least one character for the container name. + // Docker rejects "container:" without a name with "invalid container format container:". + return strings.HasPrefix(c.Mode, "container:") && len(c.Mode) > len("container:") +} + // DockerConfiguration defines the docker configuration used by the daemon when // interacting with containers and networks on the system. type DockerConfiguration struct { diff --git a/config/config_docker_test.go b/config/config_docker_test.go new file mode 100644 index 00000000..c6e0040f --- /dev/null +++ b/config/config_docker_test.go @@ -0,0 +1,32 @@ +package config + +import "testing" + +// TestDockerNetworkConfiguration_IsContainerNetworkMode tests the IsContainerNetworkMode +// method to ensure it correctly identifies when the network mode is set to share another +// container's network namespace (i.e., "container:" format). +func TestDockerNetworkConfiguration_IsContainerNetworkMode(t *testing.T) { + tests := []struct { + name string + mode string + expected bool + }{ + {"container mode with name", "container:caddy", true}, + {"container mode with different name", "container:some-vpn-container", true}, + {"container mode empty name", "container:", false}, // Docker rejects "container:" without a name + {"default pelican network", "pelican_nw", false}, + {"bridge network", "bridge", false}, + {"host network", "host", false}, + {"empty string", "", false}, + {"partial match", "containers", false}, // Should not match without colon + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := DockerNetworkConfiguration{Mode: tt.mode} + if got := c.IsContainerNetworkMode(); got != tt.expected { + t.Errorf("IsContainerNetworkMode() = %v, want %v for mode %q", got, tt.expected, tt.mode) + } + }) + } +} diff --git a/environment/docker/container.go b/environment/docker/container.go index 8284dc5b..8259f476 100644 --- a/environment/docker/container.go +++ b/environment/docker/container.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" @@ -177,15 +178,34 @@ func (e *Environment) Create() error { labels["Service"] = "Pelican" labels["ContainerType"] = "server_process" + // Only set hostname/domainname if not using container network mode. + // Containers sharing another container's network namespace inherit that container's + // hostname and domainname, so setting them would cause a Docker API error. + var hostname, domainname string + if !cfg.Docker.Network.IsContainerNetworkMode() { + hostname = e.Id + domainname = cfg.Docker.Domainname + } else { + e.log().WithField("network_mode", cfg.Docker.Network.Mode). + Debug("environment/docker: using container network mode, skipping hostname/domainname configuration") + } + + // Port exposure is not allowed when using container network mode since the network + // stack is inherited from the target container. Ports must be exposed on that container instead. + var exposedPorts nat.PortSet + if !cfg.Docker.Network.IsContainerNetworkMode() { + exposedPorts = a.Exposed() + } + conf := &container.Config{ - Hostname: e.Id, - Domainname: cfg.Docker.Domainname, + Hostname: hostname, + Domainname: domainname, AttachStdin: true, AttachStdout: true, AttachStderr: true, OpenStdin: true, Tty: true, - ExposedPorts: a.Exposed(), + ExposedPorts: exposedPorts, Image: strings.TrimPrefix(e.meta.Image, "~"), Env: e.Configuration.EnvironmentVariables(), Labels: labels, @@ -199,9 +219,14 @@ func (e *Environment) Create() error { } networkMode := container.NetworkMode(cfg.Docker.Network.Mode) + + // ForceOutgoingIP is incompatible with container network mode since the network + // stack is inherited from the target container. Skip this logic entirely. if a.ForceOutgoingIP { - // We can't use ForceOutgoingIP if we made a server with no allocation - if a.DefaultMapping.Port != 0 { + if cfg.Docker.Network.IsContainerNetworkMode() { + e.log().WithField("network_mode", cfg.Docker.Network.Mode). + Warn("environment/docker: ForceOutgoingIP is enabled but will be ignored when using container network mode") + } else if a.DefaultMapping.Port != 0 { enableIPv6 := false e.log().Debug("environment/docker: forcing outgoing IP address") networkName := "ip-" + strings.ReplaceAll(strings.ReplaceAll(a.DefaultMapping.Ip, ".", "-"), ":", "-") @@ -233,8 +258,24 @@ func (e *Environment) Create() error { } } + // DNS settings are inherited when using container network mode. + var dns []string + if !cfg.Docker.Network.IsContainerNetworkMode() { + dns = cfg.Docker.Network.Dns + } + + // Port bindings are not allowed when using container network mode since the network + // stack is inherited from the target container. Ports must be published on that container instead. + var portBindings nat.PortMap + if !cfg.Docker.Network.IsContainerNetworkMode() { + portBindings = a.DockerBindings() + } else { + e.log().WithField("network_mode", cfg.Docker.Network.Mode). + Debug("environment/docker: using container network mode, skipping port bindings configuration") + } + hostConf := &container.HostConfig{ - PortBindings: a.DockerBindings(), + PortBindings: portBindings, // Configure the mounts for this container. First mount the server data directory // into the container as an r/w bind. @@ -250,7 +291,7 @@ func (e *Environment) Create() error { // from the Panel. Resources: e.Configuration.Limits().AsContainerResources(), - DNS: cfg.Docker.Network.Dns, + DNS: dns, // Configure logging for the container to make it easier on the Daemon to grab // the server output. Ensure that we don't use too much space on the host machine diff --git a/server/install.go b/server/install.go index fc0a29de..3a56f166 100644 --- a/server/install.go +++ b/server/install.go @@ -415,8 +415,21 @@ func (ip *InstallationProcess) Execute() (string, error) { ctx, cancel := context.WithCancel(ip.Server.Context()) defer cancel() + // Get config first - must be available before container.Config struct + cfg := config.Get() + + // Only set hostname if not using container network mode. + // Containers sharing another container's network namespace inherit that container's hostname. + var hostname string + if !cfg.Docker.Network.IsContainerNetworkMode() { + hostname = "installer" + } else { + ip.Server.Log().WithField("network_mode", cfg.Docker.Network.Mode). + Debug("server/install: using container network mode, skipping hostname configuration") + } + conf := &container.Config{ - Hostname: "installer", + Hostname: hostname, AttachStdout: true, AttachStderr: true, AttachStdin: true, @@ -431,7 +444,12 @@ func (ip *InstallationProcess) Execute() (string, error) { }, } - cfg := config.Get() + // DNS settings are inherited when using container network mode. + var dns []string + if !cfg.Docker.Network.IsContainerNetworkMode() { + dns = cfg.Docker.Network.Dns + } + tmpfsSize := strconv.Itoa(int(cfg.Docker.TmpfsSize)) hostConf := &container.HostConfig{ Mounts: []mount.Mount{ @@ -452,7 +470,7 @@ func (ip *InstallationProcess) Execute() (string, error) { Tmpfs: map[string]string{ "/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M", }, - DNS: cfg.Docker.Network.Dns, + DNS: dns, LogConfig: cfg.Docker.ContainerLogConfig(), NetworkMode: container.NetworkMode(cfg.Docker.Network.Mode), UsernsMode: container.UsernsMode(cfg.Docker.UsernsMode),