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 }