github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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 // --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 ctx.Lock() 218 defer ctx.Unlock() 219 st := ctx.State() 220 221 // cache the action so that hook handler can implement default behavior 222 ctx.Cache("action", snapstate.GateAutoRefreshHold) 223 224 var affecting []string 225 if err := ctx.Get("affecting-snaps", &affecting); err != nil { 226 return fmt.Errorf("internal error: cannot get affecting-snaps") 227 } 228 229 // no duration specified, use maximum allowed for this gating snap. 230 var holdDuration time.Duration 231 if err := snapstate.HoldRefresh(st, ctx.InstanceName(), holdDuration, affecting...); err != nil { 232 // TODO: let a snap hold again once for 1h. 233 return err 234 } 235 236 return nil 237 } 238 239 func (c *refreshCommand) proceed() error { 240 ctx := c.context() 241 ctx.Lock() 242 defer ctx.Unlock() 243 244 // cache the action, hook handler will trigger proceed logic; we cannot 245 // call snapstate.ProceedWithRefresh() immediately as this would reset 246 // holdState, allowing the snap to --hold with fresh duration limit. 247 ctx.Cache("action", snapstate.GateAutoRefreshProceed) 248 249 return nil 250 }