Files
moby/integration/plugin/common/plugin_test.go
Sebastiaan van Stijn d3e45f8743 testutil: move back to internal
This package was originally internal, but was moved out when BuildKit
used it for its integration tests. That's no longer the case, so we
can make it internal again.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-08 10:08:30 +02:00

369 lines
11 KiB
Go

package common
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"testing"
c8dimages "github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/moby/moby/api/types"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/jsonmessage"
"github.com/moby/moby/v2/internal/testutil"
"github.com/moby/moby/v2/internal/testutil/daemon"
"github.com/moby/moby/v2/internal/testutil/fixtures/plugin"
"github.com/moby/moby/v2/internal/testutil/registry"
"github.com/moby/moby/v2/internal/testutil/request"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)
// TestPluginInvalidJSON tests that POST endpoints that expect a body return
// the correct error when sending invalid JSON requests.
func TestPluginInvalidJSON(t *testing.T) {
ctx := setupTest(t)
// POST endpoints that accept / expect a JSON body;
endpoints := []string{
"/plugins/foobar/set",
"/plugins/foobar/upgrade",
"/plugins/pull",
}
for _, ep := range endpoints {
t.Run(ep[1:], func(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
t.Run("invalid content type", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
res, body, err := request.Post(ctx, ep, request.RawString("[]"), request.ContentType("text/plain"))
assert.NilError(t, err)
assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
buf, err := request.ReadBody(body)
assert.NilError(t, err)
assert.Check(t, is.Contains(string(buf), "unsupported Content-Type header (text/plain): must be 'application/json'"))
})
t.Run("invalid JSON", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
res, body, err := request.Post(ctx, ep, request.RawString("{invalid json"), request.JSON)
assert.NilError(t, err)
assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
buf, err := request.ReadBody(body)
assert.NilError(t, err)
assert.Check(t, is.Contains(string(buf), "invalid JSON: invalid character 'i' looking for beginning of object key string"))
})
t.Run("extra content after JSON", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
res, body, err := request.Post(ctx, ep, request.RawString(`[] trailing content`), request.JSON)
assert.NilError(t, err)
assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
buf, err := request.ReadBody(body)
assert.NilError(t, err)
assert.Check(t, is.Contains(string(buf), "unexpected content after JSON"))
})
t.Run("empty body", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
// empty body should not produce an 500 internal server error, or
// any 5XX error (this is assuming the request does not produce
// an internal server error for another reason, but it shouldn't)
res, _, err := request.Post(ctx, ep, request.RawString(``), request.JSON)
assert.NilError(t, err)
assert.Check(t, res.StatusCode < http.StatusInternalServerError)
})
})
}
}
func TestPluginInstall(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
skip.If(t, testEnv.IsRootless, "rootless mode has different view of localhost")
ctx := testutil.StartSpan(baseContext, t)
apiclient := testEnv.APIClient()
t.Run("no auth", func(t *testing.T) {
ctx := setupTest(t)
reg := registry.NewV2(t)
defer reg.Close()
name := "test-" + strings.ToLower(t.Name())
repo := path.Join(registry.DefaultURL, name+":latest")
assert.NilError(t, plugin.CreateInRegistry(ctx, repo, nil))
rdr, err := apiclient.PluginInstall(ctx, repo, client.PluginInstallOptions{Disabled: true, RemoteRef: repo})
assert.NilError(t, err)
defer rdr.Close()
_, err = io.Copy(io.Discard, rdr)
assert.NilError(t, err)
_, _, err = apiclient.PluginInspectWithRaw(ctx, repo)
assert.NilError(t, err)
})
t.Run("with digest", func(t *testing.T) {
ctx := setupTest(t)
reg := registry.NewV2(t)
defer reg.Close()
name := "test-" + strings.ToLower(t.Name())
repo := path.Join(registry.DefaultURL, name+":latest")
err := plugin.Create(ctx, apiclient, repo)
assert.NilError(t, err)
rdr, err := apiclient.PluginPush(ctx, repo, "")
assert.NilError(t, err)
defer rdr.Close()
buf := &strings.Builder{}
assert.NilError(t, err)
var digest string
assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(rdr, buf, 0, false, func(j jsonmessage.JSONMessage) {
if j.Aux != nil {
var r types.PushResult
assert.NilError(t, json.Unmarshal(*j.Aux, &r))
digest = r.Digest
}
}), buf)
err = apiclient.PluginRemove(ctx, repo, client.PluginRemoveOptions{Force: true})
assert.NilError(t, err)
rdr, err = apiclient.PluginInstall(ctx, repo, client.PluginInstallOptions{
Disabled: true,
RemoteRef: repo + "@" + digest,
})
assert.NilError(t, err)
defer rdr.Close()
_, err = io.Copy(io.Discard, rdr)
assert.NilError(t, err)
_, _, err = apiclient.PluginInspectWithRaw(ctx, repo)
assert.NilError(t, err)
})
t.Run("with htpasswd", func(t *testing.T) {
ctx := setupTest(t)
reg := registry.NewV2(t, registry.Htpasswd)
defer reg.Close()
name := "test-" + strings.ToLower(t.Name())
repo := path.Join(registry.DefaultURL, name+":latest")
auth := &registrytypes.AuthConfig{ServerAddress: registry.DefaultURL, Username: "testuser", Password: "testpassword"}
assert.NilError(t, plugin.CreateInRegistry(ctx, repo, auth))
authEncoded, err := json.Marshal(auth)
assert.NilError(t, err)
rdr, err := apiclient.PluginInstall(ctx, repo, client.PluginInstallOptions{
RegistryAuth: base64.URLEncoding.EncodeToString(authEncoded),
Disabled: true,
RemoteRef: repo,
})
assert.NilError(t, err)
defer rdr.Close()
_, err = io.Copy(io.Discard, rdr)
assert.NilError(t, err)
_, _, err = apiclient.PluginInspectWithRaw(ctx, repo)
assert.NilError(t, err)
})
t.Run("with insecure", func(t *testing.T) {
skip.If(t, !testEnv.IsLocalDaemon())
ctx := testutil.StartSpan(ctx, t)
addrs, err := net.InterfaceAddrs()
assert.NilError(t, err)
var bindTo string
for _, addr := range addrs {
ip, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ip.IP.IsLoopback() || ip.IP.To4() == nil {
continue
}
bindTo = ip.IP.String()
}
if bindTo == "" {
t.Skip("No suitable interface to bind registry to")
}
regURL := bindTo + ":5000"
d := daemon.New(t)
defer d.Stop(t)
d.Start(t, "--insecure-registry="+regURL)
defer d.Stop(t)
reg := registry.NewV2(t, registry.URL(regURL))
defer reg.Close()
name := "test-" + strings.ToLower(t.Name())
repo := path.Join(regURL, name+":latest")
assert.NilError(t, plugin.CreateInRegistry(ctx, repo, nil, plugin.WithInsecureRegistry(regURL)))
apiclient := d.NewClientT(t)
rdr, err := apiclient.PluginInstall(ctx, repo, client.PluginInstallOptions{Disabled: true, RemoteRef: repo})
assert.NilError(t, err)
defer rdr.Close()
_, err = io.Copy(io.Discard, rdr)
assert.NilError(t, err)
_, _, err = apiclient.PluginInspectWithRaw(ctx, repo)
assert.NilError(t, err)
})
// TODO: test insecure registry with https
}
func TestPluginsWithRuntimes(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
skip.If(t, testEnv.IsRootless, "Test not supported on rootless due to buggy daemon setup in rootless mode due to daemon restart")
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
ctx := testutil.StartSpan(baseContext, t)
dir, err := os.MkdirTemp("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(dir)
d := daemon.New(t)
defer d.Cleanup(t)
d.Start(t)
defer d.Stop(t)
apiclient := d.NewClientT(t)
assert.NilError(t, plugin.Create(ctx, apiclient, "test:latest"))
defer apiclient.PluginRemove(ctx, "test:latest", client.PluginRemoveOptions{Force: true})
assert.NilError(t, apiclient.PluginEnable(ctx, "test:latest", client.PluginEnableOptions{Timeout: 30}))
p := filepath.Join(dir, "myrt")
script := fmt.Sprintf(`#!/bin/sh
file="%s/success"
if [ "$1" = "someArg" ]; then
shift
file="${file}_someArg"
fi
touch $file
exec runc $@
`, dir)
assert.NilError(t, os.WriteFile(p, []byte(script), 0o777))
type config struct {
Runtimes map[string]system.Runtime `json:"runtimes"`
}
cfg, err := json.Marshal(config{
Runtimes: map[string]system.Runtime{
"myrt": {Path: p},
"myrtArgs": {Path: p, Args: []string{"someArg"}},
},
})
configPath := filepath.Join(dir, "config.json")
os.WriteFile(configPath, cfg, 0o644)
t.Run("No Args", func(t *testing.T) {
_ = testutil.StartSpan(ctx, t)
d.Restart(t, "--default-runtime=myrt", "--config-file="+configPath)
_, err = os.Stat(filepath.Join(dir, "success"))
assert.NilError(t, err)
})
t.Run("With Args", func(t *testing.T) {
_ = testutil.StartSpan(ctx, t)
d.Restart(t, "--default-runtime=myrtArgs", "--config-file="+configPath)
_, err = os.Stat(filepath.Join(dir, "success_someArg"))
assert.NilError(t, err)
})
}
func TestPluginBackCompatMediaTypes(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
skip.If(t, testEnv.IsRootless, "Rootless has a different view of localhost (needed for test registry access)")
ctx := setupTest(t)
reg := registry.NewV2(t)
defer reg.Close()
reg.WaitReady(t)
repo := path.Join(registry.DefaultURL, strings.ToLower(t.Name())+":latest")
apiclient := testEnv.APIClient()
assert.NilError(t, plugin.Create(ctx, apiclient, repo))
rdr, err := apiclient.PluginPush(ctx, repo, "")
assert.NilError(t, err)
defer rdr.Close()
buf := &strings.Builder{}
assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(rdr, buf, 0, false, nil), buf)
// Use custom header here because older versions of the registry do not
// parse the accept header correctly and does not like the accept header
// that the default resolver code uses. "Older registries" here would be
// like the one currently included in the test suite.
headers := http.Header{}
headers.Add("Accept", c8dimages.MediaTypeDockerSchema2Manifest)
resolver := docker.NewResolver(docker.ResolverOptions{
Headers: headers,
})
assert.NilError(t, err)
n, desc, err := resolver.Resolve(ctx, repo)
assert.NilError(t, err, repo)
fetcher, err := resolver.Fetcher(ctx, n)
assert.NilError(t, err)
rdr, err = fetcher.Fetch(ctx, desc)
assert.NilError(t, err)
defer rdr.Close()
var m ocispec.Manifest
assert.NilError(t, json.NewDecoder(rdr).Decode(&m))
assert.Check(t, is.Equal(m.MediaType, c8dimages.MediaTypeDockerSchema2Manifest))
assert.Check(t, is.Len(m.Layers, 1))
assert.Check(t, is.Equal(m.Layers[0].MediaType, c8dimages.MediaTypeDockerSchema2LayerGzip))
}