github.com/windmeup/goreleaser@v1.21.95/internal/pipe/ko/ko.go (about) 1 // Package ko implements the pipe interface with the intent of 2 // building OCI compliant images with ko. 3 package ko 4 5 import ( 6 stdctx "context" 7 "errors" 8 "fmt" 9 "io" 10 "path/filepath" 11 "strconv" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" 17 "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper" 18 "github.com/google/go-containerregistry/pkg/authn" 19 "github.com/google/go-containerregistry/pkg/authn/github" 20 "github.com/google/go-containerregistry/pkg/name" 21 v1 "github.com/google/go-containerregistry/pkg/v1" 22 "github.com/google/go-containerregistry/pkg/v1/google" 23 "github.com/google/go-containerregistry/pkg/v1/remote" 24 "github.com/google/ko/pkg/build" 25 "github.com/google/ko/pkg/commands/options" 26 "github.com/google/ko/pkg/publish" 27 "github.com/windmeup/goreleaser/internal/artifact" 28 "github.com/windmeup/goreleaser/internal/ids" 29 "github.com/windmeup/goreleaser/internal/semerrgroup" 30 "github.com/windmeup/goreleaser/internal/skips" 31 "github.com/windmeup/goreleaser/internal/tmpl" 32 "github.com/windmeup/goreleaser/pkg/config" 33 "github.com/windmeup/goreleaser/pkg/context" 34 "golang.org/x/tools/go/packages" 35 ) 36 37 const chainguardStatic = "cgr.dev/chainguard/static" 38 39 var ( 40 baseImages sync.Map 41 amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard))) 42 azureKeychain authn.Keychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper()) 43 keychain = authn.NewMultiKeychain( 44 amazonKeychain, 45 authn.DefaultKeychain, 46 google.Keychain, 47 github.Keychain, 48 azureKeychain, 49 ) 50 51 errNoRepository = errors.New("ko: missing repository: please set either the repository field or a $KO_DOCKER_REPO environment variable") 52 ) 53 54 // Pipe that build OCI compliant images with ko. 55 type Pipe struct{} 56 57 func (Pipe) String() string { return "ko" } 58 func (Pipe) Skip(ctx *context.Context) bool { 59 return skips.Any(ctx, skips.Ko) || len(ctx.Config.Kos) == 0 60 } 61 62 // Default sets the Pipes defaults. 63 func (Pipe) Default(ctx *context.Context) error { 64 ids := ids.New("kos") 65 for i := range ctx.Config.Kos { 66 ko := &ctx.Config.Kos[i] 67 if ko.ID == "" { 68 ko.ID = ctx.Config.ProjectName 69 } 70 71 if ko.Build == "" { 72 ko.Build = ko.ID 73 } 74 75 build, err := findBuild(ctx, *ko) 76 if err != nil { 77 return err 78 } 79 80 if len(ko.Ldflags) == 0 { 81 ko.Ldflags = build.Ldflags 82 } 83 84 if len(ko.Flags) == 0 { 85 ko.Flags = build.Flags 86 } 87 88 if len(ko.Env) == 0 { 89 ko.Env = build.Env 90 } 91 92 if ko.Main == "" { 93 ko.Main = build.Main 94 } 95 96 if ko.WorkingDir == "" { 97 ko.WorkingDir = build.Dir 98 } 99 100 if ko.BaseImage == "" { 101 ko.BaseImage = chainguardStatic 102 } 103 104 if len(ko.Platforms) == 0 { 105 ko.Platforms = []string{"linux/amd64"} 106 } 107 108 if len(ko.Tags) == 0 { 109 ko.Tags = []string{"latest"} 110 } 111 112 if ko.SBOM == "" { 113 ko.SBOM = "spdx" 114 } 115 116 if repo := ctx.Env["KO_DOCKER_REPO"]; repo != "" { 117 ko.Repository = repo 118 } 119 120 if ko.Repository == "" { 121 return errNoRepository 122 } 123 124 ids.Inc(ko.ID) 125 } 126 return ids.Validate() 127 } 128 129 // Publish executes the Pipe. 130 func (Pipe) Publish(ctx *context.Context) error { 131 g := semerrgroup.New(ctx.Parallelism) 132 for _, ko := range ctx.Config.Kos { 133 g.Go(doBuild(ctx, ko)) 134 } 135 return g.Wait() 136 } 137 138 type buildOptions struct { 139 importPath string 140 main string 141 flags []string 142 env []string 143 imageRepo string 144 workingDir string 145 platforms []string 146 baseImage string 147 labels map[string]string 148 tags []string 149 creationTime *v1.Time 150 koDataCreationTime *v1.Time 151 sbom string 152 ldflags []string 153 bare bool 154 preserveImportPaths bool 155 baseImportPaths bool 156 } 157 158 func (o *buildOptions) makeBuilder(ctx *context.Context) (*build.Caching, error) { 159 buildOptions := []build.Option{ 160 build.WithConfig(map[string]build.Config{ 161 o.importPath: { 162 Ldflags: o.ldflags, 163 Flags: o.flags, 164 Main: o.main, 165 Env: o.env, 166 }, 167 }), 168 build.WithPlatforms(o.platforms...), 169 build.WithBaseImages(func(ctx stdctx.Context, s string) (name.Reference, build.Result, error) { 170 ref, err := name.ParseReference(o.baseImage) 171 if err != nil { 172 return nil, nil, err 173 } 174 175 if cached, found := baseImages.Load(o.baseImage); found { 176 return ref, cached.(build.Result), nil 177 } 178 179 desc, err := remote.Get( 180 ref, 181 remote.WithAuthFromKeychain(keychain), 182 ) 183 if err != nil { 184 return nil, nil, err 185 } 186 if desc.MediaType.IsImage() { 187 img, err := desc.Image() 188 baseImages.Store(o.baseImage, img) 189 return ref, img, err 190 } 191 if desc.MediaType.IsIndex() { 192 idx, err := desc.ImageIndex() 193 baseImages.Store(o.baseImage, idx) 194 return ref, idx, err 195 } 196 return nil, nil, fmt.Errorf("unexpected base image media type: %s", desc.MediaType) 197 }), 198 } 199 if o.creationTime != nil { 200 buildOptions = append(buildOptions, build.WithCreationTime(*o.creationTime)) 201 } 202 if o.koDataCreationTime != nil { 203 buildOptions = append(buildOptions, build.WithKoDataCreationTime(*o.koDataCreationTime)) 204 } 205 for k, v := range o.labels { 206 buildOptions = append(buildOptions, build.WithLabel(k, v)) 207 } 208 switch o.sbom { 209 case "spdx": 210 buildOptions = append(buildOptions, build.WithSPDX("devel")) 211 case "cyclonedx": 212 buildOptions = append(buildOptions, build.WithCycloneDX()) 213 case "go.version-m": 214 buildOptions = append(buildOptions, build.WithGoVersionSBOM()) 215 case "none": 216 buildOptions = append(buildOptions, build.WithDisabledSBOM()) 217 default: 218 return nil, fmt.Errorf("unknown sbom type: %q", o.sbom) 219 } 220 221 b, err := build.NewGo(ctx, o.workingDir, buildOptions...) 222 if err != nil { 223 return nil, fmt.Errorf("newGo: %w", err) 224 } 225 return build.NewCaching(b) 226 } 227 228 func doBuild(ctx *context.Context, ko config.Ko) func() error { 229 return func() error { 230 opts, err := buildBuildOptions(ctx, ko) 231 if err != nil { 232 return err 233 } 234 235 b, err := opts.makeBuilder(ctx) 236 if err != nil { 237 return fmt.Errorf("makeBuilder: %w", err) 238 } 239 r, err := b.Build(ctx, opts.importPath) 240 if err != nil { 241 return fmt.Errorf("build: %w", err) 242 } 243 244 po := []publish.Option{publish.WithTags(opts.tags), publish.WithNamer(options.MakeNamer(&options.PublishOptions{ 245 DockerRepo: opts.imageRepo, 246 Bare: opts.bare, 247 PreserveImportPaths: opts.preserveImportPaths, 248 BaseImportPaths: opts.baseImportPaths, 249 Tags: opts.tags, 250 })), publish.WithAuthFromKeychain(keychain)} 251 252 p, err := publish.NewDefault(opts.imageRepo, po...) 253 if err != nil { 254 return fmt.Errorf("newDefault: %w", err) 255 } 256 defer func() { _ = p.Close() }() 257 ref, err := p.Publish(ctx, r, opts.importPath) 258 if err != nil { 259 return fmt.Errorf("publish: %w", err) 260 } 261 if err := p.Close(); err != nil { 262 return fmt.Errorf("close: %w", err) 263 } 264 265 art := &artifact.Artifact{ 266 Type: artifact.DockerManifest, 267 Name: ref.Name(), 268 Path: ref.Name(), 269 Extra: map[string]interface{}{}, 270 } 271 if ko.ID != "" { 272 art.Extra[artifact.ExtraID] = ko.ID 273 } 274 if digest := ref.Context().Digest(ref.Identifier()).DigestStr(); digest != "" { 275 art.Extra[artifact.ExtraDigest] = digest 276 } 277 ctx.Artifacts.Add(art) 278 return nil 279 } 280 } 281 282 func findBuild(ctx *context.Context, ko config.Ko) (config.Build, error) { 283 for _, build := range ctx.Config.Builds { 284 if build.ID == ko.Build { 285 return build, nil 286 } 287 } 288 return config.Build{}, fmt.Errorf("no builds with id %q", ko.Build) 289 } 290 291 func buildBuildOptions(ctx *context.Context, cfg config.Ko) (*buildOptions, error) { 292 localImportPath := cfg.Main 293 294 dir := filepath.Clean(cfg.WorkingDir) 295 if dir == "." { 296 dir = "" 297 } 298 299 pkgs, err := packages.Load(&packages.Config{ 300 Mode: packages.NeedName, 301 Dir: dir, 302 }, localImportPath) 303 if err != nil { 304 return nil, fmt.Errorf( 305 "ko: %s does not contain a valid local import path (%s) for directory (%s): %w", 306 cfg.ID, localImportPath, cfg.WorkingDir, err, 307 ) 308 } 309 310 if len(pkgs) != 1 { 311 return nil, fmt.Errorf( 312 "ko: %s results in %d local packages, only 1 is expected", 313 cfg.ID, len(pkgs), 314 ) 315 } 316 317 opts := &buildOptions{ 318 importPath: pkgs[0].PkgPath, 319 workingDir: cfg.WorkingDir, 320 bare: cfg.Bare, 321 preserveImportPaths: cfg.PreserveImportPaths, 322 baseImportPaths: cfg.BaseImportPaths, 323 baseImage: cfg.BaseImage, 324 platforms: cfg.Platforms, 325 sbom: cfg.SBOM, 326 imageRepo: cfg.Repository, 327 } 328 329 tags, err := applyTemplate(ctx, cfg.Tags) 330 if err != nil { 331 return nil, err 332 } 333 opts.tags = removeEmpty(tags) 334 335 if cfg.CreationTime != "" { 336 creationTime, err := getTimeFromTemplate(ctx, cfg.CreationTime) 337 if err != nil { 338 return nil, err 339 } 340 opts.creationTime = creationTime 341 } 342 343 if cfg.KoDataCreationTime != "" { 344 koDataCreationTime, err := getTimeFromTemplate(ctx, cfg.KoDataCreationTime) 345 if err != nil { 346 return nil, err 347 } 348 opts.koDataCreationTime = koDataCreationTime 349 } 350 351 if len(cfg.Labels) > 0 { 352 opts.labels = make(map[string]string, len(cfg.Labels)) 353 for k, v := range cfg.Labels { 354 tv, err := tmpl.New(ctx).Apply(v) 355 if err != nil { 356 return nil, err 357 } 358 opts.labels[k] = tv 359 } 360 } 361 362 if len(cfg.Env) > 0 { 363 env, err := applyTemplate(ctx, cfg.Env) 364 if err != nil { 365 return nil, err 366 } 367 opts.env = env 368 } 369 370 if len(cfg.Flags) > 0 { 371 flags, err := applyTemplate(ctx, cfg.Flags) 372 if err != nil { 373 return nil, err 374 } 375 opts.flags = flags 376 } 377 378 if len(cfg.Ldflags) > 0 { 379 ldflags, err := applyTemplate(ctx, cfg.Ldflags) 380 if err != nil { 381 return nil, err 382 } 383 opts.ldflags = ldflags 384 } 385 return opts, nil 386 } 387 388 func removeEmpty(strs []string) []string { 389 var res []string 390 for _, s := range strs { 391 if strings.TrimSpace(s) == "" { 392 continue 393 } 394 res = append(res, s) 395 } 396 return res 397 } 398 399 func applyTemplate(ctx *context.Context, templateable []string) ([]string, error) { 400 var templated []string 401 for _, t := range templateable { 402 tlf, err := tmpl.New(ctx).Apply(t) 403 if err != nil { 404 return nil, err 405 } 406 templated = append(templated, tlf) 407 } 408 return templated, nil 409 } 410 411 func getTimeFromTemplate(ctx *context.Context, t string) (*v1.Time, error) { 412 epoch, err := tmpl.New(ctx).Apply(t) 413 if err != nil { 414 return nil, err 415 } 416 417 seconds, err := strconv.ParseInt(epoch, 10, 64) 418 if err != nil { 419 return nil, err 420 } 421 return &v1.Time{Time: time.Unix(seconds, 0)}, nil 422 }