diff --git a/README.md b/README.md index cd5bd65ddc..6b20567a9e 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ To configure credentials, you will need to do the following: - name: aws-secret mountPath: /root/.aws/ - name: docker-config - mountPath: /root/.docker/ + mountPath: /kaniko/.docker/ restartPolicy: Never volumes: - name: aws-secret diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 404a02d5c8..c05c21b673 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -17,7 +17,6 @@ limitations under the License. package cmd import ( - "errors" "os" "path/filepath" "strings" @@ -25,44 +24,25 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/buildcontext" "github.com/GoogleContainerTools/kaniko/pkg/constants" "github.com/GoogleContainerTools/kaniko/pkg/executor" + "github.com/GoogleContainerTools/kaniko/pkg/options" "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/genuinetools/amicontained/container" + "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var ( - dockerfilePath string - destinations multiArg - srcContext string - snapshotMode string - bucket string - dockerInsecureSkipTLSVerify bool - logLevel string - force bool - buildArgs multiArg - tarPath string - singleSnapshot bool - reproducible bool - target string - noPush bool + opts = &options.KanikoOptions{} + logLevel string + force bool ) func init() { - RootCmd.PersistentFlags().StringVarP(&dockerfilePath, "dockerfile", "f", "Dockerfile", "Path to the dockerfile to be built.") - RootCmd.PersistentFlags().StringVarP(&srcContext, "context", "c", "/workspace/", "Path to the dockerfile build context.") - RootCmd.PersistentFlags().StringVarP(&bucket, "bucket", "b", "", "Name of the GCS bucket from which to access build context as tarball.") - RootCmd.PersistentFlags().VarP(&destinations, "destination", "d", "Registry the final image should be pushed to. Set it repeatedly for multiple destinations.") - RootCmd.PersistentFlags().StringVarP(&snapshotMode, "snapshotMode", "", "full", "Set this flag to change the file attributes inspected during snapshotting") - RootCmd.PersistentFlags().VarP(&buildArgs, "build-arg", "", "This flag allows you to pass in ARG values at build time. Set it repeatedly for multiple values.") - RootCmd.PersistentFlags().BoolVarP(&dockerInsecureSkipTLSVerify, "insecure-skip-tls-verify", "", false, "Push to insecure registry ignoring TLS verify") RootCmd.PersistentFlags().StringVarP(&logLevel, "verbosity", "v", constants.DefaultLogLevel, "Log level (debug, info, warn, error, fatal, panic") RootCmd.PersistentFlags().BoolVarP(&force, "force", "", false, "Force building outside of a container") - RootCmd.PersistentFlags().StringVarP(&tarPath, "tarPath", "", "", "Path to save the image in as a tarball instead of pushing") - RootCmd.PersistentFlags().BoolVarP(&singleSnapshot, "single-snapshot", "", false, "Set this flag to take a single snapshot at the end of the build.") - RootCmd.PersistentFlags().BoolVarP(&reproducible, "reproducible", "", false, "Strip timestamps out of the image to make it reproducible") - RootCmd.PersistentFlags().StringVarP(&target, "target", "", "", " Set the target build stage to build") - RootCmd.PersistentFlags().BoolVarP(&noPush, "no-push", "", false, "Do not push the image to the registry") + addKanikoOptionsFlags(RootCmd) + addHiddenFlags(RootCmd) } var RootCmd = &cobra.Command{ @@ -71,52 +51,54 @@ var RootCmd = &cobra.Command{ if err := util.SetLogLevel(logLevel); err != nil { return err } - if err := resolveSourceContext(); err != nil { - return err - } - if !noPush && len(destinations) == 0 { + if !opts.NoPush && len(opts.Destinations) == 0 { return errors.New("You must provide --destination, or use --no-push") } - - return checkDockerfilePath() + if err := resolveSourceContext(); err != nil { + return errors.Wrap(err, "error resolving source context") + } + return resolveDockerfilePath() }, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { if !checkContained() { if !force { - logrus.Error("kaniko should only be run inside of a container, run with the --force flag if you are sure you want to continue.") - os.Exit(1) + return errors.New("kaniko should only be run inside of a container, run with the --force flag if you are sure you want to continue") } logrus.Warn("kaniko is being run outside of a container. This can have dangerous effects on your system") } if err := os.Chdir("/"); err != nil { - logrus.Error(err) - os.Exit(1) + return errors.Wrap(err, "error changing to root dir") } - image, err := executor.DoBuild(executor.KanikoBuildArgs{ - DockerfilePath: absouteDockerfilePath(), - SrcContext: srcContext, - SnapshotMode: snapshotMode, - Args: buildArgs, - SingleSnapshot: singleSnapshot, - Reproducible: reproducible, - Target: target, - }) + image, err := executor.DoBuild(opts) if err != nil { - logrus.Error(err) - os.Exit(1) - } - - if noPush { - logrus.Info("Skipping push to container registry due to --no-push flag") - os.Exit(0) + return errors.Wrap(err, "error building image") } + return executor.DoPush(image, opts) + }, +} - if err := executor.DoPush(image, destinations, tarPath, dockerInsecureSkipTLSVerify); err != nil { - logrus.Error(err) - os.Exit(1) - } +// addKanikoOptionsFlags configures opts +func addKanikoOptionsFlags(cmd *cobra.Command) { + RootCmd.PersistentFlags().StringVarP(&opts.DockerfilePath, "dockerfile", "f", "Dockerfile", "Path to the dockerfile to be built.") + RootCmd.PersistentFlags().StringVarP(&opts.SrcContext, "context", "c", "/workspace/", "Path to the dockerfile build context.") + RootCmd.PersistentFlags().StringVarP(&opts.Bucket, "bucket", "b", "", "Name of the GCS bucket from which to access build context as tarball.") + RootCmd.PersistentFlags().VarP(&opts.Destinations, "destination", "d", "Registry the final image should be pushed to. Set it repeatedly for multiple destinations.") + RootCmd.PersistentFlags().StringVarP(&opts.SnapshotMode, "snapshotMode", "", "full", "Change the file attributes inspected during snapshotting") + RootCmd.PersistentFlags().VarP(&opts.BuildArgs, "build-arg", "", "This flag allows you to pass in ARG values at build time. Set it repeatedly for multiple values.") + RootCmd.PersistentFlags().BoolVarP(&opts.DockerInsecureSkipTLSVerify, "insecure-skip-tls-verify", "", false, "Push to insecure registry ignoring TLS verify") + RootCmd.PersistentFlags().StringVarP(&opts.TarPath, "tarPath", "", "", "Path to save the image in as a tarball instead of pushing") + RootCmd.PersistentFlags().BoolVarP(&opts.SingleSnapshot, "single-snapshot", "", false, "Take a single snapshot at the end of the build.") + RootCmd.PersistentFlags().BoolVarP(&opts.Reproducible, "reproducible", "", false, "Strip timestamps out of the image to make it reproducible") + RootCmd.PersistentFlags().StringVarP(&opts.Target, "target", "", "", "Set the target build stage to build") + RootCmd.PersistentFlags().BoolVarP(&opts.NoPush, "no-push", "", false, "Do not push the image to the registry") +} - }, +// addHiddenFlags marks certain flags as hidden from the executor help text +func addHiddenFlags(cmd *cobra.Command) { + // This flag is added in a vendored directory, hide so that it doesn't come up via --help + RootCmd.PersistentFlags().MarkHidden("azure-container-registry-config") + // Hide this flag as we want to encourage people to use the --context flag instead + RootCmd.PersistentFlags().MarkHidden("bucket") } func checkContained() bool { @@ -124,56 +106,54 @@ func checkContained() bool { return err == nil } -func checkDockerfilePath() error { - if util.FilepathExists(dockerfilePath) { - if _, err := filepath.Abs(dockerfilePath); err != nil { - return err +// resolveDockerfilePath resolves the Dockerfile path to an absolute path +func resolveDockerfilePath() error { + if util.FilepathExists(opts.DockerfilePath) { + abs, err := filepath.Abs(opts.DockerfilePath) + if err != nil { + return errors.Wrap(err, "getting absolute path for dockerfile") } + opts.DockerfilePath = abs return nil } // Otherwise, check if the path relative to the build context exists - if util.FilepathExists(filepath.Join(srcContext, dockerfilePath)) { + if util.FilepathExists(filepath.Join(opts.SrcContext, opts.DockerfilePath)) { + abs, err := filepath.Abs(filepath.Join(opts.SrcContext, opts.DockerfilePath)) + if err != nil { + return errors.Wrap(err, "getting absolute path for src context/dockerfile path") + } + opts.DockerfilePath = abs return nil } - return errors.New("please provide a valid path to a Dockerfile within the build context") -} - -func absouteDockerfilePath() string { - if util.FilepathExists(dockerfilePath) { - // Ignore error since we already checked it in checkDockerfilePath() - abs, _ := filepath.Abs(dockerfilePath) - return abs - } - // Otherwise, return path relative to build context - return filepath.Join(srcContext, dockerfilePath) + return errors.New("please provide a valid path to a Dockerfile within the build context with --dockerfile") } // resolveSourceContext unpacks the source context if it is a tar in a bucket // it resets srcContext to be the path to the unpacked build context within the image func resolveSourceContext() error { - if srcContext == "" && bucket == "" { + if opts.SrcContext == "" && opts.Bucket == "" { return errors.New("please specify a path to the build context with the --context flag or a bucket with the --bucket flag") } - if srcContext != "" && !strings.Contains(srcContext, "://") { + if opts.SrcContext != "" && !strings.Contains(opts.SrcContext, "://") { return nil } - if bucket != "" { - if !strings.Contains(bucket, "://") { - srcContext = constants.GCSBuildContextPrefix + bucket + if opts.Bucket != "" { + if !strings.Contains(opts.Bucket, "://") { + opts.SrcContext = constants.GCSBuildContextPrefix + opts.Bucket } else { - srcContext = bucket + opts.SrcContext = opts.Bucket } } // if no prefix use Google Cloud Storage as default for backwards compability - contextExecutor, err := buildcontext.GetBuildContext(srcContext) + contextExecutor, err := buildcontext.GetBuildContext(opts.SrcContext) if err != nil { return err } - logrus.Debugf("Getting source context from %s", srcContext) - srcContext, err = contextExecutor.UnpackTarFromBuildContext() + logrus.Debugf("Getting source context from %s", opts.SrcContext) + opts.SrcContext, err = contextExecutor.UnpackTarFromBuildContext() if err != nil { return err } - logrus.Debugf("Build context located at %s", srcContext) + logrus.Debugf("Build context located at %s", opts.SrcContext) return nil } diff --git a/cmd/executor/main.go b/cmd/executor/main.go index 8fb6ae731f..68f60c4e98 100644 --- a/cmd/executor/main.go +++ b/cmd/executor/main.go @@ -17,7 +17,6 @@ limitations under the License. package main import ( - "fmt" "os" "github.com/GoogleContainerTools/kaniko/cmd/executor/cmd" @@ -25,7 +24,6 @@ import ( func main() { if err := cmd.RootCmd.Execute(); err != nil { - fmt.Println(err) os.Exit(1) } } diff --git a/pkg/executor/executor.go b/pkg/executor/build.go similarity index 70% rename from pkg/executor/executor.go rename to pkg/executor/build.go index ddda87e467..a3f958e62e 100644 --- a/pkg/executor/executor.go +++ b/pkg/executor/build.go @@ -18,21 +18,16 @@ package executor import ( "bytes" - "crypto/tls" "fmt" "io" "io/ioutil" - "net/http" "os" "path/filepath" "strconv" - "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/mutate" - "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" @@ -40,37 +35,26 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/commands" "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" - "github.com/GoogleContainerTools/kaniko/pkg/version" ) -// KanikoBuildArgs contains all the args required to build the image -type KanikoBuildArgs struct { - DockerfilePath string - SrcContext string - SnapshotMode string - Args []string - SingleSnapshot bool - Reproducible bool - Target string -} - -func DoBuild(k KanikoBuildArgs) (v1.Image, error) { +func DoBuild(opts *options.KanikoOptions) (v1.Image, error) { // Parse dockerfile and unpack base image to root - stages, err := dockerfile.Stages(k.DockerfilePath, k.Target) + stages, err := dockerfile.Stages(opts.DockerfilePath, opts.Target) if err != nil { return nil, err } - hasher, err := getHasher(k.SnapshotMode) + hasher, err := getHasher(opts.SnapshotMode) if err != nil { return nil, err } for index, stage := range stages { - finalStage := finalStage(index, k.Target, stages) + finalStage := finalStage(index, opts.Target, stages) // Unpack file system to root - sourceImage, err := util.RetrieveSourceImage(index, k.Args, stages) + sourceImage, err := util.RetrieveSourceImage(index, opts.BuildArgs, stages) if err != nil { return nil, err } @@ -90,10 +74,10 @@ func DoBuild(k KanikoBuildArgs) (v1.Image, error) { if err := resolveOnBuild(&stage, &imageConfig.Config); err != nil { return nil, err } - buildArgs := dockerfile.NewBuildArgs(k.Args) + buildArgs := dockerfile.NewBuildArgs(opts.BuildArgs) for index, cmd := range stage.Commands { finalCmd := index == len(stage.Commands)-1 - dockerCommand, err := commands.GetCommand(cmd, k.SrcContext) + dockerCommand, err := commands.GetCommand(cmd, opts.SrcContext) if err != nil { return nil, err } @@ -105,7 +89,7 @@ func DoBuild(k KanikoBuildArgs) (v1.Image, error) { } // Don't snapshot if it's not the final stage and not the final command // Also don't snapshot if it's the final stage, not the final command, and single snapshot is set - if (!finalStage && !finalCmd) || (finalStage && !finalCmd && k.SingleSnapshot) { + if (!finalStage && !finalCmd) || (finalStage && !finalCmd && opts.SingleSnapshot) { continue } // Now, we get the files to snapshot from this command and take the snapshot @@ -148,7 +132,7 @@ func DoBuild(k KanikoBuildArgs) (v1.Image, error) { return nil, err } if finalStage { - if k.Reproducible { + if opts.Reproducible { sourceImage, err = mutate.Canonical(sourceImage) if err != nil { return nil, err @@ -172,64 +156,6 @@ func DoBuild(k KanikoBuildArgs) (v1.Image, error) { return nil, err } -type withUserAgent struct { - t http.RoundTripper -} - -func (w *withUserAgent) RoundTrip(r *http.Request) (*http.Response, error) { - r.Header.Set("User-Agent", fmt.Sprintf("kaniko/%s", version.Version())) - return w.t.RoundTrip(r) -} - -func DoPush(image v1.Image, destinations []string, tarPath string, dockerInsecureSkipTLSVerify bool) error { - - // continue pushing unless an error occurs - for _, destination := range destinations { - // Push the image - destRef, err := name.NewTag(destination, name.WeakValidation) - if err != nil { - return err - } - - if dockerInsecureSkipTLSVerify { - newReg, err := name.NewInsecureRegistry(destRef.Repository.Registry.Name(), name.WeakValidation) - if err != nil { - return err - } - destRef.Repository.Registry = newReg - } - - if tarPath != "" { - return tarball.WriteToFile(tarPath, destRef, image, nil) - } - - k8sc, err := k8schain.NewNoClient() - if err != nil { - return err - } - kc := authn.NewMultiKeychain(authn.DefaultKeychain, k8sc) - pushAuth, err := kc.Resolve(destRef.Context().Registry) - if err != nil { - return err - } - - // Create a transport to set our user-agent. - tr := http.DefaultTransport - if dockerInsecureSkipTLSVerify { - tr.(*http.Transport).TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } - } - rt := &withUserAgent{t: tr} - - if err := remote.Write(destRef, image, pushAuth, rt, remote.WriteOptions{}); err != nil { - logrus.Error(fmt.Errorf("Failed to push to destination %s", destination)) - return err - } - } - return nil -} - func finalStage(index int, target string, stages []instructions.Stage) bool { if index == len(stages)-1 { return true diff --git a/pkg/executor/push.go b/pkg/executor/push.go new file mode 100644 index 0000000000..74bf33b394 --- /dev/null +++ b/pkg/executor/push.go @@ -0,0 +1,95 @@ +/* +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 ( + "crypto/tls" + "fmt" + "net/http" + + "github.com/GoogleContainerTools/kaniko/pkg/options" + "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/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +type withUserAgent struct { + t http.RoundTripper +} + +func (w *withUserAgent) RoundTrip(r *http.Request) (*http.Response, error) { + r.Header.Set("User-Agent", fmt.Sprintf("kaniko/%s", version.Version())) + return w.t.RoundTrip(r) +} + +// DoPush is responsible for pushing image to the destinations specified in opts +func DoPush(image v1.Image, opts *options.KanikoOptions) error { + if opts.NoPush { + logrus.Info("Skipping push to container registry due to --no-push flag") + return nil + } + // continue pushing unless an error occurs + for _, destination := range opts.Destinations { + // Push the image + destRef, err := name.NewTag(destination, name.WeakValidation) + if err != nil { + return errors.Wrap(err, "getting tag for destination") + } + + if opts.DockerInsecureSkipTLSVerify { + newReg, err := name.NewInsecureRegistry(destRef.Repository.Registry.Name(), name.WeakValidation) + if err != nil { + return errors.Wrap(err, "getting new insecure registry") + } + destRef.Repository.Registry = newReg + } + + if opts.TarPath != "" { + return tarball.WriteToFile(opts.TarPath, destRef, image, nil) + } + + k8sc, err := k8schain.NewNoClient() + if err != nil { + return errors.Wrap(err, "getting k8schain client") + } + kc := authn.NewMultiKeychain(authn.DefaultKeychain, k8sc) + pushAuth, err := kc.Resolve(destRef.Context().Registry) + if err != nil { + return errors.Wrap(err, "resolving pushAuth") + } + + // Create a transport to set our user-agent. + tr := http.DefaultTransport + if opts.DockerInsecureSkipTLSVerify { + tr.(*http.Transport).TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + rt := &withUserAgent{t: tr} + + if err := remote.Write(destRef, image, pushAuth, rt, remote.WriteOptions{}); err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to push to destination %s", destination)) + } + } + return nil +} diff --git a/cmd/executor/cmd/args.go b/pkg/options/args.go similarity index 98% rename from cmd/executor/cmd/args.go rename to pkg/options/args.go index 3e67d791a6..5f88157c98 100644 --- a/cmd/executor/cmd/args.go +++ b/pkg/options/args.go @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cmd +package options import ( - "github.com/sirupsen/logrus" "strings" + + "github.com/sirupsen/logrus" ) // This type is used to supported passing in multiple flags diff --git a/pkg/options/options.go b/pkg/options/options.go new file mode 100644 index 0000000000..9f9d593547 --- /dev/null +++ b/pkg/options/options.go @@ -0,0 +1,33 @@ +/* +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 options + +// KanikoOptions are options that are set by command line arguments +type KanikoOptions struct { + DockerfilePath string + Destinations multiArg + SrcContext string + SnapshotMode string + Bucket string + DockerInsecureSkipTLSVerify bool + BuildArgs multiArg + TarPath string + SingleSnapshot bool + Reproducible bool + Target string + NoPush bool +}