github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/overlord/snapstate/autorefresh_gating_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 snapstate_test 21 22 import ( 23 "context" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "time" 29 30 . "gopkg.in/check.v1" 31 "gopkg.in/tomb.v2" 32 33 "github.com/snapcore/snapd/dirs" 34 "github.com/snapcore/snapd/interfaces" 35 "github.com/snapcore/snapd/interfaces/builtin" 36 "github.com/snapcore/snapd/logger" 37 "github.com/snapcore/snapd/osutil" 38 "github.com/snapcore/snapd/overlord/auth" 39 "github.com/snapcore/snapd/overlord/configstate/config" 40 "github.com/snapcore/snapd/overlord/hookstate" 41 "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" 42 "github.com/snapcore/snapd/overlord/snapstate" 43 "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" 44 "github.com/snapcore/snapd/overlord/state" 45 "github.com/snapcore/snapd/release" 46 "github.com/snapcore/snapd/snap" 47 "github.com/snapcore/snapd/snap/snaptest" 48 "github.com/snapcore/snapd/store" 49 "github.com/snapcore/snapd/testutil" 50 ) 51 52 type autoRefreshGatingStore struct { 53 *fakeStore 54 refreshedSnaps []*snap.Info 55 } 56 57 type autorefreshGatingSuite struct { 58 testutil.BaseTest 59 state *state.State 60 repo *interfaces.Repository 61 store *autoRefreshGatingStore 62 } 63 64 var _ = Suite(&autorefreshGatingSuite{}) 65 66 func (s *autorefreshGatingSuite) SetUpTest(c *C) { 67 s.BaseTest.SetUpTest(c) 68 dirs.SetRootDir(c.MkDir()) 69 s.AddCleanup(func() { 70 dirs.SetRootDir("/") 71 }) 72 s.state = state.New(nil) 73 74 s.repo = interfaces.NewRepository() 75 for _, iface := range builtin.Interfaces() { 76 c.Assert(s.repo.AddInterface(iface), IsNil) 77 } 78 79 s.state.Lock() 80 defer s.state.Unlock() 81 ifacerepo.Replace(s.state, s.repo) 82 83 s.store = &autoRefreshGatingStore{fakeStore: &fakeStore{}} 84 snapstate.ReplaceStore(s.state, s.store) 85 s.state.Set("refresh-privacy-key", "privacy-key") 86 } 87 88 func (r *autoRefreshGatingStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { 89 if assertQuery != nil { 90 panic("no assertion query support") 91 } 92 if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 { 93 panic("expected in test one action for each current snaps, and at least one snap") 94 } 95 for _, a := range actions { 96 if a.Action != "refresh" { 97 panic("expected refresh actions") 98 } 99 } 100 101 res := []store.SnapActionResult{} 102 for _, rs := range r.refreshedSnaps { 103 res = append(res, store.SnapActionResult{Info: rs}) 104 } 105 106 return res, nil, nil 107 } 108 109 func mockInstalledSnap(c *C, st *state.State, snapYaml string, hasHook bool) *snap.Info { 110 snapInfo := snaptest.MockSnap(c, string(snapYaml), &snap.SideInfo{ 111 Revision: snap.R(1), 112 }) 113 114 snapName := snapInfo.SnapName() 115 si := &snap.SideInfo{RealName: snapName, SnapID: "id", Revision: snap.R(1)} 116 snapstate.Set(st, snapName, &snapstate.SnapState{ 117 Active: true, 118 Sequence: []*snap.SideInfo{si}, 119 Current: si.Revision, 120 SnapType: string(snapInfo.Type()), 121 }) 122 123 if hasHook { 124 c.Assert(os.MkdirAll(snapInfo.HooksDir(), 0775), IsNil) 125 err := ioutil.WriteFile(filepath.Join(snapInfo.HooksDir(), "gate-auto-refresh"), nil, 0755) 126 c.Assert(err, IsNil) 127 } 128 return snapInfo 129 } 130 131 func mockLastRefreshed(c *C, st *state.State, refreshedTime string, snaps ...string) { 132 refreshed, err := time.Parse(time.RFC3339, refreshedTime) 133 c.Assert(err, IsNil) 134 for _, snapName := range snaps { 135 var snapst snapstate.SnapState 136 c.Assert(snapstate.Get(st, snapName, &snapst), IsNil) 137 snapst.LastRefreshTime = &refreshed 138 snapstate.Set(st, snapName, &snapst) 139 } 140 } 141 142 const baseSnapAyaml = `name: base-snap-a 143 type: base 144 ` 145 146 const snapAyaml = `name: snap-a 147 type: app 148 base: base-snap-a 149 ` 150 151 const baseSnapByaml = `name: base-snap-b 152 type: base 153 ` 154 155 const snapByaml = `name: snap-b 156 type: app 157 base: base-snap-b 158 version: 1 159 ` 160 161 const snapBByaml = `name: snap-bb 162 type: app 163 base: base-snap-b 164 version: 1 165 ` 166 167 const kernelYaml = `name: kernel 168 type: kernel 169 version: 1 170 ` 171 172 const gadget1Yaml = `name: gadget 173 type: gadget 174 version: 1 175 ` 176 177 const snapCyaml = `name: snap-c 178 type: app 179 version: 1 180 ` 181 182 const snapDyaml = `name: snap-d 183 type: app 184 version: 1 185 slots: 186 slot: desktop 187 ` 188 189 const snapEyaml = `name: snap-e 190 type: app 191 version: 1 192 base: other-base 193 plugs: 194 plug: desktop 195 ` 196 197 const snapFyaml = `name: snap-f 198 type: app 199 version: 1 200 plugs: 201 plug: desktop 202 ` 203 204 const snapGyaml = `name: snap-g 205 type: app 206 version: 1 207 base: other-base 208 plugs: 209 desktop: 210 mir: 211 ` 212 213 const coreYaml = `name: core 214 type: os 215 version: 1 216 slots: 217 desktop: 218 mir: 219 ` 220 221 const core18Yaml = `name: core18 222 type: os 223 version: 1 224 ` 225 226 const snapdYaml = `name: snapd 227 version: 1 228 type: snapd 229 slots: 230 desktop: 231 ` 232 233 func (s *autorefreshGatingSuite) TestHoldDurationLeft(c *C) { 234 now, err := time.Parse(time.RFC3339, "2021-06-03T10:00:00Z") 235 c.Assert(err, IsNil) 236 maxPostponement := time.Hour * 24 * 90 237 238 for i, tc := range []struct { 239 lastRefresh, firstHeld string 240 maxDuration string 241 expected string 242 }{ 243 { 244 "2021-05-03T10:00:00Z", // last refreshed (1 month ago) 245 "2021-06-03T10:00:00Z", // first held now 246 "48h", // max duration 247 "48h", // expected 248 }, 249 { 250 "2021-05-03T10:00:00Z", // last refreshed (1 month ago) 251 "2021-06-02T10:00:00Z", // first held (1 day ago) 252 "48h", // max duration 253 "24h", // expected 254 }, 255 { 256 "2021-05-03T10:00:00Z", // last refreshed (1 month ago) 257 "2021-06-01T10:00:00Z", // first held (2 days ago) 258 "48h", // max duration 259 "00h", // expected 260 }, 261 { 262 "2021-03-08T10:00:00Z", // last refreshed (almost 3 months ago) 263 "2021-06-01T10:00:00Z", // first held 264 "2160h", // max duration (90 days) 265 "72h", // expected 266 }, 267 { 268 "2021-03-04T10:00:00Z", // last refreshed 269 "2021-06-01T10:00:00Z", // first held (2 days ago) 270 "2160h", // max duration (90 days) 271 "-24h", // expected (refresh is 1 day overdue) 272 }, 273 { 274 "2021-06-01T10:00:00Z", // last refreshed (2 days ago) 275 "2021-06-03T10:00:00Z", // first held now 276 "2160h", // max duration (90 days) 277 "2112h", // expected (max minus 2 days) 278 }, 279 } { 280 lastRefresh, err := time.Parse(time.RFC3339, tc.lastRefresh) 281 c.Assert(err, IsNil) 282 firstHeld, err := time.Parse(time.RFC3339, tc.firstHeld) 283 c.Assert(err, IsNil) 284 maxDuration, err := time.ParseDuration(tc.maxDuration) 285 c.Assert(err, IsNil) 286 expected, err := time.ParseDuration(tc.expected) 287 c.Assert(err, IsNil) 288 289 left := snapstate.HoldDurationLeft(now, lastRefresh, firstHeld, maxDuration, maxPostponement) 290 c.Check(left, Equals, expected, Commentf("case #%d", i)) 291 } 292 } 293 294 func (s *autorefreshGatingSuite) TestLastRefreshedHelper(c *C) { 295 st := s.state 296 st.Lock() 297 defer st.Unlock() 298 299 inf := mockInstalledSnap(c, st, snapAyaml, false) 300 stat, err := os.Stat(inf.MountFile()) 301 c.Assert(err, IsNil) 302 303 refreshed, err := snapstate.LastRefreshed(st, "snap-a") 304 c.Assert(err, IsNil) 305 c.Check(refreshed, DeepEquals, stat.ModTime()) 306 307 t, err := time.Parse(time.RFC3339, "2021-01-01T10:00:00Z") 308 c.Assert(err, IsNil) 309 310 var snapst snapstate.SnapState 311 c.Assert(snapstate.Get(st, "snap-a", &snapst), IsNil) 312 snapst.LastRefreshTime = &t 313 snapstate.Set(st, "snap-a", &snapst) 314 315 refreshed, err = snapstate.LastRefreshed(st, "snap-a") 316 c.Assert(err, IsNil) 317 c.Check(refreshed, DeepEquals, t) 318 } 319 320 func (s *autorefreshGatingSuite) TestHoldRefreshHelper(c *C) { 321 st := s.state 322 st.Lock() 323 defer st.Unlock() 324 325 restore := snapstate.MockTimeNow(func() time.Time { 326 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 327 c.Assert(err, IsNil) 328 return t 329 }) 330 defer restore() 331 332 mockInstalledSnap(c, st, snapAyaml, false) 333 mockInstalledSnap(c, st, snapByaml, false) 334 mockInstalledSnap(c, st, snapCyaml, false) 335 mockInstalledSnap(c, st, snapDyaml, false) 336 mockInstalledSnap(c, st, snapEyaml, false) 337 mockInstalledSnap(c, st, snapFyaml, false) 338 339 mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b", "snap-c", "snap-d", "snap-e", "snap-f") 340 341 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) 342 // this could be merged with the above HoldRefresh call, but it's fine if 343 // done separately too. 344 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-e"), IsNil) 345 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-e"), IsNil) 346 c.Assert(snapstate.HoldRefresh(st, "snap-f", 0, "snap-f"), IsNil) 347 348 var gating map[string]map[string]*snapstate.HoldState 349 c.Assert(st.Get("snaps-hold", &gating), IsNil) 350 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 351 "snap-b": { 352 // holding of other snaps for maxOtherHoldDuration (48h) 353 "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 354 }, 355 "snap-c": { 356 "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 357 }, 358 "snap-e": { 359 "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 360 "snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 361 }, 362 "snap-f": { 363 // holding self set for maxPostponement minus 1 day due to last refresh. 364 "snap-f": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-07T10:00:00Z"), 365 }, 366 }) 367 } 368 369 func (s *autorefreshGatingSuite) TestHoldRefreshHelperMultipleTimes(c *C) { 370 st := s.state 371 st.Lock() 372 defer st.Unlock() 373 374 lastRefreshed := "2021-05-09T10:00:00Z" 375 now := "2021-05-10T10:00:00Z" 376 restore := snapstate.MockTimeNow(func() time.Time { 377 t, err := time.Parse(time.RFC3339, now) 378 c.Assert(err, IsNil) 379 return t 380 }) 381 defer restore() 382 383 mockInstalledSnap(c, st, snapAyaml, false) 384 mockInstalledSnap(c, st, snapByaml, false) 385 // snap-a was last refreshed yesterday 386 mockLastRefreshed(c, st, lastRefreshed, "snap-a") 387 388 // hold it for just a bit (10h) initially 389 hold := time.Hour * 10 390 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 391 var gating map[string]map[string]*snapstate.HoldState 392 c.Assert(st.Get("snaps-hold", &gating), IsNil) 393 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 394 "snap-a": { 395 "snap-b": snapstate.MockHoldState(now, "2021-05-10T20:00:00Z"), 396 }, 397 }) 398 399 // holding for a shorter time is fine too 400 hold = time.Hour * 5 401 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 402 c.Assert(st.Get("snaps-hold", &gating), IsNil) 403 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 404 "snap-a": { 405 "snap-b": snapstate.MockHoldState(now, "2021-05-10T15:00:00Z"), 406 }, 407 }) 408 409 oldNow := now 410 411 // a refresh on next day 412 now = "2021-05-11T08:00:00Z" 413 414 // default hold time requested 415 hold = 0 416 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 417 c.Assert(st.Get("snaps-hold", &gating), IsNil) 418 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 419 "snap-a": { 420 // maximum for holding other snaps, but taking into consideration 421 // firstHeld time = "2021-05-10T10:00:00". 422 "snap-b": snapstate.MockHoldState(oldNow, "2021-05-12T10:00:00Z"), 423 }, 424 }) 425 } 426 427 func (s *autorefreshGatingSuite) TestHoldRefreshHelperCloseToMaxPostponement(c *C) { 428 st := s.state 429 st.Lock() 430 defer st.Unlock() 431 432 lastRefreshedStr := "2021-01-01T10:00:00Z" 433 lastRefreshed, err := time.Parse(time.RFC3339, lastRefreshedStr) 434 c.Assert(err, IsNil) 435 // we are 1 day before maxPostponent 436 now := lastRefreshed.Add(89 * time.Hour * 24) 437 438 restore := snapstate.MockTimeNow(func() time.Time { return now }) 439 defer restore() 440 441 mockInstalledSnap(c, st, snapAyaml, false) 442 mockInstalledSnap(c, st, snapByaml, false) 443 mockLastRefreshed(c, st, lastRefreshedStr, "snap-a") 444 445 // request default hold time 446 var hold time.Duration 447 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 448 449 var gating map[string]map[string]*snapstate.HoldState 450 c.Assert(st.Get("snaps-hold", &gating), IsNil) 451 c.Assert(gating, HasLen, 1) 452 c.Check(gating["snap-a"]["snap-b"].HoldUntil.String(), DeepEquals, lastRefreshed.Add(90*time.Hour*24).String()) 453 } 454 455 func (s *autorefreshGatingSuite) TestHoldRefreshExplicitHoldTime(c *C) { 456 st := s.state 457 st.Lock() 458 defer st.Unlock() 459 460 now := "2021-05-10T10:00:00Z" 461 restore := snapstate.MockTimeNow(func() time.Time { 462 t, err := time.Parse(time.RFC3339, now) 463 c.Assert(err, IsNil) 464 return t 465 }) 466 defer restore() 467 468 mockInstalledSnap(c, st, snapAyaml, false) 469 mockInstalledSnap(c, st, snapByaml, false) 470 471 hold := time.Hour * 24 * 3 472 // holding self for 3 days 473 c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), IsNil) 474 475 // snap-b holds snap-a for 1 day 476 hold = time.Hour * 24 477 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) 478 479 var gating map[string]map[string]*snapstate.HoldState 480 c.Assert(st.Get("snaps-hold", &gating), IsNil) 481 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 482 "snap-a": { 483 "snap-a": snapstate.MockHoldState(now, "2021-05-13T10:00:00Z"), 484 "snap-b": snapstate.MockHoldState(now, "2021-05-11T10:00:00Z"), 485 }, 486 }) 487 } 488 489 func (s *autorefreshGatingSuite) TestHoldRefreshHelperErrors(c *C) { 490 st := s.state 491 st.Lock() 492 defer st.Unlock() 493 494 now := "2021-05-10T10:00:00Z" 495 restore := snapstate.MockTimeNow(func() time.Time { 496 t, err := time.Parse(time.RFC3339, now) 497 c.Assert(err, IsNil) 498 return t 499 }) 500 defer restore() 501 502 mockInstalledSnap(c, st, snapAyaml, false) 503 mockInstalledSnap(c, st, snapByaml, false) 504 // snap-b was refreshed a few days ago 505 mockLastRefreshed(c, st, "2021-05-01T10:00:00Z", "snap-b") 506 507 // holding itself 508 hold := time.Hour * 24 * 96 509 c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-a" of 2304h0m0s by snap "snap-a" exceeds maximum holding time`) 510 511 // holding other snap 512 hold = time.Hour * 49 513 err := snapstate.HoldRefresh(st, "snap-a", hold, "snap-b") 514 c.Check(err, ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`) 515 herr, ok := err.(*snapstate.HoldError) 516 c.Assert(ok, Equals, true) 517 c.Check(herr.SnapsInError, DeepEquals, map[string]snapstate.HoldDurationError{ 518 "snap-b": { 519 Err: fmt.Errorf(`requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`), 520 DurationLeft: 48 * time.Hour, 521 }, 522 }) 523 524 // hold for maximum allowed for other snaps 525 hold = time.Hour * 48 526 c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), IsNil) 527 // 2 days passed since it was first held 528 now = "2021-05-12T10:00:00Z" 529 hold = time.Minute * 2 530 c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-a" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) 531 532 // refreshed long time ago (> maxPostponement) 533 mockLastRefreshed(c, st, "2021-01-01T10:00:00Z", "snap-b") 534 hold = time.Hour * 2 535 c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) 536 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) 537 } 538 539 func (s *autorefreshGatingSuite) TestHoldAndProceedWithRefreshHelper(c *C) { 540 st := s.state 541 st.Lock() 542 defer st.Unlock() 543 544 mockInstalledSnap(c, st, snapAyaml, false) 545 mockInstalledSnap(c, st, snapByaml, false) 546 mockInstalledSnap(c, st, snapCyaml, false) 547 mockInstalledSnap(c, st, snapDyaml, false) 548 549 mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-b", "snap-c", "snap-d") 550 551 restore := snapstate.MockTimeNow(func() time.Time { 552 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 553 c.Assert(err, IsNil) 554 return t 555 }) 556 defer restore() 557 558 // nothing is held initially 559 held, err := snapstate.HeldSnaps(st) 560 c.Assert(err, IsNil) 561 c.Check(held, IsNil) 562 563 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) 564 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil) 565 // holding self 566 c.Assert(snapstate.HoldRefresh(st, "snap-d", time.Hour*24*4, "snap-d"), IsNil) 567 568 held, err = snapstate.HeldSnaps(st) 569 c.Assert(err, IsNil) 570 c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-c": true, "snap-d": true}) 571 572 c.Assert(snapstate.ProceedWithRefresh(st, "snap-a"), IsNil) 573 574 held, err = snapstate.HeldSnaps(st) 575 c.Assert(err, IsNil) 576 c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-d": true}) 577 578 c.Assert(snapstate.ProceedWithRefresh(st, "snap-d"), IsNil) 579 held, err = snapstate.HeldSnaps(st) 580 c.Assert(err, IsNil) 581 c.Check(held, IsNil) 582 } 583 584 // Test that if all snaps cannot be held anymore, we don't hold only some of them 585 // e.g. is a snap and its base snap have updates and the snap wants to hold (itself 586 // and the base) but the base cannot be held, it doesn't make sense to refresh the 587 // base but hold the affected snap. 588 func (s *autorefreshGatingSuite) TestDontHoldSomeSnapsIfSomeFail(c *C) { 589 st := s.state 590 st.Lock() 591 defer st.Unlock() 592 593 // snap-b and snap-bb have base-snap-b base 594 mockInstalledSnap(c, st, snapByaml, useHook) 595 mockInstalledSnap(c, st, snapBByaml, useHook) 596 mockInstalledSnap(c, st, baseSnapByaml, noHook) 597 598 mockInstalledSnap(c, st, snapCyaml, useHook) 599 mockInstalledSnap(c, st, snapDyaml, useHook) 600 601 now := "2021-05-01T10:00:00Z" 602 restore := snapstate.MockTimeNow(func() time.Time { 603 t, err := time.Parse(time.RFC3339, now) 604 c.Assert(err, IsNil) 605 return t 606 }) 607 defer restore() 608 609 // snap-b, base-snap-b get refreshed and affect snap-b (gating snap) 610 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b", "base-snap-b"), IsNil) 611 // unrealted snap-d gets refreshed and holds itself 612 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d"), IsNil) 613 614 // advance time by 49h 615 now = "2021-05-03T11:00:00Z" 616 // snap-b, base-snap-b and snap-c get refreshed and snap-a (gating snap) wants to hold them 617 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b", "base-snap-b", "snap-c"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "base-snap-b" anymore, maximum refresh postponement exceeded`) 618 // snap-bb (gating snap) wants to hold base-snap-b as well and succeeds since it didn't exceed its holding time yet 619 c.Assert(snapstate.HoldRefresh(st, "snap-bb", 0, "base-snap-b"), IsNil) 620 621 held, err := snapstate.HeldSnaps(st) 622 c.Assert(err, IsNil) 623 // note, snap-b couldn't hold base-snap-b anymore so we didn't hold snap-b 624 // and snap-c. base-snap-b was held by snap-bb. 625 c.Check(held, DeepEquals, map[string]bool{ 626 "snap-d": true, 627 "base-snap-b": true, 628 }) 629 } 630 631 func (s *autorefreshGatingSuite) TestPruneGatingHelper(c *C) { 632 st := s.state 633 st.Lock() 634 defer st.Unlock() 635 636 restore := snapstate.MockTimeNow(func() time.Time { 637 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 638 c.Assert(err, IsNil) 639 return t 640 }) 641 defer restore() 642 643 mockInstalledSnap(c, st, snapAyaml, false) 644 mockInstalledSnap(c, st, snapByaml, false) 645 mockInstalledSnap(c, st, snapCyaml, false) 646 mockInstalledSnap(c, st, snapDyaml, false) 647 648 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) 649 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil) 650 // sanity 651 held, err := snapstate.HeldSnaps(st) 652 c.Assert(err, IsNil) 653 c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-b": true, "snap-d": true}) 654 655 candidates := map[string]*snapstate.RefreshCandidate{"snap-c": {}} 656 657 // only snap-c has a refresh candidate, snap-b and snap-d should be forgotten. 658 c.Assert(snapstate.PruneGating(st, candidates), IsNil) 659 var gating map[string]map[string]*snapstate.HoldState 660 c.Assert(st.Get("snaps-hold", &gating), IsNil) 661 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 662 "snap-c": { 663 "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 664 "snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 665 }, 666 }) 667 held, err = snapstate.HeldSnaps(st) 668 c.Assert(err, IsNil) 669 c.Check(held, DeepEquals, map[string]bool{"snap-c": true}) 670 } 671 672 func (s *autorefreshGatingSuite) TestPruneGatingHelperNoGating(c *C) { 673 st := s.state 674 st.Lock() 675 defer st.Unlock() 676 677 restore := snapstate.MockTimeNow(func() time.Time { 678 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 679 c.Assert(err, IsNil) 680 return t 681 }) 682 defer restore() 683 684 mockInstalledSnap(c, st, snapAyaml, false) 685 686 held, err := snapstate.HeldSnaps(st) 687 c.Assert(err, IsNil) 688 c.Check(held, HasLen, 0) 689 690 snapstate.MockTimeNow(func() time.Time { 691 c.Fatalf("not expected") 692 return time.Time{} 693 }) 694 695 candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}} 696 c.Assert(snapstate.PruneGating(st, candidates), IsNil) 697 held, err = snapstate.HeldSnaps(st) 698 c.Assert(err, IsNil) 699 c.Check(held, HasLen, 0) 700 } 701 702 func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelper(c *C) { 703 st := s.state 704 st.Lock() 705 defer st.Unlock() 706 707 restore := snapstate.MockTimeNow(func() time.Time { 708 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 709 c.Assert(err, IsNil) 710 return t 711 }) 712 defer restore() 713 714 mockInstalledSnap(c, st, snapAyaml, false) 715 mockInstalledSnap(c, st, snapByaml, false) 716 mockInstalledSnap(c, st, snapCyaml, false) 717 mockInstalledSnap(c, st, snapDyaml, false) 718 719 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) 720 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil) 721 722 c.Assert(snapstate.ResetGatingForRefreshed(st, "snap-b", "snap-c"), IsNil) 723 var gating map[string]map[string]*snapstate.HoldState 724 c.Assert(st.Get("snaps-hold", &gating), IsNil) 725 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 726 "snap-d": { 727 // holding self set for maxPostponement (95 days - buffer = 90 days) 728 "snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-08T10:00:00Z"), 729 }, 730 }) 731 732 held, err := snapstate.HeldSnaps(st) 733 c.Assert(err, IsNil) 734 c.Check(held, DeepEquals, map[string]bool{"snap-d": true}) 735 } 736 737 func (s *autorefreshGatingSuite) TestPruneSnapsHold(c *C) { 738 st := s.state 739 st.Lock() 740 defer st.Unlock() 741 742 mockInstalledSnap(c, st, snapAyaml, false) 743 mockInstalledSnap(c, st, snapByaml, false) 744 mockInstalledSnap(c, st, snapCyaml, false) 745 mockInstalledSnap(c, st, snapDyaml, false) 746 747 // snap-a is holding itself and 3 other snaps 748 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a", "snap-b", "snap-c", "snap-d"), IsNil) 749 // in addition, snap-c is held by snap-d. 750 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil) 751 752 // sanity check 753 held, err := snapstate.HeldSnaps(st) 754 c.Assert(err, IsNil) 755 c.Check(held, DeepEquals, map[string]bool{ 756 "snap-a": true, 757 "snap-b": true, 758 "snap-c": true, 759 "snap-d": true, 760 }) 761 762 c.Check(snapstate.PruneSnapsHold(st, "snap-a"), IsNil) 763 764 // after pruning snap-a, snap-c is still held. 765 held, err = snapstate.HeldSnaps(st) 766 c.Assert(err, IsNil) 767 c.Check(held, DeepEquals, map[string]bool{ 768 "snap-c": true, 769 }) 770 var gating map[string]map[string]*snapstate.HoldState 771 c.Assert(st.Get("snaps-hold", &gating), IsNil) 772 c.Assert(gating, HasLen, 1) 773 c.Check(gating["snap-c"], HasLen, 1) 774 c.Check(gating["snap-c"]["snap-d"], NotNil) 775 } 776 777 const useHook = true 778 const noHook = false 779 780 func checkGatingTask(c *C, task *state.Task, expected map[string]*snapstate.RefreshCandidate) { 781 c.Assert(task.Kind(), Equals, "conditional-auto-refresh") 782 var snaps map[string]*snapstate.RefreshCandidate 783 c.Assert(task.Get("snaps", &snaps), IsNil) 784 c.Check(snaps, DeepEquals, expected) 785 } 786 787 func (s *autorefreshGatingSuite) TestAffectedByBase(c *C) { 788 restore := release.MockOnClassic(true) 789 defer restore() 790 791 st := s.state 792 793 st.Lock() 794 defer st.Unlock() 795 mockInstalledSnap(c, s.state, snapAyaml, useHook) 796 baseSnapA := mockInstalledSnap(c, s.state, baseSnapAyaml, noHook) 797 // unrelated snaps 798 snapB := mockInstalledSnap(c, s.state, snapByaml, useHook) 799 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 800 801 c.Assert(s.repo.AddSnap(snapB), IsNil) 802 803 updates := []string{baseSnapA.InstanceName()} 804 affected, err := snapstate.AffectedByRefresh(st, updates) 805 c.Assert(err, IsNil) 806 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 807 "snap-a": { 808 Base: true, 809 AffectingSnaps: map[string]bool{ 810 "base-snap-a": true, 811 }}}) 812 } 813 814 func (s *autorefreshGatingSuite) TestAffectedByCore(c *C) { 815 restore := release.MockOnClassic(true) 816 defer restore() 817 818 st := s.state 819 820 st.Lock() 821 defer st.Unlock() 822 snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook) 823 core := mockInstalledSnap(c, s.state, coreYaml, noHook) 824 snapB := mockInstalledSnap(c, s.state, snapByaml, useHook) 825 826 c.Assert(s.repo.AddSnap(core), IsNil) 827 c.Assert(s.repo.AddSnap(snapB), IsNil) 828 c.Assert(s.repo.AddSnap(snapC), IsNil) 829 830 updates := []string{core.InstanceName()} 831 affected, err := snapstate.AffectedByRefresh(st, updates) 832 c.Assert(err, IsNil) 833 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 834 "snap-c": { 835 Base: true, 836 AffectingSnaps: map[string]bool{ 837 "core": true, 838 }}}) 839 } 840 841 func (s *autorefreshGatingSuite) TestAffectedByKernel(c *C) { 842 restore := release.MockOnClassic(true) 843 defer restore() 844 845 st := s.state 846 847 st.Lock() 848 defer st.Unlock() 849 kernel := mockInstalledSnap(c, s.state, kernelYaml, noHook) 850 mockInstalledSnap(c, s.state, snapCyaml, useHook) 851 mockInstalledSnap(c, s.state, snapByaml, noHook) 852 853 updates := []string{kernel.InstanceName()} 854 affected, err := snapstate.AffectedByRefresh(st, updates) 855 c.Assert(err, IsNil) 856 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 857 "snap-c": { 858 Restart: true, 859 AffectingSnaps: map[string]bool{ 860 "kernel": true, 861 }}}) 862 } 863 864 func (s *autorefreshGatingSuite) TestAffectedBySelf(c *C) { 865 restore := release.MockOnClassic(true) 866 defer restore() 867 868 st := s.state 869 870 st.Lock() 871 defer st.Unlock() 872 873 snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook) 874 updates := []string{snapC.InstanceName()} 875 affected, err := snapstate.AffectedByRefresh(st, updates) 876 c.Assert(err, IsNil) 877 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 878 "snap-c": { 879 AffectingSnaps: map[string]bool{ 880 "snap-c": true, 881 }}}) 882 } 883 884 func (s *autorefreshGatingSuite) TestAffectedByGadget(c *C) { 885 restore := release.MockOnClassic(true) 886 defer restore() 887 888 st := s.state 889 890 st.Lock() 891 defer st.Unlock() 892 kernel := mockInstalledSnap(c, s.state, gadget1Yaml, noHook) 893 mockInstalledSnap(c, s.state, snapCyaml, useHook) 894 mockInstalledSnap(c, s.state, snapByaml, noHook) 895 896 updates := []string{kernel.InstanceName()} 897 affected, err := snapstate.AffectedByRefresh(st, updates) 898 c.Assert(err, IsNil) 899 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 900 "snap-c": { 901 Restart: true, 902 AffectingSnaps: map[string]bool{ 903 "gadget": true, 904 }}}) 905 } 906 907 func (s *autorefreshGatingSuite) TestAffectedBySlot(c *C) { 908 restore := release.MockOnClassic(true) 909 defer restore() 910 911 st := s.state 912 913 st.Lock() 914 defer st.Unlock() 915 916 snapD := mockInstalledSnap(c, s.state, snapDyaml, noHook) 917 snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook) 918 // unrelated snap 919 snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook) 920 921 c.Assert(s.repo.AddSnap(snapF), IsNil) 922 c.Assert(s.repo.AddSnap(snapD), IsNil) 923 c.Assert(s.repo.AddSnap(snapE), IsNil) 924 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}} 925 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 926 c.Assert(err, IsNil) 927 928 updates := []string{snapD.InstanceName()} 929 affected, err := snapstate.AffectedByRefresh(st, updates) 930 c.Assert(err, IsNil) 931 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 932 "snap-e": { 933 Restart: true, 934 AffectingSnaps: map[string]bool{ 935 "snap-d": true, 936 }}}) 937 } 938 939 func (s *autorefreshGatingSuite) TestNotAffectedByCoreOrSnapdSlot(c *C) { 940 restore := release.MockOnClassic(true) 941 defer restore() 942 943 st := s.state 944 945 st.Lock() 946 defer st.Unlock() 947 948 snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook) 949 core := mockInstalledSnap(c, s.state, coreYaml, noHook) 950 snapB := mockInstalledSnap(c, s.state, snapByaml, useHook) 951 952 c.Assert(s.repo.AddSnap(snapG), IsNil) 953 c.Assert(s.repo.AddSnap(core), IsNil) 954 c.Assert(s.repo.AddSnap(snapB), IsNil) 955 956 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "mir"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "mir"}} 957 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 958 c.Assert(err, IsNil) 959 960 updates := []string{core.InstanceName()} 961 affected, err := snapstate.AffectedByRefresh(st, updates) 962 c.Assert(err, IsNil) 963 c.Check(affected, HasLen, 0) 964 } 965 966 func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackend(c *C) { 967 restore := release.MockOnClassic(true) 968 defer restore() 969 970 st := s.state 971 972 st.Lock() 973 defer st.Unlock() 974 975 snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook) 976 snapE := mockInstalledSnap(c, s.state, snapEyaml, noHook) 977 // unrelated snap 978 snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook) 979 980 c.Assert(s.repo.AddSnap(snapF), IsNil) 981 c.Assert(s.repo.AddSnap(snapD), IsNil) 982 c.Assert(s.repo.AddSnap(snapE), IsNil) 983 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}} 984 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 985 c.Assert(err, IsNil) 986 987 // snapE has a plug using mount backend and is refreshed, this affects slot of snap-d. 988 updates := []string{snapE.InstanceName()} 989 affected, err := snapstate.AffectedByRefresh(st, updates) 990 c.Assert(err, IsNil) 991 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 992 "snap-d": { 993 Restart: true, 994 AffectingSnaps: map[string]bool{ 995 "snap-e": true, 996 }}}) 997 } 998 999 func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendSnapdSlot(c *C) { 1000 restore := release.MockOnClassic(true) 1001 defer restore() 1002 1003 st := s.state 1004 1005 st.Lock() 1006 defer st.Unlock() 1007 1008 snapdSnap := mockInstalledSnap(c, s.state, snapdYaml, noHook) 1009 snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook) 1010 // unrelated snap 1011 snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook) 1012 1013 c.Assert(s.repo.AddSnap(snapF), IsNil) 1014 c.Assert(s.repo.AddSnap(snapdSnap), IsNil) 1015 c.Assert(s.repo.AddSnap(snapG), IsNil) 1016 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "snapd", Name: "desktop"}} 1017 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 1018 c.Assert(err, IsNil) 1019 1020 // snapE has a plug using mount backend, refreshing snapd affects snapE. 1021 updates := []string{snapdSnap.InstanceName()} 1022 affected, err := snapstate.AffectedByRefresh(st, updates) 1023 c.Assert(err, IsNil) 1024 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 1025 "snap-g": { 1026 Restart: true, 1027 AffectingSnaps: map[string]bool{ 1028 "snapd": true, 1029 }}}) 1030 } 1031 1032 func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendCoreSlot(c *C) { 1033 restore := release.MockOnClassic(true) 1034 defer restore() 1035 1036 st := s.state 1037 1038 st.Lock() 1039 defer st.Unlock() 1040 1041 coreSnap := mockInstalledSnap(c, s.state, coreYaml, noHook) 1042 snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook) 1043 1044 c.Assert(s.repo.AddSnap(coreSnap), IsNil) 1045 c.Assert(s.repo.AddSnap(snapG), IsNil) 1046 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "desktop"}} 1047 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 1048 c.Assert(err, IsNil) 1049 1050 // snapG has a plug using mount backend, refreshing core affects snapE. 1051 updates := []string{coreSnap.InstanceName()} 1052 affected, err := snapstate.AffectedByRefresh(st, updates) 1053 c.Assert(err, IsNil) 1054 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 1055 "snap-g": { 1056 Restart: true, 1057 AffectingSnaps: map[string]bool{ 1058 "core": true, 1059 }}}) 1060 } 1061 1062 func (s *autorefreshGatingSuite) TestAffectedByBootBase(c *C) { 1063 restore := release.MockOnClassic(false) 1064 defer restore() 1065 1066 st := s.state 1067 1068 r := snapstatetest.MockDeviceModel(ModelWithBase("core18")) 1069 defer r() 1070 1071 st.Lock() 1072 defer st.Unlock() 1073 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1074 mockInstalledSnap(c, s.state, snapByaml, useHook) 1075 mockInstalledSnap(c, s.state, snapDyaml, useHook) 1076 mockInstalledSnap(c, s.state, snapEyaml, useHook) 1077 core18 := mockInstalledSnap(c, s.state, core18Yaml, noHook) 1078 1079 updates := []string{core18.InstanceName()} 1080 affected, err := snapstate.AffectedByRefresh(st, updates) 1081 c.Assert(err, IsNil) 1082 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 1083 "snap-a": { 1084 Base: false, 1085 Restart: true, 1086 AffectingSnaps: map[string]bool{ 1087 "core18": true, 1088 }, 1089 }, 1090 "snap-b": { 1091 Base: false, 1092 Restart: true, 1093 AffectingSnaps: map[string]bool{ 1094 "core18": true, 1095 }, 1096 }, 1097 "snap-d": { 1098 Base: false, 1099 Restart: true, 1100 AffectingSnaps: map[string]bool{ 1101 "core18": true, 1102 }, 1103 }, 1104 "snap-e": { 1105 Base: false, 1106 Restart: true, 1107 AffectingSnaps: map[string]bool{ 1108 "core18": true, 1109 }}}) 1110 } 1111 1112 func (s *autorefreshGatingSuite) TestCreateAutoRefreshGateHooks(c *C) { 1113 st := s.state 1114 st.Lock() 1115 defer st.Unlock() 1116 1117 affected := map[string]*snapstate.AffectedSnapInfo{ 1118 "snap-a": { 1119 Base: true, 1120 Restart: true, 1121 AffectingSnaps: map[string]bool{ 1122 "snap-c": true, 1123 "snap-d": true, 1124 }, 1125 }, 1126 "snap-b": { 1127 AffectingSnaps: map[string]bool{ 1128 "snap-e": true, 1129 "snap-f": true, 1130 }, 1131 }, 1132 } 1133 1134 seenSnaps := make(map[string]bool) 1135 1136 ts := snapstate.CreateGateAutoRefreshHooks(st, affected) 1137 c.Assert(ts.Tasks(), HasLen, 2) 1138 1139 checkHook := func(t *state.Task) { 1140 c.Assert(t.Kind(), Equals, "run-hook") 1141 var hs hookstate.HookSetup 1142 c.Assert(t.Get("hook-setup", &hs), IsNil) 1143 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1144 c.Check(hs.Optional, Equals, true) 1145 seenSnaps[hs.Snap] = true 1146 1147 var data interface{} 1148 c.Assert(t.Get("hook-context", &data), IsNil) 1149 1150 // the order of hook tasks is not deterministic 1151 if hs.Snap == "snap-a" { 1152 c.Check(data, DeepEquals, map[string]interface{}{ 1153 "base": true, 1154 "restart": true, 1155 "affecting-snaps": []interface{}{"snap-c", "snap-d"}}) 1156 } else { 1157 c.Assert(hs.Snap, Equals, "snap-b") 1158 c.Check(data, DeepEquals, map[string]interface{}{ 1159 "base": false, 1160 "restart": false, 1161 "affecting-snaps": []interface{}{"snap-e", "snap-f"}}) 1162 } 1163 } 1164 1165 checkHook(ts.Tasks()[0]) 1166 checkHook(ts.Tasks()[1]) 1167 1168 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true}) 1169 } 1170 1171 func (s *autorefreshGatingSuite) TestAffectedByRefreshCandidates(c *C) { 1172 st := s.state 1173 st.Lock() 1174 defer st.Unlock() 1175 1176 mockInstalledSnap(c, st, snapAyaml, useHook) 1177 // unrelated snap 1178 mockInstalledSnap(c, st, snapByaml, useHook) 1179 1180 // no refresh-candidates in state 1181 affected, err := snapstate.AffectedByRefreshCandidates(st) 1182 c.Assert(err, IsNil) 1183 c.Check(affected, HasLen, 0) 1184 1185 candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}} 1186 st.Set("refresh-candidates", &candidates) 1187 1188 affected, err = snapstate.AffectedByRefreshCandidates(st) 1189 c.Assert(err, IsNil) 1190 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 1191 "snap-a": { 1192 AffectingSnaps: map[string]bool{ 1193 "snap-a": true, 1194 }}}) 1195 } 1196 1197 func (s *autorefreshGatingSuite) TestAutorefreshPhase1FeatureFlag(c *C) { 1198 st := s.state 1199 st.Lock() 1200 defer st.Unlock() 1201 1202 st.Set("seeded", true) 1203 1204 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1205 defer restore() 1206 1207 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 1208 return nil, nil 1209 } 1210 defer func() { snapstate.AutoAliases = nil }() 1211 1212 s.store.refreshedSnaps = []*snap.Info{{ 1213 Architectures: []string{"all"}, 1214 SnapType: snap.TypeApp, 1215 SideInfo: snap.SideInfo{ 1216 RealName: "snap-a", 1217 Revision: snap.R(8), 1218 }, 1219 }} 1220 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1221 1222 // gate-auto-refresh-hook feature not enabled, expect old-style refresh. 1223 _, tss, err := snapstate.AutoRefresh(context.TODO(), st) 1224 c.Check(err, IsNil) 1225 c.Assert(tss, HasLen, 2) 1226 c.Check(tss[0].Tasks()[0].Kind(), Equals, "prerequisites") 1227 c.Check(tss[0].Tasks()[1].Kind(), Equals, "download-snap") 1228 c.Check(tss[1].Tasks()[0].Kind(), Equals, "check-rerefresh") 1229 1230 // enable gate-auto-refresh-hook feature 1231 tr := config.NewTransaction(s.state) 1232 tr.Set("core", "experimental.gate-auto-refresh-hook", true) 1233 tr.Commit() 1234 1235 _, tss, err = snapstate.AutoRefresh(context.TODO(), st) 1236 c.Check(err, IsNil) 1237 c.Assert(tss, HasLen, 2) 1238 task := tss[0].Tasks()[0] 1239 c.Check(task.Kind(), Equals, "conditional-auto-refresh") 1240 var toUpdate map[string]*snapstate.RefreshCandidate 1241 c.Assert(task.Get("snaps", &toUpdate), IsNil) 1242 seenSnaps := make(map[string]bool) 1243 for up := range toUpdate { 1244 seenSnaps[up] = true 1245 } 1246 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true}) 1247 c.Check(tss[1].Tasks()[0].Kind(), Equals, "run-hook") 1248 } 1249 1250 func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) { 1251 s.store.refreshedSnaps = []*snap.Info{{ 1252 Architectures: []string{"all"}, 1253 SnapType: snap.TypeApp, 1254 SideInfo: snap.SideInfo{ 1255 RealName: "snap-a", 1256 Revision: snap.R(8), 1257 }, 1258 }, { 1259 Architectures: []string{"all"}, 1260 SnapType: snap.TypeBase, 1261 SideInfo: snap.SideInfo{ 1262 RealName: "base-snap-b", 1263 Revision: snap.R(3), 1264 }, 1265 }, { 1266 Architectures: []string{"all"}, 1267 SnapType: snap.TypeApp, 1268 SideInfo: snap.SideInfo{ 1269 RealName: "snap-c", 1270 Revision: snap.R(5), 1271 }, 1272 }} 1273 1274 st := s.state 1275 st.Lock() 1276 defer st.Unlock() 1277 1278 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1279 mockInstalledSnap(c, s.state, snapByaml, useHook) 1280 mockInstalledSnap(c, s.state, snapCyaml, noHook) 1281 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1282 mockInstalledSnap(c, s.state, snapDyaml, noHook) 1283 1284 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1285 defer restore() 1286 1287 // pretend some snaps are held 1288 c.Assert(snapstate.HoldRefresh(st, "gating-snap", 0, "snap-a", "snap-d"), IsNil) 1289 // sanity check 1290 heldSnaps, err := snapstate.HeldSnaps(st) 1291 c.Assert(err, IsNil) 1292 c.Check(heldSnaps, DeepEquals, map[string]bool{ 1293 "snap-a": true, 1294 "snap-d": true, 1295 }) 1296 1297 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1298 c.Assert(err, IsNil) 1299 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a", "snap-c"}) 1300 c.Assert(tss, HasLen, 2) 1301 1302 c.Assert(tss[0].Tasks(), HasLen, 1) 1303 checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{ 1304 "snap-a": { 1305 SnapSetup: snapstate.SnapSetup{ 1306 Type: "app", 1307 PlugsOnly: true, 1308 Flags: snapstate.Flags{ 1309 IsAutoRefresh: true, 1310 }, 1311 SideInfo: &snap.SideInfo{ 1312 RealName: "snap-a", 1313 Revision: snap.R(8), 1314 }, 1315 DownloadInfo: &snap.DownloadInfo{}, 1316 }, 1317 }, 1318 "base-snap-b": { 1319 SnapSetup: snapstate.SnapSetup{ 1320 Type: "base", 1321 PlugsOnly: true, 1322 Flags: snapstate.Flags{ 1323 IsAutoRefresh: true, 1324 }, 1325 SideInfo: &snap.SideInfo{ 1326 RealName: "base-snap-b", 1327 Revision: snap.R(3), 1328 }, 1329 DownloadInfo: &snap.DownloadInfo{}, 1330 }, 1331 }, 1332 "snap-c": { 1333 SnapSetup: snapstate.SnapSetup{ 1334 Type: "app", 1335 PlugsOnly: true, 1336 Flags: snapstate.Flags{ 1337 IsAutoRefresh: true, 1338 }, 1339 SideInfo: &snap.SideInfo{ 1340 RealName: "snap-c", 1341 Revision: snap.R(5), 1342 }, 1343 DownloadInfo: &snap.DownloadInfo{}, 1344 }, 1345 }, 1346 }) 1347 1348 c.Assert(tss[1].Tasks(), HasLen, 2) 1349 1350 var snapAhookData, snapBhookData map[string]interface{} 1351 1352 // check hooks for affected snaps 1353 seenSnaps := make(map[string]bool) 1354 var hs hookstate.HookSetup 1355 task := tss[1].Tasks()[0] 1356 c.Assert(task.Get("hook-setup", &hs), IsNil) 1357 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1358 seenSnaps[hs.Snap] = true 1359 switch hs.Snap { 1360 case "snap-a": 1361 task.Get("hook-context", &snapAhookData) 1362 case "snap-b": 1363 task.Get("hook-context", &snapBhookData) 1364 default: 1365 c.Fatalf("unexpected snap %q", hs.Snap) 1366 } 1367 1368 task = tss[1].Tasks()[1] 1369 c.Assert(task.Get("hook-setup", &hs), IsNil) 1370 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1371 seenSnaps[hs.Snap] = true 1372 switch hs.Snap { 1373 case "snap-a": 1374 task.Get("hook-context", &snapAhookData) 1375 case "snap-b": 1376 task.Get("hook-context", &snapBhookData) 1377 default: 1378 c.Fatalf("unexpected snap %q", hs.Snap) 1379 } 1380 1381 c.Check(snapAhookData["affecting-snaps"], DeepEquals, []interface{}{"snap-a"}) 1382 c.Check(snapBhookData["affecting-snaps"], DeepEquals, []interface{}{"base-snap-b"}) 1383 1384 // hook for snap-a because it gets refreshed, for snap-b because its base 1385 // gets refreshed. snap-c is refreshed but doesn't have the hook. 1386 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true}) 1387 1388 // check that refresh-candidates in the state were updated 1389 var candidates map[string]*snapstate.RefreshCandidate 1390 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 1391 c.Assert(candidates, HasLen, 3) 1392 c.Check(candidates["snap-a"], NotNil) 1393 c.Check(candidates["base-snap-b"], NotNil) 1394 c.Check(candidates["snap-c"], NotNil) 1395 1396 // check that after autoRefreshPhase1 any held snaps that are not in refresh 1397 // candidates got removed. 1398 heldSnaps, err = snapstate.HeldSnaps(st) 1399 c.Assert(err, IsNil) 1400 // snap-d got removed from held snaps. 1401 c.Check(heldSnaps, DeepEquals, map[string]bool{ 1402 "snap-a": true, 1403 }) 1404 } 1405 1406 // this test demonstrates that affectedByRefresh uses current snap info (not 1407 // snap infos of store updates) by simulating a different base for the updated 1408 // snap from the store. 1409 func (s *autorefreshGatingSuite) TestAffectedByRefreshUsesCurrentSnapInfo(c *C) { 1410 s.store.refreshedSnaps = []*snap.Info{{ 1411 Architectures: []string{"all"}, 1412 SnapType: snap.TypeBase, 1413 SideInfo: snap.SideInfo{ 1414 RealName: "base-snap-b", 1415 Revision: snap.R(3), 1416 }, 1417 }, { 1418 Architectures: []string{"all"}, 1419 Base: "new-base", 1420 SnapType: snap.TypeApp, 1421 SideInfo: snap.SideInfo{ 1422 RealName: "snap-b", 1423 Revision: snap.R(5), 1424 }, 1425 }} 1426 1427 st := s.state 1428 st.Lock() 1429 defer st.Unlock() 1430 1431 mockInstalledSnap(c, s.state, snapByaml, useHook) 1432 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1433 1434 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1435 defer restore() 1436 1437 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1438 c.Assert(err, IsNil) 1439 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-b"}) 1440 c.Assert(tss, HasLen, 2) 1441 1442 c.Assert(tss[0].Tasks(), HasLen, 1) 1443 checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{ 1444 "snap-b": { 1445 SnapSetup: snapstate.SnapSetup{ 1446 Type: "app", 1447 Base: "new-base", 1448 PlugsOnly: true, 1449 Flags: snapstate.Flags{ 1450 IsAutoRefresh: true, 1451 }, 1452 SideInfo: &snap.SideInfo{ 1453 RealName: "snap-b", 1454 Revision: snap.R(5), 1455 }, 1456 DownloadInfo: &snap.DownloadInfo{}, 1457 }, 1458 }, 1459 "base-snap-b": { 1460 SnapSetup: snapstate.SnapSetup{ 1461 Type: "base", 1462 PlugsOnly: true, 1463 Flags: snapstate.Flags{ 1464 IsAutoRefresh: true, 1465 }, 1466 SideInfo: &snap.SideInfo{ 1467 RealName: "base-snap-b", 1468 Revision: snap.R(3), 1469 }, 1470 DownloadInfo: &snap.DownloadInfo{}, 1471 }, 1472 }, 1473 }) 1474 1475 c.Assert(tss[1].Tasks(), HasLen, 1) 1476 var hs hookstate.HookSetup 1477 task := tss[1].Tasks()[0] 1478 c.Assert(task.Get("hook-setup", &hs), IsNil) 1479 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1480 c.Check(hs.Snap, Equals, "snap-b") 1481 var data interface{} 1482 c.Assert(task.Get("hook-context", &data), IsNil) 1483 c.Check(data, DeepEquals, map[string]interface{}{ 1484 "base": true, 1485 "restart": false, 1486 "affecting-snaps": []interface{}{"base-snap-b", "snap-b"}}) 1487 1488 // check that refresh-candidates in the state were updated 1489 var candidates map[string]*snapstate.RefreshCandidate 1490 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 1491 c.Assert(candidates, HasLen, 2) 1492 c.Check(candidates["snap-b"], NotNil) 1493 c.Check(candidates["base-snap-b"], NotNil) 1494 } 1495 1496 func (s *autorefreshGatingSuite) TestAutoRefreshPhase1ConflictsFilteredOut(c *C) { 1497 s.store.refreshedSnaps = []*snap.Info{{ 1498 Architectures: []string{"all"}, 1499 SnapType: snap.TypeApp, 1500 SideInfo: snap.SideInfo{ 1501 RealName: "snap-a", 1502 Revision: snap.R(8), 1503 }, 1504 }, { 1505 Architectures: []string{"all"}, 1506 SnapType: snap.TypeBase, 1507 SideInfo: snap.SideInfo{ 1508 RealName: "snap-c", 1509 Revision: snap.R(5), 1510 }, 1511 }} 1512 1513 st := s.state 1514 st.Lock() 1515 defer st.Unlock() 1516 1517 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1518 mockInstalledSnap(c, s.state, snapCyaml, noHook) 1519 1520 conflictChange := st.NewChange("conflicting change", "") 1521 conflictTask := st.NewTask("conflicting task", "") 1522 si := &snap.SideInfo{ 1523 RealName: "snap-c", 1524 Revision: snap.R(1), 1525 } 1526 sup := snapstate.SnapSetup{SideInfo: si} 1527 conflictTask.Set("snap-setup", sup) 1528 conflictChange.AddTask(conflictTask) 1529 1530 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1531 defer restore() 1532 1533 logbuf, restoreLogger := logger.MockLogger() 1534 defer restoreLogger() 1535 1536 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1537 c.Assert(err, IsNil) 1538 c.Check(names, DeepEquals, []string{"snap-a"}) 1539 c.Assert(tss, HasLen, 2) 1540 1541 c.Assert(tss[0].Tasks(), HasLen, 1) 1542 checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{ 1543 "snap-a": { 1544 SnapSetup: snapstate.SnapSetup{ 1545 Type: "app", 1546 PlugsOnly: true, 1547 Flags: snapstate.Flags{ 1548 IsAutoRefresh: true, 1549 }, 1550 SideInfo: &snap.SideInfo{ 1551 RealName: "snap-a", 1552 Revision: snap.R(8), 1553 }, 1554 DownloadInfo: &snap.DownloadInfo{}, 1555 }}}) 1556 1557 c.Assert(tss[1].Tasks(), HasLen, 1) 1558 1559 c.Assert(logbuf.String(), testutil.Contains, `cannot refresh snap "snap-c": snap "snap-c" has "conflicting change" change in progress`) 1560 1561 seenSnaps := make(map[string]bool) 1562 var hs hookstate.HookSetup 1563 c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil) 1564 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1565 seenSnaps[hs.Snap] = true 1566 1567 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true}) 1568 1569 // check that refresh-candidates in the state were updated 1570 var candidates map[string]*snapstate.RefreshCandidate 1571 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 1572 c.Assert(candidates, HasLen, 2) 1573 c.Check(candidates["snap-a"], NotNil) 1574 c.Check(candidates["snap-c"], NotNil) 1575 } 1576 1577 func (s *autorefreshGatingSuite) TestAutoRefreshPhase1NoHooks(c *C) { 1578 s.store.refreshedSnaps = []*snap.Info{{ 1579 Architectures: []string{"all"}, 1580 SnapType: snap.TypeBase, 1581 SideInfo: snap.SideInfo{ 1582 RealName: "base-snap-b", 1583 Revision: snap.R(3), 1584 }, 1585 }, { 1586 Architectures: []string{"all"}, 1587 SnapType: snap.TypeBase, 1588 SideInfo: snap.SideInfo{ 1589 RealName: "snap-c", 1590 Revision: snap.R(5), 1591 }, 1592 }} 1593 1594 st := s.state 1595 st.Lock() 1596 defer st.Unlock() 1597 1598 mockInstalledSnap(c, s.state, snapByaml, noHook) 1599 mockInstalledSnap(c, s.state, snapCyaml, noHook) 1600 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1601 1602 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1603 defer restore() 1604 1605 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1606 c.Assert(err, IsNil) 1607 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-c"}) 1608 c.Assert(tss, HasLen, 1) 1609 1610 c.Assert(tss[0].Tasks(), HasLen, 1) 1611 c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh") 1612 } 1613 1614 func fakeReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) { 1615 info := &snap.Info{ 1616 SuggestedName: name, 1617 SideInfo: *si, 1618 Architectures: []string{"all"}, 1619 SnapType: snap.TypeApp, 1620 Epoch: snap.Epoch{}, 1621 } 1622 switch name { 1623 case "base-snap-b": 1624 info.SnapType = snap.TypeBase 1625 case "snap-a", "snap-b": 1626 info.Hooks = map[string]*snap.HookInfo{ 1627 "gate-auto-refresh": { 1628 Name: "gate-auto-refresh", 1629 Snap: info, 1630 }, 1631 } 1632 if name == "snap-b" { 1633 info.Base = "base-snap-b" 1634 } 1635 } 1636 return info, nil 1637 } 1638 1639 func (s *snapmgrTestSuite) testAutoRefreshPhase2(c *C, beforePhase1 func(), gateAutoRefreshHook func(snapName string), expected []string) *state.Change { 1640 st := s.state 1641 st.Lock() 1642 defer st.Unlock() 1643 1644 s.o.TaskRunner().AddHandler("run-hook", func(t *state.Task, tomb *tomb.Tomb) error { 1645 var hsup hookstate.HookSetup 1646 t.State().Lock() 1647 defer t.State().Unlock() 1648 c.Assert(t.Get("hook-setup", &hsup), IsNil) 1649 if hsup.Hook == "gate-auto-refresh" && gateAutoRefreshHook != nil { 1650 gateAutoRefreshHook(hsup.Snap) 1651 } 1652 return nil 1653 }, nil) 1654 1655 restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) { 1656 c.Fatal("unexpected call to installSize") 1657 return 0, nil 1658 }) 1659 defer restoreInstallSize() 1660 1661 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 1662 fakeStore: s.fakeStore, 1663 refreshedSnaps: []*snap.Info{{ 1664 Architectures: []string{"all"}, 1665 SnapType: snap.TypeApp, 1666 SideInfo: snap.SideInfo{ 1667 RealName: "snap-a", 1668 Revision: snap.R(8), 1669 }, 1670 }, { 1671 Architectures: []string{"all"}, 1672 SnapType: snap.TypeBase, 1673 SideInfo: snap.SideInfo{ 1674 RealName: "base-snap-b", 1675 Revision: snap.R(3), 1676 }, 1677 }}}) 1678 1679 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1680 mockInstalledSnap(c, s.state, snapByaml, useHook) 1681 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1682 1683 snapstate.MockSnapReadInfo(fakeReadInfo) 1684 1685 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1686 defer restore() 1687 1688 if beforePhase1 != nil { 1689 beforePhase1() 1690 } 1691 1692 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1693 c.Assert(err, IsNil) 1694 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 1695 1696 chg := s.state.NewChange("refresh", "...") 1697 for _, ts := range tss { 1698 chg.AddAll(ts) 1699 } 1700 1701 s.state.Unlock() 1702 defer s.se.Stop() 1703 s.settle(c) 1704 s.state.Lock() 1705 1706 c.Check(chg.Status(), Equals, state.DoneStatus) 1707 c.Check(chg.Err(), IsNil) 1708 1709 verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected) 1710 1711 return chg 1712 } 1713 1714 func (s *snapmgrTestSuite) TestAutoRefreshPhase2(c *C) { 1715 expected := []string{ 1716 "conditional-auto-refresh", 1717 "run-hook [snap-a;gate-auto-refresh]", 1718 // snap-b hook is triggered because of base-snap-b refresh 1719 "run-hook [snap-b;gate-auto-refresh]", 1720 "prerequisites", 1721 "download-snap", 1722 "validate-snap", 1723 "mount-snap", 1724 "run-hook [base-snap-b;pre-refresh]", 1725 "stop-snap-services", 1726 "remove-aliases", 1727 "unlink-current-snap", 1728 "copy-snap-data", 1729 "setup-profiles", 1730 "link-snap", 1731 "auto-connect", 1732 "set-auto-aliases", 1733 "setup-aliases", 1734 "run-hook [base-snap-b;post-refresh]", 1735 "start-snap-services", 1736 "cleanup", 1737 "run-hook [base-snap-b;check-health]", 1738 "prerequisites", 1739 "download-snap", 1740 "validate-snap", 1741 "mount-snap", 1742 "run-hook [snap-a;pre-refresh]", 1743 "stop-snap-services", 1744 "remove-aliases", 1745 "unlink-current-snap", 1746 "copy-snap-data", 1747 "setup-profiles", 1748 "link-snap", 1749 "auto-connect", 1750 "set-auto-aliases", 1751 "setup-aliases", 1752 "run-hook [snap-a;post-refresh]", 1753 "start-snap-services", 1754 "cleanup", 1755 "run-hook [snap-a;configure]", 1756 "run-hook [snap-a;check-health]", 1757 "check-rerefresh", 1758 } 1759 1760 seenSnapsWithGateAutoRefreshHook := make(map[string]bool) 1761 1762 chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) { 1763 seenSnapsWithGateAutoRefreshHook[snapName] = true 1764 }, expected) 1765 1766 c.Check(seenSnapsWithGateAutoRefreshHook, DeepEquals, map[string]bool{ 1767 "snap-a": true, 1768 "snap-b": true, 1769 }) 1770 1771 s.state.Lock() 1772 defer s.state.Unlock() 1773 1774 tasks := chg.Tasks() 1775 c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b", "snap-a" as needed`) 1776 1777 // all snaps refreshed, all removed from refresh-candidates. 1778 var candidates map[string]*snapstate.RefreshCandidate 1779 c.Assert(s.state.Get("refresh-candidates", &candidates), IsNil) 1780 c.Assert(candidates, HasLen, 0) 1781 } 1782 1783 func (s *snapmgrTestSuite) TestAutoRefreshPhase2Held(c *C) { 1784 logbuf, restoreLogger := logger.MockLogger() 1785 defer restoreLogger() 1786 1787 expected := []string{ 1788 "conditional-auto-refresh", 1789 "run-hook [snap-a;gate-auto-refresh]", 1790 // snap-b hook is triggered because of base-snap-b refresh 1791 "run-hook [snap-b;gate-auto-refresh]", 1792 "prerequisites", 1793 "download-snap", 1794 "validate-snap", 1795 "mount-snap", 1796 "run-hook [snap-a;pre-refresh]", 1797 "stop-snap-services", 1798 "remove-aliases", 1799 "unlink-current-snap", 1800 "copy-snap-data", 1801 "setup-profiles", 1802 "link-snap", 1803 "auto-connect", 1804 "set-auto-aliases", 1805 "setup-aliases", 1806 "run-hook [snap-a;post-refresh]", 1807 "start-snap-services", 1808 "cleanup", 1809 "run-hook [snap-a;configure]", 1810 "run-hook [snap-a;check-health]", 1811 "check-rerefresh", 1812 } 1813 1814 chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) { 1815 if snapName == "snap-b" { 1816 // pretend than snap-b calls snapctl --hold to hold refresh of base-snap-b 1817 c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil) 1818 } 1819 }, expected) 1820 1821 s.state.Lock() 1822 defer s.state.Unlock() 1823 1824 c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`) 1825 tasks := chg.Tasks() 1826 // no re-refresh for base-snap-b because it was held. 1827 c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "snap-a" as needed`) 1828 } 1829 1830 func (s *snapmgrTestSuite) TestAutoRefreshPhase2Proceed(c *C) { 1831 logbuf, restoreLogger := logger.MockLogger() 1832 defer restoreLogger() 1833 1834 expected := []string{ 1835 "conditional-auto-refresh", 1836 "run-hook [snap-a;gate-auto-refresh]", 1837 // snap-b hook is triggered because of base-snap-b refresh 1838 "run-hook [snap-b;gate-auto-refresh]", 1839 "prerequisites", 1840 "download-snap", 1841 "validate-snap", 1842 "mount-snap", 1843 "run-hook [snap-a;pre-refresh]", 1844 "stop-snap-services", 1845 "remove-aliases", 1846 "unlink-current-snap", 1847 "copy-snap-data", 1848 "setup-profiles", 1849 "link-snap", 1850 "auto-connect", 1851 "set-auto-aliases", 1852 "setup-aliases", 1853 "run-hook [snap-a;post-refresh]", 1854 "start-snap-services", 1855 "cleanup", 1856 "run-hook [snap-a;configure]", 1857 "run-hook [snap-a;check-health]", 1858 "check-rerefresh", 1859 } 1860 1861 s.testAutoRefreshPhase2(c, func() { 1862 // pretend that snap-a and base-snap-b are initially held 1863 c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil) 1864 c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil) 1865 }, func(snapName string) { 1866 if snapName == "snap-a" { 1867 // pretend than snap-a calls snapctl --proceed 1868 c.Assert(snapstate.ProceedWithRefresh(s.state, "snap-a"), IsNil) 1869 } 1870 // note, do nothing about snap-b which just keeps its hold state in 1871 // the test, but if we were using real gate-auto-refresh hook 1872 // handler, the default behavior for snap-b if it doesn't call --hold 1873 // would be to proceed (hook handler would take care of that). 1874 }, expected) 1875 1876 c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`) 1877 } 1878 1879 func (s *snapmgrTestSuite) TestAutoRefreshPhase2AllHeld(c *C) { 1880 logbuf, restoreLogger := logger.MockLogger() 1881 defer restoreLogger() 1882 1883 expected := []string{ 1884 "conditional-auto-refresh", 1885 "run-hook [snap-a;gate-auto-refresh]", 1886 // snap-b hook is triggered because of base-snap-b refresh 1887 "run-hook [snap-b;gate-auto-refresh]", 1888 } 1889 1890 s.testAutoRefreshPhase2(c, nil, func(snapName string) { 1891 switch snapName { 1892 case "snap-b": 1893 // pretend that snap-b calls snapctl --hold to hold refresh of base-snap-b 1894 c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil) 1895 case "snap-a": 1896 // pretend that snap-a calls snapctl --hold to hold itself 1897 c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil) 1898 default: 1899 c.Fatalf("unexpected snap %q", snapName) 1900 } 1901 }, expected) 1902 1903 c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b,snap-a`) 1904 } 1905 1906 func (s *snapmgrTestSuite) testAutoRefreshPhase2DiskSpaceCheck(c *C, fail bool) { 1907 st := s.state 1908 st.Lock() 1909 defer st.Unlock() 1910 1911 restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error { 1912 c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123)) 1913 if fail { 1914 return &osutil.NotEnoughDiskSpaceError{} 1915 } 1916 return nil 1917 }) 1918 defer restore() 1919 1920 var installSizeCalled bool 1921 restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) { 1922 installSizeCalled = true 1923 seen := map[string]bool{} 1924 for _, sn := range snaps { 1925 seen[sn.InstanceName()] = true 1926 } 1927 c.Check(seen, DeepEquals, map[string]bool{ 1928 "base-snap-b": true, 1929 "snap-a": true, 1930 }) 1931 return 123, nil 1932 }) 1933 defer restoreInstallSize() 1934 1935 restoreModel := snapstatetest.MockDeviceModel(DefaultModel()) 1936 defer restoreModel() 1937 1938 tr := config.NewTransaction(s.state) 1939 tr.Set("core", "experimental.check-disk-space-refresh", true) 1940 tr.Commit() 1941 1942 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 1943 fakeStore: s.fakeStore, 1944 refreshedSnaps: []*snap.Info{{ 1945 Architectures: []string{"all"}, 1946 SnapType: snap.TypeApp, 1947 SideInfo: snap.SideInfo{ 1948 RealName: "snap-a", 1949 Revision: snap.R(8), 1950 }, 1951 }, { 1952 Architectures: []string{"all"}, 1953 SnapType: snap.TypeBase, 1954 SideInfo: snap.SideInfo{ 1955 RealName: "base-snap-b", 1956 Revision: snap.R(3), 1957 }, 1958 }}}) 1959 1960 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1961 mockInstalledSnap(c, s.state, snapByaml, useHook) 1962 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1963 1964 snapstate.MockSnapReadInfo(fakeReadInfo) 1965 1966 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1967 c.Assert(err, IsNil) 1968 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 1969 1970 chg := s.state.NewChange("refresh", "...") 1971 for _, ts := range tss { 1972 chg.AddAll(ts) 1973 } 1974 1975 s.state.Unlock() 1976 defer s.se.Stop() 1977 s.settle(c) 1978 s.state.Lock() 1979 1980 c.Check(installSizeCalled, Equals, true) 1981 if fail { 1982 c.Check(chg.Status(), Equals, state.ErrorStatus) 1983 c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n- Run auto-refresh for ready snaps \(insufficient space.*`) 1984 } else { 1985 c.Check(chg.Status(), Equals, state.DoneStatus) 1986 c.Check(chg.Err(), IsNil) 1987 } 1988 } 1989 1990 func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceError(c *C) { 1991 fail := true 1992 s.testAutoRefreshPhase2DiskSpaceCheck(c, fail) 1993 } 1994 1995 func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceHappy(c *C) { 1996 var nofail bool 1997 s.testAutoRefreshPhase2DiskSpaceCheck(c, nofail) 1998 } 1999 2000 // XXX: this case is probably artificial; with proper conflict prevention 2001 // we shouldn't get conflicts from doInstall in phase2. 2002 func (s *snapmgrTestSuite) TestAutoRefreshPhase2Conflict(c *C) { 2003 st := s.state 2004 st.Lock() 2005 defer st.Unlock() 2006 2007 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 2008 fakeStore: s.fakeStore, 2009 refreshedSnaps: []*snap.Info{{ 2010 Architectures: []string{"all"}, 2011 SnapType: snap.TypeApp, 2012 SideInfo: snap.SideInfo{ 2013 RealName: "snap-a", 2014 Revision: snap.R(8), 2015 }, 2016 }, { 2017 Architectures: []string{"all"}, 2018 SnapType: snap.TypeBase, 2019 SideInfo: snap.SideInfo{ 2020 RealName: "base-snap-b", 2021 Revision: snap.R(3), 2022 }, 2023 }}}) 2024 2025 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2026 mockInstalledSnap(c, s.state, snapByaml, useHook) 2027 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 2028 2029 snapstate.MockSnapReadInfo(fakeReadInfo) 2030 2031 restore := snapstatetest.MockDeviceModel(DefaultModel()) 2032 defer restore() 2033 2034 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 2035 c.Assert(err, IsNil) 2036 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 2037 2038 chg := s.state.NewChange("refresh", "...") 2039 for _, ts := range tss { 2040 chg.AddAll(ts) 2041 } 2042 2043 conflictChange := st.NewChange("conflicting change", "") 2044 conflictTask := st.NewTask("conflicting task", "") 2045 si := &snap.SideInfo{ 2046 RealName: "snap-a", 2047 Revision: snap.R(1), 2048 } 2049 sup := snapstate.SnapSetup{SideInfo: si} 2050 conflictTask.Set("snap-setup", sup) 2051 conflictChange.AddTask(conflictTask) 2052 conflictTask.WaitFor(tss[0].Tasks()[0]) 2053 2054 s.state.Unlock() 2055 defer s.se.Stop() 2056 s.settle(c) 2057 s.state.Lock() 2058 2059 c.Assert(chg.Status(), Equals, state.DoneStatus) 2060 c.Check(chg.Err(), IsNil) 2061 2062 // no refresh of snap-a because of the conflict. 2063 expected := []string{ 2064 "conditional-auto-refresh", 2065 "run-hook [snap-a;gate-auto-refresh]", 2066 // snap-b hook is triggered because of base-snap-b refresh 2067 "run-hook [snap-b;gate-auto-refresh]", 2068 "prerequisites", 2069 "download-snap", 2070 "validate-snap", 2071 "mount-snap", 2072 "run-hook [base-snap-b;pre-refresh]", 2073 "stop-snap-services", 2074 "remove-aliases", 2075 "unlink-current-snap", 2076 "copy-snap-data", 2077 "setup-profiles", 2078 "link-snap", 2079 "auto-connect", 2080 "set-auto-aliases", 2081 "setup-aliases", 2082 "run-hook [base-snap-b;post-refresh]", 2083 "start-snap-services", 2084 "cleanup", 2085 "run-hook [base-snap-b;check-health]", 2086 "check-rerefresh", 2087 } 2088 verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected) 2089 } 2090 2091 func (s *snapmgrTestSuite) TestAutoRefreshPhase2ConflictOtherSnapOp(c *C) { 2092 st := s.state 2093 st.Lock() 2094 defer st.Unlock() 2095 2096 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 2097 fakeStore: s.fakeStore, 2098 refreshedSnaps: []*snap.Info{{ 2099 Architectures: []string{"all"}, 2100 SnapType: snap.TypeApp, 2101 SideInfo: snap.SideInfo{ 2102 RealName: "snap-a", 2103 Revision: snap.R(8), 2104 }, 2105 }}}) 2106 2107 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2108 2109 snapstate.MockSnapReadInfo(fakeReadInfo) 2110 2111 restore := snapstatetest.MockDeviceModel(DefaultModel()) 2112 defer restore() 2113 2114 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 2115 c.Assert(err, IsNil) 2116 c.Check(names, DeepEquals, []string{"snap-a"}) 2117 2118 chg := s.state.NewChange("fake-auto-refresh", "...") 2119 for _, ts := range tss { 2120 chg.AddAll(ts) 2121 } 2122 2123 s.state.Unlock() 2124 // run first task 2125 s.se.Ensure() 2126 s.se.Wait() 2127 2128 s.state.Lock() 2129 2130 _, err = snapstate.Remove(s.state, "snap-a", snap.R(8), nil) 2131 c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ 2132 ChangeKind: "fake-auto-refresh", 2133 Snap: "snap-a", 2134 }) 2135 2136 _, err = snapstate.Update(s.state, "snap-a", nil, 0, snapstate.Flags{}) 2137 c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ 2138 ChangeKind: "fake-auto-refresh", 2139 Snap: "snap-a", 2140 }) 2141 2142 // only 2 tasks because we don't run settle() so conditional-auto-refresh 2143 // doesn't run and no new tasks get created. 2144 expected := []string{ 2145 "conditional-auto-refresh", 2146 "run-hook [snap-a;gate-auto-refresh]", 2147 } 2148 verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected) 2149 } 2150 2151 func (s *snapmgrTestSuite) TestAutoRefreshPhase2GatedSnaps(c *C) { 2152 st := s.state 2153 st.Lock() 2154 defer st.Unlock() 2155 2156 restore := snapstate.MockSnapsToRefresh(func(gatingTask *state.Task) ([]*snapstate.RefreshCandidate, error) { 2157 c.Assert(gatingTask.Kind(), Equals, "conditional-auto-refresh") 2158 var candidates map[string]*snapstate.RefreshCandidate 2159 c.Assert(gatingTask.Get("snaps", &candidates), IsNil) 2160 seenSnaps := make(map[string]bool) 2161 var filteredByGatingHooks []*snapstate.RefreshCandidate 2162 for _, cand := range candidates { 2163 seenSnaps[cand.InstanceName()] = true 2164 if cand.InstanceName() == "snap-a" { 2165 continue 2166 } 2167 filteredByGatingHooks = append(filteredByGatingHooks, cand) 2168 } 2169 c.Check(seenSnaps, DeepEquals, map[string]bool{ 2170 "snap-a": true, 2171 "base-snap-b": true, 2172 }) 2173 return filteredByGatingHooks, nil 2174 }) 2175 defer restore() 2176 2177 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 2178 fakeStore: s.fakeStore, 2179 refreshedSnaps: []*snap.Info{ 2180 { 2181 Architectures: []string{"all"}, 2182 SnapType: snap.TypeApp, 2183 SideInfo: snap.SideInfo{ 2184 RealName: "snap-a", 2185 Revision: snap.R(8), 2186 }, 2187 }, { 2188 Architectures: []string{"all"}, 2189 SnapType: snap.TypeBase, 2190 SideInfo: snap.SideInfo{ 2191 RealName: "base-snap-b", 2192 Revision: snap.R(3), 2193 }, 2194 }, 2195 }}) 2196 2197 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2198 mockInstalledSnap(c, s.state, snapByaml, useHook) 2199 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 2200 2201 snapstate.MockSnapReadInfo(fakeReadInfo) 2202 2203 restoreModel := snapstatetest.MockDeviceModel(DefaultModel()) 2204 defer restoreModel() 2205 2206 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 2207 c.Assert(err, IsNil) 2208 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 2209 2210 chg := s.state.NewChange("refresh", "...") 2211 for _, ts := range tss { 2212 chg.AddAll(ts) 2213 } 2214 2215 s.state.Unlock() 2216 defer s.se.Stop() 2217 s.settle(c) 2218 s.state.Lock() 2219 2220 c.Assert(chg.Status(), Equals, state.DoneStatus) 2221 c.Check(chg.Err(), IsNil) 2222 2223 expected := []string{ 2224 "conditional-auto-refresh", 2225 "run-hook [snap-a;gate-auto-refresh]", 2226 // snap-b hook is triggered because of base-snap-b refresh 2227 "run-hook [snap-b;gate-auto-refresh]", 2228 "prerequisites", 2229 "download-snap", 2230 "validate-snap", 2231 "mount-snap", 2232 "run-hook [base-snap-b;pre-refresh]", 2233 "stop-snap-services", 2234 "remove-aliases", 2235 "unlink-current-snap", 2236 "copy-snap-data", 2237 "setup-profiles", 2238 "link-snap", 2239 "auto-connect", 2240 "set-auto-aliases", 2241 "setup-aliases", 2242 "run-hook [base-snap-b;post-refresh]", 2243 "start-snap-services", 2244 "cleanup", 2245 "run-hook [base-snap-b;check-health]", 2246 "check-rerefresh", 2247 } 2248 tasks := chg.Tasks() 2249 verifyPhasedAutorefreshTasks(c, tasks, expected) 2250 // no re-refresh for snap-a because it was held. 2251 c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b" as needed`) 2252 2253 // only snap-a remains in refresh-candidates because it was held; 2254 // base-snap-b got pruned (was refreshed). 2255 var candidates map[string]*snapstate.RefreshCandidate 2256 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 2257 c.Assert(candidates, HasLen, 1) 2258 c.Check(candidates["snap-a"], NotNil) 2259 } 2260 2261 func (s *snapmgrTestSuite) TestAutoRefreshForGatingSnapErrorAutoRefreshInProgress(c *C) { 2262 st := s.state 2263 st.Lock() 2264 defer st.Unlock() 2265 2266 chg := st.NewChange("auto-refresh", "...") 2267 task := st.NewTask("foo", "...") 2268 chg.AddTask(task) 2269 2270 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-a"), ErrorMatches, `there is an auto-refresh in progress`) 2271 } 2272 2273 func (s *snapmgrTestSuite) TestAutoRefreshForGatingSnapErrorNothingHeld(c *C) { 2274 st := s.state 2275 st.Lock() 2276 defer st.Unlock() 2277 2278 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-a"), ErrorMatches, `no snaps are held by snap "snap-a"`) 2279 } 2280 2281 func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnap(c *C) { 2282 s.store.refreshedSnaps = []*snap.Info{{ 2283 Architectures: []string{"all"}, 2284 SnapType: snap.TypeApp, 2285 SideInfo: snap.SideInfo{ 2286 RealName: "snap-a", 2287 Revision: snap.R(8), 2288 }, 2289 }, { 2290 Architectures: []string{"all"}, 2291 SnapType: snap.TypeApp, 2292 Base: "base-snap-b", 2293 SideInfo: snap.SideInfo{ 2294 RealName: "snap-b", 2295 Revision: snap.R(2), 2296 }, 2297 }, { 2298 Architectures: []string{"all"}, 2299 SnapType: snap.TypeBase, 2300 SideInfo: snap.SideInfo{ 2301 RealName: "base-snap-b", 2302 Revision: snap.R(3), 2303 }, 2304 }} 2305 2306 st := s.state 2307 st.Lock() 2308 defer st.Unlock() 2309 2310 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2311 mockInstalledSnap(c, s.state, snapByaml, useHook) 2312 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 2313 2314 restore := snapstatetest.MockDeviceModel(DefaultModel()) 2315 defer restore() 2316 2317 // pretend some snaps are held 2318 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil) 2319 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil) 2320 2321 lastRefreshTime := time.Now().Add(-99 * time.Hour) 2322 st.Set("last-refresh", lastRefreshTime) 2323 2324 // pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed) 2325 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil) 2326 2327 changes := st.Changes() 2328 c.Assert(changes, HasLen, 1) 2329 chg := changes[0] 2330 c.Assert(chg.Kind(), Equals, "auto-refresh") 2331 c.Check(chg.Summary(), Equals, `Auto-refresh snaps "base-snap-b", "snap-b"`) 2332 var snapNames []string 2333 var apiData map[string]interface{} 2334 c.Assert(chg.Get("snap-names", &snapNames), IsNil) 2335 c.Check(snapNames, DeepEquals, []string{"base-snap-b", "snap-b"}) 2336 c.Assert(chg.Get("api-data", &apiData), IsNil) 2337 c.Check(apiData, DeepEquals, map[string]interface{}{ 2338 "snap-names": []interface{}{"base-snap-b", "snap-b"}, 2339 }) 2340 2341 tasks := chg.Tasks() 2342 c.Assert(tasks, HasLen, 2) 2343 conditionalRefreshTask := tasks[0] 2344 checkGatingTask(c, conditionalRefreshTask, map[string]*snapstate.RefreshCandidate{ 2345 "base-snap-b": { 2346 SnapSetup: snapstate.SnapSetup{ 2347 Type: "base", 2348 PlugsOnly: true, 2349 Flags: snapstate.Flags{ 2350 IsAutoRefresh: true, 2351 }, 2352 SideInfo: &snap.SideInfo{ 2353 RealName: "base-snap-b", 2354 Revision: snap.R(3), 2355 }, 2356 DownloadInfo: &snap.DownloadInfo{}, 2357 }, 2358 }, 2359 "snap-b": { 2360 SnapSetup: snapstate.SnapSetup{ 2361 Type: "app", 2362 Base: "base-snap-b", 2363 PlugsOnly: true, 2364 Flags: snapstate.Flags{ 2365 IsAutoRefresh: true, 2366 }, 2367 SideInfo: &snap.SideInfo{ 2368 RealName: "snap-b", 2369 Revision: snap.R(2), 2370 }, 2371 DownloadInfo: &snap.DownloadInfo{}, 2372 }, 2373 }, 2374 }) 2375 2376 // the gate-auto-refresh hook task for snap-b is present 2377 c.Check(tasks[1].Kind(), Equals, "run-hook") 2378 var hs hookstate.HookSetup 2379 c.Assert(tasks[1].Get("hook-setup", &hs), IsNil) 2380 c.Check(hs.Hook, Equals, "gate-auto-refresh") 2381 c.Check(hs.Snap, Equals, "snap-b") 2382 c.Check(hs.Optional, Equals, true) 2383 2384 var data interface{} 2385 c.Assert(tasks[1].Get("hook-context", &data), IsNil) 2386 c.Check(data, DeepEquals, map[string]interface{}{ 2387 "base": true, 2388 "restart": false, 2389 "affecting-snaps": []interface{}{"base-snap-b", "snap-b"}, 2390 }) 2391 2392 // last-refresh wasn't modified 2393 var lr time.Time 2394 st.Get("last-refresh", &lr) 2395 c.Check(lr.Equal(lastRefreshTime), Equals, true) 2396 } 2397 2398 func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnapMoreAffectedSnaps(c *C) { 2399 s.store.refreshedSnaps = []*snap.Info{{ 2400 Architectures: []string{"all"}, 2401 SnapType: snap.TypeApp, 2402 SideInfo: snap.SideInfo{ 2403 RealName: "snap-a", 2404 Revision: snap.R(8), 2405 }, 2406 }, { 2407 Architectures: []string{"all"}, 2408 SnapType: snap.TypeBase, 2409 SideInfo: snap.SideInfo{ 2410 RealName: "base-snap-b", 2411 Revision: snap.R(3), 2412 }, 2413 }, { 2414 Architectures: []string{"all"}, 2415 SnapType: snap.TypeApp, 2416 Base: "base-snap-b", 2417 SideInfo: snap.SideInfo{ 2418 RealName: "snap-b", 2419 Revision: snap.R(2), 2420 }, 2421 }} 2422 2423 st := s.state 2424 st.Lock() 2425 defer st.Unlock() 2426 2427 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2428 mockInstalledSnap(c, s.state, snapByaml, useHook) 2429 mockInstalledSnap(c, s.state, snapBByaml, useHook) 2430 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 2431 2432 restore := snapstatetest.MockDeviceModel(DefaultModel()) 2433 defer restore() 2434 2435 // pretend snap-b holds base-snap-b. 2436 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil) 2437 2438 // pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed) 2439 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil) 2440 2441 changes := st.Changes() 2442 c.Assert(changes, HasLen, 1) 2443 chg := changes[0] 2444 c.Assert(chg.Kind(), Equals, "auto-refresh") 2445 c.Check(chg.Summary(), Equals, `Auto-refresh snaps "base-snap-b", "snap-b"`) 2446 var snapNames []string 2447 var apiData map[string]interface{} 2448 c.Assert(chg.Get("snap-names", &snapNames), IsNil) 2449 c.Check(snapNames, DeepEquals, []string{"base-snap-b", "snap-b"}) 2450 c.Assert(chg.Get("api-data", &apiData), IsNil) 2451 c.Check(apiData, DeepEquals, map[string]interface{}{ 2452 "snap-names": []interface{}{"base-snap-b", "snap-b"}, 2453 }) 2454 2455 tasks := chg.Tasks() 2456 c.Assert(tasks, HasLen, 3) 2457 conditionalRefreshTask := tasks[0] 2458 checkGatingTask(c, conditionalRefreshTask, map[string]*snapstate.RefreshCandidate{ 2459 "base-snap-b": { 2460 SnapSetup: snapstate.SnapSetup{ 2461 Type: "base", 2462 PlugsOnly: true, 2463 Flags: snapstate.Flags{ 2464 IsAutoRefresh: true, 2465 }, 2466 SideInfo: &snap.SideInfo{ 2467 RealName: "base-snap-b", 2468 Revision: snap.R(3), 2469 }, 2470 DownloadInfo: &snap.DownloadInfo{}, 2471 }, 2472 }, 2473 "snap-b": { 2474 SnapSetup: snapstate.SnapSetup{ 2475 Type: "app", 2476 Base: "base-snap-b", 2477 PlugsOnly: true, 2478 Flags: snapstate.Flags{ 2479 IsAutoRefresh: true, 2480 }, 2481 SideInfo: &snap.SideInfo{ 2482 RealName: "snap-b", 2483 Revision: snap.R(2), 2484 }, 2485 DownloadInfo: &snap.DownloadInfo{}, 2486 }, 2487 }, 2488 }) 2489 2490 seenSnaps := make(map[string]bool) 2491 2492 // check that the gate-auto-refresh hooks are run. 2493 // snap-bb's hook is triggered because it is affected by base-snap-b refresh 2494 // (and intersects with affecting snap of snap-b). Note, snap-a is not here 2495 // because it is not affected by snaps affecting snap-b. 2496 for i := 1; i <= 2; i++ { 2497 c.Assert(tasks[i].Kind(), Equals, "run-hook") 2498 var hs hookstate.HookSetup 2499 c.Assert(tasks[i].Get("hook-setup", &hs), IsNil) 2500 c.Check(hs.Hook, Equals, "gate-auto-refresh") 2501 c.Check(hs.Optional, Equals, true) 2502 seenSnaps[hs.Snap] = true 2503 var data interface{} 2504 c.Assert(tasks[i].Get("hook-context", &data), IsNil) 2505 switch hs.Snap { 2506 case "snap-b": 2507 c.Check(data, DeepEquals, map[string]interface{}{ 2508 "base": true, 2509 "restart": false, 2510 "affecting-snaps": []interface{}{"base-snap-b", "snap-b"}, 2511 }) 2512 case "snap-bb": 2513 c.Check(data, DeepEquals, map[string]interface{}{ 2514 "base": true, 2515 "restart": false, 2516 "affecting-snaps": []interface{}{"base-snap-b"}, 2517 }) 2518 default: 2519 c.Fatalf("unexpected snap %q", hs.Snap) 2520 } 2521 } 2522 c.Check(seenSnaps, DeepEquals, map[string]bool{ 2523 "snap-b": true, 2524 "snap-bb": true, 2525 }) 2526 } 2527 2528 func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnapNoCandidatesAnymore(c *C) { 2529 // only snap-a will have a refresh available 2530 s.store.refreshedSnaps = []*snap.Info{{ 2531 Architectures: []string{"all"}, 2532 SnapType: snap.TypeApp, 2533 SideInfo: snap.SideInfo{ 2534 RealName: "snap-a", 2535 Revision: snap.R(8), 2536 }, 2537 }} 2538 2539 logbuf, restoreLogger := logger.MockLogger() 2540 defer restoreLogger() 2541 2542 st := s.state 2543 st.Lock() 2544 defer st.Unlock() 2545 2546 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2547 mockInstalledSnap(c, s.state, snapByaml, useHook) 2548 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 2549 2550 restore := snapstatetest.MockDeviceModel(DefaultModel()) 2551 defer restore() 2552 2553 // pretend some snaps are held 2554 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil) 2555 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil) 2556 2557 // pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed) 2558 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil) 2559 c.Assert(st.Changes(), HasLen, 0) 2560 2561 // but base-snap-b has no update anymore. 2562 c.Check(logbuf.String(), testutil.Contains, `auto-refresh: all snaps previously held by "snap-b" are up-to-date`) 2563 } 2564 2565 func verifyPhasedAutorefreshTasks(c *C, tasks []*state.Task, expected []string) { 2566 c.Assert(len(tasks), Equals, len(expected)) 2567 for i, t := range tasks { 2568 var got string 2569 if t.Kind() == "run-hook" { 2570 var hsup hookstate.HookSetup 2571 c.Assert(t.Get("hook-setup", &hsup), IsNil) 2572 got = fmt.Sprintf("%s [%s;%s]", t.Kind(), hsup.Snap, hsup.Hook) 2573 } else { 2574 got = t.Kind() 2575 } 2576 c.Assert(got, Equals, expected[i], Commentf("#%d", i)) 2577 } 2578 }