package docker import ( "bufio" "bytes" "encoding/base64" "encoding/json" "fmt" "io" "os" "path/filepath" "regexp" "runtime" "strings" "github.com/docker/docker/cliconfig" "github.com/docker/engine-api/client" "github.com/openshift/origin/pkg/image/reference" "github.com/openshift/source-to-image/pkg/api" "github.com/openshift/source-to-image/pkg/errors" utilglog "github.com/openshift/source-to-image/pkg/util/glog" "github.com/openshift/source-to-image/pkg/util/user" ) var ( // glog is a placeholder until the builders pass an output stream down // client facing libraries should not be using glog glog = utilglog.StderrLog // DefaultEntrypoint is the default entry point used when starting containers DefaultEntrypoint = []string{"/usr/bin/env"} ) // AuthConfigurations maps a registry name to an AuthConfig, as used for example // in the .dockercfg file type AuthConfigurations struct { Configs map[string]api.AuthConfig } type dockerConfig struct { Auth string `json:"auth"` Email string `json:"email"` } const ( // maxErrorOutput is the maximum length of the error output saved for processing maxErrorOutput = 1024 defaultRegistry = "https://index.docker.io/v1/" ) // GetImageRegistryAuth retrieves the appropriate docker client authentication object for a given // image name and a given set of client authentication objects. func GetImageRegistryAuth(auths *AuthConfigurations, imageName string) api.AuthConfig { glog.V(5).Infof("Getting docker credentials for %s", imageName) if auths == nil { return api.AuthConfig{} } ref, err := reference.ParseNamedDockerImageReference(imageName) if err != nil { glog.V(0).Infof("error: Failed to parse docker reference %s", imageName) return api.AuthConfig{} } if ref.Registry != "" { if auth, ok := auths.Configs[ref.Registry]; ok { glog.V(5).Infof("Using %s[%s] credentials for pulling %s", auth.Email, ref.Registry, imageName) return auth } } if auth, ok := auths.Configs[defaultRegistry]; ok { glog.V(5).Infof("Using %s credentials for pulling %s", auth.Email, imageName) return auth } return api.AuthConfig{} } // LoadImageRegistryAuth loads and returns the set of client auth objects from a docker config // json file. func LoadImageRegistryAuth(dockerCfg io.Reader) *AuthConfigurations { auths, err := NewAuthConfigurations(dockerCfg) if err != nil { glog.V(0).Infof("error: Unable to load docker config: %v", err) return nil } return auths } // begin next 3 methods borrowed from go-dockerclient // NewAuthConfigurations finishes creating the auth config array s2i pulls // from any auth config file it is pointed to when started from the command line func NewAuthConfigurations(r io.Reader) (*AuthConfigurations, error) { var auth *AuthConfigurations confs, err := parseDockerConfig(r) if err != nil { return nil, err } auth, err = authConfigs(confs) if err != nil { return nil, err } return auth, nil } // parseDockerConfig does the json unmarshalling of the data we read from the file func parseDockerConfig(r io.Reader) (map[string]dockerConfig, error) { buf := new(bytes.Buffer) buf.ReadFrom(r) byteData := buf.Bytes() confsWrapper := struct { Auths map[string]dockerConfig `json:"auths"` }{} if err := json.Unmarshal(byteData, &confsWrapper); err == nil { if len(confsWrapper.Auths) > 0 { return confsWrapper.Auths, nil } } var confs map[string]dockerConfig if err := json.Unmarshal(byteData, &confs); err != nil { return nil, err } return confs, nil } // authConfigs converts a dockerConfigs map to a AuthConfigurations object. func authConfigs(confs map[string]dockerConfig) (*AuthConfigurations, error) { c := &AuthConfigurations{ Configs: make(map[string]api.AuthConfig), } for reg, conf := range confs { if len(conf.Auth) == 0 { continue } data, err := base64.StdEncoding.DecodeString(conf.Auth) if err != nil { return nil, err } userpass := strings.SplitN(string(data), ":", 2) if len(userpass) != 2 { return nil, fmt.Errorf("cannot parse username/password from %s", userpass) } c.Configs[reg] = api.AuthConfig{ Email: conf.Email, Username: userpass[0], Password: userpass[1], ServerAddress: reg, } } return c, nil } // end block of 3 methods borrowed from go-dockerclient // StreamContainerIO starts a goroutine to take data from the reader and // redirect it to the log function (typically we pass in glog.Error for stderr // and glog.Info for stdout. The caller should wrap glog functions in a closure // to ensure accurate line numbers are reported: // https://github.com/openshift/source-to-image/issues/558 . // StreamContainerIO returns a channel which is closed after the reader is // closed. func StreamContainerIO(r io.Reader, errOutput *string, log func(string)) <-chan struct{} { c := make(chan struct{}, 1) go func() { reader := bufio.NewReader(r) for { text, err := reader.ReadString('\n') if text != "" { log(text) } if errOutput != nil && len(*errOutput) < maxErrorOutput { *errOutput += text + "\n" } if err != nil { if glog.Is(2) && err != io.EOF { glog.V(0).Infof("error: Error reading docker stdout/stderr: %#v", err) } break } } close(c) }() return c } // TODO remove (base, tag, id) func parseRepositoryTag(repos string) (string, string, string) { n := strings.Index(repos, "@") if n >= 0 { parts := strings.Split(repos, "@") return parts[0], "", parts[1] } n = strings.LastIndex(repos, ":") if n < 0 { return repos, "", "" } if tag := repos[n+1:]; !strings.Contains(tag, "/") { return repos[:n], tag, "" } return repos, "", "" } // PullImage pulls the Docker image specifies by name taking the pull policy // into the account. // TODO: The 'force' option will be removed func PullImage(name string, d Docker, policy api.PullPolicy, force bool) (*PullResult, error) { // TODO: Remove this after we deprecate --force-pull if force { policy = api.PullAlways } if len(policy) == 0 { return nil, fmt.Errorf("the policy for pull image must be set") } var ( image *api.Image err error ) switch policy { case api.PullIfNotPresent: image, err = d.CheckAndPullImage(name) case api.PullAlways: glog.Infof("Pulling image %q ...", name) image, err = d.PullImage(name) case api.PullNever: glog.Infof("Checking if image %q is available locally ...", name) image, err = d.CheckImage(name) } return &PullResult{Image: image, OnBuild: d.IsImageOnBuild(name)}, err } // CheckAllowedUser retrieves the user for a Docker image and checks that user against // an allowed range of uids. // - If the range of users is not empty, then the user on the Docker image needs to be a numeric user // - The user's uid must be contained by the range(s) specified by the uids Rangelist // - If the image contains ONBUILD instructions and those instructions also contain a USER directive, // then the user specified by that USER directive must meet the uid range criteria as well. func CheckAllowedUser(d Docker, imageName string, uids user.RangeList, isOnbuild bool) error { if uids == nil || uids.Empty() { return nil } imageUserSpec, err := d.GetImageUser(imageName) if err != nil { return err } imageUser := extractUser(imageUserSpec) if !user.IsUserAllowed(imageUser, &uids) { return errors.NewUserNotAllowedError(imageName, false) } if isOnbuild { cmds, err := d.GetOnBuild(imageName) if err != nil { return err } if !isOnbuildAllowed(cmds, &uids) { return errors.NewUserNotAllowedError(imageName, true) } } return nil } var dockerLineDelim = regexp.MustCompile(`[\t\v\f\r ]+`) // isOnbuildAllowed checks a list of Docker ONBUILD instructions for // user directives. It ensures that any users specified by the directives // falls within the specified range list of users. func isOnbuildAllowed(directives []string, allowed *user.RangeList) bool { for _, line := range directives { parts := dockerLineDelim.Split(line, 2) if strings.ToLower(parts[0]) != "user" { continue } uname := extractUser(parts[1]) if !user.IsUserAllowed(uname, allowed) { return false } } return true } func extractUser(userSpec string) string { user := userSpec if strings.Contains(user, ":") { parts := strings.SplitN(userSpec, ":", 2) user = parts[0] } return strings.TrimSpace(user) } // CheckReachable returns if the Docker daemon is reachable from s2i func CheckReachable(config *api.Config) error { d, err := New(config.DockerConfig, config.PullAuthentication) if err != nil { return err } _, err = d.Version() return err } func pullAndCheck(image string, docker Docker, pullPolicy api.PullPolicy, config *api.Config, forcePull bool) (*PullResult, error) { r, err := PullImage(image, docker, pullPolicy, forcePull) if err != nil { return nil, err } err = CheckAllowedUser(docker, image, config.AllowedUIDs, r.OnBuild) if err != nil { return nil, err } return r, nil } // GetBuilderImage processes the config and performs operations necessary to make // the Docker image specified as BuilderImage available locally. // It returns information about the base image, containing metadata necessary // for choosing the right STI build strategy. func GetBuilderImage(config *api.Config) (*PullResult, error) { d, err := New(config.DockerConfig, config.PullAuthentication) if err != nil { return nil, err } return pullAndCheck(config.BuilderImage, d, config.BuilderPullPolicy, config, config.ForcePull) } // GetRebuildImage obtains the metadata information for the image // specified in a s2i rebuild operation. Assumptions are made that // the build is available locally since it should have been previously built. func GetRebuildImage(config *api.Config) (*PullResult, error) { d, err := New(config.DockerConfig, config.PullAuthentication) if err != nil { return nil, err } return pullAndCheck(config.Tag, d, config.BuilderPullPolicy, config, config.ForcePull) } // GetRuntimeImage processes the config and performs operations necessary to make // the Docker image specified as RuntimeImage available locally. func GetRuntimeImage(config *api.Config, docker Docker) error { pullPolicy := config.RuntimeImagePullPolicy if len(pullPolicy) == 0 { pullPolicy = api.DefaultRuntimeImagePullPolicy } _, err := pullAndCheck(config.RuntimeImage, docker, pullPolicy, config, false) return err } // GetDefaultDockerConfig checks relevant Docker environment variables to // provide defaults for our command line flags func GetDefaultDockerConfig() *api.DockerConfig { cfg := &api.DockerConfig{} if cfg.Endpoint = os.Getenv("DOCKER_HOST"); cfg.Endpoint == "" { cfg.Endpoint = client.DefaultDockerHost // TODO: remove this when we bump engine-api to >= cf82c64276ebc2501e72b241f9fdc1e21e421743 if runtime.GOOS == "darwin" { cfg.Endpoint = "unix:///var/run/docker.sock" } } certPath := os.Getenv("DOCKER_CERT_PATH") if certPath == "" { certPath = cliconfig.ConfigDir() } cfg.CertFile = filepath.Join(certPath, "cert.pem") cfg.KeyFile = filepath.Join(certPath, "key.pem") cfg.CAFile = filepath.Join(certPath, "ca.pem") if tlsVerify := os.Getenv("DOCKER_TLS_VERIFY"); tlsVerify != "" { cfg.TLSVerify = true } return cfg }