github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/builder/writer/toml_test.go (about)

     1  package writer_test
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"testing"
     8  
     9  	"github.com/pelletier/go-toml"
    10  
    11  	"github.com/Masterminds/semver"
    12  	"github.com/buildpacks/lifecycle/api"
    13  	"github.com/heroku/color"
    14  	"github.com/sclevine/spec"
    15  	"github.com/sclevine/spec/report"
    16  
    17  	pubbldr "github.com/buildpacks/pack/builder"
    18  	"github.com/buildpacks/pack/internal/builder"
    19  	"github.com/buildpacks/pack/internal/builder/writer"
    20  	"github.com/buildpacks/pack/internal/config"
    21  	"github.com/buildpacks/pack/pkg/client"
    22  	"github.com/buildpacks/pack/pkg/dist"
    23  	"github.com/buildpacks/pack/pkg/logging"
    24  	h "github.com/buildpacks/pack/testhelpers"
    25  )
    26  
    27  func TestTOML(t *testing.T) {
    28  	color.Disable(true)
    29  	defer color.Disable(false)
    30  	spec.Run(t, "Builder Writer", testTOML, spec.Parallel(), spec.Report(report.Terminal{}))
    31  }
    32  
    33  func testTOML(t *testing.T, when spec.G, it spec.S) {
    34  	const (
    35  		expectedRemoteRunImages = `  [[remote_info.run_images]]
    36      name = "first/local"
    37      user_configured = true
    38  
    39    [[remote_info.run_images]]
    40      name = "second/local"
    41      user_configured = true
    42  
    43    [[remote_info.run_images]]
    44      name = "some/run-image"
    45  
    46    [[remote_info.run_images]]
    47      name = "first/default"
    48  
    49    [[remote_info.run_images]]
    50      name = "second/default"`
    51  
    52  		expectedLocalRunImages = `  [[local_info.run_images]]
    53      name = "first/local"
    54      user_configured = true
    55  
    56    [[local_info.run_images]]
    57      name = "second/local"
    58      user_configured = true
    59  
    60    [[local_info.run_images]]
    61      name = "some/run-image"
    62  
    63    [[local_info.run_images]]
    64      name = "first/local-default"
    65  
    66    [[local_info.run_images]]
    67      name = "second/local-default"`
    68  
    69  		expectedLocalBuildpacks = `  [[local_info.buildpacks]]
    70      id = "test.top.nested"
    71      version = "test.top.nested.version"
    72  
    73    [[local_info.buildpacks]]
    74      id = "test.nested"
    75      homepage = "http://geocities.com/top-bp"
    76  
    77    [[local_info.buildpacks]]
    78      id = "test.bp.one"
    79      version = "test.bp.one.version"
    80      homepage = "http://geocities.com/cool-bp"
    81  
    82    [[local_info.buildpacks]]
    83      id = "test.bp.two"
    84      version = "test.bp.two.version"
    85  
    86    [[local_info.buildpacks]]
    87      id = "test.bp.three"
    88      version = "test.bp.three.version"`
    89  
    90  		expectedRemoteBuildpacks = `  [[remote_info.buildpacks]]
    91      id = "test.top.nested"
    92      version = "test.top.nested.version"
    93  
    94    [[remote_info.buildpacks]]
    95      id = "test.nested"
    96      homepage = "http://geocities.com/top-bp"
    97  
    98    [[remote_info.buildpacks]]
    99      id = "test.bp.one"
   100      version = "test.bp.one.version"
   101      homepage = "http://geocities.com/cool-bp"
   102  
   103    [[remote_info.buildpacks]]
   104      id = "test.bp.two"
   105      version = "test.bp.two.version"
   106  
   107    [[remote_info.buildpacks]]
   108      id = "test.bp.three"
   109      version = "test.bp.three.version"`
   110  
   111  		expectedLocalDetectionOrder = `  [[local_info.detection_order]]
   112  
   113      [[local_info.detection_order.buildpacks]]
   114        id = "test.top.nested"
   115        version = "test.top.nested.version"
   116  
   117        [[local_info.detection_order.buildpacks.buildpacks]]
   118          id = "test.nested"
   119          homepage = "http://geocities.com/top-bp"
   120  
   121          [[local_info.detection_order.buildpacks.buildpacks.buildpacks]]
   122            id = "test.bp.one"
   123            version = "test.bp.one.version"
   124            homepage = "http://geocities.com/cool-bp"
   125            optional = true
   126  
   127        [[local_info.detection_order.buildpacks.buildpacks]]
   128          id = "test.bp.three"
   129          version = "test.bp.three.version"
   130          optional = true
   131  
   132        [[local_info.detection_order.buildpacks.buildpacks]]
   133          id = "test.nested.two"
   134          version = "test.nested.two.version"
   135  
   136          [[local_info.detection_order.buildpacks.buildpacks.buildpacks]]
   137            id = "test.bp.one"
   138            version = "test.bp.one.version"
   139            homepage = "http://geocities.com/cool-bp"
   140            optional = true
   141            cyclic = true
   142  
   143      [[local_info.detection_order.buildpacks]]
   144        id = "test.bp.two"
   145        version = "test.bp.two.version"
   146        optional = true
   147  
   148    [[local_info.detection_order]]
   149      id = "test.bp.three"
   150      version = "test.bp.three.version"`
   151  
   152  		expectedRemoteDetectionOrder = `  [[remote_info.detection_order]]
   153  
   154      [[remote_info.detection_order.buildpacks]]
   155        id = "test.top.nested"
   156        version = "test.top.nested.version"
   157  
   158        [[remote_info.detection_order.buildpacks.buildpacks]]
   159          id = "test.nested"
   160          homepage = "http://geocities.com/top-bp"
   161  
   162          [[remote_info.detection_order.buildpacks.buildpacks.buildpacks]]
   163            id = "test.bp.one"
   164            version = "test.bp.one.version"
   165            homepage = "http://geocities.com/cool-bp"
   166            optional = true
   167  
   168        [[remote_info.detection_order.buildpacks.buildpacks]]
   169          id = "test.bp.three"
   170          version = "test.bp.three.version"
   171          optional = true
   172  
   173        [[remote_info.detection_order.buildpacks.buildpacks]]
   174          id = "test.nested.two"
   175          version = "test.nested.two.version"
   176  
   177          [[remote_info.detection_order.buildpacks.buildpacks.buildpacks]]
   178            id = "test.bp.one"
   179            version = "test.bp.one.version"
   180            homepage = "http://geocities.com/cool-bp"
   181            optional = true
   182            cyclic = true
   183  
   184      [[remote_info.detection_order.buildpacks]]
   185        id = "test.bp.two"
   186        version = "test.bp.two.version"
   187        optional = true
   188  
   189    [[remote_info.detection_order]]
   190      id = "test.bp.three"
   191      version = "test.bp.three.version"`
   192  
   193  		stackWithMixins = `  [stack]
   194      id = "test.stack.id"
   195      mixins = ["mixin1", "mixin2", "build:mixin3", "build:mixin4"]`
   196  	)
   197  
   198  	var (
   199  		assert = h.NewAssertionManager(t)
   200  		outBuf bytes.Buffer
   201  
   202  		remoteInfo *client.BuilderInfo
   203  		localInfo  *client.BuilderInfo
   204  
   205  		expectedRemoteInfo = fmt.Sprintf(`[remote_info]
   206    description = "Some remote description"
   207  
   208    [remote_info.created_by]
   209      Name = "Pack CLI"
   210      Version = "1.2.3"
   211  
   212    [remote_info.stack]
   213      id = "test.stack.id"
   214  
   215    [remote_info.lifecycle]
   216      version = "6.7.8"
   217  
   218      [remote_info.lifecycle.buildpack_apis]
   219        deprecated = []
   220        supported = ["1.2", "2.3"]
   221  
   222      [remote_info.lifecycle.platform_apis]
   223        deprecated = ["0.1", "1.2"]
   224        supported = ["4.5"]
   225  
   226  %s
   227  
   228  %s
   229  
   230  %s`, expectedRemoteRunImages, expectedRemoteBuildpacks, expectedRemoteDetectionOrder)
   231  
   232  		expectedLocalInfo = fmt.Sprintf(`[local_info]
   233    description = "Some local description"
   234  
   235    [local_info.created_by]
   236      Name = "Pack CLI"
   237      Version = "4.5.6"
   238  
   239    [local_info.stack]
   240      id = "test.stack.id"
   241  
   242    [local_info.lifecycle]
   243      version = "4.5.6"
   244  
   245      [local_info.lifecycle.buildpack_apis]
   246        deprecated = ["4.5", "6.7"]
   247        supported = ["8.9", "10.11"]
   248  
   249      [local_info.lifecycle.platform_apis]
   250        deprecated = []
   251        supported = ["7.8"]
   252  
   253  %s
   254  
   255  %s
   256  
   257  %s`, expectedLocalRunImages, expectedLocalBuildpacks, expectedLocalDetectionOrder)
   258  
   259  		expectedPrettifiedTOML = fmt.Sprintf(`builder_name = "test-builder"
   260  trusted = false
   261  default = false
   262  
   263  %s
   264  
   265  %s`, expectedRemoteInfo, expectedLocalInfo)
   266  	)
   267  
   268  	when("Print", func() {
   269  		it.Before(func() {
   270  			remoteInfo = &client.BuilderInfo{
   271  				Description:     "Some remote description",
   272  				Stack:           "test.stack.id",
   273  				Mixins:          []string{"mixin1", "mixin2", "build:mixin3", "build:mixin4"},
   274  				RunImages:       []pubbldr.RunImageConfig{{Image: "some/run-image", Mirrors: []string{"first/default", "second/default"}}},
   275  				Buildpacks:      buildpacks,
   276  				Order:           order,
   277  				BuildpackLayers: dist.ModuleLayers{},
   278  				Lifecycle: builder.LifecycleDescriptor{
   279  					Info: builder.LifecycleInfo{
   280  						Version: &builder.Version{
   281  							Version: *semver.MustParse("6.7.8"),
   282  						},
   283  					},
   284  					APIs: builder.LifecycleAPIs{
   285  						Buildpack: builder.APIVersions{
   286  							Deprecated: nil,
   287  							Supported:  builder.APISet{api.MustParse("1.2"), api.MustParse("2.3")},
   288  						},
   289  						Platform: builder.APIVersions{
   290  							Deprecated: builder.APISet{api.MustParse("0.1"), api.MustParse("1.2")},
   291  							Supported:  builder.APISet{api.MustParse("4.5")},
   292  						},
   293  					},
   294  				},
   295  				CreatedBy: builder.CreatorMetadata{
   296  					Name:    "Pack CLI",
   297  					Version: "1.2.3",
   298  				},
   299  			}
   300  
   301  			localInfo = &client.BuilderInfo{
   302  				Description:     "Some local description",
   303  				Stack:           "test.stack.id",
   304  				Mixins:          []string{"mixin1", "mixin2", "build:mixin3", "build:mixin4"},
   305  				RunImages:       []pubbldr.RunImageConfig{{Image: "some/run-image", Mirrors: []string{"first/local-default", "second/local-default"}}},
   306  				Buildpacks:      buildpacks,
   307  				Order:           order,
   308  				BuildpackLayers: dist.ModuleLayers{},
   309  				Lifecycle: builder.LifecycleDescriptor{
   310  					Info: builder.LifecycleInfo{
   311  						Version: &builder.Version{
   312  							Version: *semver.MustParse("4.5.6"),
   313  						},
   314  					},
   315  					APIs: builder.LifecycleAPIs{
   316  						Buildpack: builder.APIVersions{
   317  							Deprecated: builder.APISet{api.MustParse("4.5"), api.MustParse("6.7")},
   318  							Supported:  builder.APISet{api.MustParse("8.9"), api.MustParse("10.11")},
   319  						},
   320  						Platform: builder.APIVersions{
   321  							Deprecated: nil,
   322  							Supported:  builder.APISet{api.MustParse("7.8")},
   323  						},
   324  					},
   325  				},
   326  				CreatedBy: builder.CreatorMetadata{
   327  					Name:    "Pack CLI",
   328  					Version: "4.5.6",
   329  				},
   330  			}
   331  		})
   332  
   333  		it("prints both local remote builders as valid TOML", func() {
   334  			tomlWriter := writer.NewTOML()
   335  
   336  			logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   337  			err := tomlWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   338  			assert.Nil(err)
   339  
   340  			assert.Succeeds(validTOMLOutput(outBuf))
   341  			assert.Nil(err)
   342  
   343  			assert.ContainsTOML(outBuf.String(), expectedPrettifiedTOML)
   344  		})
   345  
   346  		when("builder doesn't exist locally or remotely", func() {
   347  			it("returns an error", func() {
   348  				tomlWriter := writer.NewTOML()
   349  
   350  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   351  				err := tomlWriter.Print(logger, localRunImages, nil, nil, nil, nil, sharedBuilderInfo)
   352  				assert.ErrorWithMessage(err, "unable to find builder 'test-builder' locally or remotely")
   353  			})
   354  		})
   355  
   356  		when("builder doesn't exist locally", func() {
   357  			it("shows null for local builder, and normal output for remote", func() {
   358  				tomlWriter := writer.NewTOML()
   359  
   360  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   361  				err := tomlWriter.Print(logger, localRunImages, nil, remoteInfo, nil, nil, sharedBuilderInfo)
   362  				assert.Nil(err)
   363  
   364  				assert.Succeeds(validTOMLOutput(outBuf))
   365  				assert.Nil(err)
   366  
   367  				assert.NotContains(outBuf.String(), "local_info")
   368  				assert.ContainsTOML(outBuf.String(), expectedRemoteInfo)
   369  			})
   370  		})
   371  
   372  		when("builder doesn't exist remotely", func() {
   373  			it("shows null for remote builder, and normal output for local", func() {
   374  				tomlWriter := writer.NewTOML()
   375  
   376  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   377  				err := tomlWriter.Print(logger, localRunImages, localInfo, nil, nil, nil, sharedBuilderInfo)
   378  				assert.Nil(err)
   379  
   380  				assert.Succeeds(validTOMLOutput(outBuf))
   381  				assert.Nil(err)
   382  
   383  				assert.NotContains(outBuf.String(), "remote_info")
   384  				assert.ContainsTOML(outBuf.String(), expectedLocalInfo)
   385  			})
   386  		})
   387  
   388  		when("localErr is an error", func() {
   389  			it("returns the error, and doesn't write any toml output", func() {
   390  				expectedErr := errors.New("failed to retrieve local info")
   391  
   392  				tomlWriter := writer.NewTOML()
   393  
   394  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   395  				err := tomlWriter.Print(logger, localRunImages, localInfo, remoteInfo, expectedErr, nil, sharedBuilderInfo)
   396  				assert.ErrorWithMessage(err, "preparing output for 'test-builder': failed to retrieve local info")
   397  
   398  				assert.Equal(outBuf.String(), "")
   399  			})
   400  		})
   401  
   402  		when("remoteErr is an error", func() {
   403  			it("returns the error, and doesn't write any toml output", func() {
   404  				expectedErr := errors.New("failed to retrieve remote info")
   405  
   406  				tomlWriter := writer.NewTOML()
   407  
   408  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   409  				err := tomlWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, expectedErr, sharedBuilderInfo)
   410  				assert.ErrorWithMessage(err, "preparing output for 'test-builder': failed to retrieve remote info")
   411  
   412  				assert.Equal(outBuf.String(), "")
   413  			})
   414  		})
   415  
   416  		when("logger is verbose", func() {
   417  			it("displays mixins associated with the stack", func() {
   418  				tomlWriter := writer.NewTOML()
   419  
   420  				logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose())
   421  				err := tomlWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   422  				assert.Nil(err)
   423  
   424  				assert.Succeeds(validTOMLOutput(outBuf))
   425  				assert.ContainsTOML(outBuf.String(), stackWithMixins)
   426  			})
   427  		})
   428  
   429  		when("no run images are specified", func() {
   430  			it("omits run images from output", func() {
   431  				localInfo.RunImages = []pubbldr.RunImageConfig{}
   432  				remoteInfo.RunImages = []pubbldr.RunImageConfig{}
   433  				emptyLocalRunImages := []config.RunImage{}
   434  
   435  				tomlWriter := writer.NewTOML()
   436  
   437  				logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose())
   438  				err := tomlWriter.Print(logger, emptyLocalRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   439  				assert.Nil(err)
   440  
   441  				assert.Succeeds(validTOMLOutput(outBuf))
   442  
   443  				assert.NotContains(outBuf.String(), "run_images")
   444  			})
   445  		})
   446  
   447  		when("no buildpacks are specified", func() {
   448  			it("omits buildpacks from output", func() {
   449  				localInfo.Buildpacks = []dist.ModuleInfo{}
   450  				remoteInfo.Buildpacks = []dist.ModuleInfo{}
   451  
   452  				tomlWriter := writer.NewTOML()
   453  
   454  				logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose())
   455  				err := tomlWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   456  				assert.Nil(err)
   457  
   458  				assert.Succeeds(validTOMLOutput(outBuf))
   459  
   460  				assert.NotContains(outBuf.String(), "local_info.buildpacks")
   461  				assert.NotContains(outBuf.String(), "remote_info.buildpacks")
   462  			})
   463  		})
   464  
   465  		when("no detection order is specified", func() {
   466  			it("omits dection order in output", func() {
   467  				localInfo.Order = pubbldr.DetectionOrder{}
   468  				remoteInfo.Order = pubbldr.DetectionOrder{}
   469  
   470  				tomlWriter := writer.NewTOML()
   471  
   472  				logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose())
   473  				err := tomlWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   474  				assert.Nil(err)
   475  
   476  				assert.Succeeds(validTOMLOutput(outBuf))
   477  				assert.NotContains(outBuf.String(), "detection_order")
   478  			})
   479  		})
   480  	})
   481  }
   482  
   483  func validTOMLOutput(source bytes.Buffer) error {
   484  	err := toml.NewDecoder(&source).Decode(&struct{}{})
   485  	if err != nil {
   486  		return fmt.Errorf("failed to unmarshal to toml: %w", err)
   487  	}
   488  	return nil
   489  }