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 }