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