gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/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/overlord/state" 34 "github.com/snapcore/snapd/snap" 35 ) 36 37 var autoRefreshForGatingSnap = snapstate.AutoRefreshForGatingSnap 38 39 type refreshCommand struct { 40 baseCommand 41 42 Pending bool `long:"pending" description:"Show pending refreshes of the calling snap"` 43 // these two options are mutually exclusive 44 Proceed bool `long:"proceed" description:"Proceed with potentially disruptive refreshes"` 45 Hold bool `long:"hold" description:"Do not proceed with potentially disruptive refreshes"` 46 } 47 48 var shortRefreshHelp = i18n.G("The refresh command prints pending refreshes and can hold back disruptive ones.") 49 var longRefreshHelp = i18n.G(` 50 The refresh command prints pending refreshes of the calling snap and can hold 51 back disruptive refreshes of other snaps, such as refreshes of the kernel or 52 base snaps that can trigger a restart. This command can be used from the 53 gate-auto-refresh hook which is only run during auto-refresh. 54 55 Snap can query pending refreshes with: 56 $ snapctl refresh --pending 57 pending: ready 58 channel: stable 59 version: 2 60 revision: 2 61 base: false 62 restart: false 63 64 The 'pending' flag can be "ready", "none" or "inhibited". It is set to "none" 65 when a snap has no pending refreshes. It is set to "ready" when there are 66 pending refreshes and to ”inhibited” when pending refreshes are being 67 held back because more or more snap applications are running with the 68 “refresh app awareness” feature enabled. 69 70 The "base" and "restart" flags indicate whether the base snap is going to be 71 updated and/or if a restart will occur, both of which are disruptive. A base 72 snap update can temporarily disrupt the starting of applications or hooks from 73 the snap. 74 75 To tell snapd to proceed with pending refreshes: 76 $ snapctl refresh --pending --proceed 77 78 Note, a snap using --proceed cannot assume that the updates will occur as they 79 might be held back by other snaps. 80 81 To hold refresh for up to 90 days for the calling snap: 82 $ snapctl refresh --pending --hold 83 `) 84 85 func init() { 86 cmd := addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() command { 87 return &refreshCommand{} 88 }) 89 cmd.hidden = true 90 } 91 92 func (c *refreshCommand) Execute(args []string) error { 93 context, err := c.ensureContext() 94 if err != nil { 95 return err 96 } 97 98 if !context.IsEphemeral() && context.HookName() != "gate-auto-refresh" { 99 return fmt.Errorf("can only be used from gate-auto-refresh hook") 100 } 101 102 if c.Proceed && c.Hold { 103 return fmt.Errorf("cannot use --proceed and --hold together") 104 } 105 106 // --pending --proceed is a verbose way of saying --proceed, so only 107 // print pending if proceed wasn't requested. 108 if c.Pending && !c.Proceed { 109 if err := c.printPendingInfo(); err != nil { 110 return err 111 } 112 } 113 114 switch { 115 case c.Proceed: 116 return c.proceed() 117 case c.Hold: 118 return c.hold() 119 } 120 121 return nil 122 } 123 124 type updateDetails struct { 125 Pending string `yaml:"pending,omitempty"` 126 Channel string `yaml:"channel,omitempty"` 127 Version string `yaml:"version,omitempty"` 128 Revision int `yaml:"revision,omitempty"` 129 // TODO: epoch 130 Base bool `yaml:"base"` 131 Restart bool `yaml:"restart"` 132 } 133 134 // refreshCandidate is a subset of refreshCandidate defined by snapstate and 135 // stored in "refresh-candidates". 136 type refreshCandidate struct { 137 Channel string `json:"channel,omitempty"` 138 Version string `json:"version,omitempty"` 139 SideInfo *snap.SideInfo `json:"side-info,omitempty"` 140 InstanceKey string `json:"instance-key,omitempty"` 141 } 142 143 func getUpdateDetails(context *hookstate.Context) (*updateDetails, error) { 144 context.Lock() 145 defer context.Unlock() 146 147 st := context.State() 148 149 affected, err := snapstate.AffectedByRefreshCandidates(st) 150 if err != nil { 151 return nil, err 152 } 153 154 var base, restart bool 155 if affectedInfo, ok := affected[context.InstanceName()]; ok { 156 base = affectedInfo.Base 157 restart = affectedInfo.Restart 158 } 159 160 var snapst snapstate.SnapState 161 if err := snapstate.Get(st, context.InstanceName(), &snapst); err != nil { 162 return nil, fmt.Errorf("internal error: cannot get snap state for %q: %v", context.InstanceName(), err) 163 } 164 165 var candidates map[string]*refreshCandidate 166 if err := st.Get("refresh-candidates", &candidates); err != nil && err != state.ErrNoState { 167 return nil, err 168 } 169 170 var pending string 171 switch { 172 case snapst.RefreshInhibitedTime != nil: 173 pending = "inhibited" 174 case candidates[context.InstanceName()] != nil: 175 pending = "ready" 176 default: 177 pending = "none" 178 } 179 180 up := updateDetails{ 181 Base: base, 182 Restart: restart, 183 Pending: pending, 184 } 185 186 // try to find revision/version/channel info from refresh-candidates; it 187 // may be missing if the hook is called for snap that is just affected by 188 // refresh but not refreshed itself, in such case this data is not 189 // displayed. 190 if cand, ok := candidates[context.InstanceName()]; ok { 191 up.Channel = cand.Channel 192 up.Revision = cand.SideInfo.Revision.N 193 up.Version = cand.Version 194 return &up, nil 195 } 196 197 // refresh-hint not present, look up channel info in snapstate 198 up.Channel = snapst.TrackingChannel 199 return &up, nil 200 } 201 202 func (c *refreshCommand) printPendingInfo() error { 203 details, err := getUpdateDetails(c.context()) 204 if err != nil { 205 return err 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 affecting, err := snapstate.AffectingSnapsForAffectedByRefreshCandidates(st, ctx.InstanceName()) 228 if err != nil { 229 return err 230 } 231 if len(affecting) == 0 { 232 // this shouldn't happen because the hook is executed during auto-refresh 233 // change which conflicts with other changes (if it happens that means 234 // something changed in the meantime and we didn't handle conflicts 235 // correctly). 236 return fmt.Errorf("internal error: snap %q is not affected by any snaps", ctx.InstanceName()) 237 } 238 239 // no duration specified, use maximum allowed for this gating snap. 240 var holdDuration time.Duration 241 if err := snapstate.HoldRefresh(st, ctx.InstanceName(), holdDuration, affecting...); err != nil { 242 // TODO: let a snap hold again once for 1h. 243 return err 244 } 245 246 return nil 247 } 248 249 func (c *refreshCommand) proceed() error { 250 ctx := c.context() 251 ctx.Lock() 252 defer ctx.Unlock() 253 254 // running outside of hook 255 if ctx.IsEphemeral() { 256 // TODO: consider having a permission via an interface for this before making this not experimental 257 st := ctx.State() 258 // we need to check if GateAutoRefreshHook feature is enabled when 259 // running by the snap (we don't need to do this when running from the 260 // hook because in that case hook task won't be created if not enabled). 261 tr := config.NewTransaction(st) 262 gateAutoRefreshHook, err := features.Flag(tr, features.GateAutoRefreshHook) 263 if err != nil && !config.IsNoOption(err) { 264 return err 265 } 266 if !gateAutoRefreshHook { 267 return fmt.Errorf("cannot proceed without experimental.gate-auto-refresh feature enabled") 268 } 269 270 return autoRefreshForGatingSnap(st, ctx.InstanceName()) 271 } 272 273 // cache the action, hook handler will trigger proceed logic; we cannot 274 // call snapstate.ProceedWithRefresh() immediately as this would reset 275 // holdState, allowing the snap to --hold with fresh duration limit. 276 ctx.Cache("action", snapstate.GateAutoRefreshProceed) 277 278 return nil 279 }