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