github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/daemon/api_quotas_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 daemon_test 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "net/http" 27 "net/http/httptest" 28 29 "gopkg.in/check.v1" 30 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/daemon" 33 "github.com/snapcore/snapd/gadget/quantity" 34 "github.com/snapcore/snapd/overlord/configstate/config" 35 "github.com/snapcore/snapd/overlord/servicestate" 36 "github.com/snapcore/snapd/overlord/state" 37 "github.com/snapcore/snapd/snap/quota" 38 ) 39 40 var _ = check.Suite(&apiQuotaSuite{}) 41 42 type apiQuotaSuite struct { 43 apiBaseSuite 44 } 45 46 func (s *apiQuotaSuite) SetUpTest(c *check.C) { 47 s.apiBaseSuite.SetUpTest(c) 48 s.daemon(c) 49 50 st := s.d.Overlord().State() 51 st.Lock() 52 defer st.Unlock() 53 tr := config.NewTransaction(st) 54 tr.Set("core", "experimental.quota-groups", true) 55 tr.Commit() 56 57 r := servicestate.MockSystemdVersion(248) 58 s.AddCleanup(r) 59 60 // POST requires root 61 s.expectedWriteAccess = daemon.RootAccess{} 62 } 63 64 func mockQuotas(st *state.State, c *check.C) { 65 err := servicestate.CreateQuota(st, "foo", "", nil, 9000) 66 c.Assert(err, check.IsNil) 67 err = servicestate.CreateQuota(st, "bar", "foo", nil, 1000) 68 c.Assert(err, check.IsNil) 69 err = servicestate.CreateQuota(st, "baz", "foo", nil, 2000) 70 c.Assert(err, check.IsNil) 71 } 72 73 func (s *apiQuotaSuite) TestPostQuotaUnknownAction(c *check.C) { 74 data, err := json.Marshal(daemon.PostQuotaGroupData{Action: "foo", GroupName: "bar"}) 75 c.Assert(err, check.IsNil) 76 77 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 78 c.Assert(err, check.IsNil) 79 rspe := s.errorReq(c, req, nil) 80 c.Assert(rspe.Status, check.Equals, 400) 81 c.Check(rspe.Message, check.Equals, `unknown quota action "foo"`) 82 } 83 84 func (s *apiQuotaSuite) TestPostQuotaInvalidGroupName(c *check.C) { 85 data, err := json.Marshal(daemon.PostQuotaGroupData{Action: "ensure", GroupName: "$$$"}) 86 c.Assert(err, check.IsNil) 87 88 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 89 c.Assert(err, check.IsNil) 90 rspe := s.errorReq(c, req, nil) 91 c.Assert(rspe.Status, check.Equals, 400) 92 c.Check(rspe.Message, check.Matches, `invalid quota group name: .*`) 93 } 94 95 func (s *apiQuotaSuite) TestPostEnsureQuotaUnhappy(c *check.C) { 96 daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) error { 97 c.Check(name, check.Equals, "booze") 98 c.Check(parentName, check.Equals, "foo") 99 c.Check(snaps, check.DeepEquals, []string{"bar"}) 100 c.Check(memoryLimit, check.DeepEquals, quantity.Size(1000)) 101 return fmt.Errorf("boom") 102 }) 103 104 data, err := json.Marshal(daemon.PostQuotaGroupData{ 105 Action: "ensure", 106 GroupName: "booze", 107 Parent: "foo", 108 Snaps: []string{"bar"}, 109 MaxMemory: 1000, 110 }) 111 c.Assert(err, check.IsNil) 112 113 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 114 c.Assert(err, check.IsNil) 115 rspe := s.errorReq(c, req, nil) 116 c.Check(rspe.Status, check.Equals, 400) 117 c.Check(rspe.Message, check.Matches, `boom`) 118 } 119 120 func (s *apiQuotaSuite) TestPostEnsureQuotaCreateHappy(c *check.C) { 121 var called int 122 daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) error { 123 called++ 124 c.Check(name, check.Equals, "booze") 125 c.Check(parentName, check.Equals, "foo") 126 c.Check(snaps, check.DeepEquals, []string{"some-snap"}) 127 c.Check(memoryLimit, check.DeepEquals, quantity.Size(1000)) 128 return nil 129 }) 130 131 data, err := json.Marshal(daemon.PostQuotaGroupData{ 132 Action: "ensure", 133 GroupName: "booze", 134 Parent: "foo", 135 Snaps: []string{"some-snap"}, 136 MaxMemory: 1000, 137 }) 138 c.Assert(err, check.IsNil) 139 140 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 141 c.Assert(err, check.IsNil) 142 rsp := s.syncReq(c, req, nil) 143 c.Assert(rsp.Status, check.Equals, 200) 144 c.Assert(called, check.Equals, 1) 145 } 146 147 func (s *apiQuotaSuite) TestPostEnsureQuotaUpdateHappy(c *check.C) { 148 st := s.d.Overlord().State() 149 st.Lock() 150 err := servicestate.CreateQuota(st, "ginger-ale", "", nil, 1000) 151 st.Unlock() 152 c.Assert(err, check.IsNil) 153 154 r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) error { 155 c.Errorf("should not have called create quota") 156 return fmt.Errorf("broken test") 157 }) 158 defer r() 159 160 updateCalled := 0 161 r = daemon.MockServicestateUpdateQuota(func(st *state.State, name string, opts servicestate.QuotaGroupUpdate) error { 162 updateCalled++ 163 c.Assert(name, check.Equals, "ginger-ale") 164 c.Assert(opts, check.DeepEquals, servicestate.QuotaGroupUpdate{ 165 AddSnaps: []string{"some-snap"}, 166 NewMemoryLimit: 9000, 167 }) 168 return nil 169 }) 170 defer r() 171 172 data, err := json.Marshal(daemon.PostQuotaGroupData{ 173 Action: "ensure", 174 GroupName: "ginger-ale", 175 Snaps: []string{"some-snap"}, 176 MaxMemory: 9000, 177 }) 178 c.Assert(err, check.IsNil) 179 180 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 181 c.Assert(err, check.IsNil) 182 rsp := s.syncReq(c, req, nil) 183 c.Assert(rsp.Status, check.Equals, 200) 184 c.Assert(updateCalled, check.Equals, 1) 185 } 186 187 func (s *apiQuotaSuite) TestPostRemoveQuotaHappy(c *check.C) { 188 var called int 189 daemon.MockServicestateRemoveQuota(func(st *state.State, name string) error { 190 called++ 191 c.Check(name, check.Equals, "booze") 192 return nil 193 }) 194 195 data, err := json.Marshal(daemon.PostQuotaGroupData{ 196 Action: "remove", 197 GroupName: "booze", 198 }) 199 c.Assert(err, check.IsNil) 200 201 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 202 c.Assert(err, check.IsNil) 203 s.asRootAuth(req) 204 205 rec := httptest.NewRecorder() 206 s.serveHTTP(c, rec, req) 207 c.Assert(rec.Code, check.Equals, 200) 208 c.Assert(called, check.Equals, 1) 209 } 210 211 func (s *apiQuotaSuite) TestPostRemoveQuotaUnhappy(c *check.C) { 212 daemon.MockServicestateRemoveQuota(func(st *state.State, name string) error { 213 c.Check(name, check.Equals, "booze") 214 return fmt.Errorf("boom") 215 }) 216 217 data, err := json.Marshal(daemon.PostQuotaGroupData{ 218 Action: "remove", 219 GroupName: "booze", 220 }) 221 c.Assert(err, check.IsNil) 222 223 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 224 c.Assert(err, check.IsNil) 225 rspe := s.errorReq(c, req, nil) 226 c.Check(rspe.Status, check.Equals, 400) 227 c.Check(rspe.Message, check.Matches, `boom`) 228 } 229 230 func (s *systemsSuite) TestPostQuotaRequiresRoot(c *check.C) { 231 s.daemon(c) 232 233 daemon.MockServicestateRemoveQuota(func(st *state.State, name string) error { 234 c.Fatalf("remove quota should not get called") 235 return nil 236 }) 237 238 data, err := json.Marshal(daemon.PostQuotaGroupData{ 239 Action: "remove", 240 GroupName: "booze", 241 }) 242 c.Assert(err, check.IsNil) 243 244 req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) 245 c.Assert(err, check.IsNil) 246 s.asUserAuth(c, req) 247 248 rec := httptest.NewRecorder() 249 s.serveHTTP(c, rec, req) 250 c.Check(rec.Code, check.Equals, 403) 251 } 252 253 func (s *apiQuotaSuite) TestListQuotas(c *check.C) { 254 st := s.d.Overlord().State() 255 st.Lock() 256 mockQuotas(st, c) 257 st.Unlock() 258 259 calls := 0 260 r := daemon.MockGetQuotaMemUsage(func(grp *quota.Group) (quantity.Size, error) { 261 calls++ 262 switch grp.Name { 263 case "bar": 264 return quantity.Size(500), nil 265 case "baz": 266 return quantity.Size(1000), nil 267 case "foo": 268 return quantity.Size(5000), nil 269 default: 270 c.Errorf("unexpected call to get group memory usage for group %q", grp.Name) 271 return 0, fmt.Errorf("broken test") 272 } 273 }) 274 defer r() 275 defer func() { 276 c.Assert(calls, check.Equals, 3) 277 }() 278 279 req, err := http.NewRequest("GET", "/v2/quotas", nil) 280 c.Assert(err, check.IsNil) 281 rsp := s.syncReq(c, req, nil) 282 c.Assert(rsp.Status, check.Equals, 200) 283 c.Assert(rsp.Result, check.FitsTypeOf, []client.QuotaGroupResult{}) 284 res := rsp.Result.([]client.QuotaGroupResult) 285 c.Check(res, check.DeepEquals, []client.QuotaGroupResult{ 286 { 287 GroupName: "bar", 288 Parent: "foo", 289 MaxMemory: 1000, 290 CurrentMemory: 500, 291 }, 292 { 293 GroupName: "baz", 294 Parent: "foo", 295 MaxMemory: 2000, 296 CurrentMemory: 1000, 297 }, 298 { 299 GroupName: "foo", 300 Subgroups: []string{"bar", "baz"}, 301 MaxMemory: 9000, 302 CurrentMemory: 5000, 303 }, 304 }) 305 } 306 307 func (s *apiQuotaSuite) TestGetQuota(c *check.C) { 308 st := s.d.Overlord().State() 309 st.Lock() 310 mockQuotas(st, c) 311 st.Unlock() 312 313 calls := 0 314 r := daemon.MockGetQuotaMemUsage(func(grp *quota.Group) (quantity.Size, error) { 315 calls++ 316 c.Assert(grp.Name, check.Equals, "bar") 317 return quantity.Size(500), nil 318 }) 319 defer r() 320 defer func() { 321 c.Assert(calls, check.Equals, 1) 322 }() 323 324 req, err := http.NewRequest("GET", "/v2/quotas/bar", nil) 325 c.Assert(err, check.IsNil) 326 rsp := s.syncReq(c, req, nil) 327 c.Assert(rsp.Status, check.Equals, 200) 328 c.Assert(rsp.Result, check.FitsTypeOf, client.QuotaGroupResult{}) 329 res := rsp.Result.(client.QuotaGroupResult) 330 c.Check(res, check.DeepEquals, client.QuotaGroupResult{ 331 GroupName: "bar", 332 Parent: "foo", 333 MaxMemory: 1000, 334 CurrentMemory: 500, 335 }) 336 } 337 338 func (s *apiQuotaSuite) TestGetQuotaInvalidName(c *check.C) { 339 st := s.d.Overlord().State() 340 st.Lock() 341 mockQuotas(st, c) 342 st.Unlock() 343 344 req, err := http.NewRequest("GET", "/v2/quotas/000", nil) 345 c.Assert(err, check.IsNil) 346 rspe := s.errorReq(c, req, nil) 347 c.Check(rspe.Status, check.Equals, 400) 348 c.Check(rspe.Message, check.Matches, `invalid quota group name: .*`) 349 } 350 351 func (s *apiQuotaSuite) TestGetQuotaNotFound(c *check.C) { 352 req, err := http.NewRequest("GET", "/v2/quotas/unknown", nil) 353 c.Assert(err, check.IsNil) 354 rspe := s.errorReq(c, req, nil) 355 c.Check(rspe.Status, check.Equals, 404) 356 c.Check(rspe.Message, check.Matches, `cannot find quota group "unknown"`) 357 }