github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/snapstate/autorefresh_gating_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 snapstate_test
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"time"
    29  
    30  	. "gopkg.in/check.v1"
    31  	"gopkg.in/tomb.v2"
    32  
    33  	"github.com/snapcore/snapd/dirs"
    34  	"github.com/snapcore/snapd/interfaces"
    35  	"github.com/snapcore/snapd/interfaces/builtin"
    36  	"github.com/snapcore/snapd/logger"
    37  	"github.com/snapcore/snapd/osutil"
    38  	"github.com/snapcore/snapd/overlord/auth"
    39  	"github.com/snapcore/snapd/overlord/configstate/config"
    40  	"github.com/snapcore/snapd/overlord/hookstate"
    41  	"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
    42  	"github.com/snapcore/snapd/overlord/snapstate"
    43  	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
    44  	"github.com/snapcore/snapd/overlord/state"
    45  	"github.com/snapcore/snapd/release"
    46  	"github.com/snapcore/snapd/snap"
    47  	"github.com/snapcore/snapd/snap/snaptest"
    48  	"github.com/snapcore/snapd/store"
    49  	"github.com/snapcore/snapd/testutil"
    50  )
    51  
    52  type autoRefreshGatingStore struct {
    53  	*fakeStore
    54  	refreshedSnaps []*snap.Info
    55  }
    56  
    57  type autorefreshGatingSuite struct {
    58  	testutil.BaseTest
    59  	state *state.State
    60  	repo  *interfaces.Repository
    61  	store *autoRefreshGatingStore
    62  }
    63  
    64  var _ = Suite(&autorefreshGatingSuite{})
    65  
    66  func (s *autorefreshGatingSuite) SetUpTest(c *C) {
    67  	s.BaseTest.SetUpTest(c)
    68  	dirs.SetRootDir(c.MkDir())
    69  	s.AddCleanup(func() {
    70  		dirs.SetRootDir("/")
    71  	})
    72  	s.state = state.New(nil)
    73  
    74  	s.repo = interfaces.NewRepository()
    75  	for _, iface := range builtin.Interfaces() {
    76  		c.Assert(s.repo.AddInterface(iface), IsNil)
    77  	}
    78  
    79  	s.state.Lock()
    80  	defer s.state.Unlock()
    81  	ifacerepo.Replace(s.state, s.repo)
    82  
    83  	s.store = &autoRefreshGatingStore{fakeStore: &fakeStore{}}
    84  	snapstate.ReplaceStore(s.state, s.store)
    85  	s.state.Set("refresh-privacy-key", "privacy-key")
    86  }
    87  
    88  func (r *autoRefreshGatingStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
    89  	if assertQuery != nil {
    90  		panic("no assertion query support")
    91  	}
    92  	if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 {
    93  		panic("expected in test one action for each current snaps, and at least one snap")
    94  	}
    95  	for _, a := range actions {
    96  		if a.Action != "refresh" {
    97  			panic("expected refresh actions")
    98  		}
    99  	}
   100  
   101  	res := []store.SnapActionResult{}
   102  	for _, rs := range r.refreshedSnaps {
   103  		res = append(res, store.SnapActionResult{Info: rs})
   104  	}
   105  
   106  	return res, nil, nil
   107  }
   108  
   109  func mockInstalledSnap(c *C, st *state.State, snapYaml string, hasHook bool) *snap.Info {
   110  	snapInfo := snaptest.MockSnap(c, string(snapYaml), &snap.SideInfo{
   111  		Revision: snap.R(1),
   112  	})
   113  
   114  	snapName := snapInfo.SnapName()
   115  	si := &snap.SideInfo{RealName: snapName, SnapID: "id", Revision: snap.R(1)}
   116  	snapstate.Set(st, snapName, &snapstate.SnapState{
   117  		Active:   true,
   118  		Sequence: []*snap.SideInfo{si},
   119  		Current:  si.Revision,
   120  		SnapType: string(snapInfo.Type()),
   121  	})
   122  
   123  	if hasHook {
   124  		c.Assert(os.MkdirAll(snapInfo.HooksDir(), 0775), IsNil)
   125  		err := ioutil.WriteFile(filepath.Join(snapInfo.HooksDir(), "gate-auto-refresh"), nil, 0755)
   126  		c.Assert(err, IsNil)
   127  	}
   128  	return snapInfo
   129  }
   130  
   131  func mockLastRefreshed(c *C, st *state.State, refreshedTime string, snaps ...string) {
   132  	refreshed, err := time.Parse(time.RFC3339, refreshedTime)
   133  	c.Assert(err, IsNil)
   134  	for _, snapName := range snaps {
   135  		var snapst snapstate.SnapState
   136  		c.Assert(snapstate.Get(st, snapName, &snapst), IsNil)
   137  		snapst.LastRefreshTime = &refreshed
   138  		snapstate.Set(st, snapName, &snapst)
   139  	}
   140  }
   141  
   142  const baseSnapAyaml = `name: base-snap-a
   143  type: base
   144  `
   145  
   146  const snapAyaml = `name: snap-a
   147  type: app
   148  base: base-snap-a
   149  `
   150  
   151  const baseSnapByaml = `name: base-snap-b
   152  type: base
   153  `
   154  
   155  const snapByaml = `name: snap-b
   156  type: app
   157  base: base-snap-b
   158  version: 1
   159  `
   160  
   161  const kernelYaml = `name: kernel
   162  type: kernel
   163  version: 1
   164  `
   165  
   166  const gadget1Yaml = `name: gadget
   167  type: gadget
   168  version: 1
   169  `
   170  
   171  const snapCyaml = `name: snap-c
   172  type: app
   173  version: 1
   174  `
   175  
   176  const snapDyaml = `name: snap-d
   177  type: app
   178  version: 1
   179  slots:
   180      slot: desktop
   181  `
   182  
   183  const snapEyaml = `name: snap-e
   184  type: app
   185  version: 1
   186  base: other-base
   187  plugs:
   188      plug: desktop
   189  `
   190  
   191  const snapFyaml = `name: snap-f
   192  type: app
   193  version: 1
   194  plugs:
   195      plug: desktop
   196  `
   197  
   198  const snapGyaml = `name: snap-g
   199  type: app
   200  version: 1
   201  base: other-base
   202  plugs:
   203      desktop:
   204      mir:
   205  `
   206  
   207  const coreYaml = `name: core
   208  type: os
   209  version: 1
   210  slots:
   211      desktop:
   212      mir:
   213  `
   214  
   215  const core18Yaml = `name: core18
   216  type: os
   217  version: 1
   218  `
   219  
   220  const snapdYaml = `name: snapd
   221  version: 1
   222  type: snapd
   223  slots:
   224      desktop:
   225  `
   226  
   227  func (s *autorefreshGatingSuite) TestHoldDurationLeft(c *C) {
   228  	now, err := time.Parse(time.RFC3339, "2021-06-03T10:00:00Z")
   229  	c.Assert(err, IsNil)
   230  	maxPostponement := time.Hour * 24 * 90
   231  
   232  	for i, tc := range []struct {
   233  		lastRefresh, firstHeld string
   234  		maxDuration            string
   235  		expected               string
   236  	}{
   237  		{
   238  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   239  			"2021-06-03T10:00:00Z", // first held now
   240  			"48h", // max duration
   241  			"48h", // expected
   242  		},
   243  		{
   244  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   245  			"2021-06-02T10:00:00Z", // first held (1 day ago)
   246  			"48h", // max duration
   247  			"24h", // expected
   248  		},
   249  		{
   250  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   251  			"2021-06-01T10:00:00Z", // first held (2 days ago)
   252  			"48h", // max duration
   253  			"00h", // expected
   254  		},
   255  		{
   256  			"2021-03-08T10:00:00Z", // last refreshed (almost 3 months ago)
   257  			"2021-06-01T10:00:00Z", // first held
   258  			"2160h",                // max duration (90 days)
   259  			"72h",                  // expected
   260  		},
   261  		{
   262  			"2021-03-04T10:00:00Z", // last refreshed
   263  			"2021-06-01T10:00:00Z", // first held (2 days ago)
   264  			"2160h",                // max duration (90 days)
   265  			"-24h",                 // expected (refresh is 1 day overdue)
   266  		},
   267  		{
   268  			"2021-06-01T10:00:00Z", // last refreshed (2 days ago)
   269  			"2021-06-03T10:00:00Z", // first held now
   270  			"2160h",                // max duration (90 days)
   271  			"2112h",                // expected (max minus 2 days)
   272  		},
   273  	} {
   274  		lastRefresh, err := time.Parse(time.RFC3339, tc.lastRefresh)
   275  		c.Assert(err, IsNil)
   276  		firstHeld, err := time.Parse(time.RFC3339, tc.firstHeld)
   277  		c.Assert(err, IsNil)
   278  		maxDuration, err := time.ParseDuration(tc.maxDuration)
   279  		c.Assert(err, IsNil)
   280  		expected, err := time.ParseDuration(tc.expected)
   281  		c.Assert(err, IsNil)
   282  
   283  		left := snapstate.HoldDurationLeft(now, lastRefresh, firstHeld, maxDuration, maxPostponement)
   284  		c.Check(left, Equals, expected, Commentf("case #%d", i))
   285  	}
   286  }
   287  
   288  func (s *autorefreshGatingSuite) TestLastRefreshedHelper(c *C) {
   289  	st := s.state
   290  	st.Lock()
   291  	defer st.Unlock()
   292  
   293  	inf := mockInstalledSnap(c, st, snapAyaml, false)
   294  	stat, err := os.Stat(inf.MountFile())
   295  	c.Assert(err, IsNil)
   296  
   297  	refreshed, err := snapstate.LastRefreshed(st, "snap-a")
   298  	c.Assert(err, IsNil)
   299  	c.Check(refreshed, DeepEquals, stat.ModTime())
   300  
   301  	t, err := time.Parse(time.RFC3339, "2021-01-01T10:00:00Z")
   302  	c.Assert(err, IsNil)
   303  
   304  	var snapst snapstate.SnapState
   305  	c.Assert(snapstate.Get(st, "snap-a", &snapst), IsNil)
   306  	snapst.LastRefreshTime = &t
   307  	snapstate.Set(st, "snap-a", &snapst)
   308  
   309  	refreshed, err = snapstate.LastRefreshed(st, "snap-a")
   310  	c.Assert(err, IsNil)
   311  	c.Check(refreshed, DeepEquals, t)
   312  }
   313  
   314  func (s *autorefreshGatingSuite) TestHoldRefreshHelper(c *C) {
   315  	st := s.state
   316  	st.Lock()
   317  	defer st.Unlock()
   318  
   319  	restore := snapstate.MockTimeNow(func() time.Time {
   320  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   321  		c.Assert(err, IsNil)
   322  		return t
   323  	})
   324  	defer restore()
   325  
   326  	mockInstalledSnap(c, st, snapAyaml, false)
   327  	mockInstalledSnap(c, st, snapByaml, false)
   328  	mockInstalledSnap(c, st, snapCyaml, false)
   329  	mockInstalledSnap(c, st, snapDyaml, false)
   330  	mockInstalledSnap(c, st, snapEyaml, false)
   331  	mockInstalledSnap(c, st, snapFyaml, false)
   332  
   333  	mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b", "snap-c", "snap-d", "snap-e", "snap-f")
   334  
   335  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   336  	// this could be merged with the above HoldRefresh call, but it's fine if
   337  	// done separately too.
   338  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-e"), IsNil)
   339  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-e"), IsNil)
   340  	c.Assert(snapstate.HoldRefresh(st, "snap-f", 0, "snap-f"), IsNil)
   341  
   342  	var gating map[string]map[string]*snapstate.HoldState
   343  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   344  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   345  		"snap-b": {
   346  			// holding of other snaps for maxOtherHoldDuration (48h)
   347  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   348  		},
   349  		"snap-c": {
   350  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   351  		},
   352  		"snap-e": {
   353  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   354  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   355  		},
   356  		"snap-f": {
   357  			// holding self set for maxPostponement minus 1 day due to last refresh.
   358  			"snap-f": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-07T10:00:00Z"),
   359  		},
   360  	})
   361  }
   362  
   363  func (s *autorefreshGatingSuite) TestHoldRefreshHelperMultipleTimes(c *C) {
   364  	st := s.state
   365  	st.Lock()
   366  	defer st.Unlock()
   367  
   368  	lastRefreshed := "2021-05-09T10:00:00Z"
   369  	now := "2021-05-10T10:00:00Z"
   370  	restore := snapstate.MockTimeNow(func() time.Time {
   371  		t, err := time.Parse(time.RFC3339, now)
   372  		c.Assert(err, IsNil)
   373  		return t
   374  	})
   375  	defer restore()
   376  
   377  	mockInstalledSnap(c, st, snapAyaml, false)
   378  	mockInstalledSnap(c, st, snapByaml, false)
   379  	// snap-a was last refreshed yesterday
   380  	mockLastRefreshed(c, st, lastRefreshed, "snap-a")
   381  
   382  	// hold it for just a bit (10h) initially
   383  	hold := time.Hour * 10
   384  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   385  	var gating map[string]map[string]*snapstate.HoldState
   386  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   387  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   388  		"snap-a": {
   389  			"snap-b": snapstate.MockHoldState(now, "2021-05-10T20:00:00Z"),
   390  		},
   391  	})
   392  
   393  	// holding for a shorter time is fine too
   394  	hold = time.Hour * 5
   395  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   396  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   397  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   398  		"snap-a": {
   399  			"snap-b": snapstate.MockHoldState(now, "2021-05-10T15:00:00Z"),
   400  		},
   401  	})
   402  
   403  	oldNow := now
   404  
   405  	// a refresh on next day
   406  	now = "2021-05-11T08:00:00Z"
   407  
   408  	// default hold time requested
   409  	hold = 0
   410  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   411  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   412  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   413  		"snap-a": {
   414  			// maximum for holding other snaps, but taking into consideration
   415  			// firstHeld time = "2021-05-10T10:00:00".
   416  			"snap-b": snapstate.MockHoldState(oldNow, "2021-05-12T10:00:00Z"),
   417  		},
   418  	})
   419  }
   420  
   421  func (s *autorefreshGatingSuite) TestHoldRefreshHelperCloseToMaxPostponement(c *C) {
   422  	st := s.state
   423  	st.Lock()
   424  	defer st.Unlock()
   425  
   426  	lastRefreshedStr := "2021-01-01T10:00:00Z"
   427  	lastRefreshed, err := time.Parse(time.RFC3339, lastRefreshedStr)
   428  	c.Assert(err, IsNil)
   429  	// we are 1 day before maxPostponent
   430  	now := lastRefreshed.Add(89 * time.Hour * 24)
   431  
   432  	restore := snapstate.MockTimeNow(func() time.Time { return now })
   433  	defer restore()
   434  
   435  	mockInstalledSnap(c, st, snapAyaml, false)
   436  	mockInstalledSnap(c, st, snapByaml, false)
   437  	mockLastRefreshed(c, st, lastRefreshedStr, "snap-a")
   438  
   439  	// request default hold time
   440  	var hold time.Duration
   441  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   442  
   443  	var gating map[string]map[string]*snapstate.HoldState
   444  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   445  	c.Assert(gating, HasLen, 1)
   446  	c.Check(gating["snap-a"]["snap-b"].HoldUntil.String(), DeepEquals, lastRefreshed.Add(90*time.Hour*24).String())
   447  }
   448  
   449  func (s *autorefreshGatingSuite) TestHoldRefreshExplicitHoldTime(c *C) {
   450  	st := s.state
   451  	st.Lock()
   452  	defer st.Unlock()
   453  
   454  	now := "2021-05-10T10:00:00Z"
   455  	restore := snapstate.MockTimeNow(func() time.Time {
   456  		t, err := time.Parse(time.RFC3339, now)
   457  		c.Assert(err, IsNil)
   458  		return t
   459  	})
   460  	defer restore()
   461  
   462  	mockInstalledSnap(c, st, snapAyaml, false)
   463  	mockInstalledSnap(c, st, snapByaml, false)
   464  
   465  	hold := time.Hour * 24 * 3
   466  	// holding self for 3 days
   467  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), IsNil)
   468  
   469  	// snap-b holds snap-a for 1 day
   470  	hold = time.Hour * 24
   471  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   472  
   473  	var gating map[string]map[string]*snapstate.HoldState
   474  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   475  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   476  		"snap-a": {
   477  			"snap-a": snapstate.MockHoldState(now, "2021-05-13T10:00:00Z"),
   478  			"snap-b": snapstate.MockHoldState(now, "2021-05-11T10:00:00Z"),
   479  		},
   480  	})
   481  }
   482  
   483  func (s *autorefreshGatingSuite) TestHoldRefreshHelperErrors(c *C) {
   484  	st := s.state
   485  	st.Lock()
   486  	defer st.Unlock()
   487  
   488  	now := "2021-05-10T10:00:00Z"
   489  	restore := snapstate.MockTimeNow(func() time.Time {
   490  		t, err := time.Parse(time.RFC3339, now)
   491  		c.Assert(err, IsNil)
   492  		return t
   493  	})
   494  	defer restore()
   495  
   496  	mockInstalledSnap(c, st, snapAyaml, false)
   497  	mockInstalledSnap(c, st, snapByaml, false)
   498  	// snap-b was refreshed a few days ago
   499  	mockLastRefreshed(c, st, "2021-05-01T10:00:00Z", "snap-b")
   500  
   501  	// holding itself
   502  	hold := time.Hour * 24 * 96
   503  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-a" of 2304h0m0s by snap "snap-a" exceeds maximum holding time`)
   504  
   505  	// holding other snap
   506  	hold = time.Hour * 49
   507  	err := snapstate.HoldRefresh(st, "snap-a", hold, "snap-b")
   508  	c.Check(err, ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`)
   509  	herr, ok := err.(*snapstate.HoldError)
   510  	c.Assert(ok, Equals, true)
   511  	c.Check(herr.SnapsInError, DeepEquals, map[string]snapstate.HoldDurationError{
   512  		"snap-b": {
   513  			Err:          fmt.Errorf(`requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`),
   514  			DurationLeft: 48 * time.Hour,
   515  		},
   516  	})
   517  
   518  	// hold for maximum allowed for other snaps
   519  	hold = time.Hour * 48
   520  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), IsNil)
   521  	// 2 days passed since it was first held
   522  	now = "2021-05-12T10:00:00Z"
   523  	hold = time.Minute * 2
   524  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-a" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`)
   525  
   526  	// refreshed long time ago (> maxPostponement)
   527  	mockLastRefreshed(c, st, "2021-01-01T10:00:00Z", "snap-b")
   528  	hold = time.Hour * 2
   529  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`)
   530  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`)
   531  }
   532  
   533  func (s *autorefreshGatingSuite) TestHoldAndProceedWithRefreshHelper(c *C) {
   534  	st := s.state
   535  	st.Lock()
   536  	defer st.Unlock()
   537  
   538  	mockInstalledSnap(c, st, snapAyaml, false)
   539  	mockInstalledSnap(c, st, snapByaml, false)
   540  	mockInstalledSnap(c, st, snapCyaml, false)
   541  	mockInstalledSnap(c, st, snapDyaml, false)
   542  
   543  	mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-b", "snap-c", "snap-d")
   544  
   545  	restore := snapstate.MockTimeNow(func() time.Time {
   546  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   547  		c.Assert(err, IsNil)
   548  		return t
   549  	})
   550  	defer restore()
   551  
   552  	// nothing is held initially
   553  	held, err := snapstate.HeldSnaps(st)
   554  	c.Assert(err, IsNil)
   555  	c.Check(held, IsNil)
   556  
   557  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   558  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil)
   559  	// holding self
   560  	c.Assert(snapstate.HoldRefresh(st, "snap-d", time.Hour*24*4, "snap-d"), IsNil)
   561  
   562  	held, err = snapstate.HeldSnaps(st)
   563  	c.Assert(err, IsNil)
   564  	c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-c": true, "snap-d": true})
   565  
   566  	c.Assert(snapstate.ProceedWithRefresh(st, "snap-a"), IsNil)
   567  
   568  	held, err = snapstate.HeldSnaps(st)
   569  	c.Assert(err, IsNil)
   570  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-d": true})
   571  
   572  	c.Assert(snapstate.ProceedWithRefresh(st, "snap-d"), IsNil)
   573  	held, err = snapstate.HeldSnaps(st)
   574  	c.Assert(err, IsNil)
   575  	c.Check(held, IsNil)
   576  }
   577  
   578  func (s *autorefreshGatingSuite) TestPruneGatingHelper(c *C) {
   579  	st := s.state
   580  	st.Lock()
   581  	defer st.Unlock()
   582  
   583  	restore := snapstate.MockTimeNow(func() time.Time {
   584  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   585  		c.Assert(err, IsNil)
   586  		return t
   587  	})
   588  	defer restore()
   589  
   590  	mockInstalledSnap(c, st, snapAyaml, false)
   591  	mockInstalledSnap(c, st, snapByaml, false)
   592  	mockInstalledSnap(c, st, snapCyaml, false)
   593  	mockInstalledSnap(c, st, snapDyaml, false)
   594  
   595  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   596  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil)
   597  	// sanity
   598  	held, err := snapstate.HeldSnaps(st)
   599  	c.Assert(err, IsNil)
   600  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-b": true, "snap-d": true})
   601  
   602  	candidates := map[string]*snapstate.RefreshCandidate{"snap-c": {}}
   603  
   604  	// only snap-c has a refresh candidate, snap-b and snap-d should be forgotten.
   605  	c.Assert(snapstate.PruneGating(st, candidates), IsNil)
   606  	var gating map[string]map[string]*snapstate.HoldState
   607  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   608  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   609  		"snap-c": {
   610  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   611  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   612  		},
   613  	})
   614  	held, err = snapstate.HeldSnaps(st)
   615  	c.Assert(err, IsNil)
   616  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true})
   617  }
   618  
   619  func (s *autorefreshGatingSuite) TestPruneGatingHelperNoGating(c *C) {
   620  	st := s.state
   621  	st.Lock()
   622  	defer st.Unlock()
   623  
   624  	restore := snapstate.MockTimeNow(func() time.Time {
   625  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   626  		c.Assert(err, IsNil)
   627  		return t
   628  	})
   629  	defer restore()
   630  
   631  	mockInstalledSnap(c, st, snapAyaml, false)
   632  
   633  	held, err := snapstate.HeldSnaps(st)
   634  	c.Assert(err, IsNil)
   635  	c.Check(held, HasLen, 0)
   636  
   637  	snapstate.MockTimeNow(func() time.Time {
   638  		c.Fatalf("not expected")
   639  		return time.Time{}
   640  	})
   641  
   642  	candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}}
   643  	c.Assert(snapstate.PruneGating(st, candidates), IsNil)
   644  	held, err = snapstate.HeldSnaps(st)
   645  	c.Assert(err, IsNil)
   646  	c.Check(held, HasLen, 0)
   647  }
   648  
   649  func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelper(c *C) {
   650  	st := s.state
   651  	st.Lock()
   652  	defer st.Unlock()
   653  
   654  	restore := snapstate.MockTimeNow(func() time.Time {
   655  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   656  		c.Assert(err, IsNil)
   657  		return t
   658  	})
   659  	defer restore()
   660  
   661  	mockInstalledSnap(c, st, snapAyaml, false)
   662  	mockInstalledSnap(c, st, snapByaml, false)
   663  	mockInstalledSnap(c, st, snapCyaml, false)
   664  	mockInstalledSnap(c, st, snapDyaml, false)
   665  
   666  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   667  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil)
   668  
   669  	c.Assert(snapstate.ResetGatingForRefreshed(st, "snap-b", "snap-c"), IsNil)
   670  	var gating map[string]map[string]*snapstate.HoldState
   671  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   672  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   673  		"snap-d": {
   674  			// holding self set for maxPostponement (95 days - buffer = 90 days)
   675  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-08T10:00:00Z"),
   676  		},
   677  	})
   678  
   679  	held, err := snapstate.HeldSnaps(st)
   680  	c.Assert(err, IsNil)
   681  	c.Check(held, DeepEquals, map[string]bool{"snap-d": true})
   682  }
   683  
   684  func (s *autorefreshGatingSuite) TestPruneSnapsHold(c *C) {
   685  	st := s.state
   686  	st.Lock()
   687  	defer st.Unlock()
   688  
   689  	mockInstalledSnap(c, st, snapAyaml, false)
   690  	mockInstalledSnap(c, st, snapByaml, false)
   691  	mockInstalledSnap(c, st, snapCyaml, false)
   692  	mockInstalledSnap(c, st, snapDyaml, false)
   693  
   694  	// snap-a is holding itself and 3 other snaps
   695  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a", "snap-b", "snap-c", "snap-d"), IsNil)
   696  	// in addition, snap-c is held by snap-d.
   697  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil)
   698  
   699  	// sanity check
   700  	held, err := snapstate.HeldSnaps(st)
   701  	c.Assert(err, IsNil)
   702  	c.Check(held, DeepEquals, map[string]bool{
   703  		"snap-a": true,
   704  		"snap-b": true,
   705  		"snap-c": true,
   706  		"snap-d": true,
   707  	})
   708  
   709  	c.Check(snapstate.PruneSnapsHold(st, "snap-a"), IsNil)
   710  
   711  	// after pruning snap-a, snap-c is still held.
   712  	held, err = snapstate.HeldSnaps(st)
   713  	c.Assert(err, IsNil)
   714  	c.Check(held, DeepEquals, map[string]bool{
   715  		"snap-c": true,
   716  	})
   717  	var gating map[string]map[string]*snapstate.HoldState
   718  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   719  	c.Assert(gating, HasLen, 1)
   720  	c.Check(gating["snap-c"], HasLen, 1)
   721  	c.Check(gating["snap-c"]["snap-d"], NotNil)
   722  }
   723  
   724  const useHook = true
   725  const noHook = false
   726  
   727  func checkGatingTask(c *C, task *state.Task, expected map[string]*snapstate.RefreshCandidate) {
   728  	c.Assert(task.Kind(), Equals, "conditional-auto-refresh")
   729  	var snaps map[string]*snapstate.RefreshCandidate
   730  	c.Assert(task.Get("snaps", &snaps), IsNil)
   731  	c.Check(snaps, DeepEquals, expected)
   732  }
   733  
   734  func (s *autorefreshGatingSuite) TestAffectedByBase(c *C) {
   735  	restore := release.MockOnClassic(true)
   736  	defer restore()
   737  
   738  	st := s.state
   739  
   740  	st.Lock()
   741  	defer st.Unlock()
   742  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
   743  	baseSnapA := mockInstalledSnap(c, s.state, baseSnapAyaml, noHook)
   744  	// unrelated snaps
   745  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   746  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
   747  
   748  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   749  
   750  	updates := []*snap.Info{baseSnapA}
   751  	affected, err := snapstate.AffectedByRefresh(st, updates)
   752  	c.Assert(err, IsNil)
   753  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   754  		"snap-a": {
   755  			Base: true,
   756  			AffectingSnaps: map[string]bool{
   757  				"base-snap-a": true,
   758  			}}})
   759  }
   760  
   761  func (s *autorefreshGatingSuite) TestAffectedByCore(c *C) {
   762  	restore := release.MockOnClassic(true)
   763  	defer restore()
   764  
   765  	st := s.state
   766  
   767  	st.Lock()
   768  	defer st.Unlock()
   769  	snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook)
   770  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   771  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   772  
   773  	c.Assert(s.repo.AddSnap(core), IsNil)
   774  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   775  	c.Assert(s.repo.AddSnap(snapC), IsNil)
   776  
   777  	updates := []*snap.Info{core}
   778  	affected, err := snapstate.AffectedByRefresh(st, updates)
   779  	c.Assert(err, IsNil)
   780  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   781  		"snap-c": {
   782  			Base: true,
   783  			AffectingSnaps: map[string]bool{
   784  				"core": true,
   785  			}}})
   786  }
   787  
   788  func (s *autorefreshGatingSuite) TestAffectedByKernel(c *C) {
   789  	restore := release.MockOnClassic(true)
   790  	defer restore()
   791  
   792  	st := s.state
   793  
   794  	st.Lock()
   795  	defer st.Unlock()
   796  	kernel := mockInstalledSnap(c, s.state, kernelYaml, noHook)
   797  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   798  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   799  
   800  	updates := []*snap.Info{kernel}
   801  	affected, err := snapstate.AffectedByRefresh(st, updates)
   802  	c.Assert(err, IsNil)
   803  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   804  		"snap-c": {
   805  			Restart: true,
   806  			AffectingSnaps: map[string]bool{
   807  				"kernel": true,
   808  			}}})
   809  }
   810  
   811  func (s *autorefreshGatingSuite) TestAffectedByGadget(c *C) {
   812  	restore := release.MockOnClassic(true)
   813  	defer restore()
   814  
   815  	st := s.state
   816  
   817  	st.Lock()
   818  	defer st.Unlock()
   819  	kernel := mockInstalledSnap(c, s.state, gadget1Yaml, noHook)
   820  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   821  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   822  
   823  	updates := []*snap.Info{kernel}
   824  	affected, err := snapstate.AffectedByRefresh(st, updates)
   825  	c.Assert(err, IsNil)
   826  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   827  		"snap-c": {
   828  			Restart: true,
   829  			AffectingSnaps: map[string]bool{
   830  				"gadget": true,
   831  			}}})
   832  }
   833  
   834  func (s *autorefreshGatingSuite) TestAffectedBySlot(c *C) {
   835  	restore := release.MockOnClassic(true)
   836  	defer restore()
   837  
   838  	st := s.state
   839  
   840  	st.Lock()
   841  	defer st.Unlock()
   842  
   843  	snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook)
   844  	snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook)
   845  	// unrelated snap
   846  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   847  
   848  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   849  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   850  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   851  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   852  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   853  	c.Assert(err, IsNil)
   854  
   855  	updates := []*snap.Info{snapD}
   856  	affected, err := snapstate.AffectedByRefresh(st, updates)
   857  	c.Assert(err, IsNil)
   858  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   859  		"snap-e": {
   860  			Restart: true,
   861  			AffectingSnaps: map[string]bool{
   862  				"snap-d": true,
   863  			}}})
   864  }
   865  
   866  func (s *autorefreshGatingSuite) TestNotAffectedByCoreOrSnapdSlot(c *C) {
   867  	restore := release.MockOnClassic(true)
   868  	defer restore()
   869  
   870  	st := s.state
   871  
   872  	st.Lock()
   873  	defer st.Unlock()
   874  
   875  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   876  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   877  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   878  
   879  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   880  	c.Assert(s.repo.AddSnap(core), IsNil)
   881  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   882  
   883  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "mir"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "mir"}}
   884  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   885  	c.Assert(err, IsNil)
   886  
   887  	updates := []*snap.Info{core}
   888  	affected, err := snapstate.AffectedByRefresh(st, updates)
   889  	c.Assert(err, IsNil)
   890  	c.Check(affected, HasLen, 0)
   891  }
   892  
   893  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackend(c *C) {
   894  	restore := release.MockOnClassic(true)
   895  	defer restore()
   896  
   897  	st := s.state
   898  
   899  	st.Lock()
   900  	defer st.Unlock()
   901  
   902  	snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook)
   903  	snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook)
   904  	// unrelated snap
   905  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   906  
   907  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   908  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   909  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   910  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   911  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   912  	c.Assert(err, IsNil)
   913  
   914  	// snapE has a plug using mount backend and is refreshed, this affects slot of snap-d.
   915  	updates := []*snap.Info{snapE}
   916  	affected, err := snapstate.AffectedByRefresh(st, updates)
   917  	c.Assert(err, IsNil)
   918  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   919  		"snap-d": {
   920  			Restart: true,
   921  			AffectingSnaps: map[string]bool{
   922  				"snap-e": true,
   923  			}}})
   924  }
   925  
   926  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendSnapdSlot(c *C) {
   927  	restore := release.MockOnClassic(true)
   928  	defer restore()
   929  
   930  	st := s.state
   931  
   932  	st.Lock()
   933  	defer st.Unlock()
   934  
   935  	snapdSnap := mockInstalledSnap(c, s.state, snapdYaml, useHook)
   936  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   937  	// unrelated snap
   938  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   939  
   940  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   941  	c.Assert(s.repo.AddSnap(snapdSnap), IsNil)
   942  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   943  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "snapd", Name: "desktop"}}
   944  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   945  	c.Assert(err, IsNil)
   946  
   947  	// snapE has a plug using mount backend, refreshing snapd affects snapE.
   948  	updates := []*snap.Info{snapdSnap}
   949  	affected, err := snapstate.AffectedByRefresh(st, updates)
   950  	c.Assert(err, IsNil)
   951  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   952  		"snap-g": {
   953  			Restart: true,
   954  			AffectingSnaps: map[string]bool{
   955  				"snapd": true,
   956  			}}})
   957  }
   958  
   959  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendCoreSlot(c *C) {
   960  	restore := release.MockOnClassic(true)
   961  	defer restore()
   962  
   963  	st := s.state
   964  
   965  	st.Lock()
   966  	defer st.Unlock()
   967  
   968  	coreSnap := mockInstalledSnap(c, s.state, coreYaml, noHook)
   969  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   970  
   971  	c.Assert(s.repo.AddSnap(coreSnap), IsNil)
   972  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   973  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "desktop"}}
   974  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   975  	c.Assert(err, IsNil)
   976  
   977  	// snapG has a plug using mount backend, refreshing core affects snapE.
   978  	updates := []*snap.Info{coreSnap}
   979  	affected, err := snapstate.AffectedByRefresh(st, updates)
   980  	c.Assert(err, IsNil)
   981  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   982  		"snap-g": {
   983  			Restart: true,
   984  			AffectingSnaps: map[string]bool{
   985  				"core": true,
   986  			}}})
   987  }
   988  
   989  func (s *autorefreshGatingSuite) TestAffectedByBootBase(c *C) {
   990  	restore := release.MockOnClassic(false)
   991  	defer restore()
   992  
   993  	st := s.state
   994  
   995  	r := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
   996  	defer r()
   997  
   998  	st.Lock()
   999  	defer st.Unlock()
  1000  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1001  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1002  	mockInstalledSnap(c, s.state, snapDyaml, useHook)
  1003  	mockInstalledSnap(c, s.state, snapEyaml, useHook)
  1004  	core18 := mockInstalledSnap(c, s.state, core18Yaml, noHook)
  1005  
  1006  	updates := []*snap.Info{core18}
  1007  	affected, err := snapstate.AffectedByRefresh(st, updates)
  1008  	c.Assert(err, IsNil)
  1009  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
  1010  		"snap-a": {
  1011  			Base:    false,
  1012  			Restart: true,
  1013  			AffectingSnaps: map[string]bool{
  1014  				"core18": true,
  1015  			},
  1016  		},
  1017  		"snap-b": {
  1018  			Base:    false,
  1019  			Restart: true,
  1020  			AffectingSnaps: map[string]bool{
  1021  				"core18": true,
  1022  			},
  1023  		},
  1024  		"snap-d": {
  1025  			Base:    false,
  1026  			Restart: true,
  1027  			AffectingSnaps: map[string]bool{
  1028  				"core18": true,
  1029  			},
  1030  		},
  1031  		"snap-e": {
  1032  			Base:    false,
  1033  			Restart: true,
  1034  			AffectingSnaps: map[string]bool{
  1035  				"core18": true,
  1036  			}}})
  1037  }
  1038  
  1039  func (s *autorefreshGatingSuite) TestCreateAutoRefreshGateHooks(c *C) {
  1040  	st := s.state
  1041  	st.Lock()
  1042  	defer st.Unlock()
  1043  
  1044  	affected := map[string]*snapstate.AffectedSnapInfo{
  1045  		"snap-a": {
  1046  			Base:    true,
  1047  			Restart: true,
  1048  			AffectingSnaps: map[string]bool{
  1049  				"snap-c": true,
  1050  				"snap-d": true,
  1051  			},
  1052  		},
  1053  		"snap-b": {
  1054  			AffectingSnaps: map[string]bool{
  1055  				"snap-e": true,
  1056  				"snap-f": true,
  1057  			},
  1058  		},
  1059  	}
  1060  
  1061  	seenSnaps := make(map[string]bool)
  1062  
  1063  	ts := snapstate.CreateGateAutoRefreshHooks(st, affected)
  1064  	c.Assert(ts.Tasks(), HasLen, 2)
  1065  
  1066  	checkHook := func(t *state.Task) {
  1067  		c.Assert(t.Kind(), Equals, "run-hook")
  1068  		var hs hookstate.HookSetup
  1069  		c.Assert(t.Get("hook-setup", &hs), IsNil)
  1070  		c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1071  		c.Check(hs.Optional, Equals, true)
  1072  		seenSnaps[hs.Snap] = true
  1073  
  1074  		var data interface{}
  1075  		c.Assert(t.Get("hook-context", &data), IsNil)
  1076  
  1077  		// the order of hook tasks is not deterministic
  1078  		if hs.Snap == "snap-a" {
  1079  			c.Check(data, DeepEquals, map[string]interface{}{
  1080  				"base":            true,
  1081  				"restart":         true,
  1082  				"affecting-snaps": []interface{}{"snap-c", "snap-d"}})
  1083  		} else {
  1084  			c.Assert(hs.Snap, Equals, "snap-b")
  1085  			c.Check(data, DeepEquals, map[string]interface{}{
  1086  				"base":            false,
  1087  				"restart":         false,
  1088  				"affecting-snaps": []interface{}{"snap-e", "snap-f"}})
  1089  		}
  1090  	}
  1091  
  1092  	checkHook(ts.Tasks()[0])
  1093  	checkHook(ts.Tasks()[1])
  1094  
  1095  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
  1096  }
  1097  
  1098  func (s *autorefreshGatingSuite) TestAutorefreshPhase1FeatureFlag(c *C) {
  1099  	st := s.state
  1100  	st.Lock()
  1101  	defer st.Unlock()
  1102  
  1103  	st.Set("seeded", true)
  1104  
  1105  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1106  	defer restore()
  1107  
  1108  	snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
  1109  		return nil, nil
  1110  	}
  1111  	defer func() { snapstate.AutoAliases = nil }()
  1112  
  1113  	s.store.refreshedSnaps = []*snap.Info{{
  1114  		Architectures: []string{"all"},
  1115  		SnapType:      snap.TypeApp,
  1116  		SideInfo: snap.SideInfo{
  1117  			RealName: "snap-a",
  1118  			Revision: snap.R(8),
  1119  		},
  1120  	}}
  1121  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1122  
  1123  	// gate-auto-refresh-hook feature not enabled, expect old-style refresh.
  1124  	_, tss, err := snapstate.AutoRefresh(context.TODO(), st)
  1125  	c.Check(err, IsNil)
  1126  	c.Assert(tss, HasLen, 2)
  1127  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "prerequisites")
  1128  	c.Check(tss[0].Tasks()[1].Kind(), Equals, "download-snap")
  1129  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "check-rerefresh")
  1130  
  1131  	// enable gate-auto-refresh-hook feature
  1132  	tr := config.NewTransaction(s.state)
  1133  	tr.Set("core", "experimental.gate-auto-refresh-hook", true)
  1134  	tr.Commit()
  1135  
  1136  	_, tss, err = snapstate.AutoRefresh(context.TODO(), st)
  1137  	c.Check(err, IsNil)
  1138  	c.Assert(tss, HasLen, 2)
  1139  	task := tss[0].Tasks()[0]
  1140  	c.Check(task.Kind(), Equals, "conditional-auto-refresh")
  1141  	var toUpdate map[string]*snapstate.RefreshCandidate
  1142  	c.Assert(task.Get("snaps", &toUpdate), IsNil)
  1143  	seenSnaps := make(map[string]bool)
  1144  	for up := range toUpdate {
  1145  		seenSnaps[up] = true
  1146  	}
  1147  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true})
  1148  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "run-hook")
  1149  }
  1150  
  1151  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) {
  1152  	s.store.refreshedSnaps = []*snap.Info{{
  1153  		Architectures: []string{"all"},
  1154  		SnapType:      snap.TypeApp,
  1155  		SideInfo: snap.SideInfo{
  1156  			RealName: "snap-a",
  1157  			Revision: snap.R(8),
  1158  		},
  1159  	}, {
  1160  		Architectures: []string{"all"},
  1161  		SnapType:      snap.TypeBase,
  1162  		SideInfo: snap.SideInfo{
  1163  			RealName: "base-snap-b",
  1164  			Revision: snap.R(3),
  1165  		},
  1166  	}, {
  1167  		Architectures: []string{"all"},
  1168  		SnapType:      snap.TypeApp,
  1169  		SideInfo: snap.SideInfo{
  1170  			RealName: "snap-c",
  1171  			Revision: snap.R(5),
  1172  		},
  1173  	}}
  1174  
  1175  	st := s.state
  1176  	st.Lock()
  1177  	defer st.Unlock()
  1178  
  1179  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1180  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1181  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1182  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1183  	mockInstalledSnap(c, s.state, snapDyaml, noHook)
  1184  
  1185  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1186  	defer restore()
  1187  
  1188  	// pretend some snaps are held
  1189  	c.Assert(snapstate.HoldRefresh(st, "gating-snap", 0, "snap-a", "snap-d"), IsNil)
  1190  	// sanity check
  1191  	heldSnaps, err := snapstate.HeldSnaps(st)
  1192  	c.Assert(err, IsNil)
  1193  	c.Check(heldSnaps, DeepEquals, map[string]bool{
  1194  		"snap-a": true,
  1195  		"snap-d": true,
  1196  	})
  1197  
  1198  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1199  	c.Assert(err, IsNil)
  1200  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a", "snap-c"})
  1201  	c.Assert(tss, HasLen, 2)
  1202  
  1203  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1204  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1205  		"snap-a": {
  1206  			SnapSetup: snapstate.SnapSetup{
  1207  				Type:      "app",
  1208  				PlugsOnly: true,
  1209  				Flags: snapstate.Flags{
  1210  					IsAutoRefresh: true,
  1211  				},
  1212  				SideInfo: &snap.SideInfo{
  1213  					RealName: "snap-a",
  1214  					Revision: snap.R(8),
  1215  				},
  1216  				DownloadInfo: &snap.DownloadInfo{},
  1217  			},
  1218  		},
  1219  		"base-snap-b": {
  1220  			SnapSetup: snapstate.SnapSetup{
  1221  				Type:      "base",
  1222  				PlugsOnly: true,
  1223  				Flags: snapstate.Flags{
  1224  					IsAutoRefresh: true,
  1225  				},
  1226  				SideInfo: &snap.SideInfo{
  1227  					RealName: "base-snap-b",
  1228  					Revision: snap.R(3),
  1229  				},
  1230  				DownloadInfo: &snap.DownloadInfo{},
  1231  			},
  1232  		},
  1233  		"snap-c": {
  1234  			SnapSetup: snapstate.SnapSetup{
  1235  				Type:      "app",
  1236  				PlugsOnly: true,
  1237  				Flags: snapstate.Flags{
  1238  					IsAutoRefresh: true,
  1239  				},
  1240  				SideInfo: &snap.SideInfo{
  1241  					RealName: "snap-c",
  1242  					Revision: snap.R(5),
  1243  				},
  1244  				DownloadInfo: &snap.DownloadInfo{},
  1245  			},
  1246  		},
  1247  	})
  1248  
  1249  	c.Assert(tss[1].Tasks(), HasLen, 2)
  1250  
  1251  	var snapAhookData, snapBhookData map[string]interface{}
  1252  
  1253  	// check hooks for affected snaps
  1254  	seenSnaps := make(map[string]bool)
  1255  	var hs hookstate.HookSetup
  1256  	task := tss[1].Tasks()[0]
  1257  	c.Assert(task.Get("hook-setup", &hs), IsNil)
  1258  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1259  	seenSnaps[hs.Snap] = true
  1260  	switch hs.Snap {
  1261  	case "snap-a":
  1262  		task.Get("hook-context", &snapAhookData)
  1263  	case "snap-b":
  1264  		task.Get("hook-context", &snapBhookData)
  1265  	default:
  1266  		c.Fatalf("unexpected snap %q", hs.Snap)
  1267  	}
  1268  
  1269  	task = tss[1].Tasks()[1]
  1270  	c.Assert(task.Get("hook-setup", &hs), IsNil)
  1271  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1272  	seenSnaps[hs.Snap] = true
  1273  	switch hs.Snap {
  1274  	case "snap-a":
  1275  		task.Get("hook-context", &snapAhookData)
  1276  	case "snap-b":
  1277  		task.Get("hook-context", &snapBhookData)
  1278  	default:
  1279  		c.Fatalf("unexpected snap %q", hs.Snap)
  1280  	}
  1281  
  1282  	c.Check(snapAhookData["affecting-snaps"], DeepEquals, []interface{}{"snap-a"})
  1283  	c.Check(snapBhookData["affecting-snaps"], DeepEquals, []interface{}{"base-snap-b"})
  1284  
  1285  	// hook for snap-a because it gets refreshed, for snap-b because its base
  1286  	// gets refreshed. snap-c is refreshed but doesn't have the hook.
  1287  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
  1288  
  1289  	// check that refresh-candidates in the state were updated
  1290  	var candidates map[string]*snapstate.RefreshCandidate
  1291  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1292  	c.Assert(candidates, HasLen, 3)
  1293  	c.Check(candidates["snap-a"], NotNil)
  1294  	c.Check(candidates["base-snap-b"], NotNil)
  1295  	c.Check(candidates["snap-c"], NotNil)
  1296  
  1297  	// check that after autoRefreshPhase1 any held snaps that are not in refresh
  1298  	// candidates got removed.
  1299  	heldSnaps, err = snapstate.HeldSnaps(st)
  1300  	c.Assert(err, IsNil)
  1301  	// snap-d got removed from held snaps.
  1302  	c.Check(heldSnaps, DeepEquals, map[string]bool{
  1303  		"snap-a": true,
  1304  	})
  1305  }
  1306  
  1307  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1ConflictsFilteredOut(c *C) {
  1308  	s.store.refreshedSnaps = []*snap.Info{{
  1309  		Architectures: []string{"all"},
  1310  		SnapType:      snap.TypeApp,
  1311  		SideInfo: snap.SideInfo{
  1312  			RealName: "snap-a",
  1313  			Revision: snap.R(8),
  1314  		},
  1315  	}, {
  1316  		Architectures: []string{"all"},
  1317  		SnapType:      snap.TypeBase,
  1318  		SideInfo: snap.SideInfo{
  1319  			RealName: "snap-c",
  1320  			Revision: snap.R(5),
  1321  		},
  1322  	}}
  1323  
  1324  	st := s.state
  1325  	st.Lock()
  1326  	defer st.Unlock()
  1327  
  1328  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1329  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1330  
  1331  	conflictChange := st.NewChange("conflicting change", "")
  1332  	conflictTask := st.NewTask("conflicting task", "")
  1333  	si := &snap.SideInfo{
  1334  		RealName: "snap-c",
  1335  		Revision: snap.R(1),
  1336  	}
  1337  	sup := snapstate.SnapSetup{SideInfo: si}
  1338  	conflictTask.Set("snap-setup", sup)
  1339  	conflictChange.AddTask(conflictTask)
  1340  
  1341  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1342  	defer restore()
  1343  
  1344  	logbuf, restoreLogger := logger.MockLogger()
  1345  	defer restoreLogger()
  1346  
  1347  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1348  	c.Assert(err, IsNil)
  1349  	c.Check(names, DeepEquals, []string{"snap-a"})
  1350  	c.Assert(tss, HasLen, 2)
  1351  
  1352  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1353  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1354  		"snap-a": {
  1355  			SnapSetup: snapstate.SnapSetup{
  1356  				Type:      "app",
  1357  				PlugsOnly: true,
  1358  				Flags: snapstate.Flags{
  1359  					IsAutoRefresh: true,
  1360  				},
  1361  				SideInfo: &snap.SideInfo{
  1362  					RealName: "snap-a",
  1363  					Revision: snap.R(8),
  1364  				},
  1365  				DownloadInfo: &snap.DownloadInfo{},
  1366  			}}})
  1367  
  1368  	c.Assert(tss[1].Tasks(), HasLen, 1)
  1369  
  1370  	c.Assert(logbuf.String(), testutil.Contains, `cannot refresh snap "snap-c": snap "snap-c" has "conflicting change" change in progress`)
  1371  
  1372  	seenSnaps := make(map[string]bool)
  1373  	var hs hookstate.HookSetup
  1374  	c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil)
  1375  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1376  	seenSnaps[hs.Snap] = true
  1377  
  1378  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true})
  1379  
  1380  	// check that refresh-candidates in the state were updated
  1381  	var candidates map[string]*snapstate.RefreshCandidate
  1382  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1383  	c.Assert(candidates, HasLen, 2)
  1384  	c.Check(candidates["snap-a"], NotNil)
  1385  	c.Check(candidates["snap-c"], NotNil)
  1386  }
  1387  
  1388  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1NoHooks(c *C) {
  1389  	s.store.refreshedSnaps = []*snap.Info{{
  1390  		Architectures: []string{"all"},
  1391  		SnapType:      snap.TypeBase,
  1392  		SideInfo: snap.SideInfo{
  1393  			RealName: "base-snap-b",
  1394  			Revision: snap.R(3),
  1395  		},
  1396  	}, {
  1397  		Architectures: []string{"all"},
  1398  		SnapType:      snap.TypeBase,
  1399  		SideInfo: snap.SideInfo{
  1400  			RealName: "snap-c",
  1401  			Revision: snap.R(5),
  1402  		},
  1403  	}}
  1404  
  1405  	st := s.state
  1406  	st.Lock()
  1407  	defer st.Unlock()
  1408  
  1409  	mockInstalledSnap(c, s.state, snapByaml, noHook)
  1410  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1411  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1412  
  1413  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1414  	defer restore()
  1415  
  1416  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1417  	c.Assert(err, IsNil)
  1418  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-c"})
  1419  	c.Assert(tss, HasLen, 1)
  1420  
  1421  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1422  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh")
  1423  }
  1424  
  1425  func fakeReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) {
  1426  	info := &snap.Info{
  1427  		SuggestedName: name,
  1428  		SideInfo:      *si,
  1429  		Architectures: []string{"all"},
  1430  		SnapType:      snap.TypeApp,
  1431  		Epoch:         snap.Epoch{},
  1432  	}
  1433  	switch name {
  1434  	case "base-snap-b":
  1435  		info.SnapType = snap.TypeBase
  1436  	case "snap-a", "snap-b":
  1437  		info.Hooks = map[string]*snap.HookInfo{
  1438  			"gate-auto-refresh": {
  1439  				Name: "gate-auto-refresh",
  1440  				Snap: info,
  1441  			},
  1442  		}
  1443  		if name == "snap-b" {
  1444  			info.Base = "base-snap-b"
  1445  		}
  1446  	}
  1447  	return info, nil
  1448  }
  1449  
  1450  func (s *snapmgrTestSuite) testAutoRefreshPhase2(c *C, beforePhase1 func(), gateAutoRefreshHook func(snapName string), expected []string) *state.Change {
  1451  	st := s.state
  1452  	st.Lock()
  1453  	defer st.Unlock()
  1454  
  1455  	s.o.TaskRunner().AddHandler("run-hook", func(t *state.Task, tomb *tomb.Tomb) error {
  1456  		var hsup hookstate.HookSetup
  1457  		t.State().Lock()
  1458  		defer t.State().Unlock()
  1459  		c.Assert(t.Get("hook-setup", &hsup), IsNil)
  1460  		if hsup.Hook == "gate-auto-refresh" && gateAutoRefreshHook != nil {
  1461  			gateAutoRefreshHook(hsup.Snap)
  1462  		}
  1463  		return nil
  1464  	}, nil)
  1465  
  1466  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1467  		c.Fatal("unexpected call to installSize")
  1468  		return 0, nil
  1469  	})
  1470  	defer restoreInstallSize()
  1471  
  1472  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1473  		fakeStore: s.fakeStore,
  1474  		refreshedSnaps: []*snap.Info{{
  1475  			Architectures: []string{"all"},
  1476  			SnapType:      snap.TypeApp,
  1477  			SideInfo: snap.SideInfo{
  1478  				RealName: "snap-a",
  1479  				Revision: snap.R(8),
  1480  			},
  1481  		}, {
  1482  			Architectures: []string{"all"},
  1483  			SnapType:      snap.TypeBase,
  1484  			SideInfo: snap.SideInfo{
  1485  				RealName: "base-snap-b",
  1486  				Revision: snap.R(3),
  1487  			},
  1488  		}}})
  1489  
  1490  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1491  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1492  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1493  
  1494  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1495  
  1496  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1497  	defer restore()
  1498  
  1499  	if beforePhase1 != nil {
  1500  		beforePhase1()
  1501  	}
  1502  
  1503  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1504  	c.Assert(err, IsNil)
  1505  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1506  
  1507  	chg := s.state.NewChange("refresh", "...")
  1508  	for _, ts := range tss {
  1509  		chg.AddAll(ts)
  1510  	}
  1511  
  1512  	s.state.Unlock()
  1513  	defer s.se.Stop()
  1514  	s.settle(c)
  1515  	s.state.Lock()
  1516  
  1517  	c.Check(chg.Status(), Equals, state.DoneStatus)
  1518  	c.Check(chg.Err(), IsNil)
  1519  
  1520  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1521  
  1522  	return chg
  1523  }
  1524  
  1525  func (s *snapmgrTestSuite) TestAutoRefreshPhase2(c *C) {
  1526  	expected := []string{
  1527  		"conditional-auto-refresh",
  1528  		"run-hook [snap-a;gate-auto-refresh]",
  1529  		// snap-b hook is triggered because of base-snap-b refresh
  1530  		"run-hook [snap-b;gate-auto-refresh]",
  1531  		"prerequisites",
  1532  		"download-snap",
  1533  		"validate-snap",
  1534  		"mount-snap",
  1535  		"run-hook [base-snap-b;pre-refresh]",
  1536  		"stop-snap-services",
  1537  		"remove-aliases",
  1538  		"unlink-current-snap",
  1539  		"copy-snap-data",
  1540  		"setup-profiles",
  1541  		"link-snap",
  1542  		"auto-connect",
  1543  		"set-auto-aliases",
  1544  		"setup-aliases",
  1545  		"run-hook [base-snap-b;post-refresh]",
  1546  		"start-snap-services",
  1547  		"cleanup",
  1548  		"run-hook [base-snap-b;check-health]",
  1549  		"prerequisites",
  1550  		"download-snap",
  1551  		"validate-snap",
  1552  		"mount-snap",
  1553  		"run-hook [snap-a;pre-refresh]",
  1554  		"stop-snap-services",
  1555  		"remove-aliases",
  1556  		"unlink-current-snap",
  1557  		"copy-snap-data",
  1558  		"setup-profiles",
  1559  		"link-snap",
  1560  		"auto-connect",
  1561  		"set-auto-aliases",
  1562  		"setup-aliases",
  1563  		"run-hook [snap-a;post-refresh]",
  1564  		"start-snap-services",
  1565  		"cleanup",
  1566  		"run-hook [snap-a;configure]",
  1567  		"run-hook [snap-a;check-health]",
  1568  		"check-rerefresh",
  1569  	}
  1570  
  1571  	seenSnapsWithGateAutoRefreshHook := make(map[string]bool)
  1572  
  1573  	chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1574  		seenSnapsWithGateAutoRefreshHook[snapName] = true
  1575  	}, expected)
  1576  
  1577  	c.Check(seenSnapsWithGateAutoRefreshHook, DeepEquals, map[string]bool{
  1578  		"snap-a": true,
  1579  		"snap-b": true,
  1580  	})
  1581  
  1582  	s.state.Lock()
  1583  	defer s.state.Unlock()
  1584  
  1585  	tasks := chg.Tasks()
  1586  	c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b", "snap-a" as needed`)
  1587  
  1588  	// all snaps refreshed, all removed from refresh-candidates.
  1589  	var candidates map[string]*snapstate.RefreshCandidate
  1590  	c.Assert(s.state.Get("refresh-candidates", &candidates), IsNil)
  1591  	c.Assert(candidates, HasLen, 0)
  1592  }
  1593  
  1594  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Held(c *C) {
  1595  	logbuf, restoreLogger := logger.MockLogger()
  1596  	defer restoreLogger()
  1597  
  1598  	expected := []string{
  1599  		"conditional-auto-refresh",
  1600  		"run-hook [snap-a;gate-auto-refresh]",
  1601  		// snap-b hook is triggered because of base-snap-b refresh
  1602  		"run-hook [snap-b;gate-auto-refresh]",
  1603  		"prerequisites",
  1604  		"download-snap",
  1605  		"validate-snap",
  1606  		"mount-snap",
  1607  		"run-hook [snap-a;pre-refresh]",
  1608  		"stop-snap-services",
  1609  		"remove-aliases",
  1610  		"unlink-current-snap",
  1611  		"copy-snap-data",
  1612  		"setup-profiles",
  1613  		"link-snap",
  1614  		"auto-connect",
  1615  		"set-auto-aliases",
  1616  		"setup-aliases",
  1617  		"run-hook [snap-a;post-refresh]",
  1618  		"start-snap-services",
  1619  		"cleanup",
  1620  		"run-hook [snap-a;configure]",
  1621  		"run-hook [snap-a;check-health]",
  1622  		"check-rerefresh",
  1623  	}
  1624  
  1625  	chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1626  		if snapName == "snap-b" {
  1627  			// pretend than snap-b calls snapctl --hold to hold refresh of base-snap-b
  1628  			c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1629  		}
  1630  	}, expected)
  1631  
  1632  	s.state.Lock()
  1633  	defer s.state.Unlock()
  1634  
  1635  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`)
  1636  	tasks := chg.Tasks()
  1637  	// no re-refresh for base-snap-b because it was held.
  1638  	c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "snap-a" as needed`)
  1639  }
  1640  
  1641  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Proceed(c *C) {
  1642  	logbuf, restoreLogger := logger.MockLogger()
  1643  	defer restoreLogger()
  1644  
  1645  	expected := []string{
  1646  		"conditional-auto-refresh",
  1647  		"run-hook [snap-a;gate-auto-refresh]",
  1648  		// snap-b hook is triggered because of base-snap-b refresh
  1649  		"run-hook [snap-b;gate-auto-refresh]",
  1650  		"prerequisites",
  1651  		"download-snap",
  1652  		"validate-snap",
  1653  		"mount-snap",
  1654  		"run-hook [snap-a;pre-refresh]",
  1655  		"stop-snap-services",
  1656  		"remove-aliases",
  1657  		"unlink-current-snap",
  1658  		"copy-snap-data",
  1659  		"setup-profiles",
  1660  		"link-snap",
  1661  		"auto-connect",
  1662  		"set-auto-aliases",
  1663  		"setup-aliases",
  1664  		"run-hook [snap-a;post-refresh]",
  1665  		"start-snap-services",
  1666  		"cleanup",
  1667  		"run-hook [snap-a;configure]",
  1668  		"run-hook [snap-a;check-health]",
  1669  		"check-rerefresh",
  1670  	}
  1671  
  1672  	s.testAutoRefreshPhase2(c, func() {
  1673  		// pretend that snap-a and base-snap-b are initially held
  1674  		c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil)
  1675  		c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1676  	}, func(snapName string) {
  1677  		if snapName == "snap-a" {
  1678  			// pretend than snap-a calls snapctl --proceed
  1679  			c.Assert(snapstate.ProceedWithRefresh(s.state, "snap-a"), IsNil)
  1680  		}
  1681  		// note, do nothing about snap-b which just keeps its hold state in
  1682  		// the test, but if we were using real gate-auto-refresh hook
  1683  		// handler, the default behavior for snap-b if it doesn't call --hold
  1684  		// would be to proceed (hook handler would take care of that).
  1685  	}, expected)
  1686  
  1687  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`)
  1688  }
  1689  
  1690  func (s *snapmgrTestSuite) TestAutoRefreshPhase2AllHeld(c *C) {
  1691  	logbuf, restoreLogger := logger.MockLogger()
  1692  	defer restoreLogger()
  1693  
  1694  	expected := []string{
  1695  		"conditional-auto-refresh",
  1696  		"run-hook [snap-a;gate-auto-refresh]",
  1697  		// snap-b hook is triggered because of base-snap-b refresh
  1698  		"run-hook [snap-b;gate-auto-refresh]",
  1699  	}
  1700  
  1701  	s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1702  		switch snapName {
  1703  		case "snap-b":
  1704  			// pretend that snap-b calls snapctl --hold to hold refresh of base-snap-b
  1705  			c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1706  		case "snap-a":
  1707  			// pretend that snap-a calls snapctl --hold to hold itself
  1708  			c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil)
  1709  		default:
  1710  			c.Fatalf("unexpected snap %q", snapName)
  1711  		}
  1712  	}, expected)
  1713  
  1714  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b,snap-a`)
  1715  }
  1716  
  1717  func (s *snapmgrTestSuite) testAutoRefreshPhase2DiskSpaceCheck(c *C, fail bool) {
  1718  	st := s.state
  1719  	st.Lock()
  1720  	defer st.Unlock()
  1721  
  1722  	restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error {
  1723  		c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123))
  1724  		if fail {
  1725  			return &osutil.NotEnoughDiskSpaceError{}
  1726  		}
  1727  		return nil
  1728  	})
  1729  	defer restore()
  1730  
  1731  	var installSizeCalled bool
  1732  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1733  		installSizeCalled = true
  1734  		seen := map[string]bool{}
  1735  		for _, sn := range snaps {
  1736  			seen[sn.InstanceName()] = true
  1737  		}
  1738  		c.Check(seen, DeepEquals, map[string]bool{
  1739  			"base-snap-b": true,
  1740  			"snap-a":      true,
  1741  		})
  1742  		return 123, nil
  1743  	})
  1744  	defer restoreInstallSize()
  1745  
  1746  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  1747  	defer restoreModel()
  1748  
  1749  	tr := config.NewTransaction(s.state)
  1750  	tr.Set("core", "experimental.check-disk-space-refresh", true)
  1751  	tr.Commit()
  1752  
  1753  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1754  		fakeStore: s.fakeStore,
  1755  		refreshedSnaps: []*snap.Info{{
  1756  			Architectures: []string{"all"},
  1757  			SnapType:      snap.TypeApp,
  1758  			SideInfo: snap.SideInfo{
  1759  				RealName: "snap-a",
  1760  				Revision: snap.R(8),
  1761  			},
  1762  		}, {
  1763  			Architectures: []string{"all"},
  1764  			SnapType:      snap.TypeBase,
  1765  			SideInfo: snap.SideInfo{
  1766  				RealName: "base-snap-b",
  1767  				Revision: snap.R(3),
  1768  			},
  1769  		}}})
  1770  
  1771  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1772  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1773  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1774  
  1775  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1776  
  1777  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1778  	c.Assert(err, IsNil)
  1779  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1780  
  1781  	chg := s.state.NewChange("refresh", "...")
  1782  	for _, ts := range tss {
  1783  		chg.AddAll(ts)
  1784  	}
  1785  
  1786  	s.state.Unlock()
  1787  	defer s.se.Stop()
  1788  	s.settle(c)
  1789  	s.state.Lock()
  1790  
  1791  	c.Check(installSizeCalled, Equals, true)
  1792  	if fail {
  1793  		c.Check(chg.Status(), Equals, state.ErrorStatus)
  1794  		c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n- Run auto-refresh for ready snaps \(insufficient space.*`)
  1795  	} else {
  1796  		c.Check(chg.Status(), Equals, state.DoneStatus)
  1797  		c.Check(chg.Err(), IsNil)
  1798  	}
  1799  }
  1800  
  1801  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceError(c *C) {
  1802  	fail := true
  1803  	s.testAutoRefreshPhase2DiskSpaceCheck(c, fail)
  1804  }
  1805  
  1806  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceHappy(c *C) {
  1807  	var nofail bool
  1808  	s.testAutoRefreshPhase2DiskSpaceCheck(c, nofail)
  1809  }
  1810  
  1811  // XXX: this case is probably artificial; with proper conflict prevention
  1812  // we shouldn't get conflicts from doInstall in phase2.
  1813  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Conflict(c *C) {
  1814  	st := s.state
  1815  	st.Lock()
  1816  	defer st.Unlock()
  1817  
  1818  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1819  		fakeStore: s.fakeStore,
  1820  		refreshedSnaps: []*snap.Info{{
  1821  			Architectures: []string{"all"},
  1822  			SnapType:      snap.TypeApp,
  1823  			SideInfo: snap.SideInfo{
  1824  				RealName: "snap-a",
  1825  				Revision: snap.R(8),
  1826  			},
  1827  		}, {
  1828  			Architectures: []string{"all"},
  1829  			SnapType:      snap.TypeBase,
  1830  			SideInfo: snap.SideInfo{
  1831  				RealName: "base-snap-b",
  1832  				Revision: snap.R(3),
  1833  			},
  1834  		}}})
  1835  
  1836  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1837  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1838  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1839  
  1840  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1841  
  1842  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1843  	defer restore()
  1844  
  1845  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1846  	c.Assert(err, IsNil)
  1847  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1848  
  1849  	chg := s.state.NewChange("refresh", "...")
  1850  	for _, ts := range tss {
  1851  		chg.AddAll(ts)
  1852  	}
  1853  
  1854  	conflictChange := st.NewChange("conflicting change", "")
  1855  	conflictTask := st.NewTask("conflicting task", "")
  1856  	si := &snap.SideInfo{
  1857  		RealName: "snap-a",
  1858  		Revision: snap.R(1),
  1859  	}
  1860  	sup := snapstate.SnapSetup{SideInfo: si}
  1861  	conflictTask.Set("snap-setup", sup)
  1862  	conflictChange.AddTask(conflictTask)
  1863  	conflictTask.WaitFor(tss[0].Tasks()[0])
  1864  
  1865  	s.state.Unlock()
  1866  	defer s.se.Stop()
  1867  	s.settle(c)
  1868  	s.state.Lock()
  1869  
  1870  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  1871  	c.Check(chg.Err(), IsNil)
  1872  
  1873  	// no refresh of snap-a because of the conflict.
  1874  	expected := []string{
  1875  		"conditional-auto-refresh",
  1876  		"run-hook [snap-a;gate-auto-refresh]",
  1877  		// snap-b hook is triggered because of base-snap-b refresh
  1878  		"run-hook [snap-b;gate-auto-refresh]",
  1879  		"prerequisites",
  1880  		"download-snap",
  1881  		"validate-snap",
  1882  		"mount-snap",
  1883  		"run-hook [base-snap-b;pre-refresh]",
  1884  		"stop-snap-services",
  1885  		"remove-aliases",
  1886  		"unlink-current-snap",
  1887  		"copy-snap-data",
  1888  		"setup-profiles",
  1889  		"link-snap",
  1890  		"auto-connect",
  1891  		"set-auto-aliases",
  1892  		"setup-aliases",
  1893  		"run-hook [base-snap-b;post-refresh]",
  1894  		"start-snap-services",
  1895  		"cleanup",
  1896  		"run-hook [base-snap-b;check-health]",
  1897  		"check-rerefresh",
  1898  	}
  1899  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1900  }
  1901  
  1902  func (s *snapmgrTestSuite) TestAutoRefreshPhase2ConflictOtherSnapOp(c *C) {
  1903  	st := s.state
  1904  	st.Lock()
  1905  	defer st.Unlock()
  1906  
  1907  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1908  		fakeStore: s.fakeStore,
  1909  		refreshedSnaps: []*snap.Info{{
  1910  			Architectures: []string{"all"},
  1911  			SnapType:      snap.TypeApp,
  1912  			SideInfo: snap.SideInfo{
  1913  				RealName: "snap-a",
  1914  				Revision: snap.R(8),
  1915  			},
  1916  		}}})
  1917  
  1918  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1919  
  1920  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1921  
  1922  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1923  	defer restore()
  1924  
  1925  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1926  	c.Assert(err, IsNil)
  1927  	c.Check(names, DeepEquals, []string{"snap-a"})
  1928  
  1929  	chg := s.state.NewChange("fake-auto-refresh", "...")
  1930  	for _, ts := range tss {
  1931  		chg.AddAll(ts)
  1932  	}
  1933  
  1934  	s.state.Unlock()
  1935  	// run first task
  1936  	s.se.Ensure()
  1937  	s.se.Wait()
  1938  
  1939  	s.state.Lock()
  1940  
  1941  	_, err = snapstate.Remove(s.state, "snap-a", snap.R(8), nil)
  1942  	c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{
  1943  		ChangeKind: "fake-auto-refresh",
  1944  		Snap:       "snap-a",
  1945  	})
  1946  
  1947  	_, err = snapstate.Update(s.state, "snap-a", nil, 0, snapstate.Flags{})
  1948  	c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{
  1949  		ChangeKind: "fake-auto-refresh",
  1950  		Snap:       "snap-a",
  1951  	})
  1952  
  1953  	// only 2 tasks because we don't run settle() so conditional-auto-refresh
  1954  	// doesn't run and no new tasks get created.
  1955  	expected := []string{
  1956  		"conditional-auto-refresh",
  1957  		"run-hook [snap-a;gate-auto-refresh]",
  1958  	}
  1959  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1960  }
  1961  
  1962  func (s *snapmgrTestSuite) TestAutoRefreshPhase2GatedSnaps(c *C) {
  1963  	st := s.state
  1964  	st.Lock()
  1965  	defer st.Unlock()
  1966  
  1967  	restore := snapstate.MockSnapsToRefresh(func(gatingTask *state.Task) ([]*snapstate.RefreshCandidate, error) {
  1968  		c.Assert(gatingTask.Kind(), Equals, "conditional-auto-refresh")
  1969  		var candidates map[string]*snapstate.RefreshCandidate
  1970  		c.Assert(gatingTask.Get("snaps", &candidates), IsNil)
  1971  		seenSnaps := make(map[string]bool)
  1972  		var filteredByGatingHooks []*snapstate.RefreshCandidate
  1973  		for _, cand := range candidates {
  1974  			seenSnaps[cand.InstanceName()] = true
  1975  			if cand.InstanceName() == "snap-a" {
  1976  				continue
  1977  			}
  1978  			filteredByGatingHooks = append(filteredByGatingHooks, cand)
  1979  		}
  1980  		c.Check(seenSnaps, DeepEquals, map[string]bool{
  1981  			"snap-a":      true,
  1982  			"base-snap-b": true,
  1983  		})
  1984  		return filteredByGatingHooks, nil
  1985  	})
  1986  	defer restore()
  1987  
  1988  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1989  		fakeStore: s.fakeStore,
  1990  		refreshedSnaps: []*snap.Info{
  1991  			{
  1992  				Architectures: []string{"all"},
  1993  				SnapType:      snap.TypeApp,
  1994  				SideInfo: snap.SideInfo{
  1995  					RealName: "snap-a",
  1996  					Revision: snap.R(8),
  1997  				},
  1998  			}, {
  1999  				Architectures: []string{"all"},
  2000  				SnapType:      snap.TypeBase,
  2001  				SideInfo: snap.SideInfo{
  2002  					RealName: "base-snap-b",
  2003  					Revision: snap.R(3),
  2004  				},
  2005  			},
  2006  		}})
  2007  
  2008  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2009  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  2010  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  2011  
  2012  	snapstate.MockSnapReadInfo(fakeReadInfo)
  2013  
  2014  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  2015  	defer restoreModel()
  2016  
  2017  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  2018  	c.Assert(err, IsNil)
  2019  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  2020  
  2021  	chg := s.state.NewChange("refresh", "...")
  2022  	for _, ts := range tss {
  2023  		chg.AddAll(ts)
  2024  	}
  2025  
  2026  	s.state.Unlock()
  2027  	defer s.se.Stop()
  2028  	s.settle(c)
  2029  	s.state.Lock()
  2030  
  2031  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  2032  	c.Check(chg.Err(), IsNil)
  2033  
  2034  	expected := []string{
  2035  		"conditional-auto-refresh",
  2036  		"run-hook [snap-a;gate-auto-refresh]",
  2037  		// snap-b hook is triggered because of base-snap-b refresh
  2038  		"run-hook [snap-b;gate-auto-refresh]",
  2039  		"prerequisites",
  2040  		"download-snap",
  2041  		"validate-snap",
  2042  		"mount-snap",
  2043  		"run-hook [base-snap-b;pre-refresh]",
  2044  		"stop-snap-services",
  2045  		"remove-aliases",
  2046  		"unlink-current-snap",
  2047  		"copy-snap-data",
  2048  		"setup-profiles",
  2049  		"link-snap",
  2050  		"auto-connect",
  2051  		"set-auto-aliases",
  2052  		"setup-aliases",
  2053  		"run-hook [base-snap-b;post-refresh]",
  2054  		"start-snap-services",
  2055  		"cleanup",
  2056  		"run-hook [base-snap-b;check-health]",
  2057  		"check-rerefresh",
  2058  	}
  2059  	tasks := chg.Tasks()
  2060  	verifyPhasedAutorefreshTasks(c, tasks, expected)
  2061  	// no re-refresh for snap-a because it was held.
  2062  	c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b" as needed`)
  2063  
  2064  	// only snap-a remains in refresh-candidates because it was held;
  2065  	// base-snap-b got pruned (was refreshed).
  2066  	var candidates map[string]*snapstate.RefreshCandidate
  2067  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  2068  	c.Assert(candidates, HasLen, 1)
  2069  	c.Check(candidates["snap-a"], NotNil)
  2070  }
  2071  
  2072  func verifyPhasedAutorefreshTasks(c *C, tasks []*state.Task, expected []string) {
  2073  	c.Assert(len(tasks), Equals, len(expected))
  2074  	for i, t := range tasks {
  2075  		var got string
  2076  		if t.Kind() == "run-hook" {
  2077  			var hsup hookstate.HookSetup
  2078  			c.Assert(t.Get("hook-setup", &hsup), IsNil)
  2079  			got = fmt.Sprintf("%s [%s;%s]", t.Kind(), hsup.Snap, hsup.Hook)
  2080  		} else {
  2081  			got = t.Kind()
  2082  		}
  2083  		c.Assert(got, Equals, expected[i], Commentf("#%d", i))
  2084  	}
  2085  }