github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/commands/buildpack_package_test.go (about)

     1  package commands_test
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"path/filepath"
     7  	"testing"
     8  
     9  	"github.com/heroku/color"
    10  	"github.com/pkg/errors"
    11  	"github.com/sclevine/spec"
    12  	"github.com/sclevine/spec/report"
    13  	"github.com/spf13/cobra"
    14  
    15  	pubbldpkg "github.com/buildpacks/pack/buildpackage"
    16  	"github.com/buildpacks/pack/internal/commands"
    17  	"github.com/buildpacks/pack/internal/commands/fakes"
    18  	"github.com/buildpacks/pack/internal/config"
    19  	"github.com/buildpacks/pack/pkg/dist"
    20  	"github.com/buildpacks/pack/pkg/image"
    21  	"github.com/buildpacks/pack/pkg/logging"
    22  	h "github.com/buildpacks/pack/testhelpers"
    23  )
    24  
    25  func TestPackageCommand(t *testing.T) {
    26  	color.Disable(true)
    27  	defer color.Disable(false)
    28  	spec.Run(t, "PackageCommand", testPackageCommand, spec.Parallel(), spec.Report(report.Terminal{}))
    29  }
    30  
    31  func testPackageCommand(t *testing.T, when spec.G, it spec.S) {
    32  	var (
    33  		logger *logging.LogWithWriters
    34  		outBuf bytes.Buffer
    35  	)
    36  
    37  	it.Before(func() {
    38  		logger = logging.NewLogWithWriters(&outBuf, &outBuf)
    39  	})
    40  
    41  	when("Package#Execute", func() {
    42  		var fakeBuildpackPackager *fakes.FakeBuildpackPackager
    43  
    44  		it.Before(func() {
    45  			fakeBuildpackPackager = &fakes.FakeBuildpackPackager{}
    46  		})
    47  
    48  		when("valid package config", func() {
    49  			it("reads package config from the configured path", func() {
    50  				fakePackageConfigReader := fakes.NewFakePackageConfigReader()
    51  				expectedPackageConfigPath := "/path/to/some/file"
    52  
    53  				cmd := packageCommand(
    54  					withPackageConfigReader(fakePackageConfigReader),
    55  					withPackageConfigPath(expectedPackageConfigPath),
    56  				)
    57  				err := cmd.Execute()
    58  				h.AssertNil(t, err)
    59  
    60  				h.AssertEq(t, fakePackageConfigReader.ReadCalledWithArg, expectedPackageConfigPath)
    61  			})
    62  
    63  			it("creates package with correct image name", func() {
    64  				cmd := packageCommand(
    65  					withImageName("my-specific-image"),
    66  					withBuildpackPackager(fakeBuildpackPackager),
    67  				)
    68  				err := cmd.Execute()
    69  				h.AssertNil(t, err)
    70  
    71  				receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
    72  				h.AssertEq(t, receivedOptions.Name, "my-specific-image")
    73  			})
    74  
    75  			it("creates package with config returned by the reader", func() {
    76  				myConfig := pubbldpkg.Config{
    77  					Buildpack: dist.BuildpackURI{URI: "test"},
    78  				}
    79  
    80  				cmd := packageCommand(
    81  					withBuildpackPackager(fakeBuildpackPackager),
    82  					withPackageConfigReader(fakes.NewFakePackageConfigReader(whereReadReturns(myConfig, nil))),
    83  				)
    84  				err := cmd.Execute()
    85  				h.AssertNil(t, err)
    86  
    87  				receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
    88  				h.AssertEq(t, receivedOptions.Config, myConfig)
    89  			})
    90  
    91  			when("file format", func() {
    92  				when("extension is .cnb", func() {
    93  					it("does not modify the name", func() {
    94  						cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager))
    95  						cmd.SetArgs([]string{"test.cnb", "-f", "file"})
    96  						h.AssertNil(t, cmd.Execute())
    97  
    98  						receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
    99  						h.AssertEq(t, receivedOptions.Name, "test.cnb")
   100  					})
   101  				})
   102  				when("extension is empty", func() {
   103  					it("appends .cnb to the name", func() {
   104  						cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager))
   105  						cmd.SetArgs([]string{"test", "-f", "file"})
   106  						h.AssertNil(t, cmd.Execute())
   107  
   108  						receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
   109  						h.AssertEq(t, receivedOptions.Name, "test.cnb")
   110  					})
   111  				})
   112  				when("extension is something other than .cnb", func() {
   113  					it("does not modify the name but shows a warning", func() {
   114  						cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager), withLogger(logger))
   115  						cmd.SetArgs([]string{"test.tar.gz", "-f", "file"})
   116  						h.AssertNil(t, cmd.Execute())
   117  
   118  						receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
   119  						h.AssertEq(t, receivedOptions.Name, "test.tar.gz")
   120  						h.AssertContains(t, outBuf.String(), "'.gz' is not a valid extension for a packaged buildpack. Packaged buildpacks must have a '.cnb' extension")
   121  					})
   122  				})
   123  				when("flatten is set to true", func() {
   124  					when("experimental is true", func() {
   125  						when("flatten exclude doesn't have format <buildpack>@<version>", func() {
   126  							it("errors with a descriptive message", func() {
   127  								cmd := packageCommand(withClientConfig(config.Config{Experimental: true}), withBuildpackPackager(fakeBuildpackPackager))
   128  								cmd.SetArgs([]string{"test", "-f", "file", "--flatten", "--flatten-exclude", "some-buildpack"})
   129  
   130  								err := cmd.Execute()
   131  								h.AssertError(t, err, fmt.Sprintf("invalid format %s; please use '<buildpack-id>@<buildpack-version>' to exclude buildpack from flattening", "some-buildpack"))
   132  							})
   133  						})
   134  
   135  						when("no exclusions", func() {
   136  							it("creates package with correct image name and warns flatten is being used", func() {
   137  								cmd := packageCommand(
   138  									withClientConfig(config.Config{Experimental: true}),
   139  									withBuildpackPackager(fakeBuildpackPackager),
   140  									withLogger(logger),
   141  								)
   142  								cmd.SetArgs([]string{"my-flatten-image", "-f", "file", "--flatten"})
   143  								err := cmd.Execute()
   144  								h.AssertNil(t, err)
   145  
   146  								receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
   147  								h.AssertEq(t, receivedOptions.Name, "my-flatten-image.cnb")
   148  								h.AssertContains(t, outBuf.String(), "Flattening a buildpack package could break the distribution specification. Please use it with caution.")
   149  							})
   150  						})
   151  					})
   152  
   153  					when("experimental is false", func() {
   154  						it("errors with a descriptive message", func() {
   155  							cmd := packageCommand(withClientConfig(config.Config{Experimental: false}), withBuildpackPackager(fakeBuildpackPackager))
   156  							cmd.SetArgs([]string{"test", "-f", "file", "--flatten"})
   157  
   158  							err := cmd.Execute()
   159  							h.AssertError(t, err, "Flattening a buildpack package is currently experimental.")
   160  						})
   161  					})
   162  				})
   163  			})
   164  
   165  			when("there is a path flag", func() {
   166  				it("returns an error saying that it cannot be used with the config flag", func() {
   167  					myConfig := pubbldpkg.Config{
   168  						Buildpack: dist.BuildpackURI{URI: "test"},
   169  					}
   170  
   171  					cmd := packageCommand(
   172  						withBuildpackPackager(fakeBuildpackPackager),
   173  						withPackageConfigReader(fakes.NewFakePackageConfigReader(whereReadReturns(myConfig, nil))),
   174  						withPath(".."),
   175  					)
   176  					err := cmd.Execute()
   177  					h.AssertError(t, err, "--config and --path cannot be used together")
   178  				})
   179  			})
   180  
   181  			when("pull-policy", func() {
   182  				var pullPolicyArgs = []string{
   183  					"some-image-name",
   184  					"--config", "/path/to/some/file",
   185  					"--pull-policy",
   186  				}
   187  
   188  				it("pull-policy=never sets policy", func() {
   189  					cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager))
   190  					cmd.SetArgs(append(pullPolicyArgs, "never"))
   191  					h.AssertNil(t, cmd.Execute())
   192  
   193  					receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
   194  					h.AssertEq(t, receivedOptions.PullPolicy, image.PullNever)
   195  				})
   196  
   197  				it("pull-policy=always sets policy", func() {
   198  					cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager))
   199  					cmd.SetArgs(append(pullPolicyArgs, "always"))
   200  					h.AssertNil(t, cmd.Execute())
   201  
   202  					receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
   203  					h.AssertEq(t, receivedOptions.PullPolicy, image.PullAlways)
   204  				})
   205  			})
   206  			when("no --pull-policy", func() {
   207  				var pullPolicyArgs = []string{
   208  					"some-image-name",
   209  					"--config", "/path/to/some/file",
   210  				}
   211  
   212  				it("uses the default policy when no policy configured", func() {
   213  					cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager))
   214  					cmd.SetArgs(pullPolicyArgs)
   215  					h.AssertNil(t, cmd.Execute())
   216  
   217  					receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
   218  					h.AssertEq(t, receivedOptions.PullPolicy, image.PullAlways)
   219  				})
   220  				it("uses the configured pull policy when policy configured", func() {
   221  					cmd := packageCommand(
   222  						withBuildpackPackager(fakeBuildpackPackager),
   223  						withClientConfig(config.Config{PullPolicy: "never"}),
   224  					)
   225  
   226  					cmd.SetArgs([]string{
   227  						"some-image-name",
   228  						"--config", "/path/to/some/file",
   229  					})
   230  
   231  					err := cmd.Execute()
   232  					h.AssertNil(t, err)
   233  
   234  					receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
   235  					h.AssertEq(t, receivedOptions.PullPolicy, image.PullNever)
   236  				})
   237  			})
   238  		})
   239  
   240  		when("no config path is specified", func() {
   241  			when("no path is specified", func() {
   242  				it("creates a default config with the uri set to the current working directory", func() {
   243  					cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager))
   244  					cmd.SetArgs([]string{"some-name"})
   245  					h.AssertNil(t, cmd.Execute())
   246  
   247  					receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
   248  					h.AssertEq(t, receivedOptions.Config.Buildpack.URI, ".")
   249  				})
   250  			})
   251  			when("a path is specified", func() {
   252  				it("creates a default config with the appropriate path", func() {
   253  					cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager))
   254  					cmd.SetArgs([]string{"some-name", "-p", ".."})
   255  					h.AssertNil(t, cmd.Execute())
   256  					bpPath, _ := filepath.Abs("..")
   257  					receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
   258  					h.AssertEq(t, receivedOptions.Config.Buildpack.URI, bpPath)
   259  				})
   260  			})
   261  		})
   262  	})
   263  
   264  	when("invalid flags", func() {
   265  		when("both --publish and --pull-policy never flags are specified", func() {
   266  			it("errors with a descriptive message", func() {
   267  				cmd := packageCommand()
   268  				cmd.SetArgs([]string{
   269  					"some-image-name", "--config", "/path/to/some/file",
   270  					"--publish",
   271  					"--pull-policy", "never",
   272  				})
   273  
   274  				err := cmd.Execute()
   275  				h.AssertNotNil(t, err)
   276  				h.AssertError(t, err, "--publish and --pull-policy never cannot be used together. The --publish flag requires the use of remote images.")
   277  			})
   278  		})
   279  
   280  		it("logs an error and exits when package toml is invalid", func() {
   281  			expectedErr := errors.New("it went wrong")
   282  
   283  			cmd := packageCommand(
   284  				withLogger(logger),
   285  				withPackageConfigReader(
   286  					fakes.NewFakePackageConfigReader(whereReadReturns(pubbldpkg.Config{}, expectedErr)),
   287  				),
   288  			)
   289  
   290  			err := cmd.Execute()
   291  			h.AssertNotNil(t, err)
   292  
   293  			h.AssertContains(t, outBuf.String(), fmt.Sprintf("ERROR: reading config: %s", expectedErr))
   294  		})
   295  
   296  		when("package-config is specified", func() {
   297  			it("errors with a descriptive message", func() {
   298  				cmd := packageCommand()
   299  				cmd.SetArgs([]string{"some-name", "--package-config", "some-path"})
   300  
   301  				err := cmd.Execute()
   302  				h.AssertError(t, err, "unknown flag: --package-config")
   303  			})
   304  		})
   305  
   306  		when("--pull-policy unknown-policy", func() {
   307  			it("fails to run", func() {
   308  				cmd := packageCommand()
   309  				cmd.SetArgs([]string{
   310  					"some-image-name",
   311  					"--config", "/path/to/some/file",
   312  					"--pull-policy",
   313  					"unknown-policy",
   314  				})
   315  
   316  				h.AssertError(t, cmd.Execute(), "parsing pull policy")
   317  			})
   318  		})
   319  
   320  		when("--label cannot be parsed", func() {
   321  			it("errors with a descriptive message", func() {
   322  				cmd := packageCommand()
   323  				cmd.SetArgs([]string{
   324  					"some-image-name", "--config", "/path/to/some/file",
   325  					"--label", "name+value",
   326  				})
   327  
   328  				err := cmd.Execute()
   329  				h.AssertNotNil(t, err)
   330  				h.AssertError(t, err, "invalid argument \"name+value\" for \"-l, --label\" flag: name+value must be formatted as key=value")
   331  			})
   332  		})
   333  	})
   334  }
   335  
   336  type packageCommandConfig struct {
   337  	logger              *logging.LogWithWriters
   338  	packageConfigReader *fakes.FakePackageConfigReader
   339  	buildpackPackager   *fakes.FakeBuildpackPackager
   340  	clientConfig        config.Config
   341  	imageName           string
   342  	configPath          string
   343  	path                string
   344  }
   345  
   346  type packageCommandOption func(config *packageCommandConfig)
   347  
   348  func packageCommand(ops ...packageCommandOption) *cobra.Command {
   349  	config := &packageCommandConfig{
   350  		logger:              logging.NewLogWithWriters(&bytes.Buffer{}, &bytes.Buffer{}),
   351  		packageConfigReader: fakes.NewFakePackageConfigReader(),
   352  		buildpackPackager:   &fakes.FakeBuildpackPackager{},
   353  		clientConfig:        config.Config{},
   354  		imageName:           "some-image-name",
   355  		configPath:          "/path/to/some/file",
   356  	}
   357  
   358  	for _, op := range ops {
   359  		op(config)
   360  	}
   361  
   362  	cmd := commands.BuildpackPackage(config.logger, config.clientConfig, config.buildpackPackager, config.packageConfigReader)
   363  	cmd.SetArgs([]string{config.imageName, "--config", config.configPath, "-p", config.path})
   364  
   365  	return cmd
   366  }
   367  
   368  func withLogger(logger *logging.LogWithWriters) packageCommandOption {
   369  	return func(config *packageCommandConfig) {
   370  		config.logger = logger
   371  	}
   372  }
   373  
   374  func withPackageConfigReader(reader *fakes.FakePackageConfigReader) packageCommandOption {
   375  	return func(config *packageCommandConfig) {
   376  		config.packageConfigReader = reader
   377  	}
   378  }
   379  
   380  func withBuildpackPackager(creator *fakes.FakeBuildpackPackager) packageCommandOption {
   381  	return func(config *packageCommandConfig) {
   382  		config.buildpackPackager = creator
   383  	}
   384  }
   385  
   386  func withImageName(name string) packageCommandOption {
   387  	return func(config *packageCommandConfig) {
   388  		config.imageName = name
   389  	}
   390  }
   391  
   392  func withPath(name string) packageCommandOption {
   393  	return func(config *packageCommandConfig) {
   394  		config.path = name
   395  	}
   396  }
   397  
   398  func withPackageConfigPath(path string) packageCommandOption {
   399  	return func(config *packageCommandConfig) {
   400  		config.configPath = path
   401  	}
   402  }
   403  
   404  func withClientConfig(clientCfg config.Config) packageCommandOption {
   405  	return func(config *packageCommandConfig) {
   406  		config.clientConfig = clientCfg
   407  	}
   408  }
   409  
   410  func whereReadReturns(config pubbldpkg.Config, err error) func(*fakes.FakePackageConfigReader) {
   411  	return func(r *fakes.FakePackageConfigReader) {
   412  		r.ReadReturnConfig = config
   413  		r.ReadReturnError = err
   414  	}
   415  }