func/pkg/docker/pusher_test.go

482 lines
12 KiB
Go

//go:build !integration
// +build !integration
package docker_test
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"math/big"
"net"
"net/http"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/google/go-containerregistry/pkg/v1/remote"
"knative.dev/func/pkg/docker"
fn "knative.dev/func/pkg/functions"
)
func TestGetRegistry(t *testing.T) {
tests := []struct {
name string
arg string
want string
}{
{
name: "default registry",
arg: "docker.io/mysamplefunc:latest",
want: "index.docker.io",
},
{
name: "long-form nested url",
arg: "myregistry.io/myorg/myuser/myfunctions/mysamplefunc:latest",
want: "myregistry.io",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, _ := docker.GetRegistry(tt.arg); got != tt.want {
t.Errorf("GetRegistry() = %v, want %v", got, tt.want)
}
})
}
}
const (
testUser = "testuser"
testPwd = "testpwd"
registryHostname = "my.testing.registry"
functionImage = "/testuser/func:latest"
functionImageRemote = registryHostname + functionImage
functionImageLocal = "localhost" + functionImage
)
var testCredProvider = docker.CredentialsProvider(func(ctx context.Context, registry string) (docker.Credentials, error) {
return docker.Credentials{
Username: testUser,
Password: testPwd,
}, nil
})
func TestDaemonPush(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
var optsPassedToMock types.ImagePushOptions
var imagePassedToMock string
var closeCalledOnMock bool
dockerClient := newMockPusherDockerClient()
dockerClient.imagePush = func(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
imagePassedToMock = ref
optsPassedToMock = options
return io.NopCloser(strings.NewReader(`{
"status": "latest: digest: sha256:00af51d125f3092e157a7f8a717029412dc9d266c017e89cecdfeccb4cc3d7a7 size: 2613"
}
`)), nil
}
dockerClient.close = func() error {
closeCalledOnMock = true
return nil
}
dockerClientFactory := func() (docker.PusherDockerClient, error) {
return dockerClient, nil
}
pusher := docker.NewPusher(
docker.WithCredentialsProvider(testCredProvider),
docker.WithPusherDockerClientFactory(dockerClientFactory),
)
f := fn.Function{
Image: functionImageLocal,
}
digest, err := pusher.Push(ctx, f)
if err != nil {
t.Fatal(err)
}
if digest != "sha256:00af51d125f3092e157a7f8a717029412dc9d266c017e89cecdfeccb4cc3d7a7" {
t.Errorf("got bad digest: %q", digest)
}
authData, err := base64.StdEncoding.DecodeString(optsPassedToMock.RegistryAuth)
if err != nil {
t.Fatal(err)
}
authStruct := struct {
Username, Password string
}{}
dec := json.NewDecoder(bytes.NewReader(authData))
err = dec.Decode(&authStruct)
if err != nil {
t.Fatal(err)
}
if imagePassedToMock != functionImageLocal {
t.Errorf("Bad image name passed to the Docker API Client: %q.", imagePassedToMock)
}
if authStruct.Username != testUser || authStruct.Password != testPwd {
t.Errorf("Bad credentials passed to the Docker API Client: %q:%q", authStruct.Username, authStruct.Password)
}
if !closeCalledOnMock {
t.Error("The Close() function has not been called on the Docker API Client.")
}
}
func TestNonDaemonPush(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
// in memory network emulation
connections := conns(make(chan net.Conn))
serveRegistry(t, connections)
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
transport.DialContext = connections.DialContext
dockerClient := newMockPusherDockerClient()
var imagesPassedToMock []string
dockerClient.imageSave = func(ctx context.Context, images []string) (io.ReadCloser, error) {
imagesPassedToMock = images
f, err := os.Open("./testdata/image.tar")
if err != nil {
return nil, fmt.Errorf("failed to load image tar: %w", err)
}
return f, nil
}
dockerClient.imageInspect = func(ctx context.Context, s string) (types.ImageInspect, []byte, error) {
return types.ImageInspect{ID: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, []byte{}, nil
}
dockerClientFactory := func() (docker.PusherDockerClient, error) {
return dockerClient, nil
}
pusher := docker.NewPusher(
docker.WithTransport(transport),
docker.WithCredentialsProvider(testCredProvider),
docker.WithPusherDockerClientFactory(dockerClientFactory),
)
f := fn.Function{
Image: functionImageRemote,
}
actualDigest, err := pusher.Push(ctx, f)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(imagesPassedToMock, []string{f.Image}) {
t.Errorf("Bad image name passed to the Docker API Client: %q.", imagesPassedToMock)
}
r, err := name.NewRegistry(registryHostname)
if err != nil {
t.Fatal(err)
}
remoteOpts := []remote.Option{
remote.WithTransport(transport),
remote.WithAuth(&authn.Basic{
Username: testUser,
Password: testPwd,
}),
}
c, err := remote.Catalog(ctx, r, remoteOpts...)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(c, []string{"testuser/func"}) {
t.Error("unexpected catalog content")
}
imgRef := name.MustParseReference(functionImageRemote)
img, err := remote.Image(imgRef, remoteOpts...)
if err != nil {
t.Fatal(err)
}
expectedDigest, err := img.Digest()
if err != nil {
t.Fatal(err)
}
if actualDigest != expectedDigest.String() {
t.Error("digest does not match")
}
layers, err := img.Layers()
if err != nil {
t.Fatal(err)
}
expectedDiffs := []string{
"sha256:cba17de713254df68662c399558da08f1cd9abfa4e3b404544757982b4f11597",
"sha256:d5c07940dc570b965530593384a3cf47f47bf07d68eb741d875090392a6331c3",
"sha256:a4dd43077393aff80a0bec5c1bf2db4941d620dcaa545662068476e324efefaa",
"sha256:abe6122e067d0f8bee1a8b8f0c4bb9d2b23cc73617bc8ff4addd6c1329bca23e",
"sha256:515e67f7a08c1798cad8ee4d5f0ce7e606540f5efe5163a967d8dc58994f9641",
"sha256:a5fdabf59fa2a4a0e60d21c1ffc4d482e62debe196bae742a476f4d5b893f0ce",
"sha256:8d6bae166e585b4b503d36a7de0ba749b68ef689884e94dfa0655bbf8ce4d213",
"sha256:b136b7c51981af6493ecbb1136f6ff0f23734f7b9feacb20c8090ac9dec6333d",
"sha256:e9055109e5f7999da5add4b5fff11a34783cddc9aef492d9b790024d1bc1b7d0",
"sha256:5a1ff39e0e0291a43170cbcd70515bfccef4bed4c7e7b97f82d49d3d557fe04b",
}
actualDiffs := make([]string, 0, len(expectedDiffs))
for _, layer := range layers {
diffID, err := layer.DiffID()
if err != nil {
t.Fatal(err)
}
actualDiffs = append(actualDiffs, diffID.String())
}
if !reflect.DeepEqual(expectedDiffs, actualDiffs) {
t.Error("layer diffs in tar and from registry differs")
}
}
func newMockPusherDockerClient() *mockPusherDockerClient {
return &mockPusherDockerClient{
negotiateAPIVersion: func(ctx context.Context) {},
close: func() error { return nil },
}
}
type mockPusherDockerClient struct {
negotiateAPIVersion func(ctx context.Context)
imagePush func(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error)
imageSave func(ctx context.Context, strings []string) (io.ReadCloser, error)
imageInspect func(ctx context.Context, s string) (types.ImageInspect, []byte, error)
close func() error
}
func (m *mockPusherDockerClient) NegotiateAPIVersion(ctx context.Context) {
m.negotiateAPIVersion(ctx)
}
func (m *mockPusherDockerClient) ImageSave(ctx context.Context, strings []string) (io.ReadCloser, error) {
return m.imageSave(ctx, strings)
}
func (m *mockPusherDockerClient) ImageLoad(ctx context.Context, reader io.Reader, b bool) (types.ImageLoadResponse, error) {
panic("implement me")
}
func (m *mockPusherDockerClient) ImageTag(ctx context.Context, s string, s2 string) error {
panic("implement me")
}
func (m *mockPusherDockerClient) ImageInspectWithRaw(ctx context.Context, s string) (types.ImageInspect, []byte, error) {
return m.imageInspect(ctx, s)
}
func (m *mockPusherDockerClient) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
return m.imagePush(ctx, ref, options)
}
func (m *mockPusherDockerClient) Close() error {
return m.close()
}
func serveRegistry(t *testing.T, l net.Listener) {
caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
caPublicKey := &caPrivateKey.PublicKey
ca := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: registryHostname,
},
DNSNames: []string{registryHostname},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Minute * 10),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
ExtraExtensions: []pkix.Extension{},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, caPublicKey, caPrivateKey)
if err != nil {
t.Fatal(err)
}
ca, err = x509.ParseCertificate(caBytes)
if err != nil {
t.Fatal(err)
}
cert := tls.Certificate{
Certificate: [][]byte{caBytes},
PrivateKey: caPrivateKey,
Leaf: ca,
}
server := http.Server{
Handler: withAuth(registry.New(
registry.Logger(log.New(io.Discard, "", 0)))),
TLSConfig: &tls.Config{
ServerName: registryHostname,
Certificates: []tls.Certificate{cert},
},
// The line below disables HTTP/2.
// See: https://github.com/google/go-containerregistry/issues/1210
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}
go func() {
_ = server.ServeTLS(l, "", "")
}()
t.Cleanup(func() {
server.Close()
})
}
// middleware for basic auth
func withAuth(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if ok && user == testUser && pass == testPwd {
h.ServeHTTP(w, r)
return
}
w.Header().Add("WWW-Authenticate", "basic")
w.WriteHeader(401)
fmt.Fprintln(w, "Unauthorised.")
})
}
type conns chan net.Conn
func (c conns) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
if addr == registryHostname+":443" {
pr0, pw0 := io.Pipe()
pr1, pw1 := io.Pipe()
c <- conn{pr0, pw1}
return conn{pr1, pw0}, nil
}
return (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext(ctx, network, addr)
}
func (c conns) Accept() (net.Conn, error) {
con, ok := <-c
if !ok {
return nil, net.ErrClosed
}
return con, nil
}
func (c conns) Close() error {
close(c)
return nil
}
func (c conns) Addr() net.Addr {
return addr{}
}
type conn struct {
pr *io.PipeReader
pw *io.PipeWriter
}
type addr struct{}
func (a addr) Network() string {
return "mock-addr"
}
func (a addr) String() string {
return "mock-addr"
}
func (c conn) Read(b []byte) (n int, err error) {
return c.pr.Read(b)
}
func (c conn) Write(b []byte) (n int, err error) {
return c.pw.Write(b)
}
func (c conn) Close() error {
var err error
err = c.pr.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "err: %v\n", err)
}
err = c.pw.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "err: %v\n", err)
}
return nil
}
func (c conn) LocalAddr() net.Addr {
return addr{}
}
func (c conn) RemoteAddr() net.Addr {
return addr{}
}
func (c conn) SetDeadline(t time.Time) error { return nil }
func (c conn) SetReadDeadline(t time.Time) error { return nil }
func (c conn) SetWriteDeadline(t time.Time) error { return nil }