github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/docker.go (about)

     1  package tiltfile
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/distribution/reference"
    12  	"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
    13  	"github.com/pkg/errors"
    14  	"go.starlark.net/starlark"
    15  
    16  	"github.com/tilt-dev/tilt/internal/container"
    17  	"github.com/tilt-dev/tilt/internal/dockerfile"
    18  	"github.com/tilt-dev/tilt/internal/ospath"
    19  	"github.com/tilt-dev/tilt/internal/sliceutils"
    20  	"github.com/tilt-dev/tilt/internal/tiltfile/io"
    21  	"github.com/tilt-dev/tilt/internal/tiltfile/starkit"
    22  	"github.com/tilt-dev/tilt/internal/tiltfile/value"
    23  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    24  	"github.com/tilt-dev/tilt/pkg/model"
    25  )
    26  
    27  const dockerPlatformEnv = "DOCKER_DEFAULT_PLATFORM"
    28  
    29  var cacheObsoleteWarning = "docker_build(cache=...) is obsolete, and currently a no-op.\n" +
    30  	"You should switch to live_update to optimize your builds."
    31  
    32  type dockerImage struct {
    33  	buildType        dockerImageBuildType
    34  	configurationRef container.RefSelector
    35  	matchInEnvVars   bool
    36  	sshSpecs         []string
    37  	secretSpecs      []string
    38  	ignores          []string
    39  	onlys            []string
    40  	entrypoint       model.Cmd // optional: if specified, we override the image entrypoint/k8s command with this
    41  	targetStage      string    // optional: if specified, we build a particular target in the dockerfile
    42  	network          string
    43  	extraTags        []string // Extra tags added at build-time.
    44  	cacheFrom        []string
    45  	pullParent       bool
    46  	platform         string
    47  
    48  	// Overrides the container args. Used as an escape hatch in case people want the old entrypoint behavior.
    49  	// See discussion here:
    50  	// https://github.com/tilt-dev/tilt/pull/2933
    51  	overrideArgs *v1alpha1.ImageMapOverrideArgs
    52  
    53  	dbDockerfilePath string
    54  	dbDockerfile     dockerfile.Dockerfile
    55  
    56  	// dbBuildPath may be empty if the user is building from a URL
    57  	dbBuildPath   string
    58  	dbBuildArgs   []string
    59  	customCommand model.Cmd
    60  	customDeps    []string
    61  	customTag     string
    62  	customImgDeps []reference.Named
    63  
    64  	// Whether this has been matched up yet to a deploy resource.
    65  	matched bool
    66  
    67  	imageMapDeps []string
    68  
    69  	// Only applicable to custom_build
    70  	disablePush       bool
    71  	skipsLocalDocker  bool
    72  	outputsImageRefTo string
    73  
    74  	liveUpdate v1alpha1.LiveUpdateSpec
    75  
    76  	// TODO(milas): we should have a better way of passing the Tiltfile path around during resource assembly
    77  	tiltfilePath string
    78  
    79  	dockerComposeService          string
    80  	dockerComposeLocalVolumePaths []string
    81  
    82  	extraHosts []string
    83  }
    84  
    85  func (d *dockerImage) ID() model.TargetID {
    86  	return model.ImageID(d.configurationRef)
    87  }
    88  
    89  func (d *dockerImage) ImageMapName() string {
    90  	return string(model.ImageID(d.configurationRef).Name)
    91  }
    92  
    93  type dockerImageBuildType int
    94  
    95  const (
    96  	UnknownBuild dockerImageBuildType = iota
    97  	DockerBuild
    98  	CustomBuild
    99  	DockerComposeBuild
   100  )
   101  
   102  func (d *dockerImage) Type() dockerImageBuildType {
   103  	return d.buildType
   104  }
   105  
   106  func (s *tiltfileState) dockerBuild(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   107  	var dockerRef, targetStage string
   108  	contextVal := value.NewLocalPathUnpacker(thread)
   109  	dockerfilePathVal := value.NewLocalPathUnpacker(thread)
   110  	var dockerfileContentsVal,
   111  		cacheVal,
   112  		liveUpdateVal,
   113  		ignoreVal,
   114  		onlyVal,
   115  		entrypoint starlark.Value
   116  	var buildArgs value.StringStringMap
   117  	var network, platform value.Stringable
   118  	var ssh, secret, extraTags, cacheFrom, extraHosts value.StringOrStringList
   119  	var matchInEnvVars, pullParent bool
   120  	var overrideArgsVal starlark.Sequence
   121  	if err := s.unpackArgs(fn.Name(), args, kwargs,
   122  		"ref", &dockerRef,
   123  		"context", &contextVal,
   124  		"build_args?", &buildArgs,
   125  		"dockerfile??", &dockerfilePathVal,
   126  		"dockerfile_contents?", &dockerfileContentsVal,
   127  		"cache?", &cacheVal,
   128  		"live_update?", &liveUpdateVal,
   129  		"match_in_env_vars?", &matchInEnvVars,
   130  		"ignore?", &ignoreVal,
   131  		"only?", &onlyVal,
   132  		"entrypoint?", &entrypoint,
   133  		"container_args?", &overrideArgsVal,
   134  		"target?", &targetStage,
   135  		"ssh?", &ssh,
   136  		"secret?", &secret,
   137  		"network?", &network,
   138  		"extra_tag?", &extraTags,
   139  		"cache_from?", &cacheFrom,
   140  		"pull?", &pullParent,
   141  		"platform?", &platform,
   142  		"extra_hosts?", &extraHosts,
   143  	); err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	ref, err := container.ParseNamed(dockerRef)
   148  	if err != nil {
   149  		return nil, fmt.Errorf("Argument 1 (ref): can't parse %q: %v", dockerRef, err)
   150  	}
   151  
   152  	context := contextVal.Value
   153  	dockerfilePath := filepath.Join(context, "Dockerfile")
   154  	var dockerfileContents string
   155  	if dockerfileContentsVal != nil && dockerfilePathVal.IsSet {
   156  		return nil, fmt.Errorf("Cannot specify both dockerfile and dockerfile_contents keyword arguments")
   157  	}
   158  	if dockerfileContentsVal != nil {
   159  		switch v := dockerfileContentsVal.(type) {
   160  		case io.Blob:
   161  			dockerfileContents = v.Text
   162  		case starlark.String:
   163  			dockerfileContents = v.GoString()
   164  		default:
   165  			return nil, fmt.Errorf("Argument (dockerfile_contents): must be string or blob.")
   166  		}
   167  	} else if dockerfilePathVal.IsSet {
   168  		dockerfilePath = dockerfilePathVal.Value
   169  		bs, err := io.ReadFile(thread, dockerfilePath)
   170  		if err != nil {
   171  			return nil, errors.Wrap(err, "error reading dockerfile")
   172  		}
   173  		dockerfileContents = string(bs)
   174  	} else {
   175  		bs, err := io.ReadFile(thread, dockerfilePath)
   176  		if err != nil {
   177  			return nil, errors.Wrapf(err, "error reading dockerfile")
   178  		}
   179  		dockerfileContents = string(bs)
   180  	}
   181  
   182  	if cacheVal != nil {
   183  		s.logger.Warnf("%s", cacheObsoleteWarning)
   184  	}
   185  
   186  	liveUpdate, err := s.liveUpdateFromSteps(thread, liveUpdateVal)
   187  	if err != nil {
   188  		return nil, errors.Wrap(err, "live_update")
   189  	}
   190  
   191  	ignores, err := parseValuesToStrings(ignoreVal, "ignore")
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	onlys, err := s.parseOnly(onlyVal)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	entrypointCmd, err := value.ValueToUnixCmd(thread, entrypoint, nil, nil)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	var overrideArgs *v1alpha1.ImageMapOverrideArgs
   207  	if overrideArgsVal != nil {
   208  		args, err := value.SequenceToStringSlice(overrideArgsVal)
   209  		if err != nil {
   210  			return nil, fmt.Errorf("Argument 'container_args': %v", err)
   211  		}
   212  		overrideArgs = &v1alpha1.ImageMapOverrideArgs{Args: args}
   213  	}
   214  
   215  	for _, extraTag := range extraTags.Values {
   216  		_, err := container.ParseNamed(extraTag)
   217  		if err != nil {
   218  			return nil, fmt.Errorf("Argument extra_tag=%q not a valid image reference: %v", extraTag, err)
   219  		}
   220  	}
   221  
   222  	if platform.Value == "" {
   223  		// for compatibility with Docker CLI, support the env var fallback
   224  		// see https://docs.docker.com/engine/reference/commandline/cli/#environment-variables
   225  		platform.Value = os.Getenv(dockerPlatformEnv)
   226  	}
   227  
   228  	buildArgsList := []string{}
   229  	for k, v := range buildArgs.AsMap() {
   230  		if v == "" {
   231  			buildArgsList = append(buildArgsList, k)
   232  		} else {
   233  			buildArgsList = append(buildArgsList, fmt.Sprintf("%s=%s", k, v))
   234  		}
   235  	}
   236  	sort.Strings(buildArgsList)
   237  
   238  	r := &dockerImage{
   239  		buildType:        DockerBuild,
   240  		dbDockerfilePath: dockerfilePath,
   241  		dbDockerfile:     dockerfile.Dockerfile(dockerfileContents),
   242  		dbBuildPath:      context,
   243  		configurationRef: container.NewRefSelector(ref),
   244  		dbBuildArgs:      buildArgsList,
   245  		liveUpdate:       liveUpdate,
   246  		matchInEnvVars:   matchInEnvVars,
   247  		sshSpecs:         ssh.Values,
   248  		secretSpecs:      secret.Values,
   249  		ignores:          ignores,
   250  		onlys:            onlys,
   251  		entrypoint:       entrypointCmd,
   252  		overrideArgs:     overrideArgs,
   253  		targetStage:      targetStage,
   254  		network:          network.Value,
   255  		extraTags:        extraTags.Values,
   256  		cacheFrom:        cacheFrom.Values,
   257  		pullParent:       pullParent,
   258  		platform:         platform.Value,
   259  		tiltfilePath:     starkit.CurrentExecPath(thread),
   260  		extraHosts:       extraHosts.Values,
   261  	}
   262  	err = s.buildIndex.addImage(r)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  
   267  	return starlark.None, nil
   268  }
   269  
   270  func (s *tiltfileState) parseOnly(val starlark.Value) ([]string, error) {
   271  	paths, err := parseValuesToStrings(val, "only")
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  
   276  	for _, p := range paths {
   277  		// We want to forbid file globs due to these issues:
   278  		// https://github.com/tilt-dev/tilt/issues/1982
   279  		// https://github.com/moby/moby/issues/30018
   280  		if strings.Contains(p, "*") {
   281  			return nil, fmt.Errorf("'only' does not support '*' file globs. Must be a real path: %s", p)
   282  		}
   283  	}
   284  	return paths, nil
   285  }
   286  
   287  func (s *tiltfileState) customBuild(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   288  	var dockerRef string
   289  	var commandVal, commandBat, commandBatVal starlark.Value
   290  	deps := value.NewLocalPathListUnpacker(thread)
   291  	var tag string
   292  	var disablePush bool
   293  	var liveUpdateVal, ignoreVal starlark.Value
   294  	var matchInEnvVars bool
   295  	var entrypoint starlark.Value
   296  	var overrideArgsVal starlark.Sequence
   297  	var skipsLocalDocker bool
   298  	var imageDeps value.ImageList
   299  	var env value.StringStringMap
   300  	var dir starlark.Value
   301  	outputsImageRefTo := value.NewLocalPathUnpacker(thread)
   302  
   303  	err := s.unpackArgs(fn.Name(), args, kwargs,
   304  		"ref", &dockerRef,
   305  		"command", &commandVal,
   306  		"deps", &deps,
   307  		"tag?", &tag,
   308  		"disable_push?", &disablePush,
   309  		"skips_local_docker?", &skipsLocalDocker,
   310  		"live_update?", &liveUpdateVal,
   311  		"match_in_env_vars?", &matchInEnvVars,
   312  		"ignore?", &ignoreVal,
   313  		"entrypoint?", &entrypoint,
   314  		"container_args?", &overrideArgsVal,
   315  		"command_bat_val", &commandBatVal,
   316  		"outputs_image_ref_to", &outputsImageRefTo,
   317  
   318  		// This is a crappy fix for https://github.com/tilt-dev/tilt/issues/4061
   319  		// so that we don't break things.
   320  		"command_bat", &commandBat,
   321  
   322  		"image_deps", &imageDeps,
   323  		"env?", &env,
   324  		"dir?", &dir,
   325  	)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  
   330  	ref, err := container.ParseNamed(dockerRef)
   331  	if err != nil {
   332  		return nil, fmt.Errorf("Argument 1 (ref): can't parse %q: %v", dockerRef, err)
   333  	}
   334  
   335  	liveUpdate, err := s.liveUpdateFromSteps(thread, liveUpdateVal)
   336  	if err != nil {
   337  		return nil, errors.Wrap(err, "live_update")
   338  	}
   339  
   340  	ignores, err := parseValuesToStrings(ignoreVal, "ignore")
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  
   345  	entrypointCmd, err := value.ValueToUnixCmd(thread, entrypoint, nil, nil)
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  
   350  	var overrideArgs *v1alpha1.ImageMapOverrideArgs
   351  	if overrideArgsVal != nil {
   352  		args, err := value.SequenceToStringSlice(overrideArgsVal)
   353  		if err != nil {
   354  			return nil, fmt.Errorf("Argument 'container_args': %v", err)
   355  		}
   356  		overrideArgs = &v1alpha1.ImageMapOverrideArgs{Args: args}
   357  	}
   358  
   359  	if commandBat == nil {
   360  		commandBat = commandBatVal
   361  	}
   362  
   363  	command, err := value.ValueGroupToCmdHelper(thread, commandVal, commandBat, dir, env)
   364  	if err != nil {
   365  		return nil, fmt.Errorf("Argument 2 (command): %v", err)
   366  	} else if command.Empty() {
   367  		return nil, fmt.Errorf("Argument 2 (command) can't be empty")
   368  	}
   369  
   370  	if tag != "" && outputsImageRefTo.Value != "" {
   371  		return nil, fmt.Errorf("Cannot specify both tag= and outputs_image_ref_to=")
   372  	}
   373  
   374  	img := &dockerImage{
   375  		buildType:         CustomBuild,
   376  		configurationRef:  container.NewRefSelector(ref),
   377  		customCommand:     command,
   378  		customDeps:        deps.Value,
   379  		customTag:         tag,
   380  		customImgDeps:     []reference.Named(imageDeps),
   381  		disablePush:       disablePush,
   382  		skipsLocalDocker:  skipsLocalDocker,
   383  		liveUpdate:        liveUpdate,
   384  		matchInEnvVars:    matchInEnvVars,
   385  		ignores:           ignores,
   386  		entrypoint:        entrypointCmd,
   387  		overrideArgs:      overrideArgs,
   388  		outputsImageRefTo: outputsImageRefTo.Value,
   389  		tiltfilePath:      starkit.CurrentExecPath(thread),
   390  	}
   391  
   392  	err = s.buildIndex.addImage(img)
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  
   397  	return &customBuild{s: s, img: img}, nil
   398  }
   399  
   400  type customBuild struct {
   401  	s   *tiltfileState
   402  	img *dockerImage
   403  }
   404  
   405  var _ starlark.Value = &customBuild{}
   406  
   407  func (b *customBuild) String() string {
   408  	return fmt.Sprintf("custom_build(%q)", b.img.configurationRef.String())
   409  }
   410  
   411  func (b *customBuild) Type() string {
   412  	return "custom_build"
   413  }
   414  
   415  func (b *customBuild) Freeze() {}
   416  
   417  func (b *customBuild) Truth() starlark.Bool {
   418  	return true
   419  }
   420  
   421  func (b *customBuild) Hash() (uint32, error) {
   422  	return 0, fmt.Errorf("unhashable type: custom_build")
   423  }
   424  
   425  func (b *customBuild) AttrNames() []string {
   426  	return []string{}
   427  }
   428  
   429  func parseValuesToStrings(value starlark.Value, param string) ([]string, error) {
   430  
   431  	tempIgnores := starlarkValueOrSequenceToSlice(value)
   432  	var ignores []string
   433  	for _, v := range tempIgnores {
   434  		switch val := v.(type) {
   435  		case starlark.String: // for singular string
   436  			goString := val.GoString()
   437  			if strings.Contains(goString, "\n") {
   438  				return nil, fmt.Errorf(param+" cannot contain newlines; found "+param+": %q", goString)
   439  			}
   440  			ignores = append(ignores, val.GoString())
   441  		default:
   442  			return nil, fmt.Errorf(param+" must be a string or a sequence of strings; found a %T", val)
   443  		}
   444  	}
   445  	return ignores, nil
   446  
   447  }
   448  
   449  func isGitRepoBase(path string) bool {
   450  	return ospath.IsDir(filepath.Join(path, ".git"))
   451  }
   452  
   453  func repoIgnoresForPaths(paths []string) []v1alpha1.IgnoreDef {
   454  	var result []v1alpha1.IgnoreDef
   455  	repoSet := map[string]bool{}
   456  
   457  	for _, path := range paths {
   458  		isRepoBase := isGitRepoBase(path)
   459  		if !isRepoBase || repoSet[path] {
   460  			continue
   461  		}
   462  
   463  		repoSet[path] = true
   464  		result = append(result, v1alpha1.IgnoreDef{
   465  			BasePath: filepath.Join(path, ".git"),
   466  		})
   467  	}
   468  
   469  	return result
   470  }
   471  
   472  func (s *tiltfileState) repoIgnoresForImage(image *dockerImage) []v1alpha1.IgnoreDef {
   473  	var paths []string
   474  	paths = append(paths, image.dbDockerfilePath)
   475  	if image.dbBuildPath != "" {
   476  		paths = append(paths, image.dbBuildPath)
   477  	}
   478  	paths = append(paths, image.customDeps...)
   479  
   480  	return repoIgnoresForPaths(paths)
   481  }
   482  
   483  func (s *tiltfileState) defaultRegistry(t *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   484  	if !container.IsEmptyRegistry(s.defaultReg) {
   485  		return starlark.None, errors.New("default registry already defined")
   486  	}
   487  
   488  	var host, hostFromCluster, singleName string
   489  	if err := s.unpackArgs(fn.Name(), args, kwargs,
   490  		"host", &host,
   491  		"host_from_cluster?", &hostFromCluster,
   492  		"single_name?", &singleName); err != nil {
   493  		return nil, err
   494  	}
   495  
   496  	reg := &v1alpha1.RegistryHosting{
   497  		Host:                     host,
   498  		HostFromContainerRuntime: hostFromCluster,
   499  		SingleName:               singleName,
   500  	}
   501  
   502  	ctx, err := starkit.ContextFromThread(t)
   503  	if err != nil {
   504  		return starlark.None, err
   505  	}
   506  
   507  	if err := reg.Validate(ctx); err != nil {
   508  		return starlark.None, errors.Wrapf(err.ToAggregate(), "validating defaultRegistry")
   509  	}
   510  
   511  	reg.SingleName = singleName
   512  
   513  	s.defaultReg = reg
   514  
   515  	return starlark.None, nil
   516  }
   517  
   518  func (s *tiltfileState) dockerignoresFromPathsAndContextFilters(source string, paths []string, ignorePatterns []string, onlys []string, dbDockerfilePath string) ([]model.Dockerignore, error) {
   519  	var result []model.Dockerignore
   520  	dupeSet := map[string]bool{}
   521  	onlyPatterns := onlysToDockerignorePatterns(onlys)
   522  
   523  	for _, path := range paths {
   524  		if path == "" || dupeSet[path] {
   525  			continue
   526  		}
   527  		dupeSet[path] = true
   528  
   529  		if !ospath.IsDir(path) {
   530  			continue
   531  		}
   532  
   533  		if len(ignorePatterns) != 0 {
   534  			result = append(result, model.Dockerignore{
   535  				LocalPath: path,
   536  				Source:    source + " ignores=",
   537  				Patterns:  ignorePatterns,
   538  			})
   539  		}
   540  
   541  		if len(onlyPatterns) != 0 {
   542  			result = append(result, model.Dockerignore{
   543  				LocalPath: path,
   544  				Source:    source + " only=",
   545  				Patterns:  onlyPatterns,
   546  			})
   547  		}
   548  
   549  		diFile := filepath.Join(path, ".dockerignore")
   550  		if dbDockerfilePath != "" {
   551  			customDiFile := dbDockerfilePath + ".dockerignore"
   552  			_, err := os.Stat(customDiFile)
   553  			if !os.IsNotExist(err) {
   554  				diFile = customDiFile
   555  			}
   556  		}
   557  
   558  		s.postExecReadFiles = sliceutils.AppendWithoutDupes(s.postExecReadFiles, diFile)
   559  
   560  		contents, err := os.ReadFile(diFile)
   561  		if err != nil {
   562  			if os.IsNotExist(err) {
   563  				continue
   564  			}
   565  			return nil, err
   566  		}
   567  
   568  		patterns, err := dockerignore.ReadAll(bytes.NewBuffer(contents))
   569  		if err != nil {
   570  			return nil, err
   571  		}
   572  
   573  		result = append(result, model.Dockerignore{
   574  			LocalPath: path,
   575  			Source:    diFile,
   576  			Patterns:  patterns,
   577  		})
   578  	}
   579  
   580  	return result, nil
   581  }
   582  
   583  func onlysToDockerignorePatterns(onlys []string) []string {
   584  	if len(onlys) == 0 {
   585  		return nil
   586  	}
   587  
   588  	result := []string{"**"}
   589  
   590  	for _, only := range onlys {
   591  		result = append(result, fmt.Sprintf("!%s", only))
   592  	}
   593  
   594  	return result
   595  }
   596  
   597  func (s *tiltfileState) dockerignoresForImage(image *dockerImage) ([]model.Dockerignore, error) {
   598  	var paths []string
   599  	var source string
   600  	ref := image.configurationRef.RefFamiliarString()
   601  	switch image.Type() {
   602  	case DockerBuild:
   603  		if image.dbBuildPath != "" {
   604  			paths = append(paths, image.dbBuildPath)
   605  		}
   606  		source = fmt.Sprintf("docker_build(%q)", ref)
   607  	case CustomBuild:
   608  		paths = append(paths, image.customDeps...)
   609  		source = fmt.Sprintf("custom_build(%q)", ref)
   610  	case DockerComposeBuild:
   611  		if image.dbBuildPath != "" {
   612  			paths = append(paths, image.dbBuildPath)
   613  		}
   614  		source = fmt.Sprintf("docker_compose(%q)", ref)
   615  	}
   616  	return s.dockerignoresFromPathsAndContextFilters(
   617  		source,
   618  		paths, image.ignores, image.onlys, image.dbDockerfilePath)
   619  }
   620  
   621  // Filter out all images that are suppressed.
   622  func filterUnmatchedImages(us model.UpdateSettings, images []*dockerImage) []*dockerImage {
   623  	result := make([]*dockerImage, 0, len(images))
   624  	for _, image := range images {
   625  		name := container.FamiliarString(image.configurationRef)
   626  
   627  		ok := true
   628  		for _, suppressed := range us.SuppressUnusedImageWarnings {
   629  			if suppressed == "*" {
   630  				ok = false
   631  				break
   632  			}
   633  
   634  			if suppressed == name {
   635  				ok = false
   636  				break
   637  			}
   638  		}
   639  
   640  		if ok {
   641  			result = append(result, image)
   642  		}
   643  	}
   644  	return result
   645  }