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  }