Files
moby/integration/plugin/common/plugin_test.go
2025-10-28 17:10:34 -04:00

373 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"
"github.com/moby/moby/api/types/jsonstream"
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.PluginInspect(ctx, repo, client.PluginInspectOptions{})
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)
pushResult, err := apiclient.PluginPush(ctx, repo, client.PluginPushOptions{})
assert.NilError(t, err)
defer pushResult.Close()
buf := &strings.Builder{}
assert.NilError(t, err)
var digest string
assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(pushResult, buf, 0, false, func(j jsonstream.Message) {
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)
installResult, err := apiclient.PluginInstall(ctx, repo, client.PluginInstallOptions{
Disabled: true,
RemoteRef: repo + "@" + digest,
})
assert.NilError(t, err)
defer installResult.Close()
_, err = io.Copy(io.Discard, installResult)
assert.NilError(t, err)
_, err = apiclient.PluginInspect(ctx, repo, client.PluginInspectOptions{})
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.PluginInspect(ctx, repo, client.PluginInspectOptions{})
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.PluginInspect(ctx, repo, client.PluginInspectOptions{})
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 func() {
_, _ = apiclient.PluginRemove(ctx, "test:latest", client.PluginRemoveOptions{Force: true})
}()
_, err = apiclient.PluginEnable(ctx, "test:latest", client.PluginEnableOptions{Timeout: 30})
assert.NilError(t, err)
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))
res, err := apiclient.PluginPush(ctx, repo, client.PluginPushOptions{})
assert.NilError(t, err)
defer res.Close()
buf := &strings.Builder{}
assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(res, 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))
}