diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..b9bbfc0280 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Ignore GoLand (IntelliJ) files. +.idea/ + diff --git a/cmd/ko/commands.go b/cmd/ko/commands.go deleted file mode 100644 index 18239733c5..0000000000 --- a/cmd/ko/commands.go +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright 2018 Google LLC 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. - -package main - -import ( - "log" - "os" - "os/exec" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "k8s.io/kubernetes/pkg/kubectl/genericclioptions" -) - -// runCmd is suitable for use with cobra.Command's Run field. -type runCmd func(*cobra.Command, []string) - -// passthru returns a runCmd that simply passes our CLI arguments -// through to a binary named command. -func passthru(command string) runCmd { - return func(_ *cobra.Command, _ []string) { - // Start building a command line invocation by passing - // through our arguments to command's CLI. - cmd := exec.Command(command, os.Args[1:]...) - - // Pass through our environment - cmd.Env = os.Environ() - // Pass through our stdfoo - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - cmd.Stdin = os.Stdin - - // Run it. - if err := cmd.Run(); err != nil { - log.Fatalf("error executing %q command with args: %v; %v", command, os.Args[1:], err) - } - } -} - -// addKubeCommands augments our CLI surface with a passthru delete command, and an apply -// command that realizes the promise of ko, as outlined here: -// https://github.com/google/go-containerregistry/issues/80 -func addKubeCommands(topLevel *cobra.Command) { - topLevel.AddCommand(&cobra.Command{ - Use: "delete", - Short: `See "kubectl help delete" for detailed usage.`, - Run: passthru("kubectl"), - // We ignore unknown flags to avoid importing everything Go exposes - // from our commands. - FParseErrWhitelist: cobra.FParseErrWhitelist{ - UnknownFlags: true, - }, - }) - - topLevel.AddCommand(&cobra.Command{ - Use: "version", - Short: `Print ko version.`, - Run: func(cmd *cobra.Command, args []string) { - version() - }, - }) - - koApplyFlags := []string{} - lo := &LocalOptions{} - bo := &BinaryOptions{} - no := &NameOptions{} - fo := &FilenameOptions{} - ta := &TagsOptions{} - apply := &cobra.Command{ - Use: "apply -f FILENAME", - Short: "Apply the input files with image references resolved to built/pushed image digests.", - Long: `This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and then feeds the resulting yaml into "kubectl apply".`, - Example: ` - # Build and publish import path references to a Docker - # Registry as: - # ${KO_DOCKER_REPO}/- - # Then, feed the resulting yaml into "kubectl apply". - # When KO_DOCKER_REPO is ko.local, it is the same as if - # --local was passed. - ko apply -f config/ - - # Build and publish import path references to a Docker - # Registry preserving import path names as: - # ${KO_DOCKER_REPO}/ - # Then, feed the resulting yaml into "kubectl apply". - ko apply --preserve-import-paths -f config/ - - # Build and publish import path references to a Docker - # daemon as: - # ko.local/ - # Then, feed the resulting yaml into "kubectl apply". - ko apply --local -f config/ - - # Apply from stdin: - cat config.yaml | ko apply -f -`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - // Create a set of ko-specific flags to ignore when passing through - // kubectl global flags. - ignoreSet := make(map[string]struct{}) - for _, s := range koApplyFlags { - ignoreSet[s] = struct{}{} - } - - // Filter out ko flags from what we will pass through to kubectl. - kubectlFlags := []string{} - cmd.Flags().Visit(func(flag *pflag.Flag) { - if _, ok := ignoreSet[flag.Name]; !ok { - kubectlFlags = append(kubectlFlags, "--"+flag.Name, flag.Value.String()) - } - }) - - // Issue a "kubectl apply" command reading from stdin, - // to which we will pipe the resolved files. - argv := []string{"apply", "-f", "-"} - argv = append(argv, kubectlFlags...) - kubectlCmd := exec.Command("kubectl", argv...) - - // Pass through our environment - kubectlCmd.Env = os.Environ() - // Pass through our std{out,err} and make our resolved buffer stdin. - kubectlCmd.Stderr = os.Stderr - kubectlCmd.Stdout = os.Stdout - - // Wire up kubectl stdin to resolveFilesToWriter. - stdin, err := kubectlCmd.StdinPipe() - if err != nil { - log.Fatalf("error piping to 'kubectl apply': %v", err) - } - - go func() { - // kubectl buffers data before starting to apply it, which - // can lead to resources being created more slowly than desired. - // In the case of --watch, it can lead to resources not being - // applied at all until enough iteration has occurred. To work - // around this, we prime the stream with a bunch of empty objects - // which kubectl will discard. - // See https://github.com/google/go-containerregistry/pull/348 - for i := 0; i < 1000; i++ { - stdin.Write([]byte("---\n")) - } - // Once primed kick things off. - resolveFilesToWriter(fo, no, lo, ta, stdin) - }() - - // Run it. - if err := kubectlCmd.Run(); err != nil { - log.Fatalf("error executing 'kubectl apply': %v", err) - } - }, - } - addLocalArg(apply, lo) - addNamingArgs(apply, no) - addFileArg(apply, fo) - addTagsArg(apply, ta) - - // Collect the ko-specific apply flags before registering the kubectl global - // flags so that we can ignore them when passing kubectl global flags through - // to kubectl. - apply.Flags().VisitAll(func(flag *pflag.Flag) { - koApplyFlags = append(koApplyFlags, flag.Name) - }) - - // Register the kubectl global flags. - kubeConfigFlags := genericclioptions.NewConfigFlags() - kubeConfigFlags.AddFlags(apply.Flags()) - - topLevel.AddCommand(apply) - - resolve := &cobra.Command{ - Use: "resolve -f FILENAME", - Short: "Print the input files with image references resolved to built/pushed image digests.", - Long: `This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and prints the resulting yaml.`, - Example: ` - # Build and publish import path references to a Docker - # Registry as: - # ${KO_DOCKER_REPO}/- - # When KO_DOCKER_REPO is ko.local, it is the same as if - # --local and --preserve-import-paths were passed. - ko resolve -f config/ - - # Build and publish import path references to a Docker - # Registry preserving import path names as: - # ${KO_DOCKER_REPO}/ - # When KO_DOCKER_REPO is ko.local, it is the same as if - # --local was passed. - ko resolve --preserve-import-paths -f config/ - - # Build and publish import path references to a Docker - # daemon as: - # ko.local/ - # This always preserves import paths. - ko resolve --local -f config/`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - resolveFilesToWriter(fo, no, lo, ta, os.Stdout) - }, - } - addLocalArg(resolve, lo) - addNamingArgs(resolve, no) - addFileArg(resolve, fo) - addTagsArg(resolve, ta) - topLevel.AddCommand(resolve) - - publish := &cobra.Command{ - Use: "publish IMPORTPATH...", - Short: "Build and publish container images from the given importpaths.", - Long: `This sub-command builds the provided import paths into Go binaries, containerizes them, and publishes them.`, - Example: ` - # Build and publish import path references to a Docker - # Registry as: - # ${KO_DOCKER_REPO}/- - # When KO_DOCKER_REPO is ko.local, it is the same as if - # --local and --preserve-import-paths were passed. - ko publish github.com/foo/bar/cmd/baz github.com/foo/bar/cmd/blah - - # Build and publish a relative import path as: - # ${KO_DOCKER_REPO}/- - # When KO_DOCKER_REPO is ko.local, it is the same as if - # --local and --preserve-import-paths were passed. - ko publish ./cmd/blah - - # Build and publish a relative import path as: - # ${KO_DOCKER_REPO}/ - # When KO_DOCKER_REPO is ko.local, it is the same as if - # --local was passed. - ko publish --preserve-import-paths ./cmd/blah - - # Build and publish import path references to a Docker - # daemon as: - # ko.local/ - # This always preserves import paths. - ko publish --local github.com/foo/bar/cmd/baz github.com/foo/bar/cmd/blah`, - Args: cobra.MinimumNArgs(1), - Run: func(_ *cobra.Command, args []string) { - publishImages(args, no, lo, ta) - }, - } - addLocalArg(publish, lo) - addNamingArgs(publish, no) - addTagsArg(publish, ta) - topLevel.AddCommand(publish) - - run := &cobra.Command{ - Use: "run NAME --image=IMPORTPATH", - Short: "A variant of `kubectl run` that containerizes IMPORTPATH first.", - Long: `This sub-command combines "ko publish" and "kubectl run" to support containerizing and running Go binaries on Kubernetes in a single command.`, - Example: ` - # Publish the --image and run it on Kubernetes as: - # ${KO_DOCKER_REPO}/- - # When KO_DOCKER_REPO is ko.local, it is the same as if - # --local and --preserve-import-paths were passed. - ko run foo --image=github.com/foo/bar/cmd/baz - - # This supports relative import paths as well. - ko run foo --image=./cmd/baz`, - Run: func(cmd *cobra.Command, args []string) { - imgs := publishImages([]string{bo.Path}, no, lo, ta) - - // There's only one, but this is the simple way to access the - // reference since the import path may have been qualified. - for k, v := range imgs { - log.Printf("Running %q", k) - // Issue a "kubectl run" command with our same arguments, - // but supply a second --image to override the one we intercepted. - argv := append(os.Args[1:], "--image", v.String()) - kubectlCmd := exec.Command("kubectl", argv...) - - // Pass through our environment - kubectlCmd.Env = os.Environ() - // Pass through our std* - kubectlCmd.Stderr = os.Stderr - kubectlCmd.Stdout = os.Stdout - kubectlCmd.Stdin = os.Stdin - - // Run it. - if err := kubectlCmd.Run(); err != nil { - log.Fatalf("error executing \"kubectl run\": %v", err) - } - } - }, - // We ignore unknown flags to avoid importing everything Go exposes - // from our commands. - FParseErrWhitelist: cobra.FParseErrWhitelist{ - UnknownFlags: true, - }, - } - addLocalArg(run, lo) - addNamingArgs(run, no) - addImageArg(run, bo) - addTagsArg(run, ta) - - topLevel.AddCommand(run) -} diff --git a/cmd/ko/main.go b/cmd/ko/main.go index 6c2251c612..e04c454818 100644 --- a/cmd/ko/main.go +++ b/cmd/ko/main.go @@ -15,6 +15,7 @@ package main import ( + "github.com/google/ko/pkg/commands" "log" "github.com/spf13/cobra" @@ -29,7 +30,7 @@ func main() { cmd.Help() }, } - addKubeCommands(cmds) + commands.AddKubeCommands(cmds) if err := cmds.Execute(); err != nil { log.Fatalf("error during command execution: %v", err) diff --git a/hack/release.sh b/hack/release.sh index bcf350e8ea..ccf1a3f591 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -25,6 +25,6 @@ VERSION=$1 KO_ROOT="$(cd "$(dirname "$0")" && pwd)/.." go get github.com/ahmetb/govvv -govvv build -o $KO_ROOT/build/ko $KO_ROOT/cmd/ko -version $VERSION +govvv build -o $KO_ROOT/build/ko $KO_ROOT/cmd/ko -pkg github.com/google/ko/pkg/commands -version $VERSION git tag $VERSION git push origin $VERSION diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 7300f5e22c..cf9c43829e 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -38,12 +38,13 @@ const ( // GetBase takes an importpath and returns a base v1.Image. type GetBase func(string) (v1.Image, error) -type builder func(string) (string, error) +type builder func(string, bool) (string, error) type gobuild struct { getBase GetBase creationTime v1.Time build builder + disableOptimizations bool } // Option is a functional option for NewGo. @@ -53,6 +54,7 @@ type gobuildOpener struct { getBase GetBase creationTime v1.Time build builder + disableOptimizations bool } func (gbo *gobuildOpener) Open() (Interface, error) { @@ -63,6 +65,7 @@ func (gbo *gobuildOpener) Open() (Interface, error) { getBase: gbo.getBase, creationTime: gbo.creationTime, build: gbo.build, + disableOptimizations: gbo.disableOptimizations, }, nil } @@ -94,14 +97,22 @@ func (*gobuild) IsSupportedReference(s string) bool { return p.IsCommand() } -func build(ip string) (string, error) { +func build(ip string, disableOptimizations bool) (string, error) { tmpDir, err := ioutil.TempDir("", "ko") if err != nil { return "", err } file := filepath.Join(tmpDir, "out") - cmd := exec.Command("go", "build", "-o", file, ip) + args := make([]string, 0, 6) + args = append(args, "build") + if disableOptimizations { + // Disable optimizations (-N) and inlining (-l). + args = append(args, "-gcflags", "all=-N -l") + } + args = append(args, "-o", file) + args = append(args, ip) + cmd := exec.Command("go", args...) // Last one wins // TODO(mattmoor): GOARCH=amd64 @@ -273,7 +284,7 @@ func tarKoData(importpath string) (*bytes.Buffer, error) { // Build implements build.Interface func (gb *gobuild) Build(s string) (v1.Image, error) { // Do the build into a temporary file. - file, err := gb.build(s) + file, err := gb.build(s, gb.disableOptimizations) if err != nil { return nil, err } diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index e7e0e83ad2..11d7f3f370 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -62,7 +62,7 @@ func TestGoBuildIsSupportedRef(t *testing.T) { } // A helper method we use to substitute for the default "build" method. -func writeTempFile(s string) (string, error) { +func writeTempFile(s string, _ bool) (string, error) { tmpDir, err := ioutil.TempDir("", "ko") if err != nil { return "", err diff --git a/pkg/build/options.go b/pkg/build/options.go index df6eb40a46..78e001a4ce 100644 --- a/pkg/build/options.go +++ b/pkg/build/options.go @@ -36,6 +36,15 @@ func WithCreationTime(t v1.Time) Option { } } +// WithDisabledOptimizations is a functional option for disabling optimizations +// when compiling. +func WithDisabledOptimizations() Option { + return func(gbo *gobuildOpener) error { + gbo.disableOptimizations = true + return nil + } +} + // withBuilder is a functional option for overriding the way go binaries // are built. This is exposed for testing. func withBuilder(b builder) Option { diff --git a/pkg/commands/apply.go b/pkg/commands/apply.go new file mode 100644 index 0000000000..33dde60453 --- /dev/null +++ b/pkg/commands/apply.go @@ -0,0 +1,145 @@ +// Copyright 2018 Google LLC 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. + +package commands + +import ( + "log" + "os" + "os/exec" + + "github.com/google/ko/pkg/commands/options" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "k8s.io/kubernetes/pkg/kubectl/genericclioptions" +) + +// addApply augments our CLI surface with apply. +func addApply(topLevel *cobra.Command) { + koApplyFlags := []string{} + lo := &options.LocalOptions{} + no := &options.NameOptions{} + fo := &options.FilenameOptions{} + ta := &options.TagsOptions{} + do := &options.DebugOptions{} + apply := &cobra.Command{ + Use: "apply -f FILENAME", + Short: "Apply the input files with image references resolved to built/pushed image digests.", + Long: `This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and then feeds the resulting yaml into "kubectl apply".`, + Example: ` + # Build and publish import path references to a Docker + # Registry as: + # ${KO_DOCKER_REPO}/- + # Then, feed the resulting yaml into "kubectl apply". + # When KO_DOCKER_REPO is ko.local, it is the same as if + # --local was passed. + ko apply -f config/ + + # Build and publish import path references to a Docker + # Registry preserving import path names as: + # ${KO_DOCKER_REPO}/ + # Then, feed the resulting yaml into "kubectl apply". + ko apply --preserve-import-paths -f config/ + + # Build and publish import path references to a Docker + # daemon as: + # ko.local/ + # Then, feed the resulting yaml into "kubectl apply". + ko apply --local -f config/ + + # Apply from stdin: + cat config.yaml | ko apply -f -`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + builder, err := makeBuilder(do) + if err != nil { + log.Fatalf("error creating builder: %v", err) + } + publisher, err := makePublisher(no, lo, ta) + if err != nil { + log.Fatalf("error creating publisher: %v", err) + } + // Create a set of ko-specific flags to ignore when passing through + // kubectl global flags. + ignoreSet := make(map[string]struct{}) + for _, s := range koApplyFlags { + ignoreSet[s] = struct{}{} + } + + // Filter out ko flags from what we will pass through to kubectl. + kubectlFlags := []string{} + cmd.Flags().Visit(func(flag *pflag.Flag) { + if _, ok := ignoreSet[flag.Name]; !ok { + kubectlFlags = append(kubectlFlags, "--"+flag.Name, flag.Value.String()) + } + }) + + // Issue a "kubectl apply" command reading from stdin, + // to which we will pipe the resolved files. + argv := []string{"apply", "-f", "-"} + argv = append(argv, kubectlFlags...) + kubectlCmd := exec.Command("kubectl", argv...) + + // Pass through our environment + kubectlCmd.Env = os.Environ() + // Pass through our std{out,err} and make our resolved buffer stdin. + kubectlCmd.Stderr = os.Stderr + kubectlCmd.Stdout = os.Stdout + + // Wire up kubectl stdin to resolveFilesToWriter. + stdin, err := kubectlCmd.StdinPipe() + if err != nil { + log.Fatalf("error piping to 'kubectl apply': %v", err) + } + + go func() { + // kubectl buffers data before starting to apply it, which + // can lead to resources being created more slowly than desired. + // In the case of --watch, it can lead to resources not being + // applied at all until enough iteration has occurred. To work + // around this, we prime the stream with a bunch of empty objects + // which kubectl will discard. + // See https://github.com/google/go-containerregistry/pull/348 + for i := 0; i < 1000; i++ { + stdin.Write([]byte("---\n")) + } + // Once primed kick things off. + resolveFilesToWriter(builder, publisher, fo, stdin) + }() + + // Run it. + if err := kubectlCmd.Run(); err != nil { + log.Fatalf("error executing 'kubectl apply': %v", err) + } + }, + } + options.AddLocalArg(apply, lo) + options.AddNamingArgs(apply, no) + options.AddFileArg(apply, fo) + options.AddTagsArg(apply, ta) + options.AddDebugArg(apply, do) + + // Collect the ko-specific apply flags before registering the kubectl global + // flags so that we can ignore them when passing kubectl global flags through + // to kubectl. + apply.Flags().VisitAll(func(flag *pflag.Flag) { + koApplyFlags = append(koApplyFlags, flag.Name) + }) + + // Register the kubectl global flags. + kubeConfigFlags := genericclioptions.NewConfigFlags() + kubeConfigFlags.AddFlags(apply.Flags()) + + topLevel.AddCommand(apply) +} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go new file mode 100644 index 0000000000..4c3a4a1650 --- /dev/null +++ b/pkg/commands/commands.go @@ -0,0 +1,32 @@ +// Copyright 2018 Google LLC 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. + +package commands + +import ( + "github.com/spf13/cobra" +) + +// AddKubeCommands augments our CLI surface with a passthru delete command, and an apply +// command that realizes the promise of ko, as outlined here: +// https://github.com/google/go-containerregistry/issues/80 +func AddKubeCommands(topLevel *cobra.Command) { + addDelete(topLevel) + addVersion(topLevel) + addCreate(topLevel) + addApply(topLevel) + addResolve(topLevel) + addPublish(topLevel) + addRun(topLevel) +} diff --git a/cmd/ko/config.go b/pkg/commands/config.go similarity index 95% rename from cmd/ko/config.go rename to pkg/commands/config.go index accc646bd5..941cdd3327 100644 --- a/cmd/ko/config.go +++ b/pkg/commands/config.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package commands import ( "fmt" @@ -21,12 +21,11 @@ import ( "strconv" "time" - "github.com/spf13/viper" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/spf13/viper" ) var ( @@ -51,7 +50,7 @@ func getCreationTime() (*v1.Time, error) { seconds, err := strconv.ParseInt(epoch, 10, 64) if err != nil { - return nil, fmt.Errorf("the environment variable SOURCE_DATE_EPOCH is invalid. It's must be a number of seconds since January 1st 1970, 00:00 UTC, got %v", err) + return nil, fmt.Errorf("the environment variable SOURCE_DATE_EPOCH should be the number of seconds since January 1st 1970, 00:00 UTC, got: %v", err) } return &v1.Time{time.Unix(seconds, 0)}, nil } diff --git a/pkg/commands/create.go b/pkg/commands/create.go new file mode 100644 index 0000000000..e012709d81 --- /dev/null +++ b/pkg/commands/create.go @@ -0,0 +1,145 @@ +// Copyright 2018 Google LLC 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. + +package commands + +import ( + "log" + "os" + "os/exec" + + "github.com/google/ko/pkg/commands/options" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "k8s.io/kubernetes/pkg/kubectl/genericclioptions" +) + +// addCreate augments our CLI surface with apply. +func addCreate(topLevel *cobra.Command) { + koCreateFlags := []string{} + lo := &options.LocalOptions{} + no := &options.NameOptions{} + fo := &options.FilenameOptions{} + ta := &options.TagsOptions{} + do := &options.DebugOptions{} + create := &cobra.Command{ + Use: "create -f FILENAME", + Short: "Create the input files with image references resolved to built/pushed image digests.", + Long: `This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and then feeds the resulting yaml into "kubectl create".`, + Example: ` + # Build and publish import path references to a Docker + # Registry as: + # ${KO_DOCKER_REPO}/- + # Then, feed the resulting yaml into "kubectl create". + # When KO_DOCKER_REPO is ko.local, it is the same as if + # --local was passed. + ko create -f config/ + + # Build and publish import path references to a Docker + # Registry preserving import path names as: + # ${KO_DOCKER_REPO}/ + # Then, feed the resulting yaml into "kubectl create". + ko create --preserve-import-paths -f config/ + + # Build and publish import path references to a Docker + # daemon as: + # ko.local/ + # Then, feed the resulting yaml into "kubectl create". + ko create --local -f config/ + + # Create from stdin: + cat config.yaml | ko create -f -`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + builder, err := makeBuilder(do) + if err != nil { + log.Fatalf("error creating builder: %v", err) + } + publisher, err := makePublisher(no, lo, ta) + if err != nil { + log.Fatalf("error creating publisher: %v", err) + } + // Create a set of ko-specific flags to ignore when passing through + // kubectl global flags. + ignoreSet := make(map[string]struct{}) + for _, s := range koCreateFlags { + ignoreSet[s] = struct{}{} + } + + // Filter out ko flags from what we will pass through to kubectl. + kubectlFlags := []string{} + cmd.Flags().Visit(func(flag *pflag.Flag) { + if _, ok := ignoreSet[flag.Name]; !ok { + kubectlFlags = append(kubectlFlags, "--"+flag.Name, flag.Value.String()) + } + }) + + // Issue a "kubectl create" command reading from stdin, + // to which we will pipe the resolved files. + argv := []string{"create", "-f", "-"} + argv = append(argv, kubectlFlags...) + kubectlCmd := exec.Command("kubectl", argv...) + + // Pass through our environment + kubectlCmd.Env = os.Environ() + // Pass through our std{out,err} and make our resolved buffer stdin. + kubectlCmd.Stderr = os.Stderr + kubectlCmd.Stdout = os.Stdout + + // Wire up kubectl stdin to resolveFilesToWriter. + stdin, err := kubectlCmd.StdinPipe() + if err != nil { + log.Fatalf("error piping to 'kubectl create': %v", err) + } + + go func() { + // kubectl buffers data before starting to create it, which + // can lead to resources being created more slowly than desired. + // In the case of --watch, it can lead to resources not being + // applied at all until enough iteration has occurred. To work + // around this, we prime the stream with a bunch of empty objects + // which kubectl will discard. + // See https://github.com/google/go-containerregistry/pull/348 + for i := 0; i < 1000; i++ { + stdin.Write([]byte("---\n")) + } + // Once primed kick things off. + resolveFilesToWriter(builder, publisher, fo, stdin) + }() + + // Run it. + if err := kubectlCmd.Run(); err != nil { + log.Fatalf("error executing 'kubectl create': %v", err) + } + }, + } + options.AddLocalArg(create, lo) + options.AddNamingArgs(create, no) + options.AddFileArg(create, fo) + options.AddTagsArg(create, ta) + options.AddDebugArg(create, do) + + // Collect the ko-specific apply flags before registering the kubectl global + // flags so that we can ignore them when passing kubectl global flags through + // to kubectl. + create.Flags().VisitAll(func(flag *pflag.Flag) { + koCreateFlags = append(koCreateFlags, flag.Name) + }) + + // Register the kubectl global flags. + kubeConfigFlags := genericclioptions.NewConfigFlags() + kubeConfigFlags.AddFlags(create.Flags()) + + topLevel.AddCommand(create) +} diff --git a/pkg/commands/delete.go b/pkg/commands/delete.go new file mode 100644 index 0000000000..7595c68b2c --- /dev/null +++ b/pkg/commands/delete.go @@ -0,0 +1,61 @@ +// Copyright 2018 Google LLC 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. + +package commands + +import ( + "github.com/spf13/cobra" + "log" + "os" + "os/exec" +) + +// runCmd is suitable for use with cobra.Command's Run field. +type runCmd func(*cobra.Command, []string) + +// passthru returns a runCmd that simply passes our CLI arguments +// through to a binary named command. +func passthru(command string) runCmd { + return func(_ *cobra.Command, _ []string) { + // Start building a command line invocation by passing + // through our arguments to command's CLI. + cmd := exec.Command(command, os.Args[1:]...) + + // Pass through our environment + cmd.Env = os.Environ() + // Pass through our stdfoo + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + + // Run it. + if err := cmd.Run(); err != nil { + log.Fatalf("error executing %q command with args: %v; %v", command, os.Args[1:], err) + } + } +} + +// addDelete augments our CLI surface with publish. +func addDelete(topLevel *cobra.Command) { + topLevel.AddCommand(&cobra.Command{ + Use: "delete", + Short: `See "kubectl help delete" for detailed usage.`, + Run: passthru("kubectl"), + // We ignore unknown flags to avoid importing everything Go exposes + // from our commands. + FParseErrWhitelist: cobra.FParseErrWhitelist{ + UnknownFlags: true, + }, + }) +} diff --git a/cmd/ko/binary.go b/pkg/commands/options/binary.go similarity index 92% rename from cmd/ko/binary.go rename to pkg/commands/options/binary.go index 100b5f1849..efe164470a 100644 --- a/cmd/ko/binary.go +++ b/pkg/commands/options/binary.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package options import ( "github.com/spf13/cobra" @@ -24,7 +24,7 @@ type BinaryOptions struct { Path string } -func addImageArg(cmd *cobra.Command, lo *BinaryOptions) { +func AddImageArg(cmd *cobra.Command, lo *BinaryOptions) { cmd.Flags().StringVarP(&lo.Path, "image", "i", lo.Path, "The import path of the binary to publish.") } diff --git a/pkg/commands/options/debug.go b/pkg/commands/options/debug.go new file mode 100644 index 0000000000..878254f9ca --- /dev/null +++ b/pkg/commands/options/debug.go @@ -0,0 +1,29 @@ +// Copyright 2019 Google LLC 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. + +package options + +import ( + "github.com/spf13/cobra" +) + +// DebugOptions holds options to improve debugging containers. +type DebugOptions struct { + DisableOptimizations bool +} + +func AddDebugArg(cmd *cobra.Command, do *DebugOptions) { + cmd.Flags().BoolVar(&do.DisableOptimizations, "disable-optimizations", do.DisableOptimizations, + "Disable optimizations when building Go code. Useful when you want to interactively debug the created container.") +} diff --git a/cmd/ko/filestuff.go b/pkg/commands/options/filestuff.go similarity index 96% rename from cmd/ko/filestuff.go rename to pkg/commands/options/filestuff.go index e492ced47f..4a5c499619 100644 --- a/cmd/ko/filestuff.go +++ b/pkg/commands/options/filestuff.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package options import ( "log" @@ -30,7 +30,7 @@ type FilenameOptions struct { Watch bool } -func addFileArg(cmd *cobra.Command, fo *FilenameOptions) { +func AddFileArg(cmd *cobra.Command, fo *FilenameOptions) { // From pkg/kubectl cmd.Flags().StringSliceVarP(&fo.Filenames, "filename", "f", fo.Filenames, "Filename, directory, or URL to files to use to create the resource") @@ -41,7 +41,7 @@ func addFileArg(cmd *cobra.Command, fo *FilenameOptions) { } // Based heavily on pkg/kubectl -func enumerateFiles(fo *FilenameOptions) chan string { +func EnumerateFiles(fo *FilenameOptions) chan string { files := make(chan string) go func() { // When we're done enumerating files, close the channel diff --git a/cmd/ko/flatname.go b/pkg/commands/options/flatname.go similarity index 93% rename from cmd/ko/flatname.go rename to pkg/commands/options/flatname.go index 1fed878fb2..98574750d9 100644 --- a/cmd/ko/flatname.go +++ b/pkg/commands/options/flatname.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package options import ( "crypto/md5" @@ -31,7 +31,7 @@ type NameOptions struct { BaseImportPaths bool } -func addNamingArgs(cmd *cobra.Command, no *NameOptions) { +func AddNamingArgs(cmd *cobra.Command, no *NameOptions) { cmd.Flags().BoolVarP(&no.PreserveImportPaths, "preserve-import-paths", "P", no.PreserveImportPaths, "Whether to preserve the full import path after KO_DOCKER_REPO.") cmd.Flags().BoolVarP(&no.BaseImportPaths, "base-import-paths", "B", no.BaseImportPaths, @@ -52,7 +52,7 @@ func baseImportPaths(importpath string) string { return filepath.Base(importpath) } -func makeNamer(no *NameOptions) publish.Namer { +func MakeNamer(no *NameOptions) publish.Namer { if no.PreserveImportPaths { return preserveImportPath } else if no.BaseImportPaths { diff --git a/cmd/ko/local.go b/pkg/commands/options/local.go similarity index 92% rename from cmd/ko/local.go rename to pkg/commands/options/local.go index 1b99e255ab..bfe468e213 100644 --- a/cmd/ko/local.go +++ b/pkg/commands/options/local.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package options import ( "github.com/spf13/cobra" @@ -24,7 +24,7 @@ type LocalOptions struct { Local bool } -func addLocalArg(cmd *cobra.Command, lo *LocalOptions) { +func AddLocalArg(cmd *cobra.Command, lo *LocalOptions) { cmd.Flags().BoolVarP(&lo.Local, "local", "L", lo.Local, "Whether to publish images to a local docker daemon vs. a registry.") } diff --git a/cmd/ko/tags.go b/pkg/commands/options/tags.go similarity index 92% rename from cmd/ko/tags.go rename to pkg/commands/options/tags.go index 1111516f5d..22c1d98081 100644 --- a/cmd/ko/tags.go +++ b/pkg/commands/options/tags.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package options import ( "github.com/spf13/cobra" @@ -23,7 +23,7 @@ type TagsOptions struct { Tags []string } -func addTagsArg(cmd *cobra.Command, ta *TagsOptions) { +func AddTagsArg(cmd *cobra.Command, ta *TagsOptions) { cmd.Flags().StringSliceVarP(&ta.Tags, "tags", "t", []string{"latest"}, "Which tags to use for the produced image instead of the default 'latest' tag.") } diff --git a/pkg/commands/publish.go b/pkg/commands/publish.go new file mode 100644 index 0000000000..197d9aa1d1 --- /dev/null +++ b/pkg/commands/publish.go @@ -0,0 +1,80 @@ +// Copyright 2018 Google LLC 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. + +package commands + +import ( + "log" + + "github.com/google/ko/pkg/commands/options" + "github.com/spf13/cobra" +) + +// addPublish augments our CLI surface with publish. +func addPublish(topLevel *cobra.Command) { + lo := &options.LocalOptions{} + no := &options.NameOptions{} + ta := &options.TagsOptions{} + do := &options.DebugOptions{} + + publish := &cobra.Command{ + Use: "publish IMPORTPATH...", + Short: "Build and publish container images from the given importpaths.", + Long: `This sub-command builds the provided import paths into Go binaries, containerizes them, and publishes them.`, + Example: ` + # Build and publish import path references to a Docker + # Registry as: + # ${KO_DOCKER_REPO}/- + # When KO_DOCKER_REPO is ko.local, it is the same as if + # --local and --preserve-import-paths were passed. + ko publish github.com/foo/bar/cmd/baz github.com/foo/bar/cmd/blah + + # Build and publish a relative import path as: + # ${KO_DOCKER_REPO}/- + # When KO_DOCKER_REPO is ko.local, it is the same as if + # --local and --preserve-import-paths were passed. + ko publish ./cmd/blah + + # Build and publish a relative import path as: + # ${KO_DOCKER_REPO}/ + # When KO_DOCKER_REPO is ko.local, it is the same as if + # --local was passed. + ko publish --preserve-import-paths ./cmd/blah + + # Build and publish import path references to a Docker + # daemon as: + # ko.local/ + # This always preserves import paths. + ko publish --local github.com/foo/bar/cmd/baz github.com/foo/bar/cmd/blah`, + Args: cobra.MinimumNArgs(1), + Run: func(_ *cobra.Command, args []string) { + builder, err := makeBuilder(do) + if err != nil { + log.Fatalf("error creating builder: %v", err) + } + publisher, err := makePublisher(no, lo, ta) + if err != nil { + log.Fatalf("error creating publisher: %v", err) + } + if _, err := publishImages(args, publisher, builder); err != nil { + log.Fatalf("failed to publish images: %v", err) + } + }, + } + options.AddLocalArg(publish, lo) + options.AddNamingArgs(publish, no) + options.AddTagsArg(publish, ta) + options.AddDebugArg(publish, do) + topLevel.AddCommand(publish) +} diff --git a/cmd/ko/publish.go b/pkg/commands/publisher.go similarity index 58% rename from cmd/ko/publish.go rename to pkg/commands/publisher.go index 15158922d5..d98951c5e6 100644 --- a/cmd/ko/publish.go +++ b/pkg/commands/publisher.go @@ -12,17 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package commands import ( "fmt" gb "go/build" - "log" "os" "path/filepath" "strings" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/publish" @@ -38,15 +36,7 @@ func qualifyLocalImport(importpath, gopathsrc, pwd string) (string, error) { return filepath.Join(strings.TrimPrefix(pwd, gopathsrc+string(filepath.Separator)), importpath), nil } -func publishImages(importpaths []string, no *NameOptions, lo *LocalOptions, ta *TagsOptions) map[string]name.Reference { - opt, err := gobuildOptions() - if err != nil { - log.Fatalf("error setting up builder options: %v", err) - } - b, err := build.NewGo(opt...) - if err != nil { - log.Fatalf("error creating go builder: %v", err) - } +func publishImages(importpaths []string, pub publish.Interface, b build.Interface) (map[string]name.Reference, error) { imgs := make(map[string]name.Reference) for _, importpath := range importpaths { if gb.IsLocalImport(importpath) { @@ -55,44 +45,27 @@ func publishImages(importpaths []string, no *NameOptions, lo *LocalOptions, ta * gopathsrc := filepath.Join(gb.Default.GOPATH, "src") pwd, err := os.Getwd() if err != nil { - log.Fatalf("error getting current working directory: %v", err) + return nil, fmt.Errorf("error getting current working directory: %v", err) } importpath, err = qualifyLocalImport(importpath, gopathsrc, pwd) if err != nil { - log.Fatal(err) + return nil, err } } if !b.IsSupportedReference(importpath) { - log.Fatalf("importpath %q is not supported", importpath) + return nil, fmt.Errorf("importpath %q is not supported", importpath) } img, err := b.Build(importpath) if err != nil { - log.Fatalf("error building %q: %v", importpath, err) - } - var pub publish.Interface - repoName := os.Getenv("KO_DOCKER_REPO") - - namer := makeNamer(no) - - if lo.Local || repoName == publish.LocalDomain { - pub = publish.NewDaemon(namer, ta.Tags) - } else { - if _, err := name.NewRepository(repoName, name.WeakValidation); err != nil { - log.Fatalf("the environment variable KO_DOCKER_REPO must be set to a valid docker repository, got %v", err) - } - opts := []publish.Option{publish.WithAuthFromKeychain(authn.DefaultKeychain), publish.WithNamer(namer), publish.WithTags(ta.Tags)} - pub, err = publish.NewDefault(repoName, opts...) - if err != nil { - log.Fatalf("error setting up default image publisher: %v", err) - } + return nil, fmt.Errorf("error building %q: %v", importpath, err) } ref, err := pub.Publish(img, importpath) if err != nil { - log.Fatalf("error publishing %s: %v", importpath, err) + return nil, fmt.Errorf("error publishing %s: %v", importpath, err) } imgs[importpath] = ref } - return imgs + return imgs, nil } diff --git a/cmd/ko/publish_test.go b/pkg/commands/publisher_test.go similarity index 98% rename from cmd/ko/publish_test.go rename to pkg/commands/publisher_test.go index 0e04e61d0a..404e4e21b6 100644 --- a/cmd/ko/publish_test.go +++ b/pkg/commands/publisher_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package commands import "testing" diff --git a/pkg/commands/resolve.go b/pkg/commands/resolve.go new file mode 100644 index 0000000000..5647086a60 --- /dev/null +++ b/pkg/commands/resolve.go @@ -0,0 +1,77 @@ +// Copyright 2018 Google LLC 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. + +package commands + +import ( + "log" + "os" + + "github.com/google/ko/pkg/commands/options" + "github.com/spf13/cobra" +) + +// addResolve augments our CLI surface with resolve. +func addResolve(topLevel *cobra.Command) { + + lo := &options.LocalOptions{} + no := &options.NameOptions{} + fo := &options.FilenameOptions{} + ta := &options.TagsOptions{} + do := &options.DebugOptions{} + + resolve := &cobra.Command{ + Use: "resolve -f FILENAME", + Short: "Print the input files with image references resolved to built/pushed image digests.", + Long: `This sub-command finds import path references within the provided files, builds them into Go binaries, containerizes them, publishes them, and prints the resulting yaml.`, + Example: ` + # Build and publish import path references to a Docker + # Registry as: + # ${KO_DOCKER_REPO}/- + # When KO_DOCKER_REPO is ko.local, it is the same as if + # --local and --preserve-import-paths were passed. + ko resolve -f config/ + + # Build and publish import path references to a Docker + # Registry preserving import path names as: + # ${KO_DOCKER_REPO}/ + # When KO_DOCKER_REPO is ko.local, it is the same as if + # --local was passed. + ko resolve --preserve-import-paths -f config/ + + # Build and publish import path references to a Docker + # daemon as: + # ko.local/ + # This always preserves import paths. + ko resolve --local -f config/`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + builder, err := makeBuilder(do) + if err != nil { + log.Fatalf("error creating builder: %v", err) + } + publisher, err := makePublisher(no, lo, ta) + if err != nil { + log.Fatalf("error creating publisher: %v", err) + } + resolveFilesToWriter(builder, publisher, fo, os.Stdout) + }, + } + options.AddLocalArg(resolve, lo) + options.AddNamingArgs(resolve, no) + options.AddFileArg(resolve, fo) + options.AddTagsArg(resolve, ta) + options.AddDebugArg(resolve, do) + topLevel.AddCommand(resolve) +} diff --git a/cmd/ko/resolve.go b/pkg/commands/resolver.go similarity index 88% rename from cmd/ko/resolve.go rename to pkg/commands/resolver.go index 702b7060b1..820c3814bd 100644 --- a/cmd/ko/resolve.go +++ b/pkg/commands/resolver.go @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package commands import ( + "errors" "fmt" "io" "io/ioutil" @@ -25,12 +26,13 @@ import ( "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/ko/pkg/build" + "github.com/google/ko/pkg/commands/options" "github.com/google/ko/pkg/publish" "github.com/google/ko/pkg/resolve" "github.com/mattmoor/dep-notify/pkg/graph" ) -func gobuildOptions() ([]build.Option, error) { +func gobuildOptions(do *options.DebugOptions) ([]build.Option, error) { creationTime, err := getCreationTime() if err != nil { return nil, err @@ -41,11 +43,14 @@ func gobuildOptions() ([]build.Option, error) { if creationTime != nil { opts = append(opts, build.WithCreationTime(*creationTime)) } + if do.DisableOptimizations { + opts = append(opts, build.WithDisabledOptimizations()) + } return opts, nil } -func makeBuilder() (*build.Caching, error) { - opt, err := gobuildOptions() +func makeBuilder(do *options.DebugOptions) (*build.Caching, error) { + opt, err := gobuildOptions(do) if err != nil { log.Fatalf("error setting up builder options: %v", err) } @@ -73,19 +78,22 @@ func makeBuilder() (*build.Caching, error) { return build.NewCaching(innerBuilder) } -func makePublisher(no *NameOptions, lo *LocalOptions, ta *TagsOptions) (publish.Interface, error) { +func makePublisher(no *options.NameOptions, lo *options.LocalOptions, ta *options.TagsOptions) (publish.Interface, error) { // Create the publish.Interface that we will use to publish image references // to either a docker daemon or a container image registry. innerPublisher, err := func() (publish.Interface, error) { - namer := makeNamer(no) + namer := options.MakeNamer(no) repoName := os.Getenv("KO_DOCKER_REPO") if lo.Local || repoName == publish.LocalDomain { return publish.NewDaemon(namer, ta.Tags), nil } + if repoName == "" { + return nil, errors.New("KO_DOCKER_REPO environment variable is unset") + } _, err := name.NewRepository(repoName, name.WeakValidation) if err != nil { - return nil, fmt.Errorf("the environment variable KO_DOCKER_REPO must be set to a valid docker repository, got %v", err) + return nil, fmt.Errorf("failed to parse environment variable KO_DOCKER_REPO=%q as repository: %v", repoName, err) } return publish.NewDefault(repoName, @@ -104,30 +112,21 @@ func makePublisher(no *NameOptions, lo *LocalOptions, ta *TagsOptions) (publish. // resolvedFuture represents a "future" for the bytes of a resolved file. type resolvedFuture chan []byte -func resolveFilesToWriter(fo *FilenameOptions, no *NameOptions, lo *LocalOptions, ta *TagsOptions, out io.WriteCloser) { +func resolveFilesToWriter(builder *build.Caching, publisher publish.Interface, fo *options.FilenameOptions, out io.WriteCloser) { defer out.Close() - builder, err := makeBuilder() - if err != nil { - log.Fatalf("error creating builder: %v", err) - } - - // Wrap publisher in a memoizing publisher implementation. - publisher, err := makePublisher(no, lo, ta) - if err != nil { - log.Fatalf("error creating publisher: %v", err) - } // By having this as a channel, we can hook this up to a filesystem // watcher and leave `fs` open to stream the names of yaml files // affected by code changes (including the modification of existing or // creation of new yaml files). - fs := enumerateFiles(fo) + fs := options.EnumerateFiles(fo) // This tracks filename -> []importpath var sm sync.Map var g graph.Interface var errCh chan error + var err error if fo.Watch { // Start a dep-notify process that on notifications scans the // file-to-recorded-build map and for each affected file resends diff --git a/pkg/commands/run.go b/pkg/commands/run.go new file mode 100644 index 0000000000..64d6564f6b --- /dev/null +++ b/pkg/commands/run.go @@ -0,0 +1,96 @@ +// Copyright 2018 Google LLC 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. + +package commands + +import ( + "log" + "os" + "os/exec" + + "github.com/google/ko/pkg/commands/options" + "github.com/spf13/cobra" +) + +// addRun augments our CLI surface with run. +func addRun(topLevel *cobra.Command) { + lo := &options.LocalOptions{} + bo := &options.BinaryOptions{} + no := &options.NameOptions{} + ta := &options.TagsOptions{} + do := &options.DebugOptions{} + + run := &cobra.Command{ + Use: "run NAME --image=IMPORTPATH", + Short: "A variant of `kubectl run` that containerizes IMPORTPATH first.", + Long: `This sub-command combines "ko publish" and "kubectl run" to support containerizing and running Go binaries on Kubernetes in a single command.`, + Example: ` + # Publish the --image and run it on Kubernetes as: + # ${KO_DOCKER_REPO}/- + # When KO_DOCKER_REPO is ko.local, it is the same as if + # --local and --preserve-import-paths were passed. + ko run foo --image=github.com/foo/bar/cmd/baz + + # This supports relative import paths as well. + ko run foo --image=./cmd/baz`, + Run: func(cmd *cobra.Command, args []string) { + builder, err := makeBuilder(do) + if err != nil { + log.Fatalf("error creating builder: %v", err) + } + publisher, err := makePublisher(no, lo, ta) + if err != nil { + log.Fatalf("error creating publisher: %v", err) + } + imgs, err := publishImages([]string{bo.Path}, publisher, builder) + if err != nil { + log.Fatalf("failed to publish images: %v", err) + } + + // There's only one, but this is the simple way to access the + // reference since the import path may have been qualified. + for k, v := range imgs { + log.Printf("Running %q", k) + // Issue a "kubectl run" command with our same arguments, + // but supply a second --image to override the one we intercepted. + argv := append(os.Args[1:], "--image", v.String()) + kubectlCmd := exec.Command("kubectl", argv...) + + // Pass through our environment + kubectlCmd.Env = os.Environ() + // Pass through our std* + kubectlCmd.Stderr = os.Stderr + kubectlCmd.Stdout = os.Stdout + kubectlCmd.Stdin = os.Stdin + + // Run it. + if err := kubectlCmd.Run(); err != nil { + log.Fatalf("error executing \"kubectl run\": %v", err) + } + } + }, + // We ignore unknown flags to avoid importing everything Go exposes + // from our commands. + FParseErrWhitelist: cobra.FParseErrWhitelist{ + UnknownFlags: true, + }, + } + options.AddLocalArg(run, lo) + options.AddNamingArgs(run, no) + options.AddImageArg(run, bo) + options.AddTagsArg(run, ta) + options.AddDebugArg(run, do) + + topLevel.AddCommand(run) +} diff --git a/cmd/ko/version.go b/pkg/commands/version.go similarity index 64% rename from cmd/ko/version.go rename to pkg/commands/version.go index e5a120d2b5..b5f21bbe54 100644 --- a/cmd/ko/version.go +++ b/pkg/commands/version.go @@ -12,30 +12,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package commands import ( "fmt" "log" "os/exec" + "strings" + + "github.com/spf13/cobra" ) // provided by govvv in compile-time var Version string +// addVersion augments our CLI surface with version. +func addVersion(topLevel *cobra.Command) { + topLevel.AddCommand(&cobra.Command{ + Use: "version", + Short: `Print ko version.`, + Run: func(cmd *cobra.Command, args []string) { + version() + }, + }) +} + func version() { if Version == "" { - hash, err := gitRevParseHead() + hash, err := exec.Command("git", "rev-parse", "HEAD").Output() if err != nil { log.Fatalf("error during command execution: %v", err) } - fmt.Printf("version: %v", string(hash)) - } else { - fmt.Printf("version: %v\n", Version) + Version = strings.TrimSpace(string(hash)) } -} - -func gitRevParseHead() ([]byte, error) { - cmd := exec.Command("git", "rev-parse", "HEAD") - return cmd.Output() + fmt.Printf("version: %v\n", Version) }