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 }