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