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  }