//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" "errors" "fmt" "io" "log" "math/big" "net" "net/http" "os" "reflect" "slices" "strconv" "strings" "testing" "time" "github.com/docker/docker/api/types" api "github.com/docker/docker/api/types/image" "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" "github.com/google/go-containerregistry/pkg/v1/tarball" regTypes "github.com/google/go-containerregistry/pkg/v1/types" "gotest.tools/v3/assert" "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" functionImageRemote = registryHostname + "/testuser/func:latest" imageTarball = "testdata/image.tar" imageID = "sha256:1fb61f35700f47e1e868f8b26fdd777016f96bb1b4b0b0e623efac39eb30d12e" imageRepoDigest = "sha256:00af51d125f3092e157a7f8a717029412dc9d266c017e89cecdfeccb4cc3d7a7" ) var testCredProvider = docker.CredentialsProvider(func(ctx context.Context, registry string) (docker.Credentials, error) { return docker.Credentials{ Username: testUser, Password: testPwd, }, nil }) func TestPush(t *testing.T) { tests := []struct { Name string DaemonPush bool }{ {Name: "daemon push", DaemonPush: true}, {Name: "non daemon push"}, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() // in memory network emulation connections := conns(make(chan net.Conn)) transport := http.DefaultTransport.(*http.Transport).Clone() transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } transport.DialContext = connections.DialContext serveRegistry(t, connections) dockerClient := newMockPusherDockerClient() pushReachable := func(ctx context.Context, ref string, options api.PushOptions) (io.ReadCloser, error) { if ref != functionImageRemote { return nil, fmt.Errorf("unexpected ref") } var err error authData, err := base64.StdEncoding.DecodeString(options.RegistryAuth) if err != nil { return nil, err } authStruct := struct { Username, Password string }{} dec := json.NewDecoder(bytes.NewReader(authData)) err = dec.Decode(&authStruct) if err != nil { return nil, err } remoteOpts := []remote.Option{ remote.WithTransport(transport), remote.WithAuth(&authn.Basic{ Username: authStruct.Username, Password: authStruct.Password, }), } tag, err := name.NewTag(ref) if err != nil { return nil, err } img, err := tarball.ImageFromPath(imageTarball, &tag) if err != nil { return nil, err } err = remote.Write(tag, img, remoteOpts...) if err != nil { return nil, err } is, err := img.Size() if err != nil { return nil, err } return io.NopCloser(strings.NewReader(`{ "status": "latest: digest: ` + imageRepoDigest + ` size: ` + strconv.FormatInt(is, 10) + `" } `)), nil } pushUnreachable := func(ctx context.Context, ref string, options api.PushOptions) (io.ReadCloser, error) { return io.NopCloser(strings.NewReader(`{"errorDetail": {"message": "...no such host..."}}`)), nil } if tt.DaemonPush { dockerClient.imagePush = pushReachable } else { dockerClient.imagePush = pushUnreachable } dockerClient.imageInspect = func(ctx context.Context, s string) (types.ImageInspect, []byte, error) { return types.ImageInspect{ID: imageID}, []byte{}, nil } dockerClient.imageSave = func(ctx context.Context, tags []string) (io.ReadCloser, error) { if slices.Equal(tags, []string{functionImageRemote}) { f, err := os.Open(imageTarball) if err != nil { return nil, err } return f, nil } return nil, fmt.Errorf("unexpected tags") } var closeCalledOnMock bool dockerClient.close = func() error { closeCalledOnMock = true return nil } dockerClientFactory := func() (docker.PusherDockerClient, error) { return dockerClient, nil } pusher := docker.NewPusher( docker.WithTransport(transport), docker.WithCredentialsProvider(testCredProvider), docker.WithPusherDockerClientFactory(dockerClientFactory), ) f := fn.Function{ Build: fn.BuildSpec{ Image: functionImageRemote, }, } remoteOpts := []remote.Option{ remote.WithTransport(transport), remote.WithAuth(&authn.Basic{ Username: testUser, Password: testPwd, }), } _, err := pusher.Push(ctx, f) if err != nil { t.Fatal(err) } r, err := name.NewRegistry(registryHostname) if err != nil { t.Fatal(err) } 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") } ref := name.MustParseReference(functionImageRemote) desc, err := remote.Get(ref, remoteOpts...) if err != nil { t.Fatal(err) } assert.Equal(t, desc.MediaType, regTypes.DockerManifestList) img, err := remote.Image(ref, remoteOpts...) if err != nil { t.Fatal(err) } d, err := img.Digest() if err != nil { t.Fatal(err) } assert.Equal(t, d.String(), imageRepoDigest) if !closeCalledOnMock { t.Error("The Close() function has not been called on the Docker API Client.") } }) } } func newMockPusherDockerClient() *mockPusherDockerClient { return &mockPusherDockerClient{ imagePush: func(ctx context.Context, ref string, options api.PushOptions) (io.ReadCloser, error) { return nil, fmt.Errorf(" imagePush not implemented") }, imageSave: func(ctx context.Context, strings []string) (io.ReadCloser, error) { return nil, fmt.Errorf("imageSave not implemented") }, imageInspect: func(ctx context.Context, s string) (types.ImageInspect, []byte, error) { return types.ImageInspect{}, nil, fmt.Errorf("imageInspect not implemented") }, 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 api.PushOptions) (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) (api.LoadResponse, 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 api.PushOptions) (io.ReadCloser, error) { return m.imagePush(ctx, ref, options) } func (m *mockPusherDockerClient) ImageHistory(context.Context, string) ([]api.HistoryResponseItem, error) { return nil, errors.New("the ImageHistory() function is not implemented") } 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 }