mirror of
https://github.com/regclient/regclient.git
synced 2025-04-17 11:37:11 +03:00
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>
480 lines
14 KiB
Go
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)
|
|
}
|
|
|
|
})
|
|
}
|