github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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  
    25  	"gopkg.in/yaml.v2"
    26  
    27  	"github.com/snapcore/snapd/i18n"
    28  	"github.com/snapcore/snapd/overlord/hookstate"
    29  	"github.com/snapcore/snapd/overlord/snapstate"
    30  	"github.com/snapcore/snapd/snap"
    31  )
    32  
    33  type refreshCommand struct {
    34  	baseCommand
    35  
    36  	Pending bool `long:"pending" description:"Show pending refreshes of the calling snap"`
    37  	// these two options are mutually exclusive
    38  	Proceed bool `long:"proceed" description:"Proceed with potentially disruptive refreshes"`
    39  	Hold    bool `long:"hold" description:"Do not proceed with potentially disruptive refreshes"`
    40  }
    41  
    42  var shortRefreshHelp = i18n.G("The refresh command prints pending refreshes and can hold back disruptive ones.")
    43  var longRefreshHelp = i18n.G(`
    44  The refresh command prints pending refreshes of the calling snap and can hold
    45  back disruptive refreshes of other snaps, such as refreshes of the kernel or
    46  base snaps that can trigger a restart. This command can be used from the
    47  gate-auto-refresh hook which is only run during auto-refresh.
    48  
    49  Snap can query pending refreshes with:
    50      $ snapctl refresh --pending
    51      pending: ready
    52      channel: stable
    53      version: 2
    54      revision: 2
    55      base: false
    56      restart: false
    57  
    58  The 'pending' flag can be "ready", "none" or "inhibited". It is set to "none"
    59  when a snap has no pending refreshes. It is set to "ready" when there are
    60  pending refreshes and to ”inhibited” when pending refreshes are being
    61  held back because more or more snap applications are running with the
    62  “refresh app awareness” feature enabled.
    63  
    64  The "base" and "restart" flags indicate whether the base snap is going to be
    65  updated and/or if a restart will occur, both of which are disruptive. A base
    66  snap update can temporarily disrupt the starting of applications or hooks from
    67  the snap.
    68  
    69  To tell snapd to proceed with pending refreshes:
    70      $ snapctl refresh --pending --proceed
    71  
    72  Note, a snap using --proceed cannot assume that the updates will occur as they
    73  might be held back by other snaps.
    74  
    75  To hold refresh for up to 90 days for the calling snap:
    76      $ snapctl refresh --pending --hold
    77  `)
    78  
    79  func init() {
    80  	cmd := addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() command {
    81  		return &refreshCommand{}
    82  	})
    83  	cmd.hidden = true
    84  }
    85  
    86  func (c *refreshCommand) Execute(args []string) error {
    87  	context := c.context()
    88  	if context == nil {
    89  		return fmt.Errorf("cannot run without a context")
    90  	}
    91  	if context.IsEphemeral() {
    92  		// TODO: handle this
    93  		return fmt.Errorf("cannot run outside of gate-auto-refresh hook")
    94  	}
    95  
    96  	if context.HookName() != "gate-auto-refresh" {
    97  		return fmt.Errorf("can only be used from gate-auto-refresh hook")
    98  	}
    99  
   100  	if c.Proceed && c.Hold {
   101  		return fmt.Errorf("cannot use --proceed and --hold together")
   102  	}
   103  
   104  	if c.Pending {
   105  		if err := c.printPendingInfo(); err != nil {
   106  			return err
   107  		}
   108  	}
   109  
   110  	if c.Proceed {
   111  		return fmt.Errorf("not implemented yet")
   112  	}
   113  	if c.Hold {
   114  		return fmt.Errorf("not implemented yet")
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  type updateDetails struct {
   121  	Pending  string `yaml:"pending,omitempty"`
   122  	Channel  string `yaml:"channel,omitempty"`
   123  	Version  string `yaml:"version,omitempty"`
   124  	Revision int    `yaml:"revision,omitempty"`
   125  	// TODO: epoch
   126  	Base    bool `yaml:"base"`
   127  	Restart bool `yaml:"restart"`
   128  }
   129  
   130  // refreshCandidate is a subset of refreshCandidate defined by snapstate and
   131  // stored in "refresh-candidates".
   132  type refreshCandidate struct {
   133  	Channel     string         `json:"channel,omitempty"`
   134  	Version     string         `json:"version,omitempty"`
   135  	SideInfo    *snap.SideInfo `json:"side-info,omitempty"`
   136  	InstanceKey string         `json:"instance-key,omitempty"`
   137  }
   138  
   139  func getUpdateDetails(context *hookstate.Context) (*updateDetails, error) {
   140  	context.Lock()
   141  	defer context.Unlock()
   142  
   143  	if context.IsEphemeral() {
   144  		// TODO: support ephemeral context
   145  		return nil, nil
   146  	}
   147  
   148  	var base, restart bool
   149  	context.Get("base", &base)
   150  	context.Get("restart", &restart)
   151  
   152  	var candidates map[string]*refreshCandidate
   153  	st := context.State()
   154  	if err := st.Get("refresh-candidates", &candidates); err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	var snapst snapstate.SnapState
   159  	if err := snapstate.Get(st, context.InstanceName(), &snapst); err != nil {
   160  		return nil, fmt.Errorf("internal error: cannot get snap state for %q: %v", context.InstanceName(), err)
   161  	}
   162  
   163  	var pending string
   164  	switch {
   165  	case snapst.RefreshInhibitedTime != nil:
   166  		pending = "inhibited"
   167  	case candidates[context.InstanceName()] != nil:
   168  		pending = "ready"
   169  	default:
   170  		pending = "none"
   171  	}
   172  
   173  	up := updateDetails{
   174  		Base:    base,
   175  		Restart: restart,
   176  		Pending: pending,
   177  	}
   178  
   179  	// try to find revision/version/channel info from refresh-candidates; it
   180  	// may be missing if the hook is called for snap that is just affected by
   181  	// refresh but not refreshed itself, in such case this data is not
   182  	// displayed.
   183  	if cand, ok := candidates[context.InstanceName()]; ok {
   184  		up.Channel = cand.Channel
   185  		up.Revision = cand.SideInfo.Revision.N
   186  		up.Version = cand.Version
   187  		return &up, nil
   188  	}
   189  
   190  	// refresh-hint not present, look up channel info in snapstate
   191  	up.Channel = snapst.TrackingChannel
   192  	return &up, nil
   193  }
   194  
   195  func (c *refreshCommand) printPendingInfo() error {
   196  	details, err := getUpdateDetails(c.context())
   197  	if err != nil {
   198  		return err
   199  	}
   200  	// XXX: remove when ephemeral context is supported.
   201  	if details == nil {
   202  		return nil
   203  	}
   204  	out, err := yaml.Marshal(details)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	c.printf("%s", string(out))
   209  	return nil
   210  }