github.com/windmeup/goreleaser@v1.21.95/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/windmeup/goreleaser/internal/artifact" 19 "github.com/windmeup/goreleaser/internal/client" 20 "github.com/windmeup/goreleaser/internal/commitauthor" 21 "github.com/windmeup/goreleaser/internal/deprecate" 22 "github.com/windmeup/goreleaser/internal/pipe" 23 "github.com/windmeup/goreleaser/internal/tmpl" 24 "github.com/windmeup/goreleaser/internal/yaml" 25 "github.com/windmeup/goreleaser/pkg/config" 26 "github.com/windmeup/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 if !cfg.Repository.PullRequest.Enabled { 339 return cl.CreateFile(ctx, author, repo, content, gpath, msg) 340 } 341 342 log.Info("brews.pull_request enabled, creating a PR") 343 pcl, ok := cl.(client.PullRequestOpener) 344 if !ok { 345 return fmt.Errorf("client does not support pull requests") 346 } 347 348 if err := cl.CreateFile(ctx, author, repo, content, gpath, msg); err != nil { 349 return err 350 } 351 352 return pcl.OpenPullRequest(ctx, client.Repo{ 353 Name: cfg.Repository.PullRequest.Base.Name, 354 Owner: cfg.Repository.PullRequest.Base.Owner, 355 Branch: cfg.Repository.PullRequest.Base.Branch, 356 }, repo, msg, cfg.Repository.PullRequest.Draft) 357 } 358 359 func buildManifestPath(folder, filename string) string { 360 return path.Join(folder, filename) 361 } 362 363 type Manifest struct { 364 APIVersion string `yaml:"apiVersion,omitempty"` 365 Kind string `yaml:"kind,omitempty"` 366 Metadata Metadata `yaml:"metadata,omitempty"` 367 Spec Spec `yaml:"spec,omitempty"` 368 } 369 370 type Metadata struct { 371 Name string `yaml:"name,omitempty"` 372 } 373 374 type MatchLabels struct { 375 Os string `yaml:"os,omitempty"` 376 Arch string `yaml:"arch,omitempty"` 377 } 378 379 type Selector struct { 380 MatchLabels MatchLabels `yaml:"matchLabels,omitempty"` 381 } 382 383 type Platform struct { 384 Bin string `yaml:"bin,omitempty"` 385 URI string `yaml:"uri,omitempty"` 386 Sha256 string `yaml:"sha256,omitempty"` 387 Selector Selector `yaml:"selector,omitempty"` 388 } 389 390 type Spec struct { 391 Version string `yaml:"version,omitempty"` 392 Platforms []Platform `yaml:"platforms,omitempty"` 393 ShortDescription string `yaml:"shortDescription,omitempty"` 394 Homepage string `yaml:"homepage,omitempty"` 395 Caveats string `yaml:"caveats,omitempty"` 396 Description string `yaml:"description,omitempty"` 397 }