Ctrl K
ComunidadGuías…

iclient/registry_test.go

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package iclient

import (
	"context"
	"encoding/pem"
	"io"
	"net/http"
	"net/http/httptest"
	"reflect"
	"strings"
	"testing"

	"github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"oras.land/oras-go/v2/registry/remote/auth"
	"oras.land/oras-go/v2/registry/remote/retry"
)

// transportOfRepo digs out the transport NewRepository built.
func transportOfRepo(t *testing.T, client any) *http.Transport {
	t.Helper()

	authClient, ok := client.(*auth.Client)
	require.True(t, ok)

	retryTransport, ok := authClient.Client.Transport.(*retry.Transport)
	require.True(t, ok)

	transport, ok := retryTransport.Base.(*http.Transport)
	require.True(t, ok)

	return transport
}

func TestNewRepositoryReference(t *testing.T) {
	t.Parallel()

	for _, tt := range []struct {
		name       string
		addr       string
		image      string
		registry   string
		host       string
		repository string
		reference  string
		plainHTTP  bool
	}{
		{
			name:       "a well-known registry resolves to Docker Hub's real host",
			addr:       "https://docker.io",
			image:      "library/redis:alpine",
			registry:   "docker.io",
			host:       "registry-1.docker.io",
			repository: "library/redis",
			reference:  "alpine",
		},
		{
			name:       "a mirror is reached instead of the registry it stands in for",
			addr:       "https://docker-registry.example.com",
			image:      "library/redis:alpine",
			registry:   "docker-registry.example.com",
			host:       "docker-registry.example.com",
			repository: "library/redis",
			reference:  "alpine",
		},
		{
			name:       "a port survives",
			addr:       "https://registry.example.com:5000",
			image:      "myorg/myapp:v1.0",
			registry:   "registry.example.com:5000",
			host:       "registry.example.com:5000",
			repository: "myorg/myapp",
			reference:  "v1.0",
		},
		{
			name:       "an http address asks for plain HTTP",
			addr:       "http://registry.example.com",
			image:      "myorg/myapp:v1.0",
			registry:   "registry.example.com",
			host:       "registry.example.com",
			repository: "myorg/myapp",
			reference:  "v1.0",
			plainHTTP:  true,
		},
		{
			name:       "a digest is a reference too",
			addr:       "https://ghcr.io",
			image:      "myorg/myapp@sha256:0000000000000000000000000000000000000000000000000000000000000000",
			registry:   "ghcr.io",
			host:       "ghcr.io",
			repository: "myorg/myapp",
			reference:  "sha256:0000000000000000000000000000000000000000000000000000000000000000",
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			repo, err := NewRepository(&ConfigRemoteInfo{
				Name:     "reg",
				Addrs:    []string{tt.addr},
				Protocol: "oci",
			}, tt.image)
			require.NoError(t, err)

			assert.Equal(t, tt.registry, repo.Reference.Registry)
			assert.Equal(t, tt.host, repo.Reference.Host())
			assert.Equal(t, tt.repository, repo.Reference.Repository)
			assert.Equal(t, tt.reference, repo.Reference.Reference)
			assert.Equal(t, tt.plainHTTP, repo.PlainHTTP)
		})
	}
}

func TestNewRepositoryRejects(t *testing.T) {
	t.Parallel()

	for _, tt := range []struct {
		name  string
		info  *ConfigRemoteInfo
		image string
		errs  error
	}{
		{
			name:  "an incus remote is not a registry",
			info:  &ConfigRemoteInfo{Name: "ict", Addrs: []string{"https://127.0.0.1:8443"}, Protocol: "incus"},
			image: "library/redis:alpine",
			errs:  ErrRegistryProtocol,
		},
		{
			name:  "a simplestreams remote is not a registry",
			info:  &ConfigRemoteInfo{Name: "images", Addrs: []string{"https://images.example.com"}, Protocol: "simplestreams"},
			image: "library/redis:alpine",
			errs:  ErrRegistryProtocol,
		},
		{
			name:  "a registry with no address",
			info:  &ConfigRemoteInfo{Name: "reg", Protocol: "oci"},
			image: "library/redis:alpine",
			errs:  ErrConnectionNoAddress,
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			repo, err := NewRepository(tt.info, tt.image)
			require.ErrorIs(t, err, tt.errs)
			assert.Nil(t, repo)
		})
	}
}

