Skip to content

Commit

Permalink
tiltfile: add an API for adding image object locators. Fixes #3427 (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
nicks committed Jul 1, 2020
1 parent 035633c commit 8637c72
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 33 deletions.
53 changes: 22 additions & 31 deletions internal/tiltfile/k8s.go
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/tilt-dev/tilt/internal/container"
"github.com/tilt-dev/tilt/internal/k8s"
"github.com/tilt-dev/tilt/internal/tiltfile/io"
tiltfile_k8s "github.com/tilt-dev/tilt/internal/tiltfile/k8s"
"github.com/tilt-dev/tilt/internal/tiltfile/value"
"github.com/tilt-dev/tilt/pkg/model"
)
Expand Down Expand Up @@ -533,30 +534,11 @@ func podLabelsFromStarlarkValue(v starlark.Value) ([]labels.Selector, error) {
}
}

func starlarkValuesToJSONPathImageLocators(selector k8s.ObjectSelector, values []starlark.Value) ([]k8s.ImageLocator, error) {
var paths []k8s.ImageLocator
for _, v := range values {
s, ok := v.(starlark.String)
if !ok {
return nil, fmt.Errorf("paths must be a string or list of strings, found a list containing value '%+v' of type '%T'", v, v)
}

jp, err := k8s.NewJSONPathImageLocator(selector, string(s))
if err != nil {
return nil, errors.Wrapf(err, "error parsing json paths '%s'", s.String())
}

paths = append(paths, jp)
}

return paths, nil
}

