- Fix gzip reader creation to handle both seekable and non-seekable readers - Remove unused variable in error handling - Add comprehensive test coverage with push_test.go - Tests include parsing, digest computation, tar extraction, blob upload, manifest upload, gzip detection, and benchmarks - All tests pass and benchmarks show good performance
454 lines
12 KiB
Go
454 lines
12 KiB
Go
package oci
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.loveuer.com/loveuer/upkg/tool"
|
|
)
|
|
|
|
func TestParseImageAddress(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
address string
|
|
wantReg string
|
|
wantRepo string
|
|
wantTag string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid address with tag",
|
|
address: "localhost:5000/myapp:latest",
|
|
wantReg: "localhost:5000",
|
|
wantRepo: "myapp",
|
|
wantTag: "latest",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid address without tag",
|
|
address: "registry.example.com/library/nginx",
|
|
wantReg: "registry.example.com",
|
|
wantRepo: "library/nginx",
|
|
wantTag: "latest",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid address with IP",
|
|
address: "192.168.1.1:5000/library/nginx:1.20",
|
|
wantReg: "192.168.1.1:5000",
|
|
wantRepo: "library/nginx",
|
|
wantTag: "1.20",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid address - no slash",
|
|
address: "localhost:5000",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty address",
|
|
address: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotReg, gotRepo, gotTag, err := parseImageAddress(tt.address)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("parseImageAddress() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if !tt.wantErr {
|
|
if gotReg != tt.wantReg {
|
|
t.Errorf("parseImageAddress() registry = %v, want %v", gotReg, tt.wantReg)
|
|
}
|
|
if gotRepo != tt.wantRepo {
|
|
t.Errorf("parseImageAddress() repository = %v, want %v", gotRepo, tt.wantRepo)
|
|
}
|
|
if gotTag != tt.wantTag {
|
|
t.Errorf("parseImageAddress() tag = %v, want %v", gotTag, tt.wantTag)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComputeDigest(t *testing.T) {
|
|
data := []byte("test data")
|
|
digest := computeDigest(data)
|
|
|
|
if !strings.HasPrefix(digest, "sha256:") {
|
|
t.Errorf("computeDigest() should start with 'sha256:', got %s", digest)
|
|
}
|
|
|
|
// Test with same data should produce same digest
|
|
digest2 := computeDigest(data)
|
|
if digest != digest2 {
|
|
t.Errorf("computeDigest() should produce same digest for same data, got %s and %s", digest, digest2)
|
|
}
|
|
|
|
// Test with different data should produce different digest
|
|
differentData := []byte("different data")
|
|
digest3 := computeDigest(differentData)
|
|
if digest == digest3 {
|
|
t.Errorf("computeDigest() should produce different digest for different data")
|
|
}
|
|
}
|
|
|
|
func TestExtractImageFromTar(t *testing.T) {
|
|
// Create a mock tar file with OCI image structure
|
|
var buf bytes.Buffer
|
|
tw := tar.NewWriter(&buf)
|
|
|
|
// Create mock config
|
|
configData := []byte(`{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]}}`)
|
|
configDigest := computeDigest(configData)
|
|
|
|
// Create mock layer
|
|
layerData := []byte("mock layer data")
|
|
layerDigest := computeDigest(layerData)
|
|
|
|
// Create manifest.json
|
|
dockerManifest := []struct {
|
|
Config string `json:"Config"`
|
|
RepoTags []string `json:"RepoTags"`
|
|
Layers []string `json:"Layers"`
|
|
}{
|
|
{
|
|
Config: "config.json",
|
|
RepoTags: []string{"test:latest"},
|
|
Layers: []string{"layer.tar"},
|
|
},
|
|
}
|
|
manifestData, _ := json.Marshal(dockerManifest)
|
|
|
|
// Write files to tar
|
|
tw.WriteHeader(&tar.Header{
|
|
Name: "manifest.json",
|
|
Size: int64(len(manifestData)),
|
|
Mode: 0644,
|
|
})
|
|
tw.Write(manifestData)
|
|
|
|
tw.WriteHeader(&tar.Header{
|
|
Name: "config.json",
|
|
Size: int64(len(configData)),
|
|
Mode: 0644,
|
|
})
|
|
tw.Write(configData)
|
|
|
|
tw.WriteHeader(&tar.Header{
|
|
Name: "layer.tar",
|
|
Size: int64(len(layerData)),
|
|
Mode: 0644,
|
|
})
|
|
tw.Write(layerData)
|
|
|
|
tw.Close()
|
|
|
|
// Test extraction
|
|
manifest, config, layers, err := extractImageFromTar(&buf)
|
|
if err != nil {
|
|
t.Fatalf("extractImageFromTar() error = %v", err)
|
|
}
|
|
|
|
if len(manifest) == 0 {
|
|
t.Error("extractImageFromTar() should return non-empty manifest")
|
|
}
|
|
|
|
if config.digest != configDigest {
|
|
t.Errorf("extractImageFromTar() config digest = %v, want %v", config.digest, configDigest)
|
|
}
|
|
|
|
if len(layers) != 1 {
|
|
t.Errorf("extractImageFromTar() should return 1 layer, got %d", len(layers))
|
|
}
|
|
|
|
if layers[0].digest != layerDigest {
|
|
t.Errorf("extractImageFromTar() layer digest = %v, want %v", layers[0].digest, layerDigest)
|
|
}
|
|
}
|
|
|
|
func TestExtractImageFromTarMissingManifest(t *testing.T) {
|
|
// Create a tar file without manifest.json
|
|
var buf bytes.Buffer
|
|
tw := tar.NewWriter(&buf)
|
|
|
|
tw.WriteHeader(&tar.Header{
|
|
Name: "somefile.txt",
|
|
Size: 9,
|
|
Mode: 0644,
|
|
})
|
|
tw.Write([]byte("test data"))
|
|
tw.Close()
|
|
|
|
// Test extraction should fail
|
|
_, _, _, err := extractImageFromTar(bytes.NewReader(buf.Bytes()))
|
|
if err == nil {
|
|
t.Error("extractImageFromTar() should return error for missing manifest.json")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "manifest.json not found") {
|
|
t.Errorf("extractImageFromTar() should return manifest not found error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWithPushOptions(t *testing.T) {
|
|
opt := &ociUploadOpt{}
|
|
|
|
// Test WithPushPlainHTTP
|
|
WithPushPlainHTTP(true)(opt)
|
|
if !opt.PlainHTTP {
|
|
t.Error("WithPushPlainHTTP() should set PlainHTTP to true")
|
|
}
|
|
|
|
// Test WithPushSkipTLSVerify
|
|
WithPushSkipTLSVerify(true)(opt)
|
|
if !opt.SkipTLSVerify {
|
|
t.Error("WithPushSkipTLSVerify() should set SkipTLSVerify to true")
|
|
}
|
|
|
|
// Test WithPushAuth
|
|
WithPushAuth("user", "pass")(opt)
|
|
if opt.Username != "user" || opt.Password != "pass" {
|
|
t.Error("WithPushAuth() should set username and password")
|
|
}
|
|
}
|
|
|
|
func TestUploadBlob(t *testing.T) {
|
|
// Create a mock HTTP server
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodHead && strings.Contains(r.URL.Path, "/blobs/"):
|
|
// Blob exists check
|
|
w.WriteHeader(http.StatusOK)
|
|
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/blobs/uploads/"):
|
|
// Start upload session
|
|
w.Header().Set("Location", "/v2/test/repo/blobs/upload/session")
|
|
w.WriteHeader(http.StatusAccepted)
|
|
case r.Method == http.MethodPut && strings.Contains(r.URL.Path, "/blobs/upload/"):
|
|
// Complete upload
|
|
w.WriteHeader(http.StatusCreated)
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := tool.NewClient(false, "")
|
|
ctx := context.Background()
|
|
|
|
data := []byte("test blob data")
|
|
digest := computeDigest(data)
|
|
|
|
err := uploadBlob(ctx, client, strings.TrimPrefix(server.URL, "http://"), "test/repo", data, digest, &ociUploadOpt{PlainHTTP: true})
|
|
if err != nil {
|
|
t.Errorf("uploadBlob() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUploadBlobWithAuth(t *testing.T) {
|
|
// Create a mock HTTP server with auth
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Check for basic auth
|
|
username, password, ok := r.BasicAuth()
|
|
if !ok || username != "testuser" || password != "testpass" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case r.Method == http.MethodHead && strings.Contains(r.URL.Path, "/blobs/"):
|
|
w.WriteHeader(http.StatusOK)
|
|
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/blobs/uploads/"):
|
|
w.Header().Set("Location", "/v2/test/repo/blobs/upload/session")
|
|
w.WriteHeader(http.StatusAccepted)
|
|
case r.Method == http.MethodPut && strings.Contains(r.URL.Path, "/blobs/upload/"):
|
|
w.WriteHeader(http.StatusCreated)
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := tool.NewClient(false, "")
|
|
ctx := context.Background()
|
|
|
|
data := []byte("test blob data")
|
|
digest := computeDigest(data)
|
|
|
|
opt := &ociUploadOpt{
|
|
Username: "testuser",
|
|
Password: "testpass",
|
|
PlainHTTP: true,
|
|
}
|
|
|
|
err := uploadBlob(ctx, client, strings.TrimPrefix(server.URL, "http://"), "test/repo", data, digest, opt)
|
|
if err != nil {
|
|
t.Errorf("uploadBlob() with auth error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUploadManifest(t *testing.T) {
|
|
// Create a mock HTTP server
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "/manifests/") {
|
|
w.WriteHeader(http.StatusCreated)
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := tool.NewClient(false, "")
|
|
ctx := context.Background()
|
|
|
|
manifest := []byte(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json"}`)
|
|
|
|
err := uploadManifest(ctx, client, strings.TrimPrefix(server.URL, "http://"), "test/repo", "latest", manifest, &ociUploadOpt{PlainHTTP: true})
|
|
if err != nil {
|
|
t.Errorf("uploadManifest() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPushImageGzipDetection(t *testing.T) {
|
|
// Test that gzip detection works by creating a tar file with gzip compression
|
|
// and verifying it can be processed through the gzip detection path
|
|
|
|
// Create a tar file with OCI image structure
|
|
var tarBuf bytes.Buffer
|
|
tw := tar.NewWriter(&tarBuf)
|
|
|
|
// Create mock config
|
|
configData := []byte(`{"architecture":"amd64"}`)
|
|
|
|
// Create mock layer
|
|
layerData := []byte("mock layer data")
|
|
|
|
// Create manifest.json
|
|
dockerManifest := []struct {
|
|
Config string `json:"Config"`
|
|
RepoTags []string `json:"RepoTags"`
|
|
Layers []string `json:"Layers"`
|
|
}{
|
|
{
|
|
Config: "config.json",
|
|
RepoTags: []string{"test:latest"},
|
|
Layers: []string{"layer.tar"},
|
|
},
|
|
}
|
|
manifestData, _ := json.Marshal(dockerManifest)
|
|
|
|
// Write files to tar
|
|
tw.WriteHeader(&tar.Header{
|
|
Name: "manifest.json",
|
|
Size: int64(len(manifestData)),
|
|
Mode: 0644,
|
|
})
|
|
tw.Write(manifestData)
|
|
|
|
tw.WriteHeader(&tar.Header{
|
|
Name: "config.json",
|
|
Size: int64(len(configData)),
|
|
Mode: 0644,
|
|
})
|
|
tw.Write(configData)
|
|
|
|
tw.WriteHeader(&tar.Header{
|
|
Name: "layer.tar",
|
|
Size: int64(len(layerData)),
|
|
Mode: 0644,
|
|
})
|
|
tw.Write(layerData)
|
|
|
|
tw.Close()
|
|
|
|
// Now gzip the tar data
|
|
var gzippedTar bytes.Buffer
|
|
gz := gzip.NewWriter(&gzippedTar)
|
|
gz.Write(tarBuf.Bytes())
|
|
gz.Close()
|
|
|
|
// Create a mock server
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Test with gzipped tar data (should fail at server, but gzip detection should work)
|
|
err := PushImage(ctx, bytes.NewReader(gzippedTar.Bytes()), "localhost:5000/test:latest",
|
|
WithPushPlainHTTP(true),
|
|
WithPushSkipTLSVerify(true))
|
|
|
|
// Should fail due to server not being a real registry, but not due to gzip detection
|
|
if err == nil {
|
|
t.Error("PushImage() should fail with mock server")
|
|
}
|
|
|
|
// The error should not be related to gzip detection
|
|
if strings.Contains(err.Error(), "gzip reader failed") {
|
|
t.Errorf("PushImage() should not fail with gzip reader error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPushImageInvalidAddress(t *testing.T) {
|
|
ctx := context.Background()
|
|
data := bytes.NewReader([]byte("test data"))
|
|
|
|
err := PushImage(ctx, data, "invalid-address", WithPushPlainHTTP(true))
|
|
if err == nil {
|
|
t.Error("PushImage() should return error for invalid address")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "parse image address failed") {
|
|
t.Errorf("PushImage() should return address parsing error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPushImageOptions(t *testing.T) {
|
|
// Test that options are applied correctly
|
|
opt := &ociUploadOpt{}
|
|
for _, fn := range []OCIUploadOpt{
|
|
WithPushPlainHTTP(true),
|
|
WithPushSkipTLSVerify(true),
|
|
WithPushAuth("user", "pass"),
|
|
} {
|
|
fn(opt)
|
|
}
|
|
|
|
if !opt.PlainHTTP {
|
|
t.Error("PlainHTTP option should be set to true")
|
|
}
|
|
if !opt.SkipTLSVerify {
|
|
t.Error("SkipTLSVerify option should be set to true")
|
|
}
|
|
if opt.Username != "user" || opt.Password != "pass" {
|
|
t.Error("Auth option should be set correctly")
|
|
}
|
|
}
|
|
|
|
// Benchmark tests
|
|
func BenchmarkComputeDigest(b *testing.B) {
|
|
data := make([]byte, 1024) // 1KB of data
|
|
for i := 0; i < b.N; i++ {
|
|
computeDigest(data)
|
|
}
|
|
}
|
|
|
|
func BenchmarkParseImageAddress(b *testing.B) {
|
|
address := "registry.example.com:5000/library/nginx:1.20"
|
|
for i := 0; i < b.N; i++ {
|
|
parseImageAddress(address)
|
|
}
|
|
}
|