214 lines
6.6 KiB
Go
214 lines
6.6 KiB
Go
package docker
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/containers/image/v5/internal/private"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var _ private.ImageSource = (*dockerImageSource)(nil)
|
|
|
|
func TestDockerImageSourceReference(t *testing.T) {
|
|
manifestPathRegex := regexp.MustCompile("^/v2/.*/manifests/latest$")
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodGet && r.URL.Path == "/v2/":
|
|
rw.WriteHeader(http.StatusOK)
|
|
case r.Method == http.MethodGet && manifestPathRegex.MatchString(r.URL.Path):
|
|
rw.WriteHeader(http.StatusOK)
|
|
// Empty body is good enough for this test
|
|
default:
|
|
require.FailNowf(t, "Unexpected request", "%v %v", r.Method, r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
registryURL, err := url.Parse(server.URL)
|
|
require.NoError(t, err)
|
|
registry := registryURL.Host
|
|
|
|
mirrorConfiguration := strings.ReplaceAll(
|
|
`[[registry]]
|
|
prefix = "primary-override.example.com"
|
|
location = "@REGISTRY@/primary-override"
|
|
|
|
[[registry]]
|
|
location = "with-mirror.example.com"
|
|
|
|
[[registry.mirror]]
|
|
location = "@REGISTRY@/with-mirror"
|
|
`, "@REGISTRY@", registry)
|
|
registriesConf, err := os.CreateTemp("", "docker-image-src")
|
|
require.NoError(t, err)
|
|
defer registriesConf.Close()
|
|
defer os.Remove(registriesConf.Name())
|
|
err = os.WriteFile(registriesConf.Name(), []byte(mirrorConfiguration), 0600)
|
|
require.NoError(t, err)
|
|
|
|
for _, c := range []struct{ input, physical string }{
|
|
{registry + "/no-redirection/busybox:latest", registry + "/no-redirection/busybox:latest"},
|
|
{"primary-override.example.com/busybox:latest", registry + "/primary-override/busybox:latest"},
|
|
{"with-mirror.example.com/busybox:latest", registry + "/with-mirror/busybox:latest"},
|
|
} {
|
|
ref, err := ParseReference("//" + c.input)
|
|
require.NoError(t, err, c.input)
|
|
src, err := ref.NewImageSource(context.Background(), &types.SystemContext{
|
|
RegistriesDirPath: "/this/does/not/exist",
|
|
DockerPerHostCertDirPath: "/this/does/not/exist",
|
|
SystemRegistriesConfPath: registriesConf.Name(),
|
|
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
|
|
})
|
|
require.NoError(t, err, c.input)
|
|
defer src.Close()
|
|
|
|
// The observable behavior
|
|
assert.Equal(t, "//"+c.input, src.Reference().StringWithinTransport(), c.input)
|
|
assert.Equal(t, ref.StringWithinTransport(), src.Reference().StringWithinTransport(), c.input)
|
|
// Also peek into internal state
|
|
src2, ok := src.(*dockerImageSource)
|
|
require.True(t, ok, c.input)
|
|
assert.Equal(t, "//"+c.input, src2.logicalRef.StringWithinTransport(), c.input)
|
|
assert.Equal(t, "//"+c.physical, src2.physicalRef.StringWithinTransport(), c.input)
|
|
}
|
|
}
|
|
|
|
func TestSimplifyContentType(t *testing.T) {
|
|
for _, c := range []struct{ input, expected string }{
|
|
{"", ""},
|
|
{"application/json", "application/json"},
|
|
{"application/json;charset=utf-8", "application/json"},
|
|
{"application/json; charset=utf-8", "application/json"},
|
|
{"application/json ; charset=utf-8", "application/json"},
|
|
{"application/json\t;\tcharset=utf-8", "application/json"},
|
|
{"application/json ;charset=utf-8", "application/json"},
|
|
{`application/json; charset="utf-8"`, "application/json"},
|
|
{"completely invalid", ""},
|
|
} {
|
|
out := simplifyContentType(c.input)
|
|
assert.Equal(t, c.expected, out, c.input)
|
|
}
|
|
}
|
|
|
|
func readNextStream(streams chan io.ReadCloser, errs chan error) ([]byte, error) {
|
|
select {
|
|
case r := <-streams:
|
|
if r == nil {
|
|
return nil, nil
|
|
}
|
|
defer r.Close()
|
|
return io.ReadAll(r)
|
|
case err := <-errs:
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
type verifyGetBlobAtData struct {
|
|
expectedData []byte
|
|
expectedError error
|
|
}
|
|
|
|
func verifyGetBlobAtOutput(t *testing.T, streams chan io.ReadCloser, errs chan error, expected []verifyGetBlobAtData) {
|
|
for _, c := range expected {
|
|
data, err := readNextStream(streams, errs)
|
|
assert.Equal(t, c.expectedData, data)
|
|
assert.Equal(t, c.expectedError, err)
|
|
}
|
|
}
|
|
|
|
func TestSplitHTTP200ResponseToPartial(t *testing.T) {
|
|
body := io.NopCloser(bytes.NewReader([]byte("123456789")))
|
|
defer body.Close()
|
|
streams := make(chan io.ReadCloser)
|
|
errs := make(chan error)
|
|
chunks := []private.ImageSourceChunk{
|
|
{Offset: 1, Length: 2},
|
|
{Offset: 4, Length: 1},
|
|
}
|
|
go splitHTTP200ResponseToPartial(streams, errs, body, chunks)
|
|
|
|
expected := []verifyGetBlobAtData{
|
|
{[]byte("23"), nil},
|
|
{[]byte("5"), nil},
|
|
{[]byte(nil), nil},
|
|
}
|
|
|
|
verifyGetBlobAtOutput(t, streams, errs, expected)
|
|
}
|
|
|
|
func TestHandle206Response(t *testing.T) {
|
|
body := io.NopCloser(bytes.NewReader([]byte("--AAA\r\n\r\n23\r\n--AAA\r\n\r\n5\r\n--AAA--")))
|
|
defer body.Close()
|
|
streams := make(chan io.ReadCloser)
|
|
errs := make(chan error)
|
|
chunks := []private.ImageSourceChunk{
|
|
{Offset: 1, Length: 2},
|
|
{Offset: 4, Length: 1},
|
|
}
|
|
mediaType := "multipart/form-data"
|
|
params := map[string]string{
|
|
"boundary": "AAA",
|
|
}
|
|
go handle206Response(streams, errs, body, chunks, mediaType, params)
|
|
|
|
expected := []verifyGetBlobAtData{
|
|
{[]byte("23"), nil},
|
|
{[]byte("5"), nil},
|
|
{[]byte(nil), nil},
|
|
}
|
|
verifyGetBlobAtOutput(t, streams, errs, expected)
|
|
|
|
body = io.NopCloser(bytes.NewReader([]byte("HELLO")))
|
|
defer body.Close()
|
|
streams = make(chan io.ReadCloser)
|
|
errs = make(chan error)
|
|
chunks = []private.ImageSourceChunk{{Offset: 100, Length: 5}}
|
|
mediaType = "text/plain"
|
|
params = map[string]string{}
|
|
go handle206Response(streams, errs, body, chunks, mediaType, params)
|
|
|
|
expected = []verifyGetBlobAtData{
|
|
{[]byte("HELLO"), nil},
|
|
{[]byte(nil), nil},
|
|
}
|
|
verifyGetBlobAtOutput(t, streams, errs, expected)
|
|
}
|
|
|
|
func TestParseMediaType(t *testing.T) {
|
|
mediaType, params, err := parseMediaType("multipart/byteranges; boundary=CloudFront:3F750DE0752BEDE3882F7DBE80010D31")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, mediaType, "multipart/byteranges")
|
|
assert.Equal(t, params["boundary"], "CloudFront:3F750DE0752BEDE3882F7DBE80010D31")
|
|
|
|
mediaType, params, err = parseMediaType("multipart/byteranges; boundary=00000000000061573284")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, mediaType, "multipart/byteranges")
|
|
assert.Equal(t, params["boundary"], "00000000000061573284")
|
|
|
|
mediaType, params, err = parseMediaType("multipart/byteranges; foo=bar; bar=baz")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, mediaType, "multipart/byteranges")
|
|
assert.Equal(t, params["foo"], "bar")
|
|
assert.Equal(t, params["bar"], "baz")
|
|
|
|
// quoted symbols '@'
|
|
_, params, err = parseMediaType("multipart/byteranges; boundary=\"@:\"")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, params["boundary"], "@:")
|
|
|
|
// unquoted '@'
|
|
_, _, err = parseMediaType("multipart/byteranges; boundary=@")
|
|
require.Error(t, err)
|
|
}
|