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