github.com/ubuntu-core/snappy@v0.0.0-20210827154228-9e584df982bb/overlord/hookstate/hooks_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 hookstate_test
    21  
    22  import (
    23  	"fmt"
    24  	"strings"
    25  	"time"
    26  
    27  	. "gopkg.in/check.v1"
    28  	"gopkg.in/tomb.v2"
    29  
    30  	"github.com/snapcore/snapd/cmd/snaplock/runinhibit"
    31  	"github.com/snapcore/snapd/interfaces"
    32  	"github.com/snapcore/snapd/overlord/configstate/config"
    33  	"github.com/snapcore/snapd/overlord/hookstate"
    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/snap/snaptest"
    39  	"github.com/snapcore/snapd/testutil"
    40  )
    41  
    42  const snapaYaml = `name: snap-a
    43  version: 1
    44  base: base-snap-a
    45  hooks:
    46      gate-auto-refresh:
    47  `
    48  
    49  const snapaBaseYaml = `name: base-snap-a
    50  version: 1
    51  type: base
    52  `
    53  
    54  const snapbYaml = `name: snap-b
    55  version: 1
    56  `
    57  
    58  type gateAutoRefreshHookSuite struct {
    59  	baseHookManagerSuite
    60  }
    61  
    62  var _ = Suite(&gateAutoRefreshHookSuite{})
    63  
    64  func (s *gateAutoRefreshHookSuite) SetUpTest(c *C) {
    65  	s.commonSetUpTest(c)
    66  
    67  	s.state.Lock()
    68  	defer s.state.Unlock()
    69  
    70  	si := &snap.SideInfo{RealName: "snap-a", SnapID: "snap-a-id1", Revision: snap.R(1)}
    71  	snaptest.MockSnap(c, snapaYaml, si)
    72  	snapstate.Set(s.state, "snap-a", &snapstate.SnapState{
    73  		Active:   true,
    74  		Sequence: []*snap.SideInfo{si},
    75  		Current:  snap.R(1),
    76  	})
    77  
    78  	si2 := &snap.SideInfo{RealName: "snap-b", SnapID: "snap-b-id1", Revision: snap.R(1)}
    79  	snaptest.MockSnap(c, snapbYaml, si2)
    80  	snapstate.Set(s.state, "snap-b", &snapstate.SnapState{
    81  		Active:   true,
    82  		Sequence: []*snap.SideInfo{si2},
    83  		Current:  snap.R(1),
    84  	})
    85  
    86  	si3 := &snap.SideInfo{RealName: "base-snap-a", SnapID: "base-snap-a-id1", Revision: snap.R(1)}
    87  	snaptest.MockSnap(c, snapaBaseYaml, si3)
    88  	snapstate.Set(s.state, "base-snap-a", &snapstate.SnapState{
    89  		Active:   true,
    90  		Sequence: []*snap.SideInfo{si3},
    91  		Current:  snap.R(1),
    92  	})
    93  
    94  	repo := interfaces.NewRepository()
    95  	// no interfaces needed for this test suite
    96  	ifacerepo.Replace(s.state, repo)
    97  }
    98  
    99  func (s *gateAutoRefreshHookSuite) TearDownTest(c *C) {
   100  	s.commonTearDownTest(c)
   101  }
   102  
   103  func mockRefreshCandidate(snapName, instanceKey, channel, version string, revision snap.Revision) interface{} {
   104  	sup := &snapstate.SnapSetup{
   105  		Channel:     channel,
   106  		InstanceKey: instanceKey,
   107  		SideInfo: &snap.SideInfo{
   108  			Revision: revision,
   109  			RealName: snapName,
   110  		},
   111  	}
   112  	return snapstate.MockRefreshCandidate(sup, version)
   113  }
   114  
   115  func (s *gateAutoRefreshHookSuite) settle(c *C) {
   116  	err := s.o.Settle(5 * time.Second)
   117  	c.Assert(err, IsNil)
   118  }
   119  
   120  func checkIsHeld(c *C, st *state.State, heldSnap, gatingSnap string) {
   121  	var held map[string]map[string]interface{}
   122  	c.Assert(st.Get("snaps-hold", &held), IsNil)
   123  	c.Check(held[heldSnap][gatingSnap], NotNil)
   124  }
   125  
   126  func checkIsNotHeld(c *C, st *state.State, heldSnap string) {
   127  	var held map[string]map[string]interface{}
   128  	c.Assert(st.Get("snaps-hold", &held), IsNil)
   129  	c.Check(held[heldSnap], IsNil)
   130  }
   131  
   132  func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookProceedRuninhibitLock(c *C) {
   133  	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
   134  		c.Check(ctx.HookName(), Equals, "gate-auto-refresh")
   135  		c.Check(ctx.InstanceName(), Equals, "snap-a")
   136  		ctx.Lock()
   137  		defer ctx.Unlock()
   138  
   139  		// check that runinhibit hint has been set by Before() hook handler.
   140  		hint, err := runinhibit.IsLocked("snap-a")
   141  		c.Assert(err, IsNil)
   142  		c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh)
   143  
   144  		// action is normally set via snapctl; pretend it is --proceed.
   145  		action := snapstate.GateAutoRefreshProceed
   146  		ctx.Cache("action", action)
   147  		return nil, nil
   148  	}
   149  	restore := hookstate.MockRunHook(hookInvoke)
   150  	defer restore()
   151  
   152  	st := s.state
   153  	st.Lock()
   154  	defer st.Unlock()
   155  
   156  	// enable refresh-app-awareness
   157  	tr := config.NewTransaction(st)
   158  	tr.Set("core", "experimental.refresh-app-awareness", true)
   159  	tr.Commit()
   160  
   161  	task := hookstate.SetupGateAutoRefreshHook(st, "snap-a")
   162  	change := st.NewChange("kind", "summary")
   163  	change.AddTask(task)
   164  
   165  	st.Unlock()
   166  	s.settle(c)
   167  	st.Lock()
   168  
   169  	c.Assert(change.Err(), IsNil)
   170  	c.Assert(change.Status(), Equals, state.DoneStatus)
   171  
   172  	hint, err := runinhibit.IsLocked("snap-a")
   173  	c.Assert(err, IsNil)
   174  	c.Check(hint, Equals, runinhibit.HintInhibitedForRefresh)
   175  }
   176  
   177  func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookHoldUnlocksRuninhibit(c *C) {
   178  	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
   179  		c.Check(ctx.HookName(), Equals, "gate-auto-refresh")
   180  		c.Check(ctx.InstanceName(), Equals, "snap-a")
   181  		ctx.Lock()
   182  		defer ctx.Unlock()
   183  
   184  		// check that runinhibit hint has been set by Before() hook handler.
   185  		hint, err := runinhibit.IsLocked("snap-a")
   186  		c.Assert(err, IsNil)
   187  		c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh)
   188  
   189  		// action is normally set via snapctl; pretend it is --hold.
   190  		action := snapstate.GateAutoRefreshHold
   191  		ctx.Cache("action", action)
   192  		return nil, nil
   193  	}
   194  	restore := hookstate.MockRunHook(hookInvoke)
   195  	defer restore()
   196  
   197  	st := s.state
   198  	st.Lock()
   199  	defer st.Unlock()
   200  
   201  	// enable refresh-app-awareness
   202  	tr := config.NewTransaction(st)
   203  	tr.Set("core", "experimental.refresh-app-awareness", true)
   204  	tr.Commit()
   205  
   206  	task := hookstate.SetupGateAutoRefreshHook(st, "snap-a")
   207  	change := st.NewChange("kind", "summary")
   208  	change.AddTask(task)
   209  
   210  	st.Unlock()
   211  	s.settle(c)
   212  	st.Lock()
   213  
   214  	c.Assert(change.Err(), IsNil)
   215  	c.Assert(change.Status(), Equals, state.DoneStatus)
   216  
   217  	// runinhibit lock is released.
   218  	hint, err := runinhibit.IsLocked("snap-a")
   219  	c.Assert(err, IsNil)
   220  	c.Check(hint, Equals, runinhibit.HintNotInhibited)
   221  }
   222  
   223  // Test that if gate-auto-refresh hook does nothing, the hook handler
   224  // assumes --proceed.
   225  func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceedUnlocksRuninhibit(c *C) {
   226  	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
   227  		// sanity, refresh is inhibited for snap-a.
   228  		hint, err := runinhibit.IsLocked("snap-a")
   229  		c.Assert(err, IsNil)
   230  		c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh)
   231  
   232  		// this hook does nothing (action not set to proceed/hold).
   233  		c.Check(ctx.HookName(), Equals, "gate-auto-refresh")
   234  		c.Check(ctx.InstanceName(), Equals, "snap-a")
   235  		return nil, nil
   236  	}
   237  	restore := hookstate.MockRunHook(hookInvoke)
   238  	defer restore()
   239  
   240  	st := s.state
   241  	st.Lock()
   242  	defer st.Unlock()
   243  
   244  	// pretend that snap-a is initially held by itself.
   245  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil)
   246  	// sanity
   247  	checkIsHeld(c, st, "snap-a", "snap-a")
   248  
   249  	// enable refresh-app-awareness
   250  	tr := config.NewTransaction(st)
   251  	tr.Set("core", "experimental.refresh-app-awareness", true)
   252  	tr.Commit()
   253  
   254  	task := hookstate.SetupGateAutoRefreshHook(st, "snap-a")
   255  	change := st.NewChange("kind", "summary")
   256  	change.AddTask(task)
   257  
   258  	st.Unlock()
   259  	s.settle(c)
   260  	st.Lock()
   261  
   262  	c.Assert(change.Err(), IsNil)
   263  	c.Assert(change.Status(), Equals, state.DoneStatus)
   264  
   265  	checkIsNotHeld(c, st, "snap-a")
   266  
   267  	// runinhibit lock is released.
   268  	hint, err := runinhibit.IsLocked("snap-a")
   269  	c.Assert(err, IsNil)
   270  	c.Check(hint, Equals, runinhibit.HintNotInhibited)
   271  }
   272  
   273  // Test that if gate-auto-refresh hook does nothing, the hook handler
   274  // assumes --proceed.
   275  func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceed(c *C) {
   276  	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
   277  		// no runinhibit because the refresh-app-awareness feature is disabled.
   278  		hint, err := runinhibit.IsLocked("snap-a")
   279  		c.Assert(err, IsNil)
   280  		c.Check(hint, Equals, runinhibit.HintNotInhibited)
   281  
   282  		// this hook does nothing (action not set to proceed/hold).
   283  		c.Check(ctx.HookName(), Equals, "gate-auto-refresh")
   284  		c.Check(ctx.InstanceName(), Equals, "snap-a")
   285  		return nil, nil
   286  	}
   287  	restore := hookstate.MockRunHook(hookInvoke)
   288  	defer restore()
   289  
   290  	st := s.state
   291  	st.Lock()
   292  	defer st.Unlock()
   293  
   294  	// pretend that snap-b is initially held by snap-a.
   295  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b"), IsNil)
   296  	// sanity
   297  	checkIsHeld(c, st, "snap-b", "snap-a")
   298  
   299  	task := hookstate.SetupGateAutoRefreshHook(st, "snap-a")
   300  	change := st.NewChange("kind", "summary")
   301  	change.AddTask(task)
   302  
   303  	st.Unlock()
   304  	s.settle(c)
   305  	st.Lock()
   306  
   307  	c.Assert(change.Err(), IsNil)
   308  	c.Assert(change.Status(), Equals, state.DoneStatus)
   309  
   310  	checkIsNotHeld(c, st, "snap-b")
   311  
   312  	// no runinhibit because the refresh-app-awareness feature is disabled.
   313  	hint, err := runinhibit.IsLocked("snap-a")
   314  	c.Assert(err, IsNil)
   315  	c.Check(hint, Equals, runinhibit.HintNotInhibited)
   316  }
   317  
   318  // Test that if gate-auto-refresh hook errors out, the hook handler
   319  // assumes --hold.
   320  func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookError(c *C) {
   321  	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
   322  		// no runinhibit because the refresh-app-awareness feature is disabled.
   323  		hint, err := runinhibit.IsLocked("snap-a")
   324  		c.Assert(err, IsNil)
   325  		c.Check(hint, Equals, runinhibit.HintNotInhibited)
   326  
   327  		// this hook does nothing (action not set to proceed/hold).
   328  		c.Check(ctx.HookName(), Equals, "gate-auto-refresh")
   329  		c.Check(ctx.InstanceName(), Equals, "snap-a")
   330  		return []byte("fail"), fmt.Errorf("boom")
   331  	}
   332  	restore := hookstate.MockRunHook(hookInvoke)
   333  	defer restore()
   334  
   335  	st := s.state
   336  	st.Lock()
   337  	defer st.Unlock()
   338  
   339  	candidates := map[string]interface{}{"snap-a": mockRefreshCandidate("snap-a", "", "edge", "v1", snap.Revision{N: 3})}
   340  	st.Set("refresh-candidates", candidates)
   341  
   342  	task := hookstate.SetupGateAutoRefreshHook(st, "snap-a")
   343  	change := st.NewChange("kind", "summary")
   344  	change.AddTask(task)
   345  
   346  	st.Unlock()
   347  	s.settle(c)
   348  	st.Lock()
   349  
   350  	c.Assert(strings.Join(task.Log(), ""), testutil.Contains, "ignoring hook error: fail")
   351  	c.Assert(change.Status(), Equals, state.DoneStatus)
   352  
   353  	// and snap-a is now held.
   354  	checkIsHeld(c, st, "snap-a", "snap-a")
   355  
   356  	// no runinhibit because the refresh-app-awareness feature is disabled.
   357  	hint, err := runinhibit.IsLocked("snap-a")
   358  	c.Assert(err, IsNil)
   359  	c.Check(hint, Equals, runinhibit.HintNotInhibited)
   360  }
   361  
   362  // Test that if gate-auto-refresh hook errors out, the hook handler
   363  // assumes --hold even if --proceed was requested.
   364  func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorAfterProceed(c *C) {
   365  	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
   366  		// no runinhibit because the refresh-app-awareness feature is disabled.
   367  		hint, err := runinhibit.IsLocked("snap-a")
   368  		c.Assert(err, IsNil)
   369  		c.Check(hint, Equals, runinhibit.HintNotInhibited)
   370  
   371  		c.Check(ctx.HookName(), Equals, "gate-auto-refresh")
   372  		c.Check(ctx.InstanceName(), Equals, "snap-a")
   373  
   374  		// action is normally set via snapctl; pretend it is --proceed.
   375  		ctx.Lock()
   376  		defer ctx.Unlock()
   377  		action := snapstate.GateAutoRefreshProceed
   378  		ctx.Cache("action", action)
   379  
   380  		return []byte("fail"), fmt.Errorf("boom")
   381  	}
   382  	restore := hookstate.MockRunHook(hookInvoke)
   383  	defer restore()
   384  
   385  	st := s.state
   386  	st.Lock()
   387  	defer st.Unlock()
   388  
   389  	candidates := map[string]interface{}{"snap-a": mockRefreshCandidate("snap-a", "", "edge", "v1", snap.Revision{N: 3})}
   390  	st.Set("refresh-candidates", candidates)
   391  
   392  	task := hookstate.SetupGateAutoRefreshHook(st, "snap-a")
   393  	change := st.NewChange("kind", "summary")
   394  	change.AddTask(task)
   395  
   396  	st.Unlock()
   397  	s.settle(c)
   398  	st.Lock()
   399  
   400  	c.Assert(strings.Join(task.Log(), ""), testutil.Contains, "ignoring hook error: fail")
   401  	c.Assert(change.Status(), Equals, state.DoneStatus)
   402  
   403  	// and snap-a is now held.
   404  	checkIsHeld(c, st, "snap-a", "snap-a")
   405  
   406  	// no runinhibit because the refresh-app-awareness feature is disabled.
   407  	hint, err := runinhibit.IsLocked("snap-a")
   408  	c.Assert(err, IsNil)
   409  	c.Check(hint, Equals, runinhibit.HintNotInhibited)
   410  }
   411  
   412  // Test that if gate-auto-refresh hook errors out, the hook handler
   413  // assumes --hold.
   414  func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorRuninhibitUnlock(c *C) {
   415  	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
   416  		// no runinhibit because the refresh-app-awareness feature is disabled.
   417  		hint, err := runinhibit.IsLocked("snap-a")
   418  		c.Assert(err, IsNil)
   419  		c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh)
   420  
   421  		// this hook does nothing (action not set to proceed/hold).
   422  		c.Check(ctx.HookName(), Equals, "gate-auto-refresh")
   423  		c.Check(ctx.InstanceName(), Equals, "snap-a")
   424  		return []byte("fail"), fmt.Errorf("boom")
   425  	}
   426  	restore := hookstate.MockRunHook(hookInvoke)
   427  	defer restore()
   428  
   429  	st := s.state
   430  	st.Lock()
   431  	defer st.Unlock()
   432  
   433  	// enable refresh-app-awareness
   434  	tr := config.NewTransaction(st)
   435  	tr.Set("core", "experimental.refresh-app-awareness", true)
   436  	tr.Commit()
   437  
   438  	candidates := map[string]interface{}{"snap-a": mockRefreshCandidate("snap-a", "", "edge", "v1", snap.Revision{N: 3})}
   439  	st.Set("refresh-candidates", candidates)
   440  
   441  	task := hookstate.SetupGateAutoRefreshHook(st, "snap-a")
   442  	change := st.NewChange("kind", "summary")
   443  	change.AddTask(task)
   444  
   445  	st.Unlock()
   446  	s.settle(c)
   447  	st.Lock()
   448  
   449  	c.Assert(strings.Join(task.Log(), ""), testutil.Contains, "ignoring hook error: fail")
   450  	c.Assert(change.Status(), Equals, state.DoneStatus)
   451  
   452  	// and snap-a is now held.
   453  	checkIsHeld(c, st, "snap-a", "snap-a")
   454  
   455  	// inhibit lock is unlocked
   456  	hint, err := runinhibit.IsLocked("snap-a")
   457  	c.Assert(err, IsNil)
   458  	c.Check(hint, Equals, runinhibit.HintNotInhibited)
   459  }
   460  
   461  func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorHoldErrorLogged(c *C) {
   462  	hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
   463  		// no runinhibit because the refresh-app-awareness feature is disabled.
   464  		hint, err := runinhibit.IsLocked("snap-a")
   465  		c.Assert(err, IsNil)
   466  		c.Check(hint, Equals, runinhibit.HintNotInhibited)
   467  
   468  		// this hook does nothing (action not set to proceed/hold).
   469  		c.Check(ctx.HookName(), Equals, "gate-auto-refresh")
   470  		c.Check(ctx.InstanceName(), Equals, "snap-a")
   471  
   472  		// simulate failing hook
   473  		return []byte("fail"), fmt.Errorf("boom")
   474  	}
   475  	restore := hookstate.MockRunHook(hookInvoke)
   476  	defer restore()
   477  
   478  	st := s.state
   479  	st.Lock()
   480  	defer st.Unlock()
   481  
   482  	candidates := map[string]interface{}{"snap-a": mockRefreshCandidate("snap-a", "", "edge", "v1", snap.Revision{N: 3})}
   483  	st.Set("refresh-candidates", candidates)
   484  
   485  	task := hookstate.SetupGateAutoRefreshHook(st, "snap-a")
   486  	change := st.NewChange("kind", "summary")
   487  	change.AddTask(task)
   488  
   489  	// pretend snap-a wasn't updated for a very long time.
   490  	var snapst snapstate.SnapState
   491  	c.Assert(snapstate.Get(st, "snap-a", &snapst), IsNil)
   492  	t := time.Now().Add(-365 * 24 * time.Hour)
   493  	snapst.LastRefreshTime = &t
   494  	snapstate.Set(st, "snap-a", &snapst)
   495  
   496  	st.Unlock()
   497  	s.settle(c)
   498  	st.Lock()
   499  
   500  	c.Assert(strings.Join(task.Log(), ""), Matches, `.*error: cannot hold some snaps:
   501   - snap "snap-a" cannot hold snap "snap-a" anymore, maximum refresh postponement exceeded \(while handling previous hook error: fail\)`)
   502  	c.Assert(change.Status(), Equals, state.DoneStatus)
   503  
   504  	// and snap-b is not held (due to hold error).
   505  	var held map[string]map[string]interface{}
   506  	c.Assert(st.Get("snaps-hold", &held), IsNil)
   507  	c.Check(held, HasLen, 0)
   508  
   509  	// no runinhibit because the refresh-app-awareness feature is disabled.
   510  	hint, err := runinhibit.IsLocked("snap-a")
   511  	c.Assert(err, IsNil)
   512  	c.Check(hint, Equals, runinhibit.HintNotInhibited)
   513  }