github.com/windmeup/goreleaser@v1.21.95/internal/pipe/ko/ko.go (about)

     1  // Package ko implements the pipe interface with the intent of
     2  // building OCI compliant images with ko.
     3  package ko
     4  
     5  import (
     6  	stdctx "context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"path/filepath"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
    17  	"github.com/chrismellard/docker-credential-acr-env/pkg/credhelper"
    18  	"github.com/google/go-containerregistry/pkg/authn"
    19  	"github.com/google/go-containerregistry/pkg/authn/github"
    20  	"github.com/google/go-containerregistry/pkg/name"
    21  	v1 "github.com/google/go-containerregistry/pkg/v1"
    22  	"github.com/google/go-containerregistry/pkg/v1/google"
    23  	"github.com/google/go-containerregistry/pkg/v1/remote"
    24  	"github.com/google/ko/pkg/build"
    25  	"github.com/google/ko/pkg/commands/options"
    26  	"github.com/google/ko/pkg/publish"
    27  	"github.com/windmeup/goreleaser/internal/artifact"
    28  	"github.com/windmeup/goreleaser/internal/ids"
    29  	"github.com/windmeup/goreleaser/internal/semerrgroup"
    30  	"github.com/windmeup/goreleaser/internal/skips"
    31  	"github.com/windmeup/goreleaser/internal/tmpl"
    32  	"github.com/windmeup/goreleaser/pkg/config"
    33  	"github.com/windmeup/goreleaser/pkg/context"
    34  	"golang.org/x/tools/go/packages"
    35  )
    36  
    37  const chainguardStatic = "cgr.dev/chainguard/static"
    38  
    39  var (
    40  	baseImages     sync.Map
    41  	amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard)))
    42  	azureKeychain  authn.Keychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper())
    43  	keychain                      = authn.NewMultiKeychain(
    44  		amazonKeychain,
    45  		authn.DefaultKeychain,
    46  		google.Keychain,
    47  		github.Keychain,
    48  		azureKeychain,
    49  	)
    50  
    51  	errNoRepository = errors.New("ko: missing repository: please set either the repository field or a $KO_DOCKER_REPO environment variable")
    52  )
    53  
    54  // Pipe that build OCI compliant images with ko.
    55  type Pipe struct{}
    56  
    57  func (Pipe) String() string { return "ko" }
    58  func (Pipe) Skip(ctx *context.Context) bool {
    59  	return skips.Any(ctx, skips.Ko) || len(ctx.Config.Kos) == 0
    60  }
    61  
    62  // Default sets the Pipes defaults.
    63  func (Pipe) Default(ctx *context.Context) error {
    64  	ids := ids.New("kos")
    65  	for i := range ctx.Config.Kos {
    66  		ko := &ctx.Config.Kos[i]
    67  		if ko.ID == "" {
    68  			ko.ID = ctx.Config.ProjectName
    69  		}
    70  
    71  		if ko.Build == "" {
    72  			ko.Build = ko.ID
    73  		}
    74  
    75  		build, err := findBuild(ctx, *ko)
    76  		if err != nil {
    77  			return err
    78  		}
    79  
    80  		if len(ko.Ldflags) == 0 {
    81  			ko.Ldflags = build.Ldflags
    82  		}
    83  
    84  		if len(ko.Flags) == 0 {
    85  			ko.Flags = build.Flags
    86  		}
    87  
    88  		if len(ko.Env) == 0 {
    89  			ko.Env = build.Env
    90  		}
    91  
    92  		if ko.Main == "" {
    93  			ko.Main = build.Main
    94  		}
    95  
    96  		if ko.WorkingDir == "" {
    97  			ko.WorkingDir = build.Dir
    98  		}
    99  
   100  		if ko.BaseImage == "" {
   101  			ko.BaseImage = chainguardStatic
   102  		}
   103  
   104  		if len(ko.Platforms) == 0 {
   105  			ko.Platforms = []string{"linux/amd64"}
   106  		}
   107  
   108  		if len(ko.Tags) == 0 {
   109  			ko.Tags = []string{"latest"}
   110  		}
   111  
   112  		if ko.SBOM == "" {
   113  			ko.SBOM = "spdx"
   114  		}
   115  
   116  		if repo := ctx.Env["KO_DOCKER_REPO"]; repo != "" {
   117  			ko.Repository = repo
   118  		}
   119  
   120  		if ko.Repository == "" {
   121  			return errNoRepository
   122  		}
   123  
   124  		ids.Inc(ko.ID)
   125  	}
   126  	return ids.Validate()
   127  }
   128  
   129  // Publish executes the Pipe.
   130  func (Pipe) Publish(ctx *context.Context) error {
   131  	g := semerrgroup.New(ctx.Parallelism)
   132  	for _, ko := range ctx.Config.Kos {
   133  		g.Go(doBuild(ctx, ko))
   134  	}
   135  	return g.Wait()
   136  }
   137  
   138  type buildOptions struct {
   139  	importPath          string
   140  	main                string
   141  	flags               []string
   142  	env                 []string
   143  	imageRepo           string
   144  	workingDir          string
   145  	platforms           []string
   146  	baseImage           string
   147  	labels              map[string]string
   148  	tags                []string
   149  	creationTime        *v1.Time
   150  	koDataCreationTime  *v1.Time
   151  	sbom                string
   152  	ldflags             []string
   153  	bare                bool
   154  	preserveImportPaths bool
   155  	baseImportPaths     bool
   156  }
   157  
   158  func (o *buildOptions) makeBuilder(ctx *context.Context) (*build.Caching, error) {
   159  	buildOptions := []build.Option{
   160  		build.WithConfig(map[string]build.Config{
   161  			o.importPath: {
   162  				Ldflags: o.ldflags,
   163  				Flags:   o.flags,
   164  				Main:    o.main,
   165  				Env:     o.env,
   166  			},
   167  		}),
   168  		build.WithPlatforms(o.platforms...),
   169  		build.WithBaseImages(func(ctx stdctx.Context, s string) (name.Reference, build.Result, error) {
   170  			ref, err := name.ParseReference(o.baseImage)
   171  			if err != nil {
   172  				return nil, nil, err
   173  			}
   174  
   175  			if cached, found := baseImages.Load(o.baseImage); found {
   176  				return ref, cached.(build.Result), nil
   177  			}
   178  
   179  			desc, err := remote.Get(
   180  				ref,
   181  				remote.WithAuthFromKeychain(keychain),
   182  			)
   183  			if err != nil {
   184  				return nil, nil, err
   185  			}
   186  			if desc.MediaType.IsImage() {
   187  				img, err := desc.Image()
   188  				baseImages.Store(o.baseImage, img)
   189  				return ref, img, err
   190  			}
   191  			if desc.MediaType.IsIndex() {
   192  				idx, err := desc.ImageIndex()
   193  				baseImages.Store(o.baseImage, idx)
   194  				return ref, idx, err
   195  			}
   196  			return nil, nil, fmt.Errorf("unexpected base image media type: %s", desc.MediaType)
   197  		}),
   198  	}
   199  	if o.creationTime != nil {
   200  		buildOptions = append(buildOptions, build.WithCreationTime(*o.creationTime))
   201  	}
   202  	if o.koDataCreationTime != nil {
   203  		buildOptions = append(buildOptions, build.WithKoDataCreationTime(*o.koDataCreationTime))
   204  	}
   205  	for k, v := range o.labels {
   206  		buildOptions = append(buildOptions, build.WithLabel(k, v))
   207  	}
   208  	switch o.sbom {
   209  	case "spdx":
   210  		buildOptions = append(buildOptions, build.WithSPDX("devel"))
   211  	case "cyclonedx":
   212  		buildOptions = append(buildOptions, build.WithCycloneDX())
   213  	case "go.version-m":
   214  		buildOptions = append(buildOptions, build.WithGoVersionSBOM())
   215  	case "none":
   216  		buildOptions = append(buildOptions, build.WithDisabledSBOM())
   217  	default:
   218  		return nil, fmt.Errorf("unknown sbom type: %q", o.sbom)
   219  	}
   220  
   221  	b, err := build.NewGo(ctx, o.workingDir, buildOptions...)
   222  	if err != nil {
   223  		return nil, fmt.Errorf("newGo: %w", err)
   224  	}
   225  	return build.NewCaching(b)
   226  }
   227  
   228  func doBuild(ctx *context.Context, ko config.Ko) func() error {
   229  	return func() error {
   230  		opts, err := buildBuildOptions(ctx, ko)
   231  		if err != nil {
   232  			return err
   233  		}
   234  
   235  		b, err := opts.makeBuilder(ctx)
   236  		if err != nil {
   237  			return fmt.Errorf("makeBuilder: %w", err)
   238  		}
   239  		r, err := b.Build(ctx, opts.importPath)
   240  		if err != nil {
   241  			return fmt.Errorf("build: %w", err)
   242  		}
   243  
   244  		po := []publish.Option{publish.WithTags(opts.tags), publish.WithNamer(options.MakeNamer(&options.PublishOptions{
   245  			DockerRepo:          opts.imageRepo,
   246  			Bare:                opts.bare,
   247  			PreserveImportPaths: opts.preserveImportPaths,
   248  			BaseImportPaths:     opts.baseImportPaths,
   249  			Tags:                opts.tags,
   250  		})), publish.WithAuthFromKeychain(keychain)}
   251  
   252  		p, err := publish.NewDefault(opts.imageRepo, po...)
   253  		if err != nil {
   254  			return fmt.Errorf("newDefault: %w", err)
   255  		}
   256  		defer func() { _ = p.Close() }()
   257  		ref, err := p.Publish(ctx, r, opts.importPath)
   258  		if err != nil {
   259  			return fmt.Errorf("publish: %w", err)
   260  		}
   261  		if err := p.Close(); err != nil {
   262  			return fmt.Errorf("close: %w", err)
   263  		}
   264  
   265  		art := &artifact.Artifact{
   266  			Type:  artifact.DockerManifest,
   267  			Name:  ref.Name(),
   268  			Path:  ref.Name(),
   269  			Extra: map[string]interface{}{},
   270  		}
   271  		if ko.ID != "" {
   272  			art.Extra[artifact.ExtraID] = ko.ID
   273  		}
   274  		if digest := ref.Context().Digest(ref.Identifier()).DigestStr(); digest != "" {
   275  			art.Extra[artifact.ExtraDigest] = digest
   276  		}
   277  		ctx.Artifacts.Add(art)
   278  		return nil
   279  	}
   280  }
   281  
   282  func findBuild(ctx *context.Context, ko config.Ko) (config.Build, error) {
   283  	for _, build := range ctx.Config.Builds {
   284  		if build.ID == ko.Build {
   285  			return build, nil
   286  		}
   287  	}
   288  	return config.Build{}, fmt.Errorf("no builds with id %q", ko.Build)
   289  }
   290  
   291  func buildBuildOptions(ctx *context.Context, cfg config.Ko) (*buildOptions, error) {
   292  	localImportPath := cfg.Main
   293  
   294  	dir := filepath.Clean(cfg.WorkingDir)
   295  	if dir == "." {
   296  		dir = ""
   297  	}
   298  
   299  	pkgs, err := packages.Load(&packages.Config{
   300  		Mode: packages.NeedName,
   301  		Dir:  dir,
   302  	}, localImportPath)
   303  	if err != nil {
   304  		return nil, fmt.Errorf(
   305  			"ko: %s does not contain a valid local import path (%s) for directory (%s): %w",
   306  			cfg.ID, localImportPath, cfg.WorkingDir, err,
   307  		)
   308  	}
   309  
   310  	if len(pkgs) != 1 {
   311  		return nil, fmt.Errorf(
   312  			"ko: %s results in %d local packages, only 1 is expected",
   313  			cfg.ID, len(pkgs),
   314  		)
   315  	}
   316  
   317  	opts := &buildOptions{
   318  		importPath:          pkgs[0].PkgPath,
   319  		workingDir:          cfg.WorkingDir,
   320  		bare:                cfg.Bare,
   321  		preserveImportPaths: cfg.PreserveImportPaths,
   322  		baseImportPaths:     cfg.BaseImportPaths,
   323  		baseImage:           cfg.BaseImage,
   324  		platforms:           cfg.Platforms,
   325  		sbom:                cfg.SBOM,
   326  		imageRepo:           cfg.Repository,
   327  	}
   328  
   329  	tags, err := applyTemplate(ctx, cfg.Tags)
   330  	if err != nil {
   331  		return nil, err
   332  	}
   333  	opts.tags = removeEmpty(tags)
   334  
   335  	if cfg.CreationTime != "" {
   336  		creationTime, err := getTimeFromTemplate(ctx, cfg.CreationTime)
   337  		if err != nil {
   338  			return nil, err
   339  		}
   340  		opts.creationTime = creationTime
   341  	}
   342  
   343  	if cfg.KoDataCreationTime != "" {
   344  		koDataCreationTime, err := getTimeFromTemplate(ctx, cfg.KoDataCreationTime)
   345  		if err != nil {
   346  			return nil, err
   347  		}
   348  		opts.koDataCreationTime = koDataCreationTime
   349  	}
   350  
   351  	if len(cfg.Labels) > 0 {
   352  		opts.labels = make(map[string]string, len(cfg.Labels))
   353  		for k, v := range cfg.Labels {
   354  			tv, err := tmpl.New(ctx).Apply(v)
   355  			if err != nil {
   356  				return nil, err
   357  			}
   358  			opts.labels[k] = tv
   359  		}
   360  	}
   361  
   362  	if len(cfg.Env) > 0 {
   363  		env, err := applyTemplate(ctx, cfg.Env)
   364  		if err != nil {
   365  			return nil, err
   366  		}
   367  		opts.env = env
   368  	}
   369  
   370  	if len(cfg.Flags) > 0 {
   371  		flags, err := applyTemplate(ctx, cfg.Flags)
   372  		if err != nil {
   373  			return nil, err
   374  		}
   375  		opts.flags = flags
   376  	}
   377  
   378  	if len(cfg.Ldflags) > 0 {
   379  		ldflags, err := applyTemplate(ctx, cfg.Ldflags)
   380  		if err != nil {
   381  			return nil, err
   382  		}
   383  		opts.ldflags = ldflags
   384  	}
   385  	return opts, nil
   386  }
   387  
   388  func removeEmpty(strs []string) []string {
   389  	var res []string
   390  	for _, s := range strs {
   391  		if strings.TrimSpace(s) == "" {
   392  			continue
   393  		}
   394  		res = append(res, s)
   395  	}
   396  	return res
   397  }
   398  
   399  func applyTemplate(ctx *context.Context, templateable []string) ([]string, error) {
   400  	var templated []string
   401  	for _, t := range templateable {
   402  		tlf, err := tmpl.New(ctx).Apply(t)
   403  		if err != nil {
   404  			return nil, err
   405  		}
   406  		templated = append(templated, tlf)
   407  	}
   408  	return templated, nil
   409  }
   410  
   411  func getTimeFromTemplate(ctx *context.Context, t string) (*v1.Time, error) {
   412  	epoch, err := tmpl.New(ctx).Apply(t)
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	seconds, err := strconv.ParseInt(epoch, 10, 64)
   418  	if err != nil {
   419  		return nil, err
   420  	}
   421  	return &v1.Time{Time: time.Unix(seconds, 0)}, nil
   422  }