diff --git a/components/docker-up/docker-up/main.go b/components/docker-up/docker-up/main.go index 43c3bb1fc9c81a..e3f399f2d7e233 100644 --- a/components/docker-up/docker-up/main.go +++ b/components/docker-up/docker-up/main.go @@ -10,9 +10,11 @@ package main import ( "archive/tar" "bufio" + "bytes" "compress/gzip" "context" "embed" + "encoding/base64" "encoding/json" "fmt" "io" @@ -58,6 +60,7 @@ var aptUpdated = false const ( dockerSocketFN = "/var/run/docker.sock" gitpodUserId = 33333 + gitpodGroupId = 33333 containerIf = "eth0" ) @@ -185,6 +188,12 @@ func runWithinNetns() (err error) { }() } + if imageAuth, _ := os.LookupEnv("GITPOD_IMAGE_AUTH"); strings.TrimSpace(imageAuth) != "" { + if err := waitUntilSocketPresent(dockerSocketFN); err == nil { + tryAuthenticateForAllHosts(imageAuth) + } + } + err = cmd.Wait() if err != nil { return err @@ -192,6 +201,75 @@ func runWithinNetns() (err error) { return nil } +func waitUntilSocketPresent(dockerSocketFN string) error { + socketTimeout := 30 * time.Second + socketCtx, cancel := context.WithTimeout(context.Background(), socketTimeout) + defer cancel() + + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-socketCtx.Done(): + log.WithError(socketCtx.Err()).Warn("timeout waiting for docker socket") + return socketCtx.Err() + case <-ticker.C: + if _, err := os.Stat(dockerSocketFN); err == nil { + // Socket file exists + return nil + } + } + } +} + +func tryAuthenticateForAllHosts(imageAuth string) { + splitHostAndCredentials := func(s string) (string, string) { + parts := strings.SplitN(s, ":", 2) + if len(parts) < 2 { + return "", "" + } + return parts[0], parts[1] + } + splitCredentials := func(host, s string) (string, string, error) { + credentials, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", "", fmt.Errorf("Cannot decode docker credentials for host %s: %w", host, err) + } + parts := strings.SplitN(string(credentials), ":", 2) + if len(parts) < 2 { + return "", "", fmt.Errorf("Credentials in wrong format") + } + return parts[0], parts[1], nil + } + + authenticationPerHost := strings.Split(imageAuth, ",") + for _, hostCredentials := range authenticationPerHost { + host, credentials := splitHostAndCredentials(hostCredentials) + if host == "" || credentials == "" { + log.Warnf("Unable to authenticate with host %s, skipping.", host) + continue + } + username, password, decodeErr := splitCredentials(host, credentials) + if decodeErr != nil || username == "" || password == "" { + log.WithError(decodeErr).Warnf("Unable to authenticate with host %s, skipping.", host) + continue + } + + loginCmd := exec.Command("docker", "login", "--username", username, "--password-stdin", host) + loginCmd.SysProcAttr = &syscall.SysProcAttr{} + loginCmd.Env = append(loginCmd.Env, "USER=gitpod") + loginCmd.SysProcAttr.Credential = &syscall.Credential{Uid: gitpodUserId, Gid: gitpodGroupId} + loginCmd.Stdin = bytes.NewBufferString(password) + loginErr := loginCmd.Run() + if loginErr != nil { + log.WithError(loginErr).Warnf("Unable to authenticate with host %s, skipping.%s", host) + continue + } + log.Infof("Authenticated with host %s", host) + } +} + type ConvertUserArg func(arg, value string) ([]string, error) var allowedDockerArgs = map[string]ConvertUserArg{ diff --git a/components/supervisor/pkg/supervisor/config.go b/components/supervisor/pkg/supervisor/config.go index ef31fd25799c2a..06ae496a946ad0 100644 --- a/components/supervisor/pkg/supervisor/config.go +++ b/components/supervisor/pkg/supervisor/config.go @@ -347,6 +347,9 @@ type WorkspaceConfig struct { ConfigcatEnabled bool `env:"GITPOD_CONFIGCAT_ENABLED"` SSHGatewayCAPublicKey string `env:"GITPOD_SSH_CA_PUBLIC_KEY"` + + // Comma-separated list of host: pairs to authenticate against docker registries + GitpodImageAuth string `env:"GITPOD_IMAGE_AUTH"` } // WorkspaceGitpodToken is a list of tokens that should be added to supervisor's token service. diff --git a/components/supervisor/pkg/supervisor/docker.go b/components/supervisor/pkg/supervisor/docker.go index 4b25873ca66a8f..e7d620b4c150af 100644 --- a/components/supervisor/pkg/supervisor/docker.go +++ b/components/supervisor/pkg/supervisor/docker.go @@ -191,7 +191,13 @@ func listenToDockerSocket(parentCtx context.Context, term *terminal.Mux, cfg *Co } cmd := exec.CommandContext(ctx, "/usr/bin/docker-up") - cmd.Env = append(os.Environ(), "LISTEN_FDS=1") + // TODO(gpl): Why are we using os.Environ() in the first place, and not childProcEnvvars? + // Might be an oversight from times when we still passed all dockerupEnv vars as Pod dockerupEnv vars... otherwise I don't see why not use [] instead. + // But I'm not sure, so sticking with the slightly more verbose version here. + dockerupEnv := os.Environ() + dockerupEnv = append(dockerupEnv, fmt.Sprintf("GITPOD_IMAGE_AUTH=%s", cfg.GitpodImageAuth)) + dockerupEnv = append(dockerupEnv, "LISTEN_FDS=1") + cmd.Env = dockerupEnv cmd.ExtraFiles = []*os.File{socketFD} cmd.Stdout = stdout cmd.Stderr = stderr diff --git a/components/ws-manager-mk2/controllers/create.go b/components/ws-manager-mk2/controllers/create.go index 28ae672c2bae62..984fdd2f42e744 100644 --- a/components/ws-manager-mk2/controllers/create.go +++ b/components/ws-manager-mk2/controllers/create.go @@ -593,7 +593,8 @@ func createWorkspaceEnvironment(sctx *startWorkspaceContext) ([]corev1.EnvVar, e "GITPOD_EXTERNAL_EXTENSIONS", "GITPOD_WORKSPACE_CLASS_INFO", "GITPOD_IDE_ALIAS", - "GITPOD_RLIMIT_CORE": + "GITPOD_RLIMIT_CORE", + "GITPOD_IMAGE_AUTH": // these variables are allowed - don't skip them default: if strings.HasPrefix(e.Name, "GITPOD_") {