github.com/goreleaser/goreleaser@v1.25.1/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/goreleaser/goreleaser/internal/artifact"
    15  	"github.com/goreleaser/goreleaser/internal/skips"
    16  	"github.com/goreleaser/goreleaser/internal/testctx"
    17  	"github.com/goreleaser/goreleaser/internal/testlib"
    18  	"github.com/goreleaser/goreleaser/pkg/config"
    19  	"github.com/goreleaser/goreleaser/pkg/context"
    20  	"github.com/stretchr/testify/require"
    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 TestKoValidateMainPathIssue4382(t *testing.T) {
   344  	// testing the validation of the main path directly to cover many cases
   345  	require.NoError(t, validateMainPath(""))
   346  	require.NoError(t, validateMainPath("."))
   347  	require.NoError(t, validateMainPath("./..."))
   348  	require.NoError(t, validateMainPath("./app"))
   349  	require.NoError(t, validateMainPath("../../../..."))
   350  	require.NoError(t, validateMainPath("../../app/"))
   351  	require.NoError(t, validateMainPath("./testdata/app/main"))
   352  	require.NoError(t, validateMainPath("./testdata/app/folder.with.dots"))
   353  
   354  	require.ErrorIs(t, validateMainPath("app/"), errInvalidMainPath)
   355  	require.ErrorIs(t, validateMainPath("/src/"), errInvalidMainPath)
   356  	require.ErrorIs(t, validateMainPath("/src/app"), errInvalidMainPath)
   357  	require.ErrorIs(t, validateMainPath("./testdata/app/main.go"), errInvalidMainPath)
   358  
   359  	// testing with real context
   360  	ctxOk := testctx.NewWithCfg(config.Project{
   361  		Builds: []config.Build{
   362  			{
   363  				ID:   "foo",
   364  				Main: "./...",
   365  			},
   366  		},
   367  		Kos: []config.Ko{
   368  			{
   369  				ID:         "default",
   370  				Build:      "foo",
   371  				Repository: "fakerepo",
   372  			},
   373  		},
   374  	})
   375  	require.NoError(t, Pipe{}.Default(ctxOk))
   376  
   377  	ctxWithInvalidMainPath := testctx.NewWithCfg(config.Project{
   378  		Builds: []config.Build{
   379  			{
   380  				ID:   "foo",
   381  				Main: "/some/non/relative/path",
   382  			},
   383  		},
   384  		Kos: []config.Ko{
   385  			{
   386  				ID:         "default",
   387  				Build:      "foo",
   388  				Repository: "fakerepo",
   389  			},
   390  		},
   391  	})
   392  	require.ErrorIs(t, Pipe{}.Default(ctxWithInvalidMainPath), errInvalidMainPath)
   393  }
   394  
   395  func TestPublishPipeError(t *testing.T) {
   396  	makeCtx := func() *context.Context {
   397  		return testctx.NewWithCfg(config.Project{
   398  			Builds: []config.Build{
   399  				{
   400  					ID:   "foo",
   401  					Main: "./...",
   402  				},
   403  			},
   404  			Kos: []config.Ko{
   405  				{
   406  					ID:         "default",
   407  					Build:      "foo",
   408  					WorkingDir: "./testdata/app/",
   409  					Repository: "fakerepo:8080/",
   410  					Tags:       []string{"latest", "{{.Tag}}"},
   411  				},
   412  			},
   413  		}, testctx.WithCurrentTag("v1.0.0"))
   414  	}
   415  
   416  	t.Run("invalid base image", func(t *testing.T) {
   417  		ctx := makeCtx()
   418  		ctx.Config.Kos[0].BaseImage = "not a valid image hopefully"
   419  		require.NoError(t, Pipe{}.Default(ctx))
   420  		require.EqualError(t, Pipe{}.Publish(ctx), `build: fetching base image: could not parse reference: not a valid image hopefully`)
   421  	})
   422  
   423  	t.Run("invalid label tmpl", func(t *testing.T) {
   424  		ctx := makeCtx()
   425  		ctx.Config.Kos[0].Labels = map[string]string{"nope": "{{.Nope}}"}
   426  		require.NoError(t, Pipe{}.Default(ctx))
   427  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   428  	})
   429  
   430  	t.Run("invalid sbom", func(t *testing.T) {
   431  		ctx := makeCtx()
   432  		ctx.Config.Kos[0].SBOM = "nope"
   433  		require.NoError(t, Pipe{}.Default(ctx))
   434  		require.EqualError(t, Pipe{}.Publish(ctx), `makeBuilder: unknown sbom type: "nope"`)
   435  	})
   436  
   437  	t.Run("invalid build", func(t *testing.T) {
   438  		ctx := makeCtx()
   439  		ctx.Config.Kos[0].WorkingDir = t.TempDir()
   440  		require.NoError(t, Pipe{}.Default(ctx))
   441  		require.EqualError(
   442  			t, Pipe{}.Publish(ctx),
   443  			"build: build: go build: exit status 1: pattern ./...: directory prefix . does not contain main module or its selected dependencies\n",
   444  		)
   445  	})
   446  
   447  	t.Run("invalid tags tmpl", func(t *testing.T) {
   448  		ctx := makeCtx()
   449  		ctx.Config.Kos[0].Tags = []string{"{{.Nope}}"}
   450  		require.NoError(t, Pipe{}.Default(ctx))
   451  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   452  	})
   453  
   454  	t.Run("invalid creation time", func(t *testing.T) {
   455  		ctx := makeCtx()
   456  		ctx.Config.Kos[0].CreationTime = "nope"
   457  		require.NoError(t, Pipe{}.Default(ctx))
   458  		err := Pipe{}.Publish(ctx)
   459  		require.ErrorContains(t, err, `strconv.ParseInt: parsing "nope": invalid syntax`)
   460  	})
   461  
   462  	t.Run("invalid creation time tmpl", func(t *testing.T) {
   463  		ctx := makeCtx()
   464  		ctx.Config.Kos[0].CreationTime = "{{.Nope}}"
   465  		require.NoError(t, Pipe{}.Default(ctx))
   466  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   467  	})
   468  
   469  	t.Run("invalid kodata creation time", func(t *testing.T) {
   470  		ctx := makeCtx()
   471  		ctx.Config.Kos[0].KoDataCreationTime = "nope"
   472  		require.NoError(t, Pipe{}.Default(ctx))
   473  		err := Pipe{}.Publish(ctx)
   474  		require.ErrorContains(t, err, `strconv.ParseInt: parsing "nope": invalid syntax`)
   475  	})
   476  
   477  	t.Run("invalid kodata creation time tmpl", func(t *testing.T) {
   478  		ctx := makeCtx()
   479  		ctx.Config.Kos[0].KoDataCreationTime = "{{.Nope}}"
   480  		require.NoError(t, Pipe{}.Default(ctx))
   481  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   482  	})
   483  
   484  	t.Run("invalid env tmpl", func(t *testing.T) {
   485  		ctx := makeCtx()
   486  		ctx.Config.Builds[0].Env = []string{"{{.Nope}}"}
   487  		require.NoError(t, Pipe{}.Default(ctx))
   488  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   489  	})
   490  
   491  	t.Run("invalid ldflags tmpl", func(t *testing.T) {
   492  		ctx := makeCtx()
   493  		ctx.Config.Builds[0].Ldflags = []string{"{{.Nope}}"}
   494  		require.NoError(t, Pipe{}.Default(ctx))
   495  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   496  	})
   497  
   498  	t.Run("invalid flags tmpl", func(t *testing.T) {
   499  		ctx := makeCtx()
   500  		ctx.Config.Builds[0].Flags = []string{"{{.Nope}}"}
   501  		require.NoError(t, Pipe{}.Default(ctx))
   502  		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
   503  	})
   504  
   505  	t.Run("publish fail", func(t *testing.T) {
   506  		ctx := makeCtx()
   507  		require.NoError(t, Pipe{}.Default(ctx))
   508  		err := Pipe{}.Publish(ctx)
   509  		require.Error(t, err)
   510  		require.Contains(t, err.Error(), `publish: Get "https://fakerepo:8080/v2/": dial tcp:`)
   511  	})
   512  }
   513  
   514  func TestApplyTemplate(t *testing.T) {
   515  	t.Run("success", func(t *testing.T) {
   516  		foo, err := applyTemplate(testctx.NewWithCfg(config.Project{
   517  			Env: []string{"FOO=bar"},
   518  		}), []string{"{{ .Env.FOO }}"})
   519  		require.NoError(t, err)
   520  		require.Equal(t, []string{"bar"}, foo)
   521  	})
   522  	t.Run("error", func(t *testing.T) {
   523  		_, err := applyTemplate(testctx.New(), []string{"{{ .Nope}}"})
   524  		require.Error(t, err)
   525  	})
   526  }