gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/hookstate/ctlcmd/set_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 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 ctlcmd_test
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"strings"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/interfaces"
    30  	"github.com/snapcore/snapd/overlord/configstate/config"
    31  	"github.com/snapcore/snapd/overlord/hookstate"
    32  	"github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
    33  	"github.com/snapcore/snapd/overlord/hookstate/hooktest"
    34  	"github.com/snapcore/snapd/overlord/state"
    35  	"github.com/snapcore/snapd/snap"
    36  )
    37  
    38  type setSuite struct {
    39  	mockContext *hookstate.Context
    40  	mockHandler *hooktest.MockHandler
    41  }
    42  
    43  type setAttrSuite struct {
    44  	mockPlugHookContext *hookstate.Context
    45  	mockSlotHookContext *hookstate.Context
    46  	mockHandler         *hooktest.MockHandler
    47  }
    48  
    49  var _ = Suite(&setSuite{})
    50  var _ = Suite(&setAttrSuite{})
    51  
    52  func (s *setSuite) SetUpTest(c *C) {
    53  	s.mockHandler = hooktest.NewMockHandler()
    54  
    55  	state := state.New(nil)
    56  	state.Lock()
    57  	defer state.Unlock()
    58  
    59  	task := state.NewTask("test-task", "my test task")
    60  	setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
    61  
    62  	var err error
    63  	s.mockContext, err = hookstate.NewContext(task, task.State(), setup, s.mockHandler, "")
    64  	c.Assert(err, IsNil)
    65  }
    66  
    67  func (s *setSuite) TestInvalidArguments(c *C) {
    68  	_, _, err := ctlcmd.Run(s.mockContext, []string{"set"}, 0)
    69  	c.Check(err, ErrorMatches, "set which option.*")
    70  	_, _, err = ctlcmd.Run(s.mockContext, []string{"set", "foo", "bar"}, 0)
    71  	c.Check(err, ErrorMatches, ".*invalid parameter.*want key=value.*")
    72  	_, _, err = ctlcmd.Run(s.mockContext, []string{"set", ":foo", "bar=baz"}, 0)
    73  	c.Check(err, ErrorMatches, ".*interface attributes can only be set during the execution of prepare hooks.*")
    74  }
    75  
    76  func (s *setSuite) TestCommand(c *C) {
    77  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "foo=bar", "baz=qux"}, 0)
    78  	c.Check(err, IsNil)
    79  	c.Check(string(stdout), Equals, "")
    80  	c.Check(string(stderr), Equals, "")
    81  
    82  	// Verify that the previous set doesn't modify the global state
    83  	s.mockContext.State().Lock()
    84  	tr := config.NewTransaction(s.mockContext.State())
    85  	s.mockContext.State().Unlock()
    86  	var value string
    87  	c.Check(tr.Get("test-snap", "foo", &value), ErrorMatches, ".*snap.*has no.*configuration.*")
    88  	c.Check(tr.Get("test-snap", "baz", &value), ErrorMatches, ".*snap.*has no.*configuration.*")
    89  
    90  	// Notify the context that we're done. This should save the config.
    91  	s.mockContext.Lock()
    92  	defer s.mockContext.Unlock()
    93  	c.Check(s.mockContext.Done(), IsNil)
    94  
    95  	// Verify that the global config has been updated.
    96  	tr = config.NewTransaction(s.mockContext.State())
    97  	c.Check(tr.Get("test-snap", "foo", &value), IsNil)
    98  	c.Check(value, Equals, "bar")
    99  	c.Check(tr.Get("test-snap", "baz", &value), IsNil)
   100  	c.Check(value, Equals, "qux")
   101  }
   102  
   103  func (s *setSuite) TestSetRegularUserForbidden(c *C) {
   104  	_, _, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key1"}, 1000)
   105  	c.Assert(err, ErrorMatches, `cannot use "set" with uid 1000, try with sudo`)
   106  	forbidden, _ := err.(*ctlcmd.ForbiddenCommandError)
   107  	c.Assert(forbidden, NotNil)
   108  }
   109  
   110  func (s *setSuite) TestSetHelpRegularUserAllowed(c *C) {
   111  	_, _, err := ctlcmd.Run(s.mockContext, []string{"set", "-h"}, 1000)
   112  	c.Assert(err, NotNil)
   113  	c.Assert(strings.HasPrefix(err.Error(), "Usage:"), Equals, true)
   114  }
   115  
   116  func (s *setSuite) TestSetConfigOptionWithColon(c *C) {
   117  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "device-service.url=192.168.0.1:5555"}, 0)
   118  	c.Check(err, IsNil)
   119  	c.Check(string(stdout), Equals, "")
   120  	c.Check(string(stderr), Equals, "")
   121  
   122  	// Notify the context that we're done. This should save the config.
   123  	s.mockContext.Lock()
   124  	defer s.mockContext.Unlock()
   125  	c.Check(s.mockContext.Done(), IsNil)
   126  
   127  	// Verify that the global config has been updated.
   128  	var value string
   129  	tr := config.NewTransaction(s.mockContext.State())
   130  	c.Check(tr.Get("test-snap", "device-service.url", &value), IsNil)
   131  	c.Check(value, Equals, "192.168.0.1:5555")
   132  }
   133  
   134  func (s *setSuite) TestUnsetConfigOptionWithInitialConfiguration(c *C) {
   135  	// Setup an initial configuration
   136  	s.mockContext.State().Lock()
   137  	tr := config.NewTransaction(s.mockContext.State())
   138  	tr.Set("test-snap", "test-key1", "test-value1")
   139  	tr.Set("test-snap", "test-key2", "test-value2")
   140  	tr.Set("test-snap", "test-key3.foo", "foo-value")
   141  	tr.Set("test-snap", "test-key3.bar", "bar-value")
   142  	tr.Commit()
   143  	s.mockContext.State().Unlock()
   144  
   145  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key1!", "test-key3.foo!"}, 0)
   146  	c.Check(err, IsNil)
   147  	c.Check(string(stdout), Equals, "")
   148  	c.Check(string(stderr), Equals, "")
   149  
   150  	// Notify the context that we're done. This should save the config.
   151  	s.mockContext.Lock()
   152  	defer s.mockContext.Unlock()
   153  	c.Check(s.mockContext.Done(), IsNil)
   154  
   155  	// Verify that the global config has been updated.
   156  	var value string
   157  	tr = config.NewTransaction(s.mockContext.State())
   158  	c.Check(tr.Get("test-snap", "test-key2", &value), IsNil)
   159  	c.Check(value, Equals, "test-value2")
   160  	c.Check(tr.Get("test-snap", "test-key1", &value), ErrorMatches, `snap "test-snap" has no "test-key1" configuration option`)
   161  	var value2 interface{}
   162  	c.Check(tr.Get("test-snap", "test-key3", &value2), IsNil)
   163  	c.Check(value2, DeepEquals, map[string]interface{}{"bar": "bar-value"})
   164  }
   165  
   166  func (s *setSuite) TestUnsetConfigOptionWithNoInitialConfiguration(c *C) {
   167  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key.key1=value1", "test-key.key2=value2", "test-key.key1!"}, 0)
   168  	c.Check(err, IsNil)
   169  	c.Check(string(stdout), Equals, "")
   170  	c.Check(string(stderr), Equals, "")
   171  
   172  	// Notify the context that we're done. This should save the config.
   173  	s.mockContext.Lock()
   174  	defer s.mockContext.Unlock()
   175  	c.Check(s.mockContext.Done(), IsNil)
   176  
   177  	// Verify that the global config has been updated.
   178  	var value interface{}
   179  	tr := config.NewTransaction(s.mockContext.State())
   180  	c.Check(tr.Get("test-snap", "test-key.key2", &value), IsNil)
   181  	c.Check(value, DeepEquals, "value2")
   182  	c.Check(tr.Get("test-snap", "test-key.key1", &value), ErrorMatches, `snap "test-snap" has no "test-key.key1" configuration option`)
   183  	c.Check(value, DeepEquals, "value2")
   184  }
   185  
   186  func (s *setSuite) TestSetNumbers(c *C) {
   187  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "foo=1234567890", "bar=123456.7890"}, 0)
   188  	c.Check(err, IsNil)
   189  	c.Check(string(stdout), Equals, "")
   190  	c.Check(string(stderr), Equals, "")
   191  
   192  	// Notify the context that we're done. This should save the config.
   193  	s.mockContext.Lock()
   194  	defer s.mockContext.Unlock()
   195  	c.Check(s.mockContext.Done(), IsNil)
   196  
   197  	// Verify that the global config has been updated.
   198  	var value interface{}
   199  	tr := config.NewTransaction(s.mockContext.State())
   200  	c.Check(tr.Get("test-snap", "foo", &value), IsNil)
   201  	c.Check(value, Equals, json.Number("1234567890"))
   202  
   203  	c.Check(tr.Get("test-snap", "bar", &value), IsNil)
   204  	c.Check(value, Equals, json.Number("123456.7890"))
   205  }
   206  
   207  func (s *setSuite) TestSetStrictJSON(c *C) {
   208  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "-t", `key={"a":"b", "c": 1, "d": {"e":"f"}}`}, 0)
   209  	c.Assert(err, IsNil)
   210  	c.Check(string(stdout), Equals, "")
   211  	c.Check(string(stderr), Equals, "")
   212  
   213  	// Notify the context that we're done. This should save the config.
   214  	s.mockContext.Lock()
   215  	defer s.mockContext.Unlock()
   216  	c.Check(s.mockContext.Done(), IsNil)
   217  
   218  	// Verify that the global config has been updated.
   219  	var value interface{}
   220  	tr := config.NewTransaction(s.mockContext.State())
   221  	c.Assert(tr.Get("test-snap", "key", &value), IsNil)
   222  	c.Check(value, DeepEquals, map[string]interface{}{"a": "b", "c": json.Number("1"), "d": map[string]interface{}{"e": "f"}})
   223  }
   224  
   225  func (s *setSuite) TestSetFailWithStrictJSON(c *C) {
   226  	_, _, err := ctlcmd.Run(s.mockContext, []string{"set", "-t", `key=a`}, 0)
   227  	c.Assert(err, ErrorMatches, "failed to parse JSON:.*")
   228  }
   229  
   230  func (s *setSuite) TestSetAsString(c *C) {
   231  	expected := `{"a":"b", "c": 1, "d": {"e": "f"}}`
   232  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "-s", fmt.Sprintf("key=%s", expected)}, 0)
   233  	c.Assert(err, IsNil)
   234  	c.Check(string(stdout), Equals, "")
   235  	c.Check(string(stderr), Equals, "")
   236  
   237  	// Notify the context that we're done. This should save the config.
   238  	s.mockContext.Lock()
   239  	defer s.mockContext.Unlock()
   240  	c.Check(s.mockContext.Done(), IsNil)
   241  
   242  	// Verify that the global config has been updated.
   243  	var value interface{}
   244  	tr := config.NewTransaction(s.mockContext.State())
   245  	c.Assert(tr.Get("test-snap", "key", &value), IsNil)
   246  	c.Check(value, Equals, expected)
   247  }
   248  
   249  func (s *setSuite) TestSetErrorOnStrictJSONAndString(c *C) {
   250  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "-s", "-t", `{"a":"b"}`}, 0)
   251  	c.Assert(err, ErrorMatches, "cannot use -t and -s together")
   252  	c.Check(string(stdout), Equals, "")
   253  	c.Check(string(stderr), Equals, "")
   254  }
   255  
   256  func (s *setSuite) TestCommandSavesDeltasOnly(c *C) {
   257  	// Setup an initial configuration
   258  	s.mockContext.State().Lock()
   259  	tr := config.NewTransaction(s.mockContext.State())
   260  	tr.Set("test-snap", "test-key1", "test-value1")
   261  	tr.Set("test-snap", "test-key2", "test-value2")
   262  	tr.Commit()
   263  	s.mockContext.State().Unlock()
   264  
   265  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key2=test-value3"}, 0)
   266  	c.Check(err, IsNil)
   267  	c.Check(string(stdout), Equals, "")
   268  	c.Check(string(stderr), Equals, "")
   269  
   270  	// Notify the context that we're done. This should save the config.
   271  	s.mockContext.Lock()
   272  	defer s.mockContext.Unlock()
   273  	c.Check(s.mockContext.Done(), IsNil)
   274  
   275  	// Verify that the global config has been updated, but only test-key2
   276  	tr = config.NewTransaction(s.mockContext.State())
   277  	var value string
   278  	c.Check(tr.Get("test-snap", "test-key1", &value), IsNil)
   279  	c.Check(value, Equals, "test-value1")
   280  	c.Check(tr.Get("test-snap", "test-key2", &value), IsNil)
   281  	c.Check(value, Equals, "test-value3")
   282  }
   283  
   284  func (s *setSuite) TestCommandWithoutContext(c *C) {
   285  	_, _, err := ctlcmd.Run(nil, []string{"set", "foo=bar"}, 0)
   286  	c.Check(err, ErrorMatches, `cannot invoke snapctl operation commands \(here "set"\) from outside of a snap`)
   287  }
   288  
   289  func (s *setAttrSuite) SetUpTest(c *C) {
   290  	s.mockHandler = hooktest.NewMockHandler()
   291  	state := state.New(nil)
   292  	state.Lock()
   293  	ch := state.NewChange("mychange", "mychange")
   294  
   295  	attrsTask := state.NewTask("connect-task", "my connect task")
   296  	attrsTask.Set("plug", &interfaces.PlugRef{Snap: "a", Name: "aplug"})
   297  	attrsTask.Set("slot", &interfaces.SlotRef{Snap: "b", Name: "bslot"})
   298  	staticAttrs := map[string]interface{}{
   299  		"lorem": "ipsum",
   300  		"nested": map[string]interface{}{
   301  			"x": "y",
   302  		},
   303  	}
   304  	dynamicAttrs := make(map[string]interface{})
   305  	attrsTask.Set("plug-static", staticAttrs)
   306  	attrsTask.Set("plug-dynamic", dynamicAttrs)
   307  	attrsTask.Set("slot-static", staticAttrs)
   308  	attrsTask.Set("slot-dynamic", dynamicAttrs)
   309  	ch.AddTask(attrsTask)
   310  	state.Unlock()
   311  
   312  	var err error
   313  
   314  	// setup plug hook task
   315  	state.Lock()
   316  	plugHookTask := state.NewTask("run-hook", "my test task")
   317  	state.Unlock()
   318  	plugTaskSetup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "prepare-plug-aplug"}
   319  	s.mockPlugHookContext, err = hookstate.NewContext(plugHookTask, plugHookTask.State(), plugTaskSetup, s.mockHandler, "")
   320  	c.Assert(err, IsNil)
   321  
   322  	s.mockPlugHookContext.Lock()
   323  	s.mockPlugHookContext.Set("attrs-task", attrsTask.ID())
   324  	s.mockPlugHookContext.Unlock()
   325  	state.Lock()
   326  	ch.AddTask(plugHookTask)
   327  	state.Unlock()
   328  
   329  	// setup slot hook task
   330  	state.Lock()
   331  	slotHookTask := state.NewTask("run-hook", "my test task")
   332  	state.Unlock()
   333  	slotTaskSetup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "prepare-slot-aplug"}
   334  	s.mockSlotHookContext, err = hookstate.NewContext(slotHookTask, slotHookTask.State(), slotTaskSetup, s.mockHandler, "")
   335  	c.Assert(err, IsNil)
   336  
   337  	s.mockSlotHookContext.Lock()
   338  	s.mockSlotHookContext.Set("attrs-task", attrsTask.ID())
   339  	s.mockSlotHookContext.Unlock()
   340  
   341  	state.Lock()
   342  	defer state.Unlock()
   343  	ch.AddTask(slotHookTask)
   344  }
   345  
   346  func (s *setAttrSuite) TestSetPlugAttributesInPlugHook(c *C) {
   347  	stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"set", ":aplug", "foo=bar"}, 0)
   348  	c.Check(err, IsNil)
   349  	c.Check(string(stdout), Equals, "")
   350  	c.Check(string(stderr), Equals, "")
   351  
   352  	attrsTask, err := ctlcmd.AttributesTask(s.mockPlugHookContext)
   353  	c.Assert(err, IsNil)
   354  	st := s.mockPlugHookContext.State()
   355  	st.Lock()
   356  	defer st.Unlock()
   357  	dynattrs := make(map[string]interface{})
   358  	err = attrsTask.Get("plug-dynamic", &dynattrs)
   359  	c.Assert(err, IsNil)
   360  	c.Check(dynattrs["foo"], Equals, "bar")
   361  }
   362  
   363  func (s *setAttrSuite) TestSetPlugAttributesSupportsDottedSyntax(c *C) {
   364  	stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"set", ":aplug", "my.attr1=foo", "my.attr2=bar"}, 0)
   365  	c.Check(err, IsNil)
   366  	c.Check(string(stdout), Equals, "")
   367  	c.Check(string(stderr), Equals, "")
   368  
   369  	attrsTask, err := ctlcmd.AttributesTask(s.mockPlugHookContext)
   370  	c.Assert(err, IsNil)
   371  	st := s.mockPlugHookContext.State()
   372  	st.Lock()
   373  	defer st.Unlock()
   374  	dynattrs := make(map[string]interface{})
   375  	err = attrsTask.Get("plug-dynamic", &dynattrs)
   376  	c.Assert(err, IsNil)
   377  	c.Check(dynattrs["my"], DeepEquals, map[string]interface{}{"attr1": "foo", "attr2": "bar"})
   378  }
   379  
   380  func (s *setAttrSuite) TestPlugOrSlotEmpty(c *C) {
   381  	stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"set", ":", "foo=bar"}, 0)
   382  	c.Check(err, ErrorMatches, "plug or slot name not provided")
   383  	c.Check(string(stdout), Equals, "")
   384  	c.Check(string(stderr), Equals, "")
   385  }
   386  
   387  func (s *setAttrSuite) TestSetCommandFailsOutsideOfValidContext(c *C) {
   388  	var err error
   389  	var mockContext *hookstate.Context
   390  
   391  	state := state.New(nil)
   392  	state.Lock()
   393  	defer state.Unlock()
   394  
   395  	task := state.NewTask("test-task", "my test task")
   396  	setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "not-a-connect-hook"}
   397  	mockContext, err = hookstate.NewContext(task, task.State(), setup, s.mockHandler, "")
   398  	c.Assert(err, IsNil)
   399  
   400  	stdout, stderr, err := ctlcmd.Run(mockContext, []string{"set", ":aplug", "foo=bar"}, 0)
   401  	c.Check(err, ErrorMatches, `interface attributes can only be set during the execution of prepare hooks`)
   402  	c.Check(string(stdout), Equals, "")
   403  	c.Check(string(stderr), Equals, "")
   404  }