diff --git a/docs/content/en/schemas/v2beta9.json b/docs/content/en/schemas/v2beta9.json index ef9a2b63a75..f16b5c47a1a 100755 --- a/docs/content/en/schemas/v2beta9.json +++ b/docs/content/en/schemas/v2beta9.json @@ -1623,8 +1623,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}}'\"}" @@ -1655,9 +1655,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}}'\"}\"}" + ] }, "force": { "type": "boolean", diff --git a/pkg/skaffold/build/cluster/kaniko.go b/pkg/skaffold/build/cluster/kaniko.go index 07a5042bbba..5ef10201ae8 100644 --- a/pkg/skaffold/build/cluster/kaniko.go +++ b/pkg/skaffold/build/cluster/kaniko.go @@ -23,6 +23,7 @@ import ( "io" "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" @@ -31,11 +32,22 @@ import ( "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes" kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" ) const initContainer = "kaniko-init-container" func (b *Builder) buildWithKaniko(ctx context.Context, out io.Writer, workspace string, artifact *latest.KanikoArtifact, tag string) (string, error) { + 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) + } + artifact.Env = env + client, err := kubernetesclient.Client() if err != nil { return "", fmt.Errorf("getting Kubernetes client: %w", err) @@ -111,3 +123,57 @@ func (b *Builder) copyKanikoBuildContext(ctx context.Context, workspace string, return nil } + +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 { + return nil, fmt.Errorf("unable to get value for env variable %q: %w", envVar.Name, err) + } + + 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) { + imgRef, err := docker.ParseReference(imageStr) + if err != nil { + return nil, err + } + if imgRef.Tag == "" { + imgRef.Tag = "latest" + } + var generatedEnvs []v1.EnvVar + generatedEnvs = append(generatedEnvs, v1.EnvVar{Name: "IMAGE_REPO", Value: imgRef.Repo}) + generatedEnvs = append(generatedEnvs, v1.EnvVar{Name: "IMAGE_NAME", Value: imgRef.Name}) + generatedEnvs = append(generatedEnvs, v1.EnvVar{Name: "IMAGE_TAG", Value: imgRef.Tag}) + return generatedEnvs, nil +} diff --git a/pkg/skaffold/build/cluster/kaniko_test.go b/pkg/skaffold/build/cluster/kaniko_test.go new file mode 100644 index 00000000000..750aabf0902 --- /dev/null +++ b/pkg/skaffold/build/cluster/kaniko_test.go @@ -0,0 +1,98 @@ +/* +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) +} + +func TestEnvInterpolation_Latest(t *testing.T) { + imageStr := "why.com/is/this/such/a/long/repo/name/testimage" + 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: "latest"}, + } + testutil.CheckElementsMatch(t, expected, actual) +} diff --git a/pkg/skaffold/build/cluster/pod.go b/pkg/skaffold/build/cluster/pod.go index c8d83c85fea..b2ad881b0c3 100644 --- a/pkg/skaffold/build/cluster/pod.go +++ b/pkg/skaffold/build/cluster/pod.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/kaniko" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/version" ) @@ -239,6 +240,11 @@ func kanikoArgs(artifact *latest.KanikoArtifact, tag string, insecureRegistries } // Create pod spec + buildArgs, err := docker.EvaluateBuildArgs(artifact.BuildArgs, envMapFromVars(artifact.Env)) + if err != nil { + return nil, fmt.Errorf("unable to evaluate environment variables in build args: %w", err) + } + artifact.BuildArgs = buildArgs args, err := kaniko.Args(artifact, tag, fmt.Sprintf("dir://%s", kaniko.DefaultEmptyDirMountPath)) if err != nil { return nil, fmt.Errorf("unable build kaniko args: %w", err) diff --git a/pkg/skaffold/docker/image.go b/pkg/skaffold/docker/image.go index e92ed484060..423385b0fc6 100644 --- a/pkg/skaffold/docker/image.go +++ b/pkg/skaffold/docker/image.go @@ -507,6 +507,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, images []string, pruneChildren bool) ([]string, error) { var pruned []string var errRt error diff --git a/pkg/skaffold/docker/reference.go b/pkg/skaffold/docker/reference.go index ac19441d9bf..8edb24e04dd 100644 --- a/pkg/skaffold/docker/reference.go +++ b/pkg/skaffold/docker/reference.go @@ -16,13 +16,19 @@ limitations under the License. package docker -import "github.com/docker/distribution/reference" +import ( + "strings" + + "github.com/docker/distribution/reference" +) // ImageReference is a parsed image name. type ImageReference struct { BaseName string Domain string Path string + Repo string + Name string Tag string Digest string FullyQualified bool @@ -55,5 +61,11 @@ func ParseReference(image string) (*ImageReference, error) { parsed.FullyQualified = true } + repoParts := strings.Split(parsed.BaseName, "/") + if len(repoParts) > 1 { + parsed.Repo = strings.Join(repoParts[:len(repoParts)-1], "/") + parsed.Name = repoParts[len(repoParts)-1] + } + return parsed, nil } diff --git a/pkg/skaffold/schema/latest/config.go b/pkg/skaffold/schema/latest/config.go index b0bd671ca46..f60427c9d13 100644 --- a/pkg/skaffold/schema/latest/config.go +++ b/pkg/skaffold/schema/latest/config.go @@ -1067,6 +1067,8 @@ type KanikoArtifact struct { SkipTLSVerifyRegistry []string `yaml:"skipTLSVerifyRegistry,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"` // Cache configures Kaniko caching. If a cache is specified, Kaniko will @@ -1082,7 +1084,8 @@ type KanikoArtifact struct { Label map[string]*string `yaml:"label,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"` 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 {