func TestNewRepositoryRejectsBadImage(t *testing.T) {
	t.Parallel()

	for _, image := range []string{"", "Library/Redis:alpine", "library/redis:not a tag", "/library/redis"} {
		t.Run(image, func(t *testing.T) {
			t.Parallel()

			repo, err := NewRepository(&ConfigRemoteInfo{
				Name:     "reg",
				Addrs:    []string{"https://registry.example.com"},
				Protocol: "oci",
			}, image)
			require.Error(t, err)
			assert.Nil(t, repo)
		})
	}
}

func TestNewRepositoryCredentials(t *testing.T) {
	t.Parallel()

	t.Run("the resolved login becomes the registry credential", func(t *testing.T) {
		t.Parallel()

		repo, err := NewRepository(&ConfigRemoteInfo{
			Name:     "reg",
			Addrs:    []string{"https://registry.example.com"},
			Protocol: "oci",
			Username: "user",
			Password: "s3cret",
		}, "myorg/myapp:v1.0")
		require.NoError(t, err)

		client, ok := repo.Client.(*auth.Client)
		require.True(t, ok)
		require.NotNil(t, client.Credential)

		cred, err := client.Credential(context.Background(), "registry.example.com")
		require.NoError(t, err)
		assert.Equal(t, "user", cred.Username)
		assert.Equal(t, "s3cret", cred.Password)
	})

	// auth.Client maps docker.io to registry-1.docker.io before it looks a
	// credential up, so a credential filed under docker.io would never be found.
	t.Run("a Docker Hub credential is filed under the host auth asks for", func(t *testing.T) {
		t.Parallel()

		repo, err := NewRepository(&ConfigRemoteInfo{
			Name:     "docker.io",
			Addrs:    []string{"https://docker.io"},
			Protocol: "oci",
			Username: "user",
			Password: "s3cret",
		}, "library/redis:alpine")
		require.NoError(t, err)

		client, ok := repo.Client.(*auth.Client)
		require.True(t, ok)

		cred, err := client.Credential(context.Background(), "registry-1.docker.io")
		require.NoError(t, err)
		assert.Equal(t, "user", cred.Username)
	})

	// A password in Addrs would reach the logs and our own error strings, so it
	// is refused rather than quietly ignored.
	t.Run("an address carrying credentials is refused", func(t *testing.T) {
		t.Parallel()

		repo, err := NewRepository(&ConfigRemoteInfo{
			Name:     "reg",
			Addrs:    []string{"https://user:s3cret@registry.example.com"},
			Protocol: "oci",
		}, "myorg/myapp:v1.0")
		require.ErrorIs(t, err, ErrRegistryAddrCredentials)
		assert.Nil(t, repo)
		assert.NotContains(t, err.Error(), "s3cret")
	})

	t.Run("no login leaves the registry anonymous", func(t *testing.T) {
		t.Parallel()

		repo, err := NewRepository(&ConfigRemoteInfo{
			Name:     "reg",
			Addrs:    []string{"https://registry.example.com"},
			Protocol: "oci",
		}, "myorg/myapp:v1.0")
		require.NoError(t, err)

		client, ok := repo.Client.(*auth.Client)
		require.True(t, ok)
		assert.Nil(t, client.Credential)
	})
}

func TestNewRepositoryTransport(t *testing.T) {
	t.Parallel()

	// Driving this through HTTPS_PROXY would prove nothing: ProxyFromEnvironment
	// reads the environment once per process, so whichever test got there first
	// decides the answer. Identity is what this package controls.
	t.Run("a corporate proxy is consulted", func(t *testing.T) {
		t.Parallel()

		repo, err := NewRepository(&ConfigRemoteInfo{
			Name:     "reg",
			Addrs:    []string{"https://registry.example.com"},
			Protocol: "oci",
		}, "myorg/myapp:v1.0")
		require.NoError(t, err)

		transport := transportOfRepo(t, repo.Client)
		require.NotNil(t, transport.Proxy)
		assert.Equal(t,
			reflect.ValueOf(http.ProxyFromEnvironment).Pointer(),
			reflect.ValueOf(transport.Proxy).Pointer(),
		)
	})

	// An operation long-poll needs the hour incusTransport carries; a registry
	// answering metadata does not.
	t.Run("a registry does not wait an hour for a header", func(t *testing.T) {
		t.Parallel()

		repo, err := NewRepository(&ConfigRemoteInfo{
			Name:     "reg",
			Addrs:    []string{"https://registry.example.com"},
			Protocol: "oci",
		}, "myorg/myapp:v1.0")
		require.NoError(t, err)

		transport := transportOfRepo(t, repo.Client)
		assert.Equal(t, registryResponseHeaderTimeout, transport.ResponseHeaderTimeout)
	})

	t.Run("the user agent reaches the registry", func(t *testing.T) {
		t.Parallel()

		repo, err := NewRepository(&ConfigRemoteInfo{
			Name:      "reg",
			Addrs:     []string{"https://registry.example.com"},
			Protocol:  "oci",
			UserAgent: "incus-compose/test",
		}, "myorg/myapp:v1.0")
		require.NoError(t, err)

		client, ok := repo.Client.(*auth.Client)
		require.True(t, ok)
		assert.Equal(t, "incus-compose/test", client.Header.Get("User-Agent"))
	})
}

