1
0
mirror of https://github.com/regclient/regclient.git synced 2025-04-17 11:37:11 +03:00
regclient/manifest_test.go
Brandon Mitchell 1eb1ea4b34
Feat: Refactor logging to use log/slog
This updates the regclient Go library.
Existing users of logrus will continue to work using a logrus handler to slog.
Updates to the various commands will be made in a future commit.

Signed-off-by: Brandon Mitchell <git@bmitch.net>
2024-11-10 17:14:57 -05:00

480 lines
14 KiB
Go

package regclient
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"github.com/olareg/olareg"
oConfig "github.com/olareg/olareg/config"
"github.com/opencontainers/go-digest"
"github.com/regclient/regclient/config"
"github.com/regclient/regclient/internal/reqresp"
"github.com/regclient/regclient/types/descriptor"
"github.com/regclient/regclient/types/docker/schema2"
"github.com/regclient/regclient/types/errs"
"github.com/regclient/regclient/types/manifest"
"github.com/regclient/regclient/types/mediatype"
"github.com/regclient/regclient/types/platform"
"github.com/regclient/regclient/types/ref"
)
func TestManifest(t *testing.T) {
t.Parallel()
repoPath := "testrepo"
goodTag := "v1"
deleteTag := "v3"
noheadTag := "nohead"
nodigestTag := "nodigest"
missingTag := "missing"
digest1 := digest.FromString("example1")
digest2 := digest.FromString("example2")
m := schema2.Manifest{
Config: descriptor.Descriptor{
MediaType: mediatype.Docker2ImageConfig,
Size: 8,
Digest: digest1,
},
Layers: []descriptor.Descriptor{
{
MediaType: mediatype.Docker2LayerGzip,
Size: 8,
Digest: digest2,
},
},
}
mBody, err := json.Marshal(m)
if err != nil {
t.Errorf("Failed to marshal manifest: %v", err)
}
mDigest := digest.FromBytes(mBody)
mLen := len(mBody)
missingDigest := digest.FromString("missing descriptor")
ctx := context.Background()
rrs := []reqresp.ReqResp{
{
ReqEntry: reqresp.ReqEntry{
Name: "Get",
Method: "GET",
Path: "/v2/" + repoPath + "/manifests/" + goodTag,
},
RespEntry: reqresp.RespEntry{
Status: http.StatusOK,
Headers: http.Header{
"Content-Length": {fmt.Sprintf("%d", mLen)},
"Content-Type": []string{mediatype.Docker2Manifest},
"Docker-Content-Digest": []string{mDigest.String()},
},
Body: mBody,
},
},
{
ReqEntry: reqresp.ReqEntry{
Name: "Digest",
Method: "GET",
Path: "/v2/" + repoPath + "/manifests/" + mDigest.String(),
},
RespEntry: reqresp.RespEntry{
Status: http.StatusOK,
Headers: http.Header{
"Content-Length": {fmt.Sprintf("%d", mLen)},
"Content-Type": []string{mediatype.Docker2Manifest},
"Docker-Content-Digest": []string{mDigest.String()},
},
Body: mBody,
},
},
{
ReqEntry: reqresp.ReqEntry{
Name: "Head",
Method: "HEAD",
Path: "/v2/" + repoPath + "/manifests/" + goodTag,
},
RespEntry: reqresp.RespEntry{
Status: http.StatusOK,
Headers: http.Header{
"Content-Length": {fmt.Sprintf("%d", mLen)},
"Content-Type": []string{mediatype.Docker2Manifest},
"Docker-Content-Digest": []string{mDigest.String()},
},
},
},
{
ReqEntry: reqresp.ReqEntry{
Name: "Get nodigest",
Method: "GET",
Path: "/v2/" + repoPath + "/manifests/" + nodigestTag,
},
RespEntry: reqresp.RespEntry{
Status: http.StatusOK,
Headers: http.Header{
"Content-Length": {fmt.Sprintf("%d", mLen)},
"Content-Type": []string{mediatype.Docker2Manifest},
"Docker-Content-Digest": []string{mDigest.String()},
},
Body: mBody,
},
},
{
ReqEntry: reqresp.ReqEntry{
Name: "Head nodigest",
Method: "HEAD",
Path: "/v2/" + repoPath + "/manifests/" + nodigestTag,
},
RespEntry: reqresp.RespEntry{
Status: http.StatusOK,
Headers: http.Header{
"Content-Length": {fmt.Sprintf("%d", mLen)},
"Content-Type": []string{mediatype.Docker2Manifest},
},
},
},
{
ReqEntry: reqresp.ReqEntry{
Name: "Get nohead",
Method: "GET",
Path: "/v2/" + repoPath + "/manifests/" + noheadTag,
},
RespEntry: reqresp.RespEntry{
Status: http.StatusOK,
Headers: http.Header{
"Content-Length": {fmt.Sprintf("%d", mLen)},
"Content-Type": []string{mediatype.Docker2Manifest},
"Docker-Content-Digest": []string{mDigest.String()},
},
Body: mBody,
},
},
}
rrs = append(rrs, reqresp.BaseEntries...)
// create servers
boolT := true
tsInternal := httptest.NewServer(reqresp.NewHandler(t, rrs))
olaregHandler := olareg.New(oConfig.Config{
Storage: oConfig.ConfigStorage{
StoreType: oConfig.StoreMem,
RootDir: "./testdata",
},
API: oConfig.ConfigAPI{
DeleteEnabled: &boolT,
},
})
tsOlareg := httptest.NewServer(olaregHandler)
tsOlaregURL, _ := url.Parse(tsOlareg.URL)
tsOlaregHost := tsOlaregURL.Host
t.Cleanup(func() {
tsInternal.Close()
tsOlareg.Close()
_ = olaregHandler.Close()
})
// setup the regclient
tsInternalURL, _ := url.Parse(tsInternal.URL)
tsInternalHost := tsInternalURL.Host
rcHosts := []config.Host{
{
Name: tsOlaregHost,
Hostname: tsOlaregHost,
TLS: config.TLSDisabled,
},
{
Name: "missing." + tsOlaregHost,
Hostname: tsOlaregHost,
TLS: config.TLSDisabled,
},
{
Name: tsInternalHost,
Hostname: tsInternalHost,
TLS: config.TLSDisabled,
},
{
Name: "nohead." + tsInternalHost,
Hostname: tsInternalHost,
TLS: config.TLSDisabled,
APIOpts: map[string]string{
"disableHead": "true",
},
},
}
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
delayInit, _ := time.ParseDuration("0.05s")
delayMax, _ := time.ParseDuration("0.10s")
rc := New(
WithConfigHost(rcHosts...),
WithSlog(log),
WithRetryDelay(delayInit, delayMax),
)
t.Run("Get", func(t *testing.T) {
r, err := ref.New(tsOlaregHost + "/" + repoPath + ":" + goodTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
m, err := rc.ManifestGet(ctx, r)
if err != nil {
t.Fatalf("Failed running ManifestGet: %v", err)
}
if !m.IsSet() {
t.Errorf("manifest is not set on a get request")
}
})
t.Run("Head", func(t *testing.T) {
r, err := ref.New(tsOlaregHost + "/" + repoPath + ":" + goodTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
m, err := rc.ManifestHead(ctx, r)
if err != nil {
t.Fatalf("Failed running ManifestHead: %v", err)
}
if m.IsSet() {
t.Errorf("manifest is set on a head request")
}
})
t.Run("Head no digest", func(t *testing.T) {
r, err := ref.New(tsInternalHost + "/" + repoPath + ":" + nodigestTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
m, err := rc.ManifestHead(ctx, r, WithManifestRequireDigest())
if err != nil {
t.Fatalf("Failed running ManifestHead: %v", err)
}
if manifest.GetMediaType(m) != mediatype.Docker2Manifest {
t.Errorf("Unexpected media type: %s", manifest.GetMediaType(m))
}
if m.GetDescriptor().Digest != mDigest {
t.Errorf("Unexpected digest: %s", m.GetDescriptor().Digest.String())
}
})
t.Run("Head No Head", func(t *testing.T) {
r, err := ref.New("nohead." + tsInternalHost + "/" + repoPath + ":" + noheadTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
m, err := rc.ManifestHead(ctx, r)
if err == nil {
t.Errorf("Unexpected successful head on \"no head\" registry: %v", m)
} else if !errors.Is(err, errs.ErrUnsupportedAPI) {
t.Errorf("Expected error, expected %v, received %v", errs.ErrUnsupportedAPI, err)
}
})
t.Run("Get No Head", func(t *testing.T) {
r, err := ref.New("nohead." + tsInternalHost + "/" + repoPath + ":" + noheadTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
m, err := rc.ManifestGet(ctx, r)
if err != nil {
t.Fatalf("Failed running ManifestGet: %v", err)
}
if manifest.GetMediaType(m) != mediatype.Docker2Manifest {
t.Errorf("Unexpected media type: %s", manifest.GetMediaType(m))
}
if m.GetDescriptor().Digest != mDigest {
t.Errorf("Unexpected digest: %s", m.GetDescriptor().Digest.String())
}
})
t.Run("Missing", func(t *testing.T) {
r, err := ref.New("missing." + tsOlaregHost + "/" + repoPath + ":" + missingTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
m, err := rc.ManifestGet(ctx, r)
if err == nil {
t.Errorf("Success running ManifestGet on missing ref: %v", m)
return
}
})
t.Run("Get Platform", func(t *testing.T) {
r, err := ref.New(tsOlaregHost + "/" + repoPath + ":" + goodTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
p, err := platform.Parse("linux/amd64")
if err != nil {
t.Fatalf("Failed parsing platform: %v", err)
}
m, err := rc.ManifestGet(ctx, r, WithManifestPlatform(p))
if err != nil {
t.Fatalf("Failed running ManifestGet: %v", err)
}
if !m.IsSet() {
t.Errorf("manifest is not set on a get request")
}
if m.IsList() {
t.Errorf("returned manifest is an index")
}
})
t.Run("Get Missing Platform", func(t *testing.T) {
r, err := ref.New(tsOlaregHost + "/" + repoPath + ":" + goodTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
p, err := platform.Parse("linux/ppc64le")
if err != nil {
t.Fatalf("Failed parsing platform: %v", err)
}
_, err = rc.ManifestGet(ctx, r, WithManifestPlatform(p))
if err == nil {
t.Fatalf("Success running ManifestGet on missing platform")
}
if !errors.Is(err, errs.ErrNotFound) {
t.Errorf("Expected error %v, received %v", errs.ErrNotFound, err)
}
})
t.Run("Head Platform", func(t *testing.T) {
r, err := ref.New(tsOlaregHost + "/" + repoPath + ":" + goodTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
p, err := platform.Parse("linux/amd64")
if err != nil {
t.Fatalf("Failed parsing platform: %v", err)
}
m, err := rc.ManifestHead(ctx, r, WithManifestPlatform(p))
if err != nil {
t.Fatalf("Failed running ManifestHead: %v", err)
}
if m.IsSet() {
t.Errorf("manifest is set on a head request")
}
if m.IsList() {
t.Errorf("returned manifest is an index")
}
})
t.Run("Head Missing Platform", func(t *testing.T) {
r, err := ref.New(tsOlaregHost + "/" + repoPath + ":" + goodTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
p, err := platform.Parse("linux/ppc64le")
if err != nil {
t.Fatalf("Failed parsing platform: %v", err)
}
_, err = rc.ManifestHead(ctx, r, WithManifestPlatform(p))
if err == nil {
t.Fatalf("Success running ManifestHead on missing platform")
}
if !errors.Is(err, errs.ErrNotFound) {
t.Errorf("Expected error %v, received %v", errs.ErrNotFound, err)
}
})
t.Run("Data", func(t *testing.T) {
r, err := ref.New(tsInternalHost + "/" + repoPath + ":data")
if err != nil {
t.Errorf("Failed creating ref: %v", err)
}
d := descriptor.Descriptor{
MediaType: mediatype.Docker2Manifest,
Size: int64(mLen),
Digest: mDigest,
Data: mBody,
}
m, err := rc.ManifestGet(ctx, r, WithManifestDesc(d))
if err != nil {
t.Fatalf("failed running ManifestGet: %v", err)
}
mBodyOut, err := m.RawBody()
if err != nil {
t.Fatalf("failed running RawBody: %v", err)
}
if !bytes.Equal(mBody, mBodyOut) {
t.Errorf("manifest body mismatch: expected %s, received %s", string(mBody), string(mBodyOut))
}
})
t.Run("Data fallback", func(t *testing.T) {
r, err := ref.New(tsInternalHost + "/" + repoPath + ":" + goodTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
d := descriptor.Descriptor{
MediaType: mediatype.Docker2Manifest,
Size: int64(mLen),
Digest: mDigest,
Data: []byte("invalid data"),
}
_, err = rc.ManifestGet(ctx, r, WithManifestDesc(d))
if err != nil {
t.Errorf("Failed running ManifestGet: %v", err)
return
}
})
t.Run("Bad Data and Found Digest", func(t *testing.T) {
r, err := ref.New(tsInternalHost + "/" + repoPath + ":" + missingTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
d := descriptor.Descriptor{
MediaType: mediatype.Docker2Manifest,
Size: int64(mLen),
Digest: mDigest,
Data: []byte("invalid data"),
}
_, err = rc.ManifestGet(ctx, r, WithManifestDesc(d))
if err != nil {
t.Errorf("get with descriptor failed, didn't fall back to digest")
return
}
})
t.Run("Bad Data and Missing Digest", func(t *testing.T) {
r, err := ref.New("missing." + tsOlaregHost + "/" + repoPath + ":" + missingTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
d := descriptor.Descriptor{
MediaType: mediatype.Docker2Manifest,
Size: int64(mLen),
Digest: missingDigest,
Data: []byte("invalid data"),
}
_, err = rc.ManifestGet(ctx, r, WithManifestDesc(d))
if err == nil {
t.Errorf("Success running ManifestGet on missing ref")
return
}
})
t.Run("Invalid ref", func(t *testing.T) {
r, err := ref.NewHost("registry.example.org")
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
_, err = rc.ManifestGet(ctx, r)
if !errors.Is(err, errs.ErrInvalidReference) {
t.Errorf("ManifestGet did not respond with invalid ref: %v", err)
}
_, err = rc.ManifestHead(ctx, r)
if !errors.Is(err, errs.ErrInvalidReference) {
t.Errorf("ManifestGet did not respond with invalid ref: %v", err)
}
})
t.Run("Delete", func(t *testing.T) {
r, err := ref.New(tsOlaregHost + "/" + repoPath + ":" + deleteTag)
if err != nil {
t.Fatalf("Failed creating ref: %v", err)
}
m, err := rc.ManifestGet(ctx, r)
if err != nil {
t.Fatalf("failed to run get request: %v", err)
}
r = r.SetDigest(m.GetDescriptor().Digest.String())
err = rc.ManifestDelete(ctx, r, WithManifest(m), WithManifestCheckReferrers())
if err != nil {
t.Errorf("ManifestDelete failed: %v", err)
}
_, err = rc.ManifestHead(ctx, r)
if !errors.Is(err, errs.ErrNotFound) {
t.Fatalf("head after delete did not return a non-found: %v", err)
}
})
}