1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-09-16 09:26:52 +02:00

feat: add ko support (#3653)

continuing the PR by @developer-guy 


- [x] should be a publisher, as it does publish the images it builds
every time
- [x] `Default` method does not work
- [x] the `fromConfig` thing should probably be on the defaults, too
- [x] wire `--skip-ko`
- [x] documentation
- [x] more tests
- [x] use same registry as docker tests does
- [ ] see if we can make the log output match goreleaser's
- [ ] ??

closes #2556
closes #3490

Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
Co-authored-by: actions-user <actions@github.com>
Co-authored-by: Jason Hall <jason@chainguard.dev>
Co-authored-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
This commit is contained in:
Carlos Alexandro Becker
2023-01-16 22:34:49 -03:00
committed by GitHub
parent 48f77f9ea4
commit 2450746e5c
15 changed files with 2170 additions and 40 deletions

View File

@@ -36,6 +36,7 @@ type releaseOpts struct {
skipAnnounce bool
skipSBOMCataloging bool
skipDocker bool
skipKo bool
skipBefore bool
rmDist bool
deprecated bool
@@ -77,6 +78,7 @@ func newReleaseCmd() *releaseCmd {
cmd.Flags().BoolVar(&root.opts.skipSign, "skip-sign", false, "Skips signing artifacts")
cmd.Flags().BoolVar(&root.opts.skipSBOMCataloging, "skip-sbom", false, "Skips cataloging artifacts")
cmd.Flags().BoolVar(&root.opts.skipDocker, "skip-docker", false, "Skips Docker Images/Manifests builds")
cmd.Flags().BoolVar(&root.opts.skipKo, "skip-ko", false, "Skips Ko builds")
cmd.Flags().BoolVar(&root.opts.skipBefore, "skip-before", false, "Skips global before hooks")
cmd.Flags().BoolVar(&root.opts.skipValidate, "skip-validate", false, "Skips git checks")
cmd.Flags().BoolVar(&root.opts.rmDist, "rm-dist", false, "Removes the dist folder")
@@ -137,6 +139,7 @@ func setupReleaseContext(ctx *context.Context, options releaseOpts) {
ctx.SkipSign = options.skipSign
ctx.SkipSBOMCataloging = options.skipSBOMCataloging
ctx.SkipDocker = options.skipDocker
ctx.SkipKo = options.skipKo
ctx.SkipBefore = options.skipBefore
ctx.RmDist = options.rmDist

80
go.mod
View File

@@ -6,6 +6,7 @@ require (
code.gitea.io/sdk/gitea v0.15.1
github.com/Masterminds/semver/v3 v3.2.0
github.com/atc0005/go-teams-notify/v2 v2.7.0
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04
github.com/caarlos0/ctrlc v1.2.0
github.com/caarlos0/env/v6 v6.10.1
github.com/caarlos0/go-reddit/v3 v3.0.1
@@ -13,12 +14,16 @@ require (
github.com/caarlos0/log v0.2.1
github.com/charmbracelet/keygen v0.3.0
github.com/charmbracelet/lipgloss v0.6.0
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08
github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb
github.com/dghubble/oauth1 v0.7.2
github.com/disgoorg/disgo v0.14.1
github.com/disgoorg/snowflake/v2 v2.0.1
github.com/distribution/distribution/v3 v3.0.0-20221021092657-c47a966fded8
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/google/go-containerregistry v0.11.0
github.com/google/go-github/v48 v48.2.0
github.com/google/ko v0.12.0
github.com/google/uuid v1.3.0
github.com/goreleaser/fileglob v1.3.0
github.com/goreleaser/nfpm/v2 v2.23.0
@@ -44,6 +49,7 @@ require (
golang.org/x/oauth2 v0.4.0
golang.org/x/sync v0.1.0
golang.org/x/text v0.6.0
golang.org/x/tools v0.2.0
gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v3 v3.0.1
)
@@ -56,6 +62,7 @@ require (
cloud.google.com/go/kms v1.7.0 // indirect
cloud.google.com/go/storage v1.28.0 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // indirect
@@ -64,8 +71,16 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 // indirect
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
@@ -73,6 +88,8 @@ require (
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go v1.44.151 // indirect
github.com/aws/aws-sdk-go-v2 v1.17.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 // indirect
@@ -84,6 +101,8 @@ require (
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19 // indirect
@@ -95,24 +114,43 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5 // indirect
github.com/aws/smithy-go v1.13.4 // indirect
github.com/aymanbagabas/go-osc52 v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3 // indirect
github.com/cavaliergopher/cpio v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dghubble/sling v1.4.0 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/disgoorg/json v1.0.0 // indirect
github.com/disgoorg/log v1.2.0 // indirect
github.com/docker/cli v20.10.14+incompatible // indirect
github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v20.10.21+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect
github.com/go-openapi/errors v0.20.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/runtime v0.24.2 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/validate v0.22.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
@@ -125,22 +163,29 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/goreleaser/chglog v0.2.2 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/go-version v1.2.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/iancoleman/orderedmap v0.2.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kevinburke/ssh_config v1.1.0 // indirect
github.com/klauspost/compress v1.15.13 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/letsencrypt/boulder v0.0.0-20220929215747-76583552c2be // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -148,28 +193,49 @@ require (
github.com/muesli/mango v0.1.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.13.1 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/sigstore/cosign v1.13.1 // indirect
github.com/sigstore/rekor v0.12.1-0.20220915152154-4bb6f441c1b2 // indirect
github.com/sigstore/sigstore v1.4.4 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.13.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
go.mongodb.org/mongo-driver v1.10.2 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/term v0.4.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.103.0 // indirect
@@ -178,6 +244,10 @@ require (
google.golang.org/grpc v1.51.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
sigs.k8s.io/kind v0.14.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

1264
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,6 @@ import (
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/require"
)
@@ -28,30 +26,8 @@ const (
func start(tb testing.TB) {
tb.Helper()
tb.Log("starting registries")
startRegistry(tb, "registry", registryPort)
startRegistry(tb, "alt_registry", altRegistryPort)
}
func startRegistry(tb testing.TB, name, port string) {
tb.Helper()
pool := testlib.MustDockerPool(tb)
testlib.MustKillContainer(tb, name)
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Name: name,
Repository: "registry",
Tag: "2",
PortBindings: map[docker.Port][]docker.PortBinding{
docker.Port("5000/tcp"): {{HostPort: port}},
},
}, func(hc *docker.HostConfig) {
hc.AutoRemove = true
})
require.NoError(tb, err)
tb.Cleanup(func() {
require.NoError(tb, pool.Purge(resource))
})
testlib.StartRegistry(tb, "registry", registryPort)
testlib.StartRegistry(tb, "alt_registry", altRegistryPort)
}
// TODO: this test is too big... split in smaller tests? Mainly the manifest ones...

350
internal/pipe/ko/ko.go Normal file
View File

@@ -0,0 +1,350 @@
// Package ko implements the pipe interface with the intent of
// building OCI compliant images with ko.
package ko
import (
stdctx "context"
"errors"
"fmt"
"io"
"path/filepath"
"sync"
"github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
"github.com/chrismellard/docker-credential-acr-env/pkg/credhelper"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/github"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/google"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/ko/pkg/build"
"github.com/google/ko/pkg/commands/options"
"github.com/google/ko/pkg/publish"
"github.com/goreleaser/goreleaser/internal/ids"
"github.com/goreleaser/goreleaser/internal/semerrgroup"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"golang.org/x/tools/go/packages"
)
const chainguardStatic = "cgr.dev/chainguard/static"
var (
baseImages sync.Map
amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard)))
azureKeychain authn.Keychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper())
keychain = authn.NewMultiKeychain(
amazonKeychain,
authn.DefaultKeychain,
google.Keychain,
github.Keychain,
azureKeychain,
)
errNoRepository = errors.New("ko: missing repository: please set either the repository field or a $KO_DOCKER_REPO environment variable")
)
// Pipe that build OCI compliant images with ko.
type Pipe struct{}
func (Pipe) String() string { return "ko" }
func (Pipe) Skip(ctx *context.Context) bool {
return ctx.SkipKo || len(ctx.Config.Kos) == 0
}
// Default sets the Pipes defaults.
func (Pipe) Default(ctx *context.Context) error {
ids := ids.New("kos")
for i := range ctx.Config.Kos {
ko := &ctx.Config.Kos[i]
if ko.ID == "" {
ko.ID = ctx.Config.ProjectName
}
if ko.Build == "" {
ko.Build = ko.ID
}
build, err := findBuild(ctx, *ko)
if err != nil {
return err
}
if len(ko.Ldflags) == 0 {
ko.Ldflags = build.Ldflags
}
if len(ko.Flags) == 0 {
ko.Flags = build.Flags
}
if len(ko.Env) == 0 {
ko.Env = build.Env
}
if ko.Main == "" {
ko.Main = build.Main
}
if ko.WorkingDir == "" {
ko.WorkingDir = build.Dir
}
if ko.BaseImage == "" {
ko.BaseImage = chainguardStatic
}
if len(ko.Platforms) == 0 {
ko.Platforms = []string{"linux/amd64"}
}
if len(ko.Tags) == 0 {
ko.Tags = []string{"latest"}
}
if ko.SBOM == "" {
ko.SBOM = "spdx"
}
if repo := ctx.Env["KO_DOCKER_REPO"]; repo != "" {
ko.Repository = repo
ko.RepositoryFromEnv = true
}
if ko.Repository == "" {
return errNoRepository
}
ids.Inc(ko.ID)
}
return ids.Validate()
}
// Publish executes the Pipe.
func (Pipe) Publish(ctx *context.Context) error {
g := semerrgroup.New(ctx.Parallelism)
for _, ko := range ctx.Config.Kos {
g.Go(doBuild(ctx, ko))
}
return g.Wait()
}
type buildOptions struct {
importPath string
main string
flags []string
env []string
imageRepo string
fromEnv bool
workingDir string
platforms []string
baseImage string
tags []string
sbom string
ldflags []string
bare bool
preserveImportPaths bool
baseImportPaths bool
}
func (o *buildOptions) makeBuilder(ctx *context.Context) (*build.Caching, error) {
buildOptions := []build.Option{
build.WithConfig(map[string]build.Config{
o.importPath: {
Ldflags: o.ldflags,
Flags: o.flags,
Main: o.main,
Env: o.env,
},
}),
build.WithPlatforms(o.platforms...),
build.WithBaseImages(func(ctx stdctx.Context, s string) (name.Reference, build.Result, error) {
ref, err := name.ParseReference(o.baseImage)
if err != nil {
return nil, nil, err
}
if cached, found := baseImages.Load(o.baseImage); found {
return ref, cached.(build.Result), nil
}
desc, err := remote.Get(
ref,
remote.WithAuthFromKeychain(keychain),
)
if err != nil {
return nil, nil, err
}
if desc.MediaType.IsImage() {
img, err := desc.Image()
baseImages.Store(o.baseImage, img)
return ref, img, err
}
if desc.MediaType.IsIndex() {
idx, err := desc.ImageIndex()
baseImages.Store(o.baseImage, idx)
return ref, idx, err
}
return nil, nil, fmt.Errorf("unexpected base image media type: %s", desc.MediaType)
}),
}
switch o.sbom {
case "spdx":
buildOptions = append(buildOptions, build.WithSPDX("devel"))
case "cyclonedx":
buildOptions = append(buildOptions, build.WithCycloneDX())
case "go.version-m":
buildOptions = append(buildOptions, build.WithGoVersionSBOM())
case "none":
// don't do anything.
default:
return nil, fmt.Errorf("unknown sbom type: %q", o.sbom)
}
b, err := build.NewGo(ctx, o.workingDir, buildOptions...)
if err != nil {
return nil, fmt.Errorf("newGo: %w", err)
}
return build.NewCaching(b)
}
func doBuild(ctx *context.Context, ko config.Ko) func() error {
return func() error {
opts, err := buildBuildOptions(ctx, ko)
if err != nil {
return err
}
b, err := opts.makeBuilder(ctx)
if err != nil {
return fmt.Errorf("makeBuilder: %w", err)
}
r, err := b.Build(ctx, opts.importPath)
if err != nil {
return fmt.Errorf("build: %w", err)
}
po := []publish.Option{publish.WithTags(opts.tags), publish.WithAuthFromKeychain(keychain)}
var repo string
if opts.fromEnv {
repo = opts.imageRepo
} else {
// image resource's `repo` takes precedence if set, and selects the
// `--bare` namer so the image is named exactly `repo`.
repo = opts.imageRepo
po = append(po, publish.WithNamer(options.MakeNamer(&options.PublishOptions{
DockerRepo: opts.imageRepo,
Bare: opts.bare,
PreserveImportPaths: opts.preserveImportPaths,
BaseImportPaths: opts.baseImportPaths,
})))
}
p, err := publish.NewDefault(repo, po...)
if err != nil {
return fmt.Errorf("newDefault: %w", err)
}
defer func() { _ = p.Close() }()
if _, err = p.Publish(ctx, r, opts.importPath); err != nil {
return fmt.Errorf("publish: %w", err)
}
if err := p.Close(); err != nil {
return fmt.Errorf("close: %w", err)
}
return nil
}
}
func findBuild(ctx *context.Context, ko config.Ko) (config.Build, error) {
for _, build := range ctx.Config.Builds {
if build.ID == ko.Build {
return build, nil
}
}
return config.Build{}, fmt.Errorf("no builds with id %q", ko.Build)
}
func buildBuildOptions(ctx *context.Context, cfg config.Ko) (*buildOptions, error) {
localImportPath := cfg.Main
dir := filepath.Clean(cfg.WorkingDir)
if dir == "." {
dir = ""
}
pkgs, err := packages.Load(&packages.Config{
Mode: packages.NeedName,
Dir: dir,
}, localImportPath)
if err != nil {
return nil, fmt.Errorf(
"ko: %s does not contain a valid local import path (%s) for directory (%s): %w",
cfg.ID, localImportPath, cfg.WorkingDir, err,
)
}
if len(pkgs) != 1 {
return nil, fmt.Errorf(
"ko: %s results in %d local packages, only 1 is expected",
cfg.ID, len(pkgs),
)
}
opts := &buildOptions{
importPath: pkgs[0].PkgPath,
workingDir: cfg.WorkingDir,
bare: cfg.Bare,
preserveImportPaths: cfg.PreserveImportPaths,
baseImportPaths: cfg.BaseImportPaths,
baseImage: cfg.BaseImage,
platforms: cfg.Platforms,
sbom: cfg.SBOM,
imageRepo: cfg.Repository,
fromEnv: cfg.RepositoryFromEnv,
}
tags, err := applyTemplate(ctx, cfg.Tags)
if err != nil {
return nil, err
}
opts.tags = tags
if len(cfg.Env) > 0 {
env, err := applyTemplate(ctx, cfg.Env)
if err != nil {
return nil, err
}
opts.env = env
}
if len(cfg.Flags) > 0 {
flags, err := applyTemplate(ctx, cfg.Flags)
if err != nil {
return nil, err
}
opts.flags = flags
}
if len(cfg.Ldflags) > 0 {
ldflags, err := applyTemplate(ctx, cfg.Ldflags)
if err != nil {
return nil, err
}
opts.ldflags = ldflags
}
return opts, nil
}
func applyTemplate(ctx *context.Context, templateable []string) ([]string, error) {
var templated []string
for _, t := range templateable {
tlf, err := tmpl.New(ctx).Apply(t)
if err != nil {
return nil, err
}
templated = append(templated, tlf)
}
return templated, nil
}

280
internal/pipe/ko/ko_test.go Normal file
View File

@@ -0,0 +1,280 @@
package ko
import (
"fmt"
"testing"
_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/stretchr/testify/require"
)
const (
registryPort = "5052"
registry = "localhost:5052/"
)
func TestDefault(t *testing.T) {
ctx := context.New(config.Project{
Env: []string{
"KO_DOCKER_REPO=" + registry,
"COSIGN_REPOSITORY=" + registry,
"LDFLAGS=foobar",
"FLAGS=barfoo",
"LE_ENV=test",
},
ProjectName: "test",
Builds: []config.Build{
{
ID: "test",
Dir: ".",
BuildDetails: config.BuildDetails{
Ldflags: []string{"{{.Env.LDFLAGS}}"},
Flags: []string{"{{.Env.FLAGS}}"},
Env: []string{"SOME_ENV={{.Env.LE_ENV}}"},
},
},
},
Kos: []config.Ko{
{},
},
})
require.NoError(t, Pipe{}.Default(ctx))
require.Equal(t, config.Ko{
ID: "test",
Build: "test",
BaseImage: chainguardStatic,
Repository: registry,
RepositoryFromEnv: true,
Platforms: []string{"linux/amd64"},
SBOM: "spdx",
Tags: []string{"latest"},
WorkingDir: ".",
Ldflags: []string{"{{.Env.LDFLAGS}}"},
Flags: []string{"{{.Env.FLAGS}}"},
Env: []string{"SOME_ENV={{.Env.LE_ENV}}"},
}, ctx.Config.Kos[0])
}
func TestDefaultNoImage(t *testing.T) {
ctx := context.New(config.Project{
ProjectName: "test",
Builds: []config.Build{
{
ID: "test",
},
},
Kos: []config.Ko{
{},
},
})
require.ErrorIs(t, Pipe{}.Default(ctx), errNoRepository)
}
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}
func TestSkip(t *testing.T) {
t.Run("skip ko set", func(t *testing.T) {
ctx := context.New(config.Project{
Kos: []config.Ko{{}},
})
ctx.SkipKo = true
require.True(t, Pipe{}.Skip(ctx))
})
t.Run("skip no kos", func(t *testing.T) {
ctx := context.New(config.Project{})
require.True(t, Pipe{}.Skip(ctx))
})
t.Run("dont skip", func(t *testing.T) {
ctx := context.New(config.Project{
Kos: []config.Ko{{}},
})
require.False(t, Pipe{}.Skip(ctx))
})
}
func TestPublishPipeNoMatchingBuild(t *testing.T) {
ctx := context.New(config.Project{
Builds: []config.Build{
{
ID: "doesnt matter",
},
},
Kos: []config.Ko{
{
ID: "default",
Build: "wont match nothing",
},
},
})
require.EqualError(t, Pipe{}.Default(ctx), `no builds with id "wont match nothing"`)
}
func TestPublishPipeSuccess(t *testing.T) {
testlib.StartRegistry(t, "ko_registry", registryPort)
table := []struct {
Name string
SBOM string
BaseImage string
Platforms []string
}{
{
Name: "sbom-spdx",
SBOM: "spdx",
},
{
Name: "sbom-none",
SBOM: "none",
},
{
Name: "sbom-cyclonedx",
SBOM: "cyclonedx",
},
{
Name: "sbom-go.version-m",
SBOM: "go.version-m",
},
{
Name: "base-image-is-not-index",
BaseImage: "alpine:latest@sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
},
{
Name: "multiple-platforms",
Platforms: []string{"linux/amd64", "linux/arm64"},
},
}
for _, table := range table {
t.Run(table.Name, func(t *testing.T) {
ctx := context.New(config.Project{
Builds: []config.Build{
{
ID: "foo",
BuildDetails: config.BuildDetails{
Ldflags: []string{"-s", "-w"},
Flags: []string{"-tags", "netgo"},
Env: []string{"GOCACHE=" + t.TempDir()},
},
},
},
Kos: []config.Ko{
{
ID: "default",
Build: "foo",
WorkingDir: "./testdata/app/",
BaseImage: table.BaseImage,
Repository: fmt.Sprintf("%s/goreleasertest", registry),
Platforms: table.Platforms,
Tags: []string{table.Name},
SBOM: table.SBOM,
},
},
})
require.NoError(t, Pipe{}.Default(ctx))
require.NoError(t, Pipe{}.Publish(ctx))
})
}
}
func TestPublishPipeError(t *testing.T) {
makeCtx := func() *context.Context {
ctx := context.New(config.Project{
Builds: []config.Build{
{
ID: "foo",
Main: "./...",
},
},
Kos: []config.Ko{
{
ID: "default",
Build: "foo",
WorkingDir: "./testdata/app/",
Repository: "fakerepo:8080/",
Tags: []string{"latest", "{{.Tag}}"},
},
},
})
ctx.Git.CurrentTag = "v1.0.0"
return ctx
}
t.Run("invalid base image", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].BaseImage = "not a valid image hopefully"
require.NoError(t, Pipe{}.Default(ctx))
require.EqualError(t, Pipe{}.Publish(ctx), `build: could not parse reference: not a valid image hopefully`)
})
t.Run("invalid sbom", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].SBOM = "nope"
require.NoError(t, Pipe{}.Default(ctx))
require.EqualError(t, Pipe{}.Publish(ctx), `makeBuilder: unknown sbom type: "nope"`)
})
t.Run("invalid build", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].WorkingDir = t.TempDir()
require.NoError(t, Pipe{}.Default(ctx))
require.EqualError(t, Pipe{}.Publish(ctx), `build: exit status 1`)
})
t.Run("invalid tags tmpl", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].Tags = []string{"{{.Nope}}"}
require.NoError(t, Pipe{}.Default(ctx))
testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
})
t.Run("invalid env tmpl", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Builds[0].Env = []string{"{{.Nope}}"}
require.NoError(t, Pipe{}.Default(ctx))
testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
})
t.Run("invalid ldflags tmpl", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Builds[0].Ldflags = []string{"{{.Nope}}"}
require.NoError(t, Pipe{}.Default(ctx))
testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
})
t.Run("invalid flags tmpl", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Builds[0].Flags = []string{"{{.Nope}}"}
require.NoError(t, Pipe{}.Default(ctx))
testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
})
t.Run("publish fail", func(t *testing.T) {
ctx := makeCtx()
require.NoError(t, Pipe{}.Default(ctx))
err := Pipe{}.Publish(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), `publish: writing sbom: Get "https://fakerepo:8080/v2/": dial tcp:`)
})
}
func TestApplyTemplate(t *testing.T) {
t.Run("success", func(t *testing.T) {
foo, err := applyTemplate(context.New(config.Project{
Env: []string{"FOO=bar"},
}), []string{"{{ .Env.FOO }}"})
require.NoError(t, err)
require.Equal(t, []string{"bar"}, foo)
})
t.Run("error", func(t *testing.T) {
_, err := applyTemplate(context.New(config.Project{}), []string{"{{ .Nope}}"})
require.Error(t, err)
})
}

