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 }