//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 }