1
0
mirror of https://github.com/regclient/regclient.git synced 2025-04-17 11:37:11 +03:00
regclient/image_test.go
Brandon Mitchell 95c9152941
Chore: Modernize Go to the 1.22 specs
- Use "any" instead of an empty interface.
- Use range over an integer for for loops.
- Remove shadow variables in loops now that Go no longer reuses the variable.
- Use "slices.Contains", "slices.Delete", "slices.Equal", "slices.Index", "slices.SortFunc".
- Use "cmp.Or", "min", and "max".
- Use "fmt.Appendf" instead of "Sprintf" for generating a byte slice.
- Use "errors.Join" or "fmt.Errorf" with multiple "%w" for multiple errors.

Additionally, use modern regclient features:

- Use "ref.SetTag", "ref.SetDigest", and "ref.AddDigest".
- Call "regclient.ManifestGet" using "WithManifestDesc" instead of setting the digest on the reference.

Signed-off-by: Brandon Mitchell <git@bmitch.net>
2025-02-18 14:32:06 -05:00

533 lines
14 KiB
Go

package regclient
import (
"archive/tar"
"context"
"errors"
"io"
"log/slog"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"time"
"github.com/olareg/olareg"
oConfig "github.com/olareg/olareg/config"
"github.com/regclient/regclient/config"
"github.com/regclient/regclient/internal/copyfs"
"github.com/regclient/regclient/scheme/reg"
"github.com/regclient/regclient/types/errs"
"github.com/regclient/regclient/types/ref"
)
func TestImageCheckBase(t *testing.T) {
t.Parallel()
ctx := context.Background()
regHandler := olareg.New(oConfig.Config{
Storage: oConfig.ConfigStorage{
StoreType: oConfig.StoreMem,
RootDir: "./testdata",
},
})
ts := httptest.NewServer(regHandler)
tsURL, _ := url.Parse(ts.URL)
tsHost := tsURL.Host
t.Cleanup(func() {
ts.Close()
_ = regHandler.Close()
})
rcHosts := []config.Host{
{
Name: tsHost,
Hostname: tsHost,
TLS: config.TLSDisabled,
},
{
Name: "registry.example.org",
Hostname: tsHost,
TLS: config.TLSDisabled,
},
}
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),
WithRegOpts(reg.WithDelay(delayInit, delayMax)),
)
rb1, err := ref.New(tsHost + "/testrepo:b1")
if err != nil {
t.Fatalf("failed to setup ref: %v", err)
}
rb2, err := ref.New(tsHost + "/testrepo:b2")
if err != nil {
t.Fatalf("failed to setup ref: %v", err)
}
rb3, err := ref.New(tsHost + "/testrepo:b3")
if err != nil {
t.Fatalf("failed to setup ref: %v", err)
}
m3, err := rc.ManifestHead(ctx, rb3)
if err != nil {
t.Fatalf("failed to get digest for base3: %v", err)
}
dig3 := m3.GetDescriptor().Digest
r1, err := ref.New(tsHost + "/testrepo:v1")
if err != nil {
t.Fatalf("failed to setup ref: %v", err)
}
r2, err := ref.New(tsHost + "/testrepo:v2")
if err != nil {
t.Fatalf("failed to setup ref: %v", err)
}
r3, err := ref.New(tsHost + "/testrepo:v3")
if err != nil {
t.Fatalf("failed to setup ref: %v", err)
}
tt := []struct {
name string
opts []ImageOpts
r ref.Ref
expectErr error
}{
{
name: "missing annotation",
r: r1,
expectErr: errs.ErrMissingAnnotation,
},
{
name: "annotation v2",
r: r2,
expectErr: errs.ErrMismatch,
},
{
name: "annotation v3",
r: r3,
expectErr: errs.ErrMismatch,
},
{
name: "manual v2, b1",
r: r2,
opts: []ImageOpts{ImageWithCheckBaseRef(rb1.CommonName())},
},
{
name: "manual v2, b2",
r: r2,
opts: []ImageOpts{ImageWithCheckBaseRef(rb2.CommonName())},
expectErr: errs.ErrMismatch,
},
{
name: "manual v2, b3",
r: r2,
opts: []ImageOpts{ImageWithCheckBaseRef(rb3.CommonName())},
expectErr: errs.ErrMismatch,
},
{
name: "manual v3, b1",
r: r3,
opts: []ImageOpts{ImageWithCheckBaseRef(rb1.CommonName())},
},
{
name: "manual v3, b3 with digest",
r: r3,
opts: []ImageOpts{ImageWithCheckBaseRef(rb3.CommonName()), ImageWithCheckBaseDigest(dig3.String())},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
err := rc.ImageCheckBase(ctx, tc.r, tc.opts...)
if tc.expectErr != nil {
if err == nil {
t.Errorf("check base did not fail")
} else if err.Error() != tc.expectErr.Error() && !errors.Is(err, tc.expectErr) {
t.Errorf("error mismatch, expected %v, received %v", tc.expectErr, err)
}
} else {
if err != nil {
t.Errorf("check base failed")
}
}
})
}
}
func TestImageConfig(t *testing.T) {
t.Parallel()
ctx := context.Background()
regHandler := olareg.New(oConfig.Config{
Storage: oConfig.ConfigStorage{
StoreType: oConfig.StoreMem,
RootDir: "./testdata",
},
})
ts := httptest.NewServer(regHandler)
t.Cleanup(func() {
ts.Close()
_ = regHandler.Close()
})
tsURL, _ := url.Parse(ts.URL)
tsHost := tsURL.Host
rcHosts := []config.Host{
{
Name: tsHost,
Hostname: tsHost,
TLS: config.TLSDisabled,
},
}
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),
)
tt := []struct {
name string
r string
opts []ImageOpts
expectErr error
expectArch string
expectOS string
}{
{
name: "ocidir-v1-amd64",
r: "ocidir://testdata/testrepo:v1",
opts: []ImageOpts{ImageWithPlatform("linux/amd64")},
expectArch: "amd64",
expectOS: "linux",
},
{
name: "ocidir-not-found",
r: "ocidir://testdata/testrepo:missing",
expectErr: errs.ErrNotFound,
},
{
name: "ocidir-a1",
r: "ocidir://testdata/testrepo:a1",
opts: []ImageOpts{},
expectErr: errs.ErrUnsupportedMediaType,
},
{
name: "reg-v2-arm64",
r: tsHost + "/testrepo:v2",
opts: []ImageOpts{ImageWithPlatform("linux/arm64")},
expectArch: "arm64",
expectOS: "linux",
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
r, err := ref.New(tc.r)
if err != nil {
t.Fatalf("failed to parse ref: %v", err)
}
bConf, err := rc.ImageConfig(ctx, r, tc.opts...)
if tc.expectErr != nil {
if err == nil {
t.Fatalf("method did not fail")
}
if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() {
t.Errorf("unexpected error, expected %v, received %v", tc.expectErr, err)
}
return
}
if err != nil {
t.Fatalf("method failed: %v", err)
}
c := bConf.GetConfig()
if (tc.expectOS != "" && tc.expectOS != c.OS) || (tc.expectArch != "" && tc.expectArch != c.Architecture) {
t.Errorf("unexpected config, expected %s/%s, received %s/%s", tc.expectOS, tc.expectArch, c.OS, c.Architecture)
}
})
}
}
func TestCopy(t *testing.T) {
t.Parallel()
ctx := context.Background()
boolT := true
regHandler := olareg.New(oConfig.Config{
Storage: oConfig.ConfigStorage{
StoreType: oConfig.StoreMem,
RootDir: "./testdata",
},
})
regROHandler := olareg.New(oConfig.Config{
Storage: oConfig.ConfigStorage{
StoreType: oConfig.StoreMem,
RootDir: "./testdata",
ReadOnly: &boolT,
},
})
ts := httptest.NewServer(regHandler)
tsRO := httptest.NewServer(regROHandler)
t.Cleanup(func() {
ts.Close()
_ = regHandler.Close()
tsRO.Close()
_ = regROHandler.Close()
})
tsURL, _ := url.Parse(ts.URL)
tsHost := tsURL.Host
tsROURL, _ := url.Parse(tsRO.URL)
tsROHost := tsROURL.Host
rcHosts := []config.Host{
{
Name: tsHost,
Hostname: tsHost,
TLS: config.TLSDisabled,
},
{
Name: tsROHost,
Hostname: tsROHost,
TLS: config.TLSDisabled,
},
}
rReferrerSrc, err := ref.New("ocidir://./testdata/external")
if err != nil {
t.Fatalf("failed to parse referrer src repo: %v", err)
}
rReferrerTgt, err := ref.New(tsHost + "/dest-external")
if err != nil {
t.Fatalf("failed to parse referrer tgt repo: %v", err)
}
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),
)
tempDir := t.TempDir()
tt := []struct {
name string
src, tgt string
opts []ImageOpts
expectErr error
}{
{
name: "ocidir to registry",
src: "ocidir://./testdata/testrepo:v1",
tgt: tsHost + "/dest-ocidir:v1",
},
{
name: "ocidir to read-only registry",
src: "ocidir://./testdata/testrepo:v1",
tgt: tsROHost + "/dest-ocidir:v1",
expectErr: errs.ErrHTTPStatus,
},
{
name: "ocidir to ocidir",
src: "ocidir://./testdata/testrepo:v1",
tgt: "ocidir://" + tempDir + "/testrepo:v1",
},
{
name: "registry to registry",
src: tsHost + "/testrepo:v1",
tgt: tsHost + "/dest-reg:v1",
},
{
name: "registry to registry same repo",
src: tsHost + "/testrepo:v1",
tgt: tsHost + "/testrepo:v1-copy",
},
{
name: "ocidir to registry with referrers and digest tags",
src: "ocidir://./testdata/testrepo:v2",
tgt: tsHost + "/dest-ocidir:v2",
opts: []ImageOpts{ImageWithReferrers(), ImageWithDigestTags()},
},
{
name: "ocidir to ocidir with referrers and digest tags",
src: "ocidir://./testdata/testrepo:v2",
tgt: "ocidir://" + tempDir + "/testrepo:v2",
opts: []ImageOpts{ImageWithReferrers(), ImageWithDigestTags()},
},
{
name: "registry to registry with referrers and digest tags",
src: tsHost + "/testrepo:v2",
tgt: tsHost + "/dest-reg:v2",
opts: []ImageOpts{ImageWithReferrers(), ImageWithDigestTags()},
},
{
name: "ocidir to registry with external referrers and digest tags",
src: "ocidir://./testdata/testrepo:v2",
tgt: tsHost + "/dest-ocidir:v2",
opts: []ImageOpts{ImageWithReferrers(), ImageWithDigestTags(), ImageWithReferrerSrc(rReferrerSrc), ImageWithReferrerTgt(rReferrerTgt)},
},
{
name: "ocidir to registry with fast check",
src: "ocidir://./testdata/testrepo:v3",
tgt: tsHost + "/testrepo:v3-copy",
opts: []ImageOpts{ImageWithReferrers(), ImageWithDigestTags(), ImageWithFastCheck()},
},
{
name: "ocidir to registry child/loop",
src: "ocidir://./testdata/testrepo:child",
tgt: tsHost + "/dest-ocidir:child",
opts: []ImageOpts{ImageWithReferrers(), ImageWithDigestTags()},
},
{
name: "ocidir to ocidir child/loop",
src: "ocidir://./testdata/testrepo:child",
tgt: "ocidir://" + tempDir + "/testrepo:child",
opts: []ImageOpts{ImageWithReferrers(), ImageWithDigestTags()},
},
{
name: "registry to registry child/loop",
src: tsHost + "/testrepo:child",
tgt: tsHost + "/dest-reg:child",
opts: []ImageOpts{ImageWithReferrers(), ImageWithDigestTags()},
},
{
name: "ocidir to registry mirror digest tag",
src: "ocidir://./testdata/testrepo:mirror",
tgt: tsHost + "/dest-ocidir:mirror",
opts: []ImageOpts{ImageWithDigestTags()},
},
{
name: "ocidir to ocidir mirror digest tag",
src: "ocidir://./testdata/testrepo:mirror",
tgt: "ocidir://" + tempDir + "/testrepo:mirror",
opts: []ImageOpts{ImageWithDigestTags()},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
rSrc, err := ref.New(tc.src)
if err != nil {
t.Fatalf("failed to parse ref %s: %v", tc.src, err)
}
rTgt, err := ref.New(tc.tgt)
if err != nil {
t.Fatalf("failed to parse ref %s: %v", tc.tgt, err)
}
err = rc.ImageCopy(ctx, rSrc, rTgt, tc.opts...)
if tc.expectErr != nil {
if err == nil {
t.Errorf("copy did not fail, expected %v", tc.expectErr)
} else if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() {
t.Errorf("unexpected error, expected %v, received %v", tc.expectErr, err)
}
return
}
if err != nil {
t.Fatalf("copy failed: %v", err)
}
})
}
}
func TestExportImport(t *testing.T) {
t.Parallel()
ctx := context.Background()
// copy testdata images into tempdir
tempDir := t.TempDir()
err := copyfs.Copy(tempDir+"/testrepo", "testdata/testrepo")
if err != nil {
t.Fatalf("failed to copyfs to tempdir: %v", err)
}
// create regclient
rc := New()
rIn1, err := ref.New("ocidir://" + tempDir + "/testrepo:v1")
if err != nil {
t.Fatalf("failed to parse ref: %v", err)
}
rOut1, err := ref.New("ocidir://" + tempDir + "/testout:v1")
if err != nil {
t.Fatalf("failed to parse ref: %v", err)
}
rIn3, err := ref.New("ocidir://" + tempDir + "/testrepo:v3")
if err != nil {
t.Fatalf("failed to parse ref: %v", err)
}
rOut3, err := ref.New("ocidir://" + tempDir + "/testout:v3")
if err != nil {
t.Fatalf("failed to parse ref: %v", err)
}
// export repo to tar
fileOut1, err := os.Create(filepath.Join(tempDir, "test1.tar"))
if err != nil {
t.Fatalf("failed to create output tar: %v", err)
}
err = rc.ImageExport(ctx, rIn1, fileOut1)
fileOut1.Close()
if err != nil {
t.Errorf("failed to export: %v", err)
}
fileOut3, err := os.Create(filepath.Join(tempDir, "test3.tar.gz"))
if err != nil {
t.Fatalf("failed to create output tar: %v", err)
}
err = rc.ImageExport(ctx, rIn3, fileOut3, ImageWithExportCompress())
fileOut3.Close()
if err != nil {
t.Errorf("failed to export: %v", err)
}
// modify tar for tests
fileR, err := os.Open(filepath.Join(tempDir, "test1.tar"))
if err != nil {
t.Fatalf("failed to open tar: %v", err)
}
fileW, err := os.Create(filepath.Join(tempDir, "test2.tar"))
if err != nil {
t.Errorf("failed to create tar: %v", err)
}
tr := tar.NewReader(fileR)
tw := tar.NewWriter(fileW)
for {
th, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
t.Errorf("failed to read tar header: %v", err)
}
th.Name = "./" + th.Name
err = tw.WriteHeader(th)
if err != nil {
t.Errorf("failed to write tar header: %v", err)
}
if th.Size > 0 {
_, err = io.Copy(tw, tr)
if err != nil {
t.Errorf("failed to copy tar file contents %s: %v", th.Name, err)
}
}
}
fileR.Close()
fileW.Close()
// import tar to repo
fileIn2, err := os.Open(filepath.Join(tempDir, "test2.tar"))
if err != nil {
t.Fatalf("failed to open tar: %v", err)
}
defer fileIn2.Close()
err = rc.ImageImport(ctx, rOut1, fileIn2)
if err != nil {
t.Errorf("failed to import: %v", err)
}
fileIn3, err := os.Open(filepath.Join(tempDir, "test3.tar.gz"))
if err != nil {
t.Fatalf("failed to open tar: %v", err)
}
defer fileIn3.Close()
err = rc.ImageImport(ctx, rOut3, fileIn3)
if err != nil {
t.Errorf("failed to import: %v", err)
}
}