executors/docker/docker_command.go (334 lines of code) (raw):

package docker import ( "bytes" "context" "errors" "fmt" "strings" "sync" "time" "github.com/docker/docker/api/types" "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/gitlab-runner/common" "gitlab.com/gitlab-org/gitlab-runner/executors" "gitlab.com/gitlab-org/gitlab-runner/executors/docker/internal/exec" "gitlab.com/gitlab-org/gitlab-runner/executors/docker/internal/user" "gitlab.com/gitlab-org/gitlab-runner/helpers/docker" "gitlab.com/gitlab-org/gitlab-runner/helpers/featureflags" "gitlab.com/gitlab-org/gitlab-runner/helpers/limitwriter" ) const ( buildContainerType = "build" predefinedContainerType = "predefined" stepRunnerContainerType = "step-runner" ) type commandExecutor struct { executor helperContainer *types.ContainerJSON buildContainer *types.ContainerJSON lock sync.Mutex terminalWaitForContainerTimeout time.Duration stepRunnerContainerOnce sync.Once } func (s *commandExecutor) getBuildContainer() *types.ContainerJSON { s.lock.Lock() defer s.lock.Unlock() return s.buildContainer } func (s *commandExecutor) Prepare(options common.ExecutorPrepareOptions) error { err := s.executor.Prepare(options) if err != nil { return err } s.BuildLogger.Debugln("Starting Docker command...") if len(s.BuildShell.DockerCommand) == 0 { return errors.New("script is not compatible with Docker") } _, err = s.getHelperImage() if err != nil { return err } _, err = s.getBuildImage() if err != nil { return err } if s.isUmaskDisabled() { s.BuildLogger.Println("Not using umask - FF_DISABLE_UMASK_FOR_DOCKER_EXECUTOR is set!") } return nil } func (s *commandExecutor) isUmaskDisabled() bool { // Not usable with docker-windows executor if s.info.OSType == osTypeWindows { return false } if !s.Build.IsFeatureFlagOn(featureflags.DisableUmaskForDockerExecutor) { return false } return true } func (s *commandExecutor) Run(cmd common.ExecutorCommand) error { if cmd.Predefined { // if steps is enabled, run the step-runner container with the helper container. Eventually we can remove the // helper execution path. if s.Build.UseNativeSteps() { var err error s.stepRunnerContainerOnce.Do(func() { err = s.runContainer(stepRunnerContainerType, common.ExecutorCommand{Context: cmd.Context}) }) if err != nil { return err } } return s.runContainer(predefinedContainerType, cmd) } else { return s.runContainer(buildContainerType, cmd) } } func (s *commandExecutor) runContainer(containerType string, cmd common.ExecutorCommand) error { maxAttempts := s.Build.GetExecutorJobSectionAttempts() var runErr error for attempts := 1; attempts <= maxAttempts; attempts++ { if attempts > 1 { s.BuildLogger.Infoln(fmt.Sprintf("Retrying %s", cmd.Stage)) } ctr, err := s.requestContainer(containerType) if err != nil { return err } s.BuildLogger.Debugln("Executing on", ctr.Name, "the", cmd.Script) s.SetCurrentStage(ExecutorStageRun) runErr = s.startAndWatchContainer(cmd.Context, ctr.ID, bytes.NewBufferString(cmd.Script)) if !docker.IsErrNotFound(runErr) { return runErr } s.BuildLogger.Errorln(fmt.Sprintf("Container %q not found or removed. Will retry...", ctr.ID)) } if runErr != nil && maxAttempts > 1 { s.BuildLogger.Errorln("Execution attempts exceeded") } return runErr } func (s *commandExecutor) requestContainer(containerType string) (*types.ContainerJSON, error) { switch containerType { case buildContainerType: return s.requestBuildContainer() case predefinedContainerType: return s.requestHelperContainer() case stepRunnerContainerType: return s.requestStepRunnerContainer() default: return nil, fmt.Errorf("invalid container-type %q", containerType) } } func (s *commandExecutor) hasExistingContainer(containerType string, container *types.ContainerJSON) bool { if container == nil { return false } _, err := s.client.ContainerInspect(s.Context, container.ID) if err == nil { return true } if docker.IsErrNotFound(err) { return false } s.BuildLogger.Warningln("Failed to inspect", containerType, "container", container.ID, err.Error()) return false } func (s *commandExecutor) requestHelperContainer() (*types.ContainerJSON, error) { if s.hasExistingContainer(predefinedContainerType, s.helperContainer) { return s.helperContainer, nil } prebuildImage, err := s.getHelperImage() if err != nil { return nil, err } buildImage := common.Image{ Name: prebuildImage.ID, } s.helperContainer, err = s.createContainer( predefinedContainerType, buildImage, []string{prebuildImage.ID}, newDefaultContainerConfigurator(&s.executor, predefinedContainerType, buildImage, s.getHelperImageCmd(), []string{prebuildImage.ID}), ) if err != nil { return nil, err } return s.helperContainer, nil } func (s *commandExecutor) getHelperImageCmd() []string { if s.isUmaskDisabled() { if s.Config.IsProxyExec() { return []string{"gitlab-runner-helper", "proxy-exec", "--bootstrap", "/bin/bash"} } return []string{"/bin/bash"} } return s.helperImageInfo.Cmd } var stepRunnerServiceCommand = []string{"step-runner", "serve"} func (s *commandExecutor) requestBuildContainer() (*types.ContainerJSON, error) { s.lock.Lock() defer s.lock.Unlock() if s.hasExistingContainer(buildContainerType, s.buildContainer) { return s.buildContainer, nil } // Overwrite the build container command if using steps cmd := s.BuildShell.DockerCommand if s.Build.UseNativeSteps() { cmd = stepRunnerServiceCommand } var err error s.buildContainer, err = s.createContainer( buildContainerType, s.Build.Image, []string{}, newDefaultContainerConfigurator(&s.executor, buildContainerType, s.Build.Image, cmd, []string{}), ) if err != nil { return nil, err } err = s.changeFilesOwnership() if err != nil { return nil, err } return s.buildContainer, nil } func (s *commandExecutor) changeFilesOwnership() error { if !s.isUmaskDisabled() { return nil } dockerExec := exec.NewDocker(s.Context, s.client, s.waiter, s.Build.Log()) inspect := user.NewInspect(s.client, dockerExec) imageSHA := s.buildContainer.Image imageName := s.Build.Image.Name log := s.Build.Log().WithFields(logrus.Fields{ "imageSHA": imageSHA, "imageName": imageName, }) log.Debug("Checking if image runs with root user") usesRoot, err := inspect.IsRoot(s.Context, imageSHA) if err != nil { return fmt.Errorf("checking if image %q runs as root: %w", imageName, err) } if usesRoot { log.Debug("Image uses root user") return nil } log.Debug("Image doesn't use root user") uid, gid, err := getUIDandGID(s.Context, log, inspect, s.buildContainer.ID, imageSHA) if err != nil { return err } if uid == 0 { return nil } return s.executeChown(dockerExec, uid, gid) } func getUIDandGID( ctx context.Context, log logrus.FieldLogger, inspect user.Inspect, buildContainerID string, imageSHA string, ) (int, int, error) { containerLog := log.WithField("container", buildContainerID) containerLog.Debug("Getting the UID of the container") uid, err := inspect.UID(ctx, buildContainerID) if err != nil { return 0, 0, fmt.Errorf("checking %q image UID: %w", imageSHA, err) } containerLog.Debugf("Container UID=%d", uid) containerLog.Debug("Getting the GID of the container") gid, err := inspect.GID(ctx, buildContainerID) if err != nil { return 0, 0, fmt.Errorf("checking %q image GID: %w", imageSHA, err) } containerLog.Debugf("Container GID=%d", gid) return uid, gid, err } func (s *commandExecutor) executeChown(dockerExec exec.Docker, uid int, gid int) error { c, err := s.requestHelperContainer() if err != nil { return fmt.Errorf("requesting new predefined container: %w", err) } err = s.executeChownOnDir(c, dockerExec, uid, gid, s.Build.FullProjectDir()) if err != nil { return err } err = s.executeChownOnDir(c, dockerExec, uid, gid, s.Build.TmpProjectDir()) if err != nil { return err } return nil } func (s *commandExecutor) executeChownOnDir( c *types.ContainerJSON, dockerExec exec.Docker, uid int, gid int, dir string, ) error { s.BuildLogger.Println(fmt.Sprintf("Changing ownership of files at %q to %d:%d", dir, uid, gid)) output := new(bytes.Buffer) // limit how much data we read from the container log to // avoid memory exhaustion lw := limitwriter.New(output, 1024) streams := exec.IOStreams{ Stdin: strings.NewReader(fmt.Sprintf("chown -RP -- %d:%d %q", uid, gid, dir)), Stderr: lw, Stdout: lw, } err := dockerExec.Exec(s.Context, c.ID, streams, nil) log := s.Build.Log().WithField("updatedDir", dir) log.WithField("output", output.String()).Debug("Changing ownership of files") if err != nil { log.WithError(err).Error("Failed to change ownership of files") } return nil } func (s *commandExecutor) GetMetricsSelector() string { return fmt.Sprintf("instance=%q", s.executor.info.Name) } func init() { options := executors.ExecutorOptions{ DefaultCustomBuildsDirEnabled: true, DefaultSafeDirectoryCheckout: true, DefaultBuildsDir: "/builds", DefaultCacheDir: "/cache", SharedBuildsDir: false, Shell: common.ShellScriptInfo{ Shell: "bash", Type: common.NormalShell, RunnerCommand: "/usr/bin/gitlab-runner-helper", }, ShowHostname: true, } creator := func() common.Executor { e := &commandExecutor{ executor: executor{ AbstractExecutor: executors.AbstractExecutor{ ExecutorOptions: options, }, }, } e.SetCurrentStage(common.ExecutorStageCreated) return e } featuresUpdater := func(features *common.FeaturesInfo) { features.Variables = true features.Image = true features.Services = true features.Session = true features.Terminal = true features.ServiceVariables = true features.ServiceMultipleAliases = true features.ImageExecutorOpts = true features.ServiceExecutorOpts = true features.NativeStepsIntegration = true } common.RegisterExecutorProvider("docker", executors.DefaultExecutorProvider{ Creator: creator, FeaturesUpdater: featuresUpdater, ConfigUpdater: configUpdater, DefaultShellName: options.Shell.Shell, }) common.RegisterExecutorProvider("docker-windows", executors.DefaultExecutorProvider{ Creator: creator, FeaturesUpdater: featuresUpdater, ConfigUpdater: configUpdater, DefaultShellName: options.Shell.Shell, }) }