Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement env variable expansion for kaniko builds #4557

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions docs/content/en/schemas/v2beta9.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: IMAGE<em>REPO, IMAGE</em>NAME, IMAGE_TAG.",
"default": "{}",
"examples": [
"{\"key1\": \"value1\", \"key2\": \"value2\", \"key3\": \"'{{.ENV_VARIABLE}}'\"}"
Expand Down Expand Up @@ -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",
Expand Down
66 changes: 66 additions & 0 deletions pkg/skaffold/build/cluster/kaniko.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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)
Expand Down Expand Up @@ -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
}
98 changes: 98 additions & 0 deletions pkg/skaffold/build/cluster/kaniko_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
Copyright 2019 The Skaffold Authors
DanielSel marked this conversation as resolved.
Show resolved Hide resolved

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)
}
6 changes: 6 additions & 0 deletions pkg/skaffold/build/cluster/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions pkg/skaffold/docker/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion pkg/skaffold/docker/reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
5 changes: 4 additions & 1 deletion pkg/skaffold/schema/latest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`

Expand Down
46 changes: 46 additions & 0 deletions testutil/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down