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 }