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