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 #3974

Closed
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/v2beta3.json
Original file line number Diff line number Diff line change
Expand Up @@ -1398,8 +1398,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 All @@ -1419,9 +1419,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": {
Expand Down
84 changes: 84 additions & 0 deletions pkg/skaffold/build/cluster/kaniko.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,33 @@ import (
"context"
"fmt"
"io"
"strings"

"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"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes"
"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 := kubernetes.Client()
if err != nil {
return "", fmt.Errorf("getting Kubernetes client: %w", err)
Expand Down Expand Up @@ -110,3 +123,74 @@ 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) {
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) {
DanielSel marked this conversation as resolved.
Show resolved Hide resolved
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
}
50 changes: 50 additions & 0 deletions pkg/skaffold/build/cluster/kaniko_test.go
Original file line number Diff line number Diff line change
@@ -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...)
Copy link

@joanfabregat joanfabregat Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation in v2beta3.json states that the env vars are available for buildArgs: but it seems that is it only the case for env:. @DanielSel Should I report a bug?

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)
}
2 changes: 1 addition & 1 deletion pkg/skaffold/build/cluster/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,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)
}
Expand Down
11 changes: 9 additions & 2 deletions pkg/skaffold/docker/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,19 +448,26 @@ 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 {
evaluated[k] = nil
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)
}
Expand Down
5 changes: 4 additions & 1 deletion pkg/skaffold/schema/latest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -782,11 +782,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.
Expand Down
46 changes: 46 additions & 0 deletions testutil/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down