Skip to content

Commit

Permalink
add IMAGE_REPO, IMAGE_NAME, IMAGE_TAG to generated env variables (if …
Browse files Browse the repository at this point in the history
…not set explicitly). resolves #3229
  • Loading branch information
DanielSel committed Jul 24, 2020
1 parent 625b1ed commit 9296c71
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 12 deletions.
13 changes: 8 additions & 5 deletions docs/content/en/schemas/v2beta6.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: IMAGE<em>REPO, IMAGE</em>NAME, IMAGE_TAG.",
"default": "{}",
"examples": [
"{\"key1\": \"value1\", \"key2\": \"value2\", \"key3\": \"'{{.ENV_VARIABLE}}'\"}"
Expand All @@ -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": {
Expand Down
67 changes: 64 additions & 3 deletions pkg/skaffold/build/cluster/kaniko.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"context"
"fmt"
"io"
"strings"

"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
Expand All @@ -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)
}
Expand Down Expand Up @@ -119,17 +124,73 @@ 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 {
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) {
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...)
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 @@ -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)
}
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 @@ -458,19 +458,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 @@ -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.
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

0 comments on commit 9296c71

Please sign in to comment.