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