github.com/wtrep/tgf@v1.18.8/docker.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"os/signal"
    12  	"os/user"
    13  	"path/filepath"
    14  	"regexp"
    15  	"runtime"
    16  	"strings"
    17  	"syscall"
    18  
    19  	"github.com/aws/aws-sdk-go/aws"
    20  	"github.com/aws/aws-sdk-go/aws/session"
    21  	"github.com/aws/aws-sdk-go/service/ecr"
    22  	"github.com/blang/semver"
    23  	"github.com/coveo/gotemplate/utils"
    24  	"github.com/docker/docker/api/types"
    25  	"github.com/docker/docker/api/types/filters"
    26  	"github.com/docker/docker/client"
    27  	"github.com/fatih/color"
    28  	"github.com/gruntwork-io/terragrunt/util"
    29  )
    30  
    31  const (
    32  	minimumDockerVersion = "1.25"
    33  	tgfImageVersion      = "TGF_IMAGE_VERSION"
    34  	dockerSocketFile     = "/var/run/docker.sock"
    35  	dockerfilePattern    = "TGF_dockerfile"
    36  	maxDockerTagLength   = 128
    37  )
    38  
    39  func callDocker(withDockerMount bool, args ...string) int {
    40  	command := append([]string{config.EntryPoint}, args...)
    41  
    42  	// Change the default log level for terragrunt
    43  	const logLevelArg = "--terragrunt-logging-level"
    44  	if !util.ListContainsElement(command, logLevelArg) && filepath.Base(config.EntryPoint) == "terragrunt" {
    45  		if config.LogLevel == "6" || strings.ToLower(config.LogLevel) == "full" {
    46  			config.LogLevel = "debug"
    47  			config.Environment["TF_LOG"] = "DEBUG"
    48  			config.Environment["TERRAGRUNT_DEBUG"] = "1"
    49  		}
    50  
    51  		// The log level option should not be supplied if there is no actual command
    52  		for _, arg := range args {
    53  			if !strings.HasPrefix(arg, "-") {
    54  				command = append(command, []string{logLevelArg, config.LogLevel}...)
    55  				break
    56  			}
    57  		}
    58  	}
    59  
    60  	if flushCache && filepath.Base(config.EntryPoint) == "terragrunt" {
    61  		command = append(command, "--terragrunt-source-update")
    62  	}
    63  
    64  	imageName := getImage()
    65  
    66  	if getImageName {
    67  		Println(imageName)
    68  		return 0
    69  	}
    70  
    71  	cwd := filepath.ToSlash(must(filepath.EvalSymlinks(must(os.Getwd()).(string))).(string))
    72  	currentDrive := fmt.Sprintf("%s/", filepath.VolumeName(cwd))
    73  	sourceFolder := filepath.ToSlash(filepath.Join("/", mountPoint, strings.TrimPrefix(cwd, currentDrive)))
    74  	rootFolder := strings.Split(strings.TrimPrefix(cwd, currentDrive), "/")[0]
    75  
    76  	dockerArgs := []string{
    77  		"run", "-it",
    78  		"-v", fmt.Sprintf("%s%s:%s", convertDrive(currentDrive), rootFolder, filepath.ToSlash(filepath.Join("/", mountPoint, rootFolder))),
    79  		"-w", sourceFolder,
    80  	}
    81  
    82  	if withDockerMount {
    83  		withDockerMountArgs := []string{"-v", fmt.Sprintf(dockerSocketMountPattern, dockerSocketFile), "--group-add", getDockerGroup()}
    84  		dockerArgs = append(dockerArgs, withDockerMountArgs...)
    85  	}
    86  
    87  	if !noHome {
    88  		currentUser := must(user.Current()).(*user.User)
    89  		home := filepath.ToSlash(currentUser.HomeDir)
    90  		homeWithoutVolume := strings.TrimPrefix(home, filepath.VolumeName(home))
    91  
    92  		dockerArgs = append(dockerArgs, []string{
    93  			"-v", fmt.Sprintf("%v:%v", convertDrive(home), homeWithoutVolume),
    94  			"-e", fmt.Sprintf("HOME=%v", homeWithoutVolume),
    95  		}...)
    96  
    97  		dockerArgs = append(dockerArgs, config.DockerOptions...)
    98  	}
    99  
   100  	if !noTemp {
   101  		temp := filepath.ToSlash(filepath.Join(must(filepath.EvalSymlinks(os.TempDir())).(string), "tgf-cache"))
   102  		tempDrive := fmt.Sprintf("%s/", filepath.VolumeName(temp))
   103  		tempFolder := strings.TrimPrefix(temp, tempDrive)
   104  		if runtime.GOOS == "windows" {
   105  			os.Mkdir(temp, 0755)
   106  		}
   107  		dockerArgs = append(dockerArgs, "-v", fmt.Sprintf("%s%s:/var/tgf", convertDrive(tempDrive), tempFolder))
   108  		config.Environment["TERRAGRUNT_CACHE"] = "/var/tgf"
   109  	}
   110  
   111  	config.Environment["TGF_COMMAND"] = config.EntryPoint
   112  	config.Environment["TGF_VERSION"] = version
   113  	config.Environment["TGF_ARGS"] = strings.Join(os.Args, " ")
   114  	config.Environment["TGF_LAUNCH_FOLDER"] = sourceFolder
   115  	config.Environment["TGF_IMAGE_NAME"] = imageName // sha256 of image
   116  
   117  	if !strings.Contains(config.Image, "coveo/tgf") { // the tgf image injects its own image info
   118  		config.Environment["TGF_IMAGE"] = config.Image
   119  		if config.ImageVersion != nil {
   120  			config.Environment[tgfImageVersion] = *config.ImageVersion
   121  			if version, err := semver.Make(*config.ImageVersion); err == nil {
   122  				config.Environment["TGF_IMAGE_MAJ_MIN"] = fmt.Sprintf("%d.%d", version.Major, version.Minor)
   123  			}
   124  		}
   125  		if config.ImageTag != nil {
   126  			config.Environment["TGF_IMAGE_TAG"] = *config.ImageTag
   127  		}
   128  	}
   129  
   130  	for key, val := range config.Environment {
   131  		os.Setenv(key, val)
   132  		debugPrint("export %v=%v", key, val)
   133  	}
   134  
   135  	for _, do := range dockerOptions {
   136  		dockerArgs = append(dockerArgs, strings.Split(do, " ")...)
   137  	}
   138  
   139  	if !util.ListContainsElement(dockerArgs, "--name") {
   140  		// We do not remove the image after execution if a name has been provided
   141  		dockerArgs = append(dockerArgs, "--rm")
   142  	}
   143  
   144  	dockerArgs = append(dockerArgs, getEnviron(!noHome)...)
   145  	dockerArgs = append(dockerArgs, imageName)
   146  	dockerArgs = append(dockerArgs, command...)
   147  	dockerCmd := exec.Command("docker", dockerArgs...)
   148  	dockerCmd.Stdin, dockerCmd.Stdout = os.Stdin, os.Stdout
   149  	var stderr bytes.Buffer
   150  	dockerCmd.Stderr = &stderr
   151  
   152  	if len(config.Environment) > 0 {
   153  		debugPrint("")
   154  	}
   155  	debugPrint("%s\n", strings.Join(dockerCmd.Args, " "))
   156  
   157  	if err := runCommands(config.runBeforeCommands); err != nil {
   158  		return -1
   159  	}
   160  	if err := dockerCmd.Run(); err != nil {
   161  		if stderr.Len() > 0 {
   162  			ErrPrintf(errorString(stderr.String()))
   163  			ErrPrintf("\n%s %s\n", dockerCmd.Args[0], strings.Join(dockerArgs, " "))
   164  
   165  			if runtime.GOOS == "windows" {
   166  				ErrPrintln(windowsMessage)
   167  			}
   168  		}
   169  	}
   170  	if err := runCommands(config.runAfterCommands); err != nil {
   171  		ErrPrintf(errorString("%v", err))
   172  	}
   173  
   174  	return dockerCmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
   175  }
   176  
   177  func debugPrint(format string, args ...interface{}) {
   178  	if debugMode {
   179  		ErrPrintf(color.HiBlackString(format+"\n", args...))
   180  	}
   181  }
   182  
   183  func runCommands(commands []string) error {
   184  	for _, script := range commands {
   185  		cmd, tempFile, err := utils.GetCommandFromString(script)
   186  		if err != nil {
   187  			return err
   188  		}
   189  		if tempFile != "" {
   190  			defer func() { os.Remove(tempFile) }()
   191  		}
   192  		cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
   193  		if err := cmd.Run(); err != nil {
   194  			return err
   195  		}
   196  	}
   197  	return nil
   198  }
   199  
   200  // Returns the image name to use
   201  // If docker-image-build option has been set, an image is dynamically built and the resulting image digest is returned
   202  func getImage() (name string) {
   203  	name = config.GetImageName()
   204  	if !strings.Contains(name, ":") {
   205  		name += ":latest"
   206  	}
   207  
   208  	for i, ib := range config.imageBuildConfigs {
   209  		var temp, folder, dockerFile string
   210  		var out *os.File
   211  		if ib.Folder == "" {
   212  			// There is no explicit folder, so we create a temporary folder to store the docker file
   213  			temp = must(ioutil.TempDir("", "tgf-dockerbuild")).(string)
   214  			out = must(os.Create(filepath.Join(temp, dockerfilePattern))).(*os.File)
   215  			folder = temp
   216  		} else {
   217  			if ib.Instructions != "" {
   218  				out = must(ioutil.TempFile(ib.Dir(), dockerfilePattern)).(*os.File)
   219  				temp = out.Name()
   220  				dockerFile = temp
   221  			}
   222  			folder = ib.Dir()
   223  		}
   224  
   225  		if out != nil {
   226  			ib.Instructions = fmt.Sprintf("FROM %s\n%s\n", name, ib.Instructions)
   227  			must(fmt.Fprintf(out, ib.Instructions))
   228  			must(out.Close())
   229  		}
   230  
   231  		if temp != "" {
   232  			// A temporary file of folder has been created, we register functions to ensure proper cleanup
   233  			cleanup := func() { os.Remove(temp) }
   234  			defer cleanup()
   235  			c := make(chan os.Signal, 1)
   236  			signal.Notify(c, os.Interrupt, syscall.SIGTERM)
   237  			go func() {
   238  				<-c
   239  				Println("\nRemoving file", dockerFile)
   240  				cleanup()
   241  				panic(errorString("Execution interrupted by user: %v", c))
   242  			}()
   243  		}
   244  
   245  		name = name + "-" + ib.GetTag()
   246  		if image, tag := Split2(name, ":"); len(tag) > maxDockerTagLength {
   247  			name = image + ":" + tag[0:maxDockerTagLength]
   248  		}
   249  		if refresh || getImageHash(name) != ib.hash() {
   250  			label := fmt.Sprintf("hash=%s", ib.hash())
   251  			args := []string{"build", ".", "-f", dockerfilePattern, "--quiet", "--force-rm", "--label", label}
   252  			if i == 0 && refresh && !useLocalImage {
   253  				args = append(args, "--pull")
   254  			}
   255  			if dockerFile != "" {
   256  				args = append(args, "--file")
   257  				args = append(args, filepath.Base(dockerFile))
   258  			}
   259  
   260  			args = append(args, "--tag", name)
   261  			buildCmd := exec.Command("docker", args...)
   262  
   263  			debugPrint("%s", strings.Join(buildCmd.Args, " "))
   264  			if ib.Instructions != "" {
   265  				debugPrint("%s", ib.Instructions)
   266  			}
   267  			buildCmd.Stderr = os.Stderr
   268  			buildCmd.Dir = folder
   269  			must(buildCmd.Output())
   270  			prune()
   271  		}
   272  	}
   273  
   274  	return
   275  }
   276  
   277  func prune(images ...string) {
   278  	cli, ctx := getDockerClient()
   279  	if len(images) > 0 {
   280  		current := fmt.Sprintf(">=%s", GetActualImageVersion())
   281  		for _, image := range images {
   282  			filters := filters.NewArgs()
   283  			filters.Add("reference", image)
   284  			if images, err := cli.ImageList(ctx, types.ImageListOptions{Filters: filters}); err == nil {
   285  				for _, image := range images {
   286  					actual := getActualImageVersionFromImageID(image.ID)
   287  					if actual == "" {
   288  						for _, tag := range image.RepoTags {
   289  							matches, _ := utils.MultiMatch(tag, reImage)
   290  							if version := matches["version"]; version != "" {
   291  								if len(version) > len(actual) {
   292  									actual = version
   293  								}
   294  							}
   295  						}
   296  					}
   297  					upToDate, err := CheckVersionRange(actual, current)
   298  					if err != nil {
   299  						ErrPrintln("Check version for %s vs%s: %v", actual, current, err)
   300  					} else if !upToDate {
   301  						for _, tag := range image.RepoTags {
   302  							deleteImage(tag)
   303  						}
   304  					}
   305  				}
   306  			}
   307  		}
   308  	}
   309  
   310  	danglingFilters := filters.NewArgs()
   311  	danglingFilters.Add("dangling", "true")
   312  	must(cli.ImagesPrune(ctx, danglingFilters))
   313  	must(cli.ContainersPrune(ctx, filters.Args{}))
   314  }
   315  
   316  func deleteImage(id string) {
   317  	cli, ctx := getDockerClient()
   318  	items, err := cli.ImageRemove(ctx, id, types.ImageRemoveOptions{})
   319  	if err != nil {
   320  		printError((err.Error()))
   321  	}
   322  	for _, item := range items {
   323  		if item.Untagged != "" {
   324  			ErrPrintf("Untagged %s\n", item.Untagged)
   325  		}
   326  		if item.Deleted != "" {
   327  			ErrPrintf("Deleted %s\n", item.Deleted)
   328  		}
   329  	}
   330  }
   331  
   332  // GetActualImageVersion returns the real image version stored in the environment variable TGF_IMAGE_VERSION
   333  func GetActualImageVersion() string {
   334  	return getActualImageVersionInternal(getImage())
   335  }
   336  
   337  func getDockerClient() (*client.Client, context.Context) {
   338  	if dockerClient == nil {
   339  		os.Setenv("DOCKER_API_VERSION", minimumDockerVersion)
   340  		dockerClient = must(client.NewEnvClient()).(*client.Client)
   341  		dockerContext = context.Background()
   342  	}
   343  	return dockerClient, dockerContext
   344  }
   345  
   346  var dockerClient *client.Client
   347  var dockerContext context.Context
   348  
   349  func getImageSummary(imageName string) *types.ImageSummary {
   350  	cli, ctx := getDockerClient()
   351  	// Find image
   352  	filters := filters.NewArgs()
   353  	filters.Add("reference", imageName)
   354  	images, err := cli.ImageList(ctx, types.ImageListOptions{Filters: filters})
   355  	if err != nil || len(images) != 1 {
   356  		return nil
   357  	}
   358  	return &images[0]
   359  }
   360  
   361  func getActualImageVersionInternal(imageName string) string {
   362  	if image := getImageSummary(imageName); image != nil {
   363  		return getActualImageVersionFromImageID(image.ID)
   364  	}
   365  	return ""
   366  }
   367  
   368  func getImageHash(imageName string) string {
   369  	if image := getImageSummary(imageName); image != nil {
   370  		return image.Labels["hash"]
   371  	}
   372  	return ""
   373  }
   374  
   375  func getActualImageVersionFromImageID(imageID string) string {
   376  	cli, ctx := getDockerClient()
   377  	inspect, _, err := cli.ImageInspectWithRaw(ctx, imageID)
   378  	if err != nil {
   379  		panic(err)
   380  	}
   381  	for _, v := range inspect.ContainerConfig.Env {
   382  		values := strings.SplitN(v, "=", 2)
   383  		if values[0] == tgfImageVersion {
   384  			return values[1]
   385  		}
   386  	}
   387  	// We do not found an environment variable with the version in the images
   388  	return ""
   389  }
   390  
   391  func checkImage(image string) bool {
   392  	var out bytes.Buffer
   393  	dockerCmd := exec.Command("docker", []string{"images", "-q", image}...)
   394  	dockerCmd.Stdout = &out
   395  	dockerCmd.Run()
   396  	return out.String() != ""
   397  }
   398  
   399  // ECR Regex: https://regex101.com/r/GRxU06/1
   400  var reECR = regexp.MustCompile(`(?P<account>[0-9]+)\.dkr\.ecr\.(?P<region>[a-z0-9\-]+)\.amazonaws\.com`)
   401  
   402  func refreshImage(image string) {
   403  	refresh = true // Setting this to true will ensure that dependant built images will also be refreshed
   404  
   405  	if useLocalImage {
   406  		ErrPrintf("Not refreshing %v because `local-image` is set\n", image)
   407  		return
   408  	}
   409  
   410  	ErrPrintf("Checking if there is a newer version of docker image %v\n", image)
   411  	err := getDockerUpdateCmd(image).Run()
   412  	if err != nil {
   413  		matches, _ := utils.MultiMatch(image, reECR)
   414  		account, accountOk := matches["account"]
   415  		region, regionOk := matches["region"]
   416  		if accountOk && regionOk && awsConfigExist() {
   417  			ErrPrintf("Failed to pull %v. It is an ECR image, trying again after a login.\n", image)
   418  			loginToECR(account, region)
   419  			must(getDockerUpdateCmd(image).Run())
   420  		} else {
   421  			panic(err)
   422  		}
   423  	}
   424  	touchImageRefresh(image)
   425  	ErrPrintln()
   426  }
   427  
   428  func loginToECR(account string, region string) {
   429  	awsSession := session.Must(session.NewSessionWithOptions(session.Options{SharedConfigState: session.SharedConfigEnable}))
   430  	svc := ecr.New(awsSession, &aws.Config{Region: aws.String(region)})
   431  	requestInput := &ecr.GetAuthorizationTokenInput{RegistryIds: []*string{aws.String(account)}}
   432  	result := must(svc.GetAuthorizationToken(requestInput)).(*ecr.GetAuthorizationTokenOutput)
   433  
   434  	decodedLogin := string(must(base64.StdEncoding.DecodeString(*result.AuthorizationData[0].AuthorizationToken)).([]byte))
   435  	dockerUpdateCmd := exec.Command("docker", "login", "-u", strings.Split(decodedLogin, ":")[0],
   436  		"-p", strings.Split(decodedLogin, ":")[1], *result.AuthorizationData[0].ProxyEndpoint)
   437  	must(dockerUpdateCmd.Run())
   438  }
   439  
   440  func getDockerUpdateCmd(image string) *exec.Cmd {
   441  	dockerUpdateCmd := exec.Command("docker", "pull", image)
   442  	dockerUpdateCmd.Stdout, dockerUpdateCmd.Stderr = os.Stderr, os.Stderr
   443  	return dockerUpdateCmd
   444  }
   445  
   446  func getEnviron(noHome bool) (result []string) {
   447  	for _, env := range os.Environ() {
   448  		split := strings.Split(env, "=")
   449  		varName := strings.TrimSpace(split[0])
   450  		varUpper := strings.ToUpper(varName)
   451  		if varName == "" || strings.Contains(varUpper, "PATH") {
   452  			continue
   453  		}
   454  
   455  		if runtime.GOOS == "windows" {
   456  			if strings.Contains(strings.ToUpper(split[1]), `C:\`) || strings.Contains(varUpper, "WIN") {
   457  				continue
   458  			}
   459  		}
   460  
   461  		switch varName {
   462  		case
   463  			"_", "PWD", "OLDPWD", "TMPDIR",
   464  			"PROMPT", "SHELL", "SH", "ZSH", "HOME",
   465  			"LANG", "LC_CTYPE", "DISPLAY", "TERM":
   466  		default:
   467  			result = append(result, "-e")
   468  			result = append(result, split[0])
   469  		}
   470  	}
   471  	return
   472  }
   473  
   474  // This function set the path converter function
   475  // For old Windows version still using docker-machine and VirtualBox,
   476  // it transforms the C:\ to /C/.
   477  func getPathConversionFunction() func(string) string {
   478  	if runtime.GOOS != "windows" || os.Getenv("DOCKER_MACHINE_NAME") == "" {
   479  		return func(path string) string { return path }
   480  	}
   481  
   482  	return func(path string) string {
   483  		return fmt.Sprintf("/%s%s", strings.ToUpper(path[:1]), path[2:])
   484  	}
   485  }
   486  
   487  var convertDrive = getPathConversionFunction()
   488  
   489  var windowsMessage = `
   490  You may have to share your drives with your Docker virtual machine to make them accessible.
   491  
   492  On Windows 10+ using Hyper-V to run Docker, simply right click on Docker icon in your tray and
   493  choose "Settings", then go to "Shared Drives" and enable the share for the drives you want to 
   494  be accessible to your dockers.
   495  
   496  On previous version using VirtualBox, start the VirtualBox application and add shared drives
   497  for all drives you want to make shareable with your dockers.
   498  
   499  IMPORTANT, to make your drives accessible to tgf, you have to give them uppercase name corresponding
   500  to the drive letter:
   501  	C:\ ==> /C
   502  	D:\ ==> /D
   503  	...
   504  	Z:\ ==> /Z
   505  `