// fakeRegistry serves one image's manifest over the Distribution API.
func fakeRegistry(t *testing.T, manifest []byte, wantAuth string) *httptest.Server {
	t.Helper()

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if wantAuth != "" && r.Header.Get("Authorization") != wantAuth {
			w.Header().Set("WWW-Authenticate", `Basic realm="registry"`)
			w.WriteHeader(http.StatusUnauthorized)

			return
		}

		if r.URL.Path != "/v2/library/redis/manifests/alpine" {
			w.WriteHeader(http.StatusNotFound)

			return
		}

		w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
		w.Header().Set("Docker-Content-Digest", digest.FromBytes(manifest).String())
		_, _ = w.Write(manifest)
	})

	server := httptest.NewServer(handler)
	t.Cleanup(server.Close)

	return server
}

// testManifest is the smallest manifest oras-go will hand back.
var testManifest = []byte(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[]}`)

func TestNewRepositoryFetches(t *testing.T) {
	t.Parallel()

	server := fakeRegistry(t, testManifest, "")

	repo, err := NewRepository(&ConfigRemoteInfo{
		Name:     "reg",
		Addrs:    []string{server.URL},
		Protocol: "oci",
	}, "library/redis:alpine")
	require.NoError(t, err)
	require.True(t, repo.PlainHTTP)

	desc, rc, err := repo.FetchReference(t.Context(), repo.Reference.Reference)
	require.NoError(t, err)

	defer func() { _ = rc.Close() }()

	assert.Equal(t, ocispec.MediaTypeImageManifest, desc.MediaType)
	assert.Equal(t, digest.FromBytes(testManifest).String(), desc.Digest.String())

	body, err := io.ReadAll(rc)
	require.NoError(t, err)
	assert.JSONEq(t, string(testManifest), string(body))
}

func TestNewRepositoryFetchesAuthenticated(t *testing.T) {
	t.Parallel()

	server := fakeRegistry(t, testManifest, "Basic dXNlcjpzM2NyZXQ=")

	repo, err := NewRepository(&ConfigRemoteInfo{
		Name:     "reg",
		Addrs:    []string{server.URL},
		Protocol: "oci",
		Username: "user",
		Password: "s3cret",
	}, "library/redis:alpine")
	require.NoError(t, err)

	_, rc, err := repo.FetchReference(t.Context(), repo.Reference.Reference)
	require.NoError(t, err)

	_ = rc.Close()
}

func TestNewRepositoryPinsTheServerCertificate(t *testing.T) {
	t.Parallel()

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
		w.Header().Set("Docker-Content-Digest", digest.FromBytes(testManifest).String())
		_, _ = w.Write(testManifest)
	})

	server := httptest.NewTLSServer(handler)
	t.Cleanup(server.Close)

	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw})
	require.NotNil(t, certPEM)

	t.Run("the pinned certificate is accepted", func(t *testing.T) {
		t.Parallel()

		repo, err := NewRepository(&ConfigRemoteInfo{
			Name:       "reg",
			Addrs:      []string{server.URL},
			Protocol:   "oci",
			ServerCert: string(certPEM),
		}, "library/redis:alpine")
		require.NoError(t, err)
		require.False(t, repo.PlainHTTP)

		_, rc, err := repo.FetchReference(t.Context(), repo.Reference.Reference)
		require.NoError(t, err)

		_ = rc.Close()
	})

	t.Run("an unknown certificate is refused", func(t *testing.T) {
		t.Parallel()

		repo, err := NewRepository(&ConfigRemoteInfo{
			Name:     "reg",
			Addrs:    []string{server.URL},
			Protocol: "oci",
		}, "library/redis:alpine")
		require.NoError(t, err)

		_, _, err = repo.FetchReference(t.Context(), repo.Reference.Reference)
		require.Error(t, err)
		assert.True(t, strings.Contains(err.Error(), "certificate"), "want a certificate error, got %v", err)
	})
}