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