diff --git a/cmd/selenoid.go b/cmd/selenoid.go index 0eac65c..f72ff81 100644 --- a/cmd/selenoid.go +++ b/cmd/selenoid.go @@ -26,6 +26,7 @@ var ( browserEnv string port uint16 uiPort uint16 + userNS string ) func init() { @@ -155,6 +156,7 @@ func initFlags() { } { c.Flags().StringVarP(&args, "args", "g", "", "additional service arguments (e.g. \"-limit 5\")") c.Flags().StringVarP(&env, "env", "e", "", "override service environment variables (e.g. \"KEY1=value1 KEY2=value2\")") + c.Flags().StringVarP(&userNS, "userns", "", "", "override user namespace, similarly to \"docker run --userns host ...\" (Docker only)") } } @@ -174,6 +176,7 @@ func createLifecycle(configDir string, port uint16) (*selenoid.Lifecycle, error) RegistryUrl: registry, Tmpfs: tmpfs, VNC: vnc, + UserNS: userNS, BrowsersJsonUrl: browsersJSONUrl, OS: operatingSystem, diff --git a/selenoid/base.go b/selenoid/base.go index 769ffac..fc65fe4 100644 --- a/selenoid/base.go +++ b/selenoid/base.go @@ -8,7 +8,7 @@ import ( "path/filepath" "github.com/fatih/color" - colorable "github.com/mattn/go-colorable" + "github.com/mattn/go-colorable" ) type StatusProvider interface { @@ -118,8 +118,11 @@ type PortAware struct { Port int } +type UserNSAware struct { + UserNS string +} + const ( - GgrUIDefaultPort = 8888 SelenoidDefaultPort = 4444 SelenoidUIDefaultPort = 8080 DefaultRegistryUrl = "https://registry.hub.docker.com" diff --git a/selenoid/docker.go b/selenoid/docker.go index 0d5bb1a..351a496 100644 --- a/selenoid/docker.go +++ b/selenoid/docker.go @@ -72,6 +72,7 @@ type DockerConfigurator struct { EnvAware BrowserEnvAware PortAware + UserNSAware LastVersions int Pull bool RegistryUrl string @@ -94,6 +95,7 @@ func NewDockerConfigurator(config *LifecycleConfig) (*DockerConfigurator, error) EnvAware: EnvAware{Env: config.Env}, BrowserEnvAware: BrowserEnvAware{BrowserEnv: config.BrowserEnv}, PortAware: PortAware{Port: config.Port}, + UserNSAware: UserNSAware{UserNS: config.UserNS}, RegistryUrl: config.RegistryUrl, LastVersions: config.LastVersions, Tmpfs: config.Tmpfs, @@ -617,7 +619,12 @@ func (c *DockerConfigurator) PrintArgs() error { if image == nil { return errors.New("Selenoid image is not downloaded: this is probably a bug") } - return c.startContainer("", image, 0, 0, []string{}, []string{}, []string{"--help"}, []string{}, true) + cfg := &containerConfig{ + Image: image, + Cmd: []string{"--help"}, + PrintLogs: true, + } + return c.startContainer(cfg) } const ( @@ -667,7 +674,17 @@ func (c *DockerConfigurator) Start() error { if !strings.Contains(c.Env, "OVERRIDE_VIDEO_OUTPUT_DIR") { overrideEnv = append(overrideEnv, fmt.Sprintf("OVERRIDE_VIDEO_OUTPUT_DIR=%s", videoConfigDir)) } - return c.startContainer(selenoidContainerName, image, c.Port, SelenoidDefaultPort, volumes, []string{}, cmd, overrideEnv, false) + cfg := &containerConfig{ + Name: selenoidContainerName, + Image: image, + HostPort: c.Port, + ServicePort: SelenoidDefaultPort, + Volumes: volumes, + Cmd: cmd, + OverrideEnv: overrideEnv, + UserNS: c.UserNS, + } + return c.startContainer(cfg) } func (c *DockerConfigurator) isDockerForWindows() bool { @@ -737,7 +754,12 @@ func (c *DockerConfigurator) PrintUIArgs() error { if image == nil { return errors.New("Selenoid UI image is not downloaded: this is probably a bug") } - return c.startContainer("", image, 0, 0, []string{}, []string{}, []string{"--help"}, []string{}, true) + cfg := &containerConfig{ + Image: image, + Cmd: []string{"--help"}, + PrintLogs: true, + } + return c.startContainer(cfg) } func (c *DockerConfigurator) StartUI() error { @@ -775,7 +797,17 @@ containers: } overrideEnv := strings.Fields(c.Env) - return c.startContainer(selenoidUIContainerName, image, c.Port, SelenoidUIDefaultPort, []string{}, links, cmd, overrideEnv, false) + cfg := &containerConfig{ + Name: selenoidUIContainerName, + Image: image, + HostPort: c.Port, + ServicePort: SelenoidUIDefaultPort, + Links: links, + Cmd: cmd, + OverrideEnv: overrideEnv, + UserNS: c.UserNS, + } + return c.startContainer(cfg) } func validateEnviron(envs []string) []string { @@ -789,45 +821,65 @@ func validateEnviron(envs []string) []string { return validEnv } -func (c *DockerConfigurator) startContainer(name string, image *types.ImageSummary, hostPort int, servicePort int, volumes []string, links []string, cmd []string, envOverride []string, printLogs bool) error { +type containerConfig struct { + Name string + Image *types.ImageSummary + HostPort int + ServicePort int + Volumes []string + Links []string + Cmd []string + OverrideEnv []string + UserNS string + PrintLogs bool +} + +func (c *DockerConfigurator) startContainer(cfg *containerConfig) error { ctx := context.Background() env := validateEnviron(os.Environ()) env = append(env, fmt.Sprintf("TZ=%s", time.Local)) - if len(envOverride) > 0 { - env = envOverride + if len(cfg.OverrideEnv) > 0 { + env = cfg.OverrideEnv } if !contains(env, dockerApiVersion) { env = append(env, fmt.Sprintf("%s=%s", dockerApiVersion, c.docker.ClientVersion())) } - servicePortString := strconv.Itoa(servicePort) + servicePortString := strconv.Itoa(cfg.ServicePort) port, err := nat.NewPort("tcp", servicePortString) if err != nil { return fmt.Errorf("failed to init port: %v", err) } containerConfig := container.Config{ Hostname: "localhost", - Image: image.RepoTags[0], + Image: cfg.Image.RepoTags[0], Env: env, } - if servicePort > 0 { + if cfg.ServicePort > 0 { containerConfig.ExposedPorts = map[nat.Port]struct{}{port: {}} } - if len(cmd) > 0 { - containerConfig.Cmd = strslice.StrSlice(cmd) + if len(cfg.Cmd) > 0 { + containerConfig.Cmd = strslice.StrSlice(cfg.Cmd) } hostConfig := container.HostConfig{ - Binds: volumes, - Links: links, + Binds: cfg.Volumes, + Links: cfg.Links, + } + if cfg.UserNS != "" { + mode := container.UsernsMode(cfg.UserNS) + if !mode.Valid() { + return fmt.Errorf("invalid userns value: %s", cfg.UserNS) + } + hostConfig.UsernsMode = mode } - if printLogs { + if cfg.PrintLogs { containerConfig.Tty = true } else { hostConfig.RestartPolicy = container.RestartPolicy{ Name: "always", } } - if hostPort > 0 && servicePort > 0 { - hostPortString := strconv.Itoa(hostPort) + if cfg.HostPort > 0 && cfg.ServicePort > 0 { + hostPortString := strconv.Itoa(cfg.HostPort) portBindings := nat.PortMap{} portBindings[port] = []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: hostPortString}} hostConfig.PortBindings = portBindings @@ -835,7 +887,7 @@ func (c *DockerConfigurator) startContainer(name string, image *types.ImageSumma ctr, err := c.docker.ContainerCreate(ctx, &containerConfig, &hostConfig, - &network.NetworkingConfig{}, name) + &network.NetworkingConfig{}, cfg.Name) if err != nil { return fmt.Errorf("failed to create container: %v", err) } @@ -844,7 +896,7 @@ func (c *DockerConfigurator) startContainer(name string, image *types.ImageSumma c.removeContainer(ctr.ID) return fmt.Errorf("failed to start container: %v", err) } - if printLogs { + if cfg.PrintLogs { defer c.removeContainer(ctr.ID) r, err := c.docker.ContainerLogs(ctx, ctr.ID, types.ContainerLogsOptions{ ShowStdout: true, diff --git a/selenoid/docker_test.go b/selenoid/docker_test.go index 9528b4b..8685516 100644 --- a/selenoid/docker_test.go +++ b/selenoid/docker_test.go @@ -324,6 +324,7 @@ func TestStartStopContainer(t *testing.T) { RegistryUrl: mockDockerServer.URL, Port: SelenoidDefaultPort, Version: Latest, + UserNS: "host", }) AssertThat(t, err, Is{nil}) AssertThat(t, c.IsRunning(), Is{true}) diff --git a/selenoid/lifecycle.go b/selenoid/lifecycle.go index e68bae7..249c804 100644 --- a/selenoid/lifecycle.go +++ b/selenoid/lifecycle.go @@ -25,6 +25,7 @@ type LifecycleConfig struct { RegistryUrl string Tmpfs int VNC bool + UserNS string // Drivers specific BrowsersJsonUrl string