github.com/goreleaser/goreleaser@v1.25.1/internal/pipe/krew/krew.go (about)

     1  // Package krew implements Piper and Publisher, providing krew plugin manifest
     2  // creation and upload to a repository (aka krew plugin index).
     3  //
     4  // nolint:tagliatelle
     5  package krew
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"reflect"
    14  	"sort"
    15  	"strings"
    16  
    17  	"github.com/caarlos0/log"
    18  	"github.com/goreleaser/goreleaser/internal/artifact"
    19  	"github.com/goreleaser/goreleaser/internal/client"
    20  	"github.com/goreleaser/goreleaser/internal/commitauthor"
    21  	"github.com/goreleaser/goreleaser/internal/deprecate"
    22  	"github.com/goreleaser/goreleaser/internal/pipe"
    23  	"github.com/goreleaser/goreleaser/internal/tmpl"
    24  	"github.com/goreleaser/goreleaser/internal/yaml"
    25  	"github.com/goreleaser/goreleaser/pkg/config"
    26  	"github.com/goreleaser/goreleaser/pkg/context"
    27  )
    28  
    29  const (
    30  	krewConfigExtra = "KrewConfig"
    31  	manifestsFolder = "plugins"
    32  	kind            = "Plugin"
    33  	apiVersion      = "krew.googlecontainertools.github.com/v1alpha2"
    34  )
    35  
    36  var ErrNoArchivesFound = errors.New("no archives found")
    37  
    38  // Pipe for krew manifest deployment.
    39  type Pipe struct{}
    40  
    41  func (Pipe) String() string                 { return "krew plugin manifest" }
    42  func (Pipe) ContinueOnError() bool          { return true }
    43  func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Krews) == 0 }
    44  
    45  func (Pipe) Default(ctx *context.Context) error {
    46  	for i := range ctx.Config.Krews {
    47  		krew := &ctx.Config.Krews[i]
    48  
    49  		krew.CommitAuthor = commitauthor.Default(krew.CommitAuthor)
    50  		if krew.CommitMessageTemplate == "" {
    51  			krew.CommitMessageTemplate = "Krew manifest update for {{ .ProjectName }} version {{ .Tag }}"
    52  		}
    53  		if krew.Name == "" {
    54  			krew.Name = ctx.Config.ProjectName
    55  		}
    56  		if krew.Goamd64 == "" {
    57  			krew.Goamd64 = "v1"
    58  		}
    59  		if !reflect.DeepEqual(krew.Index, config.RepoRef{}) {
    60  			krew.Repository = krew.Index
    61  			deprecate.Notice(ctx, "krews.index")
    62  		}
    63  	}
    64  
    65  	return nil
    66  }
    67  
    68  func (Pipe) Run(ctx *context.Context) error {
    69  	cli, err := client.NewReleaseClient(ctx)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	return runAll(ctx, cli)
    75  }
    76  
    77  func runAll(ctx *context.Context, cli client.ReleaseURLTemplater) error {
    78  	for _, krew := range ctx.Config.Krews {
    79  		err := doRun(ctx, krew, cli)
    80  		if err != nil {
    81  			return err
    82  		}
    83  	}
    84  	return nil
    85  }
    86  
    87  func doRun(ctx *context.Context, krew config.Krew, cl client.ReleaseURLTemplater) error {
    88  	if krew.Name == "" {
    89  		return pipe.Skip("krew: manifest name is not set")
    90  	}
    91  	if krew.Description == "" {
    92  		return fmt.Errorf("krew: manifest description is not set")
    93  	}
    94  	if krew.ShortDescription == "" {
    95  		return fmt.Errorf("krew: manifest short description is not set")
    96  	}
    97  
    98  	filters := []artifact.Filter{
    99  		artifact.Or(
   100  			artifact.ByGoos("darwin"),
   101  			artifact.ByGoos("linux"),
   102  			artifact.ByGoos("windows"),
   103  		),
   104  		artifact.Or(
   105  			artifact.And(
   106  				artifact.ByGoarch("amd64"),
   107  				artifact.ByGoamd64(krew.Goamd64),
   108  			),
   109  			artifact.ByGoarch("arm64"),
   110  			artifact.ByGoarch("all"),
   111  			artifact.And(
   112  				artifact.ByGoarch("arm"),
   113  				artifact.ByGoarm(krew.Goarm),
   114  			),
   115  		),
   116  		artifact.ByType(artifact.UploadableArchive),
   117  		artifact.OnlyReplacingUnibins,
   118  	}
   119  	if len(krew.IDs) > 0 {
   120  		filters = append(filters, artifact.ByIDs(krew.IDs...))
   121  	}
   122  
   123  	archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
   124  	if len(archives) == 0 {
   125  		return ErrNoArchivesFound
   126  	}
   127  
   128  	krew, err := templateFields(ctx, krew)
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	content, err := buildmanifest(ctx, krew, cl, archives)
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	filename := krew.Name + ".yaml"
   139  	yamlPath := filepath.Join(ctx.Config.Dist, "krew", filename)
   140  	if err := os.MkdirAll(filepath.Dir(yamlPath), 0o755); err != nil {
   141  		return err
   142  	}
   143  	log.WithField("manifest", yamlPath).Info("writing")
   144  	if err := os.WriteFile(yamlPath, []byte("# This file was generated by GoReleaser. DO NOT EDIT.\n"+content), 0o644); err != nil { //nolint: gosec
   145  		return fmt.Errorf("failed to write krew manifest: %w", err)
   146  	}
   147  
   148  	ctx.Artifacts.Add(&artifact.Artifact{
   149  		Name: filename,
   150  		Path: yamlPath,
   151  		Type: artifact.KrewPluginManifest,
   152  		Extra: map[string]interface{}{
   153  			krewConfigExtra: krew,
   154  		},
   155  	})
   156  
   157  	return nil
   158  }
   159  
   160  func templateFields(ctx *context.Context, krew config.Krew) (config.Krew, error) {
   161  	t := tmpl.New(ctx)
   162  
   163  	if err := t.ApplyAll(
   164  		&krew.Name,
   165  		&krew.Homepage,
   166  		&krew.Description,
   167  		&krew.Caveats,
   168  		&krew.ShortDescription,
   169  	); err != nil {
   170  		return config.Krew{}, err
   171  	}
   172  
   173  	return krew, nil
   174  }
   175  
   176  func buildmanifest(
   177  	ctx *context.Context,
   178  	krew config.Krew,
   179  	client client.ReleaseURLTemplater,
   180  	artifacts []*artifact.Artifact,
   181  ) (string, error) {
   182  	data, err := manifestFor(ctx, krew, client, artifacts)
   183  	if err != nil {
   184  		return "", err
   185  	}
   186  	return doBuildManifest(data)
   187  }
   188  
   189  func doBuildManifest(data Manifest) (string, error) {
   190  	out, err := yaml.Marshal(data)
   191  	if err != nil {
   192  		return "", fmt.Errorf("krew: failed to marshal yaml: %w", err)
   193  	}
   194  	return string(out), nil
   195  }
   196  
   197  func manifestFor(
   198  	ctx *context.Context,
   199  	cfg config.Krew,
   200  	cl client.ReleaseURLTemplater,
   201  	artifacts []*artifact.Artifact,
   202  ) (Manifest, error) {
   203  	result := Manifest{
   204  		APIVersion: apiVersion,
   205  		Kind:       kind,
   206  		Metadata: Metadata{
   207  			Name: cfg.Name,
   208  		},
   209  		Spec: Spec{
   210  			Homepage:         cfg.Homepage,
   211  			Version:          "v" + ctx.Version,
   212  			ShortDescription: cfg.ShortDescription,
   213  			Description:      cfg.Description,
   214  			Caveats:          cfg.Caveats,
   215  		},
   216  	}
   217  
   218  	for _, art := range artifacts {
   219  		sum, err := art.Checksum("sha256")
   220  		if err != nil {
   221  			return result, err
   222  		}
   223  
   224  		if cfg.URLTemplate == "" {
   225  			url, err := cl.ReleaseURLTemplate(ctx)
   226  			if err != nil {
   227  				return result, err
   228  			}
   229  			cfg.URLTemplate = url
   230  		}
   231  		url, err := tmpl.New(ctx).WithArtifact(art).Apply(cfg.URLTemplate)
   232  		if err != nil {
   233  			return result, err
   234  		}
   235  
   236  		goarch := []string{art.Goarch}
   237  		if art.Goarch == "all" {
   238  			goarch = []string{"amd64", "arm64"}
   239  		}
   240  
   241  		for _, arch := range goarch {
   242  			bins := artifact.ExtraOr(*art, artifact.ExtraBinaries, []string{})
   243  			if len(bins) != 1 {
   244  				return result, fmt.Errorf("krew: only one binary per archive allowed, got %d on %q", len(bins), art.Name)
   245  			}
   246  			result.Spec.Platforms = append(result.Spec.Platforms, Platform{
   247  				Bin:    bins[0],
   248  				URI:    url,
   249  				Sha256: sum,
   250  				Selector: Selector{
   251  					MatchLabels: MatchLabels{
   252  						Os:   art.Goos,
   253  						Arch: arch,
   254  					},
   255  				},
   256  			})
   257  		}
   258  	}
   259  
   260  	sort.Slice(result.Spec.Platforms, func(i, j int) bool {
   261  		return result.Spec.Platforms[i].URI > result.Spec.Platforms[j].URI
   262  	})
   263  
   264  	return result, nil
   265  }
   266  
   267  // Publish krew manifest.
   268  func (Pipe) Publish(ctx *context.Context) error {
   269  	cli, err := client.New(ctx)
   270  	if err != nil {
   271  		return err
   272  	}
   273  	return publishAll(ctx, cli)
   274  }
   275  
   276  func publishAll(ctx *context.Context, cli client.Client) error {
   277  	skips := pipe.SkipMemento{}
   278  	for _, manifest := range ctx.Artifacts.Filter(artifact.ByType(artifact.KrewPluginManifest)).List() {
   279  		err := doPublish(ctx, manifest, cli)
   280  		if err != nil && pipe.IsSkip(err) {
   281  			skips.Remember(err)
   282  			continue
   283  		}
   284  		if err != nil {
   285  			return err
   286  		}
   287  	}
   288  	return skips.Evaluate()
   289  }
   290  
   291  func doPublish(ctx *context.Context, manifest *artifact.Artifact, cl client.Client) error {
   292  	cfg, err := artifact.Extra[config.Krew](*manifest, krewConfigExtra)
   293  	if err != nil {
   294  		return err
   295  	}
   296  
   297  	if strings.TrimSpace(cfg.SkipUpload) == "true" {
   298  		return pipe.Skip("krews.skip_upload is set")
   299  	}
   300  
   301  	if strings.TrimSpace(cfg.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
   302  		return pipe.Skip("prerelease detected with 'auto' upload, skipping krew publish")
   303  	}
   304  
   305  	ref, err := client.TemplateRef(tmpl.New(ctx).Apply, cfg.Repository)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	cfg.Repository = ref
   310  	repo := client.RepoFromRef(cfg.Repository)
   311  	gpath := buildManifestPath(manifestsFolder, manifest.Name)
   312  
   313  	msg, err := tmpl.New(ctx).Apply(cfg.CommitMessageTemplate)
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	author, err := commitauthor.Get(ctx, cfg.CommitAuthor)
   319  	if err != nil {
   320  		return err
   321  	}
   322  
   323  	content, err := os.ReadFile(manifest.Path)
   324  	if err != nil {
   325  		return err
   326  	}
   327  
   328  	if cfg.Repository.Git.URL != "" {
   329  		return client.NewGitUploadClient(repo.Branch).
   330  			CreateFile(ctx, author, repo, content, gpath, msg)
   331  	}
   332  
   333  	cl, err = client.NewIfToken(ctx, cl, cfg.Repository.Token)
   334  	if err != nil {
   335  		return err
   336  	}
   337  
   338  	base := client.Repo{
   339  		Name:   cfg.Repository.PullRequest.Base.Name,
   340  		Owner:  cfg.Repository.PullRequest.Base.Owner,
   341  		Branch: cfg.Repository.PullRequest.Base.Branch,
   342  	}
   343  
   344  	// try to sync branch
   345  	fscli, ok := cl.(client.ForkSyncer)
   346  	if ok && cfg.Repository.PullRequest.Enabled {
   347  		if err := fscli.SyncFork(ctx, repo, base); err != nil {
   348  			log.WithError(err).Warn("could not sync fork")
   349  		}
   350  	}
   351  
   352  	if err := cl.CreateFile(ctx, author, repo, content, gpath, msg); err != nil {
   353  		return err
   354  	}
   355  
   356  	if !cfg.Repository.PullRequest.Enabled {
   357  		log.Debug("krews.pull_request disabled")
   358  		return nil
   359  	}
   360  
   361  	log.Info("krews.pull_request enabled, creating a PR")
   362  	pcl, ok := cl.(client.PullRequestOpener)
   363  	if !ok {
   364  		return fmt.Errorf("client does not support pull requests")
   365  	}
   366  
   367  	return pcl.OpenPullRequest(ctx, base, repo, msg, cfg.Repository.PullRequest.Draft)
   368  }
   369  
   370  func buildManifestPath(folder, filename string) string {
   371  	return path.Join(folder, filename)
   372  }
   373  
   374  type Manifest struct {
   375  	APIVersion string   `yaml:"apiVersion,omitempty"`
   376  	Kind       string   `yaml:"kind,omitempty"`
   377  	Metadata   Metadata `yaml:"metadata,omitempty"`
   378  	Spec       Spec     `yaml:"spec,omitempty"`
   379  }
   380  
   381  type Metadata struct {
   382  	Name string `yaml:"name,omitempty"`
   383  }
   384  
   385  type MatchLabels struct {
   386  	Os   string `yaml:"os,omitempty"`
   387  	Arch string `yaml:"arch,omitempty"`
   388  }
   389  
   390  type Selector struct {
   391  	MatchLabels MatchLabels `yaml:"matchLabels,omitempty"`
   392  }
   393  
   394  type Platform struct {
   395  	Bin      string   `yaml:"bin,omitempty"`
   396  	URI      string   `yaml:"uri,omitempty"`
   397  	Sha256   string   `yaml:"sha256,omitempty"`
   398  	Selector Selector `yaml:"selector,omitempty"`
   399  }
   400  
   401  type Spec struct {
   402  	Version          string     `yaml:"version,omitempty"`
   403  	Platforms        []Platform `yaml:"platforms,omitempty"`
   404  	ShortDescription string     `yaml:"shortDescription,omitempty"`
   405  	Homepage         string     `yaml:"homepage,omitempty"`
   406  	Caveats          string     `yaml:"caveats,omitempty"`
   407  	Description      string     `yaml:"description,omitempty"`
   408  }