github.com/grahambrereton-form3/tilt@v0.10.18/internal/tiltfile/docker.go (about) 1 package tiltfile 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "path/filepath" 7 "strings" 8 9 "github.com/docker/distribution/reference" 10 "github.com/pkg/errors" 11 "go.starlark.net/starlark" 12 13 "github.com/windmilleng/tilt/internal/container" 14 "github.com/windmilleng/tilt/internal/dockerfile" 15 "github.com/windmilleng/tilt/internal/ospath" 16 "github.com/windmilleng/tilt/internal/sliceutils" 17 "github.com/windmilleng/tilt/internal/tiltfile/io" 18 "github.com/windmilleng/tilt/internal/tiltfile/starkit" 19 "github.com/windmilleng/tilt/internal/tiltfile/value" 20 "github.com/windmilleng/tilt/pkg/model" 21 ) 22 23 var fastBuildDeletedErr = fmt.Errorf("fast_build is no longer supported. live_update provides the same functionality with less set-up: https://docs.tilt.dev/live_update_tutorial.html . If you run into problems, let us know: https://tilt.dev/contact") 24 25 type dockerImage struct { 26 tiltfilePath string 27 configurationRef container.RefSelector 28 deploymentRef reference.Named 29 cachePaths []string 30 matchInEnvVars bool 31 ignores []string 32 onlys []string 33 entrypoint model.Cmd // optional: if specified, we override the image entrypoint/k8s command with this 34 targetStage string // optional: if specified, we build a particular target in the dockerfile 35 36 dbDockerfilePath string 37 dbDockerfile dockerfile.Dockerfile 38 dbBuildPath string 39 dbBuildArgs model.DockerBuildArgs 40 customCommand string 41 customDeps []string 42 customTag string 43 44 // Whether this has been matched up yet to a deploy resource. 45 matched bool 46 47 dependencyIDs []model.TargetID 48 disablePush bool 49 50 liveUpdate model.LiveUpdate 51 } 52 53 func (d *dockerImage) ID() model.TargetID { 54 return model.ImageID(d.configurationRef) 55 } 56 57 type dockerImageBuildType int 58 59 const ( 60 UnknownBuild = iota 61 DockerBuild 62 CustomBuild 63 ) 64 65 func (d *dockerImage) Type() dockerImageBuildType { 66 if d.dbBuildPath != "" { 67 return DockerBuild 68 } 69 70 if d.customCommand != "" { 71 return CustomBuild 72 } 73 74 return UnknownBuild 75 } 76 77 func (s *tiltfileState) dockerBuild(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 78 var dockerRef, entrypoint, targetStage string 79 var contextVal, dockerfilePathVal, buildArgs, dockerfileContentsVal, cacheVal, liveUpdateVal, ignoreVal, onlyVal starlark.Value 80 var matchInEnvVars bool 81 if err := s.unpackArgs(fn.Name(), args, kwargs, 82 "ref", &dockerRef, 83 "context", &contextVal, 84 "build_args?", &buildArgs, 85 "dockerfile?", &dockerfilePathVal, 86 "dockerfile_contents?", &dockerfileContentsVal, 87 "cache?", &cacheVal, 88 "live_update?", &liveUpdateVal, 89 "match_in_env_vars?", &matchInEnvVars, 90 "ignore?", &ignoreVal, 91 "only?", &onlyVal, 92 "entrypoint?", &entrypoint, 93 "target?", &targetStage, 94 ); err != nil { 95 return nil, err 96 } 97 98 ref, err := container.ParseNamed(dockerRef) 99 if err != nil { 100 return nil, fmt.Errorf("Argument 1 (ref): can't parse %q: %v", dockerRef, err) 101 } 102 103 if contextVal == nil { 104 return nil, fmt.Errorf("Argument 2 (context): empty but is required") 105 } 106 context, err := value.ValueToAbsPath(thread, contextVal) 107 if err != nil { 108 return nil, err 109 } 110 111 sba, err := value.ValueToStringMap(buildArgs) 112 if err != nil { 113 return nil, fmt.Errorf("Argument 3 (build_args): %v", err) 114 } 115 116 dockerfilePath := filepath.Join(context, "Dockerfile") 117 var dockerfileContents string 118 if dockerfileContentsVal != nil && dockerfilePathVal != nil { 119 return nil, fmt.Errorf("Cannot specify both dockerfile and dockerfile_contents keyword arguments") 120 } 121 if dockerfileContentsVal != nil { 122 switch v := dockerfileContentsVal.(type) { 123 case io.Blob: 124 dockerfileContents = v.Text 125 case starlark.String: 126 dockerfileContents = v.GoString() 127 default: 128 return nil, fmt.Errorf("Argument (dockerfile_contents): must be string or blob.") 129 } 130 } else if dockerfilePathVal != nil { 131 dockerfilePath, err = value.ValueToAbsPath(thread, dockerfilePathVal) 132 if err != nil { 133 return nil, err 134 } 135 136 bs, err := io.ReadFile(thread, dockerfilePath) 137 if err != nil { 138 return nil, errors.Wrap(err, "error reading dockerfile") 139 } 140 dockerfileContents = string(bs) 141 } else { 142 bs, err := io.ReadFile(thread, dockerfilePath) 143 if err != nil { 144 return nil, errors.Wrapf(err, "error reading dockerfile") 145 } 146 dockerfileContents = string(bs) 147 } 148 149 cachePaths, err := s.cachePathsFromSkylarkValue(cacheVal) 150 if err != nil { 151 return nil, err 152 } 153 154 liveUpdate, err := s.liveUpdateFromSteps(thread, liveUpdateVal) 155 if err != nil { 156 return nil, errors.Wrap(err, "live_update") 157 } 158 159 ignores, err := parseValuesToStrings(ignoreVal, "ignore") 160 if err != nil { 161 return nil, err 162 } 163 164 onlys, err := s.parseOnly(onlyVal) 165 if err != nil { 166 return nil, err 167 } 168 169 var entrypointCmd model.Cmd 170 if entrypoint != "" { 171 entrypointCmd = model.ToShellCmd(entrypoint) 172 } 173 174 r := &dockerImage{ 175 tiltfilePath: starkit.CurrentExecPath(thread), 176 dbDockerfilePath: dockerfilePath, 177 dbDockerfile: dockerfile.Dockerfile(dockerfileContents), 178 dbBuildPath: context, 179 configurationRef: container.NewRefSelector(ref), 180 dbBuildArgs: sba, 181 cachePaths: cachePaths, 182 liveUpdate: liveUpdate, 183 matchInEnvVars: matchInEnvVars, 184 ignores: ignores, 185 onlys: onlys, 186 entrypoint: entrypointCmd, 187 targetStage: targetStage, 188 } 189 err = s.buildIndex.addImage(r) 190 if err != nil { 191 return nil, err 192 } 193 194 // NOTE(maia): docker_build returned a fast build that users can optionally 195 // populate; now it just errors 196 fb := &fastBuild{} 197 return fb, nil 198 } 199 200 func (s *tiltfileState) parseOnly(val starlark.Value) ([]string, error) { 201 paths, err := parseValuesToStrings(val, "only") 202 if err != nil { 203 return nil, err 204 } 205 206 for _, p := range paths { 207 // We want to forbid file globs due to these issues: 208 // https://github.com/windmilleng/tilt/issues/1982 209 // https://github.com/moby/moby/issues/30018 210 if strings.Contains(p, "*") { 211 return nil, fmt.Errorf("'only' does not support '*' file globs. Must be a real path: %s", p) 212 } 213 } 214 return paths, nil 215 } 216 217 func (s *tiltfileState) customBuild(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 218 var dockerRef string 219 var command string 220 var deps *starlark.List 221 var tag string 222 var disablePush bool 223 var liveUpdateVal, ignoreVal starlark.Value 224 var matchInEnvVars bool 225 var entrypoint string 226 227 err := s.unpackArgs(fn.Name(), args, kwargs, 228 "ref", &dockerRef, 229 "command", &command, 230 "deps", &deps, 231 "tag?", &tag, 232 "disable_push?", &disablePush, 233 "live_update?", &liveUpdateVal, 234 "match_in_env_vars?", &matchInEnvVars, 235 "ignore?", &ignoreVal, 236 "entrypoint?", &entrypoint, 237 ) 238 if err != nil { 239 return nil, err 240 } 241 242 ref, err := container.ParseNamed(dockerRef) 243 if err != nil { 244 return nil, fmt.Errorf("Argument 1 (ref): can't parse %q: %v", dockerRef, err) 245 } 246 247 if command == "" { 248 return nil, fmt.Errorf("Argument 2 (command) can't be empty") 249 } 250 251 if deps == nil || deps.Len() == 0 { 252 return nil, fmt.Errorf("Argument 3 (deps) can't be empty") 253 } 254 255 var localDeps []string 256 iter := deps.Iterate() 257 defer iter.Done() 258 var v starlark.Value 259 for iter.Next(&v) { 260 p, err := value.ValueToAbsPath(thread, v) 261 if err != nil { 262 return nil, fmt.Errorf("Argument 3 (deps): %v", err) 263 } 264 localDeps = append(localDeps, p) 265 } 266 267 liveUpdate, err := s.liveUpdateFromSteps(thread, liveUpdateVal) 268 if err != nil { 269 return nil, errors.Wrap(err, "live_update") 270 } 271 272 ignores, error := parseValuesToStrings(ignoreVal, "ignore") 273 if error != nil { 274 return nil, error 275 } 276 277 var entrypointCmd model.Cmd 278 if entrypoint != "" { 279 entrypointCmd = model.ToShellCmd(entrypoint) 280 } 281 282 img := &dockerImage{ 283 configurationRef: container.NewRefSelector(ref), 284 customCommand: command, 285 customDeps: localDeps, 286 customTag: tag, 287 disablePush: disablePush, 288 liveUpdate: liveUpdate, 289 matchInEnvVars: matchInEnvVars, 290 ignores: ignores, 291 entrypoint: entrypointCmd, 292 } 293 294 err = s.buildIndex.addImage(img) 295 if err != nil { 296 return nil, err 297 } 298 299 return &customBuild{s: s, img: img}, nil 300 } 301 302 type customBuild struct { 303 s *tiltfileState 304 img *dockerImage 305 } 306 307 var _ starlark.Value = &customBuild{} 308 309 func (b *customBuild) String() string { 310 return fmt.Sprintf("custom_build(%q)", b.img.configurationRef.String()) 311 } 312 313 func (b *customBuild) Type() string { 314 return "custom_build" 315 } 316 317 func (b *customBuild) Freeze() {} 318 319 func (b *customBuild) Truth() starlark.Bool { 320 return true 321 } 322 323 func (b *customBuild) Hash() (uint32, error) { 324 return 0, fmt.Errorf("unhashable type: custom_build") 325 } 326 327 func (b *customBuild) Attr(name string) (starlark.Value, error) { 328 switch name { 329 case "add_fast_build": 330 return nil, fastBuildDeletedErr 331 default: 332 return nil, nil 333 } 334 } 335 336 func (b *customBuild) AttrNames() []string { 337 return []string{} 338 } 339 340 func parseValuesToStrings(value starlark.Value, param string) ([]string, error) { 341 342 tempIgnores := starlarkValueOrSequenceToSlice(value) 343 var ignores []string 344 for _, v := range tempIgnores { 345 switch val := v.(type) { 346 case starlark.String: // for singular string 347 goString := val.GoString() 348 if strings.Contains(goString, "\n") { 349 return nil, fmt.Errorf(param+" cannot contain newlines; found "+param+": %q", goString) 350 } 351 ignores = append(ignores, val.GoString()) 352 default: 353 return nil, fmt.Errorf(param+" must be a string or a sequence of strings; found a %T", val) 354 } 355 } 356 return ignores, nil 357 358 } 359 func (s *tiltfileState) fastBuild(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 360 return nil, fastBuildDeletedErr 361 } 362 363 func (s *tiltfileState) cachePathsFromSkylarkValue(val starlark.Value) ([]string, error) { 364 if val == nil { 365 return nil, nil 366 } 367 cachePaths := starlarkValueOrSequenceToSlice(val) 368 369 var ret []string 370 for _, v := range cachePaths { 371 str, ok := v.(starlark.String) 372 if !ok { 373 return nil, fmt.Errorf("cache param %v is a %T; must be a string", v, v) 374 } 375 ret = append(ret, string(str)) 376 } 377 return ret, nil 378 } 379 380 // fastBuild exists just to error 381 type fastBuild struct { 382 } 383 384 var _ starlark.Value = &fastBuild{} 385 386 func (b *fastBuild) String() string { 387 return "fast_build(%q)" 388 } 389 390 func (b *fastBuild) Type() string { 391 return "fast_build" 392 } 393 394 func (b *fastBuild) Freeze() {} 395 396 func (b *fastBuild) Truth() starlark.Bool { 397 return true 398 } 399 400 func (b *fastBuild) Hash() (uint32, error) { 401 return 0, fmt.Errorf("unhashable type: fast_build") 402 } 403 404 func (b *fastBuild) Attr(name string) (starlark.Value, error) { 405 return nil, fastBuildDeletedErr 406 } 407 408 func (b *fastBuild) AttrNames() []string { 409 return []string{} 410 } 411 412 func isGitRepoBase(path string) bool { 413 return ospath.IsDir(filepath.Join(path, ".git")) 414 } 415 416 func reposForPaths(paths []string) []model.LocalGitRepo { 417 var result []model.LocalGitRepo 418 repoSet := map[string]bool{} 419 420 for _, path := range paths { 421 isRepoBase := isGitRepoBase(path) 422 if !isRepoBase || repoSet[path] { 423 continue 424 } 425 426 repoSet[path] = true 427 result = append(result, model.LocalGitRepo{ 428 LocalPath: path, 429 }) 430 } 431 432 return result 433 } 434 435 func (s *tiltfileState) reposForImage(image *dockerImage) []model.LocalGitRepo { 436 var paths []string 437 paths = append(paths, 438 image.dbDockerfilePath, 439 image.dbBuildPath, 440 image.tiltfilePath) 441 442 return reposForPaths(paths) 443 } 444 445 func (s *tiltfileState) defaultRegistry(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 446 if s.defaultRegistryHost != "" { 447 return starlark.None, errors.New("default registry already defined") 448 } 449 450 var dr string 451 if err := s.unpackArgs(fn.Name(), args, kwargs, "name", &dr); err != nil { 452 return nil, err 453 } 454 455 s.defaultRegistryHost = container.Registry(dr) 456 457 return starlark.None, nil 458 } 459 460 func (s *tiltfileState) dockerignoresFromPathsAndContextFilters(paths []string, ignores []string, onlys []string) []model.Dockerignore { 461 var result []model.Dockerignore 462 dupeSet := map[string]bool{} 463 ignoreContents := ignoresToDockerignoreContents(ignores) 464 onlyContents := onlysToDockerignoreContents(onlys) 465 466 for _, path := range paths { 467 if path == "" || dupeSet[path] { 468 continue 469 } 470 dupeSet[path] = true 471 472 if !ospath.IsDir(path) { 473 continue 474 } 475 476 if ignoreContents != "" { 477 result = append(result, model.Dockerignore{ 478 LocalPath: path, 479 Contents: ignoreContents, 480 }) 481 } 482 483 if onlyContents != "" { 484 result = append(result, model.Dockerignore{ 485 LocalPath: path, 486 Contents: onlyContents, 487 }) 488 } 489 490 diFile := filepath.Join(path, ".dockerignore") 491 s.postExecReadFiles = sliceutils.AppendWithoutDupes(s.postExecReadFiles, diFile) 492 493 contents, err := ioutil.ReadFile(diFile) 494 if err != nil { 495 continue 496 } 497 498 result = append(result, model.Dockerignore{ 499 LocalPath: path, 500 Contents: string(contents), 501 }) 502 } 503 504 return result 505 } 506 507 func ignoresToDockerignoreContents(ignores []string) string { 508 var output strings.Builder 509 510 for _, ignore := range ignores { 511 output.WriteString(ignore) 512 output.WriteString("\n") 513 } 514 515 return output.String() 516 } 517 518 func onlysToDockerignoreContents(onlys []string) string { 519 if len(onlys) == 0 { 520 return "" 521 } 522 var output strings.Builder 523 output.WriteString("**\n") 524 525 for _, ignore := range onlys { 526 output.WriteString("!") 527 output.WriteString(ignore) 528 output.WriteString("\n") 529 } 530 531 return output.String() 532 } 533 534 func (s *tiltfileState) dockerignoresForImage(image *dockerImage) []model.Dockerignore { 535 var paths []string 536 switch image.Type() { 537 case DockerBuild: 538 paths = append(paths, image.dbBuildPath) 539 case CustomBuild: 540 paths = append(paths, image.customDeps...) 541 } 542 return s.dockerignoresFromPathsAndContextFilters(paths, image.ignores, image.onlys) 543 }