3
internal/pipe/ko/testdata/app/go.mod vendored Normal file
View File

@@ -0,0 +1,3 @@
module testapp
go 1.19

5
internal/pipe/ko/testdata/app/main.go vendored Normal file
View File

@@ -0,0 +1,5 @@
package main
func main() {
println("hello")
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/chocolatey"
"github.com/goreleaser/goreleaser/internal/pipe/custompublishers"
"github.com/goreleaser/goreleaser/internal/pipe/docker"
"github.com/goreleaser/goreleaser/internal/pipe/ko"
"github.com/goreleaser/goreleaser/internal/pipe/krew"
"github.com/goreleaser/goreleaser/internal/pipe/milestone"
"github.com/goreleaser/goreleaser/internal/pipe/release"
@@ -40,6 +41,7 @@ var publishers = []Publisher{
custompublishers.Pipe{},
docker.Pipe{},
docker.ManifestPipe{},
ko.Pipe{},
sign.DockerPipe{},
snapcraft.Pipe{},
// This should be one of the last steps

View File

@@ -2,8 +2,11 @@ package testlib
import (
"sync"
"testing"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/require"
)
var (
@@ -41,3 +44,27 @@ func MustKillContainer(f Fataler, name string) {
type Fataler interface {
Fatal(args ...any)
}
// StartRegistry starts a registry with the given name in the given port, and
// sets up its deletion on test.Cleanup.
func StartRegistry(tb testing.TB, name, port string) {
tb.Helper()
pool := MustDockerPool(tb)
MustKillContainer(tb, name)
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Name: name,
Repository: "registry",
Tag: "2",
PortBindings: map[docker.Port][]docker.PortBinding{
docker.Port("5000/tcp"): {{HostPort: port}},
},
}, func(hc *docker.HostConfig) {
hc.AutoRemove = true
})
require.NoError(tb, err)
tb.Cleanup(func() {
require.NoError(tb, pool.Purge(resource))
})
}

View File

@@ -199,6 +199,26 @@ type Krew struct {
SkipUpload string `yaml:"skip_upload,omitempty" json:"skip_upload,omitempty" jsonschema:"oneof_type=string;boolean"`
}
// Ko contains the ko section
type Ko struct {
ID string `yaml:"id,omitempty" json:"id,omitempty"`
Build string `yaml:"build,omitempty" json:"build,omitempty"`
Main string `yaml:"main,omitempty" json:"main,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
BaseImage string `yaml:"base_image,omitempty" json:"base_image,omitempty"`
Repository string `yaml:"repository,omitempty" json:"repository,omitempty"`
RepositoryFromEnv bool `yaml:"-" json:"-"`
Platforms []string `yaml:"platforms,omitempty" json:"platforms,omitempty"`
Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"`
SBOM string `yaml:"sbom,omitempty" json:"sbom,omitempty"`
Ldflags []string `yaml:"ldflags,omitempty" json:"ldflags,omitempty"`
Flags []string `yaml:"flags,omitempty" json:"flags,omitempty"`
Env []string `yaml:"env,omitempty" json:"env,omitempty"`
Bare bool `yaml:"bare,omitempty" json:"bare,omitempty"`
PreserveImportPaths bool `yaml:"preserve_import_paths,omitempty" json:"preserve_import_paths,omitempty"`
BaseImportPaths bool `yaml:"base_import_paths,omitempty" json:"base_import_paths,omitempty"`
}
// Scoop contains the scoop.sh section.
type Scoop struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
@@ -916,6 +936,7 @@ type Project struct {
Brews []Homebrew `yaml:"brews,omitempty" json:"brews,omitempty"`
AURs []AUR `yaml:"aurs,omitempty" json:"aurs,omitempty"`
Krews []Krew `yaml:"krews,omitempty" json:"krews,omitempty"`
Kos []Ko `yaml:"kos,omitempty" json:"kos,omitempty"`
Scoop Scoop `yaml:"scoop,omitempty" json:"scoop,omitempty"`
Builds []Build `yaml:"builds,omitempty" json:"builds,omitempty"`
Archives []Archive `yaml:"archives,omitempty" json:"archives,omitempty"`

View File

@@ -96,6 +96,7 @@ type Context struct {
SkipSign bool
SkipValidate bool
SkipSBOMCataloging bool
SkipKo bool
SkipDocker bool
SkipBefore bool
RmDist bool

View File

@@ -16,6 +16,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/discord"
"github.com/goreleaser/goreleaser/internal/pipe/docker"
"github.com/goreleaser/goreleaser/internal/pipe/gomod"
"github.com/goreleaser/goreleaser/internal/pipe/ko"
"github.com/goreleaser/goreleaser/internal/pipe/krew"
"github.com/goreleaser/goreleaser/internal/pipe/linkedin"
"github.com/goreleaser/goreleaser/internal/pipe/mastodon"
@@ -74,6 +75,7 @@ var Defaulters = []Defaulter{
aur.Pipe{},
brew.Pipe{},
krew.Pipe{},
ko.Pipe{},
scoop.Pipe{},
discord.Pipe{},
reddit.Pipe{},

View File

@@ -0,0 +1,143 @@
# Docker Images with Ko
> Since v1.15.
You can also use [ko][] to build and publish Docker container images.
Please notice that ko will build your binary again.
That shouldn't increase the release times too much, as it'll use the same build
options as the [build][] pipe when possible, so the results will probably be cached.
!!! warning
Ko only runs on the publish phase, so it might be a bit hard to test — you
might need to push to a fake repository (or a fake tag) when playing around
with its configuration.
```yaml
# .goreleaser.yaml
kos:
-
# ID of this image.
id: foo
# Build ID that should be used to import the build settings.
build: build-id
# Main path to build.
#
# Defaults to the build's main.
main: ./cmd/...
# Working directory used to build.
#
# Defaults to the build's dir.
working_dir: .
# Base image to publish to use.
#
# Defaults to cgr.dev/chainguard/static.
base_image: alpine
# Repository to push to.
#
# Defaults to the value of $KO_DOCKER_REPO.
repository: ghcr.io/foo/bar
# Platforms to build and publish.
#
# Defaults to linux/amd64.
platforms:
- linux/amd64
- linux/arm64
# Tag templates to build and push.
#
# Defaults to `latest`.
tags:
- latest
- '{{.Tag}}'
# SBOM format to use.
#
# Defaults to spdx.
# Valid options are: spdx, cyclonedx, go.version-m and none.
sbom: none
# Ldflags to use on build.
#
# Defaults to the build's ldflags.
ldflags:
- foo
- bar
# Flags to use on build.
#
# Defaults to the build's flags.
flags:
- foo
- bar
# Env to use on build.
#
# Defaults to the build's env.
env:
- FOO=bar
- SOMETHING=value
# Bare uses a tag on the KO_DOCKER_REPO without anything additional.
#
# Defaults to false.
bare: true
# Whether to preserve the full import path after the repository name.
#
# Defaults to false.
preserve_import_paths: true
# Whether to use the base path without the MD5 hash after the repository name.
#
# Defaults to false.
base_import_paths: true
```
Refer to [ko's project page][ko] for more information.
## Example
Here's a minimal example:
```yaml
# .goreleaser.yml
before:
hooks:
- go mod tidy
builds:
- env: [ "CGO_ENABLED=1" ]
binary: test
goos:
- darwin
- linux
goarch:
- amd64
- arch64
kos:
- repository: ghcr.io/caarlos0/test-ko
tags:
- '{{.Version}}'
- latest
bare: true
preserve_import_paths: false
platforms:
- linux/amd64
- linux/arm64
```
This will build the binaries for `linux/arm64`, `linux/amd64`, `darwin/amd64`
and `darwin/arm64`, as well as the Docker images and manifest for Linux.
[ko]: https://ko.build
[build]: /customization/build/

View File

@@ -106,6 +106,7 @@ nav:
- customization/chocolatey.md
- customization/docker.md
- customization/docker_manifest.md
- customization/ko.md
- customization/sbom.md
- Signing:
- Checksums and artifacts: customization/sign.md