145 lines
3.3 KiB
Go
145 lines
3.3 KiB
Go
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)
|
|
}
|
|
}
|