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