package ociimg import ( "context" "fmt" "log/slog" "net/http" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/types" "code.icb4dc0.de/buildr/buildr/internal/maps" ) type registryPublisherOption func(p *registryPublisher) func withTags(tags ...string) registryPublisherOption { return func(p *registryPublisher) { tagsSet := make(map[string]bool) for i := range p.tags { tagsSet[p.tags[i]] = true } for i := range tags { tagsSet[tags[i]] = true } p.tags = maps.Keys(tagsSet) } } func withKeyChain(kc authn.Keychain) registryPublisherOption { return func(p *registryPublisher) { p.keychain = kc } } func newRegistryPublisher( imageName string, logger *slog.Logger, opts ...registryPublisherOption, ) registryPublisher { p := registryPublisher{ imageName: imageName, logger: logger, tags: []string{latestTag}, t: http.DefaultTransport, keychain: authn.DefaultKeychain, } for i := range opts { opts[i](&p) } return p } type registryPublisher struct { t http.RoundTripper keychain authn.Keychain logger *slog.Logger imageName string tags []string insecure bool } func (p registryPublisher) Publish(ctx context.Context, result imageOrIndex) (name.Reference, error) { ro := []remote.Option{ remote.WithAuthFromKeychain(p.keychain), remote.WithTransport(p.t), remote.WithContext(ctx), remote.WithUserAgent(userAgent), } var no []name.Option if p.insecure { no = append(no, name.Insecure) } for i, tagName := range p.tags { tag, err := name.NewTag(fmt.Sprintf("%s:%s", p.imageName, tagName), no...) if err != nil { return nil, err } if i == 0 { p.logger.Debug("Publishing", slog.String("tag", tag.String())) if err := pushResult(tag, result, ro); err != nil { return nil, err } } else { p.logger.Debug("Tagging", slog.String("tag", tag.String())) if err := remote.Tag(tag, result, ro...); err != nil { return nil, err } } } h, err := result.Digest() if err != nil { return nil, err } ref := fmt.Sprintf("%s@%s", p.imageName, h) if len(p.tags) == 1 && p.tags[0] != latestTag { // If a single tag is explicitly set (not latest), then this // is probably a release, so include the tag in the reference. ref = fmt.Sprintf("%s:%s@%s", p.imageName, p.tags[0], h) } dig, err := name.NewDigest(ref) if err != nil { return nil, err } p.logger.Debug("Published", slog.String("digest", dig.String())) return &dig, nil } func pushResult(tag name.Tag, br imageOrIndex, opt []remote.Option) error { mt, err := br.MediaType() if err != nil { return err } //nolint:exhaustive // not necessary switch mt { case types.OCIImageIndex, types.DockerManifestList: idx, ok := br.(v1.ImageIndex) if !ok { return fmt.Errorf("failed to interpret result as index: %v", br) } return remote.WriteIndex(tag, idx, opt...) case types.OCIManifestSchema1, types.DockerManifestSchema2: img, ok := br.(v1.Image) if !ok { return fmt.Errorf("failed to interpret result as image: %v", br) } return remote.Write(tag, img, opt...) default: return fmt.Errorf("result image media type: %s", mt) } }