Updating the Fork

This commit is contained in:
Kartik Verma
2018-10-02 16:21:44 +05:30
committed by GitHub
89 changed files with 4515 additions and 663 deletions
+3 -2
View File
@@ -17,14 +17,15 @@ limitations under the License.
package buildcontext
import (
"os"
"path/filepath"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"os"
"path/filepath"
)
// S3 unifies calls to download and unpack the build context.
+70
View File
@@ -0,0 +1,70 @@
/*
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 cache
import (
"fmt"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/k8schain"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// RetrieveLayer checks the specified cache for a layer with the tag :cacheKey
func RetrieveLayer(opts *config.KanikoOptions, cacheKey string) (v1.Image, error) {
cache, err := Destination(opts, cacheKey)
if err != nil {
return nil, errors.Wrap(err, "getting cache destination")
}
logrus.Infof("Checking for cached layer %s...", cache)
cacheRef, err := name.NewTag(cache, name.WeakValidation)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("getting reference for %s", cache))
}
k8sc, err := k8schain.NewNoClient()
if err != nil {
return nil, err
}
kc := authn.NewMultiKeychain(authn.DefaultKeychain, k8sc)
img, err := remote.Image(cacheRef, remote.WithAuthFromKeychain(kc))
if err != nil {
return nil, err
}
_, err = img.Layers()
return img, err
}
// Destination returns the repo where the layer should be stored
// If no cache is specified, one is inferred from the destination provided
func Destination(opts *config.KanikoOptions, cacheKey string) (string, error) {
cache := opts.CacheRepo
if cache == "" {
destination := opts.Destinations[0]
destRef, err := name.NewTag(destination, name.WeakValidation)
if err != nil {
return "", errors.Wrap(err, "getting tag for destination")
}
return fmt.Sprintf("%s/cache:%s", destRef.Context(), cacheKey), nil
}
return fmt.Sprintf("%s:%s", cache, cacheKey), nil
}
+10 -12
View File
@@ -18,7 +18,6 @@ package commands
import (
"path/filepath"
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
@@ -44,21 +43,15 @@ type AddCommand struct {
// 2. If <src> is a local tar archive:
// -If <src> is a local tar archive, it is unpacked at the dest, as 'tar -x' would
func (a *AddCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
srcs := a.cmd.SourcesAndDest[:len(a.cmd.SourcesAndDest)-1]
dest := a.cmd.SourcesAndDest[len(a.cmd.SourcesAndDest)-1]
logrus.Infof("cmd: Add %s", srcs)
logrus.Infof("dest: %s", dest)
// First, resolve any environment replacement
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
resolvedEnvs, err := util.ResolveEnvironmentReplacementList(a.cmd.SourcesAndDest, replacementEnvs, true)
if err != nil {
return err
}
dest = resolvedEnvs[len(resolvedEnvs)-1]
dest := resolvedEnvs[len(resolvedEnvs)-1]
// Resolve wildcards and get a list of resolved sources
srcs, err = util.ResolveSources(resolvedEnvs, a.buildcontext)
srcs, err := util.ResolveSources(resolvedEnvs, a.buildcontext)
if err != nil {
return err
}
@@ -112,7 +105,12 @@ func (a *AddCommand) FilesToSnapshot() []string {
return a.snapshotFiles
}
// CreatedBy returns some information about the command for the image config
func (a *AddCommand) CreatedBy() string {
return strings.Join(a.cmd.SourcesAndDest, " ")
// String returns some information about the command for the image config
func (a *AddCommand) String() string {
return a.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (a *AddCommand) CacheCommand() bool {
return false
}
+8 -7
View File
@@ -17,13 +17,10 @@ limitations under the License.
package commands
import (
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/sirupsen/logrus"
)
type ArgCommand struct {
@@ -32,7 +29,6 @@ type ArgCommand struct {
// ExecuteCommand only needs to add this ARG key/value as seen
func (r *ArgCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
logrus.Info("ARG")
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
resolvedKey, err := util.ResolveEnvironmentReplacement(r.cmd.Key, replacementEnvs, false)
if err != nil {
@@ -55,7 +51,12 @@ func (r *ArgCommand) FilesToSnapshot() []string {
return []string{}
}
// CreatedBy returns some information about the command for the image config history
func (r *ArgCommand) CreatedBy() string {
return strings.Join([]string{r.cmd.Name(), r.cmd.Key}, " ")
// String returns some information about the command for the image config history
func (r *ArgCommand) String() string {
return r.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (r *ArgCommand) CacheCommand() bool {
return false
}
+8 -14
View File
@@ -23,7 +23,6 @@ import (
"github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/sirupsen/logrus"
)
type CmdCommand struct {
@@ -33,7 +32,6 @@ type CmdCommand struct {
// ExecuteCommand executes the CMD command
// Argument handling is the same as RUN.
func (c *CmdCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
logrus.Info("cmd: CMD")
var newCommand []string
if c.cmd.PrependShell {
// This is the default shell on Linux
@@ -49,7 +47,6 @@ func (c *CmdCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
newCommand = c.cmd.CmdLine
}
logrus.Infof("Replacing CMD in config with %v", newCommand)
config.Cmd = newCommand
config.ArgsEscaped = true
return nil
@@ -60,15 +57,12 @@ func (c *CmdCommand) FilesToSnapshot() []string {
return []string{}
}
// CreatedBy returns some information about the command for the image config history
func (c *CmdCommand) CreatedBy() string {
cmd := []string{"CMD"}
cmdLine := strings.Join(c.cmd.CmdLine, " ")
if c.cmd.PrependShell {
// TODO: Support shell command here
shell := []string{"/bin/sh", "-c"}
appendedShell := append(cmd, shell...)
return strings.Join(append(appendedShell, cmdLine), " ")
}
return strings.Join(append(cmd, cmdLine), " ")
// String returns some information about the command for the image config history
func (c *CmdCommand) String() string {
return c.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (c *CmdCommand) CacheCommand() bool {
return false
}
+5 -2
View File
@@ -30,10 +30,13 @@ type DockerCommand interface {
// 2. Updating metadata fields in the config
// It should not change the config history.
ExecuteCommand(*v1.Config, *dockerfile.BuildArgs) error
// The config history has a "created by" field, should return information about the command
CreatedBy() string
// Returns a string representation of the command
String() string
// A list of files to snapshot, empty for metadata commands or nil if we don't know
FilesToSnapshot() []string
// Return true if this command should be true
// Currently only true for RUN
CacheCommand() bool
}
func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) {
+10 -13
View File
@@ -19,7 +19,6 @@ package commands
import (
"os"
"path/filepath"
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
@@ -27,7 +26,6 @@ import (
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/sirupsen/logrus"
)
type CopyCommand struct {
@@ -37,12 +35,6 @@ type CopyCommand struct {
}
func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
srcs := c.cmd.SourcesAndDest[:len(c.cmd.SourcesAndDest)-1]
dest := c.cmd.SourcesAndDest[len(c.cmd.SourcesAndDest)-1]
logrus.Infof("cmd: copy %s", srcs)
logrus.Infof("dest: %s", dest)
// Resolve from
if c.cmd.From != "" {
c.buildcontext = filepath.Join(constants.KanikoDir, c.cmd.From)
@@ -53,9 +45,9 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
if err != nil {
return err
}
dest = resolvedEnvs[len(resolvedEnvs)-1]
dest := resolvedEnvs[len(resolvedEnvs)-1]
// Resolve wildcards and get a list of resolved sources
srcs, err = util.ResolveSources(resolvedEnvs, c.buildcontext)
srcs, err := util.ResolveSources(resolvedEnvs, c.buildcontext)
if err != nil {
return err
}
@@ -106,7 +98,12 @@ func (c *CopyCommand) FilesToSnapshot() []string {
return c.snapshotFiles
}
// CreatedBy returns some information about the command for the image config
func (c *CopyCommand) CreatedBy() string {
return strings.Join(c.cmd.SourcesAndDest, " ")
// String returns some information about the command for the image config
func (c *CopyCommand) String() string {
return c.cmd.String()
}
// CacheCommand returns true since this command should be cached
func (c *CopyCommand) CacheCommand() bool {
return false
}
+8 -14
View File
@@ -23,7 +23,6 @@ import (
"github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/sirupsen/logrus"
)
type EntrypointCommand struct {
@@ -32,7 +31,6 @@ type EntrypointCommand struct {
// ExecuteCommand handles command processing similar to CMD and RUN,
func (e *EntrypointCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
logrus.Info("cmd: ENTRYPOINT")
var newCommand []string
if e.cmd.PrependShell {
// This is the default shell on Linux
@@ -48,7 +46,6 @@ func (e *EntrypointCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerf
newCommand = e.cmd.CmdLine
}
logrus.Infof("Replacing Entrypoint in config with %v", newCommand)
config.Entrypoint = newCommand
return nil
}
@@ -58,15 +55,12 @@ func (e *EntrypointCommand) FilesToSnapshot() []string {
return []string{}
}
// CreatedBy returns some information about the command for the image config history
func (e *EntrypointCommand) CreatedBy() string {
entrypoint := []string{"ENTRYPOINT"}
cmdLine := strings.Join(e.cmd.CmdLine, " ")
if e.cmd.PrependShell {
// TODO: Support shell command here
shell := []string{"/bin/sh", "-c"}
appendedShell := append(entrypoint, shell...)
return strings.Join(append(appendedShell, cmdLine), " ")
}
return strings.Join(append(entrypoint, cmdLine), " ")
// String returns some information about the command for the image config history
func (e *EntrypointCommand) String() string {
return e.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (e *EntrypointCommand) CacheCommand() bool {
return false
}
+8 -11
View File
@@ -17,14 +17,11 @@ limitations under the License.
package commands
import (
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/sirupsen/logrus"
)
type EnvCommand struct {
@@ -32,7 +29,6 @@ type EnvCommand struct {
}
func (e *EnvCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
logrus.Info("cmd: ENV")
newEnvs := e.cmd.Env
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
return util.UpdateConfigEnv(newEnvs, config, replacementEnvs)
@@ -43,11 +39,12 @@ func (e *EnvCommand) FilesToSnapshot() []string {
return []string{}
}
// CreatedBy returns some information about the command for the image config history
func (e *EnvCommand) CreatedBy() string {
envArray := []string{e.cmd.Name()}
for _, pair := range e.cmd.Env {
envArray = append(envArray, pair.Key+"="+pair.Value)
}
return strings.Join(envArray, " ")
// String returns some information about the command for the image config history
func (e *EnvCommand) String() string {
return e.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (e *EnvCommand) CacheCommand() bool {
return false
}
+7 -3
View File
@@ -76,7 +76,11 @@ func (r *ExposeCommand) FilesToSnapshot() []string {
return []string{}
}
func (r *ExposeCommand) CreatedBy() string {
s := []string{r.cmd.Name()}
return strings.Join(append(s, r.cmd.Ports...), " ")
func (r *ExposeCommand) String() string {
return r.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (r *ExposeCommand) CacheCommand() bool {
return false
}
+7 -9
View File
@@ -17,12 +17,9 @@ limitations under the License.
package commands
import (
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/sirupsen/logrus"
)
type HealthCheckCommand struct {
@@ -31,8 +28,6 @@ type HealthCheckCommand struct {
// ExecuteCommand handles command processing similar to CMD and RUN,
func (h *HealthCheckCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
logrus.Info("cmd: HEALTHCHECK")
check := v1.HealthConfig(*h.cmd.Health)
config.Healthcheck = &check
@@ -44,9 +39,12 @@ func (h *HealthCheckCommand) FilesToSnapshot() []string {
return []string{}
}
// CreatedBy returns some information about the command for the image config history
func (h *HealthCheckCommand) CreatedBy() string {
entrypoint := []string{"HEALTHCHECK"}
// String returns some information about the command for the image config history
func (h *HealthCheckCommand) String() string {
return h.cmd.String()
}
return strings.Join(append(entrypoint, strings.Join(h.cmd.Health.Test, " ")), " ")
// CacheCommand returns false since this command shouldn't be cached
func (h *HealthCheckCommand) CacheCommand() bool {
return false
}
+8 -10
View File
@@ -17,8 +17,6 @@ limitations under the License.
package commands
import (
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/pkg/util"
@@ -32,7 +30,6 @@ type LabelCommand struct {
}
func (r *LabelCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
logrus.Info("cmd: LABEL")
return updateLabels(r.cmd.Labels, config, buildArgs)
}
@@ -72,11 +69,12 @@ func (r *LabelCommand) FilesToSnapshot() []string {
return []string{}
}
// CreatedBy returns some information about the command for the image config history
func (r *LabelCommand) CreatedBy() string {
l := []string{r.cmd.Name()}
for _, kvp := range r.cmd.Labels {
l = append(l, kvp.String())
}
return strings.Join(l, " ")
// String returns some information about the command for the image config history
func (r *LabelCommand) String() string {
return r.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (r *LabelCommand) CacheCommand() bool {
return false
}
+8 -3
View File
@@ -44,7 +44,12 @@ func (o *OnBuildCommand) FilesToSnapshot() []string {
return []string{}
}
// CreatedBy returns some information about the command for the image config history
func (o *OnBuildCommand) CreatedBy() string {
return o.cmd.Expression
// String returns some information about the command for the image config history
func (o *OnBuildCommand) String() string {
return o.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (o *OnBuildCommand) CacheCommand() bool {
return false
}
+23 -13
View File
@@ -20,6 +20,7 @@ import (
"fmt"
"os"
"os/exec"
"os/user"
"strconv"
"strings"
"syscall"
@@ -116,19 +117,29 @@ func (r *RunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
}
// addDefaultHOME adds the default value for HOME if it isn't already set
func addDefaultHOME(user string, envs []string) []string {
func addDefaultHOME(u string, envs []string) []string {
for _, env := range envs {
split := strings.SplitN(env, "=", 2)
if split[0] == constants.HOME {
return envs
}
}
// If user isn't set, set default value of HOME
if user == "" {
if u == "" {
return append(envs, fmt.Sprintf("%s=%s", constants.HOME, constants.DefaultHOMEValue))
}
// If user is set, set value of HOME to /home/${user}
return append(envs, fmt.Sprintf("%s=/home/%s", constants.HOME, user))
// If user is set to username, set value of HOME to /home/${user}
// Otherwise the user is set to uid and HOME is /
home := fmt.Sprintf("%s=/", constants.HOME)
userObj, err := user.Lookup(u)
if err == nil {
u = userObj.Username
home = fmt.Sprintf("%s=/home/%s", constants.HOME, u)
}
return append(envs, home)
}
// FilesToSnapshot returns nil for this command because we don't know which files
@@ -137,13 +148,12 @@ func (r *RunCommand) FilesToSnapshot() []string {
return nil
}
// CreatedBy returns some information about the command for the image config
func (r *RunCommand) CreatedBy() string {
cmdLine := strings.Join(r.cmd.CmdLine, " ")
if r.cmd.PrependShell {
// TODO: Support shell command here
shell := []string{"/bin/sh", "-c"}
return strings.Join(append(shell, cmdLine), " ")
}
return cmdLine
// String returns some information about the command for the image config
func (r *RunCommand) String() string {
return r.cmd.String()
}
// CacheCommand returns true since this command should be cached
func (r *RunCommand) CacheCommand() bool {
return true
}
+1 -1
View File
@@ -59,7 +59,7 @@ func Test_addDefaultHOME(t *testing.T) {
},
expected: []string{
"PATH=/something/else",
"HOME=/home/newuser",
"HOME=/",
},
},
}
+8 -15
View File
@@ -17,12 +17,9 @@ limitations under the License.
package commands
import (
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/sirupsen/logrus"
)
type ShellCommand struct {
@@ -31,13 +28,7 @@ type ShellCommand struct {
// ExecuteCommand handles command processing similar to CMD and RUN,
func (s *ShellCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
logrus.Info("cmd: SHELL")
var newShell []string
newShell = s.cmd.Shell
logrus.Infof("Replacing Shell in config with %v", newShell)
config.Shell = newShell
config.Shell = s.cmd.Shell
return nil
}
@@ -46,10 +37,12 @@ func (s *ShellCommand) FilesToSnapshot() []string {
return []string{}
}
// CreatedBy returns some information about the command for the image config history
func (s *ShellCommand) CreatedBy() string {
entrypoint := []string{"SHELL"}
cmdLine := strings.Join(s.cmd.Shell, " ")
// String returns some information about the command for the image config history
func (s *ShellCommand) String() string {
return s.cmd.String()
}
return strings.Join(append(entrypoint, cmdLine), " ")
// CacheCommand returns false since this command shouldn't be cached
func (s *ShellCommand) CacheCommand() bool {
return false
}
+7 -6
View File
@@ -17,8 +17,6 @@ limitations under the License.
package commands
import (
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/docker/docker/pkg/signal"
@@ -59,9 +57,12 @@ func (s *StopSignalCommand) FilesToSnapshot() []string {
return []string{}
}
// CreatedBy returns some information about the command for the image config history
func (s *StopSignalCommand) CreatedBy() string {
entrypoint := []string{"STOPSIGNAL"}
// String returns some information about the command for the image config history
func (s *StopSignalCommand) String() string {
return s.cmd.String()
}
return strings.Join(append(entrypoint, s.cmd.Signal), " ")
// CacheCommand returns false since this command shouldn't be cached
func (s *StopSignalCommand) CacheCommand() bool {
return false
}
+7 -3
View File
@@ -63,7 +63,11 @@ func (r *UserCommand) FilesToSnapshot() []string {
return []string{}
}
func (r *UserCommand) CreatedBy() string {
s := []string{r.cmd.Name(), r.cmd.User}
return strings.Join(s, " ")
func (r *UserCommand) String() string {
return r.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (r *UserCommand) CacheCommand() bool {
return false
}
+12 -12
View File
@@ -27,57 +27,57 @@ import (
var userTests = []struct {
user string
expectedUid string
expectedUID string
shouldError bool
}{
{
user: "root",
expectedUid: "root",
expectedUID: "root",
shouldError: false,
},
{
user: "0",
expectedUid: "0",
expectedUID: "0",
shouldError: false,
},
{
user: "fakeUser",
expectedUid: "",
expectedUID: "",
shouldError: true,
},
{
user: "root:root",
expectedUid: "root:root",
expectedUID: "root:root",
shouldError: false,
},
{
user: "0:root",
expectedUid: "0:root",
expectedUID: "0:root",
shouldError: false,
},
{
user: "root:0",
expectedUid: "root:0",
expectedUID: "root:0",
shouldError: false,
},
{
user: "0:0",
expectedUid: "0:0",
expectedUID: "0:0",
shouldError: false,
},
{
user: "root:fakeGroup",
expectedUid: "",
expectedUID: "",
shouldError: true,
},
{
user: "$envuser",
expectedUid: "root",
expectedUID: "root",
shouldError: false,
},
{
user: "root:$envgroup",
expectedUid: "root:root",
expectedUID: "root:root",
shouldError: false,
},
}
@@ -97,6 +97,6 @@ func TestUpdateUser(t *testing.T) {
}
buildArgs := dockerfile.NewBuildArgs([]string{})
err := cmd.ExecuteCommand(cfg, buildArgs)
testutil.CheckErrorAndDeepEqual(t, test.shouldError, err, test.expectedUid, cfg.User)
testutil.CheckErrorAndDeepEqual(t, test.shouldError, err, test.expectedUID, cfg.User)
}
}
+9 -5
View File
@@ -19,7 +19,6 @@ package commands
import (
"fmt"
"os"
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
@@ -49,7 +48,7 @@ func (v *VolumeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.
for _, volume := range resolvedVolumes {
var x struct{}
existingVolumes[volume] = x
err := util.AddPathToVolumeWhitelist(volume)
err := util.AddVolumePathToWhitelist(volume)
if err != nil {
return err
}
@@ -57,7 +56,7 @@ func (v *VolumeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.
// Only create and snapshot the dir if it didn't exist already
if _, err := os.Stat(volume); os.IsNotExist(err) {
logrus.Infof("Creating directory %s", volume)
v.snapshotFiles = []string{volume}
v.snapshotFiles = append(v.snapshotFiles, volume)
if err := os.MkdirAll(volume, 0755); err != nil {
return fmt.Errorf("Could not create directory for volume %s: %s", volume, err)
}
@@ -72,6 +71,11 @@ func (v *VolumeCommand) FilesToSnapshot() []string {
return v.snapshotFiles
}
func (v *VolumeCommand) CreatedBy() string {
return strings.Join(append([]string{v.cmd.Name()}, v.cmd.Volumes...), " ")
func (v *VolumeCommand) String() string {
return v.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (v *VolumeCommand) CacheCommand() bool {
return false
}
+8 -3
View File
@@ -62,7 +62,12 @@ func (w *WorkdirCommand) FilesToSnapshot() []string {
return w.snapshotFiles
}
// CreatedBy returns some information about the command for the image config history
func (w *WorkdirCommand) CreatedBy() string {
return w.cmd.Name() + " " + w.cmd.Path
// String returns some information about the command for the image config history
func (w *WorkdirCommand) String() string {
return w.cmd.String()
}
// CacheCommand returns false since this command shouldn't be cached
func (w *WorkdirCommand) CacheCommand() bool {
return false
}
+2 -2
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package options
package config
import (
"strings"
@@ -34,7 +34,7 @@ func (b *multiArg) String() string {
// The second method is Set(value string) error
func (b *multiArg) Set(value string) error {
logrus.Infof("appending to multi args %s", value)
logrus.Debugf("appending to multi args %s", value)
*b = append(*b, value)
return nil
}
@@ -14,21 +14,24 @@ 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 {
DockerfilePath string
Destinations multiArg
SrcContext string
SnapshotMode string
Bucket string
InsecurePush bool
SkipTlsVerify bool
BuildArgs multiArg
TarPath string
Target string
CacheRepo string
Destinations multiArg
BuildArgs multiArg
InsecurePush bool
SkipTLSVerify bool
SingleSnapshot bool
Reproducible bool
Target string
NoPush bool
Cache bool
Cleanup bool
}
+28
View File
@@ -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
BaseImageIndex int
Final bool
BaseImageStoredLocally bool
SaveStage bool
}
+8 -1
View File
@@ -56,9 +56,16 @@ const (
LocalDirBuildContextPrefix = "dir://"
GitBuildContextPrefix = "git://"
HOME = "HOME"
// DefaultHOMEValue is the default value Docker sets for $HOME
HOME = "HOME"
DefaultHOMEValue = "/root"
// Docker command names
Cmd = "cmd"
Entrypoint = "entrypoint"
// VolumeCmdName is the name of the volume command
VolumeCmdName = "volume"
)
// KanikoBuildFiles is the list of files required to build kaniko
+4 -3
View File
@@ -17,8 +17,9 @@ limitations under the License.
package dockerfile
import (
d "github.com/docker/docker/builder/dockerfile"
"strings"
d "github.com/docker/docker/builder/dockerfile"
)
type BuildArgs struct {
@@ -36,14 +37,14 @@ func NewBuildArgs(args []string) *BuildArgs {
}
}
return &BuildArgs{
*d.NewBuildArgs(argsFromOptions),
BuildArgs: *d.NewBuildArgs(argsFromOptions),
}
}
func (b *BuildArgs) Clone() *BuildArgs {
clone := b.BuildArgs.Clone()
return &BuildArgs{
*clone,
BuildArgs: *clone,
}
}
+53 -17
View File
@@ -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, errors.Wrap(err, "parsing dockerfile")
}
targetStage, err := targetStage(stages, opts.Target)
if err != nil {
return nil, err
}
if err := ValidateTarget(stages, target); err != nil {
return nil, err
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(index, stages),
BaseImageStoredLocally: (baseImageIndex(index, stages) != -1),
SaveStage: saveStage(index, stages),
Final: index == targetStage,
})
if index == targetStage {
break
}
}
ResolveStages(stages)
return stages, nil
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(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
@@ -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)
@@ -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
+70 -58
View File
@@ -17,9 +17,6 @@ limitations under the License.
package dockerfile
import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"testing"
@@ -27,7 +24,7 @@ import (
"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
@@ -42,7 +39,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
@@ -55,7 +52,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
@@ -71,70 +68,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
@@ -171,10 +142,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(test.currentStage, stages)
if actual != test.expected {
t.Fatalf("unexpected result, expected %d got %d", test.expected, actual)
}
})
}
}
+226 -98
View File
@@ -18,6 +18,7 @@ package executor
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@@ -30,127 +31,238 @@ import (
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/GoogleContainerTools/kaniko/pkg/cache"
"github.com/GoogleContainerTools/kaniko/pkg/commands"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/pkg/options"
"github.com/GoogleContainerTools/kaniko/pkg/snapshot"
"github.com/GoogleContainerTools/kaniko/pkg/util"
)
func DoBuild(opts *options.KanikoOptions) (v1.Image, error) {
// Parse dockerfile and unpack base image to root
stages, err := dockerfile.Stages(opts.DockerfilePath, opts.Target)
// stageBuilder contains all fields necessary to build one stage of a Dockerfile
type stageBuilder struct {
stage config.KanikoStage
image v1.Image
cf *v1.ConfigFile
snapshotter *snapshot.Snapshotter
baseImageDigest string
}
// newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage
func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*stageBuilder, error) {
sourceImage, err := util.RetrieveSourceImage(stage, opts.BuildArgs)
if err != nil {
return nil, err
}
imageConfig, err := util.RetrieveConfigFile(sourceImage)
if err != nil {
return nil, err
}
if err := resolveOnBuild(&stage, &imageConfig.Config); err != nil {
return nil, err
}
hasher, err := getHasher(opts.SnapshotMode)
if err != nil {
return nil, err
}
for index, stage := range stages {
finalStage := finalStage(index, opts.Target, stages)
// Unpack file system to root
sourceImage, err := util.RetrieveSourceImage(index, opts.BuildArgs, stages)
l := snapshot.NewLayeredMap(hasher, util.CacheHasher())
snapshotter := snapshot.NewSnapshotter(l, constants.RootDir)
digest, err := sourceImage.Digest()
if err != nil {
return nil, err
}
return &stageBuilder{
stage: stage,
image: sourceImage,
cf: imageConfig,
snapshotter: snapshotter,
baseImageDigest: digest.String(),
}, nil
}
// key will return a string representation of the build at the cmd
func (s *stageBuilder) key(cmd string) (string, error) {
fsKey, err := s.snapshotter.Key()
if err != nil {
return "", err
}
c := bytes.NewBuffer([]byte{})
enc := json.NewEncoder(c)
enc.Encode(s.cf)
cf, err := util.SHA256(c)
if err != nil {
return "", err
}
logrus.Debugf("%s\n%s\n%s\n%s\n", s.baseImageDigest, fsKey, cf, cmd)
return util.SHA256(bytes.NewReader([]byte(s.baseImageDigest + fsKey + cf + cmd)))
}
// extractCachedLayer will extract the cached layer and append it to the config file
func (s *stageBuilder) extractCachedLayer(layer v1.Image, createdBy string) error {
logrus.Infof("Found cached layer, extracting to filesystem")
extractedFiles, err := util.GetFSFromImage(constants.RootDir, layer)
if err != nil {
return errors.Wrap(err, "extracting fs from image")
}
if _, err := s.snapshotter.TakeSnapshot(extractedFiles); err != nil {
return err
}
logrus.Infof("Appending cached layer to base image")
l, err := layer.Layers()
if err != nil {
return errors.Wrap(err, "getting cached layer from image")
}
s.image, err = mutate.Append(s.image,
mutate.Addendum{
Layer: l[0],
History: v1.History{
Author: constants.Author,
CreatedBy: createdBy,
},
},
)
return err
}
func (s *stageBuilder) build(opts *config.KanikoOptions) error {
// Unpack file system to root
if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil {
return err
}
// Take initial snapshot
if err := s.snapshotter.Init(); err != nil {
return err
}
var volumes []string
args := dockerfile.NewBuildArgs(opts.BuildArgs)
for index, cmd := range s.stage.Commands {
finalCmd := index == len(s.stage.Commands)-1
command, err := commands.GetCommand(cmd, opts.SrcContext)
if err != nil {
return nil, err
return err
}
if err := util.GetFSFromImage(constants.RootDir, sourceImage); err != nil {
return nil, err
if command == nil {
continue
}
l := snapshot.NewLayeredMap(hasher)
snapshotter := snapshot.NewSnapshotter(l, constants.RootDir)
// Take initial snapshot
if err := snapshotter.Init(); err != nil {
return nil, err
}
imageConfig, err := util.RetrieveConfigFile(sourceImage)
logrus.Info(command.String())
cacheKey, err := s.key(command.String())
if err != nil {
return nil, err
return errors.Wrap(err, "getting key")
}
if err := resolveOnBuild(&stage, &imageConfig.Config); err != nil {
return nil, err
}
buildArgs := dockerfile.NewBuildArgs(opts.BuildArgs)
for index, cmd := range stage.Commands {
finalCmd := index == len(stage.Commands)-1
dockerCommand, err := commands.GetCommand(cmd, opts.SrcContext)
if err != nil {
return nil, err
}
if dockerCommand == nil {
if command.CacheCommand() && opts.Cache {
image, err := cache.RetrieveLayer(opts, cacheKey)
if err == nil {
if err := s.extractCachedLayer(image, command.String()); err != nil {
return errors.Wrap(err, "extracting cached layer")
}
continue
}
if err := dockerCommand.ExecuteCommand(&imageConfig.Config, buildArgs); err != nil {
return nil, err
}
snapshotFiles := dockerCommand.FilesToSnapshot()
var contents []byte
logrus.Info("No cached layer found, executing command...")
}
if err := command.ExecuteCommand(&s.cf.Config, args); err != nil {
return err
}
files := command.FilesToSnapshot()
if cmd.Name() == constants.VolumeCmdName {
volumes = append(volumes, files...)
continue
}
var contents []byte
// If this is an intermediate stage, we only snapshot for the last command and we
// want to snapshot the entire filesystem since we aren't tracking what was changed
// by previous commands.
if !finalStage {
// If this is an intermediate stage, we only snapshot for the last command and we
// want to snapshot the entire filesystem since we aren't tracking what was changed
// by previous commands.
if !s.stage.Final {
if finalCmd {
contents, err = s.snapshotter.TakeSnapshotFS()
}
} else {
// If we are in single snapshot mode, we only take a snapshot once, after all
// commands have completed.
if opts.SingleSnapshot {
if finalCmd {
contents, err = snapshotter.TakeSnapshotFS()
contents, err = s.snapshotter.TakeSnapshotFS()
}
} else {
// If we are in single snapshot mode, we only take a snapshot once, after all
// commands have completed.
if opts.SingleSnapshot {
if finalCmd {
contents, err = snapshotter.TakeSnapshotFS()
// Otherwise, in the final stage we take a snapshot at each command. If we know
// the files that were changed, we'll snapshot those explicitly, otherwise we'll
// check if anything in the filesystem changed.
if files != nil {
if len(files) > 0 {
files = append(files, volumes...)
volumes = []string{}
}
contents, err = s.snapshotter.TakeSnapshot(files)
} else {
// Otherwise, in the final stage we take a snapshot at each command. If we know
// the files that were changed, we'll snapshot those explicitly, otherwise we'll
// check if anything in the filesystem changed.
if snapshotFiles != nil {
contents, err = snapshotter.TakeSnapshot(snapshotFiles)
} else {
contents, err = snapshotter.TakeSnapshotFS()
}
contents, err = s.snapshotter.TakeSnapshotFS()
volumes = []string{}
}
}
if err != nil {
return nil, fmt.Errorf("Error taking snapshot of files for command %s: %s", dockerCommand, err)
}
}
if err != nil {
return fmt.Errorf("Error taking snapshot of files for command %s: %s", command, err)
}
util.MoveVolumeWhitelistToWhitelist()
if contents == nil {
logrus.Info("No files were changed, appending empty layer to config. No layer added to image.")
continue
}
// Append the layer to the image
opener := func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(contents)), nil
}
layer, err := tarball.LayerFromOpener(opener)
if err != nil {
return nil, err
}
sourceImage, err = mutate.Append(sourceImage,
mutate.Addendum{
Layer: layer,
History: v1.History{
Author: constants.Author,
CreatedBy: dockerCommand.CreatedBy(),
},
},
)
if err != nil {
return nil, err
if contents == nil {
logrus.Info("No files were changed, appending empty layer to config. No layer added to image.")
continue
}
// Append the layer to the image
opener := func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(contents)), nil
}
layer, err := tarball.LayerFromOpener(opener)
if err != nil {
return err
}
// Push layer to cache now along with new config file
if command.CacheCommand() && opts.Cache {
if err := pushLayerToCache(opts, cacheKey, layer, command.String()); err != nil {
return err
}
}
sourceImage, err = mutate.Config(sourceImage, imageConfig.Config)
s.image, err = mutate.Append(s.image,
mutate.Addendum{
Layer: layer,
History: v1.History{
Author: constants.Author,
CreatedBy: command.String(),
},
},
)
if err != nil {
return err
}
}
return nil
}
// DoBuild executes building the Dockerfile
func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
// Parse dockerfile and unpack base image to root
stages, err := dockerfile.Stages(opts)
if err != nil {
return nil, err
}
for index, stage := range stages {
sb, err := newStageBuilder(opts, stage)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("getting stage builder for stage %d", index))
}
if err := sb.build(opts); err != nil {
return nil, errors.Wrap(err, "error building stage")
}
reviewConfig(stage, &sb.cf.Config)
sourceImage, err := mutate.Config(sb.image, sb.cf.Config)
if err != nil {
return nil, err
}
if finalStage {
if stage.Final {
sourceImage, err = mutate.CreatedAt(sourceImage, v1.Time{Time: time.Now()})
if err != nil {
return nil, err
@@ -161,9 +273,14 @@ func DoBuild(opts *options.KanikoOptions) (v1.Image, error) {
return nil, err
}
}
if opts.Cleanup {
if err = util.DeleteFilesystem(); err != nil {
return nil, err
}
}
return sourceImage, nil
}
if dockerfile.SaveStage(index, stages) {
if stage.SaveStage {
if err := saveStageAsTarball(index, sourceImage); err != nil {
return nil, err
}
@@ -179,23 +296,14 @@ func DoBuild(opts *options.KanikoOptions) (v1.Image, error) {
return nil, err
}
func finalStage(index int, target string, stages []instructions.Stage) bool {
if index == len(stages)-1 {
return true
}
if target == "" {
return false
}
return target == stages[index].Name
}
func extractImageToDependecyDir(index int, image v1.Image) error {
dependencyDir := filepath.Join(constants.KanikoDir, strconv.Itoa(index))
if err := os.MkdirAll(dependencyDir, 0755); err != nil {
return err
}
logrus.Infof("trying to extract to %s", dependencyDir)
return util.GetFSFromImage(dependencyDir, image)
_, err := util.GetFSFromImage(dependencyDir, image)
return err
}
func saveStageAsTarball(stageIndex int, image v1.Image) error {
@@ -208,7 +316,7 @@ func saveStageAsTarball(stageIndex int, image v1.Image) error {
}
tarPath := filepath.Join(constants.KanikoIntermediateStagesDir, strconv.Itoa(stageIndex))
logrus.Infof("Storing source image from stage %d at path %s", stageIndex, tarPath)
return tarball.WriteToFile(tarPath, destRef, image, nil)
return tarball.WriteToFile(tarPath, destRef, image)
}
func getHasher(snapshotMode string) (func(string) (string, error), error) {
@@ -222,7 +330,7 @@ func getHasher(snapshotMode string) (func(string) (string, error), error) {
return nil, fmt.Errorf("%s is not a valid snapshot mode", snapshotMode)
}
func resolveOnBuild(stage *instructions.Stage, config *v1.Config) error {
func resolveOnBuild(stage *config.KanikoStage, config *v1.Config) error {
if config.OnBuild == nil {
return nil
}
@@ -239,3 +347,23 @@ func resolveOnBuild(stage *instructions.Stage, config *v1.Config) error {
config.OnBuild = nil
return nil
}
// reviewConfig makes sure the value of CMD is correct after building the stage
// If ENTRYPOINT was set in this stage but CMD wasn't, then CMD should be cleared out
// See Issue #346 for more info
func reviewConfig(stage config.KanikoStage, config *v1.Config) {
entrypoint := false
cmd := false
for _, c := range stage.Commands {
if c.Name() == constants.Cmd {
cmd = true
}
if c.Name() == constants.Entrypoint {
entrypoint = true
}
}
if entrypoint && !cmd {
config.Cmd = nil
}
}
+76
View File
@@ -0,0 +1,76 @@
/*
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 executor
import (
"testing"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/testutil"
"github.com/google/go-containerregistry/pkg/v1"
)
func Test_reviewConfig(t *testing.T) {
tests := []struct {
name string
dockerfile string
originalCmd []string
originalEntrypoint []string
expectedCmd []string
}{
{
name: "entrypoint and cmd declared",
dockerfile: `
FROM scratch
CMD ["mycmd"]
ENTRYPOINT ["myentrypoint"]`,
originalEntrypoint: []string{"myentrypoint"},
originalCmd: []string{"mycmd"},
expectedCmd: []string{"mycmd"},
},
{
name: "only entrypoint declared",
dockerfile: `
FROM scratch
ENTRYPOINT ["myentrypoint"]`,
originalEntrypoint: []string{"myentrypoint"},
originalCmd: []string{"mycmd"},
expectedCmd: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
config := &v1.Config{
Cmd: test.originalCmd,
Entrypoint: test.originalEntrypoint,
}
reviewConfig(stage(t, test.dockerfile), config)
testutil.CheckErrorAndDeepEqual(t, false, nil, test.expectedCmd, config.Cmd)
})
}
}
func stage(t *testing.T, d string) config.KanikoStage {
stages, err := dockerfile.Parse([]byte(d))
if err != nil {
t.Fatalf("error parsing dockerfile: %v", err)
}
return config.KanikoStage{
Stage: stages[0],
}
}
+35 -5
View File
@@ -21,12 +21,16 @@ import (
"fmt"
"net/http"
"github.com/GoogleContainerTools/kaniko/pkg/options"
"github.com/GoogleContainerTools/kaniko/pkg/cache"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/version"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/k8schain"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/pkg/errors"
@@ -43,7 +47,7 @@ func (w *withUserAgent) RoundTrip(r *http.Request) (*http.Response, error) {
}
// DoPush is responsible for pushing image to the destinations specified in opts
func DoPush(image v1.Image, opts *options.KanikoOptions) error {
func DoPush(image v1.Image, opts *config.KanikoOptions) error {
if opts.NoPush {
logrus.Info("Skipping push to container registry due to --no-push flag")
return nil
@@ -62,7 +66,7 @@ func DoPush(image v1.Image, opts *options.KanikoOptions) error {
for _, destRef := range destRefs {
tagToImage[destRef] = image
}
return tarball.MultiWriteToFile(opts.TarPath, tagToImage, nil)
return tarball.MultiWriteToFile(opts.TarPath, tagToImage)
}
// continue pushing unless an error occurs
@@ -87,16 +91,42 @@ func DoPush(image v1.Image, opts *options.KanikoOptions) error {
// Create a transport to set our user-agent.
tr := http.DefaultTransport
if opts.SkipTlsVerify {
if opts.SkipTLSVerify {
tr.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
rt := &withUserAgent{t: tr}
if err := remote.Write(destRef, image, pushAuth, rt, remote.WriteOptions{}); err != nil {
if err := remote.Write(destRef, image, pushAuth, rt); err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to push to destination %s", destRef))
}
}
return nil
}
// pushLayerToCache pushes layer (tagged with cacheKey) to opts.Cache
// if opts.Cache doesn't exist, infer the cache from the given destination
func pushLayerToCache(opts *config.KanikoOptions, cacheKey string, layer v1.Layer, createdBy string) error {
cache, err := cache.Destination(opts, cacheKey)
if err != nil {
return errors.Wrap(err, "getting cache destination")
}
logrus.Infof("Pushing layer %s to cache now", cache)
empty := empty.Image
empty, err = mutate.Append(empty,
mutate.Addendum{
Layer: layer,
History: v1.History{
Author: constants.Author,
CreatedBy: createdBy,
},
},
)
if err != nil {
return errors.Wrap(err, "appending layer onto empty image")
}
return DoPush(empty, &config.KanikoOptions{
Destinations: []string{cache},
})
}
+28 -3
View File
@@ -17,20 +17,28 @@ limitations under the License.
package snapshot
import (
"bytes"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/util"
)
type LayeredMap struct {
layers []map[string]string
whiteouts []map[string]string
added []map[string]string
hasher func(string) (string, error)
// cacheHasher doesn't include mtime in it's hash so that filesystem cache keys are stable
cacheHasher func(string) (string, error)
}
func NewLayeredMap(h func(string) (string, error)) *LayeredMap {
func NewLayeredMap(h func(string) (string, error), c func(string) (string, error)) *LayeredMap {
l := LayeredMap{
hasher: h,
hasher: h,
cacheHasher: c,
}
l.layers = []map[string]string{}
return &l
@@ -39,8 +47,18 @@ func NewLayeredMap(h func(string) (string, error)) *LayeredMap {
func (l *LayeredMap) Snapshot() {
l.whiteouts = append(l.whiteouts, map[string]string{})
l.layers = append(l.layers, map[string]string{})
l.added = append(l.added, map[string]string{})
}
// Key returns a hash for added files
func (l *LayeredMap) Key() (string, error) {
c := bytes.NewBuffer([]byte{})
enc := json.NewEncoder(c)
enc.Encode(l.added)
return util.SHA256(c)
}
// GetFlattenedPathsForWhiteOut returns all paths in the current FS
func (l *LayeredMap) GetFlattenedPathsForWhiteOut() map[string]struct{} {
paths := map[string]struct{}{}
for _, l := range l.layers {
@@ -85,11 +103,18 @@ func (l *LayeredMap) MaybeAddWhiteout(s string) (bool, error) {
// Add will add the specified file s to the layered map.
func (l *LayeredMap) Add(s string) error {
// Use hash function and add to layers
newV, err := l.hasher(s)
if err != nil {
return fmt.Errorf("Error creating hash for %s: %s", s, err)
return fmt.Errorf("Error creating hash for %s: %v", s, err)
}
l.layers[len(l.layers)-1][s] = newV
// Use cache hash function and add to added
cacheV, err := l.cacheHasher(s)
if err != nil {
return fmt.Errorf("Error creating cache hash for %s: %v", s, err)
}
l.added[len(l.added)-1][s] = cacheV
return nil
}
+79
View File
@@ -0,0 +1,79 @@
/*
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 snapshot
import (
"testing"
)
func Test_CacheKey(t *testing.T) {
tests := []struct {
name string
map1 map[string]string
map2 map[string]string
equal bool
}{
{
name: "maps are the same",
map1: map[string]string{
"a": "apple",
"b": "bat",
"c": "cat",
"d": "dog",
"e": "egg",
},
map2: map[string]string{
"c": "cat",
"d": "dog",
"b": "bat",
"a": "apple",
"e": "egg",
},
equal: true,
},
{
name: "maps are different",
map1: map[string]string{
"a": "apple",
"b": "bat",
"c": "cat",
},
map2: map[string]string{
"c": "",
"b": "bat",
"a": "apple",
},
equal: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
lm1 := LayeredMap{added: []map[string]string{test.map1}}
lm2 := LayeredMap{added: []map[string]string{test.map2}}
k1, err := lm1.Key()
if err != nil {
t.Fatalf("error getting key for map 1: %v", err)
}
k2, err := lm2.Key()
if err != nil {
t.Fatalf("error getting key for map 2: %v", err)
}
if test.equal != (k1 == k2) {
t.Fatalf("unexpected result: \nExpected\n%s\nActual\n%s\n", k1, k2)
}
})
}
}
+11 -38
View File
@@ -25,7 +25,6 @@ import (
"path/filepath"
"syscall"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/sirupsen/logrus"
)
@@ -49,6 +48,11 @@ func (s *Snapshotter) Init() error {
return nil
}
// Key returns a string based on the current state of the file system
func (s *Snapshotter) Key() (string, error) {
return s.l.Key()
}
// TakeSnapshot takes a snapshot of the specified files, avoiding directories in the whitelist, and creates
// a tarball of the changed files. Return contents of the tarball, and whether or not any files were changed
func (s *Snapshotter) TakeSnapshot(files []string) ([]byte, error) {
@@ -79,21 +83,6 @@ func (s *Snapshotter) TakeSnapshotFS() ([]byte, error) {
return contents, err
}
func shouldSnapshot(file string, snapshottedFiles map[string]bool) (bool, error) {
if val, ok := snapshottedFiles[file]; ok && val {
return false, nil
}
whitelisted, err := util.CheckWhitelist(file)
if err != nil {
return false, fmt.Errorf("Error checking for %s in whitelist: %s", file, err)
}
if whitelisted && !isBuildFile(file) {
logrus.Infof("Not adding %s to layer, as it's whitelisted", file)
return false, nil
}
return true, nil
}
// snapshotFiles creates a snapshot (tar) and adds the specified files.
// It will not add files which are whitelisted.
func (s *Snapshotter) snapshotFiles(f io.Writer, files []string) (bool, error) {
@@ -102,7 +91,8 @@ func (s *Snapshotter) snapshotFiles(f io.Writer, files []string) (bool, error) {
logrus.Info("No files changed in this command, skipping snapshotting.")
return false, nil
}
logrus.Infof("Taking snapshot of files %v...", files)
logrus.Info("Taking snapshot of files...")
logrus.Debugf("Taking snapshot of files %v", files)
snapshottedFiles := make(map[string]bool)
filesAdded := false
@@ -117,11 +107,7 @@ func (s *Snapshotter) snapshotFiles(f io.Writer, files []string) (bool, error) {
}
for _, file := range parentDirs {
file = filepath.Clean(file)
shouldSnapshot, err := shouldSnapshot(file, snapshottedFiles)
if err != nil {
return false, fmt.Errorf("Error checking if parent dir %s can be snapshotted: %s", file, err)
}
if !shouldSnapshot {
if val, ok := snapshottedFiles[file]; ok && val {
continue
}
snapshottedFiles[file] = true
@@ -142,19 +128,15 @@ func (s *Snapshotter) snapshotFiles(f io.Writer, files []string) (bool, error) {
// Next add the files themselves to the tar
for _, file := range files {
file = filepath.Clean(file)
shouldSnapshot, err := shouldSnapshot(file, snapshottedFiles)
if err != nil {
return false, fmt.Errorf("Error checking if file %s can be snapshotted: %s", file, err)
}
if !shouldSnapshot {
if val, ok := snapshottedFiles[file]; ok && val {
continue
}
snapshottedFiles[file] = true
if err = s.l.Add(file); err != nil {
if err := s.l.Add(file); err != nil {
return false, fmt.Errorf("Unable to add file %s to layered map: %s", file, err)
}
if err = t.AddFileToTar(file); err != nil {
if err := t.AddFileToTar(file); err != nil {
return false, fmt.Errorf("Error adding file %s to tar: %s", file, err)
}
filesAdded = true
@@ -162,15 +144,6 @@ func (s *Snapshotter) snapshotFiles(f io.Writer, files []string) (bool, error) {
return filesAdded, nil
}
func isBuildFile(file string) bool {
for _, buildFile := range constants.KanikoBuildFiles {
if file == buildFile {
return true
}
}
return false
}
// shapShotFS creates a snapshot (tar) of all files in the system which are not
// whitelisted and which have changed.
func (s *Snapshotter) snapShotFS(f io.Writer) (bool, error) {
+1 -1
View File
@@ -198,7 +198,7 @@ func setUpTestDir() (string, *Snapshotter, error) {
}
// Take the initial snapshot
l := NewLayeredMap(util.Hasher())
l := NewLayeredMap(util.Hasher(), util.CacheHasher())
snapshotter := NewSnapshotter(l, testDir)
if err := snapshotter.Init(); err != nil {
return testDir, nil, errors.Wrap(err, "initializing snapshotter")
+90
View File
@@ -0,0 +1,90 @@
/*
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 timing
import (
"bytes"
"fmt"
"sync"
"text/template"
"time"
)
// For testing
var currentTimeFunc = time.Now
// DefaultRun is the default "singleton" TimedRun instance.
var DefaultRun = NewTimedRun()
// TimedRun provides a running store of how long is spent in each category.
type TimedRun struct {
cl sync.Mutex
categories map[string]time.Duration // protected by cl
}
// Stop stops the specified timer and increments the time spent in that category.
func (tr *TimedRun) Stop(t *Timer) {
stop := currentTimeFunc()
if _, ok := tr.categories[t.category]; !ok {
tr.categories[t.category] = 0
}
fmt.Println(stop)
tr.cl.Lock()
defer tr.cl.Unlock()
tr.categories[t.category] += stop.Sub(t.startTime)
}
// Start starts a new Timer and returns it.
func Start(category string) *Timer {
t := Timer{
category: category,
startTime: currentTimeFunc(),
}
return &t
}
// NewTimedRun returns an initialized TimedRun instance.
func NewTimedRun() *TimedRun {
tr := TimedRun{
categories: map[string]time.Duration{},
}
return &tr
}
// Timer represents a running timer.
type Timer struct {
category string
startTime time.Time
}
// DefaultFormat is a default format string used by Summary.
var DefaultFormat = template.Must(template.New("").Parse("{{range $c, $t := .}}{{$c}}: {{$t}}\n{{end}}"))
// Summary outputs a summary of the DefaultTimedRun.
func Summary() string {
return DefaultRun.Summary()
}
// Summary outputs a summary of the specified TimedRun.
func (tr *TimedRun) Summary() string {
b := bytes.Buffer{}
tr.cl.Lock()
defer tr.cl.Unlock()
DefaultFormat.Execute(&b, tr.categories)
return b.String()
}
+139
View File
@@ -0,0 +1,139 @@
/*
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 timing
import (
"testing"
"time"
)
func patchTime(timeFunc func() time.Time) func() {
old := currentTimeFunc
currentTimeFunc = timeFunc
return func() {
currentTimeFunc = old
}
}
func mockTimeFunc(t time.Time) func() time.Time {
return func() time.Time {
return t
}
}
func TestTimedRun_StartStop(t *testing.T) {
type args struct {
categories map[string]time.Duration
category string
waitTime time.Duration
}
tests := []struct {
name string
args args
want time.Duration
}{
{
name: "new category",
args: args{
categories: map[string]time.Duration{},
category: "foo",
waitTime: 3 * time.Second,
},
want: 3 * time.Second,
},
{
name: "existing category",
args: args{
categories: map[string]time.Duration{
"foo": 4 * time.Second,
},
category: "foo",
waitTime: 2 * time.Second,
},
want: 6 * time.Second,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tr := &TimedRun{
categories: tt.args.categories,
}
timer := Timer{
category: tt.args.category,
startTime: time.Time{},
}
defer patchTime(mockTimeFunc(timer.startTime.Add(tt.args.waitTime)))()
tr.Stop(&timer)
if got := tr.categories[tt.args.category]; got != tt.want {
t.Errorf("Expected %d, got %d", tt.want, got)
}
})
}
}
func TestTimedRun_Summary(t *testing.T) {
type fields struct {
categories map[string]time.Duration
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "single key",
fields: fields{
categories: map[string]time.Duration{
"foo": 3 * time.Second,
},
},
want: "foo: 3s\n",
},
{
name: "two keys",
fields: fields{
categories: map[string]time.Duration{
"foo": 3 * time.Second,
"bar": 1 * time.Second,
},
},
want: "bar: 1s\nfoo: 3s\n",
},
{
name: "units",
fields: fields{
categories: map[string]time.Duration{
"foo": 3 * time.Second,
"bar": 1 * time.Millisecond,
},
},
want: "bar: 1ms\nfoo: 3s\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tr := &TimedRun{
categories: tt.fields.categories,
}
if got := tr.Summary(); got != tt.want {
t.Errorf("TimedRun.Summary() = %v, want %v", got, tt.want)
}
})
}
}
+2 -1
View File
@@ -17,8 +17,9 @@ limitations under the License.
package util
import (
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
)
func GetBucketAndItem(context string) (string, string) {
+1 -4
View File
@@ -228,10 +228,7 @@ func IsSrcRemoteFileURL(rawurl string) bool {
return false
}
_, err = http.Get(rawurl)
if err != nil {
return false
}
return true
return err == nil
}
func UpdateConfigEnv(newEnvs []instructions.KeyValuePair, config *v1.Config, replacementEnvs []string) error {
+7 -7
View File
@@ -23,7 +23,7 @@ import (
"github.com/GoogleContainerTools/kaniko/testutil"
)
var testUrl = "https://github.com/GoogleContainerTools/runtimes-common/blob/master/LICENSE"
var testURL = "https://github.com/GoogleContainerTools/runtimes-common/blob/master/LICENSE"
var testEnvReplacement = []struct {
path string
@@ -220,7 +220,7 @@ var matchSourcesTests = []struct {
srcs: []string{
"pkg/*",
"/root/dir?",
testUrl,
testURL,
},
files: []string{
"pkg/a",
@@ -234,7 +234,7 @@ var matchSourcesTests = []struct {
"/root/dir1",
"pkg/a",
"pkg/b",
testUrl,
testURL,
},
},
}
@@ -344,11 +344,11 @@ var isSrcValidTests = []struct {
},
{
srcsAndDest: []string{
testUrl,
testURL,
"dest",
},
resolvedSources: []string{
testUrl,
testURL,
},
shouldErr: false,
},
@@ -369,13 +369,13 @@ var testResolveSources = []struct {
srcsAndDest: []string{
"context/foo",
"context/b*",
testUrl,
testURL,
"dest/",
},
expectedList: []string{
"context/foo",
"context/bar",
testUrl,
testURL,
},
},
}
+96 -62
View File
@@ -34,31 +34,50 @@ import (
"github.com/sirupsen/logrus"
)
var whitelist = []string{
"/kaniko",
// /var/run is a special case. It's common to mount in /var/run/docker.sock or something similar
// which leads to a special mount on the /var/run/docker.sock file itself, but the directory to exist
// in the image with no way to tell if it came from the base image or not.
"/var/run",
type WhitelistEntry struct {
Path string
PrefixMatchOnly bool
}
var volumeWhitelist = []string{}
func GetFSFromImage(root string, img v1.Image) error {
var whitelist = []WhitelistEntry{
{
Path: "/kaniko",
PrefixMatchOnly: false,
},
{
// /var/run is a special case. It's common to mount in /var/run/docker.sock or something similar
// which leads to a special mount on the /var/run/docker.sock file itself, but the directory to exist
// in the image with no way to tell if it came from the base image or not.
Path: "/var/run",
PrefixMatchOnly: false,
},
{
// similarly, we whitelist /etc/mtab, since there is no way to know if the file was mounted or came
// from the base image
Path: "/etc/mtab",
PrefixMatchOnly: false,
},
}
// GetFSFromImage extracts the layers of img to root
// It returns a list of all files extracted
func GetFSFromImage(root string, img v1.Image) ([]string, error) {
whitelist, err := fileSystemWhitelist(constants.WhitelistPath)
if err != nil {
return err
return nil, err
}
logrus.Infof("Mounted directories: %v", whitelist)
logrus.Debugf("Mounted directories: %v", whitelist)
layers, err := img.Layers()
if err != nil {
return err
return nil, err
}
extractedFiles := []string{}
for i, l := range layers {
logrus.Infof("Extracting layer %d", i)
r, err := l.Uncompressed()
if err != nil {
return err
return nil, err
}
tr := tar.NewReader(r)
for {
@@ -67,7 +86,7 @@ func GetFSFromImage(root string, img v1.Image) error {
break
}
if err != nil {
return err
return nil, err
}
path := filepath.Join(root, filepath.Clean(hdr.Name))
base := filepath.Base(path)
@@ -76,40 +95,23 @@ func GetFSFromImage(root string, img v1.Image) error {
logrus.Debugf("Whiting out %s", path)
name := strings.TrimPrefix(base, ".wh.")
if err := os.RemoveAll(filepath.Join(dir, name)); err != nil {
return errors.Wrapf(err, "removing whiteout %s", hdr.Name)
return nil, errors.Wrapf(err, "removing whiteout %s", hdr.Name)
}
continue
}
whitelisted, err := CheckWhitelist(path)
if err != nil {
return err
}
if whitelisted && !checkWhitelistRoot(root) {
logrus.Debugf("Not adding %s because it is whitelisted", path)
continue
}
if hdr.Typeflag == tar.TypeSymlink {
whitelisted, err := CheckWhitelist(hdr.Linkname)
if err != nil {
return err
}
if whitelisted {
logrus.Debugf("skipping symlink from %s to %s because %s is whitelisted", hdr.Linkname, path, hdr.Linkname)
continue
}
}
if err := extractFile(root, hdr, tr); err != nil {
return err
return nil, err
}
extractedFiles = append(extractedFiles, filepath.Join(root, filepath.Clean(hdr.Name)))
}
}
return nil
return extractedFiles, nil
}
// DeleteFilesystem deletes the extracted image file system
func DeleteFilesystem() error {
logrus.Info("Deleting filesystem...")
err := filepath.Walk(constants.RootDir, func(path string, info os.FileInfo, err error) error {
return filepath.Walk(constants.RootDir, func(path string, info os.FileInfo, _ error) error {
whitelisted, err := CheckWhitelist(path)
if err != nil {
return err
@@ -123,20 +125,19 @@ func DeleteFilesystem() error {
}
return os.RemoveAll(path)
})
return err
}
// ChildDirInWhitelist returns true if there is a child file or directory of the path in the whitelist
func ChildDirInWhitelist(path, directory string) bool {
for _, d := range constants.KanikoBuildFiles {
dirPath := filepath.Join(directory, d)
if HasFilepathPrefix(dirPath, path) {
if HasFilepathPrefix(dirPath, path, false) {
return true
}
}
for _, d := range whitelist {
dirPath := filepath.Join(directory, d)
if HasFilepathPrefix(dirPath, path) {
dirPath := filepath.Join(directory, d.Path)
if HasFilepathPrefix(dirPath, path, d.PrefixMatchOnly) {
return true
}
}
@@ -170,6 +171,15 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader) error {
mode := hdr.FileInfo().Mode()
uid := hdr.Uid
gid := hdr.Gid
whitelisted, err := CheckWhitelist(path)
if err != nil {
return err
}
if whitelisted && !checkWhitelistRoot(dest) {
logrus.Debugf("Not adding %s because it is whitelisted", path)
return nil
}
switch hdr.Typeflag {
case tar.TypeReg:
logrus.Debugf("creating file %s", path)
@@ -180,6 +190,13 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader) error {
return err
}
}
// Check if something already exists at path (symlinks etc.)
// If so, delete it
if FilepathExists(path) {
if err := os.Remove(path); err != nil {
return errors.Wrapf(err, "error removing %s to make way for new file.", path)
}
}
currFile, err := os.Create(path)
if err != nil {
return err
@@ -195,7 +212,6 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader) error {
return err
}
currFile.Close()
case tar.TypeDir:
logrus.Debugf("creating dir %s", path)
if err := os.MkdirAll(path, mode); err != nil {
@@ -211,10 +227,26 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader) error {
case tar.TypeLink:
logrus.Debugf("link from %s to %s", hdr.Linkname, path)
whitelisted, err := CheckWhitelist(hdr.Linkname)
if err != nil {
return err
}
if whitelisted {
logrus.Debugf("skipping symlink from %s to %s because %s is whitelisted", hdr.Linkname, path, hdr.Linkname)
return nil
}
// The base directory for a link may not exist before it is created.
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
// Check if something already exists at path
// If so, delete it
if FilepathExists(path) {
if err := os.Remove(path); err != nil {
return errors.Wrapf(err, "error removing %s to make way for new link", hdr.Name)
}
}
if err := os.Link(filepath.Clean(filepath.Join("/", hdr.Linkname)), path); err != nil {
return err
}
@@ -246,7 +278,7 @@ func CheckWhitelist(path string) (bool, error) {
return false, err
}
for _, wl := range whitelist {
if HasFilepathPrefix(abs, wl) {
if HasFilepathPrefix(abs, wl.Path, wl.PrefixMatchOnly) {
return true, nil
}
}
@@ -258,7 +290,7 @@ func checkWhitelistRoot(root string) bool {
return false
}
for _, wl := range whitelist {
if HasFilepathPrefix(root, wl) {
if HasFilepathPrefix(root, wl.Path, wl.PrefixMatchOnly) {
return true
}
}
@@ -271,7 +303,7 @@ func checkWhitelistRoot(root string) bool {
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
// Where (5) is the mount point relative to the process's root
// From: https://www.kernel.org/doc/Documentation/filesystems/proc.txt
func fileSystemWhitelist(path string) ([]string, error) {
func fileSystemWhitelist(path string) ([]WhitelistEntry, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
@@ -294,7 +326,10 @@ func fileSystemWhitelist(path string) ([]string, error) {
}
if lineArr[4] != constants.RootDir {
logrus.Debugf("Appending %s from line: %s", lineArr[4], line)
whitelist = append(whitelist, lineArr[4])
whitelist = append(whitelist, WhitelistEntry{
Path: lineArr[4],
PrefixMatchOnly: false,
})
}
if err == io.EOF {
logrus.Debugf("Reached end of file %s", path)
@@ -310,11 +345,14 @@ func RelativeFiles(fp string, root string) ([]string, error) {
fullPath := filepath.Join(root, fp)
logrus.Debugf("Getting files and contents at root %s", fullPath)
err := filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
whitelisted, err := CheckWhitelist(path)
if err != nil {
return err
}
if whitelisted && !HasFilepathPrefix(path, root) {
if whitelisted && !HasFilepathPrefix(path, root, false) {
return nil
}
if err != nil {
@@ -377,22 +415,15 @@ func CreateFile(path string, reader io.Reader, perm os.FileMode, uid uint32, gid
return dest.Chown(int(uid), int(gid))
}
// AddPathToVolumeWhitelist adds the given path to the volume whitelist
// It will get snapshotted when the VOLUME command is run then ignored
// for subsequent commands.
func AddPathToVolumeWhitelist(path string) error {
logrus.Infof("adding %s to volume whitelist", path)
volumeWhitelist = append(volumeWhitelist, path)
return nil
}
// MoveVolumeWhitelistToWhitelist copies over all directories that were volume mounted
// in this step to be whitelisted for all subsequent docker commands.
func MoveVolumeWhitelistToWhitelist() error {
if len(volumeWhitelist) > 0 {
whitelist = append(whitelist, volumeWhitelist...)
volumeWhitelist = []string{}
}
// AddVolumePathToWhitelist adds the given path to the whitelist with
// PrefixMatchOnly set to true. Snapshotting will ignore paths prefixed
// with the volume, but the volume itself will not be ignored.
func AddVolumePathToWhitelist(path string) error {
logrus.Infof("adding volume %s to whitelist", path)
whitelist = append(whitelist, WhitelistEntry{
Path: path,
PrefixMatchOnly: true,
})
return nil
}
@@ -492,7 +523,7 @@ func CopyFile(src, dest string) error {
}
// HasFilepathPrefix checks if the given file path begins with prefix
func HasFilepathPrefix(path, prefix string) bool {
func HasFilepathPrefix(path, prefix string, prefixMatchOnly bool) bool {
path = filepath.Clean(path)
prefix = filepath.Clean(prefix)
pathArray := strings.Split(path, "/")
@@ -501,6 +532,9 @@ func HasFilepathPrefix(path, prefix string) bool {
if len(pathArray) < len(prefixArray) {
return false
}
if prefixMatchOnly && len(pathArray) == len(prefixArray) {
return false
}
for index := range prefixArray {
if prefixArray[index] == pathArray[index] {
continue
+50 -23
View File
@@ -50,9 +50,21 @@ func Test_fileSystemWhitelist(t *testing.T) {
}
actualWhitelist, err := fileSystemWhitelist(path)
expectedWhitelist := []string{"/kaniko", "/proc", "/dev", "/dev/pts", "/sys", "/var/run"}
sort.Strings(actualWhitelist)
sort.Strings(expectedWhitelist)
expectedWhitelist := []WhitelistEntry{
{"/kaniko", false},
{"/proc", false},
{"/dev", false},
{"/dev/pts", false},
{"/sys", false},
{"/var/run", false},
{"/etc/mtab", false},
}
sort.Slice(actualWhitelist, func(i, j int) bool {
return actualWhitelist[i].Path < actualWhitelist[j].Path
})
sort.Slice(expectedWhitelist, func(i, j int) bool {
return expectedWhitelist[i].Path < expectedWhitelist[j].Path
})
testutil.CheckErrorAndDeepEqual(t, false, err, expectedWhitelist, actualWhitelist)
}
@@ -167,7 +179,7 @@ func Test_ParentDirectories(t *testing.T) {
func Test_CheckWhitelist(t *testing.T) {
type args struct {
path string
whitelist []string
whitelist []WhitelistEntry
}
tests := []struct {
name string
@@ -178,7 +190,7 @@ func Test_CheckWhitelist(t *testing.T) {
name: "file whitelisted",
args: args{
path: "/foo",
whitelist: []string{"/foo"},
whitelist: []WhitelistEntry{{"/foo", false}},
},
want: true,
},
@@ -186,7 +198,7 @@ func Test_CheckWhitelist(t *testing.T) {
name: "directory whitelisted",
args: args{
path: "/foo/bar",
whitelist: []string{"/foo"},
whitelist: []WhitelistEntry{{"/foo", false}},
},
want: true,
},
@@ -194,7 +206,7 @@ func Test_CheckWhitelist(t *testing.T) {
name: "grandparent whitelisted",
args: args{
path: "/foo/bar/baz",
whitelist: []string{"/foo"},
whitelist: []WhitelistEntry{{"/foo", false}},
},
want: true,
},
@@ -202,7 +214,7 @@ func Test_CheckWhitelist(t *testing.T) {
name: "sibling whitelisted",
args: args{
path: "/foo/bar/baz",
whitelist: []string{"/foo/bat"},
whitelist: []WhitelistEntry{{"/foo/bat", false}},
},
want: false,
},
@@ -227,8 +239,9 @@ func Test_CheckWhitelist(t *testing.T) {
func TestHasFilepathPrefix(t *testing.T) {
type args struct {
path string
prefix string
path string
prefix string
prefixMatchOnly bool
}
tests := []struct {
name string
@@ -238,47 +251,61 @@ func TestHasFilepathPrefix(t *testing.T) {
{
name: "parent",
args: args{
path: "/foo/bar",
prefix: "/foo",
path: "/foo/bar",
prefix: "/foo",
prefixMatchOnly: false,
},
want: true,
},
{
name: "nested parent",
args: args{
path: "/foo/bar/baz",
prefix: "/foo/bar",
path: "/foo/bar/baz",
prefix: "/foo/bar",
prefixMatchOnly: false,
},
want: true,
},
{
name: "sibling",
args: args{
path: "/foo/bar",
prefix: "/bar",
path: "/foo/bar",
prefix: "/bar",
prefixMatchOnly: false,
},
want: false,
},
{
name: "nested sibling",
args: args{
path: "/foo/bar/baz",
prefix: "/foo/bar",
path: "/foo/bar/baz",
prefix: "/foo/bar",
prefixMatchOnly: false,
},
want: true,
},
{
name: "name prefix",
args: args{
path: "/foo2/bar",
prefix: "/foo",
path: "/foo2/bar",
prefix: "/foo",
prefixMatchOnly: false,
},
want: false,
},
{
name: "prefix match only (volume)",
args: args{
path: "/foo",
prefix: "/foo",
prefixMatchOnly: true,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasFilepathPrefix(tt.args.path, tt.args.prefix); got != tt.want {
if got := HasFilepathPrefix(tt.args.path, tt.args.prefix, tt.args.prefixMatchOnly); got != tt.want {
t.Errorf("HasFilepathPrefix() = %v, want %v", got, tt.want)
}
})
@@ -343,8 +370,8 @@ func filesAreHardlinks(first, second string) checker {
if err != nil {
t.Fatalf("error getting file %s", second)
}
stat1 := getSyscallStat_t(fi1)
stat2 := getSyscallStat_t(fi2)
stat1 := getSyscallStatT(fi1)
stat2 := getSyscallStatT(fi2)
if stat1.Ino != stat2.Ino {
t.Errorf("%s and %s aren't hardlinks as they dont' have the same inode", first, second)
}
+8 -12
View File
@@ -25,11 +25,12 @@ import (
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/sirupsen/logrus"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
)
@@ -40,9 +41,8 @@ var (
)
// RetrieveSourceImage returns the base image of the stage at index
func RetrieveSourceImage(index int, buildArgs []string, stages []instructions.Stage) (v1.Image, error) {
currentStage := stages[index]
currentBaseName, err := ResolveEnvironmentReplacement(currentStage.BaseName, buildArgs, false)
func RetrieveSourceImage(stage config.KanikoStage, buildArgs []string) (v1.Image, error) {
currentBaseName, err := ResolveEnvironmentReplacement(stage.BaseName, buildArgs, false)
if err != nil {
return nil, err
}
@@ -53,20 +53,16 @@ func RetrieveSourceImage(index int, buildArgs []string, stages []instructions.St
}
// Next, check if the base image of the current stage is built from a previous stage
// If so, retrieve the image from the stored tarball
for i, stage := range stages {
if i > index {
continue
}
if stage.Name == currentBaseName {
return retrieveTarImage(i)
}
if stage.BaseImageStoredLocally {
return retrieveTarImage(stage.BaseImageIndex)
}
// Otherwise, initialize image as usual
return retrieveRemoteImage(currentBaseName)
}
// RetrieveConfigFile returns the config file for an image
func RetrieveConfigFile(sourceImage v1.Image) (*v1.ConfigFile, error) {
func RetrieveConfigFile(sourceImage partial.WithConfigFile) (*v1.ConfigFile, error) {
imageConfig, err := sourceImage.ConfigFile()
if err != nil {
return nil, err
+12 -3
View File
@@ -20,6 +20,7 @@ import (
"bytes"
"testing"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/testutil"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
@@ -54,7 +55,9 @@ func Test_StandardImage(t *testing.T) {
return nil, nil
}
retrieveRemoteImage = mock
actual, err := RetrieveSourceImage(0, nil, stages)
actual, err := RetrieveSourceImage(config.KanikoStage{
Stage: stages[0],
}, nil)
testutil.CheckErrorAndDeepEqual(t, false, err, nil, actual)
}
func Test_ScratchImage(t *testing.T) {
@@ -62,7 +65,9 @@ func Test_ScratchImage(t *testing.T) {
if err != nil {
t.Error(err)
}
actual, err := RetrieveSourceImage(1, nil, stages)
actual, err := RetrieveSourceImage(config.KanikoStage{
Stage: stages[1],
}, nil)
expected := empty.Image
testutil.CheckErrorAndDeepEqual(t, false, err, expected, actual)
}
@@ -80,7 +85,11 @@ func Test_TarImage(t *testing.T) {
return nil, nil
}
retrieveTarImage = mock
actual, err := RetrieveSourceImage(2, nil, stages)
actual, err := RetrieveSourceImage(config.KanikoStage{
BaseImageStoredLocally: true,
BaseImageIndex: 0,
Stage: stages[2],
}, nil)
testutil.CheckErrorAndDeepEqual(t, false, err, nil, actual)
}
+5 -10
View File
@@ -118,7 +118,7 @@ func (t *Tar) Whiteout(p string) error {
func (t *Tar) checkHardlink(p string, i os.FileInfo) (bool, string) {
hardlink := false
linkDst := ""
stat := getSyscallStat_t(i)
stat := getSyscallStatT(i)
if stat != nil {
nlinks := stat.Nlink
if nlinks > 1 {
@@ -135,7 +135,7 @@ func (t *Tar) checkHardlink(p string, i os.FileInfo) (bool, string) {
return hardlink, linkDst
}
func getSyscallStat_t(i os.FileInfo) *syscall.Stat_t {
func getSyscallStatT(i os.FileInfo) *syscall.Stat_t {
if sys := i.Sys(); sys != nil {
if stat, ok := sys.(*syscall.Stat_t); ok {
return stat
@@ -195,10 +195,10 @@ func fileIsCompressedTar(src string) (bool, archive.Compression) {
func fileIsUncompressedTar(src string) bool {
r, err := os.Open(src)
defer r.Close()
if err != nil {
return false
}
defer r.Close()
fi, err := os.Lstat(src)
if err != nil {
return false
@@ -210,13 +210,8 @@ func fileIsUncompressedTar(src string) bool {
if tr == nil {
return false
}
for {
_, err := tr.Next()
if err != nil {
return false
}
return true
}
_, err = tr.Next()
return err == nil
}
// UnpackCompressedTar unpacks the compressed tar at path to dir
+41
View File
@@ -18,6 +18,7 @@ package util
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"io"
"os"
@@ -72,6 +73,36 @@ func Hasher() func(string) (string, error) {
return hasher
}
// CacheHasher takes into account everything the regular hasher does except for mtime
func CacheHasher() func(string) (string, error) {
hasher := func(p string) (string, error) {
h := md5.New()
fi, err := os.Lstat(p)
if err != nil {
return "", err
}
h.Write([]byte(fi.Mode().String()))
h.Write([]byte(strconv.FormatUint(uint64(fi.Sys().(*syscall.Stat_t).Uid), 36)))
h.Write([]byte(","))
h.Write([]byte(strconv.FormatUint(uint64(fi.Sys().(*syscall.Stat_t).Gid), 36)))
if fi.Mode().IsRegular() {
f, err := os.Open(p)
if err != nil {
return "", err
}
defer f.Close()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
}
return hex.EncodeToString(h.Sum(nil)), nil
}
return hasher
}
// MtimeHasher returns a hash function, which only looks at mtime to determine if a file has changed.
// Note that the mtime can lag, so it's possible that a file will have changed but the mtime may look the same.
func MtimeHasher() func(string) (string, error) {
@@ -86,3 +117,13 @@ func MtimeHasher() func(string) (string, error) {
}
return hasher
}
// SHA256 returns the shasum of the contents of r
func SHA256(r io.Reader) (string, error) {
hasher := sha256.New()
_, err := io.Copy(hasher, r)
if err != nil {
return "", err
}
return hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))), nil
}