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 `