func/pkg/oci/pusher.go

180 lines
4.7 KiB
Go

package oci
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"os"
"path/filepath"
"golang.org/x/term"
"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/google"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/pkg/errors"
progress "github.com/schollz/progressbar/v3"
fn "knative.dev/func/pkg/functions"
)
// Pusher of OCI multi-arch layout directories.
type Pusher struct {
Anonymous bool
Insecure bool
Token string
Username string
Verbose bool
updates chan v1.Update
done chan bool
}
func NewPusher(insecure, anon, verbose bool) *Pusher {
return &Pusher{
Insecure: insecure,
Anonymous: anon,
Verbose: verbose,
updates: make(chan v1.Update, 10),
done: make(chan bool, 1),
}
}
func (p *Pusher) Push(ctx context.Context, f fn.Function) (digest string, err error) {
go p.handleUpdates(ctx)
defer func() { p.done <- true }()
buildDir, err := getLastBuildDir(f)
if err != nil {
return
}
var opts []name.Option
if p.Insecure {
opts = append(opts, name.Insecure)
}
// TODO: GitOps Tagging: tag :latest by default, :[branch] for pinned
// environments and :[user]-[branch] for development/testing feature branches.
// has been enabled, where branch is tag-encoded.
ref, err := name.ParseReference(f.Build.Image, opts...)
if err != nil {
return
}
ii, err := layout.ImageIndexFromPath(filepath.Join(buildDir, "oci"))
if err != nil {
return
}
if err = p.writeIndex(ctx, ref, ii); err != nil {
return
}
h, err := ii.Digest()
if err != nil {
return
}
digest = h.String()
if p.Verbose {
fmt.Printf("\ndigest: %s\n", h)
}
return
}
func (p *Pusher) handleUpdates(ctx context.Context) {
var bar *progress.ProgressBar
for {
select {
case update := <-p.updates:
if bar == nil {
bar = progress.NewOptions64(update.Total,
progress.OptionSetVisibility(term.IsTerminal(int(os.Stdin.Fd()))),
progress.OptionSetDescription("pushing"),
progress.OptionShowCount(),
progress.OptionShowBytes(true),
progress.OptionShowElapsedTimeOnFinish())
}
_ = bar.Set64(update.Complete)
continue
case <-p.done:
if bar != nil {
_ = bar.Finish()
}
return
case <-ctx.Done():
if bar != nil {
_ = bar.Finish()
}
return
}
}
}
// The last build directory is symlinked upon successful build.
func getLastBuildDir(f fn.Function) (string, error) {
dir := filepath.Join(f.Root, fn.RunDataDir, "builds", "last")
if _, err := os.Stat(dir); os.IsNotExist(err) {
return dir, fmt.Errorf("last build directory not found '%v'. Has it been built?", dir)
}
return dir, nil
}
// writeIndex to its defined registry.
func (p *Pusher) writeIndex(ctx context.Context, ref name.Reference, ii v1.ImageIndex) error {
oo := []remote.Option{
remote.WithContext(ctx),
remote.WithProgress(p.updates),
}
if p.Insecure {
t := remote.DefaultTransport.(*http.Transport).Clone()
t.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
oo = append(oo, remote.WithTransport(t))
}
if !p.Anonymous {
a, err := p.authOption(ctx)
if err != nil {
return err
}
oo = append(oo, a)
}
return remote.WriteIndex(ref, ii, oo...)
}
// authOption selects an appropriate authentication option.
// If user provided = basic auth (secret is password)
// If only secret provided = bearer token auth
// If neither are provided = Returned is a cascading keychain auth mthod
// which performs the following in order:
// - Default Keychain (docker and podman config files)
// - Google Keychain
// - TODO: ECR Amazon
// - TODO: ACR Azure
func (p *Pusher) authOption(ctx context.Context) (remote.Option, error) {
// Basic Auth if provided
username, _ := ctx.Value(fn.PushUsernameKey{}).(string)
password, _ := ctx.Value(fn.PushPasswordKey{}).(string)
token, _ := ctx.Value(fn.PushTokenKey{}).(string)
if username != "" && token != "" {
return nil, errors.New("only one of username/password or token authentication allowed. Received both a token and username")
} else if token != "" {
return remote.WithAuth(&authn.Bearer{Token: token}), nil
} else if username != "" {
return remote.WithAuth(&authn.Basic{Username: username, Password: password}), nil
}
// Default chain
return remote.WithAuthFromKeychain(authn.NewMultiKeychain(
authn.DefaultKeychain, // Podman and Docker config files
google.Keychain, // Google
// TODO: Integrate and test ECR and ACR credential helpers:
// authn.NewKeychainFromHelper(ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}),
// authn.NewKeychainFromHelper(acr.ACRCredHelper{}),
)), nil
}