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