github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/overlord/snapstate/refreshhints.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 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
    21  
    22  import (
    23  	"fmt"
    24  	"time"
    25  
    26  	"github.com/snapcore/snapd/logger"
    27  	"github.com/snapcore/snapd/overlord/auth"
    28  	"github.com/snapcore/snapd/overlord/state"
    29  	"github.com/snapcore/snapd/release"
    30  	"github.com/snapcore/snapd/snap"
    31  	"github.com/snapcore/snapd/store"
    32  	"github.com/snapcore/snapd/timings"
    33  )
    34  
    35  var refreshHintsDelay = time.Duration(24 * time.Hour)
    36  
    37  // refreshHints will ensure that we get regular data about refreshes
    38  // so that we can potentially warn the user about important missing
    39  // refreshes.
    40  type refreshHints struct {
    41  	state *state.State
    42  }
    43  
    44  func newRefreshHints(st *state.State) *refreshHints {
    45  	return &refreshHints{state: st}
    46  }
    47  
    48  func (r *refreshHints) lastRefresh(timestampKey string) (time.Time, error) {
    49  	return getTime(r.state, timestampKey)
    50  }
    51  
    52  func (r *refreshHints) needsUpdate() (bool, error) {
    53  	tFull, err := r.lastRefresh("last-refresh")
    54  	if err != nil {
    55  		return false, err
    56  	}
    57  	tHints, err := r.lastRefresh("last-refresh-hints")
    58  	if err != nil {
    59  		return false, err
    60  	}
    61  
    62  	recentEnough := time.Now().Add(-refreshHintsDelay)
    63  	if tFull.After(recentEnough) || tFull.Equal(recentEnough) {
    64  		return false, nil
    65  	}
    66  	return tHints.Before(recentEnough), nil
    67  }
    68  
    69  func (r *refreshHints) refresh() error {
    70  	var refreshManaged bool
    71  	refreshManaged, _, _ = refreshScheduleManaged(r.state)
    72  
    73  	var err error
    74  	perfTimings := timings.New(map[string]string{"ensure": "refresh-hints"})
    75  	defer perfTimings.Save(r.state)
    76  
    77  	var updates []*snap.Info
    78  	var ignoreValidationByInstanceName map[string]bool
    79  	timings.Run(perfTimings, "refresh-candidates", "query store for refresh candidates", func(tm timings.Measurer) {
    80  		updates, _, ignoreValidationByInstanceName, err = refreshCandidates(auth.EnsureContextTODO(), r.state, nil, nil, &store.RefreshOptions{RefreshManaged: refreshManaged})
    81  	})
    82  	// TODO: we currently set last-refresh-hints even when there was an
    83  	// error. In the future we may retry with a backoff.
    84  	r.state.Set("last-refresh-hints", time.Now())
    85  
    86  	if err != nil {
    87  		return err
    88  	}
    89  	deviceCtx, err := DeviceCtxFromState(r.state, nil)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	hints, err := refreshHintsFromCandidates(r.state, updates, ignoreValidationByInstanceName, deviceCtx)
    94  	if err != nil {
    95  		return fmt.Errorf("internal error: cannot get refresh-candidates: %v", err)
    96  	}
    97  	r.state.Set("refresh-candidates", hints)
    98  	return nil
    99  }
   100  
   101  // AtSeed configures hints refresh policies at end of seeding.
   102  func (r *refreshHints) AtSeed() error {
   103  	// on classic hold hints refreshes for a full 24h
   104  	if release.OnClassic {
   105  		var t1 time.Time
   106  		err := r.state.Get("last-refresh-hints", &t1)
   107  		if err != state.ErrNoState {
   108  			// already set or other error
   109  			return err
   110  		}
   111  		r.state.Set("last-refresh-hints", time.Now())
   112  	}
   113  	return nil
   114  }
   115  
   116  // Ensure will ensure that refresh hints are available on a regular
   117  // interval.
   118  func (r *refreshHints) Ensure() error {
   119  	r.state.Lock()
   120  	defer r.state.Unlock()
   121  
   122  	// CanAutoRefresh is a hook that is set by the devicestate
   123  	// code to ensure that we only AutoRefersh if the device has
   124  	// bootstraped itself enough. This is only nil when snapstate
   125  	// is used in isolation (like in tests).
   126  	if CanAutoRefresh == nil {
   127  		return nil
   128  	}
   129  	if ok, err := CanAutoRefresh(r.state); err != nil || !ok {
   130  		return err
   131  	}
   132  
   133  	needsUpdate, err := r.needsUpdate()
   134  	if err != nil {
   135  		return err
   136  	}
   137  	if !needsUpdate {
   138  		return nil
   139  	}
   140  	return r.refresh()
   141  }
   142  
   143  func refreshHintsFromCandidates(st *state.State, updates []*snap.Info, ignoreValidationByInstanceName map[string]bool, deviceCtx DeviceContext) (map[string]*refreshCandidate, error) {
   144  	if ValidateRefreshes != nil && len(updates) != 0 {
   145  		userID := 0
   146  		var err error
   147  		updates, err = ValidateRefreshes(st, updates, ignoreValidationByInstanceName, userID, deviceCtx)
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  	}
   152  
   153  	hints := make(map[string]*refreshCandidate, len(updates))
   154  	for _, update := range updates {
   155  		var snapst SnapState
   156  		if err := Get(st, update.InstanceName(), &snapst); err != nil {
   157  			return nil, err
   158  		}
   159  
   160  		flags := snapst.Flags
   161  		flags.IsAutoRefresh = true
   162  		flags, err := earlyChecks(st, &snapst, update, flags)
   163  		if err != nil {
   164  			logger.Debugf("update hint for %q is not applicable: %v", update.InstanceName(), err)
   165  			continue
   166  		}
   167  
   168  		snapsup := &refreshCandidate{
   169  			SnapSetup: SnapSetup{
   170  				Base:      update.Base,
   171  				Prereq:    defaultContentPlugProviders(st, update),
   172  				Channel:   snapst.TrackingChannel,
   173  				CohortKey: snapst.CohortKey,
   174  				// UserID not set
   175  				Flags:        flags.ForSnapSetup(),
   176  				DownloadInfo: &update.DownloadInfo,
   177  				SideInfo:     &update.SideInfo,
   178  				Type:         update.Type(),
   179  				PlugsOnly:    len(update.Slots) == 0,
   180  				InstanceKey:  update.InstanceKey,
   181  				auxStoreInfo: auxStoreInfo{
   182  					Website: update.Website,
   183  					Media:   update.Media,
   184  				},
   185  			},
   186  			Version: update.Version,
   187  		}
   188  		hints[update.InstanceName()] = snapsup
   189  	}
   190  	return hints, nil
   191  }
   192  
   193  // pruneRefreshCandidates removes the given snaps from refresh-candidates map
   194  // in the state.
   195  func pruneRefreshCandidates(st *state.State, snaps ...string) error {
   196  	var candidates map[string]*refreshCandidate
   197  
   198  	err := st.Get("refresh-candidates", &candidates)
   199  	if err != nil {
   200  		if err == state.ErrNoState {
   201  			return nil
   202  		}
   203  		return err
   204  	}
   205  
   206  	for _, snapName := range snaps {
   207  		delete(candidates, snapName)
   208  	}
   209  
   210  	st.Set("refresh-candidates", candidates)
   211  	return nil
   212  }