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  }