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 }