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(&quotaSuite{})
    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  }