661 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			661 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
/*
 | 
						|
Copyright 2018 Google LLC
 | 
						|
 | 
						|
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 dockerfile
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"io/ioutil"
 | 
						|
	"os"
 | 
						|
	"reflect"
 | 
						|
	"testing"
 | 
						|
 | 
						|
	"github.com/GoogleContainerTools/kaniko/pkg/config"
 | 
						|
	"github.com/GoogleContainerTools/kaniko/testutil"
 | 
						|
	v1 "github.com/google/go-containerregistry/pkg/v1"
 | 
						|
	"github.com/moby/buildkit/frontend/dockerfile/instructions"
 | 
						|
)
 | 
						|
 | 
						|
func Test_ParseStages_ArgValueWithQuotes(t *testing.T) {
 | 
						|
	dockerfile := `
 | 
						|
	ARG IMAGE="ubuntu:16.04"
 | 
						|
	ARG FOO=bar
 | 
						|
	ARG HELLO="Hello"
 | 
						|
	ARG WORLD="World"
 | 
						|
	ARG NESTED="$HELLO $WORLD"
 | 
						|
	FROM ${IMAGE}
 | 
						|
	RUN echo hi > /hi
 | 
						|
 | 
						|
	FROM scratch AS second
 | 
						|
	COPY --from=0 /hi /hi2
 | 
						|
 | 
						|
	FROM scratch
 | 
						|
	COPY --from=second /hi2 /hi3
 | 
						|
	`
 | 
						|
	tmpfile, err := ioutil.TempFile("", "Dockerfile.test")
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	defer os.Remove(tmpfile.Name())
 | 
						|
 | 
						|
	if _, err := tmpfile.Write([]byte(dockerfile)); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	if err := tmpfile.Close(); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	stages, metaArgs, err := ParseStages(&config.KanikoOptions{DockerfilePath: tmpfile.Name()})
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(stages) == 0 {
 | 
						|
		t.Fatal("length of stages expected to be greater than zero, but was zero")
 | 
						|
	}
 | 
						|
 | 
						|
	if len(metaArgs) != 5 {
 | 
						|
		t.Fatalf("length of stage meta args expected to be 5, but was %d", len(metaArgs))
 | 
						|
	}
 | 
						|
 | 
						|
	for i, expectedVal := range []string{"ubuntu:16.04", "bar", "Hello", "World", "Hello World"} {
 | 
						|
		if metaArgs[i].Args[0].ValueString() != expectedVal {
 | 
						|
			t.Fatalf("expected metaArg %d val to be %s but was %s", i, expectedVal, metaArgs[i].Args[0].ValueString())
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func Test_stripEnclosingQuotes(t *testing.T) {
 | 
						|
	type testCase struct {
 | 
						|
		name     string
 | 
						|
		inArgs   []instructions.ArgCommand
 | 
						|
		expected []string
 | 
						|
		success  bool
 | 
						|
	}
 | 
						|
 | 
						|
	newArgCommand := func(key, val string) instructions.ArgCommand {
 | 
						|
		return instructions.ArgCommand{
 | 
						|
			Args: []instructions.KeyValuePairOptional{{Key: key, Value: &val}},
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	cases := []testCase{{
 | 
						|
		name:     "value with no enclosing quotes",
 | 
						|
		inArgs:   []instructions.ArgCommand{newArgCommand("MEOW", "Purr")},
 | 
						|
		expected: []string{"Purr"},
 | 
						|
		success:  true,
 | 
						|
	}, {
 | 
						|
		name:   "value with unmatched leading double quote",
 | 
						|
		inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "\"Purr")},
 | 
						|
	}, {
 | 
						|
		name:   "value with unmatched trailing double quote",
 | 
						|
		inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "Purr\"")},
 | 
						|
	}, {
 | 
						|
		name:     "value with enclosing double quotes",
 | 
						|
		inArgs:   []instructions.ArgCommand{newArgCommand("MEOW", "\"mrow\"")},
 | 
						|
		expected: []string{"mrow"},
 | 
						|
		success:  true,
 | 
						|
	}, {
 | 
						|
		name:   "value with unmatched leading single quote",
 | 
						|
		inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "'Purr")},
 | 
						|
	}, {
 | 
						|
		name:   "value with unmatched trailing single quote",
 | 
						|
		inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "Purr'")},
 | 
						|
	}, {
 | 
						|
		name:     "value with enclosing single quotes",
 | 
						|
		inArgs:   []instructions.ArgCommand{newArgCommand("MEOW", "'mrow'")},
 | 
						|
		expected: []string{"mrow"},
 | 
						|
		success:  true,
 | 
						|
	}, {
 | 
						|
		name:     "blank value with enclosing double quotes",
 | 
						|
		inArgs:   []instructions.ArgCommand{newArgCommand("MEOW", `""`)},
 | 
						|
		expected: []string{""},
 | 
						|
		success:  true,
 | 
						|
	}, {
 | 
						|
		name:     "blank value with enclosing single quotes",
 | 
						|
		inArgs:   []instructions.ArgCommand{newArgCommand("MEOW", "''")},
 | 
						|
		expected: []string{""},
 | 
						|
		success:  true,
 | 
						|
	}, {
 | 
						|
		name:     "value with escaped, enclosing double quotes",
 | 
						|
		inArgs:   []instructions.ArgCommand{newArgCommand("MEOW", `\"Purr\"`)},
 | 
						|
		expected: []string{`\"Purr\"`},
 | 
						|
		success:  true,
 | 
						|
	}, {
 | 
						|
		name:     "value with escaped, enclosing single quotes",
 | 
						|
		inArgs:   []instructions.ArgCommand{newArgCommand("MEOW", `\'Purr\'`)},
 | 
						|
		expected: []string{`\'Purr\'`},
 | 
						|
		success:  true,
 | 
						|
	}, {
 | 
						|
		name: "multiple values enclosed with single quotes",
 | 
						|
		inArgs: []instructions.ArgCommand{
 | 
						|
			newArgCommand("MEOW", `'Purr'`),
 | 
						|
			newArgCommand("MEW", `'Mrow'`),
 | 
						|
		},
 | 
						|
		expected: []string{"Purr", "Mrow"},
 | 
						|
		success:  true,
 | 
						|
	}, {
 | 
						|
		name: "multiple values, one blank, one a single int",
 | 
						|
		inArgs: []instructions.ArgCommand{
 | 
						|
			newArgCommand("MEOW", `""`),
 | 
						|
			newArgCommand("MEW", `1`),
 | 
						|
		},
 | 
						|
		expected: []string{"", "1"},
 | 
						|
		success:  true,
 | 
						|
	}, {
 | 
						|
		name:    "no values",
 | 
						|
		success: true,
 | 
						|
	}}
 | 
						|
 | 
						|
	for _, test := range cases {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			inArgs := test.inArgs
 | 
						|
			expected := test.expected
 | 
						|
			success := test.success
 | 
						|
 | 
						|
			out, err := stripEnclosingQuotes(inArgs)
 | 
						|
			if success && err != nil {
 | 
						|
				t.Fatal(err)
 | 
						|
			}
 | 
						|
 | 
						|
			if !success && err == nil {
 | 
						|
				t.Fatal("expected an error but none received")
 | 
						|
			}
 | 
						|
 | 
						|
			if len(expected) != len(out) {
 | 
						|
				t.Fatalf("Expected %d args but got %d", len(expected), len(out))
 | 
						|
			}
 | 
						|
 | 
						|
			for i := range out {
 | 
						|
				if expected[i] != out[i].Args[0].ValueString() {
 | 
						|
					t.Errorf(
 | 
						|
						"Expected arg at index %d to equal %v but instead equaled %v",
 | 
						|
						i,
 | 
						|
						expected[i],
 | 
						|
						out[i].Args[0].ValueString())
 | 
						|
				}
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func Test_GetOnBuildInstructions(t *testing.T) {
 | 
						|
	type testCase struct {
 | 
						|
		name        string
 | 
						|
		cfg         *v1.Config
 | 
						|
		stageToIdx  map[string]string
 | 
						|
		expCommands []instructions.Command
 | 
						|
	}
 | 
						|
 | 
						|
	tests := []testCase{
 | 
						|
		{name: "no on-build on config",
 | 
						|
			cfg:         &v1.Config{},
 | 
						|
			stageToIdx:  map[string]string{"builder": "0"},
 | 
						|
			expCommands: nil,
 | 
						|
		},
 | 
						|
		{name: "onBuild on config, nothing to resolve",
 | 
						|
			cfg:         &v1.Config{OnBuild: []string{"WORKDIR /app"}},
 | 
						|
			stageToIdx:  map[string]string{"builder": "0", "temp": "1"},
 | 
						|
			expCommands: []instructions.Command{&instructions.WorkdirCommand{Path: "/app"}},
 | 
						|
		},
 | 
						|
		{name: "onBuild on config, resolve multiple stages",
 | 
						|
			cfg:        &v1.Config{OnBuild: []string{"COPY --from=builder a.txt b.txt", "COPY --from=temp /app /app"}},
 | 
						|
			stageToIdx: map[string]string{"builder": "0", "temp": "1"},
 | 
						|
			expCommands: []instructions.Command{
 | 
						|
				&instructions.CopyCommand{
 | 
						|
					SourcesAndDest: instructions.SourcesAndDest{SourcePaths: []string{"a.txt"}, DestPath: "b.txt"},
 | 
						|
					From:           "0",
 | 
						|
				},
 | 
						|
				&instructions.CopyCommand{
 | 
						|
					SourcesAndDest: instructions.SourcesAndDest{SourcePaths: []string{"/app"}, DestPath: "/app"},
 | 
						|
					From:           "1",
 | 
						|
				},
 | 
						|
			}},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, test := range tests {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			cmds, err := GetOnBuildInstructions(test.cfg, test.stageToIdx)
 | 
						|
			if err != nil {
 | 
						|
				t.Fatalf("Failed to parse config for on-build instructions")
 | 
						|
			}
 | 
						|
			if len(cmds) != len(test.expCommands) {
 | 
						|
				t.Fatalf("Expected %d commands, got %d", len(test.expCommands), len(cmds))
 | 
						|
			}
 | 
						|
 | 
						|
			for i, cmd := range cmds {
 | 
						|
				if reflect.TypeOf(cmd) != reflect.TypeOf(test.expCommands[i]) {
 | 
						|
					t.Fatalf("Got command %s, expected %s", cmd, test.expCommands[i])
 | 
						|
				}
 | 
						|
				switch c := cmd.(type) {
 | 
						|
				case *instructions.CopyCommand:
 | 
						|
					{
 | 
						|
						exp := test.expCommands[i].(*instructions.CopyCommand)
 | 
						|
						testutil.CheckDeepEqual(t, exp.From, c.From)
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func Test_targetStage(t *testing.T) {
 | 
						|
	dockerfile := `
 | 
						|
	FROM scratch
 | 
						|
	RUN echo hi > /hi
 | 
						|
 | 
						|
	FROM scratch AS second
 | 
						|
	COPY --from=0 /hi /hi2
 | 
						|
 | 
						|
	FROM scratch AS UPPER_CASE
 | 
						|
	COPY --from=0 /hi /hi2
 | 
						|
 | 
						|
	FROM scratch
 | 
						|
	COPY --from=second /hi2 /hi3
 | 
						|
	`
 | 
						|
	stages, _, err := Parse([]byte(dockerfile))
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	tests := []struct {
 | 
						|
		name        string
 | 
						|
		target      string
 | 
						|
		targetIndex int
 | 
						|
		shouldErr   bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:        "test valid target",
 | 
						|
			target:      "second",
 | 
						|
			targetIndex: 1,
 | 
						|
			shouldErr:   false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:        "test valid upper case target",
 | 
						|
			target:      "UPPER_CASE",
 | 
						|
			targetIndex: 2,
 | 
						|
			shouldErr:   false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:        "test no target",
 | 
						|
			target:      "",
 | 
						|
			targetIndex: 3,
 | 
						|
			shouldErr:   false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:        "test invalid target",
 | 
						|
			target:      "invalid",
 | 
						|
			targetIndex: -1,
 | 
						|
			shouldErr:   true,
 | 
						|
		},
 | 
						|
	}
 | 
						|
	for _, test := range tests {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			target, err := targetStage(stages, test.target)
 | 
						|
			testutil.CheckError(t, test.shouldErr, err)
 | 
						|
			if !test.shouldErr {
 | 
						|
				if target != test.targetIndex {
 | 
						|
					t.Errorf("got incorrect target, expected %d got %d", test.targetIndex, target)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func Test_SaveStage(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name     string
 | 
						|
		index    int
 | 
						|
		expected bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:     "reference stage in later copy command",
 | 
						|
			index:    0,
 | 
						|
			expected: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:     "reference stage in later from command",
 | 
						|
			index:    1,
 | 
						|
			expected: true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:     "don't reference stage later",
 | 
						|
			index:    2,
 | 
						|
			expected: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:     "reference current stage in next stage",
 | 
						|
			index:    4,
 | 
						|
			expected: true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:     "from prebuilt stage, and reference current stage in next stage",
 | 
						|
			index:    5,
 | 
						|
			expected: true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:     "final stage",
 | 
						|
			index:    6,
 | 
						|
			expected: false,
 | 
						|
		},
 | 
						|
	}
 | 
						|
	stages, _, err := Parse([]byte(testutil.Dockerfile))
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("couldn't retrieve stages from Dockerfile: %v", err)
 | 
						|
	}
 | 
						|
	for _, test := range tests {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			actual := saveStage(test.index, stages)
 | 
						|
			testutil.CheckErrorAndDeepEqual(t, false, nil, test.expected, actual)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func Test_baseImageIndex(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name         string
 | 
						|
		currentStage int
 | 
						|
		expected     int
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:         "stage that is built off of a previous stage",
 | 
						|
			currentStage: 2,
 | 
						|
			expected:     1,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:         "another stage that is built off of a previous stage",
 | 
						|
			currentStage: 5,
 | 
						|
			expected:     4,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:         "stage that isn't built off of a previous stage",
 | 
						|
			currentStage: 4,
 | 
						|
			expected:     -1,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	stages, _, err := Parse([]byte(testutil.Dockerfile))
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("couldn't retrieve stages from Dockerfile: %v", err)
 | 
						|
	}
 | 
						|
	for _, test := range tests {
 | 
						|
		t.Run(test.name, func(t *testing.T) {
 | 
						|
			actual := baseImageIndex(test.currentStage, stages)
 | 
						|
			if actual != test.expected {
 | 
						|
				t.Fatalf("unexpected result, expected %d got %d", test.expected, actual)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func Test_ResolveStagesArgs(t *testing.T) {
 | 
						|
	dockerfile := `
 | 
						|
	ARG IMAGE="ubuntu:16.04"
 | 
						|
	ARG LAST_STAGE_VARIANT
 | 
						|
	FROM ${IMAGE} as base
 | 
						|
	RUN echo hi > /hi
 | 
						|
	FROM base AS base-dev
 | 
						|
	RUN echo dev >> /hi
 | 
						|
	FROM base AS base-prod
 | 
						|
	RUN echo prod >> /hi
 | 
						|
	FROM base-${LAST_STAGE_VARIANT}
 | 
						|
	RUN cat /hi
 | 
						|
	`
 | 
						|
 | 
						|
	buildArgLastVariants := []string{"dev", "prod"}
 | 
						|
	buildArgImages := []string{"alpine:3.11", ""}
 | 
						|
	var expectedImage string
 | 
						|
 | 
						|
	for _, buildArgLastVariant := range buildArgLastVariants {
 | 
						|
		for _, buildArgImage := range buildArgImages {
 | 
						|
			if buildArgImage != "" {
 | 
						|
				expectedImage = buildArgImage
 | 
						|
			} else {
 | 
						|
				expectedImage = "ubuntu:16.04"
 | 
						|
			}
 | 
						|
			buildArgs := []string{fmt.Sprintf("IMAGE=%s", buildArgImage), fmt.Sprintf("LAST_STAGE_VARIANT=%s", buildArgLastVariant)}
 | 
						|
 | 
						|
			stages, metaArgs, err := Parse([]byte(dockerfile))
 | 
						|
			if err != nil {
 | 
						|
				t.Fatal(err)
 | 
						|
			}
 | 
						|
			stagesLen := len(stages)
 | 
						|
			args := unifyArgs(metaArgs, buildArgs)
 | 
						|
			if err := resolveStagesArgs(stages, args); err != nil {
 | 
						|
				t.Fatalf("fail to resolves args %v: %v", buildArgs, err)
 | 
						|
			}
 | 
						|
			tests := []struct {
 | 
						|
				name               string
 | 
						|
				actualSourceCode   string
 | 
						|
				actualBaseName     string
 | 
						|
				expectedSourceCode string
 | 
						|
				expectedBaseName   string
 | 
						|
			}{
 | 
						|
				{
 | 
						|
					name:               "Test_BuildArg_From_First_Stage",
 | 
						|
					actualSourceCode:   stages[0].SourceCode,
 | 
						|
					actualBaseName:     stages[0].BaseName,
 | 
						|
					expectedSourceCode: "FROM ${IMAGE} as base",
 | 
						|
					expectedBaseName:   expectedImage,
 | 
						|
				},
 | 
						|
				{
 | 
						|
					name:               "Test_BuildArg_From_Last_Stage",
 | 
						|
					actualSourceCode:   stages[stagesLen-1].SourceCode,
 | 
						|
					actualBaseName:     stages[stagesLen-1].BaseName,
 | 
						|
					expectedSourceCode: "FROM base-${LAST_STAGE_VARIANT}",
 | 
						|
					expectedBaseName:   fmt.Sprintf("base-%s", buildArgLastVariant),
 | 
						|
				},
 | 
						|
			}
 | 
						|
			for _, test := range tests {
 | 
						|
				t.Run(test.name, func(t *testing.T) {
 | 
						|
					testutil.CheckDeepEqual(t, test.expectedSourceCode, test.actualSourceCode)
 | 
						|
					testutil.CheckDeepEqual(t, test.expectedBaseName, test.actualBaseName)
 | 
						|
				})
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func Test_SkipingUnusedStages(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		description                   string
 | 
						|
		dockerfile                    string
 | 
						|
		targets                       []string
 | 
						|
		expectedSourceCodes           map[string][]string
 | 
						|
		expectedTargetIndexBeforeSkip map[string]int
 | 
						|
		expectedTargetIndexAfterSkip  map[string]int
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			description: "dockerfile_without_copyFrom",
 | 
						|
			dockerfile: `
 | 
						|
			FROM alpine:3.11 AS base-dev
 | 
						|
			RUN echo dev > /hi
 | 
						|
			FROM alpine:3.11 AS base-prod
 | 
						|
			RUN echo prod > /hi
 | 
						|
			FROM base-dev as final-stage
 | 
						|
			RUN cat /hi
 | 
						|
			`,
 | 
						|
			targets: []string{"base-dev", "base-prod", ""},
 | 
						|
			expectedSourceCodes: map[string][]string{
 | 
						|
				"base-dev":  {"FROM alpine:3.11 AS base-dev"},
 | 
						|
				"base-prod": {"FROM alpine:3.11 AS base-prod"},
 | 
						|
				"":          {"FROM alpine:3.11 AS base-dev", "FROM base-dev as final-stage"},
 | 
						|
			},
 | 
						|
			expectedTargetIndexBeforeSkip: map[string]int{
 | 
						|
				"base-dev":  0,
 | 
						|
				"base-prod": 1,
 | 
						|
				"":          2,
 | 
						|
			},
 | 
						|
			expectedTargetIndexAfterSkip: map[string]int{
 | 
						|
				"base-dev":  0,
 | 
						|
				"base-prod": 0,
 | 
						|
				"":          1,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			description: "dockerfile_with_copyFrom",
 | 
						|
			dockerfile: `
 | 
						|
			FROM alpine:3.11 AS base-dev
 | 
						|
			RUN echo dev > /hi
 | 
						|
			FROM alpine:3.11 AS base-prod
 | 
						|
			RUN echo prod > /hi
 | 
						|
			FROM alpine:3.11
 | 
						|
			COPY --from=base-prod /hi /finalhi
 | 
						|
			RUN cat /finalhi
 | 
						|
			`,
 | 
						|
			targets: []string{"base-dev", "base-prod", ""},
 | 
						|
			expectedSourceCodes: map[string][]string{
 | 
						|
				"base-dev":  {"FROM alpine:3.11 AS base-dev"},
 | 
						|
				"base-prod": {"FROM alpine:3.11 AS base-prod"},
 | 
						|
				"":          {"FROM alpine:3.11 AS base-prod", "FROM alpine:3.11"},
 | 
						|
			},
 | 
						|
			expectedTargetIndexBeforeSkip: map[string]int{
 | 
						|
				"base-dev":  0,
 | 
						|
				"base-prod": 1,
 | 
						|
				"":          2,
 | 
						|
			},
 | 
						|
			expectedTargetIndexAfterSkip: map[string]int{
 | 
						|
				"base-dev":  0,
 | 
						|
				"base-prod": 0,
 | 
						|
				"":          1,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			description: "dockerfile_with_two_copyFrom",
 | 
						|
			dockerfile: `
 | 
						|
			FROM alpine:3.11 AS base-dev
 | 
						|
			RUN echo dev > /hi
 | 
						|
			FROM alpine:3.11 AS base-prod
 | 
						|
			RUN echo prod > /hi
 | 
						|
			FROM alpine:3.11
 | 
						|
			COPY --from=base-dev /hi /finalhidev
 | 
						|
			COPY --from=base-prod /hi /finalhiprod
 | 
						|
			RUN cat /finalhidev
 | 
						|
			RUN cat /finalhiprod
 | 
						|
			`,
 | 
						|
			targets: []string{"base-dev", "base-prod", ""},
 | 
						|
			expectedSourceCodes: map[string][]string{
 | 
						|
				"base-dev":  {"FROM alpine:3.11 AS base-dev"},
 | 
						|
				"base-prod": {"FROM alpine:3.11 AS base-prod"},
 | 
						|
				"":          {"FROM alpine:3.11 AS base-dev", "FROM alpine:3.11 AS base-prod", "FROM alpine:3.11"},
 | 
						|
			},
 | 
						|
			expectedTargetIndexBeforeSkip: map[string]int{
 | 
						|
				"base-dev":  0,
 | 
						|
				"base-prod": 1,
 | 
						|
				"":          2,
 | 
						|
			},
 | 
						|
			expectedTargetIndexAfterSkip: map[string]int{
 | 
						|
				"base-dev":  0,
 | 
						|
				"base-prod": 0,
 | 
						|
				"":          2,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			description: "dockerfile_with_two_copyFrom_and_arg",
 | 
						|
			dockerfile: `
 | 
						|
			FROM debian:10.13 as base
 | 
						|
			COPY . .
 | 
						|
			FROM scratch as second
 | 
						|
			ENV foopath context/foo
 | 
						|
			COPY --from=0 $foopath context/b* /foo/
 | 
						|
			FROM second as third
 | 
						|
			COPY --from=base /context/foo /new/foo
 | 
						|
			FROM base as fourth
 | 
						|
			# Make sure that we snapshot intermediate images correctly
 | 
						|
			RUN date > /date
 | 
						|
			ENV foo bar
 | 
						|
			# This base image contains symlinks with relative paths to ignored directories
 | 
						|
			# We need to test they're extracted correctly
 | 
						|
			FROM fedora@sha256:c4cc32b09c6ae3f1353e7e33a8dda93dc41676b923d6d89afa996b421cc5aa48
 | 
						|
			FROM fourth
 | 
						|
			ARG file=/foo2
 | 
						|
			COPY --from=second /foo ${file}
 | 
						|
			COPY --from=debian:10.13 /etc/os-release /new
 | 
						|
			`,
 | 
						|
			targets: []string{"base", ""},
 | 
						|
			expectedSourceCodes: map[string][]string{
 | 
						|
				"base":   {"FROM debian:10.13 as base"},
 | 
						|
				"second": {"FROM debian:10.13 as base", "FROM scratch as second"},
 | 
						|
				"":       {"FROM debian:10.13 as base", "FROM scratch as second", "FROM base as fourth", "FROM fourth"},
 | 
						|
			},
 | 
						|
			expectedTargetIndexBeforeSkip: map[string]int{
 | 
						|
				"base":   0,
 | 
						|
				"second": 1,
 | 
						|
				"":       5,
 | 
						|
			},
 | 
						|
			expectedTargetIndexAfterSkip: map[string]int{
 | 
						|
				"base":   0,
 | 
						|
				"second": 1,
 | 
						|
				"":       3,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			description: "dockerfile_without_final_dependencies",
 | 
						|
			dockerfile: `
 | 
						|
			FROM alpine:3.11
 | 
						|
			FROM debian:10.13 as base
 | 
						|
			RUN echo foo > /foo
 | 
						|
			FROM debian:10.13 as fizz
 | 
						|
			RUN echo fizz >> /fizz
 | 
						|
			COPY --from=base /foo /fizz
 | 
						|
			FROM alpine:3.11 as buzz
 | 
						|
			RUN echo buzz > /buzz
 | 
						|
			FROM alpine:3.11 as final
 | 
						|
			RUN echo bar > /bar
 | 
						|
			`,
 | 
						|
			targets: []string{"final", "buzz", "fizz", ""},
 | 
						|
			expectedSourceCodes: map[string][]string{
 | 
						|
				"final": {"FROM alpine:3.11 as final"},
 | 
						|
				"buzz":  {"FROM alpine:3.11 as buzz"},
 | 
						|
				"fizz":  {"FROM debian:10.13 as base", "FROM debian:10.13 as fizz"},
 | 
						|
				"":      {"FROM alpine:3.11", "FROM debian:10.13 as base", "FROM debian:10.13 as fizz", "FROM alpine:3.11 as buzz", "FROM alpine:3.11 as final"},
 | 
						|
			},
 | 
						|
			expectedTargetIndexBeforeSkip: map[string]int{
 | 
						|
				"final": 4,
 | 
						|
				"buzz":  3,
 | 
						|
				"fizz":  2,
 | 
						|
				"":      4,
 | 
						|
			},
 | 
						|
			expectedTargetIndexAfterSkip: map[string]int{
 | 
						|
				"final": 0,
 | 
						|
				"buzz":  0,
 | 
						|
				"fizz":  1,
 | 
						|
				"":      4,
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, test := range tests {
 | 
						|
		stages, _, err := Parse([]byte(test.dockerfile))
 | 
						|
		testutil.CheckError(t, false, err)
 | 
						|
		actualSourceCodes := make(map[string][]string)
 | 
						|
		for _, target := range test.targets {
 | 
						|
			targetIndex, err := targetStage(stages, target)
 | 
						|
			testutil.CheckError(t, false, err)
 | 
						|
			targetIndexBeforeSkip := targetIndex
 | 
						|
			onlyUsedStages := skipUnusedStages(stages, &targetIndex, target)
 | 
						|
			for _, s := range onlyUsedStages {
 | 
						|
				actualSourceCodes[target] = append(actualSourceCodes[target], s.SourceCode)
 | 
						|
			}
 | 
						|
			t.Run(test.description, func(t *testing.T) {
 | 
						|
				testutil.CheckDeepEqual(t, test.expectedSourceCodes[target], actualSourceCodes[target])
 | 
						|
				testutil.CheckDeepEqual(t, test.expectedTargetIndexBeforeSkip[target], targetIndexBeforeSkip)
 | 
						|
				testutil.CheckDeepEqual(t, test.expectedTargetIndexAfterSkip[target], targetIndex)
 | 
						|
			})
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |