github.com/rawahars/moby@v24.0.4+incompatible/integration/plugin/common/plugin_test.go (about)

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