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 := ®istrytypes.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 }