Skip to content

Commit

Permalink
Merge pull request #320 from priyawadhwa/stages
Browse files Browse the repository at this point in the history
Added a KanikoStage type for each stage of a Dockerfile
  • Loading branch information
priyawadhwa authored Sep 7, 2018
2 parents 637f14e + 0636fe6 commit 4dc3434
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 115 deletions.
4 changes: 2 additions & 2 deletions cmd/executor/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import (
"strings"

"github.com/GoogleContainerTools/kaniko/pkg/buildcontext"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/executor"
"github.com/GoogleContainerTools/kaniko/pkg/options"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/genuinetools/amicontained/container"
"github.com/pkg/errors"
Expand All @@ -33,7 +33,7 @@ import (
)

var (
opts = &options.KanikoOptions{}
opts = &config.KanikoOptions{}
logLevel string
force bool
)
Expand Down
2 changes: 1 addition & 1 deletion pkg/options/args.go → pkg/config/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package options
package config

import (
"strings"
Expand Down
2 changes: 1 addition & 1 deletion pkg/options/options.go → pkg/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package options
package config

// KanikoOptions are options that are set by command line arguments
type KanikoOptions struct {
Expand Down
28 changes: 28 additions & 0 deletions pkg/config/stage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
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 config

import "github.com/moby/buildkit/frontend/dockerfile/instructions"

// KanikoStage wraps a stage of the Dockerfile and provides extra information
type KanikoStage struct {
instructions.Stage
FinalStage bool
BaseImageStoredLocally bool
BaseImageIndex int
SaveStage bool
}
70 changes: 53 additions & 17 deletions pkg/dockerfile/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,61 @@ import (
"strconv"
"strings"

"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/pkg/errors"
)

// Stages reads the Dockerfile, validates it's contents, and returns stages
func Stages(dockerfilePath, target string) ([]instructions.Stage, error) {
d, err := ioutil.ReadFile(dockerfilePath)
// Stages parses a Dockerfile and returns an array of KanikoStage
func Stages(opts *config.KanikoOptions) ([]config.KanikoStage, error) {
d, err := ioutil.ReadFile(opts.DockerfilePath)
if err != nil {
return nil, err
return nil, errors.Wrap(err, fmt.Sprintf("reading dockerfile at path %s", opts.DockerfilePath))
}

stages, err := Parse(d)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing dockerfile")
}
if err := ValidateTarget(stages, target); err != nil {
targetStage, err := targetStage(stages, opts.Target)
if err != nil {
return nil, err
}
ResolveStages(stages)
return stages, nil
resolveStages(stages)
var kanikoStages []config.KanikoStage
for index, stage := range stages {
resolvedBaseName, err := util.ResolveEnvironmentReplacement(stage.BaseName, opts.BuildArgs, false)
if err != nil {
return nil, errors.Wrap(err, "resolving base name")
}
stage.Name = resolvedBaseName
kanikoStages = append(kanikoStages, config.KanikoStage{
Stage: stage,
BaseImageIndex: baseImageIndex(opts, index, stages),
BaseImageStoredLocally: (baseImageIndex(opts, index, stages) != -1),
SaveStage: saveStage(index, stages),
FinalStage: index == targetStage,
})
if index == targetStage {
break
}
}
return kanikoStages, nil
}

// baseImageIndex returns the index of the stage the current stage is built off
// returns -1 if the current stage isn't built off a previous stage
func baseImageIndex(opts *config.KanikoOptions, currentStage int, stages []instructions.Stage) int {
for i, stage := range stages {
if i > currentStage {
break
}
if stage.Name == stages[currentStage].BaseName {
return i
}
}
return -1
}

// Parse parses the contents of a Dockerfile and returns a list of commands
Expand All @@ -58,21 +93,22 @@ func Parse(b []byte) ([]instructions.Stage, error) {
return stages, err
}

func ValidateTarget(stages []instructions.Stage, target string) error {
// targetStage returns the index of the target stage kaniko is trying to build
func targetStage(stages []instructions.Stage, target string) (int, error) {
if target == "" {
return nil
return len(stages) - 1, nil
}
for _, stage := range stages {
for i, stage := range stages {
if stage.Name == target {
return nil
return i, nil
}
}
return fmt.Errorf("%s is not a valid target build stage", target)
return -1, fmt.Errorf("%s is not a valid target build stage", target)
}

// ResolveStages resolves any calls to previous stages with names to indices
// resolveStages resolves any calls to previous stages with names to indices
// Ex. --from=second_stage should be --from=1 for easier processing later on
func ResolveStages(stages []instructions.Stage) {
func resolveStages(stages []instructions.Stage) {
nameToIndex := make(map[string]string)
for i, stage := range stages {
index := strconv.Itoa(i)
Expand Down Expand Up @@ -111,7 +147,7 @@ func ParseCommands(cmdArray []string) ([]instructions.Command, error) {
}

// SaveStage returns true if the current stage will be needed later in the Dockerfile
func SaveStage(index int, stages []instructions.Stage) bool {
func saveStage(index int, stages []instructions.Stage) bool {
for stageIndex, stage := range stages {
if stageIndex <= index {
continue
Expand Down
129 changes: 71 additions & 58 deletions pkg/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,15 @@ limitations under the License.
package dockerfile

import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"testing"

"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/testutil"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
)

func Test_ResolveStages(t *testing.T) {
func Test_resolveStages(t *testing.T) {
dockerfile := `
FROM scratch
RUN echo hi > /hi
Expand All @@ -42,7 +40,7 @@ func Test_ResolveStages(t *testing.T) {
if err != nil {
t.Fatal(err)
}
ResolveStages(stages)
resolveStages(stages)
for index, stage := range stages {
if index == 0 {
continue
Expand All @@ -55,7 +53,7 @@ func Test_ResolveStages(t *testing.T) {
}
}

func Test_ValidateTarget(t *testing.T) {
func Test_targetStage(t *testing.T) {
dockerfile := `
FROM scratch
RUN echo hi > /hi
Expand All @@ -71,70 +69,44 @@ func Test_ValidateTarget(t *testing.T) {
t.Fatal(err)
}
tests := []struct {
name string
target string
shouldErr bool
name string
target string
targetIndex int
shouldErr bool
}{
{
name: "test valid target",
target: "second",
shouldErr: false,
name: "test valid target",
target: "second",
targetIndex: 1,
shouldErr: false,
},
{
name: "test invalid target",
target: "invalid",
shouldErr: true,
name: "test no target",
target: "",
targetIndex: 2,
shouldErr: false,
},
{
name: "test invalid target",
target: "invalid",
targetIndex: -1,
shouldErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualErr := ValidateTarget(stages, test.target)
testutil.CheckError(t, test.shouldErr, actualErr)
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) {
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("couldn't create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
files := map[string]string{
"Dockerfile": `
FROM scratch
RUN echo hi > /hi
FROM scratch AS second
COPY --from=0 /hi /hi2
FROM second
RUN xxx
FROM scratch
COPY --from=second /hi2 /hi3
FROM ubuntu:16.04 AS base
ENV DEBIAN_FRONTEND noninteractive
ENV LC_ALL C.UTF-8
FROM base AS development
ENV PS1 " 🐳 \[\033[1;36m\]\W\[\033[0;35m\] # \[\033[0m\]"
FROM development AS test
ENV ORG_ENV UnitTest
FROM base AS production
COPY . /code
`,
}
if err := testutil.SetupFiles(tempDir, files); err != nil {
t.Fatalf("couldn't create dockerfile: %v", err)
}
stages, err := Stages(filepath.Join(tempDir, "Dockerfile"), "")
if err != nil {
t.Fatalf("couldn't retrieve stages from Dockerfile: %v", err)
}
tests := []struct {
name string
index int
Expand Down Expand Up @@ -171,10 +143,51 @@ func Test_SaveStage(t *testing.T) {
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)
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(&config.KanikoOptions{}, test.currentStage, stages)
if actual != test.expected {
t.Fatalf("unexpected result, expected %d got %d", test.expected, actual)
}
})
}
}
Loading

0 comments on commit 4dc3434

Please sign in to comment.