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 }