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 }