mirror of https://github.com/knative/func.git
180 lines
4.7 KiB
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
|
|
}
|