gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/hookstate/ctlcmd/refresh_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 ctlcmd_test
    21  
    22  import (
    23  	"fmt"
    24  	"time"
    25  
    26  	. "gopkg.in/check.v1"
    27  
    28  	"github.com/snapcore/snapd/dirs"
    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/ifacestate/ifacerepo"
    35  	"github.com/snapcore/snapd/overlord/snapstate"
    36  	"github.com/snapcore/snapd/overlord/state"
    37  	"github.com/snapcore/snapd/snap"
    38  	"github.com/snapcore/snapd/testutil"
    39  )
    40  
    41  type refreshSuite struct {
    42  	testutil.BaseTest
    43  	st          *state.State
    44  	mockHandler *hooktest.MockHandler
    45  }
    46  
    47  var _ = Suite(&refreshSuite{})
    48  
    49  func mockRefreshCandidate(snapName, instanceKey, channel, version string, revision snap.Revision) interface{} {
    50  	sup := &snapstate.SnapSetup{
    51  		Channel:     channel,
    52  		InstanceKey: instanceKey,
    53  		SideInfo: &snap.SideInfo{
    54  			Revision: revision,
    55  			RealName: snapName,
    56  		},
    57  	}
    58  	return snapstate.MockRefreshCandidate(sup, version)
    59  }
    60  
    61  func (s *refreshSuite) SetUpTest(c *C) {
    62  	s.BaseTest.SetUpTest(c)
    63  	dirs.SetRootDir(c.MkDir())
    64  	s.AddCleanup(func() { dirs.SetRootDir("/") })
    65  	s.st = state.New(nil)
    66  	s.mockHandler = hooktest.NewMockHandler()
    67  
    68  	// snapstate.AffectedByRefreshCandidates needs a cached iface repo
    69  	repo := interfaces.NewRepository()
    70  	// no interfaces needed for this test suite
    71  	s.st.Lock()
    72  	defer s.st.Unlock()
    73  	ifacerepo.Replace(s.st, repo)
    74  }
    75  
    76  var refreshFromHookTests = []struct {
    77  	args                []string
    78  	base, restart       bool
    79  	inhibited           bool
    80  	refreshCandidates   map[string]interface{}
    81  	stdout, stderr, err string
    82  	exitCode            int
    83  }{{
    84  	args: []string{"refresh", "--proceed", "--hold"},
    85  	err:  "cannot use --proceed and --hold together",
    86  }, {
    87  	args: []string{"refresh", "--pending"},
    88  	refreshCandidates: map[string]interface{}{
    89  		"snap1": mockRefreshCandidate("snap1", "", "edge", "v1", snap.Revision{N: 3}),
    90  	},
    91  	stdout: "pending: ready\nchannel: edge\nversion: v1\nrevision: 3\nbase: false\nrestart: false\n",
    92  }, {
    93  	args:   []string{"refresh", "--pending"},
    94  	stdout: "pending: none\nchannel: stable\nbase: false\nrestart: false\n",
    95  }, {
    96  	args: []string{"refresh", "--pending"},
    97  	refreshCandidates: map[string]interface{}{
    98  		"snap1-base": mockRefreshCandidate("snap1-base", "", "edge", "v1", snap.Revision{N: 3}),
    99  	},
   100  	stdout: "pending: none\nchannel: stable\nbase: true\nrestart: false\n",
   101  }, {
   102  	args: []string{"refresh", "--pending"},
   103  	refreshCandidates: map[string]interface{}{
   104  		"kernel": mockRefreshCandidate("kernel", "", "edge", "v1", snap.Revision{N: 3}),
   105  	},
   106  	stdout: "pending: none\nchannel: stable\nbase: false\nrestart: true\n",
   107  }, {
   108  	args:      []string{"refresh", "--pending"},
   109  	inhibited: true,
   110  	stdout:    "pending: inhibited\nchannel: stable\nbase: false\nrestart: false\n",
   111  }, {
   112  	args: []string{"refresh", "--hold"},
   113  	err:  `internal error: snap "snap1" is not affected by any snaps`,
   114  }}
   115  
   116  func (s *refreshSuite) TestRefreshFromHook(c *C) {
   117  	s.st.Lock()
   118  	task := s.st.NewTask("test-task", "my test task")
   119  	setup := &hookstate.HookSetup{Snap: "snap1", Revision: snap.R(1), Hook: "gate-auto-refresh"}
   120  	mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "")
   121  	c.Check(err, IsNil)
   122  	mockInstalledSnap(c, s.st, `name: snap1
   123  base: snap1-base
   124  version: 1
   125  hooks:
   126   gate-auto-refresh:
   127  `)
   128  	mockInstalledSnap(c, s.st, `name: snap1-base
   129  type: base
   130  version: 1
   131  `)
   132  	mockInstalledSnap(c, s.st, `name: kernel
   133  type: kernel
   134  version: 1
   135  `)
   136  	s.st.Unlock()
   137  
   138  	for _, test := range refreshFromHookTests {
   139  		s.st.Lock()
   140  		s.st.Set("refresh-candidates", test.refreshCandidates)
   141  		if test.inhibited {
   142  			var snapst snapstate.SnapState
   143  			c.Assert(snapstate.Get(s.st, "snap1", &snapst), IsNil)
   144  			snapst.RefreshInhibitedTime = &time.Time{}
   145  			snapstate.Set(s.st, "snap1", &snapst)
   146  		}
   147  		s.st.Unlock()
   148  
   149  		stdout, stderr, err := ctlcmd.Run(mockContext, test.args, 0)
   150  		comment := Commentf("%s", test.args)
   151  		if test.exitCode > 0 {
   152  			c.Check(err, DeepEquals, &ctlcmd.UnsuccessfulError{ExitCode: test.exitCode}, comment)
   153  		} else {
   154  			if test.err == "" {
   155  				c.Check(err, IsNil, comment)
   156  			} else {
   157  				c.Check(err, ErrorMatches, test.err, comment)
   158  			}
   159  		}
   160  
   161  		c.Check(string(stdout), Equals, test.stdout, comment)
   162  		c.Check(string(stderr), Equals, "", comment)
   163  	}
   164  }
   165  
   166  func (s *refreshSuite) TestRefreshHold(c *C) {
   167  	s.st.Lock()
   168  	task := s.st.NewTask("test-task", "my test task")
   169  	setup := &hookstate.HookSetup{Snap: "snap1", Revision: snap.R(1), Hook: "gate-auto-refresh"}
   170  	mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "")
   171  	c.Check(err, IsNil)
   172  
   173  	mockInstalledSnap(c, s.st, `name: snap1
   174  base: snap1-base
   175  version: 1
   176  hooks:
   177   gate-auto-refresh:
   178  `)
   179  	mockInstalledSnap(c, s.st, `name: snap1-base
   180  type: base
   181  version: 1
   182  `)
   183  
   184  	candidates := map[string]interface{}{
   185  		"snap1-base": mockRefreshCandidate("snap1-base", "", "edge", "v1", snap.Revision{N: 3}),
   186  	}
   187  	s.st.Set("refresh-candidates", candidates)
   188  
   189  	s.st.Unlock()
   190  
   191  	stdout, stderr, err := ctlcmd.Run(mockContext, []string{"refresh", "--hold"}, 0)
   192  	c.Assert(err, IsNil)
   193  	c.Check(string(stdout), Equals, "")
   194  	c.Check(string(stderr), Equals, "")
   195  
   196  	mockContext.Lock()
   197  	defer mockContext.Unlock()
   198  	action := mockContext.Cached("action")
   199  	c.Assert(action, NotNil)
   200  	c.Check(action, Equals, snapstate.GateAutoRefreshHold)
   201  
   202  	var gating map[string]map[string]interface{}
   203  	c.Assert(s.st.Get("snaps-hold", &gating), IsNil)
   204  	c.Check(gating["snap1-base"]["snap1"], NotNil)
   205  }
   206  
   207  func (s *refreshSuite) TestRefreshProceed(c *C) {
   208  	s.st.Lock()
   209  	task := s.st.NewTask("test-task", "my test task")
   210  	setup := &hookstate.HookSetup{Snap: "snap1", Revision: snap.R(1), Hook: "gate-auto-refresh"}
   211  	mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "")
   212  	c.Check(err, IsNil)
   213  
   214  	mockInstalledSnap(c, s.st, `name: foo
   215  version: 1
   216  `)
   217  
   218  	// pretend snap foo is held initially
   219  	c.Check(snapstate.HoldRefresh(s.st, "snap1", 0, "foo"), IsNil)
   220  	s.st.Unlock()
   221  
   222  	// sanity check
   223  	var gating map[string]map[string]interface{}
   224  	s.st.Lock()
   225  	snapsHold := s.st.Get("snaps-hold", &gating)
   226  	s.st.Unlock()
   227  	c.Assert(snapsHold, IsNil)
   228  	c.Check(gating["foo"]["snap1"], NotNil)
   229  
   230  	mockContext.Lock()
   231  	mockContext.Set("affecting-snaps", []string{"foo"})
   232  	mockContext.Unlock()
   233  
   234  	stdout, stderr, err := ctlcmd.Run(mockContext, []string{"refresh", "--proceed"}, 0)
   235  	c.Assert(err, IsNil)
   236  	c.Check(string(stdout), Equals, "")
   237  	c.Check(string(stderr), Equals, "")
   238  
   239  	mockContext.Lock()
   240  	defer mockContext.Unlock()
   241  	action := mockContext.Cached("action")
   242  	c.Assert(action, NotNil)
   243  	c.Check(action, Equals, snapstate.GateAutoRefreshProceed)
   244  
   245  	// and it is still held (for hook handler to execute actual proceed logic).
   246  	gating = nil
   247  	c.Assert(s.st.Get("snaps-hold", &gating), IsNil)
   248  	c.Check(gating["foo"]["snap1"], NotNil)
   249  
   250  	mockContext.Cache("action", nil)
   251  
   252  	mockContext.Unlock()
   253  	defer mockContext.Lock()
   254  
   255  	// refresh --pending --proceed is the same as just saying --proceed.
   256  	stdout, stderr, err = ctlcmd.Run(mockContext, []string{"refresh", "--pending", "--proceed"}, 0)
   257  	c.Assert(err, IsNil)
   258  	c.Check(string(stdout), Equals, "")
   259  	c.Check(string(stderr), Equals, "")
   260  
   261  	mockContext.Lock()
   262  	defer mockContext.Unlock()
   263  	action = mockContext.Cached("action")
   264  	c.Assert(action, NotNil)
   265  	c.Check(action, Equals, snapstate.GateAutoRefreshProceed)
   266  }
   267  
   268  func (s *refreshSuite) TestRefreshFromUnsupportedHook(c *C) {
   269  	s.st.Lock()
   270  
   271  	task := s.st.NewTask("test-task", "my test task")
   272  	setup := &hookstate.HookSetup{Snap: "snap", Revision: snap.R(1), Hook: "install"}
   273  	mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "")
   274  	c.Check(err, IsNil)
   275  	s.st.Unlock()
   276  
   277  	_, _, err = ctlcmd.Run(mockContext, []string{"refresh"}, 0)
   278  	c.Check(err, ErrorMatches, `can only be used from gate-auto-refresh hook`)
   279  }
   280  
   281  func (s *refreshSuite) TestRefreshProceedFromSnap(c *C) {
   282  	var called bool
   283  	restore := ctlcmd.MockAutoRefreshForGatingSnap(func(st *state.State, gatingSnap string) error {
   284  		called = true
   285  		c.Check(gatingSnap, Equals, "foo")
   286  		return nil
   287  	})
   288  	defer restore()
   289  
   290  	s.st.Lock()
   291  	defer s.st.Unlock()
   292  	mockInstalledSnap(c, s.st, `name: foo
   293  version: 1
   294  `)
   295  
   296  	// enable gate-auto-refresh-hook feature
   297  	tr := config.NewTransaction(s.st)
   298  	tr.Set("core", "experimental.gate-auto-refresh-hook", true)
   299  	tr.Commit()
   300  
   301  	// foo is the snap that is going to call --proceed.
   302  	setup := &hookstate.HookSetup{Snap: "foo", Revision: snap.R(1)}
   303  	mockContext, err := hookstate.NewContext(nil, s.st, setup, nil, "")
   304  	c.Check(err, IsNil)
   305  	s.st.Unlock()
   306  	defer s.st.Lock()
   307  
   308  	_, _, err = ctlcmd.Run(mockContext, []string{"refresh", "--proceed"}, 0)
   309  	c.Assert(err, IsNil)
   310  	c.Check(called, Equals, true)
   311  }
   312  
   313  func (s *refreshSuite) TestPendingFromSnapNoRefreshCandidates(c *C) {
   314  	s.st.Lock()
   315  	defer s.st.Unlock()
   316  
   317  	mockInstalledSnap(c, s.st, `name: foo
   318  version: 1
   319  `)
   320  
   321  	setup := &hookstate.HookSetup{Snap: "foo", Revision: snap.R(1)}
   322  	mockContext, err := hookstate.NewContext(nil, s.st, setup, nil, "")
   323  	c.Check(err, IsNil)
   324  	s.st.Unlock()
   325  	defer s.st.Lock()
   326  
   327  	stdout, _, err := ctlcmd.Run(mockContext, []string{"refresh", "--pending"}, 0)
   328  	c.Assert(err, IsNil)
   329  	c.Check(string(stdout), Equals, "pending: none\nchannel: stable\nbase: false\nrestart: false\n")
   330  }
   331  
   332  func (s *refreshSuite) TestRefreshProceedFromSnapError(c *C) {
   333  	restore := ctlcmd.MockAutoRefreshForGatingSnap(func(st *state.State, gatingSnap string) error {
   334  		c.Check(gatingSnap, Equals, "foo")
   335  		return fmt.Errorf("boom")
   336  	})
   337  	defer restore()
   338  
   339  	s.st.Lock()
   340  	defer s.st.Unlock()
   341  	mockInstalledSnap(c, s.st, `name: foo
   342  version: 1
   343  `)
   344  
   345  	// enable gate-auto-refresh-hook feature
   346  	tr := config.NewTransaction(s.st)
   347  	tr.Set("core", "experimental.gate-auto-refresh-hook", true)
   348  	tr.Commit()
   349  
   350  	// foo is the snap that is going to call --proceed.
   351  	setup := &hookstate.HookSetup{Snap: "foo", Revision: snap.R(1)}
   352  	mockContext, err := hookstate.NewContext(nil, s.st, setup, nil, "")
   353  	c.Check(err, IsNil)
   354  	s.st.Unlock()
   355  	defer s.st.Lock()
   356  
   357  	_, _, err = ctlcmd.Run(mockContext, []string{"refresh", "--proceed"}, 0)
   358  	c.Assert(err, ErrorMatches, "boom")
   359  }
   360  
   361  func (s *refreshSuite) TestRefreshRegularUserForbidden(c *C) {
   362  	s.st.Lock()
   363  	setup := &hookstate.HookSetup{Snap: "snap", Revision: snap.R(1)}
   364  	s.st.Unlock()
   365  
   366  	mockContext, err := hookstate.NewContext(nil, s.st, setup, s.mockHandler, "")
   367  	c.Assert(err, IsNil)
   368  	_, _, err = ctlcmd.Run(mockContext, []string{"refresh"}, 1000)
   369  	c.Assert(err, ErrorMatches, `cannot use "refresh" with uid 1000, try with sudo`)
   370  	forbidden, _ := err.(*ctlcmd.ForbiddenCommandError)
   371  	c.Assert(forbidden, NotNil)
   372  }