github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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/i18n"
    29  	"github.com/snapcore/snapd/overlord/hookstate"
    30  	"github.com/snapcore/snapd/overlord/snapstate"
    31  	"github.com/snapcore/snapd/snap"
    32  )
    33  
    34  type refreshCommand struct {
    35  	baseCommand
    36  
    37  	Pending bool `long:"pending" description:"Show pending refreshes of the calling snap"`
    38  	// these two options are mutually exclusive
    39  	Proceed bool `long:"proceed" description:"Proceed with potentially disruptive refreshes"`
    40  	Hold    bool `long:"hold" description:"Do not proceed with potentially disruptive refreshes"`
    41  }
    42  
    43  var shortRefreshHelp = i18n.G("The refresh command prints pending refreshes and can hold back disruptive ones.")
    44  var longRefreshHelp = i18n.G(`
    45  The refresh command prints pending refreshes of the calling snap and can hold
    46  back disruptive refreshes of other snaps, such as refreshes of the kernel or
    47  base snaps that can trigger a restart. This command can be used from the
    48  gate-auto-refresh hook which is only run during auto-refresh.
    49  
    50  Snap can query pending refreshes with:
    51      $ snapctl refresh --pending
    52      pending: ready
    53      channel: stable
    54      version: 2
    55      revision: 2
    56      base: false
    57      restart: false
    58  
    59  The 'pending' flag can be "ready", "none" or "inhibited". It is set to "none"
    60  when a snap has no pending refreshes. It is set to "ready" when there are
    61  pending refreshes and to ”inhibited” when pending refreshes are being
    62  held back because more or more snap applications are running with the
    63  “refresh app awareness” feature enabled.
    64  
    65  The "base" and "restart" flags indicate whether the base snap is going to be
    66  updated and/or if a restart will occur, both of which are disruptive. A base
    67  snap update can temporarily disrupt the starting of applications or hooks from
    68  the snap.
    69  
    70  To tell snapd to proceed with pending refreshes:
    71      $ snapctl refresh --pending --proceed
    72  
    73  Note, a snap using --proceed cannot assume that the updates will occur as they
    74  might be held back by other snaps.
    75  
    76  To hold refresh for up to 90 days for the calling snap:
    77      $ snapctl refresh --pending --hold
    78  `)
    79  
    80  func init() {
    81  	cmd := addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() command {
    82  		return &refreshCommand{}
    83  	})
    84  	cmd.hidden = true
    85  }
    86  
    87  func (c *refreshCommand) Execute(args []string) error {
    88  	context := c.context()
    89  	if context == nil {
    90  		return fmt.Errorf("cannot run without a context")
    91  	}
    92  	if context.IsEphemeral() {
    93  		// TODO: handle this
    94  		return fmt.Errorf("cannot run outside of gate-auto-refresh hook")
    95  	}
    96  
    97  	if 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  	ctx.Lock()
   218  	defer ctx.Unlock()
   219  	st := ctx.State()
   220  
   221  	// cache the action so that hook handler can implement default behavior
   222  	ctx.Cache("action", snapstate.GateAutoRefreshHold)
   223  
   224  	var affecting []string
   225  	if err := ctx.Get("affecting-snaps", &affecting); err != nil {
   226  		return fmt.Errorf("internal error: cannot get affecting-snaps")
   227  	}
   228  
   229  	// no duration specified, use maximum allowed for this gating snap.
   230  	var holdDuration time.Duration
   231  	if err := snapstate.HoldRefresh(st, ctx.InstanceName(), holdDuration, affecting...); err != nil {
   232  		// TODO: let a snap hold again once for 1h.
   233  		return err
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  func (c *refreshCommand) proceed() error {
   240  	ctx := c.context()
   241  	ctx.Lock()
   242  	defer ctx.Unlock()
   243  
   244  	// cache the action, hook handler will trigger proceed logic; we cannot
   245  	// call snapstate.ProceedWithRefresh() immediately as this would reset
   246  	// holdState, allowing the snap to --hold with fresh duration limit.
   247  	ctx.Cache("action", snapstate.GateAutoRefreshProceed)
   248  
   249  	return nil
   250  }