iclient/registry_test.go
Rama develop · réplica de lectura del repositorio original.
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)
})
}