github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/cmd/snap/cmd_quota_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package main_test 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "io/ioutil" 27 "net/http" 28 29 "gopkg.in/check.v1" 30 31 main "github.com/snapcore/snapd/cmd/snap" 32 "github.com/snapcore/snapd/jsonutil" 33 ) 34 35 type quotaSuite struct { 36 BaseSnapSuite 37 } 38 39 var _ = check.Suite("aSuite{}) 40 41 func makeFakeGetQuotaGroupNotFoundHandler(c *check.C, group string) func(w http.ResponseWriter, r *http.Request) { 42 return func(w http.ResponseWriter, r *http.Request) { 43 c.Check(r.URL.Path, check.Equals, "/v2/quotas/"+group) 44 c.Check(r.Method, check.Equals, "GET") 45 w.WriteHeader(404) 46 fmt.Fprintln(w, `{ 47 "result": { 48 "message": "not found" 49 }, 50 "status": "Not Found", 51 "status-code": 404, 52 "type": "error" 53 }`) 54 } 55 56 } 57 58 func makeFakeGetQuotaGroupHandler(c *check.C, body string) func(w http.ResponseWriter, r *http.Request) { 59 var called bool 60 return func(w http.ResponseWriter, r *http.Request) { 61 if called { 62 c.Fatalf("expected a single request") 63 } 64 called = true 65 c.Check(r.URL.Path, check.Equals, "/v2/quotas/foo") 66 c.Check(r.Method, check.Equals, "GET") 67 w.WriteHeader(200) 68 fmt.Fprintln(w, body) 69 } 70 } 71 72 func makeFakeGetQuotaGroupsHandler(c *check.C, body string) func(w http.ResponseWriter, r *http.Request) { 73 var called bool 74 return func(w http.ResponseWriter, r *http.Request) { 75 if called { 76 c.Fatalf("expected a single request") 77 } 78 called = true 79 c.Check(r.URL.Path, check.Equals, "/v2/quotas") 80 c.Check(r.Method, check.Equals, "GET") 81 w.WriteHeader(200) 82 fmt.Fprintln(w, body) 83 } 84 } 85 86 func dispatchFakeHandlers(c *check.C, routes map[string]http.HandlerFunc) func(w http.ResponseWriter, r *http.Request) { 87 return func(w http.ResponseWriter, r *http.Request) { 88 if router, ok := routes[r.URL.Path]; ok { 89 router(w, r) 90 return 91 } 92 c.Errorf("unexpected call to %s", r.URL.Path) 93 } 94 } 95 96 type fakeQuotaGroupPostHandlerOpts struct { 97 action string 98 body string 99 groupName string 100 parentName string 101 snaps []string 102 maxMemory int64 103 } 104 105 type quotasEnsureBody struct { 106 Action string `json:"action"` 107 GroupName string `json:"group-name,omitempty"` 108 ParentName string `json:"parent,omitempty"` 109 Snaps []string `json:"snaps,omitempty"` 110 Constraints map[string]interface{} `json:"constraints,omitempty"` 111 } 112 113 func makeFakeQuotaPostHandler(c *check.C, opts fakeQuotaGroupPostHandlerOpts) func(w http.ResponseWriter, r *http.Request) { 114 var called bool 115 return func(w http.ResponseWriter, r *http.Request) { 116 if called { 117 c.Fatalf("expected a single request") 118 } 119 called = true 120 c.Check(r.URL.Path, check.Equals, "/v2/quotas") 121 c.Check(r.Method, check.Equals, "POST") 122 123 buf, err := ioutil.ReadAll(r.Body) 124 c.Assert(err, check.IsNil) 125 126 switch opts.action { 127 case "remove": 128 c.Check(string(buf), check.Equals, fmt.Sprintf(`{"action":"remove","group-name":%q}`+"\n", opts.groupName)) 129 case "ensure": 130 exp := quotasEnsureBody{ 131 Action: "ensure", 132 GroupName: opts.groupName, 133 ParentName: opts.parentName, 134 Snaps: opts.snaps, 135 Constraints: map[string]interface{}{}, 136 } 137 if opts.maxMemory != 0 { 138 exp.Constraints["memory"] = json.Number(fmt.Sprintf("%d", opts.maxMemory)) 139 } 140 141 postJSON := quotasEnsureBody{} 142 err := jsonutil.DecodeWithNumber(bytes.NewReader(buf), &postJSON) 143 c.Assert(err, check.IsNil) 144 c.Assert(postJSON, check.DeepEquals, exp) 145 default: 146 c.Fatalf("unexpected action %q", opts.action) 147 } 148 w.WriteHeader(202) 149 fmt.Fprintln(w, opts.body) 150 } 151 } 152 153 func makeChangesHandler(c *check.C) func(w http.ResponseWriter, r *http.Request) { 154 n := 0 155 return func(w http.ResponseWriter, r *http.Request) { 156 n++ 157 switch n { 158 case 1: 159 c.Check(r.Method, check.Equals, "GET") 160 c.Check(r.URL.Path, check.Equals, "/v2/changes/42") 161 fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) 162 case 2: 163 c.Check(r.Method, check.Equals, "GET") 164 c.Check(r.URL.Path, check.Equals, "/v2/changes/42") 165 fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) 166 default: 167 c.Fatalf("expected to get 2 requests, now on %d", n+1) 168 } 169 } 170 } 171 172 func (s *quotaSuite) TestSetQuotaInvalidArgs(c *check.C) { 173 for _, args := range []struct { 174 args []string 175 err string 176 }{ 177 {[]string{"set-quota"}, "the required argument `<group-name>` was not provided"}, 178 {[]string{"set-quota", "--memory=99B"}, "the required argument `<group-name>` was not provided"}, 179 {[]string{"set-quota", "--memory=99", "foo"}, `cannot parse "99": need a number with a unit as input`}, 180 {[]string{"set-quota", "--memory=888X", "foo"}, `cannot parse "888X\": try 'kB' or 'MB'`}, 181 // remove-quota command 182 {[]string{"remove-quota"}, "the required argument `<group-name>` was not provided"}, 183 } { 184 s.stdout.Reset() 185 s.stderr.Reset() 186 187 _, err := main.Parser(main.Client()).ParseArgs(args.args) 188 c.Assert(err, check.ErrorMatches, args.err) 189 } 190 } 191 192 func (s *quotaSuite) TestGetQuotaGroup(c *check.C) { 193 restore := main.MockIsStdinTTY(true) 194 defer restore() 195 196 const json = `{ 197 "type": "sync", 198 "status-code": 200, 199 "result": { 200 "group-name":"foo", 201 "parent":"bar", 202 "subgroups":["subgrp1"], 203 "snaps":["snap-a","snap-b"], 204 "constraints": { "memory": 1000 }, 205 "current": { "memory": 900 } 206 } 207 }` 208 209 s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, json)) 210 211 rest, err := main.Parser(main.Client()).ParseArgs([]string{"quota", "foo"}) 212 c.Assert(err, check.IsNil) 213 c.Check(rest, check.HasLen, 0) 214 c.Check(s.Stderr(), check.Equals, "") 215 c.Check(s.Stdout(), check.Equals, ` 216 name: foo 217 parent: bar 218 constraints: 219 memory: 1000B 220 current: 221 memory: 900B 222 subgroups: 223 - subgrp1 224 snaps: 225 - snap-a 226 - snap-b 227 `[1:]) 228 } 229 230 func (s *quotaSuite) TestGetQuotaGroupSimple(c *check.C) { 231 restore := main.MockIsStdinTTY(true) 232 defer restore() 233 234 const jsonTemplate = `{ 235 "type": "sync", 236 "status-code": 200, 237 "result": { 238 "group-name": "foo", 239 "constraints": {"memory": 1000}, 240 "current": {"memory": %d} 241 } 242 }` 243 244 s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 0))) 245 246 outputTemplate := ` 247 name: foo 248 constraints: 249 memory: 1000B 250 current: 251 memory: %dB 252 `[1:] 253 254 rest, err := main.Parser(main.Client()).ParseArgs([]string{"quota", "foo"}) 255 c.Assert(err, check.IsNil) 256 c.Check(rest, check.HasLen, 0) 257 c.Check(s.Stderr(), check.Equals, "") 258 c.Check(s.Stdout(), check.Equals, fmt.Sprintf(outputTemplate, 0)) 259 260 s.stdout.Reset() 261 s.stderr.Reset() 262 263 s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 500))) 264 265 rest, err = main.Parser(main.Client()).ParseArgs([]string{"quota", "foo"}) 266 c.Assert(err, check.IsNil) 267 c.Check(rest, check.HasLen, 0) 268 c.Check(s.Stderr(), check.Equals, "") 269 c.Check(s.Stdout(), check.Equals, fmt.Sprintf(outputTemplate, 500)) 270 } 271 272 func (s *quotaSuite) TestSetQuotaGroupCreateNew(c *check.C) { 273 const postJSON = `{"type": "async", "status-code": 202,"change":"42", "result": []}` 274 fakeHandlerOpts := fakeQuotaGroupPostHandlerOpts{ 275 action: "ensure", 276 body: postJSON, 277 groupName: "foo", 278 parentName: "bar", 279 snaps: []string{"snap-a"}, 280 maxMemory: 999, 281 } 282 283 routes := map[string]http.HandlerFunc{ 284 "/v2/quotas": makeFakeQuotaPostHandler( 285 c, 286 fakeHandlerOpts, 287 ), 288 // the foo quota group is not found since it doesn't exist yet 289 "/v2/quotas/foo": makeFakeGetQuotaGroupNotFoundHandler(c, "foo"), 290 291 "/v2/changes/42": makeChangesHandler(c), 292 } 293 294 s.RedirectClientToTestServer(dispatchFakeHandlers(c, routes)) 295 296 rest, err := main.Parser(main.Client()).ParseArgs([]string{"set-quota", "foo", "--memory=999B", "--parent=bar", "snap-a"}) 297 c.Assert(err, check.IsNil) 298 c.Check(rest, check.HasLen, 0) 299 c.Check(s.Stderr(), check.Equals, "") 300 c.Check(s.Stdout(), check.Equals, "") 301 } 302 303 func (s *quotaSuite) TestSetQuotaGroupUpdateExistingUnhappy(c *check.C) { 304 const exists = true 305 s.testSetQuotaGroupUpdateExistingUnhappy(c, "no options set to change quota group", exists) 306 } 307 308 func (s *quotaSuite) TestSetQuotaGroupCreateNewUnhappy(c *check.C) { 309 const exists = false 310 s.testSetQuotaGroupUpdateExistingUnhappy(c, "cannot create quota group without memory limit", exists) 311 } 312 313 func (s *quotaSuite) TestSetQuotaGroupCreateNewUnhappyWithParent(c *check.C) { 314 const exists = false 315 s.testSetQuotaGroupUpdateExistingUnhappy(c, "cannot create quota group without memory limit", exists, "--parent=bar") 316 } 317 318 func (s *quotaSuite) TestSetQuotaGroupUpdateExistingUnhappyWithParent(c *check.C) { 319 const exists = true 320 s.testSetQuotaGroupUpdateExistingUnhappy(c, "cannot move a quota group to a new parent", exists, "--parent=bar") 321 } 322 323 func (s *quotaSuite) testSetQuotaGroupUpdateExistingUnhappy(c *check.C, errPattern string, exists bool, args ...string) { 324 if exists { 325 // existing group has 1000 memory limit 326 const getJson = `{ 327 "type": "sync", 328 "status-code": 200, 329 "result": { 330 "group-name":"foo", 331 "current": { 332 "memory": 500 333 }, 334 "constraints": { 335 "memory": 1000 336 } 337 } 338 }` 339 340 s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, getJson)) 341 } else { 342 s.RedirectClientToTestServer(makeFakeGetQuotaGroupNotFoundHandler(c, "foo")) 343 } 344 345 cmdArgs := append([]string{"set-quota", "foo"}, args...) 346 _, err := main.Parser(main.Client()).ParseArgs(cmdArgs) 347 c.Assert(err, check.ErrorMatches, errPattern) 348 c.Check(s.Stdout(), check.Equals, "") 349 } 350 351 func (s *quotaSuite) TestSetQuotaGroupUpdateExisting(c *check.C) { 352 const postJSON = `{"type": "async", "status-code": 202,"change":"42", "result": []}` 353 fakeHandlerOpts := fakeQuotaGroupPostHandlerOpts{ 354 action: "ensure", 355 body: postJSON, 356 groupName: "foo", 357 maxMemory: 2000, 358 } 359 360 const getJsonTemplate = `{ 361 "type": "sync", 362 "status-code": 200, 363 "result": { 364 "group-name":"foo", 365 "constraints": { "memory": %d }, 366 "current": { "memory": 500 } 367 } 368 }` 369 370 routes := map[string]http.HandlerFunc{ 371 "/v2/quotas": makeFakeQuotaPostHandler( 372 c, 373 fakeHandlerOpts, 374 ), 375 "/v2/quotas/foo": makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(getJsonTemplate, 1000)), 376 "/v2/changes/42": makeChangesHandler(c), 377 } 378 379 s.RedirectClientToTestServer(dispatchFakeHandlers(c, routes)) 380 381 // increase the memory limit to 2000 382 rest, err := main.Parser(main.Client()).ParseArgs([]string{"set-quota", "foo", "--memory=2000B"}) 383 c.Assert(err, check.IsNil) 384 c.Check(rest, check.HasLen, 0) 385 c.Check(s.Stderr(), check.Equals, "") 386 c.Check(s.Stdout(), check.Equals, "") 387 388 s.stdout.Reset() 389 s.stderr.Reset() 390 391 fakeHandlerOpts2 := fakeQuotaGroupPostHandlerOpts{ 392 action: "ensure", 393 body: postJSON, 394 groupName: "foo", 395 snaps: []string{"some-snap"}, 396 } 397 398 routes = map[string]http.HandlerFunc{ 399 "/v2/quotas": makeFakeQuotaPostHandler( 400 c, 401 fakeHandlerOpts2, 402 ), 403 // the group was updated to have a 2000 memory limit now 404 "/v2/quotas/foo": makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(getJsonTemplate, 2000)), 405 406 "/v2/changes/42": makeChangesHandler(c), 407 } 408 409 s.RedirectClientToTestServer(dispatchFakeHandlers(c, routes)) 410 411 // add a snap to the group 412 rest, err = main.Parser(main.Client()).ParseArgs([]string{"set-quota", "foo", "some-snap"}) 413 c.Assert(err, check.IsNil) 414 c.Check(rest, check.HasLen, 0) 415 c.Check(s.Stderr(), check.Equals, "") 416 c.Check(s.Stdout(), check.Equals, "") 417 } 418 419 func (s *quotaSuite) TestRemoveQuotaGroup(c *check.C) { 420 const json = `{"type": "async", "status-code": 202,"change": "42"}` 421 fakeHandlerOpts := fakeQuotaGroupPostHandlerOpts{ 422 action: "remove", 423 body: json, 424 groupName: "foo", 425 } 426 427 routes := map[string]http.HandlerFunc{ 428 "/v2/quotas": makeFakeQuotaPostHandler(c, fakeHandlerOpts), 429 430 "/v2/changes/42": makeChangesHandler(c), 431 } 432 433 s.RedirectClientToTestServer(dispatchFakeHandlers(c, routes)) 434 435 rest, err := main.Parser(main.Client()).ParseArgs([]string{"remove-quota", "foo"}) 436 c.Assert(err, check.IsNil) 437 c.Check(rest, check.HasLen, 0) 438 c.Check(s.Stderr(), check.Equals, "") 439 c.Check(s.Stdout(), check.Equals, "") 440 } 441 442 func (s *quotaSuite) TestGetAllQuotaGroups(c *check.C) { 443 restore := main.MockIsStdinTTY(true) 444 defer restore() 445 446 s.RedirectClientToTestServer(makeFakeGetQuotaGroupsHandler(c, 447 `{"type": "sync", "status-code": 200, "result": [ 448 {"group-name":"aaa","subgroups":["ccc","ddd","fff"],"parent":"zzz","constraints":{"memory":1000}}, 449 {"group-name":"ddd","parent":"aaa","constraints":{"memory":400}}, 450 {"group-name":"bbb","parent":"zzz","constraints":{"memory":1000},"current":{"memory":400}}, 451 {"group-name":"yyyyyyy","constraints":{"memory":1000}}, 452 {"group-name":"zzz","subgroups":["bbb","aaa"],"constraints":{"memory":5000}}, 453 {"group-name":"ccc","parent":"aaa","constraints":{"memory":400}}, 454 {"group-name":"fff","parent":"aaa","constraints":{"memory":1000},"current":{"memory":0}}, 455 {"group-name":"xxx","constraints":{"memory":9900},"current":{"memory":10000}} 456 ]}`)) 457 458 rest, err := main.Parser(main.Client()).ParseArgs([]string{"quotas"}) 459 c.Assert(err, check.IsNil) 460 c.Check(rest, check.HasLen, 0) 461 c.Check(s.Stderr(), check.Equals, "") 462 c.Check(s.Stdout(), check.Equals, ` 463 Quota Parent Constraints Current 464 xxx memory=9.9kB memory=10.0kB 465 yyyyyyy memory=1000B 466 zzz memory=5000B 467 aaa zzz memory=1000B 468 ccc aaa memory=400B 469 ddd aaa memory=400B 470 fff aaa memory=1000B 471 bbb zzz memory=1000B memory=400B 472 `[1:]) 473 } 474 475 func (s *quotaSuite) TestGetAllQuotaGroupsInconsistencyError(c *check.C) { 476 restore := main.MockIsStdinTTY(true) 477 defer restore() 478 479 s.RedirectClientToTestServer(makeFakeGetQuotaGroupsHandler(c, 480 `{"type": "sync", "status-code": 200, "result": [ 481 {"group-name":"aaa","subgroups":["ccc"],"max-memory":1000}]}`)) 482 483 _, err := main.Parser(main.Client()).ParseArgs([]string{"quotas"}) 484 c.Assert(err, check.ErrorMatches, `internal error: inconsistent groups received, unknown subgroup "ccc"`) 485 } 486 487 func (s *quotaSuite) TestNoQuotaGroups(c *check.C) { 488 restore := main.MockIsStdinTTY(true) 489 defer restore() 490 491 s.RedirectClientToTestServer(makeFakeGetQuotaGroupsHandler(c, 492 `{"type": "sync", "status-code": 200, "result": []}`)) 493 494 rest, err := main.Parser(main.Client()).ParseArgs([]string{"quotas"}) 495 c.Assert(err, check.IsNil) 496 c.Check(rest, check.HasLen, 0) 497 c.Check(s.Stderr(), check.Equals, "") 498 c.Check(s.Stdout(), check.Equals, "No quota groups defined.\n") 499 }