diff --git a/docs/content/en/schemas/v2beta8.json b/docs/content/en/schemas/v2beta8.json index 27c22d6f4c9..031d034d987 100755 --- a/docs/content/en/schemas/v2beta8.json +++ b/docs/content/en/schemas/v2beta8.json @@ -1498,8 +1498,8 @@ "type": "string" }, "type": "object", - "description": "arguments passed to the docker build. It also accepts environment variables via the go template syntax.", - "x-intellij-html-description": "arguments passed to the docker build. It also accepts environment variables via the go template syntax.", + "description": "arguments passed to the docker build. It also accepts environment variables and generated values via the go template syntax. Exposed generated values: IMAGE_REPO, IMAGE_NAME, IMAGE_TAG.", + "x-intellij-html-description": "arguments passed to the docker build. It also accepts environment variables and generated values via the go template syntax. Exposed generated values: IMAGEREPO, IMAGENAME, IMAGE_TAG.", "default": "{}", "examples": [ "{\"key1\": \"value1\", \"key2\": \"value2\", \"key3\": \"'{{.ENV_VARIABLE}}'\"}" @@ -1519,9 +1519,12 @@ "env": { "items": {}, "type": "array", - "description": "environment variables passed to the kaniko pod.", - "x-intellij-html-description": "environment variables passed to the kaniko pod.", - "default": "[]" + "description": "environment variables passed to the kaniko pod. It also accepts environment variables via the go template syntax.", + "x-intellij-html-description": "environment variables passed to the kaniko pod. It also accepts environment variables via the go template syntax.", + "default": "[]", + "examples": [ + "{{name: \"key1\", value: \"value1\"}, {name: \"key2\", value: \"value2\"}, {name: \"key3\", value: \"'{{.ENV_VARIABLE}}'\"}\"}" + ] }, "flags": { "items": { diff --git a/pkg/skaffold/build/cluster/kaniko.go b/pkg/skaffold/build/cluster/kaniko.go index fd25c41fd16..c15fef23559 100644 --- a/pkg/skaffold/build/cluster/kaniko.go +++ b/pkg/skaffold/build/cluster/kaniko.go @@ -21,6 +21,7 @@ import ( "context" "fmt" "io" + "strings" "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" @@ -38,7 +39,11 @@ import ( const initContainer = "kaniko-init-container" func (b *Builder) buildWithKaniko(ctx context.Context, out io.Writer, workspace string, artifact *latest.KanikoArtifact, tag string) (string, error) { - env, err := evaluateEnv(artifact.Env) + generatedEnvs, err := generateEnvFromImage(tag) + if err != nil { + return "", fmt.Errorf("error processing generated env variables from image uri: %w", err) + } + env, err := evaluateEnv(artifact.Env, generatedEnvs...) if err != nil { return "", fmt.Errorf("unable to evaluate env variables: %w", err) } @@ -117,9 +122,15 @@ func (b *Builder) copyKanikoBuildContext(ctx context.Context, workspace string, return nil } -func evaluateEnv(env []v1.EnvVar) ([]v1.EnvVar, error) { - var evaluated []v1.EnvVar +func evaluateEnv(env []v1.EnvVar, additional ...v1.EnvVar) ([]v1.EnvVar, error) { + // Prepare additional envs + addEnv := make(map[string]string) + for _, addEnvVar := range additional { + addEnv[addEnvVar.Name] = addEnvVar.Value + } + // Evaluate provided env variables + var evaluated []v1.EnvVar for _, envVar := range env { val, err := util.ExpandEnvTemplate(envVar.Value, nil) if err != nil { @@ -127,7 +138,72 @@ func evaluateEnv(env []v1.EnvVar) ([]v1.EnvVar, error) { } evaluated = append(evaluated, v1.EnvVar{Name: envVar.Name, Value: val}) + + // Provided env variables have higher priority than additional (generated) ones + delete(addEnv, envVar.Name) + } + + // Append additional (generated) env variables + for name, value := range addEnv { + if value != "" { + evaluated = append(evaluated, v1.EnvVar{Name: name, Value: value}) + } } return evaluated, nil } + +func envMapFromVars(env []v1.EnvVar) map[string]string { + envMap := make(map[string]string) + for _, envVar := range env { + envMap[envVar.Name] = envVar.Value + } + return envMap +} + +func generateEnvFromImage(imageStr string) ([]v1.EnvVar, error) { + repoStr, nameStr, tagStr, err := parseImageParts(imageStr) + if err != nil { + return nil, err + } + var generatedEnvs []v1.EnvVar + generatedEnvs = append(generatedEnvs, v1.EnvVar{Name: "IMAGE_REPO", Value: repoStr}) + generatedEnvs = append(generatedEnvs, v1.EnvVar{Name: "IMAGE_NAME", Value: nameStr}) + generatedEnvs = append(generatedEnvs, v1.EnvVar{Name: "IMAGE_TAG", Value: tagStr}) + return generatedEnvs, nil +} + +func parseImageParts(imageStr string) (string, string, string, error) { + var repo, name, tag string + var err error + parts := strings.Split(imageStr, ":") + switch len(parts) { + case 1: + // default tag: latest + parts = append(parts, "latest") + case 2: + case 3: + if strings.ContainsRune(parts[0], '/') { + err = fmt.Errorf("invalid image uri string: %q", imageStr) + return repo, name, tag, err + } + parts[0] = parts[0] + ":" + parts[1] + parts[1] = parts[2] + parts = parts[:2] + default: + err = fmt.Errorf("invalid image uri string: %q", imageStr) + return repo, name, tag, err + } + tag = parts[1] + imageParts := strings.Split(parts[0], "/") + switch len(imageParts) { + case 0: + name = parts[1] + case 1: + name = imageParts[0] + default: + repo = strings.Join(imageParts[:len(imageParts)-1], "/") + name = imageParts[len(imageParts)-1] + } + return repo, name, tag, err +} diff --git a/pkg/skaffold/build/cluster/kaniko_test.go b/pkg/skaffold/build/cluster/kaniko_test.go new file mode 100644 index 00000000000..2b4e078b427 --- /dev/null +++ b/pkg/skaffold/build/cluster/kaniko_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2019 The Skaffold Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cluster + +import ( + "testing" + + v1 "k8s.io/api/core/v1" + + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" + "github.com/GoogleContainerTools/skaffold/testutil" +) + +func TestEnvInterpolation(t *testing.T) { + imageStr := "why.com/is/this/such/a/long/repo/name/testimage:testtag" + artifact := &latest.KanikoArtifact{ + Env: []v1.EnvVar{{Name: "hui", Value: "buh"}}, + } + generatedEnvs, err := generateEnvFromImage(imageStr) + if err != nil { + t.Fatalf("error generating env: %s", err) + } + env, err := evaluateEnv(artifact.Env, generatedEnvs...) + if err != nil { + t.Fatalf("unable to evaluate env variables: %s", err) + } + + actual := env + expected := []v1.EnvVar{ + {Name: "hui", Value: "buh"}, + {Name: "IMAGE_REPO", Value: "why.com/is/this/such/a/long/repo/name"}, + {Name: "IMAGE_NAME", Value: "testimage"}, + {Name: "IMAGE_TAG", Value: "testtag"}, + } + testutil.CheckElementsMatch(t, expected, actual) +} + +func TestEnvInterpolation_IPPort(t *testing.T) { + imageStr := "10.10.10.10:1000/is/this/such/a/long/repo/name/testimage:testtag" + artifact := &latest.KanikoArtifact{ + Env: []v1.EnvVar{{Name: "hui", Value: "buh"}}, + } + generatedEnvs, err := generateEnvFromImage(imageStr) + if err != nil { + t.Fatalf("error generating env: %s", err) + } + env, err := evaluateEnv(artifact.Env, generatedEnvs...) + if err != nil { + t.Fatalf("unable to evaluate env variables: %s", err) + } + + actual := env + expected := []v1.EnvVar{ + {Name: "hui", Value: "buh"}, + {Name: "IMAGE_REPO", Value: "10.10.10.10:1000/is/this/such/a/long/repo/name"}, + {Name: "IMAGE_NAME", Value: "testimage"}, + {Name: "IMAGE_TAG", Value: "testtag"}, + } + testutil.CheckElementsMatch(t, expected, actual) +} diff --git a/pkg/skaffold/build/cluster/pod.go b/pkg/skaffold/build/cluster/pod.go index 4cf5cde0426..4676c4e56ff 100644 --- a/pkg/skaffold/build/cluster/pod.go +++ b/pkg/skaffold/build/cluster/pod.go @@ -28,8 +28,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" - "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/version" ) @@ -250,7 +250,7 @@ func kanikoArgs(artifact *latest.KanikoArtifact, tag string, insecureRegistries args = append(args, artifact.AdditionalFlags...) } - buildArgs, err := util.EvaluateEnvTemplateMap(artifact.BuildArgs) + buildArgs, err := docker.EvaluateBuildArgs(artifact.BuildArgs, envMapFromVars(artifact.Env)) if err != nil { return nil, fmt.Errorf("unable to evaluate build args: %w", err) } diff --git a/pkg/skaffold/docker/image.go b/pkg/skaffold/docker/image.go index ae75d1df892..6996fe18aad 100644 --- a/pkg/skaffold/docker/image.go +++ b/pkg/skaffold/docker/image.go @@ -466,6 +466,37 @@ func ToCLIBuildArgs(a *latest.DockerArtifact, evaluatedArgs map[string]*string) return args, nil } +// EvaluateBuildArgs evaluates templated build args. +// An additional envMap can optionally be specified. +// If multiple additional envMaps are specified, all but the first one will be ignored +func EvaluateBuildArgs(args map[string]*string, envMap ...map[string]string) (map[string]*string, error) { + if args == nil { + return nil, nil + } + + var env map[string]string + if len(envMap) > 0 { + env = envMap[0] + } + + evaluated := map[string]*string{} + for k, v := range args { + if v == nil { + evaluated[k] = nil + continue + } + + value, err := util.ExpandEnvTemplate(*v, env) + if err != nil { + return nil, fmt.Errorf("unable to get value for build arg %q: %w", k, err) + } + + evaluated[k] = &value + } + + return evaluated, nil +} + func (l *localDaemon) Prune(ctx context.Context, out io.Writer, images []string, pruneChildren bool) error { for _, id := range images { resp, err := l.ImageRemove(ctx, id, types.ImageRemoveOptions{ diff --git a/pkg/skaffold/schema/latest/config.go b/pkg/skaffold/schema/latest/config.go index 32c09c7be42..1e7042cf584 100644 --- a/pkg/skaffold/schema/latest/config.go +++ b/pkg/skaffold/schema/latest/config.go @@ -956,11 +956,14 @@ type KanikoArtifact struct { Target string `yaml:"target,omitempty"` // BuildArgs are arguments passed to the docker build. - // It also accepts environment variables via the go template syntax. + // It also accepts environment variables and generated values via the go template syntax. + // Exposed generated values: IMAGE_REPO, IMAGE_NAME, IMAGE_TAG. // For example: `{"key1": "value1", "key2": "value2", "key3": "'{{.ENV_VARIABLE}}'"}`. BuildArgs map[string]*string `yaml:"buildArgs,omitempty"` // Env are environment variables passed to the kaniko pod. + // It also accepts environment variables via the go template syntax. + // For example: `{{name: "key1", value: "value1"}, {name: "key2", value: "value2"}, {name: "key3", value: "'{{.ENV_VARIABLE}}'"}"}`. Env []v1.EnvVar `yaml:"env,omitempty"` // InitImage is the image used to run init container which mounts kaniko context. diff --git a/testutil/util.go b/testutil/util.go index 8f1215ab482..11005d23388 100644 --- a/testutil/util.go +++ b/testutil/util.go @@ -246,6 +246,40 @@ func CheckDeepEqual(t *testing.T, expected, actual interface{}, opts ...cmp.Opti } } +// CheckElementsMatch validates that two given slices contain the same elements +// while disregarding their order. +// Elements of both slices have to be comparable by '==' +func CheckElementsMatch(t *testing.T, expected, actual interface{}) { + t.Helper() + expectedSlc, err := interfaceSlice(expected) + if err != nil { + t.Fatalf("error converting `expected` to interface slice: %s", err) + } + actualSlc, err := interfaceSlice(actual) + if err != nil { + t.Fatalf("error converting `actual` to interface slice: %s", err) + } + expectedLen := len(expectedSlc) + actualLen := len(actualSlc) + + if expectedLen != actualLen { + t.Fatalf("length of the slices differ: Expected %d, but was %d", expectedLen, actualLen) + } + + wmap := make(map[interface{}]int) + for _, elem := range expectedSlc { + wmap[elem]++ + } + for _, elem := range actualSlc { + wmap[elem]-- + } + for _, v := range wmap { + if v != 0 { + t.Fatalf("elements are missing (negative integers) or excess (positive integers): %#v", wmap) + } + } +} + func CheckErrorAndDeepEqual(t *testing.T, shouldErr bool, err error, expected, actual interface{}, opts ...cmp.Option) { t.Helper() if err := checkErr(shouldErr, err); err != nil { @@ -281,6 +315,18 @@ func checkErr(shouldErr bool, err error) error { return nil } +func interfaceSlice(slice interface{}) ([]interface{}, error) { + s := reflect.ValueOf(slice) + if s.Kind() != reflect.Slice { + return nil, fmt.Errorf("not a slice") + } + ret := make([]interface{}, s.Len()) + for i := 0; i < s.Len(); i++ { + ret[i] = s.Index(i).Interface() + } + return ret, nil +} + // ServeFile serves a file with http. Returns the url to the file and a teardown // function that should be called to properly stop the server. func ServeFile(t *testing.T, content []byte) string {