Skip to content

Commit

Permalink
feat: multistages is now built without unusued stages
Browse files Browse the repository at this point in the history
  • Loading branch information
JordanGoasdoue authored and goasdoue committed Apr 16, 2020
1 parent 1534f90 commit 09379ae
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 54 deletions.
115 changes: 61 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

![kaniko logo](logo/Kaniko-Logo.png)

kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster.
kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster.

kaniko doesn't depend on a Docker daemon and executes each command within a Dockerfile completely in userspace.
This enables building container images in environments that can't easily or securely run a Docker daemon, such as a standard Kubernetes cluster.
Expand All @@ -15,7 +15,7 @@ We'd love to hear from you! Join us on [#kaniko Kubernetes Slack](https://kuber

:mega: **Please fill out our [quick 5-question survey](https://forms.gle/HhZGEM33x4FUz9Qa6)** so that we can learn how satisfied you are with Kaniko, and what improvements we should make. Thank you! :dancers:

Kaniko is not an officially supported Google project.
Kaniko is not an officially supported Google project.

_If you are interested in contributing to kaniko, see [DEVELOPMENT.md](DEVELOPMENT.md) and [CONTRIBUTING.md](CONTRIBUTING.md)._

Expand All @@ -24,57 +24,59 @@ _If you are interested in contributing to kaniko, see [DEVELOPMENT.md](DEVELOPME
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*

- [Community](#community)
- [How does kaniko work?](#how-does-kaniko-work)
- [Known Issues](#known-issues)
- [Demo](#demo)
- [Tutorial](#tutorial)
- [Using kaniko](#using-kaniko)
- [kaniko Build Contexts](#kaniko-build-contexts)
- [Using Azure Blob Storage](#using-azure-blob-storage)
- [Using Private Git Repository](#using-private-git-repository)
- [Running kaniko](#running-kaniko)
- [Running kaniko in a Kubernetes cluster](#running-kaniko-in-a-kubernetes-cluster)
- [Kubernetes secret](#kubernetes-secret)
- [Running kaniko in gVisor](#running-kaniko-in-gvisor)
- [Running kaniko in Google Cloud Build](#running-kaniko-in-google-cloud-build)
- [Running kaniko in Docker](#running-kaniko-in-docker)
- [Caching](#caching)
- [Caching Layers](#caching-layers)
- [Caching Base Images](#caching-base-images)
- [Pushing to Different Registries](#pushing-to-different-registries)
- [Pushing to Docker Hub](#pushing-to-docker-hub)
- [Pushing to Amazon ECR](#pushing-to-amazon-ecr)
- [Additional Flags](#additional-flags)
- [--build-arg](#--build-arg)
- [--cache](#--cache)
- [--cache-dir](#--cache-dir)
- [--cache-repo](#--cache-repo)
- [--digest-file](#--digest-file)
- [--oci-layout-path](#--oci-layout-path)
- [--insecure-registry](#--insecure-registry)
- [--skip-tls-verify-registry](#--skip-tls-verify-registry)
- [--cleanup](#--cleanup)
- [--insecure](#--insecure)
- [--insecure-pull](#--insecure-pull)
- [--no-push](#--no-push)
- [--registry-mirror](#--registry-mirror)
- [--reproducible](#--reproducible)
- [--single-snapshot](#--single-snapshot)
- [--skip-tls-verify](#--skip-tls-verify)
- [--skip-tls-verify-pull](#--skip-tls-verify-pull)
- [--snapshotMode](#--snapshotmode)
- [--target](#--target)
- [--tarPath](#--tarpath)
- [--verbosity](#--verbosity)
- [--whitelist-var-run](#--whitelist-var-run)
- [--label](#--label)
- [Debug Image](#debug-image)
- [Security](#security)
- [Comparison with Other Tools](#comparison-with-other-tools)
- [Community](#community-1)
- [Limitations](#limitations)
- [mtime and snapshotting](#mtime-and-snapshotting)
- [Community](#community)
- [How does kaniko work?](#how-does-kaniko-work)
- [Known Issues](#known-issues)
- [Demo](#demo)
- [Tutorial](#tutorial)
- [Using kaniko](#using-kaniko)
- [kaniko Build Contexts](#kaniko-build-contexts)
- [Using Azure Blob Storage](#using-azure-blob-storage)
- [Using Private Git Repository](#using-private-git-repository)
- [Running kaniko](#running-kaniko)
- [Running kaniko in a Kubernetes cluster](#running-kaniko-in-a-kubernetes-cluster)
- [Kubernetes secret](#kubernetes-secret)
- [Running kaniko in gVisor](#running-kaniko-in-gvisor)
- [Running kaniko in Google Cloud Build](#running-kaniko-in-google-cloud-build)
- [Running kaniko in Docker](#running-kaniko-in-docker)
- [Caching](#caching)
- [Caching Layers](#caching-layers)
- [Caching Base Images](#caching-base-images)
- [Pushing to Different Registries](#pushing-to-different-registries)
- [Pushing to Docker Hub](#pushing-to-docker-hub)
- [Pushing to Amazon ECR](#pushing-to-amazon-ecr)
- [Additional Flags](#additional-flags)
- [--build-arg](#build-arg)
- [--cache](#cache)
- [--cache-dir](#cache-dir)
- [--cache-repo](#cache-repo)
- [--context-sub-path](#context-sub-path)
- [--digest-file](#digest-file)
- [--oci-layout-path](#oci-layout-path)
- [--insecure-registry](#insecure-registry)
- [--skip-tls-verify-registry](#skip-tls-verify-registry)
- [--cleanup](#cleanup)
- [--insecure](#insecure)
- [--insecure-pull](#insecure-pull)
- [--no-push](#no-push)
- [--registry-mirror](#registry-mirror)
- [--reproducible](#reproducible)
- [--single-snapshot](#single-snapshot)
- [--skip-tls-verify](#skip-tls-verify)
- [--skip-tls-verify-pull](#skip-tls-verify-pull)
- [--snapshotMode](#snapshotmode)
- [--target](#target)
- [--tarPath](#tarpath)
- [--verbosity](#verbosity)
- [--whitelist-var-run](#whitelist-var-run)
- [--label](#label)
- [--build-used-stages](#build-used-stages)
- [Debug Image](#debug-image)
- [Security](#security)
- [Comparison with Other Tools](#comparison-with-other-tools)
- [Community](#community-1)
- [Limitations](#limitations)
- [mtime and snapshotting](#mtime-and-snapshotting)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -280,7 +282,7 @@ There is also a utility script [`run_in_docker.sh`](./run_in_docker.sh) that can
./run_in_docker.sh <path to Dockerfile> <path to build context> <destination of final image>
```

_NOTE: `run_in_docker.sh` expects a path to a
_NOTE: `run_in_docker.sh` expects a path to a
Dockerfile relative to the absolute path of the build context._

An example run, specifying the Dockerfile in the container directory `/workspace`, the build
Expand Down Expand Up @@ -536,6 +538,11 @@ Ignore /var/run when taking image snapshot. Set it to false to preserve /var/run

Set this flag as `--label key=value` to set some metadata to the final image. This is equivalent as using the `LABEL` within the Dockerfile.

#### --build-used-stages

This flag builds only used stages if defined to `true`.
Otherwise it builds by default all stages, even the unnecessaries ones until it reaches the target stage / end of Dockerfile

### Debug Image

The kaniko executor image is based on scratch and doesn't contain a shell.
Expand Down
1 change: 1 addition & 0 deletions cmd/executor/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func addKanikoOptionsFlags() {
RootCmd.PersistentFlags().StringVarP(&opts.RegistryMirror, "registry-mirror", "", "", "Registry mirror to use has pull-through cache instead of docker.io.")
RootCmd.PersistentFlags().BoolVarP(&opts.WhitelistVarRun, "whitelist-var-run", "", true, "Ignore /var/run directory when taking image snapshot. Set it to false to preserve /var/run/ in destination image. (Default true).")
RootCmd.PersistentFlags().VarP(&opts.Labels, "label", "", "Set metadata for an image. Set it repeatedly for multiple labels.")
RootCmd.PersistentFlags().BoolVarP(&opts.BuildUsedStages, "build-used-stages", "", false, "Build only used stages if defined to true. Otherwise it builds by default all stages, even the unnecessaries ones until it reaches the target stage / end of Dockerfile")
}

// addHiddenFlags marks certain flags as hidden from the executor help text
Expand Down
1 change: 1 addition & 0 deletions pkg/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type KanikoOptions struct {
Cache bool
Cleanup bool
WhitelistVarRun bool
BuildUsedStages bool
}

// WarmerOptions are options that are set by command line arguments to the cache warmer.
Expand Down
51 changes: 51 additions & 0 deletions pkg/dockerfile/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"

v1 "github.com/google/go-containerregistry/pkg/v1"
Expand Down Expand Up @@ -253,6 +254,9 @@ func MakeKanikoStages(opts *config.KanikoOptions, stages []instructions.Stage, m
if err := resolveStagesArgs(stages, args); err != nil {
return nil, errors.Wrap(err, "resolving args")
}
if opts.BuildUsedStages {
stages, targetStage = getUsedStages(stages, targetStage, opts.Target)
}
var kanikoStages []config.KanikoStage
for index, stage := range stages {
if len(stage.Name) > 0 {
Expand Down Expand Up @@ -312,3 +316,50 @@ func unifyArgs(metaArgs []instructions.ArgCommand, buildArgs []string) []string
}
return args
}

// getUsedStages returns the list of used stages calculated from dependencies
func getUsedStages(stages []instructions.Stage, lastStageIndex int, target string) ([]instructions.Stage, int) {
stagesDependencies := make(map[string]bool)
var usedStages []instructions.Stage

lastStageBaseName := stages[lastStageIndex].BaseName

for i := lastStageIndex; i >= 0; i-- {
s := stages[i]
if (s.Name != "" && stagesDependencies[s.Name]) || s.Name == lastStageBaseName || i == lastStageIndex {
for _, c := range s.Commands {
switch cmd := c.(type) {
case *instructions.CopyCommand:
stageName := cmd.From
if copyFromIndex, err := strconv.Atoi(stageName); err == nil {
stageName = stages[copyFromIndex].Name
}
if !stagesDependencies[stageName] {
stagesDependencies[stageName] = true
}
}
}
if i != lastStageIndex {
stagesDependencies[s.BaseName] = true
}
}
}
if target == "" && len(stagesDependencies) == 0 {
return stages, lastStageIndex
}
for i := 0; i < lastStageIndex; i++ {
s := stages[i]
if s.Name == "" {
continue
}
if stagesDependencies[s.Name] || s.Name == lastStageBaseName {
usedStages = append(usedStages, s)
}
}
usedStages = append(usedStages, stages[lastStageIndex])
if lastStageIndex > len(usedStages)-1 {
lastStageIndex = len(usedStages) - 1
}

return usedStages, lastStageIndex
}
132 changes: 132 additions & 0 deletions pkg/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,135 @@ func Test_ResolveStagesArgs(t *testing.T) {
}
}
}

func Test_MutiStageDependencies(t *testing.T) {
tests := []struct {
description string
dockerfile string
targets []string
expectedSourceCodes map[string][]string
}{
{
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"},
},
},
{
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"},
},
},
{
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"},
},
},
{
description: "dockerfile_with_two_copyFrom_and_arg",
dockerfile: `
FROM debian:9.11 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 whitelisted 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:9.11 /etc/os-release /new
`,
targets: []string{},
expectedSourceCodes: map[string][]string{
"": {"FROM debian:9.11 as base", "FROM scratch as second", "FROM base as fourth", "FROM fourth"},
},
},
{
description: "dockerfile_without_final_dependencies",
dockerfile: `
FROM alpine:3.11
FROM debian:9.11 as base
RUN echo foo > /foo
FROM debian:9.11 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:9.11 as base", "FROM debian:9.11 as fizz"},
"": {"FROM alpine:3.11", "FROM debian:9.11 as base", "FROM debian:9.11 as fizz", "FROM alpine:3.11 as buzz", "FROM alpine:3.11 as final"},
},
},
}

for _, test := range tests {
stages, _, err := Parse([]byte(test.dockerfile))
if err != nil {
t.Fatal(err)
}
actualSourceCodes := make(map[string][]string)
for _, target := range test.targets {
targetIndex, _ := targetStage(stages, target)
usedStages, _ := getUsedStages(stages, targetIndex, target)
for _, s := range usedStages {
actualSourceCodes[target] = append(actualSourceCodes[target], s.SourceCode)
}
t.Run(test.description, func(t *testing.T) {
testutil.CheckErrorAndDeepEqual(t, false, err, test.expectedSourceCodes[target], actualSourceCodes[target])
})
}
}
}

0 comments on commit 09379ae

Please sign in to comment.