diff --git a/docs/content/en/schemas/v2beta6.json b/docs/content/en/schemas/v2beta6.json index b08df6c0ece..79e913db949 100755 --- a/docs/content/en/schemas/v2beta6.json +++ b/docs/content/en/schemas/v2beta6.json @@ -1456,8 +1456,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}}'\"}" @@ -1477,9 +1477,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 4063cd9d6b2..fb77c927fe4 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" @@ -37,7 +38,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) } @@ -119,9 +124,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 { @@ -129,7 +140,57 @@ 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) (repo, name, tag string, err error) { + parts := strings.Split(imageStr, ":") + if len(parts) != 2 { + err = fmt.Errorf("invalid image uri string: %q", imageStr) + return + } + 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 +} diff --git a/pkg/skaffold/build/cluster/kaniko_test.go b/pkg/skaffold/build/cluster/kaniko_test.go new file mode 100644 index 00000000000..f2f149a37c2 --- /dev/null +++ b/pkg/skaffold/build/cluster/kaniko_test.go @@ -0,0 +1,50 @@ +/* +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 processing generated env variables from image uri: %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) +} diff --git a/pkg/skaffold/build/cluster/pod.go b/pkg/skaffold/build/cluster/pod.go index 42822fb5f49..4676c4e56ff 100644 --- a/pkg/skaffold/build/cluster/pod.go +++ b/pkg/skaffold/build/cluster/pod.go @@ -250,7 +250,7 @@ func kanikoArgs(artifact *latest.KanikoArtifact, tag string, insecureRegistries args = append(args, artifact.AdditionalFlags...) } - buildArgs, err := docker.EvaluateBuildArgs(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 86a6f64e828..5b3ea1bd000 100644 --- a/pkg/skaffold/docker/image.go +++ b/pkg/skaffold/docker/image.go @@ -458,11 +458,18 @@ func GetBuildArgs(a *latest.DockerArtifact) ([]string, error) { } // EvaluateBuildArgs evaluates templated build args. -func EvaluateBuildArgs(args map[string]*string) (map[string]*string, error) { +// 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 { @@ -470,7 +477,7 @@ func EvaluateBuildArgs(args map[string]*string) (map[string]*string, error) { continue } - value, err := util.ExpandEnvTemplate(*v, nil) + value, err := util.ExpandEnvTemplate(*v, env) if err != nil { return nil, fmt.Errorf("unable to get value for build arg %q: %w", k, err) } diff --git a/pkg/skaffold/schema/latest/config.go b/pkg/skaffold/schema/latest/config.go index d699b79deea..ba912eab38a 100644 --- a/pkg/skaffold/schema/latest/config.go +++ b/pkg/skaffold/schema/latest/config.go @@ -851,11 +851,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 643f3990efb..6cd894354d8 100644 --- a/testutil/util.go +++ b/testutil/util.go @@ -238,6 +238,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 { @@ -273,6 +307,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 {