github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/commands/builder_inspect_test.go (about) 1 package commands_test 2 3 import ( 4 "bytes" 5 "errors" 6 "regexp" 7 "testing" 8 9 pubbldr "github.com/buildpacks/pack/builder" 10 11 "github.com/buildpacks/lifecycle/api" 12 "github.com/heroku/color" 13 "github.com/sclevine/spec" 14 "github.com/sclevine/spec/report" 15 16 "github.com/buildpacks/pack/internal/builder" 17 "github.com/buildpacks/pack/internal/builder/writer" 18 "github.com/buildpacks/pack/internal/commands" 19 "github.com/buildpacks/pack/internal/commands/fakes" 20 "github.com/buildpacks/pack/internal/config" 21 "github.com/buildpacks/pack/pkg/client" 22 "github.com/buildpacks/pack/pkg/logging" 23 h "github.com/buildpacks/pack/testhelpers" 24 ) 25 26 var ( 27 minimalLifecycleDescriptor = builder.LifecycleDescriptor{ 28 Info: builder.LifecycleInfo{Version: builder.VersionMustParse("3.4")}, 29 API: builder.LifecycleAPI{ 30 BuildpackVersion: api.MustParse("1.2"), 31 PlatformVersion: api.MustParse("2.3"), 32 }, 33 } 34 35 expectedLocalRunImages = []config.RunImage{ 36 {Image: "some/run-image", Mirrors: []string{"first/local", "second/local"}}, 37 } 38 expectedLocalInfo = &client.BuilderInfo{ 39 Description: "test-local-builder", 40 Stack: "local-stack", 41 RunImages: []pubbldr.RunImageConfig{{Image: "local/image"}}, 42 Lifecycle: minimalLifecycleDescriptor, 43 } 44 expectedRemoteInfo = &client.BuilderInfo{ 45 Description: "test-remote-builder", 46 Stack: "remote-stack", 47 RunImages: []pubbldr.RunImageConfig{{Image: "remote/image"}}, 48 Lifecycle: minimalLifecycleDescriptor, 49 } 50 expectedLocalDisplay = "Sample output for local builder" 51 expectedRemoteDisplay = "Sample output for remote builder" 52 expectedBuilderInfo = writer.SharedBuilderInfo{ 53 Name: "default/builder", 54 Trusted: false, 55 IsDefault: true, 56 } 57 ) 58 59 func TestBuilderInspectCommand(t *testing.T) { 60 color.Disable(true) 61 defer color.Disable(false) 62 spec.Run(t, "BuilderInspectCommand", testBuilderInspectCommand, spec.Parallel(), spec.Report(report.Terminal{})) 63 } 64 65 func testBuilderInspectCommand(t *testing.T, when spec.G, it spec.S) { 66 var ( 67 logger logging.Logger 68 outBuf bytes.Buffer 69 cfg config.Config 70 ) 71 72 it.Before(func() { 73 cfg = config.Config{ 74 DefaultBuilder: "default/builder", 75 RunImages: expectedLocalRunImages, 76 } 77 logger = logging.NewLogWithWriters(&outBuf, &outBuf) 78 }) 79 80 when("BuilderInspect", func() { 81 var ( 82 assert = h.NewAssertionManager(t) 83 ) 84 85 it("passes output of local and remote builders to correct writer", func() { 86 builderInspector := newDefaultBuilderInspector() 87 builderWriter := newDefaultBuilderWriter() 88 builderWriterFactory := newWriterFactory(returnsForWriter(builderWriter)) 89 90 command := commands.BuilderInspect(logger, cfg, builderInspector, builderWriterFactory) 91 command.SetArgs([]string{}) 92 err := command.Execute() 93 assert.Nil(err) 94 95 assert.Equal(builderWriter.ReceivedInfoForLocal, expectedLocalInfo) 96 assert.Equal(builderWriter.ReceivedInfoForRemote, expectedRemoteInfo) 97 assert.Equal(builderWriter.ReceivedBuilderInfo, expectedBuilderInfo) 98 assert.Equal(builderWriter.ReceivedLocalRunImages, expectedLocalRunImages) 99 assert.Equal(builderWriterFactory.ReceivedForKind, "human-readable") 100 assert.Equal(builderInspector.ReceivedForLocalName, "default/builder") 101 assert.Equal(builderInspector.ReceivedForRemoteName, "default/builder") 102 assert.ContainsF(outBuf.String(), "LOCAL:\n%s", expectedLocalDisplay) 103 assert.ContainsF(outBuf.String(), "REMOTE:\n%s", expectedRemoteDisplay) 104 }) 105 106 when("image name is provided as first arg", func() { 107 it("passes that image name to the inspector", func() { 108 builderInspector := newDefaultBuilderInspector() 109 writer := newDefaultBuilderWriter() 110 command := commands.BuilderInspect(logger, cfg, builderInspector, newWriterFactory(returnsForWriter(writer))) 111 command.SetArgs([]string{"some/image"}) 112 113 err := command.Execute() 114 assert.Nil(err) 115 116 assert.Equal(builderInspector.ReceivedForLocalName, "some/image") 117 assert.Equal(builderInspector.ReceivedForRemoteName, "some/image") 118 assert.Equal(writer.ReceivedBuilderInfo.IsDefault, false) 119 }) 120 }) 121 122 when("depth flag is provided", func() { 123 it("passes a modifier to the builder inspector", func() { 124 builderInspector := newDefaultBuilderInspector() 125 command := commands.BuilderInspect(logger, cfg, builderInspector, newDefaultWriterFactory()) 126 command.SetArgs([]string{"--depth", "5"}) 127 128 err := command.Execute() 129 assert.Nil(err) 130 131 assert.Equal(builderInspector.CalculatedConfigForLocal.OrderDetectionDepth, 5) 132 assert.Equal(builderInspector.CalculatedConfigForRemote.OrderDetectionDepth, 5) 133 }) 134 }) 135 136 when("output type is set to json", func() { 137 it("passes json to the writer factory", func() { 138 writerFactory := newDefaultWriterFactory() 139 command := commands.BuilderInspect(logger, cfg, newDefaultBuilderInspector(), writerFactory) 140 command.SetArgs([]string{"--output", "json"}) 141 142 err := command.Execute() 143 assert.Nil(err) 144 145 assert.Equal(writerFactory.ReceivedForKind, "json") 146 }) 147 }) 148 149 when("output type is set to toml using the shorthand flag", func() { 150 it("passes toml to the writer factory", func() { 151 writerFactory := newDefaultWriterFactory() 152 command := commands.BuilderInspect(logger, cfg, newDefaultBuilderInspector(), writerFactory) 153 command.SetArgs([]string{"-o", "toml"}) 154 155 err := command.Execute() 156 assert.Nil(err) 157 158 assert.Equal(writerFactory.ReceivedForKind, "toml") 159 }) 160 }) 161 162 when("builder inspector returns an error for local builder", func() { 163 it("passes that error to the writer to handle appropriately", func() { 164 baseError := errors.New("couldn't inspect local") 165 166 builderInspector := newBuilderInspector(errorsForLocal(baseError)) 167 builderWriter := newDefaultBuilderWriter() 168 builderWriterFactory := newWriterFactory(returnsForWriter(builderWriter)) 169 170 command := commands.BuilderInspect(logger, cfg, builderInspector, builderWriterFactory) 171 command.SetArgs([]string{}) 172 err := command.Execute() 173 assert.Nil(err) 174 175 assert.ErrorWithMessage(builderWriter.ReceivedErrorForLocal, "couldn't inspect local") 176 }) 177 }) 178 179 when("builder inspector returns an error remote builder", func() { 180 it("passes that error to the writer to handle appropriately", func() { 181 baseError := errors.New("couldn't inspect remote") 182 183 builderInspector := newBuilderInspector(errorsForRemote(baseError)) 184 builderWriter := newDefaultBuilderWriter() 185 builderWriterFactory := newWriterFactory(returnsForWriter(builderWriter)) 186 187 command := commands.BuilderInspect(logger, cfg, builderInspector, builderWriterFactory) 188 command.SetArgs([]string{}) 189 err := command.Execute() 190 assert.Nil(err) 191 192 assert.ErrorWithMessage(builderWriter.ReceivedErrorForRemote, "couldn't inspect remote") 193 }) 194 }) 195 196 when("image is trusted", func() { 197 it("passes builder info with trusted true to the writer's `Print` method", func() { 198 cfg.TrustedBuilders = []config.TrustedBuilder{ 199 {Name: "trusted/builder"}, 200 } 201 writer := newDefaultBuilderWriter() 202 203 command := commands.BuilderInspect( 204 logger, 205 cfg, 206 newDefaultBuilderInspector(), 207 newWriterFactory(returnsForWriter(writer)), 208 ) 209 command.SetArgs([]string{"trusted/builder"}) 210 211 err := command.Execute() 212 assert.Nil(err) 213 214 assert.Equal(writer.ReceivedBuilderInfo.Trusted, true) 215 }) 216 }) 217 218 when("default builder is configured and is the same as specified by the command", func() { 219 it("passes builder info with isDefault true to the writer's `Print` method", func() { 220 cfg.DefaultBuilder = "the/default-builder" 221 writer := newDefaultBuilderWriter() 222 223 command := commands.BuilderInspect( 224 logger, 225 cfg, 226 newDefaultBuilderInspector(), 227 newWriterFactory(returnsForWriter(writer)), 228 ) 229 command.SetArgs([]string{"the/default-builder"}) 230 231 err := command.Execute() 232 assert.Nil(err) 233 234 assert.Equal(writer.ReceivedBuilderInfo.IsDefault, true) 235 }) 236 }) 237 238 when("default builder is empty and no builder is specified in command args", func() { 239 it("suggests builders and returns a soft error", func() { 240 cfg.DefaultBuilder = "" 241 242 command := commands.BuilderInspect(logger, cfg, newDefaultBuilderInspector(), newDefaultWriterFactory()) 243 command.SetArgs([]string{}) 244 245 err := command.Execute() 246 assert.Error(err) 247 if !errors.Is(err, client.SoftError{}) { 248 t.Fatalf("expect a client.SoftError, got: %s", err) 249 } 250 251 assert.Contains(outBuf.String(), `Please select a default builder with: 252 253 pack config default-builder <builder-image>`) 254 255 assert.Matches(outBuf.String(), regexp.MustCompile(`Paketo Buildpacks:\s+'paketobuildpacks/builder-jammy-base'`)) 256 assert.Matches(outBuf.String(), regexp.MustCompile(`Paketo Buildpacks:\s+'paketobuildpacks/builder-jammy-full'`)) 257 assert.Matches(outBuf.String(), regexp.MustCompile(`Heroku:\s+'heroku/builder:22'`)) 258 }) 259 }) 260 261 when("print returns an error", func() { 262 it("returns that error", func() { 263 baseError := errors.New("couldn't write builder") 264 265 builderWriter := newBuilderWriter(errorsForPrint(baseError)) 266 command := commands.BuilderInspect( 267 logger, 268 cfg, 269 newDefaultBuilderInspector(), 270 newWriterFactory(returnsForWriter(builderWriter)), 271 ) 272 command.SetArgs([]string{}) 273 274 err := command.Execute() 275 assert.ErrorWithMessage(err, "couldn't write builder") 276 }) 277 }) 278 279 when("writer factory returns an error", func() { 280 it("returns that error", func() { 281 baseError := errors.New("invalid output format") 282 283 writerFactory := newWriterFactory(errorsForWriter(baseError)) 284 command := commands.BuilderInspect(logger, cfg, newDefaultBuilderInspector(), writerFactory) 285 command.SetArgs([]string{}) 286 287 err := command.Execute() 288 assert.ErrorWithMessage(err, "invalid output format") 289 }) 290 }) 291 }) 292 } 293 294 func newDefaultBuilderInspector() *fakes.FakeBuilderInspector { 295 return &fakes.FakeBuilderInspector{ 296 InfoForLocal: expectedLocalInfo, 297 InfoForRemote: expectedRemoteInfo, 298 } 299 } 300 301 func newDefaultBuilderWriter() *fakes.FakeBuilderWriter { 302 return &fakes.FakeBuilderWriter{ 303 PrintForLocal: expectedLocalDisplay, 304 PrintForRemote: expectedRemoteDisplay, 305 } 306 } 307 308 func newDefaultWriterFactory() *fakes.FakeBuilderWriterFactory { 309 return &fakes.FakeBuilderWriterFactory{ 310 ReturnForWriter: newDefaultBuilderWriter(), 311 } 312 } 313 314 type BuilderWriterModifier func(w *fakes.FakeBuilderWriter) 315 316 func errorsForPrint(err error) BuilderWriterModifier { 317 return func(w *fakes.FakeBuilderWriter) { 318 w.ErrorForPrint = err 319 } 320 } 321 322 func newBuilderWriter(modifiers ...BuilderWriterModifier) *fakes.FakeBuilderWriter { 323 w := newDefaultBuilderWriter() 324 325 for _, mod := range modifiers { 326 mod(w) 327 } 328 329 return w 330 } 331 332 type WriterFactoryModifier func(f *fakes.FakeBuilderWriterFactory) 333 334 func returnsForWriter(writer writer.BuilderWriter) WriterFactoryModifier { 335 return func(f *fakes.FakeBuilderWriterFactory) { 336 f.ReturnForWriter = writer 337 } 338 } 339 340 func errorsForWriter(err error) WriterFactoryModifier { 341 return func(f *fakes.FakeBuilderWriterFactory) { 342 f.ErrorForWriter = err 343 } 344 } 345 346 func newWriterFactory(modifiers ...WriterFactoryModifier) *fakes.FakeBuilderWriterFactory { 347 f := newDefaultWriterFactory() 348 349 for _, mod := range modifiers { 350 mod(f) 351 } 352 353 return f 354 } 355 356 type BuilderInspectorModifier func(i *fakes.FakeBuilderInspector) 357 358 func errorsForLocal(err error) BuilderInspectorModifier { 359 return func(i *fakes.FakeBuilderInspector) { 360 i.ErrorForLocal = err 361 } 362 } 363 364 func errorsForRemote(err error) BuilderInspectorModifier { 365 return func(i *fakes.FakeBuilderInspector) { 366 i.ErrorForRemote = err 367 } 368 } 369 370 func newBuilderInspector(modifiers ...BuilderInspectorModifier) *fakes.FakeBuilderInspector { 371 i := newDefaultBuilderInspector() 372 373 for _, mod := range modifiers { 374 mod(i) 375 } 376 377 return i 378 }