github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/hookstate/hooks_test.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 hookstate_test 21 22 import ( 23 "fmt" 24 "strings" 25 "time" 26 27 . "gopkg.in/check.v1" 28 "gopkg.in/tomb.v2" 29 30 "github.com/snapcore/snapd/cmd/snaplock/runinhibit" 31 "github.com/snapcore/snapd/overlord/configstate/config" 32 "github.com/snapcore/snapd/overlord/hookstate" 33 "github.com/snapcore/snapd/overlord/snapstate" 34 "github.com/snapcore/snapd/overlord/state" 35 "github.com/snapcore/snapd/snap" 36 "github.com/snapcore/snapd/snap/snaptest" 37 "github.com/snapcore/snapd/testutil" 38 ) 39 40 const snapaYaml = `name: snap-a 41 version: 1 42 hooks: 43 gate-auto-refresh: 44 ` 45 46 const snapbYaml = `name: snap-b 47 version: 1 48 ` 49 50 type gateAutoRefreshHookSuite struct { 51 baseHookManagerSuite 52 } 53 54 var _ = Suite(&gateAutoRefreshHookSuite{}) 55 56 func (s *gateAutoRefreshHookSuite) SetUpTest(c *C) { 57 s.commonSetUpTest(c) 58 59 s.state.Lock() 60 defer s.state.Unlock() 61 62 si := &snap.SideInfo{RealName: "snap-a", SnapID: "snap-a-id1", Revision: snap.R(1)} 63 snaptest.MockSnap(c, snapaYaml, si) 64 snapstate.Set(s.state, "snap-a", &snapstate.SnapState{ 65 Active: true, 66 Sequence: []*snap.SideInfo{si}, 67 Current: snap.R(1), 68 }) 69 70 si2 := &snap.SideInfo{RealName: "snap-b", SnapID: "snap-b-id1", Revision: snap.R(1)} 71 snaptest.MockSnap(c, snapbYaml, si2) 72 snapstate.Set(s.state, "snap-b", &snapstate.SnapState{ 73 Active: true, 74 Sequence: []*snap.SideInfo{si2}, 75 Current: snap.R(1), 76 }) 77 } 78 79 func (s *gateAutoRefreshHookSuite) TearDownTest(c *C) { 80 s.commonTearDownTest(c) 81 } 82 83 func (s *gateAutoRefreshHookSuite) settle(c *C) { 84 err := s.o.Settle(5 * time.Second) 85 c.Assert(err, IsNil) 86 } 87 88 func checkIsHeld(c *C, st *state.State, heldSnap, gatingSnap string) { 89 var held map[string]map[string]interface{} 90 c.Assert(st.Get("snaps-hold", &held), IsNil) 91 c.Check(held[heldSnap][gatingSnap], NotNil) 92 } 93 94 func checkIsNotHeld(c *C, st *state.State, heldSnap string) { 95 var held map[string]map[string]interface{} 96 c.Assert(st.Get("snaps-hold", &held), IsNil) 97 c.Check(held[heldSnap], IsNil) 98 } 99 100 func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookProceedRuninhibitLock(c *C) { 101 hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { 102 c.Check(ctx.HookName(), Equals, "gate-auto-refresh") 103 c.Check(ctx.InstanceName(), Equals, "snap-a") 104 ctx.Lock() 105 defer ctx.Unlock() 106 107 // check that runinhibit hint has been set by Before() hook handler. 108 hint, err := runinhibit.IsLocked("snap-a") 109 c.Assert(err, IsNil) 110 c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh) 111 112 // action is normally set via snapctl; pretend it is --proceed. 113 action := snapstate.GateAutoRefreshProceed 114 ctx.Cache("action", action) 115 return nil, nil 116 } 117 restore := hookstate.MockRunHook(hookInvoke) 118 defer restore() 119 120 st := s.state 121 st.Lock() 122 defer st.Unlock() 123 124 // enable refresh-app-awareness 125 tr := config.NewTransaction(st) 126 tr.Set("core", "experimental.refresh-app-awareness", true) 127 tr.Commit() 128 129 task := hookstate.SetupGateAutoRefreshHook(st, "snap-a", false, false, map[string]bool{"snap-b": true}) 130 change := st.NewChange("kind", "summary") 131 change.AddTask(task) 132 133 st.Unlock() 134 s.settle(c) 135 st.Lock() 136 137 c.Assert(change.Err(), IsNil) 138 c.Assert(change.Status(), Equals, state.DoneStatus) 139 140 hint, err := runinhibit.IsLocked("snap-a") 141 c.Assert(err, IsNil) 142 c.Check(hint, Equals, runinhibit.HintInhibitedForRefresh) 143 } 144 145 func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookHoldUnlocksRuninhibit(c *C) { 146 hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { 147 c.Check(ctx.HookName(), Equals, "gate-auto-refresh") 148 c.Check(ctx.InstanceName(), Equals, "snap-a") 149 ctx.Lock() 150 defer ctx.Unlock() 151 152 // check that runinhibit hint has been set by Before() hook handler. 153 hint, err := runinhibit.IsLocked("snap-a") 154 c.Assert(err, IsNil) 155 c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh) 156 157 // action is normally set via snapctl; pretend it is --hold. 158 action := snapstate.GateAutoRefreshHold 159 ctx.Cache("action", action) 160 return nil, nil 161 } 162 restore := hookstate.MockRunHook(hookInvoke) 163 defer restore() 164 165 st := s.state 166 st.Lock() 167 defer st.Unlock() 168 169 // enable refresh-app-awareness 170 tr := config.NewTransaction(st) 171 tr.Set("core", "experimental.refresh-app-awareness", true) 172 tr.Commit() 173 174 task := hookstate.SetupGateAutoRefreshHook(st, "snap-a", false, false, map[string]bool{"snap-b": true}) 175 change := st.NewChange("kind", "summary") 176 change.AddTask(task) 177 178 st.Unlock() 179 s.settle(c) 180 st.Lock() 181 182 c.Assert(change.Err(), IsNil) 183 c.Assert(change.Status(), Equals, state.DoneStatus) 184 185 // runinhibit lock is released. 186 hint, err := runinhibit.IsLocked("snap-a") 187 c.Assert(err, IsNil) 188 c.Check(hint, Equals, runinhibit.HintNotInhibited) 189 } 190 191 // Test that if gate-auto-refresh hook does nothing, the hook handler 192 // assumes --proceed. 193 func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceedUnlocksRuninhibit(c *C) { 194 hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { 195 // sanity, refresh is inhibited for snap-a. 196 hint, err := runinhibit.IsLocked("snap-a") 197 c.Assert(err, IsNil) 198 c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh) 199 200 // this hook does nothing (action not set to proceed/hold). 201 c.Check(ctx.HookName(), Equals, "gate-auto-refresh") 202 c.Check(ctx.InstanceName(), Equals, "snap-a") 203 return nil, nil 204 } 205 restore := hookstate.MockRunHook(hookInvoke) 206 defer restore() 207 208 st := s.state 209 st.Lock() 210 defer st.Unlock() 211 212 // pretend that snap-a is initially held by itself. 213 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil) 214 // sanity 215 checkIsHeld(c, st, "snap-a", "snap-a") 216 217 // enable refresh-app-awareness 218 tr := config.NewTransaction(st) 219 tr.Set("core", "experimental.refresh-app-awareness", true) 220 tr.Commit() 221 222 task := hookstate.SetupGateAutoRefreshHook(st, "snap-a", false, false, map[string]bool{"snap-a": true}) 223 change := st.NewChange("kind", "summary") 224 change.AddTask(task) 225 226 st.Unlock() 227 s.settle(c) 228 st.Lock() 229 230 c.Assert(change.Err(), IsNil) 231 c.Assert(change.Status(), Equals, state.DoneStatus) 232 233 checkIsNotHeld(c, st, "snap-a") 234 235 // runinhibit lock is released. 236 hint, err := runinhibit.IsLocked("snap-a") 237 c.Assert(err, IsNil) 238 c.Check(hint, Equals, runinhibit.HintNotInhibited) 239 } 240 241 // Test that if gate-auto-refresh hook does nothing, the hook handler 242 // assumes --proceed. 243 func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceed(c *C) { 244 hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { 245 // no runinhibit because the refresh-app-awareness feature is disabled. 246 hint, err := runinhibit.IsLocked("snap-a") 247 c.Assert(err, IsNil) 248 c.Check(hint, Equals, runinhibit.HintNotInhibited) 249 250 // this hook does nothing (action not set to proceed/hold). 251 c.Check(ctx.HookName(), Equals, "gate-auto-refresh") 252 c.Check(ctx.InstanceName(), Equals, "snap-a") 253 return nil, nil 254 } 255 restore := hookstate.MockRunHook(hookInvoke) 256 defer restore() 257 258 st := s.state 259 st.Lock() 260 defer st.Unlock() 261 262 // pretend that snap-b is initially held by snap-a. 263 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b"), IsNil) 264 // sanity 265 checkIsHeld(c, st, "snap-b", "snap-a") 266 267 task := hookstate.SetupGateAutoRefreshHook(st, "snap-a", false, false, map[string]bool{"snap-b": true}) 268 change := st.NewChange("kind", "summary") 269 change.AddTask(task) 270 271 st.Unlock() 272 s.settle(c) 273 st.Lock() 274 275 c.Assert(change.Err(), IsNil) 276 c.Assert(change.Status(), Equals, state.DoneStatus) 277 278 checkIsNotHeld(c, st, "snap-b") 279 280 // no runinhibit because the refresh-app-awareness feature is disabled. 281 hint, err := runinhibit.IsLocked("snap-a") 282 c.Assert(err, IsNil) 283 c.Check(hint, Equals, runinhibit.HintNotInhibited) 284 } 285 286 // Test that if gate-auto-refresh hook errors out, the hook handler 287 // assumes --hold. 288 func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookError(c *C) { 289 hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { 290 // no runinhibit because the refresh-app-awareness feature is disabled. 291 hint, err := runinhibit.IsLocked("snap-a") 292 c.Assert(err, IsNil) 293 c.Check(hint, Equals, runinhibit.HintNotInhibited) 294 295 // this hook does nothing (action not set to proceed/hold). 296 c.Check(ctx.HookName(), Equals, "gate-auto-refresh") 297 c.Check(ctx.InstanceName(), Equals, "snap-a") 298 return []byte("fail"), fmt.Errorf("boom") 299 } 300 restore := hookstate.MockRunHook(hookInvoke) 301 defer restore() 302 303 st := s.state 304 st.Lock() 305 defer st.Unlock() 306 307 task := hookstate.SetupGateAutoRefreshHook(st, "snap-a", false, false, map[string]bool{"snap-b": true}) 308 change := st.NewChange("kind", "summary") 309 change.AddTask(task) 310 311 st.Unlock() 312 s.settle(c) 313 st.Lock() 314 315 c.Assert(strings.Join(task.Log(), ""), testutil.Contains, "ignoring hook error: fail") 316 c.Assert(change.Status(), Equals, state.DoneStatus) 317 318 // and snap-b is now held. 319 checkIsHeld(c, st, "snap-b", "snap-a") 320 321 // no runinhibit because the refresh-app-awareness feature is disabled. 322 hint, err := runinhibit.IsLocked("snap-a") 323 c.Assert(err, IsNil) 324 c.Check(hint, Equals, runinhibit.HintNotInhibited) 325 } 326 327 // Test that if gate-auto-refresh hook errors out, the hook handler 328 // assumes --hold even if --proceed was requested. 329 func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorAfterProceed(c *C) { 330 hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { 331 // no runinhibit because the refresh-app-awareness feature is disabled. 332 hint, err := runinhibit.IsLocked("snap-a") 333 c.Assert(err, IsNil) 334 c.Check(hint, Equals, runinhibit.HintNotInhibited) 335 336 c.Check(ctx.HookName(), Equals, "gate-auto-refresh") 337 c.Check(ctx.InstanceName(), Equals, "snap-a") 338 339 // action is normally set via snapctl; pretend it is --proceed. 340 ctx.Lock() 341 defer ctx.Unlock() 342 action := snapstate.GateAutoRefreshProceed 343 ctx.Cache("action", action) 344 345 return []byte("fail"), fmt.Errorf("boom") 346 } 347 restore := hookstate.MockRunHook(hookInvoke) 348 defer restore() 349 350 st := s.state 351 st.Lock() 352 defer st.Unlock() 353 354 task := hookstate.SetupGateAutoRefreshHook(st, "snap-a", false, false, map[string]bool{"snap-b": true}) 355 change := st.NewChange("kind", "summary") 356 change.AddTask(task) 357 358 st.Unlock() 359 s.settle(c) 360 st.Lock() 361 362 c.Assert(strings.Join(task.Log(), ""), testutil.Contains, "ignoring hook error: fail") 363 c.Assert(change.Status(), Equals, state.DoneStatus) 364 365 // and snap-b is now held. 366 checkIsHeld(c, st, "snap-b", "snap-a") 367 368 // no runinhibit because the refresh-app-awareness feature is disabled. 369 hint, err := runinhibit.IsLocked("snap-a") 370 c.Assert(err, IsNil) 371 c.Check(hint, Equals, runinhibit.HintNotInhibited) 372 } 373 374 // Test that if gate-auto-refresh hook errors out, the hook handler 375 // assumes --hold. 376 func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorRuninhibitUnlock(c *C) { 377 hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { 378 // no runinhibit because the refresh-app-awareness feature is disabled. 379 hint, err := runinhibit.IsLocked("snap-a") 380 c.Assert(err, IsNil) 381 c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh) 382 383 // this hook does nothing (action not set to proceed/hold). 384 c.Check(ctx.HookName(), Equals, "gate-auto-refresh") 385 c.Check(ctx.InstanceName(), Equals, "snap-a") 386 return []byte("fail"), fmt.Errorf("boom") 387 } 388 restore := hookstate.MockRunHook(hookInvoke) 389 defer restore() 390 391 st := s.state 392 st.Lock() 393 defer st.Unlock() 394 395 // enable refresh-app-awareness 396 tr := config.NewTransaction(st) 397 tr.Set("core", "experimental.refresh-app-awareness", true) 398 tr.Commit() 399 400 task := hookstate.SetupGateAutoRefreshHook(st, "snap-a", false, false, map[string]bool{"snap-b": true}) 401 change := st.NewChange("kind", "summary") 402 change.AddTask(task) 403 404 st.Unlock() 405 s.settle(c) 406 st.Lock() 407 408 c.Assert(strings.Join(task.Log(), ""), testutil.Contains, "ignoring hook error: fail") 409 c.Assert(change.Status(), Equals, state.DoneStatus) 410 411 // and snap-b is now held. 412 checkIsHeld(c, st, "snap-b", "snap-a") 413 414 // inhibit lock is unlocked 415 hint, err := runinhibit.IsLocked("snap-a") 416 c.Assert(err, IsNil) 417 c.Check(hint, Equals, runinhibit.HintNotInhibited) 418 } 419 420 func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorHoldErrorLogged(c *C) { 421 hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { 422 // no runinhibit because the refresh-app-awareness feature is disabled. 423 hint, err := runinhibit.IsLocked("snap-a") 424 c.Assert(err, IsNil) 425 c.Check(hint, Equals, runinhibit.HintNotInhibited) 426 427 // this hook does nothing (action not set to proceed/hold). 428 c.Check(ctx.HookName(), Equals, "gate-auto-refresh") 429 c.Check(ctx.InstanceName(), Equals, "snap-a") 430 431 // simulate failing hook 432 return []byte("fail"), fmt.Errorf("boom") 433 } 434 restore := hookstate.MockRunHook(hookInvoke) 435 defer restore() 436 437 st := s.state 438 st.Lock() 439 defer st.Unlock() 440 441 task := hookstate.SetupGateAutoRefreshHook(st, "snap-a", false, false, map[string]bool{"snap-b": true}) 442 change := st.NewChange("kind", "summary") 443 change.AddTask(task) 444 445 // pretend snap-b wasn't updated for a very long time. 446 var snapst snapstate.SnapState 447 c.Assert(snapstate.Get(st, "snap-b", &snapst), IsNil) 448 t := time.Now().Add(-365 * 24 * time.Hour) 449 snapst.LastRefreshTime = &t 450 snapstate.Set(st, "snap-b", &snapst) 451 452 st.Unlock() 453 s.settle(c) 454 st.Lock() 455 456 c.Assert(strings.Join(task.Log(), ""), Matches, `.*error: cannot hold some snaps: 457 - snap "snap-a" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded \(while handling previous hook error: fail\)`) 458 c.Assert(change.Status(), Equals, state.DoneStatus) 459 460 // and snap-b is not held (due to hold error). 461 var held map[string]map[string]interface{} 462 c.Assert(st.Get("snaps-hold", &held), Equals, state.ErrNoState) 463 464 // no runinhibit because the refresh-app-awareness feature is disabled. 465 hint, err := runinhibit.IsLocked("snap-a") 466 c.Assert(err, IsNil) 467 c.Check(hint, Equals, runinhibit.HintNotInhibited) 468 }