gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/snapstate/refreshhints_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017-2018 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  	"time"
    25  
    26  	. "gopkg.in/check.v1"
    27  
    28  	"github.com/snapcore/snapd/dirs"
    29  	"github.com/snapcore/snapd/interfaces"
    30  	"github.com/snapcore/snapd/interfaces/builtin"
    31  	"github.com/snapcore/snapd/overlord/auth"
    32  	"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
    33  	"github.com/snapcore/snapd/overlord/snapstate"
    34  	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
    35  	"github.com/snapcore/snapd/overlord/state"
    36  	"github.com/snapcore/snapd/release"
    37  	"github.com/snapcore/snapd/snap"
    38  	"github.com/snapcore/snapd/snap/snaptest"
    39  	"github.com/snapcore/snapd/store"
    40  	"github.com/snapcore/snapd/store/storetest"
    41  )
    42  
    43  type recordingStore struct {
    44  	storetest.Store
    45  
    46  	ops            []string
    47  	refreshedSnaps []*snap.Info
    48  }
    49  
    50  func (r *recordingStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
    51  	if assertQuery != nil {
    52  		panic("no assertion query support")
    53  	}
    54  	if ctx == nil || !auth.IsEnsureContext(ctx) {
    55  		panic("Ensure marked context required")
    56  	}
    57  	if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 {
    58  		panic("expected in test one action for each current snaps, and at least one snap")
    59  	}
    60  	for _, a := range actions {
    61  		if a.Action != "refresh" {
    62  			panic("expected refresh actions")
    63  		}
    64  	}
    65  	r.ops = append(r.ops, "list-refresh")
    66  
    67  	res := []store.SnapActionResult{}
    68  	for _, rs := range r.refreshedSnaps {
    69  		res = append(res, store.SnapActionResult{Info: rs})
    70  	}
    71  	return res, nil, nil
    72  }
    73  
    74  type refreshHintsTestSuite struct {
    75  	state *state.State
    76  
    77  	store        *recordingStore
    78  	restoreModel func()
    79  }
    80  
    81  var _ = Suite(&refreshHintsTestSuite{})
    82  
    83  func (s *refreshHintsTestSuite) SetUpTest(c *C) {
    84  	dirs.SetRootDir(c.MkDir())
    85  
    86  	s.state = state.New(nil)
    87  
    88  	s.store = &recordingStore{}
    89  	s.state.Lock()
    90  	defer s.state.Unlock()
    91  	snapstate.ReplaceStore(s.state, s.store)
    92  
    93  	snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
    94  		Active: true,
    95  		Sequence: []*snap.SideInfo{
    96  			{RealName: "some-snap", Revision: snap.R(5), SnapID: "some-snap-id"},
    97  		},
    98  		Current:         snap.R(5),
    99  		SnapType:        "app",
   100  		UserID:          1,
   101  		CohortKey:       "cohort",
   102  		TrackingChannel: "stable",
   103  	})
   104  
   105  	snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
   106  	snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
   107  		return nil, nil
   108  	}
   109  
   110  	s.state.Set("refresh-privacy-key", "privacy-key")
   111  
   112  	s.restoreModel = snapstatetest.MockDeviceModel(DefaultModel())
   113  }
   114  
   115  func (s *refreshHintsTestSuite) TearDownTest(c *C) {
   116  	dirs.SetRootDir("/")
   117  	snapstate.CanAutoRefresh = nil
   118  	snapstate.AutoAliases = nil
   119  	s.restoreModel()
   120  }
   121  
   122  func (s *refreshHintsTestSuite) TestLastRefresh(c *C) {
   123  	rh := snapstate.NewRefreshHints(s.state)
   124  	err := rh.Ensure()
   125  	c.Check(err, IsNil)
   126  	c.Check(s.store.ops, DeepEquals, []string{"list-refresh"})
   127  }
   128  
   129  func (s *refreshHintsTestSuite) TestLastRefreshNoRefreshNeeded(c *C) {
   130  	s.state.Lock()
   131  	s.state.Set("last-refresh-hints", time.Now().Add(-23*time.Hour))
   132  	s.state.Unlock()
   133  
   134  	rh := snapstate.NewRefreshHints(s.state)
   135  	err := rh.Ensure()
   136  	c.Check(err, IsNil)
   137  	c.Check(s.store.ops, HasLen, 0)
   138  }
   139  
   140  func (s *refreshHintsTestSuite) TestLastRefreshNoRefreshNeededBecauseOfFullAutoRefresh(c *C) {
   141  	s.state.Lock()
   142  	s.state.Set("last-refresh-hints", time.Now().Add(-48*time.Hour))
   143  	s.state.Unlock()
   144  
   145  	s.state.Lock()
   146  	s.state.Set("last-refresh", time.Now().Add(-23*time.Hour))
   147  	s.state.Unlock()
   148  
   149  	rh := snapstate.NewRefreshHints(s.state)
   150  	err := rh.Ensure()
   151  	c.Check(err, IsNil)
   152  	c.Check(s.store.ops, HasLen, 0)
   153  }
   154  
   155  func (s *refreshHintsTestSuite) TestAtSeedPolicy(c *C) {
   156  	r := release.MockOnClassic(false)
   157  	defer r()
   158  
   159  	s.state.Lock()
   160  	defer s.state.Unlock()
   161  
   162  	rh := snapstate.NewRefreshHints(s.state)
   163  
   164  	// on core, does nothing
   165  	err := rh.AtSeed()
   166  	c.Assert(err, IsNil)
   167  	var t1 time.Time
   168  	err = s.state.Get("last-refresh-hints", &t1)
   169  	c.Check(err, Equals, state.ErrNoState)
   170  
   171  	release.MockOnClassic(true)
   172  	// on classic it sets last-refresh-hints to now,
   173  	// postponing it of 24h
   174  	err = rh.AtSeed()
   175  	c.Assert(err, IsNil)
   176  	err = s.state.Get("last-refresh-hints", &t1)
   177  	c.Check(err, IsNil)
   178  
   179  	// nop if tried again
   180  	err = rh.AtSeed()
   181  	c.Assert(err, IsNil)
   182  	var t2 time.Time
   183  	err = s.state.Get("last-refresh-hints", &t2)
   184  	c.Check(err, IsNil)
   185  	c.Check(t1.Equal(t2), Equals, true)
   186  }
   187  
   188  func (s *refreshHintsTestSuite) TestRefreshHintsStoresRefreshCandidates(c *C) {
   189  	s.state.Lock()
   190  	repo := interfaces.NewRepository()
   191  	for _, iface := range builtin.Interfaces() {
   192  		err := repo.AddInterface(iface)
   193  		c.Assert(err, IsNil)
   194  	}
   195  	ifacerepo.Replace(s.state, repo)
   196  
   197  	snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
   198  		Active: true,
   199  		Sequence: []*snap.SideInfo{
   200  			{RealName: "other-snap", Revision: snap.R(1), SnapID: "other-snap-id"},
   201  		},
   202  		Current:         snap.R(1),
   203  		SnapType:        "app",
   204  		TrackingChannel: "devel",
   205  		UserID:          0,
   206  	})
   207  	s.state.Unlock()
   208  
   209  	info2 := &snap.Info{
   210  		Version:       "v1",
   211  		Architectures: []string{"all"},
   212  		SnapType:      snap.TypeApp,
   213  		SideInfo: snap.SideInfo{
   214  			RealName: "other-snap",
   215  			Revision: snap.R(2),
   216  		},
   217  		DownloadInfo: snap.DownloadInfo{
   218  			Size: int64(88),
   219  		},
   220  	}
   221  	plugs := map[string]*snap.PlugInfo{
   222  		"plug": {
   223  			Snap:      info2,
   224  			Name:      "plug",
   225  			Interface: "content",
   226  			Attrs: map[string]interface{}{
   227  				"default-provider": "foo-snap:",
   228  				"content":          "some-content",
   229  			},
   230  			Apps:  map[string]*snap.AppInfo{},
   231  			Hooks: map[string]*snap.HookInfo{},
   232  		}}
   233  	info2.Plugs = plugs
   234  
   235  	s.store.refreshedSnaps = []*snap.Info{{
   236  		Version:       "2",
   237  		Architectures: []string{"all"},
   238  		Base:          "some-base",
   239  		SnapType:      snap.TypeApp,
   240  		SideInfo: snap.SideInfo{
   241  			RealName: "some-snap",
   242  			Revision: snap.R(1),
   243  		},
   244  		DownloadInfo: snap.DownloadInfo{
   245  			Size: int64(99),
   246  		},
   247  	}, info2}
   248  
   249  	rh := snapstate.NewRefreshHints(s.state)
   250  	err := rh.Ensure()
   251  	c.Check(err, IsNil)
   252  	c.Check(s.store.ops, DeepEquals, []string{"list-refresh"})
   253  
   254  	s.state.Lock()
   255  	defer s.state.Unlock()
   256  
   257  	var candidates map[string]*snapstate.RefreshCandidate
   258  	c.Assert(s.state.Get("refresh-candidates", &candidates), IsNil)
   259  	c.Assert(candidates, HasLen, 2)
   260  	cand1 := candidates["some-snap"]
   261  	c.Assert(cand1, NotNil)
   262  	c.Check(cand1.InstanceName(), Equals, "some-snap")
   263  	c.Check(cand1.SnapBase(), Equals, "some-base")
   264  	c.Check(cand1.Type(), Equals, snap.TypeApp)
   265  	c.Check(cand1.DownloadSize(), Equals, int64(99))
   266  	c.Check(cand1.Version, Equals, "2")
   267  
   268  	cand2 := candidates["other-snap"]
   269  	c.Assert(cand2, NotNil)
   270  	c.Check(cand2.InstanceName(), Equals, "other-snap")
   271  	c.Check(cand2.SnapBase(), Equals, "")
   272  	c.Check(cand2.Type(), Equals, snap.TypeApp)
   273  	c.Check(cand2.DownloadSize(), Equals, int64(88))
   274  	c.Check(cand2.Version, Equals, "v1")
   275  
   276  	var snapst1 snapstate.SnapState
   277  	err = snapstate.Get(s.state, "some-snap", &snapst1)
   278  	c.Assert(err, IsNil)
   279  
   280  	sup, snapst, err := cand1.SnapSetupForUpdate(s.state, nil, 0, nil)
   281  	c.Assert(err, IsNil)
   282  	c.Check(sup, DeepEquals, &snapstate.SnapSetup{
   283  		Base: "some-base",
   284  		Type: "app",
   285  		SideInfo: &snap.SideInfo{
   286  			RealName: "some-snap",
   287  			Revision: snap.R(1),
   288  		},
   289  		PlugsOnly: true,
   290  		CohortKey: "cohort",
   291  		Channel:   "stable",
   292  		Flags: snapstate.Flags{
   293  			IsAutoRefresh: true,
   294  		},
   295  		DownloadInfo: &snap.DownloadInfo{
   296  			Size: int64(99),
   297  		},
   298  	})
   299  	c.Check(snapst, DeepEquals, &snapst1)
   300  
   301  	var snapst2 snapstate.SnapState
   302  	err = snapstate.Get(s.state, "other-snap", &snapst2)
   303  	c.Assert(err, IsNil)
   304  
   305  	sup, snapst, err = cand2.SnapSetupForUpdate(s.state, nil, 0, nil)
   306  	c.Assert(err, IsNil)
   307  	c.Check(sup, DeepEquals, &snapstate.SnapSetup{
   308  		Type: "app",
   309  		SideInfo: &snap.SideInfo{
   310  			RealName: "other-snap",
   311  			Revision: snap.R(2),
   312  		},
   313  		Prereq:    []string{"foo-snap"},
   314  		PlugsOnly: true,
   315  		Channel:   "devel",
   316  		Flags: snapstate.Flags{
   317  			IsAutoRefresh: true,
   318  		},
   319  		DownloadInfo: &snap.DownloadInfo{
   320  			Size: int64(88),
   321  		},
   322  	})
   323  	c.Check(snapst, DeepEquals, &snapst2)
   324  }
   325  
   326  func (s *refreshHintsTestSuite) TestPruneRefreshCandidates(c *C) {
   327  	st := s.state
   328  	st.Lock()
   329  	defer st.Unlock()
   330  
   331  	// check that calling PruneRefreshCandidates when there is nothing to do is fine.
   332  	c.Assert(snapstate.PruneRefreshCandidates(st, "some-snap"), IsNil)
   333  
   334  	candidates := map[string]*snapstate.RefreshCandidate{
   335  		"snap-a": {
   336  			SnapSetup: snapstate.SnapSetup{
   337  				Type: "app",
   338  				SideInfo: &snap.SideInfo{
   339  					RealName: "snap-a",
   340  					Revision: snap.R(1),
   341  				},
   342  			},
   343  		},
   344  		"snap-b": {
   345  			SnapSetup: snapstate.SnapSetup{
   346  				Type: "app",
   347  				SideInfo: &snap.SideInfo{
   348  					RealName: "snap-b",
   349  					Revision: snap.R(1),
   350  				},
   351  			},
   352  		},
   353  		"snap-c": {
   354  			SnapSetup: snapstate.SnapSetup{
   355  				Type: "app",
   356  				SideInfo: &snap.SideInfo{
   357  					RealName: "snap-c",
   358  					Revision: snap.R(1),
   359  				},
   360  			},
   361  		},
   362  	}
   363  	st.Set("refresh-candidates", candidates)
   364  
   365  	c.Assert(snapstate.PruneRefreshCandidates(st, "snap-a"), IsNil)
   366  
   367  	var candidates2 map[string]*snapstate.RefreshCandidate
   368  	c.Assert(st.Get("refresh-candidates", &candidates2), IsNil)
   369  	_, ok := candidates2["snap-a"]
   370  	c.Check(ok, Equals, false)
   371  	_, ok = candidates2["snap-b"]
   372  	c.Check(ok, Equals, true)
   373  	_, ok = candidates2["snap-c"]
   374  	c.Check(ok, Equals, true)
   375  
   376  	var candidates3 map[string]*snapstate.RefreshCandidate
   377  	c.Assert(snapstate.PruneRefreshCandidates(st, "snap-b"), IsNil)
   378  	c.Assert(st.Get("refresh-candidates", &candidates3), IsNil)
   379  	_, ok = candidates3["snap-a"]
   380  	c.Check(ok, Equals, false)
   381  	_, ok = candidates3["snap-b"]
   382  	c.Check(ok, Equals, false)
   383  	_, ok = candidates3["snap-c"]
   384  	c.Check(ok, Equals, true)
   385  }
   386  
   387  func (s *refreshHintsTestSuite) TestRefreshHintsNotApplicableWrongArch(c *C) {
   388  	s.state.Lock()
   389  	snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
   390  		Active: true,
   391  		Sequence: []*snap.SideInfo{
   392  			{RealName: "other-snap", Revision: snap.R(1), SnapID: "other-snap-id"},
   393  		},
   394  		Current:  snap.R(1),
   395  		SnapType: "app",
   396  	})
   397  	s.state.Unlock()
   398  
   399  	s.store.refreshedSnaps = []*snap.Info{{
   400  		Architectures: []string{"all"},
   401  		SnapType:      snap.TypeApp,
   402  		SideInfo: snap.SideInfo{
   403  			RealName: "some-snap",
   404  			Revision: snap.R(1),
   405  		},
   406  	}, {
   407  		Architectures: []string{"somearch"},
   408  		SnapType:      snap.TypeApp,
   409  		SideInfo: snap.SideInfo{
   410  			RealName: "other-snap",
   411  			Revision: snap.R(2),
   412  		},
   413  	}}
   414  
   415  	rh := snapstate.NewRefreshHints(s.state)
   416  	c.Assert(rh.Ensure(), IsNil)
   417  
   418  	s.state.Lock()
   419  	defer s.state.Unlock()
   420  
   421  	var candidates map[string]*snapstate.RefreshCandidate
   422  	c.Assert(s.state.Get("refresh-candidates", &candidates), IsNil)
   423  	c.Assert(candidates, HasLen, 1)
   424  	c.Check(candidates["some-snap"], NotNil)
   425  }
   426  
   427  const otherSnapYaml = `name: other-snap
   428  version: 1.0
   429  epoch: 1
   430  type: app
   431  `
   432  
   433  func (s *refreshHintsTestSuite) TestRefreshHintsNotApplicableWrongEpoch(c *C) {
   434  	s.state.Lock()
   435  
   436  	si := &snap.SideInfo{RealName: "other-snap", Revision: snap.R(1), SnapID: "other-snap-id"}
   437  	snaptest.MockSnap(c, otherSnapYaml, si)
   438  	snapstate.Set(s.state, "other-snap", &snapstate.SnapState{
   439  		Active:   true,
   440  		Sequence: []*snap.SideInfo{si},
   441  		Current:  snap.R(1),
   442  		SnapType: "app",
   443  	})
   444  	s.state.Unlock()
   445  
   446  	s.store.refreshedSnaps = []*snap.Info{{
   447  		Architectures: []string{"all"},
   448  		SnapType:      snap.TypeApp,
   449  		SideInfo: snap.SideInfo{
   450  			RealName: "some-snap",
   451  			Revision: snap.R(1),
   452  		},
   453  	}, {
   454  		Architectures: []string{"all"},
   455  		SnapType:      snap.TypeApp,
   456  		SideInfo: snap.SideInfo{
   457  			RealName: "other-snap",
   458  			Revision: snap.R(2),
   459  		},
   460  		Epoch: snap.Epoch{Read: []uint32{2}, Write: []uint32{2}},
   461  	}}
   462  
   463  	rh := snapstate.NewRefreshHints(s.state)
   464  	c.Assert(rh.Ensure(), IsNil)
   465  
   466  	s.state.Lock()
   467  	defer s.state.Unlock()
   468  
   469  	var candidates map[string]*snapstate.RefreshCandidate
   470  	c.Assert(s.state.Get("refresh-candidates", &candidates), IsNil)
   471  	c.Assert(candidates, HasLen, 1)
   472  	// other-snap ignored due to epoch
   473  	c.Check(candidates["some-snap"], NotNil)
   474  }