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