mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-22 04:08:49 +02:00
708cd8904d
this makes ko run on snapshot builds, too. the image will be `goreleaser.ko.local:[your tags]`, not sure if we can change this, seems like we can't. also fixed a small doc error around it, as well as added a new test to cover this. closes #4683 --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
470 lines
12 KiB
Go
470 lines
12 KiB
Go
// 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"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"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"
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"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/v2/internal/artifact"
|
|
"github.com/goreleaser/goreleaser/v2/internal/deprecate"
|
|
"github.com/goreleaser/goreleaser/v2/internal/ids"
|
|
"github.com/goreleaser/goreleaser/v2/internal/semerrgroup"
|
|
"github.com/goreleaser/goreleaser/v2/internal/skips"
|
|
"github.com/goreleaser/goreleaser/v2/internal/tmpl"
|
|
"github.com/goreleaser/goreleaser/v2/pkg/config"
|
|
"github.com/goreleaser/goreleaser/v2/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")
|
|
errInvalidMainPath = errors.New("ko: invalid Main path: ko.main (or build.main if ko.main is not set) should be a relative path")
|
|
errInvalidMainGoPath = errors.New("ko: invalid Main path: your path should point to a directory instead of a .go file")
|
|
)
|
|
|
|
// 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 skips.Any(ctx, skips.Ko) || 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 err := validateMainPath(ko.Main); err != nil {
|
|
return err
|
|
}
|
|
|
|
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"}
|
|
}
|
|
|
|
switch ko.SBOM {
|
|
case "cyclonedx", "go.version-m":
|
|
deprecate.Notice(ctx, "kos.sbom")
|
|
ko.SBOM = "none"
|
|
case "":
|
|
ko.SBOM = "spdx"
|
|
}
|
|
|
|
if repo := ctx.Env["KO_DOCKER_REPO"]; repo != "" {
|
|
ko.Repository = repo
|
|
}
|
|
|
|
if ko.Repository == "" {
|
|
return errNoRepository
|
|
}
|
|
|
|
ids.Inc(ko.ID)
|
|
}
|
|
return ids.Validate()
|
|
}
|
|
|
|
func (p Pipe) Run(ctx *context.Context) error {
|
|
if ctx.Snapshot {
|
|
// publish actually handles pushing to the local docker daemon when
|
|
// snapshot is true.
|
|
return p.Publish(ctx)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
workingDir string
|
|
platforms []string
|
|
baseImage string
|
|
labels map[string]string
|
|
tags []string
|
|
creationTime *v1.Time
|
|
koDataCreationTime *v1.Time
|
|
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(_ stdctx.Context, _ 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)
|
|
}),
|
|
}
|
|
if o.creationTime != nil {
|
|
buildOptions = append(buildOptions, build.WithCreationTime(*o.creationTime))
|
|
}
|
|
if o.koDataCreationTime != nil {
|
|
buildOptions = append(buildOptions, build.WithKoDataCreationTime(*o.koDataCreationTime))
|
|
}
|
|
for k, v := range o.labels {
|
|
buildOptions = append(buildOptions, build.WithLabel(k, v))
|
|
}
|
|
switch o.sbom {
|
|
case "spdx":
|
|
buildOptions = append(buildOptions, build.WithSPDX("devel"))
|
|
case "none":
|
|
buildOptions = append(buildOptions, build.WithDisabledSBOM())
|
|
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 := &options.PublishOptions{
|
|
DockerRepo: opts.imageRepo,
|
|
Bare: opts.bare,
|
|
PreserveImportPaths: opts.preserveImportPaths,
|
|
BaseImportPaths: opts.baseImportPaths,
|
|
Tags: opts.tags,
|
|
Local: ctx.Snapshot,
|
|
LocalDomain: "goreleaser.ko.local",
|
|
}
|
|
var p publish.Interface
|
|
if ctx.Snapshot {
|
|
p, err = publish.NewDaemon(
|
|
options.MakeNamer(po),
|
|
opts.tags,
|
|
publish.WithLocalDomain(po.LocalDomain),
|
|
)
|
|
} else {
|
|
p, err = publish.NewDefault(
|
|
opts.imageRepo,
|
|
publish.WithTags(opts.tags),
|
|
publish.WithNamer(options.MakeNamer(po)),
|
|
publish.WithAuthFromKeychain(keychain),
|
|
)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("newPublisher: %w", err)
|
|
}
|
|
defer func() { _ = p.Close() }()
|
|
ref, err := p.Publish(ctx, r, opts.importPath)
|
|
if err != nil {
|
|
return fmt.Errorf("publish: %w", err)
|
|
}
|
|
if err := p.Close(); err != nil {
|
|
return fmt.Errorf("close: %w", err)
|
|
}
|
|
|
|
art := &artifact.Artifact{
|
|
Type: artifact.DockerManifest,
|
|
Name: ref.Name(),
|
|
Path: ref.Name(),
|
|
Extra: map[string]interface{}{},
|
|
}
|
|
if ko.ID != "" {
|
|
art.Extra[artifact.ExtraID] = ko.ID
|
|
}
|
|
if digest := ref.Context().Digest(ref.Identifier()).DigestStr(); digest != "" {
|
|
art.Extra[artifact.ExtraDigest] = digest
|
|
}
|
|
ctx.Artifacts.Add(art)
|
|
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,
|
|
}
|
|
|
|
tags, err := applyTemplate(ctx, cfg.Tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts.tags = removeEmpty(tags)
|
|
|
|
if cfg.CreationTime != "" {
|
|
creationTime, err := getTimeFromTemplate(ctx, cfg.CreationTime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts.creationTime = creationTime
|
|
}
|
|
|
|
if cfg.KoDataCreationTime != "" {
|
|
koDataCreationTime, err := getTimeFromTemplate(ctx, cfg.KoDataCreationTime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts.koDataCreationTime = koDataCreationTime
|
|
}
|
|
|
|
if len(cfg.Labels) > 0 {
|
|
opts.labels = make(map[string]string, len(cfg.Labels))
|
|
for k, v := range cfg.Labels {
|
|
tv, err := tmpl.New(ctx).Apply(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts.labels[k] = tv
|
|
}
|
|
}
|
|
|
|
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 removeEmpty(strs []string) []string {
|
|
var res []string
|
|
for _, s := range strs {
|
|
if strings.TrimSpace(s) == "" {
|
|
continue
|
|
}
|
|
res = append(res, s)
|
|
}
|
|
return res
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func getTimeFromTemplate(ctx *context.Context, t string) (*v1.Time, error) {
|
|
epoch, err := tmpl.New(ctx).Apply(t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
seconds, err := strconv.ParseInt(epoch, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &v1.Time{Time: time.Unix(seconds, 0)}, nil
|
|
}
|
|
|
|
func validateMainPath(path string) error {
|
|
// if the path is empty, it's probably fine as ko will use the default value
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
if matched, _ := regexp.MatchString(`^\.?(\.\/[^\/]?.*)?$`, path); !matched {
|
|
return errInvalidMainPath
|
|
}
|
|
// paths sure can have dots in them, but if the path ends in .go, it's propably a file that one misundertood as a valid value
|
|
if strings.HasSuffix(path, ".go") {
|
|
return errInvalidMainGoPath
|
|
}
|
|
return nil
|
|
}
|