github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/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 if c.Pending { 106 if err := c.printPendingInfo(); err != nil { 107 return err 108 } 109 } 110 111 switch { 112 case c.Proceed: 113 return c.proceed() 114 case c.Hold: 115 return c.hold() 116 } 117 118 return nil 119 } 120 121 type updateDetails struct { 122 Pending string `yaml:"pending,omitempty"` 123 Channel string `yaml:"channel,omitempty"` 124 Version string `yaml:"version,omitempty"` 125 Revision int `yaml:"revision,omitempty"` 126 // TODO: epoch 127 Base bool `yaml:"base"` 128 Restart bool `yaml:"restart"` 129 } 130 131 // refreshCandidate is a subset of refreshCandidate defined by snapstate and 132 // stored in "refresh-candidates". 133 type refreshCandidate struct { 134 Channel string `json:"channel,omitempty"` 135 Version string `json:"version,omitempty"` 136 SideInfo *snap.SideInfo `json:"side-info,omitempty"` 137 InstanceKey string `json:"instance-key,omitempty"` 138 } 139 140 func getUpdateDetails(context *hookstate.Context) (*updateDetails, error) { 141 context.Lock() 142 defer context.Unlock() 143 144 if context.IsEphemeral() { 145 // TODO: support ephemeral context 146 return nil, nil 147 } 148 149 var base, restart bool 150 context.Get("base", &base) 151 context.Get("restart", &restart) 152 153 var candidates map[string]*refreshCandidate 154 st := context.State() 155 if err := st.Get("refresh-candidates", &candidates); err != nil { 156 return nil, err 157 } 158 159 var snapst snapstate.SnapState 160 if err := snapstate.Get(st, context.InstanceName(), &snapst); err != nil { 161 return nil, fmt.Errorf("internal error: cannot get snap state for %q: %v", context.InstanceName(), err) 162 } 163 164 var pending string 165 switch { 166 case snapst.RefreshInhibitedTime != nil: 167 pending = "inhibited" 168 case candidates[context.InstanceName()] != nil: 169 pending = "ready" 170 default: 171 pending = "none" 172 } 173 174 up := updateDetails{ 175 Base: base, 176 Restart: restart, 177 Pending: pending, 178 } 179 180 // try to find revision/version/channel info from refresh-candidates; it 181 // may be missing if the hook is called for snap that is just affected by 182 // refresh but not refreshed itself, in such case this data is not 183 // displayed. 184 if cand, ok := candidates[context.InstanceName()]; ok { 185 up.Channel = cand.Channel 186 up.Revision = cand.SideInfo.Revision.N 187 up.Version = cand.Version 188 return &up, nil 189 } 190 191 // refresh-hint not present, look up channel info in snapstate 192 up.Channel = snapst.TrackingChannel 193 return &up, nil 194 } 195 196 func (c *refreshCommand) printPendingInfo() error { 197 details, err := getUpdateDetails(c.context()) 198 if err != nil { 199 return err 200 } 201 // XXX: remove when ephemeral context is supported. 202 if details == nil { 203 return nil 204 } 205 out, err := yaml.Marshal(details) 206 if err != nil { 207 return err 208 } 209 c.printf("%s", string(out)) 210 return nil 211 } 212 213 func (c *refreshCommand) hold() error { 214 ctx := c.context() 215 ctx.Lock() 216 defer ctx.Unlock() 217 st := ctx.State() 218 219 // cache the action so that hook handler can implement default behavior 220 ctx.Cache("action", snapstate.GateAutoRefreshHold) 221 222 var affecting []string 223 if err := ctx.Get("affecting-snaps", &affecting); err != nil { 224 return fmt.Errorf("internal error: cannot get affecting-snaps") 225 } 226 227 // no duration specified, use maximum allowed for this gating snap. 228 var holdDuration time.Duration 229 if err := snapstate.HoldRefresh(st, ctx.InstanceName(), holdDuration, affecting...); err != nil { 230 // TODO: let a snap hold again once for 1h. 231 return err 232 } 233 234 return nil 235 } 236 237 func (c *refreshCommand) proceed() error { 238 ctx := c.context() 239 ctx.Lock() 240 defer ctx.Unlock() 241 242 // cache the action, hook handler will trigger proceed logic; we cannot 243 // call snapstate.ProceedWithRefresh() immediately as this would reset 244 // holdState, allowing the snap to --hold with fresh duration limit. 245 ctx.Cache("action", snapstate.GateAutoRefreshProceed) 246 247 return nil 248 }