diff --git a/cmd/cmd.go b/cmd/cmd.go index 9b1c39136..e5ad77e78 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,6 +1,8 @@ package cmd import ( + "os" + "github.com/heroku/color" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -23,6 +25,12 @@ type ConfigurableLogger interface { WantVerbose(f bool) } +// clientHolder defers client initialization until PersistentPreRunE, +// allowing root persistent flags +type clientHolder struct { + commands.PackClient +} + // NewPackCommand generates a Pack command // //nolint:staticcheck @@ -33,15 +41,23 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) { return nil, err } - packClient, err := initClient(logger, cfg) - if err != nil { - return nil, err - } + holder := &clientHolder{} + var dockerHost string rootCmd := &cobra.Command{ Use: "pack", Short: "CLI for building apps using Cloud Native Buildpacks", - PersistentPreRun: func(cmd *cobra.Command, args []string) { + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if dockerHost != "" && dockerHost != "inherit" { + os.Setenv("DOCKER_HOST", dockerHost) + } + + packClient, err := initClient(logger, cfg) + if err != nil { + return err + } + holder.PackClient = packClient + if fs := cmd.Flags(); fs != nil { if forceColor, err := fs.GetBool("force-color"); err == nil && !forceColor { if flag, err := fs.GetBool("no-color"); err == nil && flag { @@ -63,6 +79,8 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) { logger.WantTime(flag) } } + + return nil }, } @@ -71,40 +89,47 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) { rootCmd.PersistentFlags().Bool("timestamps", false, "Enable timestamps in output") rootCmd.PersistentFlags().BoolP("quiet", "q", false, "Show less output") rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Show more output") + rootCmd.PersistentFlags().StringVar(&dockerHost, "docker-host", "", + `Address to docker daemon to connect to. +If not set (or set to empty string) the standard socket location will be used. +Special value 'inherit' may be used in which case DOCKER_HOST environment variable will be used. +This flag is available on all subcommands; for 'build', it controls which daemon is +exposed to the build container's lifecycle phases. +`) rootCmd.Flags().Bool("version", false, "Show current 'pack' version") commands.AddHelpFlag(rootCmd, "pack") - rootCmd.AddCommand(commands.Build(logger, cfg, packClient)) - rootCmd.AddCommand(commands.NewBuilderCommand(logger, cfg, packClient)) - rootCmd.AddCommand(commands.NewBuildpackCommand(logger, cfg, packClient, buildpackage.NewConfigReader())) - rootCmd.AddCommand(commands.NewExtensionCommand(logger, cfg, packClient, buildpackage.NewConfigReader())) - rootCmd.AddCommand(commands.NewConfigCommand(logger, cfg, cfgPath, packClient)) - rootCmd.AddCommand(commands.InspectImage(logger, imagewriter.NewFactory(), cfg, packClient)) + rootCmd.AddCommand(commands.Build(logger, cfg, holder)) + rootCmd.AddCommand(commands.NewBuilderCommand(logger, cfg, holder)) + rootCmd.AddCommand(commands.NewBuildpackCommand(logger, cfg, holder, buildpackage.NewConfigReader())) + rootCmd.AddCommand(commands.NewExtensionCommand(logger, cfg, holder, buildpackage.NewConfigReader())) + rootCmd.AddCommand(commands.NewConfigCommand(logger, cfg, cfgPath, holder)) + rootCmd.AddCommand(commands.InspectImage(logger, imagewriter.NewFactory(), cfg, holder)) rootCmd.AddCommand(commands.NewStackCommand(logger)) - rootCmd.AddCommand(commands.Rebase(logger, cfg, packClient)) - rootCmd.AddCommand(commands.NewSBOMCommand(logger, cfg, packClient)) + rootCmd.AddCommand(commands.Rebase(logger, cfg, holder)) + rootCmd.AddCommand(commands.NewSBOMCommand(logger, cfg, holder)) - rootCmd.AddCommand(commands.InspectBuildpack(logger, cfg, packClient)) - rootCmd.AddCommand(commands.InspectBuilder(logger, cfg, packClient, builderwriter.NewFactory())) + rootCmd.AddCommand(commands.InspectBuildpack(logger, cfg, holder)) + rootCmd.AddCommand(commands.InspectBuilder(logger, cfg, holder, builderwriter.NewFactory())) - rootCmd.AddCommand(commands.SetDefaultBuilder(logger, cfg, cfgPath, packClient)) + rootCmd.AddCommand(commands.SetDefaultBuilder(logger, cfg, cfgPath, holder)) rootCmd.AddCommand(commands.SetRunImagesMirrors(logger, cfg, cfgPath)) - rootCmd.AddCommand(commands.SuggestBuilders(logger, packClient)) + rootCmd.AddCommand(commands.SuggestBuilders(logger, holder)) rootCmd.AddCommand(commands.TrustBuilder(logger, cfg, cfgPath)) rootCmd.AddCommand(commands.UntrustBuilder(logger, cfg, cfgPath)) rootCmd.AddCommand(commands.ListTrustedBuilders(logger, cfg)) - rootCmd.AddCommand(commands.CreateBuilder(logger, cfg, packClient)) - rootCmd.AddCommand(commands.PackageBuildpack(logger, cfg, packClient, buildpackage.NewConfigReader())) + rootCmd.AddCommand(commands.CreateBuilder(logger, cfg, holder)) + rootCmd.AddCommand(commands.PackageBuildpack(logger, cfg, holder, buildpackage.NewConfigReader())) if cfg.Experimental { rootCmd.AddCommand(commands.AddBuildpackRegistry(logger, cfg, cfgPath)) rootCmd.AddCommand(commands.ListBuildpackRegistries(logger, cfg)) - rootCmd.AddCommand(commands.RegisterBuildpack(logger, cfg, packClient)) + rootCmd.AddCommand(commands.RegisterBuildpack(logger, cfg, holder)) rootCmd.AddCommand(commands.SetDefaultRegistry(logger, cfg, cfgPath)) rootCmd.AddCommand(commands.RemoveRegistry(logger, cfg, cfgPath)) - rootCmd.AddCommand(commands.YankBuildpack(logger, cfg, packClient)) - rootCmd.AddCommand(commands.NewManifestCommand(logger, packClient)) + rootCmd.AddCommand(commands.YankBuildpack(logger, cfg, holder)) + rootCmd.AddCommand(commands.NewManifestCommand(logger, holder)) } packHome, err := config.PackHome() @@ -113,10 +138,10 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) { } rootCmd.AddCommand(commands.CompletionCommand(logger, packHome)) - rootCmd.AddCommand(commands.Report(logger, packClient.Version(), cfgPath)) - rootCmd.AddCommand(commands.Version(logger, packClient.Version())) + rootCmd.AddCommand(commands.Report(logger, client.Version, cfgPath)) + rootCmd.AddCommand(commands.Version(logger, client.Version)) - rootCmd.Version = packClient.Version() + rootCmd.Version = client.Version rootCmd.SetVersionTemplate(`{{.Version}}{{"\n"}}`) rootCmd.SetOut(logging.GetWriterForLevel(logger, logging.InfoLevel)) rootCmd.SetErr(logging.GetWriterForLevel(logger, logging.ErrorLevel)) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 000000000..a6fe9a9b4 --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,135 @@ +package cmd + +import ( + "io" + "os" + "testing" + + h "github.com/buildpacks/pack/testhelpers" +) + +// saveAndRestoreEnv saves the current value (or unset state) of an environment +func saveAndRestoreEnv(t *testing.T, key string) { + t.Helper() + orig, wasSet := os.LookupEnv(key) + t.Cleanup(func() { + if wasSet { + os.Setenv(key, orig) + } else { + os.Unsetenv(key) + } + }) +} + +func TestNewPackCommand_DockerHostPersistentFlag(t *testing.T) { + saveAndRestoreEnv(t, "DOCKER_HOST") + + logger := newTestLogger() + rootCmd, err := NewPackCommand(logger) + h.AssertNil(t, err) + + flag := rootCmd.PersistentFlags().Lookup("docker-host") + h.AssertNotNil(t, flag) + h.AssertEq(t, flag.DefValue, "") +} + +func TestNewPackCommand_DockerHostInheritedBySubcommands(t *testing.T) { + saveAndRestoreEnv(t, "DOCKER_HOST") + + logger := newTestLogger() + rootCmd, err := NewPackCommand(logger) + h.AssertNil(t, err) + + for _, childName := range []string{"builder", "buildpack", "rebase"} { + child, _, err := rootCmd.Find([]string{childName}) + h.AssertNil(t, err) + flag := child.InheritedFlags().Lookup("docker-host") + h.AssertNotNil(t, flag) + } +} + +func TestNewPackCommand_DockerHostInheritedByNestedSubcommands(t *testing.T) { + saveAndRestoreEnv(t, "DOCKER_HOST") + + logger := newTestLogger() + rootCmd, err := NewPackCommand(logger) + h.AssertNil(t, err) + + for _, path := range [][]string{ + {"builder", "create"}, + {"builder", "inspect"}, + {"buildpack", "package"}, + } { + child, _, err := rootCmd.Find(path) + h.AssertNil(t, err) + flag := child.InheritedFlags().Lookup("docker-host") + h.AssertNotNil(t, flag) + } +} + +func TestNewPackCommand_BuildCommandHasLocalDockerHost(t *testing.T) { + saveAndRestoreEnv(t, "DOCKER_HOST") + + logger := newTestLogger() + rootCmd, err := NewPackCommand(logger) + h.AssertNil(t, err) + + buildCmd, _, err := rootCmd.Find([]string{"build"}) + h.AssertNil(t, err) + + localFlag := buildCmd.Flags().Lookup("docker-host") + h.AssertNotNil(t, localFlag) + + localFlags := buildCmd.LocalFlags() + h.AssertNotNil(t, localFlags.Lookup("docker-host")) +} + +func TestNewPackCommand_DockerHostSetsEnv(t *testing.T) { + saveAndRestoreEnv(t, "DOCKER_HOST") + os.Unsetenv("DOCKER_HOST") + + logger := newTestLogger() + rootCmd, err := NewPackCommand(logger) + h.AssertNil(t, err) + + rootCmd.SetArgs([]string{"version", "--docker-host", "unix:///custom/docker.sock"}) + err = rootCmd.Execute() + h.AssertNil(t, err) + + h.AssertEq(t, os.Getenv("DOCKER_HOST"), "unix:///custom/docker.sock") +} + +func TestNewPackCommand_DockerHostInheritDoesNotOverrideEnv(t *testing.T) { + saveAndRestoreEnv(t, "DOCKER_HOST") + os.Setenv("DOCKER_HOST", "unix:///original/socket.sock") + + logger := newTestLogger() + rootCmd, err := NewPackCommand(logger) + h.AssertNil(t, err) + + rootCmd.SetArgs([]string{"version", "--docker-host", "inherit"}) + err = rootCmd.Execute() + h.AssertNil(t, err) + + h.AssertEq(t, os.Getenv("DOCKER_HOST"), "unix:///original/socket.sock") +} + +func newTestLogger() ConfigurableLogger { + return &testLogger{} +} + +type testLogger struct{} + +func (l *testLogger) Debug(msg string) {} +func (l *testLogger) Debugf(fmt string, v ...interface{}) {} +func (l *testLogger) Info(msg string) {} +func (l *testLogger) Infof(fmt string, v ...interface{}) {} +func (l *testLogger) Warn(msg string) {} +func (l *testLogger) Warnf(fmt string, v ...interface{}) {} +func (l *testLogger) Error(msg string) {} +func (l *testLogger) Errorf(fmt string, v ...interface{}) {} +func (l *testLogger) Writer() io.Writer { return os.Stderr } +func (l *testLogger) IsVerbose() bool { return false } +func (l *testLogger) WantTime(f bool) {} +func (l *testLogger) WantQuiet(f bool) {} +func (l *testLogger) WantVerbose(f bool) {}