github.com/khulnasoft-lab/khulnasoft@v26.0.1-0.20240328202558-330a6f959fe0+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 digest", func(t *testing.T) {
   128  		ctx := setupTest(t)
   129  
   130  		reg := registry.NewV2(t)
   131  		defer reg.Close()
   132  
   133  		name := "test-" + strings.ToLower(t.Name())
   134  		repo := path.Join(registry.DefaultURL, name+":latest")
   135  		err := plugin.Create(ctx, client, repo)
   136  		assert.NilError(t, err)
   137  
   138  		rdr, err := client.PluginPush(ctx, repo, "")
   139  		assert.NilError(t, err)
   140  		defer rdr.Close()
   141  
   142  		buf := &strings.Builder{}
   143  		assert.NilError(t, err)
   144  		var digest string
   145  		assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(rdr, buf, 0, false, func(j jsonmessage.JSONMessage) {
   146  			if j.Aux != nil {
   147  				var r types.PushResult
   148  				assert.NilError(t, json.Unmarshal(*j.Aux, &r))
   149  				digest = r.Digest
   150  			}
   151  		}), buf)
   152  
   153  		err = client.PluginRemove(ctx, repo, types.PluginRemoveOptions{Force: true})
   154  		assert.NilError(t, err)
   155  
   156  		rdr, err = client.PluginInstall(ctx, repo, types.PluginInstallOptions{
   157  			Disabled:  true,
   158  			RemoteRef: repo + "@" + digest,
   159  		})
   160  		assert.NilError(t, err)
   161  		defer rdr.Close()
   162  
   163  		_, err = io.Copy(io.Discard, rdr)
   164  		assert.NilError(t, err)
   165  
   166  		_, _, err = client.PluginInspectWithRaw(ctx, repo)
   167  		assert.NilError(t, err)
   168  	})
   169  
   170  	t.Run("with htpasswd", func(t *testing.T) {
   171  		ctx := setupTest(t)
   172  
   173  		reg := registry.NewV2(t, registry.Htpasswd)
   174  		defer reg.Close()
   175  
   176  		name := "test-" + strings.ToLower(t.Name())
   177  		repo := path.Join(registry.DefaultURL, name+":latest")
   178  		auth := &registrytypes.AuthConfig{ServerAddress: registry.DefaultURL, Username: "testuser", Password: "testpassword"}
   179  		assert.NilError(t, plugin.CreateInRegistry(ctx, repo, auth))
   180  
   181  		authEncoded, err := json.Marshal(auth)
   182  		assert.NilError(t, err)
   183  
   184  		rdr, err := client.PluginInstall(ctx, repo, types.PluginInstallOptions{
   185  			RegistryAuth: base64.URLEncoding.EncodeToString(authEncoded),
   186  			Disabled:     true,
   187  			RemoteRef:    repo,
   188  		})
   189  		assert.NilError(t, err)
   190  		defer rdr.Close()
   191  
   192  		_, err = io.Copy(io.Discard, rdr)
   193  		assert.NilError(t, err)
   194  
   195  		_, _, err = client.PluginInspectWithRaw(ctx, repo)
   196  		assert.NilError(t, err)
   197  	})
   198  	t.Run("with insecure", func(t *testing.T) {
   199  		skip.If(t, !testEnv.IsLocalDaemon())
   200  
   201  		ctx := testutil.StartSpan(ctx, t)
   202  
   203  		addrs, err := net.InterfaceAddrs()
   204  		assert.NilError(t, err)
   205  
   206  		var bindTo string
   207  		for _, addr := range addrs {
   208  			ip, ok := addr.(*net.IPNet)
   209  			if !ok {
   210  				continue
   211  			}
   212  			if ip.IP.IsLoopback() || ip.IP.To4() == nil {
   213  				continue
   214  			}
   215  			bindTo = ip.IP.String()
   216  		}
   217  
   218  		if bindTo == "" {
   219  			t.Skip("No suitable interface to bind registry to")
   220  		}
   221  
   222  		regURL := bindTo + ":5000"
   223  
   224  		d := daemon.New(t)
   225  		defer d.Stop(t)
   226  
   227  		d.Start(t, "--insecure-registry="+regURL)
   228  		defer d.Stop(t)
   229  
   230  		reg := registry.NewV2(t, registry.URL(regURL))
   231  		defer reg.Close()
   232  
   233  		name := "test-" + strings.ToLower(t.Name())
   234  		repo := path.Join(regURL, name+":latest")
   235  		assert.NilError(t, plugin.CreateInRegistry(ctx, repo, nil, plugin.WithInsecureRegistry(regURL)))
   236  
   237  		client := d.NewClientT(t)
   238  		rdr, err := client.PluginInstall(ctx, repo, types.PluginInstallOptions{Disabled: true, RemoteRef: repo})
   239  		assert.NilError(t, err)
   240  		defer rdr.Close()
   241  
   242  		_, err = io.Copy(io.Discard, rdr)
   243  		assert.NilError(t, err)
   244  
   245  		_, _, err = client.PluginInspectWithRaw(ctx, repo)
   246  		assert.NilError(t, err)
   247  	})
   248  	// TODO: test insecure registry with https
   249  }
   250  
   251  func TestPluginsWithRuntimes(t *testing.T) {
   252  	skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
   253  	skip.If(t, testEnv.IsRootless, "Test not supported on rootless due to buggy daemon setup in rootless mode due to daemon restart")
   254  	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
   255  
   256  	ctx := testutil.StartSpan(baseContext, t)
   257  
   258  	dir, err := os.MkdirTemp("", t.Name())
   259  	assert.NilError(t, err)
   260  	defer os.RemoveAll(dir)
   261  
   262  	d := daemon.New(t)
   263  	defer d.Cleanup(t)
   264  
   265  	d.Start(t)
   266  	defer d.Stop(t)
   267  
   268  	client := d.NewClientT(t)
   269  
   270  	assert.NilError(t, plugin.Create(ctx, client, "test:latest"))
   271  	defer client.PluginRemove(ctx, "test:latest", types.PluginRemoveOptions{Force: true})
   272  
   273  	assert.NilError(t, client.PluginEnable(ctx, "test:latest", types.PluginEnableOptions{Timeout: 30}))
   274  
   275  	p := filepath.Join(dir, "myrt")
   276  	script := fmt.Sprintf(`#!/bin/sh
   277  	file="%s/success"
   278  	if [ "$1" = "someArg" ]; then
   279  		shift
   280  		file="${file}_someArg"
   281  	fi
   282  
   283  	touch $file
   284  	exec runc $@
   285  	`, dir)
   286  
   287  	assert.NilError(t, os.WriteFile(p, []byte(script), 0o777))
   288  
   289  	type config struct {
   290  		Runtimes map[string]system.Runtime `json:"runtimes"`
   291  	}
   292  
   293  	cfg, err := json.Marshal(config{
   294  		Runtimes: map[string]system.Runtime{
   295  			"myrt":     {Path: p},
   296  			"myrtArgs": {Path: p, Args: []string{"someArg"}},
   297  		},
   298  	})
   299  	configPath := filepath.Join(dir, "config.json")
   300  	os.WriteFile(configPath, cfg, 0o644)
   301  
   302  	t.Run("No Args", func(t *testing.T) {
   303  		_ = testutil.StartSpan(ctx, t)
   304  		d.Restart(t, "--default-runtime=myrt", "--config-file="+configPath)
   305  		_, err = os.Stat(filepath.Join(dir, "success"))
   306  		assert.NilError(t, err)
   307  	})
   308  
   309  	t.Run("With Args", func(t *testing.T) {
   310  		_ = testutil.StartSpan(ctx, t)
   311  		d.Restart(t, "--default-runtime=myrtArgs", "--config-file="+configPath)
   312  		_, err = os.Stat(filepath.Join(dir, "success_someArg"))
   313  		assert.NilError(t, err)
   314  	})
   315  }
   316  
   317  func TestPluginBackCompatMediaTypes(t *testing.T) {
   318  	skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
   319  	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
   320  	skip.If(t, testEnv.IsRootless, "Rootless has a different view of localhost (needed for test registry access)")
   321  
   322  	ctx := setupTest(t)
   323  
   324  	reg := registry.NewV2(t)
   325  	defer reg.Close()
   326  	reg.WaitReady(t)
   327  
   328  	repo := path.Join(registry.DefaultURL, strings.ToLower(t.Name())+":latest")
   329  
   330  	client := testEnv.APIClient()
   331  
   332  	assert.NilError(t, plugin.Create(ctx, client, repo))
   333  
   334  	rdr, err := client.PluginPush(ctx, repo, "")
   335  	assert.NilError(t, err)
   336  	defer rdr.Close()
   337  
   338  	buf := &strings.Builder{}
   339  	assert.NilError(t, jsonmessage.DisplayJSONMessagesStream(rdr, buf, 0, false, nil), buf)
   340  
   341  	// Use custom header here because older versions of the registry do not
   342  	// parse the accept header correctly and does not like the accept header
   343  	// that the default resolver code uses. "Older registries" here would be
   344  	// like the one currently included in the test suite.
   345  	headers := http.Header{}
   346  	headers.Add("Accept", images.MediaTypeDockerSchema2Manifest)
   347  
   348  	resolver := docker.NewResolver(docker.ResolverOptions{
   349  		Headers: headers,
   350  	})
   351  	assert.NilError(t, err)
   352  
   353  	n, desc, err := resolver.Resolve(ctx, repo)
   354  	assert.NilError(t, err, repo)
   355  
   356  	fetcher, err := resolver.Fetcher(ctx, n)
   357  	assert.NilError(t, err)
   358  
   359  	rdr, err = fetcher.Fetch(ctx, desc)
   360  	assert.NilError(t, err)
   361  	defer rdr.Close()
   362  
   363  	var m ocispec.Manifest
   364  	assert.NilError(t, json.NewDecoder(rdr).Decode(&m))
   365  	assert.Check(t, is.Equal(m.MediaType, images.MediaTypeDockerSchema2Manifest))
   366  	assert.Check(t, is.Len(m.Layers, 1))
   367  	assert.Check(t, is.Equal(m.Layers[0].MediaType, images.MediaTypeDockerSchema2LayerGzip))
   368  }