gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/hookstate/ctlcmd/refresh.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 ctlcmd
    21  
    22  import (
    23  	"fmt"
    24  	"time"
    25  
    26  	"gopkg.in/yaml.v2"
    27  
    28  	"github.com/snapcore/snapd/features"
    29  	"github.com/snapcore/snapd/i18n"
    30  	"github.com/snapcore/snapd/overlord/configstate/config"
    31  	"github.com/snapcore/snapd/overlord/hookstate"
    32  	"github.com/snapcore/snapd/overlord/snapstate"
    33  	"github.com/snapcore/snapd/overlord/state"
    34  	"github.com/snapcore/snapd/snap"
    35  )
    36  
    37  var autoRefreshForGatingSnap = snapstate.AutoRefreshForGatingSnap
    38  
    39  type refreshCommand struct {
    40  	baseCommand
    41  
    42  	Pending bool `long:"pending" description:"Show pending refreshes of the calling snap"`
    43  	// these two options are mutually exclusive
    44  	Proceed bool `long:"proceed" description:"Proceed with potentially disruptive refreshes"`
    45  	Hold    bool `long:"hold" description:"Do not proceed with potentially disruptive refreshes"`
    46  }
    47  
    48  var shortRefreshHelp = i18n.G("The refresh command prints pending refreshes and can hold back disruptive ones.")
    49  var longRefreshHelp = i18n.G(`
    50  The refresh command prints pending refreshes of the calling snap and can hold
    51  back disruptive refreshes of other snaps, such as refreshes of the kernel or
    52  base snaps that can trigger a restart. This command can be used from the
    53  gate-auto-refresh hook which is only run during auto-refresh.
    54  
    55  Snap can query pending refreshes with:
    56      $ snapctl refresh --pending
    57      pending: ready
    58      channel: stable
    59      version: 2
    60      revision: 2
    61      base: false
    62      restart: false
    63  
    64  The 'pending' flag can be "ready", "none" or "inhibited". It is set to "none"
    65  when a snap has no pending refreshes. It is set to "ready" when there are
    66  pending refreshes and to ”inhibited” when pending refreshes are being
    67  held back because more or more snap applications are running with the
    68  “refresh app awareness” feature enabled.
    69  
    70  The "base" and "restart" flags indicate whether the base snap is going to be
    71  updated and/or if a restart will occur, both of which are disruptive. A base
    72  snap update can temporarily disrupt the starting of applications or hooks from
    73  the snap.
    74  
    75  To tell snapd to proceed with pending refreshes:
    76      $ snapctl refresh --pending --proceed
    77  
    78  Note, a snap using --proceed cannot assume that the updates will occur as they
    79  might be held back by other snaps.
    80  
    81  To hold refresh for up to 90 days for the calling snap:
    82      $ snapctl refresh --pending --hold
    83  `)
    84  
    85  func init() {
    86  	cmd := addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() command {
    87  		return &refreshCommand{}
    88  	})
    89  	cmd.hidden = true
    90  }
    91  
    92  func (c *refreshCommand) Execute(args []string) error {
    93  	context, err := c.ensureContext()
    94  	if err != nil {
    95  		return err
    96  	}
    97  
    98  	if !context.IsEphemeral() && context.HookName() != "gate-auto-refresh" {
    99  		return fmt.Errorf("can only be used from gate-auto-refresh hook")
   100  	}
   101  
   102  	if c.Proceed && c.Hold {
   103  		return fmt.Errorf("cannot use --proceed and --hold together")
   104  	}
   105  
   106  	// --pending --proceed is a verbose way of saying --proceed, so only
   107  	// print pending if proceed wasn't requested.
   108  	if c.Pending && !c.Proceed {
   109  		if err := c.printPendingInfo(); err != nil {
   110  			return err
   111  		}
   112  	}
   113  
   114  	switch {
   115  	case c.Proceed:
   116  		return c.proceed()
   117  	case c.Hold:
   118  		return c.hold()
   119  	}
   120  
   121  	return nil
   122  }
   123  
   124  type updateDetails struct {
   125  	Pending  string `yaml:"pending,omitempty"`
   126  	Channel  string `yaml:"channel,omitempty"`
   127  	Version  string `yaml:"version,omitempty"`
   128  	Revision int    `yaml:"revision,omitempty"`
   129  	// TODO: epoch
   130  	Base    bool `yaml:"base"`
   131  	Restart bool `yaml:"restart"`
   132  }
   133  
   134  // refreshCandidate is a subset of refreshCandidate defined by snapstate and
   135  // stored in "refresh-candidates".
   136  type refreshCandidate struct {
   137  	Channel     string         `json:"channel,omitempty"`
   138  	Version     string         `json:"version,omitempty"`
   139  	SideInfo    *snap.SideInfo `json:"side-info,omitempty"`
   140  	InstanceKey string         `json:"instance-key,omitempty"`
   141  }
   142  
   143  func getUpdateDetails(context *hookstate.Context) (*updateDetails, error) {
   144  	context.Lock()
   145  	defer context.Unlock()
   146  
   147  	st := context.State()
   148  
   149  	affected, err := snapstate.AffectedByRefreshCandidates(st)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	var base, restart bool
   155  	if affectedInfo, ok := affected[context.InstanceName()]; ok {
   156  		base = affectedInfo.Base
   157  		restart = affectedInfo.Restart
   158  	}
   159  
   160  	var snapst snapstate.SnapState
   161  	if err := snapstate.Get(st, context.InstanceName(), &snapst); err != nil {
   162  		return nil, fmt.Errorf("internal error: cannot get snap state for %q: %v", context.InstanceName(), err)
   163  	}
   164  
   165  	var candidates map[string]*refreshCandidate
   166  	if err := st.Get("refresh-candidates", &candidates); err != nil && err != state.ErrNoState {
   167  		return nil, err
   168  	}
   169  
   170  	var pending string
   171  	switch {
   172  	case snapst.RefreshInhibitedTime != nil:
   173  		pending = "inhibited"
   174  	case candidates[context.InstanceName()] != nil:
   175  		pending = "ready"
   176  	default:
   177  		pending = "none"
   178  	}
   179  
   180  	up := updateDetails{
   181  		Base:    base,
   182  		Restart: restart,
   183  		Pending: pending,
   184  	}
   185  
   186  	// try to find revision/version/channel info from refresh-candidates; it
   187  	// may be missing if the hook is called for snap that is just affected by
   188  	// refresh but not refreshed itself, in such case this data is not
   189  	// displayed.
   190  	if cand, ok := candidates[context.InstanceName()]; ok {
   191  		up.Channel = cand.Channel
   192  		up.Revision = cand.SideInfo.Revision.N
   193  		up.Version = cand.Version
   194  		return &up, nil
   195  	}
   196  
   197  	// refresh-hint not present, look up channel info in snapstate
   198  	up.Channel = snapst.TrackingChannel
   199  	return &up, nil
   200  }
   201  
   202  func (c *refreshCommand) printPendingInfo() error {
   203  	details, err := getUpdateDetails(c.context())
   204  	if err != nil {
   205  		return err
   206  	}
   207  	out, err := yaml.Marshal(details)
   208  	if err != nil {
   209  		return err
   210  	}
   211  	c.printf("%s", string(out))
   212  	return nil
   213  }
   214  
   215  func (c *refreshCommand) hold() error {
   216  	ctx := c.context()
   217  	if ctx.IsEphemeral() {
   218  		return fmt.Errorf("cannot hold outside of gate-auto-refresh hook")
   219  	}
   220  	ctx.Lock()
   221  	defer ctx.Unlock()
   222  	st := ctx.State()
   223  
   224  	// cache the action so that hook handler can implement default behavior
   225  	ctx.Cache("action", snapstate.GateAutoRefreshHold)
   226  
   227  	affecting, err := snapstate.AffectingSnapsForAffectedByRefreshCandidates(st, ctx.InstanceName())
   228  	if err != nil {
   229  		return err
   230  	}
   231  	if len(affecting) == 0 {
   232  		// this shouldn't happen because the hook is executed during auto-refresh
   233  		// change which conflicts with other changes (if it happens that means
   234  		// something changed in the meantime and we didn't handle conflicts
   235  		// correctly).
   236  		return fmt.Errorf("internal error: snap %q is not affected by any snaps", ctx.InstanceName())
   237  	}
   238  
   239  	// no duration specified, use maximum allowed for this gating snap.
   240  	var holdDuration time.Duration
   241  	if err := snapstate.HoldRefresh(st, ctx.InstanceName(), holdDuration, affecting...); err != nil {
   242  		// TODO: let a snap hold again once for 1h.
   243  		return err
   244  	}
   245  
   246  	return nil
   247  }
   248  
   249  func (c *refreshCommand) proceed() error {
   250  	ctx := c.context()
   251  	ctx.Lock()
   252  	defer ctx.Unlock()
   253  
   254  	// running outside of hook
   255  	if ctx.IsEphemeral() {
   256  		// TODO: consider having a permission via an interface for this before making this not experimental
   257  		st := ctx.State()
   258  		// we need to check if GateAutoRefreshHook feature is enabled when
   259  		// running by the snap (we don't need to do this when running from the
   260  		// hook because in that case hook task won't be created if not enabled).
   261  		tr := config.NewTransaction(st)
   262  		gateAutoRefreshHook, err := features.Flag(tr, features.GateAutoRefreshHook)
   263  		if err != nil && !config.IsNoOption(err) {
   264  			return err
   265  		}
   266  		if !gateAutoRefreshHook {
   267  			return fmt.Errorf("cannot proceed without experimental.gate-auto-refresh feature enabled")
   268  		}
   269  
   270  		return autoRefreshForGatingSnap(st, ctx.InstanceName())
   271  	}
   272  
   273  	// cache the action, hook handler will trigger proceed logic; we cannot
   274  	// call snapstate.ProceedWithRefresh() immediately as this would reset
   275  	// holdState, allowing the snap to --hold with fresh duration limit.
   276  	ctx.Cache("action", snapstate.GateAutoRefreshProceed)
   277  
   278  	return nil
   279  }