func (s *tiltfileState) k8sImageJsonPath(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var apiVersion, kind, name, namespace string
var imageJSONPath starlark.Value
var locatorList tiltfile_k8s.JSONPathImageLocatorListSpec
if err := s.unpackArgs(fn.Name(), args, kwargs,
"paths", &imageJSONPath,
"paths", &locatorList,
"api_version?", &apiVersion,
"kind?", &kind,
"name?", &name,
Expand All @@ -574,9 +556,7 @@ func (s *tiltfileState) k8sImageJsonPath(thread *starlark.Thread, fn *starlark.B
return nil, err
}

values := starlarkValueOrSequenceToSlice(imageJSONPath)

paths, err := starlarkValuesToJSONPathImageLocators(k, values)
paths, err := locatorList.ToImageLocators(k)
if err != nil {
return nil, err
}
Expand All @@ -593,11 +573,13 @@ func (s *tiltfileState) k8sKind(thread *starlark.Thread, fn *starlark.Builtin, a
}

var apiVersion, kind string
var imageJSONPath starlark.Value
var jpLocators tiltfile_k8s.JSONPathImageLocatorListSpec
var jpObjectLocator tiltfile_k8s.JSONPathImageObjectLocatorSpec
if err := s.unpackArgs(fn.Name(), args, kwargs,
"kind", &kind,
"image_json_path?", &imageJSONPath,
"image_json_path?", &jpLocators,
"api_version?", &apiVersion,
"image_object?", &jpObjectLocator,
); err != nil {
return nil, err
}
Expand All @@ -607,16 +589,25 @@ func (s *tiltfileState) k8sKind(thread *starlark.Thread, fn *starlark.Builtin, a
return nil, err
}

if imageJSONPath == nil {
s.workloadTypes = append(s.workloadTypes, k)
} else {
values := starlarkValueOrSequenceToSlice(imageJSONPath)
locators, err := starlarkValuesToJSONPathImageLocators(k, values)
if !jpLocators.IsEmpty() && !jpObjectLocator.IsEmpty() {
return nil, fmt.Errorf("Cannot specify both image_json_path and image_object")
}

if !jpLocators.IsEmpty() {
locators, err := jpLocators.ToImageLocators(k)
if err != nil {
return nil, err
}

s.k8sImageLocators[k] = locators
} else if !jpObjectLocator.IsEmpty() {
locator, err := jpObjectLocator.ToImageLocator(k)
if err != nil {
return nil, err
}
s.k8sImageLocators[k] = []k8s.ImageLocator{locator}
} else {
s.workloadTypes = append(s.workloadTypes, k)
}

return starlark.None, nil
Expand Down
129 changes: 129 additions & 0 deletions internal/tiltfile/k8s/locators.go
@@ -0,0 +1,129 @@
package k8s

import (
"fmt"

"go.starlark.net/starlark"

"github.com/pkg/errors"

"github.com/tilt-dev/tilt/internal/k8s"
"github.com/tilt-dev/tilt/internal/tiltfile/value"
)

// Deserializing locators from starlark values.
type JSONPathImageLocatorListSpec struct {
Specs []JSONPathImageLocatorSpec
}

func (s JSONPathImageLocatorListSpec) IsEmpty() bool {
return len(s.Specs) == 0
}

func (s *JSONPathImageLocatorListSpec) Unpack(v starlark.Value) error {
list := value.ValueOrSequenceToSlice(v)
for _, item := range list {
spec := JSONPathImageLocatorSpec{}
err := spec.Unpack(item)
if err != nil {
return err
}
s.Specs = append(s.Specs, spec)
}
return nil
}

func (s JSONPathImageLocatorListSpec) ToImageLocators(selector k8s.ObjectSelector) ([]k8s.ImageLocator, error) {
result := []k8s.ImageLocator{}
for _, spec := range s.Specs {
locator, err := spec.ToImageLocator(selector)
if err != nil {
return nil, err
}
result = append(result, locator)
}
return result, nil
}

type JSONPathImageLocatorSpec struct {
jsonPath string
}

func (s *JSONPathImageLocatorSpec) badValueErr(v starlark.Value) error {
return fmt.Errorf("Expected map of the form {'json_path': ..., 'repo_field': ..., 'tag_field': ...}. Actual: %s", v)
}

func (s *JSONPathImageLocatorSpec) Unpack(v starlark.Value) error {
var ok bool
s.jsonPath, ok = starlark.AsString(v)
if !ok {
return fmt.Errorf("Expected string, got: %s", v)
}
return nil
}

func (s JSONPathImageLocatorSpec) ToImageLocator(selector k8s.ObjectSelector) (k8s.ImageLocator, error) {
return k8s.NewJSONPathImageLocator(selector, s.jsonPath)
}

type JSONPathImageObjectLocatorSpec struct {
jsonPath string
repoField string
tagField string
}

func (s JSONPathImageObjectLocatorSpec) IsEmpty() bool {
return s == JSONPathImageObjectLocatorSpec{}
}

func (s *JSONPathImageObjectLocatorSpec) Unpack(v starlark.Value) error {
d, ok := v.(*starlark.Dict)
if !ok {
return fmt.Errorf("Expected dict of the form {'json_path': str, 'repo_field': str, 'tag_field': str}. Actual: %s", v)
}

values, err := validateStringDict(d, []string{"json_path", "repo_field", "tag_field"})
if err != nil {
return errors.Wrap(err, "Expected dict of the form {'json_path': str, 'repo_field': str, 'tag_field': str}")
}

s.jsonPath, s.repoField, s.tagField = values[0], values[1], values[2]
return nil
}

func (s JSONPathImageObjectLocatorSpec) ToImageLocator(selector k8s.ObjectSelector) (k8s.ImageLocator, error) {
return k8s.NewJSONPathImageObjectLocator(selector, s.jsonPath, s.repoField, s.tagField)
}

func validateStringDict(d *starlark.Dict, expectedFields []string) ([]string, error) {
indices := map[string]int{}
result := make([]string, len(expectedFields))
for i, f := range expectedFields {
indices[f] = i
}

for _, item := range d.Items() {
key, val := item[0], item[1]
keyString, ok := starlark.AsString(key)
if !ok {
return nil, fmt.Errorf("Unexpected key: %s", key)
}

index, ok := indices[keyString]
if !ok {
return nil, fmt.Errorf("Unexpected key: %s", key)
}

valString, ok := starlark.AsString(val)
if !ok {
return nil, fmt.Errorf("Expected string at key %q. Got: %s", key, val.Type())
}

result[index] = valString
}

if len(d.Items()) != len(expectedFields) {
return nil, fmt.Errorf("Missing keys. Actual keys: %s", d.Keys())
}
return result, nil
}
27 changes: 25 additions & 2 deletions internal/tiltfile/tiltfile_test.go
Expand Up @@ -2148,6 +2148,29 @@ custom_build(
assert.True(t, m.ImageTargets[0].CustomBuildInfo().SkipsPush())
}

func TestImageObjectJSONPath(t *testing.T) {
f := newFixture(t)
defer f.TearDown()
f.file("um.yaml", `apiVersion: tilt.dev/v1alpha1
kind: UselessMachine
metadata:
name: um
spec:
image:
repo: tilt.dev/frontend`)
f.dockerfile("Dockerfile")
f.file("Tiltfile", `
k8s_yaml('um.yaml')
k8s_kind(kind='UselessMachine', image_object={'json_path': '{.spec.image}', 'repo_field': 'repo', 'tag_field': 'tag'})
docker_build('tilt.dev/frontend', '.')
`)

f.load()
m := f.assertNextManifest("um")
assert.Equal(t, "tilt.dev/frontend",
m.ImageTargets[0].Refs.LocalRef().String())
}

func TestExtraImageLocationOneImage(t *testing.T) {
f := newFixture(t)
defer f.TearDown()
Expand Down Expand Up @@ -2513,14 +2536,14 @@ func TestExtraImageLocationNotListOrString(t *testing.T) {
f := newFixture(t)
defer f.TearDown()
f.file("Tiltfile", `k8s_image_json_path(kind='MyType', paths=8)`)
f.loadErrString("paths must be a string or list of strings", "Int")
f.loadErrString("for parameter \"paths\": Expected string, got: 8")
}

func TestExtraImageLocationListContainsNonString(t *testing.T) {
f := newFixture(t)
defer f.TearDown()
f.file("Tiltfile", `k8s_image_json_path(kind='MyType', paths=["foo", 8])`)
f.loadErrString("paths must be a string or list of strings", "8", "Int")
f.loadErrString("for parameter \"paths\": Expected string, got: 8")
}

func TestExtraImageLocationNoSelectorSpecified(t *testing.T) {
Expand Down

0 comments on commit 8637c72

Please sign in to comment.