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

     1  package tiltfile
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/distribution/reference"
    11  	"github.com/pkg/errors"
    12  	"go.starlark.net/starlark"
    13  	"go.starlark.net/syntax"
    14  	v1 "k8s.io/api/core/v1"
    15  	"k8s.io/apimachinery/pkg/labels"
    16  
    17  	"github.com/tilt-dev/tilt/internal/tiltfile/links"
    18  
    19  	"github.com/tilt-dev/tilt/internal/container"
    20  	"github.com/tilt-dev/tilt/internal/k8s"
    21  	"github.com/tilt-dev/tilt/internal/tiltfile/io"
    22  	tiltfile_k8s "github.com/tilt-dev/tilt/internal/tiltfile/k8s"
    23  	"github.com/tilt-dev/tilt/internal/tiltfile/value"
    24  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    25  	"github.com/tilt-dev/tilt/pkg/model"
    26  )
    27  
    28  var emptyYAMLError = fmt.Errorf("Empty YAML passed to k8s_yaml")
    29  
    30  type referenceList []reference.Named
    31  
    32  func (l referenceList) Len() int           { return len(l) }
    33  func (l referenceList) Less(i, j int) bool { return l[i].String() < l[j].String() }
    34  func (l referenceList) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
    35  
    36  type imageDepMetadata struct {
    37  	required bool
    38  	count    int
    39  }
    40  
    41  type k8sResource struct {
    42  	// The name of this group, for display in the UX.
    43  	name string
    44  
    45  	// All k8s resources to be deployed.
    46  	entities []k8s.K8sEntity
    47  
    48  	imageRefs         referenceList
    49  	imageDepsMetadata map[string]*imageDepMetadata
    50  
    51  	portForwards []model.PortForward
    52  
    53  	// labels for pods that we should watch and associate with this resource
    54  	extraPodSelectors []labels.Set
    55  
    56  	podReadinessMode model.PodReadinessMode
    57  
    58  	discoveryStrategy v1alpha1.KubernetesDiscoveryStrategy
    59  
    60  	imageMapDeps []string
    61  
    62  	triggerMode triggerMode
    63  	autoInit    bool
    64  
    65  	resourceDeps []string
    66  
    67  	manuallyGrouped bool
    68  
    69  	links []model.Link
    70  
    71  	labels map[string]string
    72  
    73  	customDeploy *k8sCustomDeploy
    74  }
    75  
    76  // holds options passed to `k8s_resource` until assembly happens
    77  type k8sResourceOptions struct {
    78  	workload string
    79  	// if non-empty, how to rename this resource
    80  	newName           string
    81  	portForwards      []model.PortForward
    82  	extraPodSelectors []labels.Set
    83  	triggerMode       triggerMode
    84  	autoInit          value.Optional[starlark.Bool]
    85  	tiltfilePosition  syntax.Position
    86  	resourceDeps      []string
    87  	objects           []string
    88  	manuallyGrouped   bool
    89  	podReadinessMode  model.PodReadinessMode
    90  	discoveryStrategy v1alpha1.KubernetesDiscoveryStrategy
    91  	links             []model.Link
    92  	labels            map[string]string
    93  }
    94  
    95  // Count image injection for analytics.
    96  func (r *k8sResource) imageRefInjectCounts() map[string]int {
    97  	result := make(map[string]int, len(r.imageDepsMetadata))
    98  	for key, value := range r.imageDepsMetadata {
    99  		result[key] = value.count
   100  	}
   101  	return result
   102  }
   103  
   104  // Add a dependency on an image.
   105  //
   106  // Most image deps are optional. e.g., if you apply an nginx deployment,
   107  // but don't build an nginx image, your cluster can pull the production
   108  // nginx image. But if you want to use your own nginx image, you can specify one.
   109  //
   110  // But you can also specify required deps. e.g., a k8s_custom_deploy
   111  // can declare that an image must be built locally and injected into the
   112  // deploy command.
   113  func (r *k8sResource) addImageDep(image reference.Named, required bool) {
   114  	metadata, ok := r.imageDepsMetadata[image.String()]
   115  	if !ok {
   116  		r.imageRefs = append(r.imageRefs, image)
   117  
   118  		metadata = &imageDepMetadata{}
   119  		r.imageDepsMetadata[image.String()] = metadata
   120  	}
   121  	metadata.count++
   122  	metadata.required = metadata.required || required
   123  }
   124  
   125  func (r *k8sResource) addEntities(entities []k8s.K8sEntity,
   126  	locators []k8s.ImageLocator, envVarImages []container.RefSelector) error {
   127  	r.entities = append(r.entities, entities...)
   128  
   129  	for _, entity := range entities {
   130  		images, err := entity.FindImages(locators, envVarImages)
   131  		if err != nil {
   132  			return errors.Wrapf(err, "finding image in %s/%s", entity.GVK().Kind, entity.Name())
   133  		}
   134  		for _, image := range images {
   135  			r.addImageDep(image, false)
   136  		}
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  func (s *tiltfileState) k8sYaml(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   143  	var yamlValue starlark.Value
   144  	var allowDuplicates bool
   145  
   146  	if err := s.unpackArgs(fn.Name(), args, kwargs,
   147  		"yaml", &yamlValue,
   148  		"allow_duplicates?", &allowDuplicates,
   149  	); err != nil {
   150  		return nil, err
   151  	}
   152  	//normalize the starlark value into a slice
   153  	value := starlarkValueOrSequenceToSlice(yamlValue)
   154  
   155  	//if `None` was passed into k8s_yaml, len(val) = 0
   156  	if len(value) > 0 {
   157  
   158  		val, _ := starlark.AsString(value[0])
   159  		entities, err := s.yamlEntitiesFromSkylarkValueOrList(thread, yamlValue)
   160  
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  
   165  		//the parameter blob('') results in an empty string
   166  		if len(entities) == 0 && val == "" {
   167  			return nil, emptyYAMLError
   168  		}
   169  		err = s.k8sObjectIndex.Append(thread, entities, allowDuplicates)
   170  		if err != nil {
   171  			return nil, err
   172  		}
   173  
   174  		s.k8sUnresourced = append(s.k8sUnresourced, entities...)
   175  
   176  	} else {
   177  		return nil, emptyYAMLError
   178  	}
   179  
   180  	return starlark.None, nil
   181  }
   182  
   183  func (s *tiltfileState) extractSecrets() model.SecretSet {
   184  	result := model.SecretSet{}
   185  	for _, e := range s.k8sUnresourced {
   186  		secrets := s.maybeExtractSecrets(e)
   187  		result.AddAll(secrets)
   188  	}
   189  
   190  	for _, k := range s.k8s {
   191  		for _, e := range k.entities {
   192  			secrets := s.maybeExtractSecrets(e)
   193  			result.AddAll(secrets)
   194  		}
   195  	}
   196  	return result
   197  }
   198  
   199  func (s *tiltfileState) maybeExtractSecrets(e k8s.K8sEntity) model.SecretSet {
   200  	if !s.secretSettings.ScrubSecrets {
   201  		// Secret scrubbing disabled, don't extract any secrets
   202  		return nil
   203  	}
   204  
   205  	secret, ok := e.Obj.(*v1.Secret)
   206  	if !ok {
   207  		return nil
   208  	}
   209  
   210  	result := model.SecretSet{}
   211  	for key, data := range secret.Data {
   212  		result.AddSecret(secret.Name, key, data)
   213  	}
   214  
   215  	for key, data := range secret.StringData {
   216  		result.AddSecret(secret.Name, key, []byte(data))
   217  	}
   218  	return result
   219  }
   220  
   221  func (s *tiltfileState) filterYaml(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   222  	var yamlValue starlark.Value
   223  	var metaLabels value.StringStringMap
   224  	var name, namespace, kind, apiVersion string
   225  	err := s.unpackArgs(fn.Name(), args, kwargs,
   226  		"yaml", &yamlValue,
   227  		"labels?", &metaLabels,
   228  		"name?", &name,
   229  		"namespace?", &namespace,
   230  		"kind?", &kind,
   231  		"api_version?", &apiVersion,
   232  	)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	entities, err := s.yamlEntitiesFromSkylarkValueOrList(thread, yamlValue)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	k, err := k8s.NewPartialMatchObjectSelector(apiVersion, kind, name, namespace)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  
   247  	var match, rest []k8s.K8sEntity
   248  	for _, e := range entities {
   249  		if k.Matches(e) {
   250  			match = append(match, e)
   251  		} else {
   252  			rest = append(rest, e)
   253  		}
   254  	}
   255  
   256  	if len(metaLabels) > 0 {
   257  		var r []k8s.K8sEntity
   258  		match, r, err = k8s.FilterByMetadataLabels(match, metaLabels)
   259  		if err != nil {
   260  			return nil, err
   261  		}
   262  		rest = append(rest, r...)
   263  	}
   264  
   265  	matchingStr, err := k8s.SerializeSpecYAML(match)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	restStr, err := k8s.SerializeSpecYAML(rest)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	var source string
   275  	switch y := yamlValue.(type) {
   276  	case io.Blob:
   277  		source = y.Source
   278  	default:
   279  		source = "filter_yaml"
   280  	}
   281  
   282  	return starlark.Tuple{
   283  		io.NewBlob(matchingStr, source), io.NewBlob(restStr, source),
   284  	}, nil
   285  }
   286  
   287  func (s *tiltfileState) k8sImageLocatorsList() []k8s.ImageLocator {
   288  	locators := []k8s.ImageLocator{}
   289  	for _, info := range s.k8sKinds {
   290  		locators = append(locators, info.ImageLocators...)
   291  	}
   292  	return locators
   293  }
   294  
   295  func (s *tiltfileState) k8sResource(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   296  	var workload value.Name
   297  	var newName value.Name
   298  	var portForwardsVal starlark.Value
   299  	var extraPodSelectorsVal starlark.Value
   300  	var triggerMode triggerMode
   301  	var resourceDepsVal starlark.Sequence
   302  	var objectsVal starlark.Sequence
   303  	var podReadinessMode tiltfile_k8s.PodReadinessMode
   304  	var links links.LinkList
   305  	var autoInit = value.Optional[starlark.Bool]{Value: true}
   306  	var labels value.LabelSet
   307  	var discoveryStrategy tiltfile_k8s.DiscoveryStrategy
   308  
   309  	if err := s.unpackArgs(fn.Name(), args, kwargs,
   310  		"workload?", &workload,
   311  		"new_name?", &newName,
   312  		"port_forwards?", &portForwardsVal,
   313  		"extra_pod_selectors?", &extraPodSelectorsVal,
   314  		"trigger_mode?", &triggerMode,
   315  		"resource_deps?", &resourceDepsVal,
   316  		"objects?", &objectsVal,
   317  		"auto_init?", &autoInit,
   318  		"pod_readiness?", &podReadinessMode,
   319  		"links?", &links,
   320  		"labels?", &labels,
   321  		"discovery_strategy?", &discoveryStrategy,
   322  	); err != nil {
   323  		return nil, err
   324  	}
   325  
   326  	resourceName := workload.String()
   327  	manuallyGrouped := false
   328  	if workload == "" {
   329  		resourceName = newName.String()
   330  		// If a resource doesn't specify an existing workload then it needs to have objects to be valid
   331  		manuallyGrouped = true
   332  	}
   333  
   334  	if resourceName == "" {
   335  		return nil, fmt.Errorf("Resource name missing. Must give a name for an existing resource or a new_name to create a new resource.")
   336  	}
   337  
   338  	portForwards, err := convertPortForwards(portForwardsVal)
   339  	if err != nil {
   340  		return nil, errors.Wrapf(err, "%s %q", fn.Name(), resourceName)
   341  	}
   342  
   343  	extraPodSelectors, err := podLabelsFromStarlarkValue(extraPodSelectorsVal)
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	resourceDeps, err := value.SequenceToStringSlice(resourceDepsVal)
   349  	if err != nil {
   350  		return nil, errors.Wrapf(err, "%s: resource_deps", fn.Name())
   351  	}
   352  
   353  	objects, err := value.SequenceToStringSlice(objectsVal)
   354  	if err != nil {
   355  		return nil, errors.Wrapf(err, "%s: resource_deps", fn.Name())
   356  	}
   357  
   358  	if manuallyGrouped && len(objects) == 0 {
   359  		return nil, fmt.Errorf("k8s_resource doesn't specify a workload or any objects. All non-workload resources must specify 1 or more objects")
   360  	}
   361  
   362  	labelMap := make(map[string]string)
   363  	for k, v := range labels.Values {
   364  		labelMap[k] = v
   365  	}
   366  
   367  	s.k8sResourceOptions = append(s.k8sResourceOptions, k8sResourceOptions{
   368  		workload:          resourceName,
   369  		newName:           string(newName),
   370  		portForwards:      portForwards,
   371  		extraPodSelectors: extraPodSelectors,
   372  		tiltfilePosition:  thread.CallFrame(1).Pos,
   373  		triggerMode:       triggerMode,
   374  		autoInit:          autoInit,
   375  		resourceDeps:      resourceDeps,
   376  		objects:           objects,
   377  		manuallyGrouped:   manuallyGrouped,
   378  		podReadinessMode:  podReadinessMode.Value,
   379  		links:             links.Links,
   380  		labels:            labelMap,
   381  		discoveryStrategy: v1alpha1.KubernetesDiscoveryStrategy(discoveryStrategy),
   382  	})
   383  
   384  	return starlark.None, nil
   385  }
   386  
   387  func labelSetFromStarlarkDict(d *starlark.Dict) (labels.Set, error) {
   388  	ret := make(labels.Set)
   389  
   390  	for _, t := range d.Items() {
   391  		kVal := t[0]
   392  		k, ok := kVal.(starlark.String)
   393  		if !ok {
   394  			return nil, fmt.Errorf("pod label keys must be strings; got '%s' of type %T", kVal.String(), kVal)
   395  		}
   396  		vVal := t[1]
   397  		v, ok := vVal.(starlark.String)
   398  		if !ok {
   399  			return nil, fmt.Errorf("pod label values must be strings; got '%s' of type %T", vVal.String(), vVal)
   400  		}
   401  		ret[string(k)] = string(v)
   402  	}
   403  	if len(ret) > 0 {
   404  		return ret, nil
   405  	} else {
   406  		return nil, nil
   407  	}
   408  }
   409  
   410  func podLabelsFromStarlarkValue(v starlark.Value) ([]labels.Set, error) {
   411  	if v == nil {
   412  		return nil, nil
   413  	}
   414  
   415  	switch x := v.(type) {
   416  	case *starlark.Dict:
   417  		s, err := labelSetFromStarlarkDict(x)
   418  		if err != nil {
   419  			return nil, err
   420  		} else if s == nil {
   421  			return nil, nil
   422  		} else {
   423  			return []labels.Set{s}, nil
   424  		}
   425  	case *starlark.List:
   426  		var ret []labels.Set
   427  
   428  		it := x.Iterate()
   429  		defer it.Done()
   430  		var i starlark.Value
   431  		for it.Next(&i) {
   432  			d, ok := i.(*starlark.Dict)
   433  			if !ok {
   434  				return nil, fmt.Errorf("pod labels elements must be dicts; got %T", i)
   435  			}
   436  			s, err := labelSetFromStarlarkDict(d)
   437  			if err != nil {
   438  				return nil, err
   439  			} else if s != nil {
   440  				ret = append(ret, s)
   441  			}
   442  		}
   443  
   444  		return ret, nil
   445  	default:
   446  		return nil, fmt.Errorf("pod labels must be a dict or a list; got %T", v)
   447  	}
   448  }
   449  
   450  func (s *tiltfileState) k8sImageJsonPath(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   451  	var apiVersion, kind, name, namespace string
   452  	var locatorList tiltfile_k8s.JSONPathImageLocatorListSpec
   453  	if err := s.unpackArgs(fn.Name(), args, kwargs,
   454  		"paths", &locatorList,
   455  		"api_version?", &apiVersion,
   456  		"kind?", &kind,
   457  		"name?", &name,
   458  		"namespace?", &namespace,
   459  	); err != nil {
   460  		return nil, err
   461  	}
   462  
   463  	if kind == "" && name == "" && namespace == "" {
   464  		return nil, errors.New("at least one of kind, name, or namespace must be specified")
   465  	}
   466  
   467  	k, err := k8s.NewPartialMatchObjectSelector(apiVersion, kind, name, namespace)
   468  	if err != nil {
   469  		return nil, err
   470  	}
   471  
   472  	paths, err := locatorList.ToImageLocators(k)
   473  	if err != nil {
   474  		return nil, err
   475  	}
   476  
   477  	kindInfo, ok := s.k8sKinds[k]
   478  	if !ok {
   479  		kindInfo = &tiltfile_k8s.KindInfo{}
   480  		s.k8sKinds[k] = kindInfo
   481  	}
   482  	kindInfo.ImageLocators = paths
   483  
   484  	return starlark.None, nil
   485  }
   486  
   487  func (s *tiltfileState) k8sKind(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   488  	// require image_json_path to be passed as a kw arg since `k8s_kind("Environment", "{.foo.bar}")` feels confusing
   489  	if len(args) > 1 {
   490  		return nil, fmt.Errorf("%s: got %d arguments, want at most %d", fn.Name(), len(args), 1)
   491  	}
   492  
   493  	var apiVersion, kind string
   494  	var jpLocators tiltfile_k8s.JSONPathImageLocatorListSpec
   495  	var jpObjectLocator tiltfile_k8s.JSONPathImageObjectLocatorSpec
   496  	var podReadiness tiltfile_k8s.PodReadinessMode
   497  	if err := s.unpackArgs(fn.Name(), args, kwargs,
   498  		"kind", &kind,
   499  		"image_json_path?", &jpLocators,
   500  		"api_version?", &apiVersion,
   501  		"image_object?", &jpObjectLocator,
   502  		"pod_readiness?", &podReadiness,
   503  	); err != nil {
   504  		return nil, err
   505  	}
   506  
   507  	k, err := k8s.NewPartialMatchObjectSelector(apiVersion, kind, "", "")
   508  	if err != nil {
   509  		return nil, err
   510  	}
   511  
   512  	if !jpLocators.IsEmpty() && !jpObjectLocator.IsEmpty() {
   513  		return nil, fmt.Errorf("Cannot specify both image_json_path and image_object")
   514  	}
   515  
   516  	kindInfo, ok := s.k8sKinds[k]
   517  	if !ok {
   518  		kindInfo = &tiltfile_k8s.KindInfo{}
   519  		s.k8sKinds[k] = kindInfo
   520  	}
   521  
   522  	if !jpLocators.IsEmpty() {
   523  		locators, err := jpLocators.ToImageLocators(k)
   524  		if err != nil {
   525  			return nil, err
   526  		}
   527  
   528  		kindInfo.ImageLocators = locators
   529  	} else if !jpObjectLocator.IsEmpty() {
   530  		locator, err := jpObjectLocator.ToImageLocator(k)
   531  		if err != nil {
   532  			return nil, err
   533  		}
   534  		kindInfo.ImageLocators = []k8s.ImageLocator{locator}
   535  	}
   536  
   537  	if podReadiness.Value != "" {
   538  		kindInfo.PodReadinessMode = podReadiness.Value
   539  	}
   540  
   541  	return starlark.None, nil
   542  }
   543  
   544  func (s *tiltfileState) workloadToResourceFunctionFn(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   545  	var wtrf *starlark.Function
   546  	if err := s.unpackArgs(fn.Name(), args, kwargs,
   547  		"func", &wtrf); err != nil {
   548  		return nil, err
   549  	}
   550  
   551  	workloadToResourceFunction, err := makeWorkloadToResourceFunction(wtrf)
   552  	if err != nil {
   553  		return starlark.None, err
   554  	}
   555  
   556  	s.workloadToResourceFunction = workloadToResourceFunction
   557  
   558  	return starlark.None, nil
   559  }
   560  
   561  type k8sObjectID struct {
   562  	name      string
   563  	kind      string
   564  	namespace string
   565  	group     string
   566  }
   567  
   568  func (k k8sObjectID) Attr(name string) (starlark.Value, error) {
   569  	switch name {
   570  	case "name":
   571  		return starlark.String(k.name), nil
   572  	case "kind":
   573  		return starlark.String(k.kind), nil
   574  	case "namespace":
   575  		return starlark.String(k.namespace), nil
   576  	case "group":
   577  		return starlark.String(k.group), nil
   578  	default:
   579  		return starlark.None, fmt.Errorf("%T has no attribute '%s'", k, name)
   580  	}
   581  }
   582  
   583  func (k k8sObjectID) AttrNames() []string {
   584  	return []string{"name", "kind", "namespace", "group"}
   585  }
   586  
   587  func (k k8sObjectID) String() string {
   588  	return strings.ToLower(fmt.Sprintf("%s:%s:%s:%s", k.name, k.kind, k.namespace, k.group))
   589  }
   590  
   591  func (k k8sObjectID) Type() string {
   592  	return "K8sObjectID"
   593  }
   594  
   595  func (k k8sObjectID) Freeze() {
   596  }
   597  
   598  func (k k8sObjectID) Truth() starlark.Bool {
   599  	return k.name != "" || k.kind != "" || k.namespace != "" || k.group != ""
   600  }
   601  
   602  func (k k8sObjectID) Hash() (uint32, error) {
   603  	return starlark.Tuple{starlark.String(k.name), starlark.String(k.kind), starlark.String(k.namespace), starlark.String(k.group)}.Hash()
   604  }
   605  
   606  var _ starlark.Value = k8sObjectID{}
   607  
   608  type workloadToResourceFunction struct {
   609  	fn  func(thread *starlark.Thread, id k8sObjectID) (string, error)
   610  	pos syntax.Position
   611  }
   612  
   613  func makeWorkloadToResourceFunction(f *starlark.Function) (workloadToResourceFunction, error) {
   614  	if f.NumParams() != 1 {
   615  		return workloadToResourceFunction{}, fmt.Errorf("%s arg must take 1 argument. %s takes %d", workloadToResourceFunctionN, f.Name(), f.NumParams())
   616  	}
   617  	fn := func(thread *starlark.Thread, id k8sObjectID) (string, error) {
   618  		ret, err := starlark.Call(thread, f, starlark.Tuple{id}, nil)
   619  		if err != nil {
   620  			return "", err
   621  		}
   622  		s, ok := ret.(starlark.String)
   623  		if !ok {
   624  			return "", fmt.Errorf("%s: invalid return value. wanted: string. got: %T", f.Name(), ret)
   625  		}
   626  		return string(s), nil
   627  	}
   628  
   629  	return workloadToResourceFunction{
   630  		fn:  fn,
   631  		pos: f.Position(),
   632  	}, nil
   633  }
   634  
   635  func (s *tiltfileState) checkResourceConflict(name string) error {
   636  	if s.k8sByName[name] != nil {
   637  		return fmt.Errorf("k8s_resource named %q already exists", name)
   638  	}
   639  	if s.localByName[name] != nil {
   640  		return fmt.Errorf("local_resource named %q already exists", name)
   641  	}
   642  	for _, dc := range s.dc {
   643  		for n := range dc.services {
   644  			if name == n {
   645  				return fmt.Errorf("dc_resource named %q already exists", name)
   646  			}
   647  		}
   648  	}
   649  	return nil
   650  }
   651  
   652  func (s *tiltfileState) makeK8sResource(name string) (*k8sResource, error) {
   653  	err := s.checkResourceConflict(name)
   654  	if err != nil {
   655  		return nil, err
   656  	}
   657  
   658  	r := &k8sResource{
   659  		name:              name,
   660  		imageDepsMetadata: make(map[string]*imageDepMetadata),
   661  		autoInit:          true,
   662  		labels:            make(map[string]string),
   663  	}
   664  	s.k8s = append(s.k8s, r)
   665  	s.k8sByName[name] = r
   666  
   667  	return r, nil
   668  }
   669  
   670  func (s *tiltfileState) yamlEntitiesFromSkylarkValueOrList(thread *starlark.Thread, v starlark.Value) ([]k8s.K8sEntity, error) {
   671  	values := starlarkValueOrSequenceToSlice(v)
   672  
   673  	var ret []k8s.K8sEntity
   674  
   675  	for _, value := range values {
   676  		entities, err := s.yamlEntitiesFromSkylarkValue(thread, value)
   677  		if err != nil {
   678  			return nil, err
   679  		}
   680  		ret = append(ret, entities...)
   681  	}
   682  
   683  	return ret, nil
   684  }
   685  
   686  func parseYAMLFromBlob(blob io.Blob) ([]k8s.K8sEntity, error) {
   687  	ret, err := k8s.ParseYAMLFromString(blob.String())
   688  	if err != nil {
   689  		return nil, errors.Wrapf(err, "Error reading yaml from %s", blob.Source)
   690  	}
   691  	return ret, nil
   692  }
   693  
   694  func (s *tiltfileState) yamlEntitiesFromSkylarkValue(thread *starlark.Thread, v starlark.Value) ([]k8s.K8sEntity, error) {
   695  	switch v := v.(type) {
   696  	case nil:
   697  		return nil, nil
   698  	case io.Blob:
   699  		return parseYAMLFromBlob(v)
   700  	default:
   701  		yamlPath, err := value.ValueToAbsPath(thread, v)
   702  		if err != nil {
   703  			return nil, err
   704  		}
   705  		bs, err := io.ReadFile(thread, yamlPath)
   706  		if err != nil {
   707  			return nil, errors.Wrap(err, "error reading yaml file")
   708  		}
   709  
   710  		entities, err := k8s.ParseYAMLFromString(string(bs))
   711  		if err != nil {
   712  			if strings.Contains(err.Error(), "json parse error: ") {
   713  				return entities, fmt.Errorf("%s is not a valid YAML file: %s", yamlPath, err)
   714  			}
   715  			return entities, err
   716  		}
   717  
   718  		return entities, nil
   719  	}
   720  }
   721  
   722  func convertPortForwards(val starlark.Value) ([]model.PortForward, error) {
   723  	if val == nil {
   724  		return nil, nil
   725  	}
   726  	switch val := val.(type) {
   727  	case starlark.NoneType:
   728  		return nil, nil
   729  
   730  	case starlark.Int:
   731  		pf, err := intToPortForward(val)
   732  		if err != nil {
   733  			return nil, err
   734  		}
   735  		return []model.PortForward{pf}, nil
   736  
   737  	case starlark.String:
   738  		pf, err := stringToPortForward(val)
   739  		if err != nil {
   740  			return nil, err
   741  		}
   742  		return []model.PortForward{pf}, nil
   743  
   744  	case portForward:
   745  		return []model.PortForward{val.PortForward}, nil
   746  	case starlark.Sequence:
   747  		var result []model.PortForward
   748  		it := val.Iterate()
   749  		defer it.Done()
   750  		var i starlark.Value
   751  		for it.Next(&i) {
   752  			switch i := i.(type) {
   753  			case starlark.Int:
   754  				pf, err := intToPortForward(i)
   755  				if err != nil {
   756  					return nil, err
   757  				}
   758  				result = append(result, pf)
   759  
   760  			case starlark.String:
   761  				pf, err := stringToPortForward(i)
   762  				if err != nil {
   763  					return nil, err
   764  				}
   765  				result = append(result, pf)
   766  
   767  			case portForward:
   768  				result = append(result, i.PortForward)
   769  			default:
   770  				return nil, fmt.Errorf("port_forwards arg %v includes element %v which must be an int or a port_forward; is a %T", val, i, i)
   771  			}
   772  		}
   773  		return result, nil
   774  	default:
   775  		return nil, fmt.Errorf("port_forwards must be an int, a port_forward, or a sequence of those; is a %T", val)
   776  	}
   777  }
   778  
   779  func (s *tiltfileState) portForward(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   780  	var local, container int
   781  	var name, path, host string
   782  
   783  	// TODO: can specify host (see `stringToPortForward` for host validation logic)
   784  	if err := s.unpackArgs(fn.Name(), args, kwargs,
   785  		"local_port", &local,
   786  		"container_port?", &container,
   787  		"name?", &name,
   788  		"link_path?", &path,
   789  		"host?", &host); err != nil {
   790  		return nil, err
   791  	}
   792  
   793  	var parsedPath *url.URL
   794  	if path != "" {
   795  		var err error
   796  		parsedPath, err = url.Parse(path)
   797  		if err != nil {
   798  			return portForward{}, errors.Wrapf(err, "parsing `path` param")
   799  		}
   800  	}
   801  	return portForward{
   802  		model.PortForward{LocalPort: local, ContainerPort: container, Host: host, Name: name}.WithPath(parsedPath),
   803  	}, nil
   804  }
   805  
   806  type portForward struct {
   807  	model.PortForward
   808  }
   809  
   810  var _ starlark.Value = portForward{}
   811  
   812  func (f portForward) String() string {
   813  	return fmt.Sprintf("port_forward(local_port=%d, container_port=%d, name=%q)",
   814  		f.LocalPort, f.ContainerPort, f.Name)
   815  }
   816  
   817  func (f portForward) Type() string {
   818  	return "port_forward"
   819  }
   820  
   821  func (f portForward) Freeze() {}
   822  
   823  func (f portForward) Truth() starlark.Bool {
   824  	return f.PortForward != model.PortForward{}
   825  }
   826  
   827  func (f portForward) Hash() (uint32, error) {
   828  	return 0, fmt.Errorf("unhashable type: port_forward")
   829  }
   830  
   831  func intToPortForward(i starlark.Int) (model.PortForward, error) {
   832  	n, ok := i.Int64()
   833  	if !ok {
   834  		return model.PortForward{}, fmt.Errorf("portForward port value %v is not representable as an int64", i)
   835  	}
   836  	if n < 0 || n > 65535 {
   837  		return model.PortForward{}, fmt.Errorf("portForward port value %v is not in the valid range [0-65535]", n)
   838  	}
   839  	return model.PortForward{LocalPort: int(n)}, nil
   840  }
   841  
   842  const ipReStr = `^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`
   843  const hostnameReStr = `^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`
   844  
   845  var validHost = regexp.MustCompile(ipReStr + "|" + hostnameReStr)
   846  
   847  func stringToPortForward(s starlark.String) (model.PortForward, error) {
   848  	parts := strings.SplitN(string(s), ":", 3)
   849  
   850  	var host string
   851  	var localString string
   852  	if len(parts) == 3 {
   853  		localString = parts[1]
   854  		host = parts[0]
   855  		if !validHost.MatchString(host) {
   856  			return model.PortForward{}, fmt.Errorf("portForward host value %q is not a valid hostname or IP address", localString)
   857  		}
   858  	} else {
   859  		localString = parts[0]
   860  	}
   861  
   862  	local, err := strconv.Atoi(localString)
   863  	if err != nil || local < 0 || local > 65535 {
   864  		return model.PortForward{}, fmt.Errorf("portForward port value %q is not in the valid range [0-65535]", localString)
   865  	}
   866  
   867  	var container int
   868  	if len(parts) > 1 {
   869  		last := parts[len(parts)-1]
   870  		container, err = strconv.Atoi(last)
   871  		if err != nil || container < 0 || container > 65535 {
   872  			return model.PortForward{}, fmt.Errorf("portForward port value %q is not in the valid range [0-65535]", last)
   873  		}
   874  	}
   875  	return model.PortForward{LocalPort: local, ContainerPort: container, Host: host}, nil
   876  }
   877  
   878  func (s *tiltfileState) calculateResourceNames(workloads []k8s.K8sEntity) ([]string, error) {
   879  	if s.workloadToResourceFunction.fn != nil {
   880  		names, err := s.workloadToResourceFunctionNames(workloads)
   881  		if err != nil {
   882  			return nil, errors.Wrapf(err, "%s: error applying workload_to_resource_function", s.workloadToResourceFunction.pos.String())
   883  		}
   884  		return names, nil
   885  	} else {
   886  		return k8s.UniqueNames(workloads, 1), nil
   887  	}
   888  }
   889  
   890  // calculates names for workloads using s.workloadToResourceFunction
   891  func (s *tiltfileState) workloadToResourceFunctionNames(workloads []k8s.K8sEntity) ([]string, error) {
   892  	takenNames := make(map[string]k8s.K8sEntity)
   893  	ret := make([]string, len(workloads))
   894  	thread := &starlark.Thread{
   895  		Print: s.print,
   896  	}
   897  	for i, e := range workloads {
   898  		id := newK8sObjectID(e)
   899  		name, err := s.workloadToResourceFunction.fn(thread, id)
   900  		if err != nil {
   901  			return nil, errors.Wrapf(err, "error determining resource name for '%s'", id.String())
   902  		}
   903  
   904  		if conflictingWorkload, ok := takenNames[name]; ok {
   905  			return nil, fmt.Errorf("both '%s' and '%s' mapped to resource name '%s'", newK8sObjectID(e).String(), newK8sObjectID(conflictingWorkload).String(), name)
   906  		}
   907  
   908  		ret[i] = name
   909  		takenNames[name] = e
   910  	}
   911  	return ret, nil
   912  }
   913  
   914  func newK8sObjectID(e k8s.K8sEntity) k8sObjectID {
   915  	gvk := e.GVK()
   916  	return k8sObjectID{
   917  		name:      e.Name(),
   918  		kind:      gvk.Kind,
   919  		namespace: e.Namespace().String(),
   920  		group:     gvk.Group,
   921  	}
   922  }