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