cmd/werf/run/run.go
package run
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/docker/cli/cli"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stdcopy"
"github.com/spf13/cobra"
"github.com/werf/logboek"
"github.com/werf/werf/v2/cmd/werf/common"
"github.com/werf/werf/v2/pkg/build"
"github.com/werf/werf/v2/pkg/buildah"
"github.com/werf/werf/v2/pkg/container_backend"
"github.com/werf/werf/v2/pkg/docker"
"github.com/werf/werf/v2/pkg/git_repo"
"github.com/werf/werf/v2/pkg/git_repo/gitdata"
"github.com/werf/werf/v2/pkg/giterminism_manager"
"github.com/werf/werf/v2/pkg/image"
"github.com/werf/werf/v2/pkg/logging"
"github.com/werf/werf/v2/pkg/ssh_agent"
"github.com/werf/werf/v2/pkg/storage/lrumeta"
"github.com/werf/werf/v2/pkg/storage/manager"
"github.com/werf/werf/v2/pkg/tmp_manager"
"github.com/werf/werf/v2/pkg/true_git"
"github.com/werf/werf/v2/pkg/werf"
"github.com/werf/werf/v2/pkg/werf/global_warnings"
)
type cmdDataType struct {
Shell bool
Bash bool
RawDockerOptions string
DockerOptions []string
DockerCommand []string
ImageName string
}
var (
cmdData cmdDataType
commonCmdData common.CmdData
)
func NewCmd(ctx context.Context) *cobra.Command {
ctx = common.NewContextWithCmdData(ctx, &commonCmdData)
cmd := common.SetCommandContext(ctx, &cobra.Command{
Use: "run [options] [IMAGE_NAME] [-- COMMAND ARG...]",
Short: "Run container for project image",
Long: common.GetLongCommandDescription(GetRunDocs().Long),
DisableFlagsInUseLine: true,
Example: ` # Run specified image and remove after execution
$ werf run application
# Run image with predefined docker run options and command for debug
$ werf run --shell
# Run image with specified docker run options and command
$ werf run --docker-options="-d -p 5000:5000 --restart=always --name registry" -- /app/run.sh
# Print a resulting docker run command
$ werf run --shell --dry-run
docker run -ti --rm image-stage-test:1ffe83860127e68e893b6aece5b0b7619f903f8492a285c6410371c87018c6a0 /bin/sh`,
Annotations: map[string]string{
common.DisableOptionsInUseLineAnno: "1",
common.DocsLongMD: GetRunDocs().LongMD,
},
RunE: func(cmd *cobra.Command, args []string) error {
if mode, _, err := common.GetBuildahMode(); err != nil {
return err
} else if *mode != buildah.ModeDisabled {
return fmt.Errorf(`command "werf run" is not implemented for Buildah mode`)
}
ctx := cmd.Context()
defer global_warnings.PrintGlobalWarnings(ctx)
if err := common.ProcessLogOptions(&commonCmdData); err != nil {
common.PrintHelp(cmd)
return err
}
if err := processArgs(cmd, args); err != nil {
common.PrintHelp(cmd)
return err
}
if cmdData.RawDockerOptions != "" {
cmdData.DockerOptions = strings.Fields(cmdData.RawDockerOptions)
}
if cmdData.Shell && cmdData.Bash {
return fmt.Errorf("cannot use --shell and --bash options at the same time")
}
if cmdData.Shell || cmdData.Bash {
if len(cmdData.DockerOptions) == 0 && len(cmdData.DockerCommand) == 0 {
cmdData.DockerOptions = []string{"-ti", "--rm"}
if cmdData.Shell {
cmdData.DockerOptions = append(cmdData.DockerOptions, "--entrypoint=/bin/sh")
}
if cmdData.Bash {
cmdData.DockerOptions = append(cmdData.DockerOptions, "--entrypoint=/bin/bash")
}
} else {
common.PrintHelp(cmd)
return fmt.Errorf("shell option cannot be used with other docker run arguments")
}
} else if len(cmdData.DockerOptions) == 0 {
cmdData.DockerOptions = append(cmdData.DockerOptions, "--rm")
}
return runMain(ctx)
},
})
common.SetupDir(&commonCmdData, cmd)
common.SetupGitWorkTree(&commonCmdData, cmd)
common.SetupConfigTemplatesDir(&commonCmdData, cmd)
common.SetupConfigPath(&commonCmdData, cmd)
common.SetupGiterminismConfigPath(&commonCmdData, cmd)
common.SetupEnvironment(&commonCmdData, cmd)
common.SetupGiterminismOptions(&commonCmdData, cmd)
common.SetupTmpDir(&commonCmdData, cmd, common.SetupTmpDirOptions{})
common.SetupHomeDir(&commonCmdData, cmd, common.SetupHomeDirOptions{})
common.SetupSSHKey(&commonCmdData, cmd)
common.SetupSecondaryStagesStorageOptions(&commonCmdData, cmd)
common.SetupCacheStagesStorageOptions(&commonCmdData, cmd)
common.SetupRepoOptions(&commonCmdData, cmd, common.RepoDataOptions{OptionalRepo: true})
common.SetupFinalRepo(&commonCmdData, cmd)
common.SetupRequireBuiltImages(&commonCmdData, cmd)
common.SetupFollow(&commonCmdData, cmd)
common.SetupDockerConfig(&commonCmdData, cmd, "Command needs granted permissions to read and pull images from the specified repo")
common.SetupInsecureRegistry(&commonCmdData, cmd)
common.SetupInsecureHelmDependencies(&commonCmdData, cmd, true)
common.SetupSkipTlsVerifyRegistry(&commonCmdData, cmd)
common.SetupContainerRegistryMirror(&commonCmdData, cmd)
common.SetupLogOptions(&commonCmdData, cmd)
common.SetupLogProjectDir(&commonCmdData, cmd)
common.SetupSynchronization(&commonCmdData, cmd)
common.SetupKubeConfig(&commonCmdData, cmd)
common.SetupKubeConfigBase64(&commonCmdData, cmd)
common.SetupKubeContext(&commonCmdData, cmd)
common.SetupDryRun(&commonCmdData, cmd)
common.SetupVirtualMerge(&commonCmdData, cmd)
commonCmdData.SetupPlatform(cmd)
cmd.Flags().BoolVarP(&cmdData.Shell, "shell", "", false, "Use predefined docker options and command for debug")
cmd.Flags().BoolVarP(&cmdData.Bash, "bash", "", false, "Use predefined docker options and command for debug")
cmd.Flags().StringVarP(&cmdData.RawDockerOptions, "docker-options", "", os.Getenv("WERF_DOCKER_OPTIONS"), "Define docker run options (default $WERF_DOCKER_OPTIONS)")
return cmd
}
func processArgs(cmd *cobra.Command, args []string) error {
doubleDashInd := cmd.ArgsLenAtDash()
doubleDashExist := cmd.ArgsLenAtDash() != -1
if doubleDashExist {
if doubleDashInd == len(args) {
return fmt.Errorf("unsupported position args format")
}
switch doubleDashInd {
case 0:
cmdData.DockerCommand = args[doubleDashInd:]
case 1:
cmdData.ImageName = args[0]
cmdData.DockerCommand = args[doubleDashInd:]
default:
return fmt.Errorf("unsupported position args format")
}
} else {
switch len(args) {
case 0:
case 1:
cmdData.ImageName = args[0]
default:
return fmt.Errorf("unsupported position args format")
}
}
return nil
}
func checkDetachDockerOption() error {
for _, value := range cmdData.DockerOptions {
if value == "-d" || value == "--detach" {
return nil
}
}
return fmt.Errorf("the container must be launched in the background (in follow mode): pass -d/--detach with --docker-options option")
}
func getContainerName() string {
for ind, value := range cmdData.DockerOptions {
if value == "--name" {
if ind+1 < len(cmdData.DockerOptions) {
return cmdData.DockerOptions[ind+1]
}
} else if strings.HasPrefix(value, "--name=") {
return strings.TrimPrefix(value, "--name=")
}
}
return ""
}
func runMain(ctx context.Context) error {
global_warnings.PostponeMultiwerfNotUpToDateWarning()
if err := werf.Init(*commonCmdData.TmpDir, *commonCmdData.HomeDir); err != nil {
return fmt.Errorf("initialization error: %w", err)
}
registryMirrors, err := common.GetContainerRegistryMirror(ctx, &commonCmdData)
if err != nil {
return fmt.Errorf("get container registry mirrors: %w", err)
}
containerBackend, processCtx, err := common.InitProcessContainerBackend(ctx, &commonCmdData, registryMirrors)
if err != nil {
return err
}
ctx = processCtx
gitDataManager, err := gitdata.GetHostGitDataManager(ctx)
if err != nil {
return fmt.Errorf("error getting host git data manager: %w", err)
}
if err := git_repo.Init(gitDataManager); err != nil {
return err
}
if err := image.Init(); err != nil {
return err
}
if err := lrumeta.Init(); err != nil {
return err
}
if err := true_git.Init(ctx, true_git.Options{LiveGitOutput: *commonCmdData.LogDebug}); err != nil {
return err
}
if err := common.DockerRegistryInit(ctx, &commonCmdData, registryMirrors); err != nil {
return err
}
if err := ssh_agent.Init(ctx, common.GetSSHKey(&commonCmdData)); err != nil {
return fmt.Errorf("cannot initialize ssh agent: %w", err)
}
defer func() {
err := ssh_agent.Terminate()
if err != nil {
logboek.Warn().LogF("WARNING: ssh agent termination failed: %s\n", err)
}
}()
giterminismManager, err := common.GetGiterminismManager(ctx, &commonCmdData)
if err != nil {
return err
}
common.ProcessLogProjectDir(&commonCmdData, giterminismManager.ProjectDir())
if *commonCmdData.Follow {
if cmdData.Shell || cmdData.Bash {
return fmt.Errorf("follow mode does not work with --shell and --bash options")
}
if err := checkDetachDockerOption(); err != nil {
return err
}
containerName := getContainerName()
if containerName == "" {
return fmt.Errorf("follow mode does not work without specific container name: pass --name=CONTAINER_NAME with --docker-options option")
}
return common.FollowGitHead(ctx, &commonCmdData, func(ctx context.Context, headCommitGiterminismManager giterminism_manager.Interface) error {
if err := safeDockerCliRmFunc(ctx, containerName); err != nil {
return err
}
if err := run(ctx, containerBackend, headCommitGiterminismManager); err != nil {
return err
}
go func() {
time.Sleep(500 * time.Millisecond)
fmt.Printf("Attaching to container %s ...\n", containerName)
resp, err := docker.ContainerAttach(ctx, containerName, types.ContainerAttachOptions{
Stream: true,
Stdout: true,
Stderr: true,
Logs: true,
})
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "WARNING:", err)
}
if _, err := stdcopy.StdCopy(os.Stdout, os.Stderr, resp.Reader); err != nil {
_, _ = fmt.Fprintln(os.Stderr, "WARNING:", err)
}
}()
return nil
})
} else {
if err := run(ctx, containerBackend, giterminismManager); err != nil {
if statusErr, ok := err.(cli.StatusError); ok {
common.TerminateWithError(err.Error(), statusErr.StatusCode)
}
return err
}
return nil
}
}
func run(ctx context.Context, containerBackend container_backend.ContainerBackend, giterminismManager giterminism_manager.Interface) error {
_, werfConfig, err := common.GetRequiredWerfConfig(ctx, &commonCmdData, giterminismManager, common.GetWerfConfigOptions(&commonCmdData, false))
if err != nil {
return fmt.Errorf("unable to load werf config: %w", err)
}
projectName := werfConfig.Meta.Project
projectTmpDir, err := tmp_manager.CreateProjectDir(ctx)
if err != nil {
return fmt.Errorf("getting project tmp dir failed: %w", err)
}
defer tmp_manager.ReleaseProjectDir(projectTmpDir)
imageName := cmdData.ImageName
if imageName == "" && len(werfConfig.GetAllImages()) == 1 {
imageName = werfConfig.GetAllImages()[0].GetName()
}
if !werfConfig.HasImage(imageName) {
return fmt.Errorf("image %q is not defined in werf.yaml", logging.ImageLogName(imageName, false))
}
stagesStorage, err := common.GetStagesStorage(ctx, containerBackend, &commonCmdData)
if err != nil {
return err
}
finalStagesStorage, err := common.GetOptionalFinalStagesStorage(ctx, containerBackend, &commonCmdData)
if err != nil {
return err
}
synchronization, err := common.GetSynchronization(ctx, &commonCmdData, projectName, stagesStorage)
if err != nil {
return err
}
storageLockManager, err := common.GetStorageLockManager(ctx, synchronization)
if err != nil {
return err
}
secondaryStagesStorageList, err := common.GetSecondaryStagesStorageList(ctx, stagesStorage, containerBackend, &commonCmdData)
if err != nil {
return err
}
cacheStagesStorageList, err := common.GetCacheStagesStorageList(ctx, containerBackend, &commonCmdData)
if err != nil {
return err
}
storageManager := manager.NewStorageManager(projectName, stagesStorage, finalStagesStorage, secondaryStagesStorageList, cacheStagesStorageList, storageLockManager)
logboek.Context(ctx).Info().LogOptionalLn()
imagesToProcess := build.NewImagesToProcess([]string{imageName}, false)
conveyorOptions, err := common.GetConveyorOptions(ctx, &commonCmdData, imagesToProcess)
if err != nil {
return err
}
conveyorWithRetry := build.NewConveyorWithRetryWrapper(werfConfig, giterminismManager, giterminismManager.ProjectDir(), projectTmpDir, ssh_agent.SSHAuthSock, containerBackend, storageManager, storageLockManager, conveyorOptions)
defer conveyorWithRetry.Terminate()
var dockerImageName string
if err := conveyorWithRetry.WithRetryBlock(ctx, func(c *build.Conveyor) error {
if common.GetRequireBuiltImages(ctx, &commonCmdData) {
if err := c.ShouldBeBuilt(ctx, build.ShouldBeBuiltOptions{}); err != nil {
return err
}
} else {
if err := c.Build(ctx, build.BuildOptions{SkipImageMetadataPublication: *commonCmdData.Dev}); err != nil {
return err
}
}
dockerImageName, err = c.GetFullImageName(ctx, imageName)
if err != nil {
return fmt.Errorf("unable to get full name for image %q: %w", imageName, err)
}
return nil
}); err != nil {
return err
}
var dockerRunArgs []string
dockerRunArgs = append(dockerRunArgs, cmdData.DockerOptions...)
dockerRunArgs = append(dockerRunArgs, dockerImageName)
dockerRunArgs = append(dockerRunArgs, cmdData.DockerCommand...)
if *commonCmdData.DryRun {
fmt.Printf("docker run %s\n", strings.Join(dockerRunArgs, " "))
return nil
} else {
return logboek.Streams().DoErrorWithoutProxyStreamDataFormatting(func() error {
return common.WithoutTerminationSignalsTrap(func() error {
return docker.CliRun_LiveOutput(ctx, dockerRunArgs...)
})
})
}
}
func safeDockerCliRmFunc(ctx context.Context, containerName string) error {
if exist, err := docker.ContainerExist(ctx, containerName); err != nil {
return fmt.Errorf("unable to check container %s existence: %w", containerName, err)
} else if exist {
logboek.Context(ctx).LogF("Removing container %s ...\n", containerName)
if err := docker.CliRm(ctx, "-f", containerName); err != nil {
return fmt.Errorf("unable to remove container %s: %w", containerName, err)
}
}
return nil
}