github.com/rish1988/moby@v25.0.2+incompatible/integration/plugin/common/plugin_test.go (about)

     1  package common // import "github.com/docker/docker/integration/plugin/common"
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/containerd/containerd/images"
    17  	"github.com/containerd/containerd/remotes/docker"
    18  	"github.com/docker/docker/api/types"
    19  	registrytypes "github.com/docker/docker/api/types/registry"
    20  	"github.com/docker/docker/api/types/system"
    21  	"github.com/docker/docker/pkg/jsonmessage"
    22  	"github.com/docker/docker/testutil"
    23  	"github.com/docker/docker/testutil/daemon"
    24  	"github.com/docker/docker/testutil/fixtures/plugin"
    25  	"github.com/docker/docker/testutil/registry"
    26  	"github.com/docker/docker/testutil/request"
    27  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    28  	"gotest.tools/v3/assert"
    29  	is "gotest.tools/v3/assert/cmp"
    30  	"gotest.tools/v3/skip"
    31  )
    32  
    33  // TestPluginInvalidJSON tests that POST endpoints that expect a body return
    34  // the correct error when sending invalid JSON requests.
    35  func TestPluginInvalidJSON(t *testing.T) {
    36  	ctx := setupTest(t)
    37  
    38  	// POST endpoints that accept / expect a JSON body;
    39  	endpoints := []string{
    40  		"/plugins/foobar/set",
    41  		"/plugins/foobar/upgrade",
    42  		"/plugins/pull",
    43  	}
    44  
    45  	for _, ep := range endpoints {
    46  		ep := ep
    47  		t.Run(ep[1:], func(t *testing.T) {
    48  			t.Parallel()
    49  
    50  			ctx := testutil.StartSpan(ctx, t)
    51  
    52  			t.Run("invalid content type", func(t *testing.T) {
    53  				ctx := testutil.StartSpan(ctx, t)
    54  				res, body, err := request.Post(ctx, ep, request.RawString("[]"), request.ContentType("text/plain"))
    55  				assert.NilError(t, err)
    56  				assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
    57  
    58  				buf, err := request.ReadBody(body)
    59  				assert.NilError(t, err)
    60  				assert.Check(t, is.Contains(string(buf), "unsupported Content-Type header (text/plain): must be 'application/json'"))
    61  			})
    62  
    63  			t.Run("invalid JSON", func(t *testing.T) {
    64  				ctx := testutil.StartSpan(ctx, t)
    65  				res, body, err := request.Post(ctx, ep, request.RawString("{invalid json"), request.JSON)
    66  				assert.NilError(t, err)
    67  				assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
    68  
    69  				buf, err := request.ReadBody(body)
    70  				assert.NilError(t, err)
    71  				assert.Check(t, is.Contains(string(buf), "invalid JSON: invalid character 'i' looking for beginning of object key string"))
    72  			})
    73  
    74  			t.Run("extra content after JSON", func(t *testing.T) {
    75  				ctx := testutil.StartSpan(ctx, t)
    76  				res, body, err := request.Post(ctx, ep, request.RawString(`[] trailing content`), request.JSON)
    77  				assert.NilError(t, err)
    78  				assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
    79  
    80  				buf, err := request.ReadBody(body)
    81  				assert.NilError(t, err)
    82  				assert.Check(t, is.Contains(string(buf), "unexpected content after JSON"))
    83  			})
    84  
    85  			t.Run("empty body", func(t *testing.T) {
    86  				ctx := testutil.StartSpan(ctx, t)
    87  				// empty body should not produce an 500 internal server error, or
    88  				// any 5XX error (this is assuming the request does not produce
    89  				// an internal server error for another reason, but it shouldn't)
    90  				res, _, err := request.Post(ctx, ep, request.RawString(``), request.JSON)
    91  				assert.NilError(t, err)
    92  				assert.Check(t, res.StatusCode < http.StatusInternalServerError)
    93  			})
    94  		})
    95  	}
    96  }
    97  
    98  func TestPluginInstall(t *testing.T) {
    99  	skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
   100  	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
   101  	skip.If(t, testEnv.IsRootless, "rootless mode has different view of localhost")
   102  
   103  	ctx := testutil.StartSpan(baseContext, t)
   104  	client := testEnv.APIClient()
   105  
   106  	t.Run("no auth", func(t *testing.T) {
   107  		ctx := setupTest(t)
   108  
   109  		reg := registry.NewV2(t)
   110  		defer reg.Close()
   111  
   112  		name := "test-" + strings.ToLower(t.Name())
   113  		repo := path.Join(registry.DefaultURL, name+":latest")
   114  		assert.NilError(t, plugin.CreateInRegistry(ctx, repo, nil))
   115  
   116  		rdr, err := client.PluginInstall(ctx, repo, types.PluginInstallOptions{Disabled: true, RemoteRef: repo})
   117  		assert.NilError(t, err)
   118  		defer rdr.Close()
   119  
   120  		_, err = io.Copy(io.Discard, rdr)
   121  		assert.NilError(t, err)
   122  
   123  		_, _, err = client.PluginInspectWithRaw(ctx, repo)
   124  		assert.NilError(t, err)
   125  	})
   126  
   127  	t.Run("with htpasswd", func(t *testing.T) {
   128  		ctx := setupTest(t)
   129  
   130  		reg := registry.NewV2(t, registry.Htpasswd)
   131  		defer reg.Close()
   132  
   133  		name := "test-" + strings.ToLower(t.Name())
   134  		repo := path.Join(registry.DefaultURL, name+":latest")
   135  		auth := &registrytypes.AuthConfig{ServerAddress: registry.DefaultURL, Username: "testuser", Password: "testpassword"}
   136  		assert.NilError(t, plugin.CreateInRegistry(ctx, repo, auth))
   137  
   138  		authEncoded, err := json.Marshal(auth)
   139  		assert.NilError(t, err)
   140  
   141  		rdr, err := client.PluginInstall(ctx, repo, types.PluginInstallOptions{
   142  			RegistryAuth: base64.URLEncoding.EncodeToString(authEncoded),
   143  			Disabled:     true,
   144  			RemoteRef:    repo,
   145  		})
   146  		assert.NilError(t, err)
   147  		defer rdr.Close()
   148  
   149  		_, err = io.Copy(io.Discard, rdr)
   150  		assert.NilError(t, err)
   151  
   152  		_, _, err = client.PluginInspectWithRaw(ctx, repo)
   153  		assert.NilError(t, err)
   154  	})
   155  	t.Run("with insecure", func(t *testing.T) {
   156  		skip.If(t, !testEnv.IsLocalDaemon())
   157  
   158  		ctx := testutil.StartSpan(ctx, t)
   159  
   160  		addrs, err := net.InterfaceAddrs()
   161  		assert.NilError(t, err)
   162  
   163  		var bindTo string
   164  		for _, addr := range addrs {
   165  			ip, ok := addr.(*net.IPNet)
   166  			if !ok {
   167  				continue
   168  			}
   169  			if ip.IP.IsLoopback() || ip.IP.To4() == nil {
   170  				continue
   171  			}
   172  			bindTo = ip.IP.String()
   173  		}
   174  
   175  		if bindTo == "" {
   176  			t.Skip("No suitable interface to bind registry to")
   177  		}
   178  
   179  		regURL := bindTo + ":5000"
   180  
   181  		d := daemon.New(t)
   182  		defer d.Stop(t)
   183  
   184  		d.Start(t, "--insecure-registry="+regURL)
   185  		defer d.Stop(t)
   186  
   187  		reg := registry.NewV2(t, registry.URL(regURL))
   188  		defer reg.Close()
   189  
   190  		name := "test-" + strings.ToLower(t.Name())
   191  		repo := path.Join(regURL, name+":latest")
   192  		assert.NilError(t, plugin.CreateInRegistry(ctx, repo, nil, plugin.WithInsecureRegistry(regURL)))
   193  
   194  		client := d.NewClientT(t)
   195  		rdr, err := client.PluginInstall(ctx, repo, types.PluginInstallOptions{Disabled: true, RemoteRef: repo})
   196  		assert.NilError(t, err)
   197  		defer rdr.Close()
   198  
   199  		_, err = io.Copy(io.Discard, rdr)
   200  		assert.NilError(t, err)
   201  
   202  		_, _, err = client.PluginInspectWithRaw(ctx, repo)
   203  		assert.NilError(t, err)
   204  	})
   205  	// TODO: test insecure registry with https
   206  }
   207  
   208  func TestPluginsWithRuntimes(t *testing.T) {
   209  	skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
   210  	skip.If(t, testEnv.IsRootless, "Test not supported on rootless due to buggy daemon setup in rootless mode due to daemon restart")
   211  	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
   212  
   213  	ctx := testutil.StartSpan(baseContext, t)
   214  
   215  	dir, err := os.MkdirTemp("", t.Name())
   216  	assert.NilError(t, err)
   217  	defer os.RemoveAll(dir)
   218  
   219  	d := daemon.New(t)
   220  	defer d.Cleanup(t)
   221  
   222  	d.Start(t)
   223  	defer d.Stop(t)
   224  
   225  	client := d.NewClientT(t)
   226  
   227  	assert.NilError(t, plugin.Create(ctx, client, "test:latest"))
   228  	defer client.PluginRemove(ctx, "test:latest", types.PluginRemoveOptions{Force: true})
   229  
   230  	assert.NilError(t, client.PluginEnable(ctx, "test:latest", types.PluginEnableOptions{Timeout: 30}))
   231  
   232  	p := filepath.Join(dir, "myrt")
   233  	script := fmt.Sprintf(`#!/bin/sh
   234  	file="%s/success"
   235  	if [ "$1" = "someArg" ]; then
   236  		shift
   237  		file="${file}_someArg"
   238  	fi
   239  
   240  	touch $file
   241  	exec runc $@
   242  	`, dir)
   243  
   244  	assert.NilError(t, os.WriteFile(p, []byte(script), 0o777))
   245  
   246  	type config struct {
   247  		Runtimes map[string]system.Runtime `json:"runtimes"`
   248  	}
   249  
   250  	cfg, err := json.Marshal(config{
   251  		Runtimes: map[string]system.Runtime{
   252  			"myrt":     {Path: p},
   253  			"myrtArgs": {Path: p, Args: []string{"someArg"}},
   254  		},
   255  	})
   256  	configPath := filepath.Join(dir, "config.json")
   257  	os.WriteFile(configPath, cfg, 0o644)
   258  
   259  	t.Run("No Args", func(t *testing.T) {
   260  		_ = testutil.StartSpan(ctx, t)
   261  		d.Restart(t, "--default-runtime=myrt", "--config-file="+configPath)
   262  		_, err = os.Stat(filepath.Join(dir, "success"))
   263  		assert.NilError(t, err)
   264  	})
   265  
   266  	t.Run("With Args", func(t *testing.T) {
   267  		_ = testutil.StartSpan(ctx, t)
   268  		d.Restart(t, "--default-runtime=myrtArgs", "--config-file="+configPath)
   269  		_, err = os.Stat(filepath.Join(dir, "success_someArg"))
   270  		assert.NilError(t, err)
   271  	})
   272  }
   273  
   274  func TestPluginBackCompatMediaTypes(t *testing.T) {
   275  	skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
   276  	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
   277  	skip.If(t, testEnv.IsRootless, "Rootless has a different view of localhost (needed for test registry access)")
   278  
   279  	ctx := setupTest(t)
   280  
   281  	reg := registry.NewV2(t)
   282  	defer reg.Close()
   283  	reg.WaitReady(t)
   284  
   285  	repo := path.Join(registry.DefaultURL, strings.ToLower(t.Name())+":latest")
   286  
   287  	client := testEnv.APIClient()
   288  
   289  	assert.NilError(t, plugin.Create(ctx, client, repo))
   290  
   291  	rdr, err := client.PluginPush(ctx, repo, "")
   292  	assert.NilError(t, err)
   293  	defer rdr.Close()
   294  
   295  	buf := &strings.Builder{}
   296  	assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(rdr, buf, 0, false, nil), buf)
   297  
   298  	// Use custom header here because older versions of the registry do not
   299  	// parse the accept header correctly and does not like the accept header
   300  	// that the default resolver code uses. "Older registries" here would be
   301  	// like the one currently included in the test suite.
   302  	headers := http.Header{}
   303  	headers.Add("Accept", images.MediaTypeDockerSchema2Manifest)
   304  
   305  	resolver := docker.NewResolver(docker.ResolverOptions{
   306  		Headers: headers,
   307  	})
   308  	assert.NilError(t, err)
   309  
   310  	n, desc, err := resolver.Resolve(ctx, repo)
   311  	assert.NilError(t, err, repo)
   312  
   313  	fetcher, err := resolver.Fetcher(ctx, n)
   314  	assert.NilError(t, err)
   315  
   316  	rdr, err = fetcher.Fetch(ctx, desc)
   317  	assert.NilError(t, err)
   318  	defer rdr.Close()
   319  
   320  	var m ocispec.Manifest
   321  	assert.NilError(t, json.NewDecoder(rdr).Decode(&m))
   322  	assert.Check(t, is.Equal(m.MediaType, images.MediaTypeDockerSchema2Manifest))
   323  	assert.Check(t, is.Len(m.Layers, 1))
   324  	assert.Check(t, is.Equal(m.Layers[0].MediaType, images.MediaTypeDockerSchema2LayerGzip))
   325  }