github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/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  	if c.Pending {
   106  		if err := c.printPendingInfo(); err != nil {
   107  			return err
   108  		}
   109  	}
   110  
   111  	switch {
   112  	case c.Proceed:
   113  		return c.proceed()
   114  	case c.Hold:
   115  		return c.hold()
   116  	}
   117  
   118  	return nil
   119  }
   120  
   121  type updateDetails struct {
   122  	Pending  string `yaml:"pending,omitempty"`
   123  	Channel  string `yaml:"channel,omitempty"`
   124  	Version  string `yaml:"version,omitempty"`
   125  	Revision int    `yaml:"revision,omitempty"`
   126  	// TODO: epoch
   127  	Base    bool `yaml:"base"`
   128  	Restart bool `yaml:"restart"`
   129  }
   130  
   131  // refreshCandidate is a subset of refreshCandidate defined by snapstate and
   132  // stored in "refresh-candidates".
   133  type refreshCandidate struct {
   134  	Channel     string         `json:"channel,omitempty"`
   135  	Version     string         `json:"version,omitempty"`
   136  	SideInfo    *snap.SideInfo `json:"side-info,omitempty"`
   137  	InstanceKey string         `json:"instance-key,omitempty"`
   138  }
   139  
   140  func getUpdateDetails(context *hookstate.Context) (*updateDetails, error) {
   141  	context.Lock()
   142  	defer context.Unlock()
   143  
   144  	if context.IsEphemeral() {
   145  		// TODO: support ephemeral context
   146  		return nil, nil
   147  	}
   148  
   149  	var base, restart bool
   150  	context.Get("base", &base)
   151  	context.Get("restart", &restart)
   152  
   153  	var candidates map[string]*refreshCandidate
   154  	st := context.State()
   155  	if err := st.Get("refresh-candidates", &candidates); err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	var snapst snapstate.SnapState
   160  	if err := snapstate.Get(st, context.InstanceName(), &snapst); err != nil {
   161  		return nil, fmt.Errorf("internal error: cannot get snap state for %q: %v", context.InstanceName(), err)
   162  	}
   163  
   164  	var pending string
   165  	switch {
   166  	case snapst.RefreshInhibitedTime != nil:
   167  		pending = "inhibited"
   168  	case candidates[context.InstanceName()] != nil:
   169  		pending = "ready"
   170  	default:
   171  		pending = "none"
   172  	}
   173  
   174  	up := updateDetails{
   175  		Base:    base,
   176  		Restart: restart,
   177  		Pending: pending,
   178  	}
   179  
   180  	// try to find revision/version/channel info from refresh-candidates; it
   181  	// may be missing if the hook is called for snap that is just affected by
   182  	// refresh but not refreshed itself, in such case this data is not
   183  	// displayed.
   184  	if cand, ok := candidates[context.InstanceName()]; ok {
   185  		up.Channel = cand.Channel
   186  		up.Revision = cand.SideInfo.Revision.N
   187  		up.Version = cand.Version
   188  		return &up, nil
   189  	}
   190  
   191  	// refresh-hint not present, look up channel info in snapstate
   192  	up.Channel = snapst.TrackingChannel
   193  	return &up, nil
   194  }
   195  
   196  func (c *refreshCommand) printPendingInfo() error {
   197  	details, err := getUpdateDetails(c.context())
   198  	if err != nil {
   199  		return err
   200  	}
   201  	// XXX: remove when ephemeral context is supported.
   202  	if details == nil {
   203  		return nil
   204  	}
   205  	out, err := yaml.Marshal(details)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	c.printf("%s", string(out))
   210  	return nil
   211  }
   212  
   213  func (c *refreshCommand) hold() error {
   214  	ctx := c.context()
   215  	ctx.Lock()
   216  	defer ctx.Unlock()
   217  	st := ctx.State()
   218  
   219  	// cache the action so that hook handler can implement default behavior
   220  	ctx.Cache("action", snapstate.GateAutoRefreshHold)
   221  
   222  	var affecting []string
   223  	if err := ctx.Get("affecting-snaps", &affecting); err != nil {
   224  		return fmt.Errorf("internal error: cannot get affecting-snaps")
   225  	}
   226  
   227  	// no duration specified, use maximum allowed for this gating snap.
   228  	var holdDuration time.Duration
   229  	if err := snapstate.HoldRefresh(st, ctx.InstanceName(), holdDuration, affecting...); err != nil {
   230  		// TODO: let a snap hold again once for 1h.
   231  		return err
   232  	}
   233  
   234  	return nil
   235  }
   236  
   237  func (c *refreshCommand) proceed() error {
   238  	ctx := c.context()
   239  	ctx.Lock()
   240  	defer ctx.Unlock()
   241  
   242  	// cache the action, hook handler will trigger proceed logic; we cannot
   243  	// call snapstate.ProceedWithRefresh() immediately as this would reset
   244  	// holdState, allowing the snap to --hold with fresh duration limit.
   245  	ctx.Cache("action", snapstate.GateAutoRefreshProceed)
   246  
   247  	return nil
   248  }