github.com/windmeup/goreleaser@v1.21.95/internal/pipe/ko/ko_test.go (about)

     1  package ko
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
    11  	_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
    12  	"github.com/google/go-containerregistry/pkg/name"
    13  	"github.com/google/go-containerregistry/pkg/v1/remote"
    14  	"github.com/stretchr/testify/require"
    15  	"github.com/windmeup/goreleaser/internal/artifact"
    16  	"github.com/windmeup/goreleaser/internal/skips"
    17  	"github.com/windmeup/goreleaser/internal/testctx"
    18  	"github.com/windmeup/goreleaser/internal/testlib"
    19  	"github.com/windmeup/goreleaser/pkg/config"
    20  	"github.com/windmeup/goreleaser/pkg/context"
    21  )
    22  
    23  const (
    24  	registryPort = "5052"
    25  	registry     = "localhost:5052/"
    26  )
    27  
    28  func TestDefault(t *testing.T) {
    29  	ctx := testctx.NewWithCfg(config.Project{
    30  		Env: []string{
    31  			"KO_DOCKER_REPO=" + registry,
    32  			"COSIGN_REPOSITORY=" + registry,
    33  			"LDFLAGS=foobar",
    34  			"FLAGS=barfoo",
    35  			"LE_ENV=test",
    36  		},
    37  		ProjectName: "test",
    38  		Builds: []config.Build{
    39  			{
    40  				ID:  "test",
    41  				Dir: ".",
    42  				BuildDetails: config.BuildDetails{
    43  					Ldflags: []string{"{{.Env.LDFLAGS}}"},
    44  					Flags:   []string{"{{.Env.FLAGS}}"},
    45  					Env:     []string{"SOME_ENV={{.Env.LE_ENV}}"},
    46  				},
    47  			},
    48  		},
    49  		Kos: []config.Ko{
    50  			{},
    51  		},
    52  	})
    53  	require.NoError(t, Pipe{}.Default(ctx))
    54  	require.Equal(t, config.Ko{
    55  		ID:         "test",
    56  		Build:      "test",
    57  		BaseImage:  chainguardStatic,
    58  		Repository: registry,
    59  		Platforms:  []string{"linux/amd64"},
    60  		SBOM:       "spdx",
    61  		Tags:       []string{"latest"},
    62  		WorkingDir: ".",
    63  		Ldflags:    []string{"{{.Env.LDFLAGS}}"},
    64  		Flags:      []string{"{{.Env.FLAGS}}"},
    65  		Env:        []string{"SOME_ENV={{.Env.LE_ENV}}"},
    66  	}, ctx.Config.Kos[0])
    67  }
    68  
    69  func TestDefaultNoImage(t *testing.T) {
    70  	ctx := testctx.NewWithCfg(config.Project{
    71  		ProjectName: "test",
    72  		Builds: []config.Build{
    73  			{
    74  				ID: "test",
    75  			},
    76  		},
    77  		Kos: []config.Ko{
    78  			{},
    79  		},
    80  	})
    81  	require.ErrorIs(t, Pipe{}.Default(ctx), errNoRepository)
    82  }
    83  
    84  func TestDescription(t *testing.T) {
    85  	require.NotEmpty(t, Pipe{}.String())
    86  }
    87  
    88  func TestSkip(t *testing.T) {
    89  	t.Run("skip ko set", func(t *testing.T) {
    90  		ctx := testctx.NewWithCfg(config.Project{
    91  			Kos: []config.Ko{{}},
    92  		}, testctx.Skip(skips.Ko))
    93  		require.True(t, Pipe{}.Skip(ctx))
    94  	})
    95  	t.Run("skip no kos", func(t *testing.T) {
    96  		ctx := testctx.New()
    97  		require.True(t, Pipe{}.Skip(ctx))
    98  	})
    99  	t.Run("dont skip", func(t *testing.T) {
   100  		ctx := testctx.NewWithCfg(config.Project{
   101  			Kos: []config.Ko{{}},
   102  		})
   103  		require.False(t, Pipe{}.Skip(ctx))
   104  	})
   105  }
   106  
   107  func TestPublishPipeNoMatchingBuild(t *testing.T) {
   108  	ctx := testctx.NewWithCfg(config.Project{
   109  		Builds: []config.Build{
   110  			{
   111  				ID: "doesnt matter",
   112  			},
   113  		},
   114  		Kos: []config.Ko{
   115  			{
   116  				ID:    "default",
   117  				Build: "wont match nothing",
   118  			},
   119  		},
   120  	})
   121  
   122  	require.EqualError(t, Pipe{}.Default(ctx), `no builds with id "wont match nothing"`)
   123  }
   124  
   125  func TestPublishPipeSuccess(t *testing.T) {
   126  	testlib.StartRegistry(t, "ko_registry", registryPort)
   127  
   128  	table := []struct {
   129  		Name               string
   130  		SBOM               string
   131  		BaseImage          string
   132  		Labels             map[string]string
   133  		ExpectedLabels     map[string]string
   134  		Platforms          []string
   135  		Tags               []string
   136  		CreationTime       string
   137  		KoDataCreationTime string
   138  	}{
   139  		{
   140  			// Must be first as others add an SBOM for the same image
   141  			Name: "sbom-none",
   142  			SBOM: "none",
   143  		},
   144  		{
   145  			Name: "sbom-spdx",
   146  			SBOM: "spdx",
   147  		},
   148  		{
   149  			Name: "sbom-cyclonedx",
   150  			SBOM: "cyclonedx",
   151  		},
   152  		{
   153  			Name: "sbom-go.version-m",
   154  			SBOM: "go.version-m",
   155  		},
   156  		{
   157  			Name:      "base-image-is-not-index",
   158  			BaseImage: "alpine:latest@sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
   159  		},
   160  		{
   161  			Name:      "multiple-platforms",
   162  			Platforms: []string{"linux/amd64", "linux/arm64"},
   163  		},
   164  		{
   165  			Name:           "labels",
   166  			Labels:         map[string]string{"foo": "bar", "project": "{{.ProjectName}}"},
   167  			ExpectedLabels: map[string]string{"foo": "bar", "project": "test"},
   168  		},
   169  		{
   170  			Name:         "creation-time",
   171  			CreationTime: "1672531200",
   172  		},
   173  		{
   174  			Name:               "kodata-creation-time",
   175  			KoDataCreationTime: "1672531200",
   176  		},
   177  		{
   178  			Name: "tag-templates",
   179  			Tags: []string{
   180  				"{{if not .Prerelease }}{{.Version}}{{ end }}",
   181  				"   ", // empty
   182  			},
   183  		},
   184  		{
   185  			Name: "tag-template-eval-empty",
   186  			Tags: []string{
   187  				"{{.Version}}",
   188  				"{{if .Prerelease }}latest{{ end }}",
   189  			},
   190  		},
   191  	}
   192  
   193  	repository := fmt.Sprintf("%sgoreleasertest/testapp", registry)
   194  
   195  	for _, table := range table {
   196  		table := table
   197  		t.Run(table.Name, func(t *testing.T) {
   198  			if len(table.Tags) == 0 {
   199  				table.Tags = []string{table.Name}
   200  			}
   201  			ctx := testctx.NewWithCfg(config.Project{
   202  				ProjectName: "test",
   203  				Builds: []config.Build{
   204  					{
   205  						ID: "foo",
   206  						BuildDetails: config.BuildDetails{
   207  							Ldflags: []string{"-s", "-w"},
   208  							Flags:   []string{"-tags", "netgo"},
   209  							Env:     []string{"GOCACHE=" + t.TempDir()},
   210  						},
   211  					},
   212  				},
   213  				Kos: []config.Ko{
   214  					{
   215  						ID:                 "default",
   216  						Build:              "foo",
   217  						WorkingDir:         "./testdata/app/",
   218  						BaseImage:          table.BaseImage,
   219  						Repository:         repository,
   220  						Labels:             table.Labels,
   221  						Platforms:          table.Platforms,
   222  						Tags:               table.Tags,
   223  						CreationTime:       table.CreationTime,
   224  						KoDataCreationTime: table.KoDataCreationTime,
   225  						SBOM:               table.SBOM,
   226  						Bare:               true,
   227  					},
   228  				},
   229  			}, testctx.WithVersion("1.2.0"))
   230  
   231  			require.NoError(t, Pipe{}.Default(ctx))
   232  			require.NoError(t, Pipe{}.Publish(ctx))
   233  
   234  			manifests := ctx.Artifacts.Filter(artifact.ByType(artifact.DockerManifest)).List()
   235  			require.Len(t, manifests, 1)
   236  			require.NotEmpty(t, manifests[0].Name)
   237  			require.Equal(t, manifests[0].Name, manifests[0].Path)
   238  			require.NotEmpty(t, manifests[0].Extra[artifact.ExtraDigest])
   239  			require.Equal(t, "default", manifests[0].Extra[artifact.ExtraID])
   240  
   241  			tags, err := applyTemplate(ctx, table.Tags)
   242  			require.NoError(t, err)
   243  			tags = removeEmpty(tags)
   244  			require.Len(t, tags, 1)
   245  
   246  			ref, err := name.ParseReference(
   247  				fmt.Sprintf("%s:latest", repository),
   248  				name.Insecure,
   249  			)
   250  			require.NoError(t, err)
   251  			_, err = remote.Index(ref)
   252  			require.Error(t, err) // latest should not exist
   253  
   254  			ref, err = name.ParseReference(
   255  				fmt.Sprintf("%s:%s", repository, tags[0]),
   256  				name.Insecure,
   257  			)
   258  			require.NoError(t, err)
   259  
   260  			index, err := remote.Index(ref)
   261  			if len(table.Platforms) > 1 {
   262  				require.NoError(t, err)
   263  				imf, err := index.IndexManifest()
   264  				require.NoError(t, err)
   265  
   266  				platforms := make([]string, 0, len(imf.Manifests))
   267  				for _, mf := range imf.Manifests {
   268  					platforms = append(platforms, mf.Platform.String())
   269  				}
   270  				require.ElementsMatch(t, table.Platforms, platforms)
   271  			} else {
   272  				require.Error(t, err)
   273  			}
   274  
   275  			image, err := remote.Image(ref)
   276  			require.NoError(t, err)
   277  
   278  			digest, err := image.Digest()
   279  			require.NoError(t, err)
   280  
   281  			sbomRef, err := name.ParseReference(
   282  				fmt.Sprintf(
   283  					"%s:%s.sbom",
   284  					repository,
   285  					strings.Replace(digest.String(), ":", "-", 1),
   286  				),
   287  				name.Insecure,
   288  			)
   289  			require.NoError(t, err)
   290  
   291  			sbom, err := remote.Image(sbomRef)
   292  			if table.SBOM == "none" {
   293  				require.Error(t, err)
   294  			} else {
   295  				require.NoError(t, err)
   296  
   297  				layers, err := sbom.Layers()
   298  				require.NoError(t, err)
   299  				require.NotEmpty(t, layers)
   300  
   301  				mediaType, err := layers[0].MediaType()
   302  				require.NoError(t, err)
   303  
   304  				switch table.SBOM {
   305  				case "spdx", "":
   306  					require.Equal(t, "text/spdx+json", string(mediaType))
   307  				case "cyclonedx":
   308  					require.Equal(t, "application/vnd.cyclonedx+json", string(mediaType))
   309  				case "go.version-m":
   310  					require.Equal(t, "application/vnd.go.version-m", string(mediaType))
   311  				default:
   312  					require.Fail(t, "unknown SBOM type", table.SBOM)
   313  				}
   314  			}
   315  
   316  			configFile, err := image.ConfigFile()
   317  			require.NoError(t, err)
   318  			require.GreaterOrEqual(t, len(configFile.History), 3)
   319  
   320  			require.Equal(t, table.ExpectedLabels, configFile.Config.Labels)
   321  
   322  			var creationTime time.Time
   323  			if table.CreationTime != "" {
   324  				ct, err := strconv.ParseInt(table.CreationTime, 10, 64)
   325  				require.NoError(t, err)
   326  				creationTime = time.Unix(ct, 0).UTC()
   327  
   328  				require.Equal(t, creationTime, configFile.Created.Time.UTC())
   329  			}
   330  			require.Equal(t, creationTime, configFile.History[len(configFile.History)-1].Created.Time.UTC())
   331  
   332  			var koDataCreationTime time.Time
   333  			if table.KoDataCreationTime != "" {
   334  				kdct, err := strconv.ParseInt(table.KoDataCreationTime, 10, 64)
   335  				require.NoError(t, err)
   336  				koDataCreationTime = time.Unix(kdct, 0).UTC()
   337  			}
   338  			require.Equal(t, koDataCreationTime, configFile.History[len(configFile.History)-2].Created.Time.UTC())
   339  		})
   340  	}
   341  }
   342  
   343  func TestPublishPipeError(t *testing.T) {
   344  	makeCtx := func() *context.Context {
   345  		return testctx.NewWithCfg(config.Project{
   346  			Builds: []config.Build{
   347  				{
   348  					ID:   "foo",
   349  					Main: "./...",
   350  				},
   351  			},
   352  			Kos: []config.Ko{
   353  				{
   354  					ID:         "default",
   355  					Build:      "foo",
   356  					WorkingDir: "./testdata/app/",
   357  					Repository: "fakerepo:8080/",
   358  					Tags:       []string{"latest", "{{.Tag}}"},
   359  				},
   360  			},
   361  		}, testctx.WithCurrentTag("v1.0.0"))
   362  	}
   363  
   364  	t.Run("invalid base image", func(t *testing.T) {
   365  		ctx := makeCtx()
   366  		ctx.Config.Kos[0].BaseImage = "not a valid image hopefully"
   367  		require.NoError(t, Pipe{}.Default(ctx))
   368  		require.EqualError(t, Pipe{}.Publish(ctx), `build: fetching base image: could not parse reference: not a valid image hopefully`)
   369  	})
   370  
   371  	t.Run("invalid label tmpl", func(t *testing.T) {
   372  		ctx := makeCtx()
   373  		ctx.Config.Kos[0].Labels = map[string]string{"nope": "{{.Nope}}"}
   374  		require.NoError(t, Pipe{}.Default(ctx))
   375  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   376  	})
   377  
   378  	t.Run("invalid sbom", func(t *testing.T) {
   379  		ctx := makeCtx()
   380  		ctx.Config.Kos[0].SBOM = "nope"
   381  		require.NoError(t, Pipe{}.Default(ctx))
   382  		require.EqualError(t, Pipe{}.Publish(ctx), `makeBuilder: unknown sbom type: "nope"`)
   383  	})
   384  
   385  	t.Run("invalid build", func(t *testing.T) {
   386  		ctx := makeCtx()
   387  		ctx.Config.Kos[0].WorkingDir = t.TempDir()
   388  		require.NoError(t, Pipe{}.Default(ctx))
   389  		require.EqualError(
   390  			t, Pipe{}.Publish(ctx),
   391  			"build: build: go build: exit status 1: pattern ./...: directory prefix . does not contain main module or its selected dependencies\n",
   392  		)
   393  	})
   394  
   395  	t.Run("invalid tags tmpl", func(t *testing.T) {
   396  		ctx := makeCtx()
   397  		ctx.Config.Kos[0].Tags = []string{"{{.Nope}}"}
   398  		require.NoError(t, Pipe{}.Default(ctx))
   399  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   400  	})
   401  
   402  	t.Run("invalid creation time", func(t *testing.T) {
   403  		ctx := makeCtx()
   404  		ctx.Config.Kos[0].CreationTime = "nope"
   405  		require.NoError(t, Pipe{}.Default(ctx))
   406  		err := Pipe{}.Publish(ctx)
   407  		require.Error(t, err)
   408  		require.Contains(t, err.Error(), `strconv.ParseInt: parsing "nope": invalid syntax`)
   409  	})
   410  
   411  	t.Run("invalid creation time tmpl", func(t *testing.T) {
   412  		ctx := makeCtx()
   413  		ctx.Config.Kos[0].CreationTime = "{{.Nope}}"
   414  		require.NoError(t, Pipe{}.Default(ctx))
   415  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   416  	})
   417  
   418  	t.Run("invalid kodata creation time", func(t *testing.T) {
   419  		ctx := makeCtx()
   420  		ctx.Config.Kos[0].KoDataCreationTime = "nope"
   421  		require.NoError(t, Pipe{}.Default(ctx))
   422  		err := Pipe{}.Publish(ctx)
   423  		require.Error(t, err)
   424  		require.Contains(t, err.Error(), `strconv.ParseInt: parsing "nope": invalid syntax`)
   425  	})
   426  
   427  	t.Run("invalid kodata creation time tmpl", func(t *testing.T) {
   428  		ctx := makeCtx()
   429  		ctx.Config.Kos[0].KoDataCreationTime = "{{.Nope}}"
   430  		require.NoError(t, Pipe{}.Default(ctx))
   431  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   432  	})
   433  
   434  	t.Run("invalid env tmpl", func(t *testing.T) {
   435  		ctx := makeCtx()
   436  		ctx.Config.Builds[0].Env = []string{"{{.Nope}}"}
   437  		require.NoError(t, Pipe{}.Default(ctx))
   438  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   439  	})
   440  
   441  	t.Run("invalid ldflags tmpl", func(t *testing.T) {
   442  		ctx := makeCtx()
   443  		ctx.Config.Builds[0].Ldflags = []string{"{{.Nope}}"}
   444  		require.NoError(t, Pipe{}.Default(ctx))
   445  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   446  	})
   447  
   448  	t.Run("invalid flags tmpl", func(t *testing.T) {
   449  		ctx := makeCtx()
   450  		ctx.Config.Builds[0].Flags = []string{"{{.Nope}}"}
   451  		require.NoError(t, Pipe{}.Default(ctx))
   452  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   453  	})
   454  
   455  	t.Run("publish fail", func(t *testing.T) {
   456  		ctx := makeCtx()
   457  		require.NoError(t, Pipe{}.Default(ctx))
   458  		err := Pipe{}.Publish(ctx)
   459  		require.Error(t, err)
   460  		require.Contains(t, err.Error(), `publish: Get "https://fakerepo:8080/v2/": dial tcp:`)
   461  	})
   462  }
   463  
   464  func TestApplyTemplate(t *testing.T) {
   465  	t.Run("success", func(t *testing.T) {
   466  		foo, err := applyTemplate(testctx.NewWithCfg(config.Project{
   467  			Env: []string{"FOO=bar"},
   468  		}), []string{"{{ .Env.FOO }}"})
   469  		require.NoError(t, err)
   470  		require.Equal(t, []string{"bar"}, foo)
   471  	})
   472  	t.Run("error", func(t *testing.T) {
   473  		_, err := applyTemplate(testctx.New(), []string{"{{ .Nope}}"})
   474  		require.Error(t, err)
   475  	})
   476  }