Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
245c70c
feat(pkg/commands): add support for build secrets
SasSwart Oct 18, 2024
81f924f
feat(pkg/commands): pass secrets into the run command
SasSwart Oct 21, 2024
b509184
feat(pkg/commands): pass secrets into the run marker
SasSwart Oct 21, 2024
4a667d0
chore(pkg): pass a nil linter to tests that now require is
SasSwart Oct 21, 2024
83c8a18
chore(commands): improve variable naming and add documentation
SasSwart Oct 21, 2024
d37707e
chore(commands): improve variable naming and add documentation
SasSwart Oct 21, 2024
7d27c5a
chore(pkg/executor): set a default working directory if one hasn't be…
SasSwart Oct 21, 2024
80e0bdb
deps: bump github.com/moby/buildkit and github.com/docker/docker (#3242)
aaron-prindle Jul 8, 2024
75c841a
chore(deps): slim down to a failing test for reproducibility
SasSwart Oct 22, 2024
c0d174f
chore(deps): bump github.com/moby/buildkit from v0.14.1 to v0.16.0
SasSwart Oct 22, 2024
d1daf05
chore(deps): test against the latest version of docker
SasSwart Oct 22, 2024
bf140c2
chore(deps): test against the latest version of docker
SasSwart Oct 22, 2024
6e3f198
chore(pkg/executor): remove defunct code
SasSwart Oct 22, 2024
2b0d796
chore(integration): add integration tests for build secrets
SasSwart Oct 23, 2024
2999f79
Merge remote-tracking branch 'origin/main' into jjs/build-secrets
SasSwart Oct 23, 2024
df94a9d
fix(pkg/commands): improve code documentation and style
SasSwart Oct 23, 2024
0f29fa1
fix(pkg/integration): test relative targets for docker build secrets
SasSwart Oct 23, 2024
97120de
fix(pkg/integration): test relative targets for docker build secrets
SasSwart Oct 23, 2024
8f144a6
fix(pkg/integration): fix TestLayers
SasSwart Oct 24, 2024
8783c3e
fix(pkg/commands): guarantee secret cleanup after run commands
SasSwart Oct 24, 2024
0bd5bf7
fix(pkg/commands): add tests for fileCreatorCleaner
SasSwart Oct 25, 2024
91a95ae
fix(pkg/commands): use the filesystem package where appropriate
SasSwart Oct 25, 2024
7224b96
fix(pkg/filesystem): remove defunct package level functions
SasSwart Oct 25, 2024
a8c3296
fix(pkg/filesystem): fix an edge case in the fileCreatorCleaner
SasSwart Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/executor/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func init() {

addKanikoOptionsFlags()
addHiddenFlags(RootCmd)
opts.BuildSecrets = readBuildSecrets(os.Environ())
RootCmd.PersistentFlags().BoolVarP(&opts.IgnoreVarRun, "whitelist-var-run", "", true, "Ignore /var/run directory when taking image snapshot. Set it to false to preserve /var/run/ in destination image.")
RootCmd.PersistentFlags().MarkDeprecated("whitelist-var-run", "Please use ignore-var-run instead.")
}
Expand Down Expand Up @@ -296,6 +297,19 @@ func addHiddenFlags(cmd *cobra.Command) {
cmd.PersistentFlags().MarkHidden("bucket")
}

const buildSecretPrefix = "KANIKO_BUILD_SECRET_"

func readBuildSecrets(environment []string) []string {
var buildSecrets []string
for _, secret := range environment {
if strings.HasPrefix(secret, buildSecretPrefix) {
buildSecrets = append(buildSecrets, strings.TrimPrefix(secret, buildSecretPrefix))
}
}

return buildSecrets
}

// checkKanikoDir will check whether the executor is operating in the default '/kaniko' directory,
// conducting the relevant operations if it is not
func checkKanikoDir(dir string) error {
Expand Down
28 changes: 28 additions & 0 deletions integration/dockerfiles/Dockerfile_test_run_mount
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright 2020 Google, Inc. All rights reserved.
#
# 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.

FROM debian:10.13
# Make sure we set secrets to env the same way docker does
RUN --mount=type=secret,id=FOO,env=FOO,required=true echo "$FOO" > /etc/foo
# Make sure we set secrets to default disk location the same way docker does
RUN --mount=type=secret,id=BAR cat /run/secrets/BAR > /etc/bar
# Make sure we set secrets to custom disk location the same way docker does
RUN --mount=type=secret,id=BAZ,target=/baz.secret cat /baz.secret > /etc/baz
# Make sure relative targets for secret mounts work:
WORKDIR /etc
RUN --mount=type=secret,id=QUX,target=qux.secret cat qux.secret > /etc/qux

# Test with ARG so that we know our implementation of secrets don't override args
ARG file
RUN echo "run" > $file
32 changes: 31 additions & 1 deletion integration/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var argsMap = map[string][]string{
"Dockerfile_test_run": {"file=/file"},
"Dockerfile_test_run_new": {"file=/file"},
"Dockerfile_test_run_redo": {"file=/file"},
"Dockerfile_test_run_mount": {"file=/file"},
"Dockerfile_test_workdir": {"workdir=/arg/workdir"},
"Dockerfile_test_add": {"file=context/foo"},
"Dockerfile_test_arg_secret": {"SSH_PRIVATE_KEY", "SSH_PUBLIC_KEY=Pµbl1cK€Y"},
Expand All @@ -68,6 +69,20 @@ var argsMap = map[string][]string{
"Dockerfile_test_multistage": {"file=/foo2"},
}

type buildSecret struct {
name string
value string
}

var secretsMap = map[string][]buildSecret{
"Dockerfile_test_run_mount": {
{name: "FOO", value: "foo"},
{name: "BAR", value: "bar"},
{name: "BAZ", value: "baz"},
{name: "QUX", value: "qux"},
},
}

// Environment to build Dockerfiles with, used for both docker and kaniko builds
var envsMap = map[string][]string{
"Dockerfile_test_arg_secret": {"SSH_PRIVATE_KEY=ThEPriv4t3Key"},
Expand Down Expand Up @@ -257,8 +272,15 @@ func (d *DockerFileBuilder) BuildDockerImage(t *testing.T, imageRepo, dockerfile
buildArgs = append(buildArgs, buildArgFlag, arg)
}

var buildSecrets []string
secretFlag := "--secret"
for _, secret := range secretsMap[dockerfile] {
buildSecrets = append(buildSecrets, secretFlag, "id="+secret.name)
}

// build docker image
additionalFlags := append(buildArgs, additionalDockerFlagsMap[dockerfile]...)
additionalFlags := append(buildArgs, buildSecrets...)
additionalFlags = append(additionalFlags, additionalDockerFlagsMap[dockerfile]...)
dockerImage := strings.ToLower(imageRepo + dockerPrefix + dockerfile)

dockerArgs := []string{
Expand All @@ -278,6 +300,9 @@ func (d *DockerFileBuilder) BuildDockerImage(t *testing.T, imageRepo, dockerfile
if env, ok := envsMap[dockerfile]; ok {
dockerCmd.Env = append(dockerCmd.Env, env...)
}
for _, secret := range secretsMap[dockerfile] {
dockerCmd.Env = append(dockerCmd.Env, fmt.Sprintf("%s=%s", secret.name, secret.value))
}

out, err := RunCommandWithoutTest(dockerCmd)
if err != nil {
Expand Down Expand Up @@ -506,6 +531,11 @@ func buildKanikoImage(
}
}

if secrets, ok := secretsMap[dockerfile]; ok {
for _, secret := range secrets {
dockerRunFlags = append(dockerRunFlags, "-e", fmt.Sprintf("KANIKO_BUILD_SECRET_%s=%s", secret.name, secret.value))
}
}
dockerRunFlags = addServiceAccountFlags(dockerRunFlags, serviceAccount)

kanikoDockerfilePath := path.Join(buildContextPath, dockerfilesPath, dockerfile)
Expand Down
5 changes: 3 additions & 2 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,8 +591,9 @@ func TestBuildWithHTTPError(t *testing.T) {

func TestLayers(t *testing.T) {
offset := map[string]int{
"Dockerfile_test_add": 12,
"Dockerfile_test_scratch": 3,
"Dockerfile_test_add": 12,
"Dockerfile_test_scratch": 3,
"Dockerfile_test_run_mount": 1, // The WORKDIR layer is not present in the kaniko image
}

if os.Getenv("CI") == "true" {
Expand Down
6 changes: 3 additions & 3 deletions pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ type DockerCommand interface {
IsArgsEnvsRequiredInCache() bool
}

func GetCommand(cmd instructions.Command, fileContext util.FileContext, useNewRun bool, cacheCopy bool, cacheRun bool, output *RunOutput) (DockerCommand, error) {
func GetCommand(cmd instructions.Command, fileContext util.FileContext, useNewRun bool, cacheCopy bool, cacheRun bool, output *RunOutput, buildSecrets []string) (DockerCommand, error) {
switch c := cmd.(type) {
case *instructions.RunCommand:
if useNewRun {
return &RunMarkerCommand{cmd: c, shdCache: cacheRun, output: output}, nil
return &RunMarkerCommand{cmd: c, shdCache: cacheRun, output: output, buildSecrets: buildSecrets}, nil
}
return &RunCommand{cmd: c, shdCache: cacheRun, output: output}, nil
return &RunCommand{cmd: c, shdCache: cacheRun, output: output, buildSecrets: buildSecrets}, nil
case *instructions.CopyCommand:
return &CopyCommand{cmd: c, fileContext: fileContext, shdCache: cacheCopy}, nil
case *instructions.ExposeCommand:
Expand Down
148 changes: 143 additions & 5 deletions pkg/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"

Expand All @@ -42,11 +43,14 @@ type RunOutput struct {

type RunCommand struct {
BaseCommand
cmd *instructions.RunCommand
output *RunOutput
shdCache bool
cmd *instructions.RunCommand
output *RunOutput
buildSecrets []string
shdCache bool
}

const secretsDir = "/run/secrets"

// for testing
var (
userLookup = util.LookupUser
Expand All @@ -57,10 +61,10 @@ func (r *RunCommand) IsArgsEnvsRequiredInCache() bool {
}

func (r *RunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
return runCommandInExec(config, buildArgs, r.cmd, r.output)
return runCommandInExec(config, buildArgs, r.cmd, r.output, r.buildSecrets)
}

func runCommandInExec(config *v1.Config, buildArgs *dockerfile.BuildArgs, cmdRun *instructions.RunCommand, output *RunOutput) error {
func runCommandInExec(config *v1.Config, buildArgs *dockerfile.BuildArgs, cmdRun *instructions.RunCommand, output *RunOutput, buildSecrets []string) (err error) {
if output == nil {
output = &RunOutput{}
}
Expand Down Expand Up @@ -131,6 +135,82 @@ func runCommandInExec(config *v1.Config, buildArgs *dockerfile.BuildArgs, cmdRun
return errors.Wrap(err, "adding default HOME variable")
}

cmdRun.Expand(func(word string) (string, error) {
// NOTE(SasSwart): This is a noop function. It's here to satisfy the buildkit parser.
// Without this, the buildkit parser won't parse --mount flags for RUN directives.
// Support for expansion in RUN directives deferred until its needed.
// https://docs.docker.com/build/building/variables/
return word, nil
})

buildSecretsMap := make(map[string]string)
for _, s := range buildSecrets {
secretName, secretValue, found := strings.Cut(s, "=")
if !found {
return fmt.Errorf("invalid secret %s", s)
}
buildSecretsMap[secretName] = secretValue
}

secretFileManager := FileCreatorCleaner{}
defer func() {
cleanupErr := secretFileManager.Clean()
if err == nil {
err = cleanupErr
}
}()

mounts := instructions.GetMounts(cmdRun)
for _, mount := range mounts {
switch mount.Type {
case instructions.MountTypeSecret:
// Implemented as per:
// https://docs.docker.com/reference/dockerfile/#run---mounttypesecret

envName := mount.CacheID
secret, secretSet := buildSecretsMap[envName]
if !secretSet && mount.Required {
return fmt.Errorf("required secret %s not found", mount.CacheID)
}

// If a target is specified, we write to the file specified by the target:
// If no target is specified and no env is specified, we write to /run/secrets/<id>
// If no target is specified and an env is specified, we set the env and don't write to file
if mount.Env == nil || mount.Target != "" {
targetFile := mount.Target
if targetFile == "" {
targetFile = filepath.Join(secretsDir, mount.CacheID)
}
if !filepath.IsAbs(targetFile) {
targetFile = filepath.Join(config.WorkingDir, targetFile)
}
secretFileManager.MkdirAndWriteFile(targetFile, []byte(secret), 0700, 0600)
}

// We don't return in the block above, because its possible to have both a target and an env.
// As such we need this guard clause or we risk getting a nil pointer below.
if mount.Env == nil {
continue
}

targetEnv := *mount.Env
if targetEnv == "" {
targetEnv = mount.CacheID
}

env = append(env, fmt.Sprintf("%s=%s", targetEnv, secret))
// NOTE(SasSwart):
// Buildkit v0.16.0 brought support for `RUN --mount` flags. Kaniko support for the mount
// types below is deferred until its needed.
// case instructions.MountTypeBind:
// case instructions.MountTypeTmpfs:
// case instructions.MountTypeCache:
// case instructions.MountTypeSSH
default:
logrus.Warnf("Mount type %s is not supported", mount.Type)
}
}

cmd.Env = env

logrus.Infof("Running: %s", cmd.Args)
Expand All @@ -153,6 +233,64 @@ func runCommandInExec(config *v1.Config, buildArgs *dockerfile.BuildArgs, cmdRun
return nil
}

// FileCreatorCleaner keeps tracks of all files and directories that it created in the order that they were created.
// Once asked to clean up, it will remove all files and directories in the reverse order that they were created.
type FileCreatorCleaner struct {
filesToClean []string
dirsToClean []string
}

func (s *FileCreatorCleaner) MkdirAndWriteFile(path string, data []byte, dirPerm, filePerm os.FileMode) error {
dirPath := filepath.Dir(path)
parentDirs := filepath.SplitList(dirPath)

// Start at the root directory
currentPath := string(os.PathSeparator)

for _, nextDirDown := range parentDirs {
// Traverse one level down
currentPath = filepath.Join(currentPath, nextDirDown)

if _, err := filesystem.FS.Stat(currentPath); os.IsNotExist(err) {
if err := os.Mkdir(currentPath, dirPerm); err != nil {
return err
}
s.dirsToClean = append(s.dirsToClean, currentPath)
}
}

// With all parent directories created, we can now create the actual secret file
if err := os.WriteFile(path, []byte(data), 0600); err != nil {
return errors.Wrap(err, "writing secret to file")
}
s.filesToClean = append(s.filesToClean, path)

return nil
}

func (s *FileCreatorCleaner) Clean() error {
for i := len(s.filesToClean) - 1; i >= 0; i-- {
if err := os.Remove(s.filesToClean[i]); err != nil {
return err
}
}

for i := len(s.dirsToClean) - 1; i >= 0; i-- {
if err := os.Remove(s.dirsToClean[i]); err != nil {
pathErr := os.PathError{}
// If a path that we need to clean up is not empty, then that means
// that a third party has placed something in there since we created it.
// In that case, we should not remove it, because it no longer belongs exclusively to us.
if errors.As(err, &pathErr); pathErr.Err == syscall.ENOTEMPTY {
continue
}
return err
}
}

return nil
}

// addDefaultHOME adds the default value for HOME if it isn't already set
func addDefaultHOME(u string, envs []string) ([]string, error) {
for _, env := range envs {
Expand Down
11 changes: 6 additions & 5 deletions pkg/commands/run_marker.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,18 @@ import (

type RunMarkerCommand struct {
BaseCommand
cmd *instructions.RunCommand
output *RunOutput
Files []string
shdCache bool
cmd *instructions.RunCommand
output *RunOutput
Files []string
buildSecrets []string
shdCache bool
}

func (r *RunMarkerCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
// run command `touch filemarker`
logrus.Debugf("Using new RunMarker command")
prevFilesMap, _ := util.GetFSInfoMap("/", map[string]os.FileInfo{})
if err := runCommandInExec(config, buildArgs, r.cmd, r.output); err != nil {
if err := runCommandInExec(config, buildArgs, r.cmd, r.output, r.buildSecrets); err != nil {
return err
}
_, r.Files = util.GetFSInfoMap("/", prevFilesMap)
Expand Down
1 change: 1 addition & 0 deletions pkg/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type KanikoOptions struct {
IgnorePaths multiArg
DockerfilePath string
DockerfileContent string
BuildSecrets []string
SrcContext string
SnapshotMode string
SnapshotModeDeprecated string
Expand Down
2 changes: 1 addition & 1 deletion pkg/executor/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, sta
command, err := commands.GetCommand(cmd, fileContext, opts.RunV2, opts.CacheCopyLayers, opts.CacheRunLayers, &commands.RunOutput{
Stdout: opts.RunStdout,
Stderr: opts.RunStderr,
})
}, opts.BuildSecrets)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading