github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/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 func (s *autorefreshGatingSuite) TestPruneGatingHelper(c *C) { 585 st := s.state 586 st.Lock() 587 defer st.Unlock() 588 589 restore := snapstate.MockTimeNow(func() time.Time { 590 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 591 c.Assert(err, IsNil) 592 return t 593 }) 594 defer restore() 595 596 mockInstalledSnap(c, st, snapAyaml, false) 597 mockInstalledSnap(c, st, snapByaml, false) 598 mockInstalledSnap(c, st, snapCyaml, false) 599 mockInstalledSnap(c, st, snapDyaml, false) 600 601 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) 602 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil) 603 // sanity 604 held, err := snapstate.HeldSnaps(st) 605 c.Assert(err, IsNil) 606 c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-b": true, "snap-d": true}) 607 608 candidates := map[string]*snapstate.RefreshCandidate{"snap-c": {}} 609 610 // only snap-c has a refresh candidate, snap-b and snap-d should be forgotten. 611 c.Assert(snapstate.PruneGating(st, candidates), IsNil) 612 var gating map[string]map[string]*snapstate.HoldState 613 c.Assert(st.Get("snaps-hold", &gating), IsNil) 614 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 615 "snap-c": { 616 "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 617 "snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), 618 }, 619 }) 620 held, err = snapstate.HeldSnaps(st) 621 c.Assert(err, IsNil) 622 c.Check(held, DeepEquals, map[string]bool{"snap-c": true}) 623 } 624 625 func (s *autorefreshGatingSuite) TestPruneGatingHelperNoGating(c *C) { 626 st := s.state 627 st.Lock() 628 defer st.Unlock() 629 630 restore := snapstate.MockTimeNow(func() time.Time { 631 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 632 c.Assert(err, IsNil) 633 return t 634 }) 635 defer restore() 636 637 mockInstalledSnap(c, st, snapAyaml, false) 638 639 held, err := snapstate.HeldSnaps(st) 640 c.Assert(err, IsNil) 641 c.Check(held, HasLen, 0) 642 643 snapstate.MockTimeNow(func() time.Time { 644 c.Fatalf("not expected") 645 return time.Time{} 646 }) 647 648 candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}} 649 c.Assert(snapstate.PruneGating(st, candidates), IsNil) 650 held, err = snapstate.HeldSnaps(st) 651 c.Assert(err, IsNil) 652 c.Check(held, HasLen, 0) 653 } 654 655 func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelper(c *C) { 656 st := s.state 657 st.Lock() 658 defer st.Unlock() 659 660 restore := snapstate.MockTimeNow(func() time.Time { 661 t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") 662 c.Assert(err, IsNil) 663 return t 664 }) 665 defer restore() 666 667 mockInstalledSnap(c, st, snapAyaml, false) 668 mockInstalledSnap(c, st, snapByaml, false) 669 mockInstalledSnap(c, st, snapCyaml, false) 670 mockInstalledSnap(c, st, snapDyaml, false) 671 672 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) 673 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil) 674 675 c.Assert(snapstate.ResetGatingForRefreshed(st, "snap-b", "snap-c"), IsNil) 676 var gating map[string]map[string]*snapstate.HoldState 677 c.Assert(st.Get("snaps-hold", &gating), IsNil) 678 c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ 679 "snap-d": { 680 // holding self set for maxPostponement (95 days - buffer = 90 days) 681 "snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-08T10:00:00Z"), 682 }, 683 }) 684 685 held, err := snapstate.HeldSnaps(st) 686 c.Assert(err, IsNil) 687 c.Check(held, DeepEquals, map[string]bool{"snap-d": true}) 688 } 689 690 func (s *autorefreshGatingSuite) TestPruneSnapsHold(c *C) { 691 st := s.state 692 st.Lock() 693 defer st.Unlock() 694 695 mockInstalledSnap(c, st, snapAyaml, false) 696 mockInstalledSnap(c, st, snapByaml, false) 697 mockInstalledSnap(c, st, snapCyaml, false) 698 mockInstalledSnap(c, st, snapDyaml, false) 699 700 // snap-a is holding itself and 3 other snaps 701 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a", "snap-b", "snap-c", "snap-d"), IsNil) 702 // in addition, snap-c is held by snap-d. 703 c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil) 704 705 // sanity check 706 held, err := snapstate.HeldSnaps(st) 707 c.Assert(err, IsNil) 708 c.Check(held, DeepEquals, map[string]bool{ 709 "snap-a": true, 710 "snap-b": true, 711 "snap-c": true, 712 "snap-d": true, 713 }) 714 715 c.Check(snapstate.PruneSnapsHold(st, "snap-a"), IsNil) 716 717 // after pruning snap-a, snap-c is still held. 718 held, err = snapstate.HeldSnaps(st) 719 c.Assert(err, IsNil) 720 c.Check(held, DeepEquals, map[string]bool{ 721 "snap-c": true, 722 }) 723 var gating map[string]map[string]*snapstate.HoldState 724 c.Assert(st.Get("snaps-hold", &gating), IsNil) 725 c.Assert(gating, HasLen, 1) 726 c.Check(gating["snap-c"], HasLen, 1) 727 c.Check(gating["snap-c"]["snap-d"], NotNil) 728 } 729 730 const useHook = true 731 const noHook = false 732 733 func checkGatingTask(c *C, task *state.Task, expected map[string]*snapstate.RefreshCandidate) { 734 c.Assert(task.Kind(), Equals, "conditional-auto-refresh") 735 var snaps map[string]*snapstate.RefreshCandidate 736 c.Assert(task.Get("snaps", &snaps), IsNil) 737 c.Check(snaps, DeepEquals, expected) 738 } 739 740 func (s *autorefreshGatingSuite) TestAffectedByBase(c *C) { 741 restore := release.MockOnClassic(true) 742 defer restore() 743 744 st := s.state 745 746 st.Lock() 747 defer st.Unlock() 748 mockInstalledSnap(c, s.state, snapAyaml, useHook) 749 baseSnapA := mockInstalledSnap(c, s.state, baseSnapAyaml, noHook) 750 // unrelated snaps 751 snapB := mockInstalledSnap(c, s.state, snapByaml, useHook) 752 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 753 754 c.Assert(s.repo.AddSnap(snapB), IsNil) 755 756 updates := []string{baseSnapA.InstanceName()} 757 affected, err := snapstate.AffectedByRefresh(st, updates) 758 c.Assert(err, IsNil) 759 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 760 "snap-a": { 761 Base: true, 762 AffectingSnaps: map[string]bool{ 763 "base-snap-a": true, 764 }}}) 765 } 766 767 func (s *autorefreshGatingSuite) TestAffectedByCore(c *C) { 768 restore := release.MockOnClassic(true) 769 defer restore() 770 771 st := s.state 772 773 st.Lock() 774 defer st.Unlock() 775 snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook) 776 core := mockInstalledSnap(c, s.state, coreYaml, noHook) 777 snapB := mockInstalledSnap(c, s.state, snapByaml, useHook) 778 779 c.Assert(s.repo.AddSnap(core), IsNil) 780 c.Assert(s.repo.AddSnap(snapB), IsNil) 781 c.Assert(s.repo.AddSnap(snapC), IsNil) 782 783 updates := []string{core.InstanceName()} 784 affected, err := snapstate.AffectedByRefresh(st, updates) 785 c.Assert(err, IsNil) 786 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 787 "snap-c": { 788 Base: true, 789 AffectingSnaps: map[string]bool{ 790 "core": true, 791 }}}) 792 } 793 794 func (s *autorefreshGatingSuite) TestAffectedByKernel(c *C) { 795 restore := release.MockOnClassic(true) 796 defer restore() 797 798 st := s.state 799 800 st.Lock() 801 defer st.Unlock() 802 kernel := mockInstalledSnap(c, s.state, kernelYaml, noHook) 803 mockInstalledSnap(c, s.state, snapCyaml, useHook) 804 mockInstalledSnap(c, s.state, snapByaml, noHook) 805 806 updates := []string{kernel.InstanceName()} 807 affected, err := snapstate.AffectedByRefresh(st, updates) 808 c.Assert(err, IsNil) 809 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 810 "snap-c": { 811 Restart: true, 812 AffectingSnaps: map[string]bool{ 813 "kernel": true, 814 }}}) 815 } 816 817 func (s *autorefreshGatingSuite) TestAffectedBySelf(c *C) { 818 restore := release.MockOnClassic(true) 819 defer restore() 820 821 st := s.state 822 823 st.Lock() 824 defer st.Unlock() 825 826 snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook) 827 updates := []string{snapC.InstanceName()} 828 affected, err := snapstate.AffectedByRefresh(st, updates) 829 c.Assert(err, IsNil) 830 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 831 "snap-c": { 832 AffectingSnaps: map[string]bool{ 833 "snap-c": true, 834 }}}) 835 } 836 837 func (s *autorefreshGatingSuite) TestAffectedByGadget(c *C) { 838 restore := release.MockOnClassic(true) 839 defer restore() 840 841 st := s.state 842 843 st.Lock() 844 defer st.Unlock() 845 kernel := mockInstalledSnap(c, s.state, gadget1Yaml, noHook) 846 mockInstalledSnap(c, s.state, snapCyaml, useHook) 847 mockInstalledSnap(c, s.state, snapByaml, noHook) 848 849 updates := []string{kernel.InstanceName()} 850 affected, err := snapstate.AffectedByRefresh(st, updates) 851 c.Assert(err, IsNil) 852 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 853 "snap-c": { 854 Restart: true, 855 AffectingSnaps: map[string]bool{ 856 "gadget": true, 857 }}}) 858 } 859 860 func (s *autorefreshGatingSuite) TestAffectedBySlot(c *C) { 861 restore := release.MockOnClassic(true) 862 defer restore() 863 864 st := s.state 865 866 st.Lock() 867 defer st.Unlock() 868 869 snapD := mockInstalledSnap(c, s.state, snapDyaml, noHook) 870 snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook) 871 // unrelated snap 872 snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook) 873 874 c.Assert(s.repo.AddSnap(snapF), IsNil) 875 c.Assert(s.repo.AddSnap(snapD), IsNil) 876 c.Assert(s.repo.AddSnap(snapE), IsNil) 877 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}} 878 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 879 c.Assert(err, IsNil) 880 881 updates := []string{snapD.InstanceName()} 882 affected, err := snapstate.AffectedByRefresh(st, updates) 883 c.Assert(err, IsNil) 884 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 885 "snap-e": { 886 Restart: true, 887 AffectingSnaps: map[string]bool{ 888 "snap-d": true, 889 }}}) 890 } 891 892 func (s *autorefreshGatingSuite) TestNotAffectedByCoreOrSnapdSlot(c *C) { 893 restore := release.MockOnClassic(true) 894 defer restore() 895 896 st := s.state 897 898 st.Lock() 899 defer st.Unlock() 900 901 snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook) 902 core := mockInstalledSnap(c, s.state, coreYaml, noHook) 903 snapB := mockInstalledSnap(c, s.state, snapByaml, useHook) 904 905 c.Assert(s.repo.AddSnap(snapG), IsNil) 906 c.Assert(s.repo.AddSnap(core), IsNil) 907 c.Assert(s.repo.AddSnap(snapB), IsNil) 908 909 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "mir"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "mir"}} 910 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 911 c.Assert(err, IsNil) 912 913 updates := []string{core.InstanceName()} 914 affected, err := snapstate.AffectedByRefresh(st, updates) 915 c.Assert(err, IsNil) 916 c.Check(affected, HasLen, 0) 917 } 918 919 func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackend(c *C) { 920 restore := release.MockOnClassic(true) 921 defer restore() 922 923 st := s.state 924 925 st.Lock() 926 defer st.Unlock() 927 928 snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook) 929 snapE := mockInstalledSnap(c, s.state, snapEyaml, noHook) 930 // unrelated snap 931 snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook) 932 933 c.Assert(s.repo.AddSnap(snapF), IsNil) 934 c.Assert(s.repo.AddSnap(snapD), IsNil) 935 c.Assert(s.repo.AddSnap(snapE), IsNil) 936 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}} 937 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 938 c.Assert(err, IsNil) 939 940 // snapE has a plug using mount backend and is refreshed, this affects slot of snap-d. 941 updates := []string{snapE.InstanceName()} 942 affected, err := snapstate.AffectedByRefresh(st, updates) 943 c.Assert(err, IsNil) 944 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 945 "snap-d": { 946 Restart: true, 947 AffectingSnaps: map[string]bool{ 948 "snap-e": true, 949 }}}) 950 } 951 952 func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendSnapdSlot(c *C) { 953 restore := release.MockOnClassic(true) 954 defer restore() 955 956 st := s.state 957 958 st.Lock() 959 defer st.Unlock() 960 961 snapdSnap := mockInstalledSnap(c, s.state, snapdYaml, noHook) 962 snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook) 963 // unrelated snap 964 snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook) 965 966 c.Assert(s.repo.AddSnap(snapF), IsNil) 967 c.Assert(s.repo.AddSnap(snapdSnap), IsNil) 968 c.Assert(s.repo.AddSnap(snapG), IsNil) 969 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "snapd", Name: "desktop"}} 970 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 971 c.Assert(err, IsNil) 972 973 // snapE has a plug using mount backend, refreshing snapd affects snapE. 974 updates := []string{snapdSnap.InstanceName()} 975 affected, err := snapstate.AffectedByRefresh(st, updates) 976 c.Assert(err, IsNil) 977 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 978 "snap-g": { 979 Restart: true, 980 AffectingSnaps: map[string]bool{ 981 "snapd": true, 982 }}}) 983 } 984 985 func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendCoreSlot(c *C) { 986 restore := release.MockOnClassic(true) 987 defer restore() 988 989 st := s.state 990 991 st.Lock() 992 defer st.Unlock() 993 994 coreSnap := mockInstalledSnap(c, s.state, coreYaml, noHook) 995 snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook) 996 997 c.Assert(s.repo.AddSnap(coreSnap), IsNil) 998 c.Assert(s.repo.AddSnap(snapG), IsNil) 999 cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "desktop"}} 1000 _, err := s.repo.Connect(cref, nil, nil, nil, nil, nil) 1001 c.Assert(err, IsNil) 1002 1003 // snapG has a plug using mount backend, refreshing core affects snapE. 1004 updates := []string{coreSnap.InstanceName()} 1005 affected, err := snapstate.AffectedByRefresh(st, updates) 1006 c.Assert(err, IsNil) 1007 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 1008 "snap-g": { 1009 Restart: true, 1010 AffectingSnaps: map[string]bool{ 1011 "core": true, 1012 }}}) 1013 } 1014 1015 func (s *autorefreshGatingSuite) TestAffectedByBootBase(c *C) { 1016 restore := release.MockOnClassic(false) 1017 defer restore() 1018 1019 st := s.state 1020 1021 r := snapstatetest.MockDeviceModel(ModelWithBase("core18")) 1022 defer r() 1023 1024 st.Lock() 1025 defer st.Unlock() 1026 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1027 mockInstalledSnap(c, s.state, snapByaml, useHook) 1028 mockInstalledSnap(c, s.state, snapDyaml, useHook) 1029 mockInstalledSnap(c, s.state, snapEyaml, useHook) 1030 core18 := mockInstalledSnap(c, s.state, core18Yaml, noHook) 1031 1032 updates := []string{core18.InstanceName()} 1033 affected, err := snapstate.AffectedByRefresh(st, updates) 1034 c.Assert(err, IsNil) 1035 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 1036 "snap-a": { 1037 Base: false, 1038 Restart: true, 1039 AffectingSnaps: map[string]bool{ 1040 "core18": true, 1041 }, 1042 }, 1043 "snap-b": { 1044 Base: false, 1045 Restart: true, 1046 AffectingSnaps: map[string]bool{ 1047 "core18": true, 1048 }, 1049 }, 1050 "snap-d": { 1051 Base: false, 1052 Restart: true, 1053 AffectingSnaps: map[string]bool{ 1054 "core18": true, 1055 }, 1056 }, 1057 "snap-e": { 1058 Base: false, 1059 Restart: true, 1060 AffectingSnaps: map[string]bool{ 1061 "core18": true, 1062 }}}) 1063 } 1064 1065 func (s *autorefreshGatingSuite) TestCreateAutoRefreshGateHooks(c *C) { 1066 st := s.state 1067 st.Lock() 1068 defer st.Unlock() 1069 1070 affected := map[string]*snapstate.AffectedSnapInfo{ 1071 "snap-a": { 1072 Base: true, 1073 Restart: true, 1074 AffectingSnaps: map[string]bool{ 1075 "snap-c": true, 1076 "snap-d": true, 1077 }, 1078 }, 1079 "snap-b": { 1080 AffectingSnaps: map[string]bool{ 1081 "snap-e": true, 1082 "snap-f": true, 1083 }, 1084 }, 1085 } 1086 1087 seenSnaps := make(map[string]bool) 1088 1089 ts := snapstate.CreateGateAutoRefreshHooks(st, affected) 1090 c.Assert(ts.Tasks(), HasLen, 2) 1091 1092 checkHook := func(t *state.Task) { 1093 c.Assert(t.Kind(), Equals, "run-hook") 1094 var hs hookstate.HookSetup 1095 c.Assert(t.Get("hook-setup", &hs), IsNil) 1096 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1097 c.Check(hs.Optional, Equals, true) 1098 seenSnaps[hs.Snap] = true 1099 1100 var data interface{} 1101 c.Assert(t.Get("hook-context", &data), IsNil) 1102 1103 // the order of hook tasks is not deterministic 1104 if hs.Snap == "snap-a" { 1105 c.Check(data, DeepEquals, map[string]interface{}{ 1106 "base": true, 1107 "restart": true, 1108 "affecting-snaps": []interface{}{"snap-c", "snap-d"}}) 1109 } else { 1110 c.Assert(hs.Snap, Equals, "snap-b") 1111 c.Check(data, DeepEquals, map[string]interface{}{ 1112 "base": false, 1113 "restart": false, 1114 "affecting-snaps": []interface{}{"snap-e", "snap-f"}}) 1115 } 1116 } 1117 1118 checkHook(ts.Tasks()[0]) 1119 checkHook(ts.Tasks()[1]) 1120 1121 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true}) 1122 } 1123 1124 func (s *autorefreshGatingSuite) TestAffectedByRefreshCandidates(c *C) { 1125 st := s.state 1126 st.Lock() 1127 defer st.Unlock() 1128 1129 mockInstalledSnap(c, st, snapAyaml, useHook) 1130 // unrelated snap 1131 mockInstalledSnap(c, st, snapByaml, useHook) 1132 1133 // no refresh-candidates in state 1134 affected, err := snapstate.AffectedByRefreshCandidates(st) 1135 c.Assert(err, IsNil) 1136 c.Check(affected, HasLen, 0) 1137 1138 candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}} 1139 st.Set("refresh-candidates", &candidates) 1140 1141 affected, err = snapstate.AffectedByRefreshCandidates(st) 1142 c.Assert(err, IsNil) 1143 c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{ 1144 "snap-a": { 1145 AffectingSnaps: map[string]bool{ 1146 "snap-a": true, 1147 }}}) 1148 } 1149 1150 func (s *autorefreshGatingSuite) TestAutorefreshPhase1FeatureFlag(c *C) { 1151 st := s.state 1152 st.Lock() 1153 defer st.Unlock() 1154 1155 st.Set("seeded", true) 1156 1157 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1158 defer restore() 1159 1160 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 1161 return nil, nil 1162 } 1163 defer func() { snapstate.AutoAliases = nil }() 1164 1165 s.store.refreshedSnaps = []*snap.Info{{ 1166 Architectures: []string{"all"}, 1167 SnapType: snap.TypeApp, 1168 SideInfo: snap.SideInfo{ 1169 RealName: "snap-a", 1170 Revision: snap.R(8), 1171 }, 1172 }} 1173 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1174 1175 // gate-auto-refresh-hook feature not enabled, expect old-style refresh. 1176 _, tss, err := snapstate.AutoRefresh(context.TODO(), st) 1177 c.Check(err, IsNil) 1178 c.Assert(tss, HasLen, 2) 1179 c.Check(tss[0].Tasks()[0].Kind(), Equals, "prerequisites") 1180 c.Check(tss[0].Tasks()[1].Kind(), Equals, "download-snap") 1181 c.Check(tss[1].Tasks()[0].Kind(), Equals, "check-rerefresh") 1182 1183 // enable gate-auto-refresh-hook feature 1184 tr := config.NewTransaction(s.state) 1185 tr.Set("core", "experimental.gate-auto-refresh-hook", true) 1186 tr.Commit() 1187 1188 _, tss, err = snapstate.AutoRefresh(context.TODO(), st) 1189 c.Check(err, IsNil) 1190 c.Assert(tss, HasLen, 2) 1191 task := tss[0].Tasks()[0] 1192 c.Check(task.Kind(), Equals, "conditional-auto-refresh") 1193 var toUpdate map[string]*snapstate.RefreshCandidate 1194 c.Assert(task.Get("snaps", &toUpdate), IsNil) 1195 seenSnaps := make(map[string]bool) 1196 for up := range toUpdate { 1197 seenSnaps[up] = true 1198 } 1199 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true}) 1200 c.Check(tss[1].Tasks()[0].Kind(), Equals, "run-hook") 1201 } 1202 1203 func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) { 1204 s.store.refreshedSnaps = []*snap.Info{{ 1205 Architectures: []string{"all"}, 1206 SnapType: snap.TypeApp, 1207 SideInfo: snap.SideInfo{ 1208 RealName: "snap-a", 1209 Revision: snap.R(8), 1210 }, 1211 }, { 1212 Architectures: []string{"all"}, 1213 SnapType: snap.TypeBase, 1214 SideInfo: snap.SideInfo{ 1215 RealName: "base-snap-b", 1216 Revision: snap.R(3), 1217 }, 1218 }, { 1219 Architectures: []string{"all"}, 1220 SnapType: snap.TypeApp, 1221 SideInfo: snap.SideInfo{ 1222 RealName: "snap-c", 1223 Revision: snap.R(5), 1224 }, 1225 }} 1226 1227 st := s.state 1228 st.Lock() 1229 defer st.Unlock() 1230 1231 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1232 mockInstalledSnap(c, s.state, snapByaml, useHook) 1233 mockInstalledSnap(c, s.state, snapCyaml, noHook) 1234 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1235 mockInstalledSnap(c, s.state, snapDyaml, noHook) 1236 1237 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1238 defer restore() 1239 1240 // pretend some snaps are held 1241 c.Assert(snapstate.HoldRefresh(st, "gating-snap", 0, "snap-a", "snap-d"), IsNil) 1242 // sanity check 1243 heldSnaps, err := snapstate.HeldSnaps(st) 1244 c.Assert(err, IsNil) 1245 c.Check(heldSnaps, DeepEquals, map[string]bool{ 1246 "snap-a": true, 1247 "snap-d": true, 1248 }) 1249 1250 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1251 c.Assert(err, IsNil) 1252 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a", "snap-c"}) 1253 c.Assert(tss, HasLen, 2) 1254 1255 c.Assert(tss[0].Tasks(), HasLen, 1) 1256 checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{ 1257 "snap-a": { 1258 SnapSetup: snapstate.SnapSetup{ 1259 Type: "app", 1260 PlugsOnly: true, 1261 Flags: snapstate.Flags{ 1262 IsAutoRefresh: true, 1263 }, 1264 SideInfo: &snap.SideInfo{ 1265 RealName: "snap-a", 1266 Revision: snap.R(8), 1267 }, 1268 DownloadInfo: &snap.DownloadInfo{}, 1269 }, 1270 }, 1271 "base-snap-b": { 1272 SnapSetup: snapstate.SnapSetup{ 1273 Type: "base", 1274 PlugsOnly: true, 1275 Flags: snapstate.Flags{ 1276 IsAutoRefresh: true, 1277 }, 1278 SideInfo: &snap.SideInfo{ 1279 RealName: "base-snap-b", 1280 Revision: snap.R(3), 1281 }, 1282 DownloadInfo: &snap.DownloadInfo{}, 1283 }, 1284 }, 1285 "snap-c": { 1286 SnapSetup: snapstate.SnapSetup{ 1287 Type: "app", 1288 PlugsOnly: true, 1289 Flags: snapstate.Flags{ 1290 IsAutoRefresh: true, 1291 }, 1292 SideInfo: &snap.SideInfo{ 1293 RealName: "snap-c", 1294 Revision: snap.R(5), 1295 }, 1296 DownloadInfo: &snap.DownloadInfo{}, 1297 }, 1298 }, 1299 }) 1300 1301 c.Assert(tss[1].Tasks(), HasLen, 2) 1302 1303 var snapAhookData, snapBhookData map[string]interface{} 1304 1305 // check hooks for affected snaps 1306 seenSnaps := make(map[string]bool) 1307 var hs hookstate.HookSetup 1308 task := tss[1].Tasks()[0] 1309 c.Assert(task.Get("hook-setup", &hs), IsNil) 1310 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1311 seenSnaps[hs.Snap] = true 1312 switch hs.Snap { 1313 case "snap-a": 1314 task.Get("hook-context", &snapAhookData) 1315 case "snap-b": 1316 task.Get("hook-context", &snapBhookData) 1317 default: 1318 c.Fatalf("unexpected snap %q", hs.Snap) 1319 } 1320 1321 task = tss[1].Tasks()[1] 1322 c.Assert(task.Get("hook-setup", &hs), IsNil) 1323 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1324 seenSnaps[hs.Snap] = true 1325 switch hs.Snap { 1326 case "snap-a": 1327 task.Get("hook-context", &snapAhookData) 1328 case "snap-b": 1329 task.Get("hook-context", &snapBhookData) 1330 default: 1331 c.Fatalf("unexpected snap %q", hs.Snap) 1332 } 1333 1334 c.Check(snapAhookData["affecting-snaps"], DeepEquals, []interface{}{"snap-a"}) 1335 c.Check(snapBhookData["affecting-snaps"], DeepEquals, []interface{}{"base-snap-b"}) 1336 1337 // hook for snap-a because it gets refreshed, for snap-b because its base 1338 // gets refreshed. snap-c is refreshed but doesn't have the hook. 1339 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true}) 1340 1341 // check that refresh-candidates in the state were updated 1342 var candidates map[string]*snapstate.RefreshCandidate 1343 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 1344 c.Assert(candidates, HasLen, 3) 1345 c.Check(candidates["snap-a"], NotNil) 1346 c.Check(candidates["base-snap-b"], NotNil) 1347 c.Check(candidates["snap-c"], NotNil) 1348 1349 // check that after autoRefreshPhase1 any held snaps that are not in refresh 1350 // candidates got removed. 1351 heldSnaps, err = snapstate.HeldSnaps(st) 1352 c.Assert(err, IsNil) 1353 // snap-d got removed from held snaps. 1354 c.Check(heldSnaps, DeepEquals, map[string]bool{ 1355 "snap-a": true, 1356 }) 1357 } 1358 1359 // this test demonstrates that affectedByRefresh uses current snap info (not 1360 // snap infos of store updates) by simulating a different base for the updated 1361 // snap from the store. 1362 func (s *autorefreshGatingSuite) TestAffectedByRefreshUsesCurrentSnapInfo(c *C) { 1363 s.store.refreshedSnaps = []*snap.Info{{ 1364 Architectures: []string{"all"}, 1365 SnapType: snap.TypeBase, 1366 SideInfo: snap.SideInfo{ 1367 RealName: "base-snap-b", 1368 Revision: snap.R(3), 1369 }, 1370 }, { 1371 Architectures: []string{"all"}, 1372 Base: "new-base", 1373 SnapType: snap.TypeApp, 1374 SideInfo: snap.SideInfo{ 1375 RealName: "snap-b", 1376 Revision: snap.R(5), 1377 }, 1378 }} 1379 1380 st := s.state 1381 st.Lock() 1382 defer st.Unlock() 1383 1384 mockInstalledSnap(c, s.state, snapByaml, useHook) 1385 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1386 1387 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1388 defer restore() 1389 1390 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1391 c.Assert(err, IsNil) 1392 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-b"}) 1393 c.Assert(tss, HasLen, 2) 1394 1395 c.Assert(tss[0].Tasks(), HasLen, 1) 1396 checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{ 1397 "snap-b": { 1398 SnapSetup: snapstate.SnapSetup{ 1399 Type: "app", 1400 Base: "new-base", 1401 PlugsOnly: true, 1402 Flags: snapstate.Flags{ 1403 IsAutoRefresh: true, 1404 }, 1405 SideInfo: &snap.SideInfo{ 1406 RealName: "snap-b", 1407 Revision: snap.R(5), 1408 }, 1409 DownloadInfo: &snap.DownloadInfo{}, 1410 }, 1411 }, 1412 "base-snap-b": { 1413 SnapSetup: snapstate.SnapSetup{ 1414 Type: "base", 1415 PlugsOnly: true, 1416 Flags: snapstate.Flags{ 1417 IsAutoRefresh: true, 1418 }, 1419 SideInfo: &snap.SideInfo{ 1420 RealName: "base-snap-b", 1421 Revision: snap.R(3), 1422 }, 1423 DownloadInfo: &snap.DownloadInfo{}, 1424 }, 1425 }, 1426 }) 1427 1428 c.Assert(tss[1].Tasks(), HasLen, 1) 1429 var hs hookstate.HookSetup 1430 task := tss[1].Tasks()[0] 1431 c.Assert(task.Get("hook-setup", &hs), IsNil) 1432 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1433 c.Check(hs.Snap, Equals, "snap-b") 1434 var data interface{} 1435 c.Assert(task.Get("hook-context", &data), IsNil) 1436 c.Check(data, DeepEquals, map[string]interface{}{ 1437 "base": true, 1438 "restart": false, 1439 "affecting-snaps": []interface{}{"base-snap-b", "snap-b"}}) 1440 1441 // check that refresh-candidates in the state were updated 1442 var candidates map[string]*snapstate.RefreshCandidate 1443 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 1444 c.Assert(candidates, HasLen, 2) 1445 c.Check(candidates["snap-b"], NotNil) 1446 c.Check(candidates["base-snap-b"], NotNil) 1447 } 1448 1449 func (s *autorefreshGatingSuite) TestAutoRefreshPhase1ConflictsFilteredOut(c *C) { 1450 s.store.refreshedSnaps = []*snap.Info{{ 1451 Architectures: []string{"all"}, 1452 SnapType: snap.TypeApp, 1453 SideInfo: snap.SideInfo{ 1454 RealName: "snap-a", 1455 Revision: snap.R(8), 1456 }, 1457 }, { 1458 Architectures: []string{"all"}, 1459 SnapType: snap.TypeBase, 1460 SideInfo: snap.SideInfo{ 1461 RealName: "snap-c", 1462 Revision: snap.R(5), 1463 }, 1464 }} 1465 1466 st := s.state 1467 st.Lock() 1468 defer st.Unlock() 1469 1470 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1471 mockInstalledSnap(c, s.state, snapCyaml, noHook) 1472 1473 conflictChange := st.NewChange("conflicting change", "") 1474 conflictTask := st.NewTask("conflicting task", "") 1475 si := &snap.SideInfo{ 1476 RealName: "snap-c", 1477 Revision: snap.R(1), 1478 } 1479 sup := snapstate.SnapSetup{SideInfo: si} 1480 conflictTask.Set("snap-setup", sup) 1481 conflictChange.AddTask(conflictTask) 1482 1483 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1484 defer restore() 1485 1486 logbuf, restoreLogger := logger.MockLogger() 1487 defer restoreLogger() 1488 1489 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1490 c.Assert(err, IsNil) 1491 c.Check(names, DeepEquals, []string{"snap-a"}) 1492 c.Assert(tss, HasLen, 2) 1493 1494 c.Assert(tss[0].Tasks(), HasLen, 1) 1495 checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{ 1496 "snap-a": { 1497 SnapSetup: snapstate.SnapSetup{ 1498 Type: "app", 1499 PlugsOnly: true, 1500 Flags: snapstate.Flags{ 1501 IsAutoRefresh: true, 1502 }, 1503 SideInfo: &snap.SideInfo{ 1504 RealName: "snap-a", 1505 Revision: snap.R(8), 1506 }, 1507 DownloadInfo: &snap.DownloadInfo{}, 1508 }}}) 1509 1510 c.Assert(tss[1].Tasks(), HasLen, 1) 1511 1512 c.Assert(logbuf.String(), testutil.Contains, `cannot refresh snap "snap-c": snap "snap-c" has "conflicting change" change in progress`) 1513 1514 seenSnaps := make(map[string]bool) 1515 var hs hookstate.HookSetup 1516 c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil) 1517 c.Check(hs.Hook, Equals, "gate-auto-refresh") 1518 seenSnaps[hs.Snap] = true 1519 1520 c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true}) 1521 1522 // check that refresh-candidates in the state were updated 1523 var candidates map[string]*snapstate.RefreshCandidate 1524 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 1525 c.Assert(candidates, HasLen, 2) 1526 c.Check(candidates["snap-a"], NotNil) 1527 c.Check(candidates["snap-c"], NotNil) 1528 } 1529 1530 func (s *autorefreshGatingSuite) TestAutoRefreshPhase1NoHooks(c *C) { 1531 s.store.refreshedSnaps = []*snap.Info{{ 1532 Architectures: []string{"all"}, 1533 SnapType: snap.TypeBase, 1534 SideInfo: snap.SideInfo{ 1535 RealName: "base-snap-b", 1536 Revision: snap.R(3), 1537 }, 1538 }, { 1539 Architectures: []string{"all"}, 1540 SnapType: snap.TypeBase, 1541 SideInfo: snap.SideInfo{ 1542 RealName: "snap-c", 1543 Revision: snap.R(5), 1544 }, 1545 }} 1546 1547 st := s.state 1548 st.Lock() 1549 defer st.Unlock() 1550 1551 mockInstalledSnap(c, s.state, snapByaml, noHook) 1552 mockInstalledSnap(c, s.state, snapCyaml, noHook) 1553 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1554 1555 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1556 defer restore() 1557 1558 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1559 c.Assert(err, IsNil) 1560 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-c"}) 1561 c.Assert(tss, HasLen, 1) 1562 1563 c.Assert(tss[0].Tasks(), HasLen, 1) 1564 c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh") 1565 } 1566 1567 func fakeReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) { 1568 info := &snap.Info{ 1569 SuggestedName: name, 1570 SideInfo: *si, 1571 Architectures: []string{"all"}, 1572 SnapType: snap.TypeApp, 1573 Epoch: snap.Epoch{}, 1574 } 1575 switch name { 1576 case "base-snap-b": 1577 info.SnapType = snap.TypeBase 1578 case "snap-a", "snap-b": 1579 info.Hooks = map[string]*snap.HookInfo{ 1580 "gate-auto-refresh": { 1581 Name: "gate-auto-refresh", 1582 Snap: info, 1583 }, 1584 } 1585 if name == "snap-b" { 1586 info.Base = "base-snap-b" 1587 } 1588 } 1589 return info, nil 1590 } 1591 1592 func (s *snapmgrTestSuite) testAutoRefreshPhase2(c *C, beforePhase1 func(), gateAutoRefreshHook func(snapName string), expected []string) *state.Change { 1593 st := s.state 1594 st.Lock() 1595 defer st.Unlock() 1596 1597 s.o.TaskRunner().AddHandler("run-hook", func(t *state.Task, tomb *tomb.Tomb) error { 1598 var hsup hookstate.HookSetup 1599 t.State().Lock() 1600 defer t.State().Unlock() 1601 c.Assert(t.Get("hook-setup", &hsup), IsNil) 1602 if hsup.Hook == "gate-auto-refresh" && gateAutoRefreshHook != nil { 1603 gateAutoRefreshHook(hsup.Snap) 1604 } 1605 return nil 1606 }, nil) 1607 1608 restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) { 1609 c.Fatal("unexpected call to installSize") 1610 return 0, nil 1611 }) 1612 defer restoreInstallSize() 1613 1614 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 1615 fakeStore: s.fakeStore, 1616 refreshedSnaps: []*snap.Info{{ 1617 Architectures: []string{"all"}, 1618 SnapType: snap.TypeApp, 1619 SideInfo: snap.SideInfo{ 1620 RealName: "snap-a", 1621 Revision: snap.R(8), 1622 }, 1623 }, { 1624 Architectures: []string{"all"}, 1625 SnapType: snap.TypeBase, 1626 SideInfo: snap.SideInfo{ 1627 RealName: "base-snap-b", 1628 Revision: snap.R(3), 1629 }, 1630 }}}) 1631 1632 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1633 mockInstalledSnap(c, s.state, snapByaml, useHook) 1634 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1635 1636 snapstate.MockSnapReadInfo(fakeReadInfo) 1637 1638 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1639 defer restore() 1640 1641 if beforePhase1 != nil { 1642 beforePhase1() 1643 } 1644 1645 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1646 c.Assert(err, IsNil) 1647 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 1648 1649 chg := s.state.NewChange("refresh", "...") 1650 for _, ts := range tss { 1651 chg.AddAll(ts) 1652 } 1653 1654 s.state.Unlock() 1655 defer s.se.Stop() 1656 s.settle(c) 1657 s.state.Lock() 1658 1659 c.Check(chg.Status(), Equals, state.DoneStatus) 1660 c.Check(chg.Err(), IsNil) 1661 1662 verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected) 1663 1664 return chg 1665 } 1666 1667 func (s *snapmgrTestSuite) TestAutoRefreshPhase2(c *C) { 1668 expected := []string{ 1669 "conditional-auto-refresh", 1670 "run-hook [snap-a;gate-auto-refresh]", 1671 // snap-b hook is triggered because of base-snap-b refresh 1672 "run-hook [snap-b;gate-auto-refresh]", 1673 "prerequisites", 1674 "download-snap", 1675 "validate-snap", 1676 "mount-snap", 1677 "run-hook [base-snap-b;pre-refresh]", 1678 "stop-snap-services", 1679 "remove-aliases", 1680 "unlink-current-snap", 1681 "copy-snap-data", 1682 "setup-profiles", 1683 "link-snap", 1684 "auto-connect", 1685 "set-auto-aliases", 1686 "setup-aliases", 1687 "run-hook [base-snap-b;post-refresh]", 1688 "start-snap-services", 1689 "cleanup", 1690 "run-hook [base-snap-b;check-health]", 1691 "prerequisites", 1692 "download-snap", 1693 "validate-snap", 1694 "mount-snap", 1695 "run-hook [snap-a;pre-refresh]", 1696 "stop-snap-services", 1697 "remove-aliases", 1698 "unlink-current-snap", 1699 "copy-snap-data", 1700 "setup-profiles", 1701 "link-snap", 1702 "auto-connect", 1703 "set-auto-aliases", 1704 "setup-aliases", 1705 "run-hook [snap-a;post-refresh]", 1706 "start-snap-services", 1707 "cleanup", 1708 "run-hook [snap-a;configure]", 1709 "run-hook [snap-a;check-health]", 1710 "check-rerefresh", 1711 } 1712 1713 seenSnapsWithGateAutoRefreshHook := make(map[string]bool) 1714 1715 chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) { 1716 seenSnapsWithGateAutoRefreshHook[snapName] = true 1717 }, expected) 1718 1719 c.Check(seenSnapsWithGateAutoRefreshHook, DeepEquals, map[string]bool{ 1720 "snap-a": true, 1721 "snap-b": true, 1722 }) 1723 1724 s.state.Lock() 1725 defer s.state.Unlock() 1726 1727 tasks := chg.Tasks() 1728 c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b", "snap-a" as needed`) 1729 1730 // all snaps refreshed, all removed from refresh-candidates. 1731 var candidates map[string]*snapstate.RefreshCandidate 1732 c.Assert(s.state.Get("refresh-candidates", &candidates), IsNil) 1733 c.Assert(candidates, HasLen, 0) 1734 } 1735 1736 func (s *snapmgrTestSuite) TestAutoRefreshPhase2Held(c *C) { 1737 logbuf, restoreLogger := logger.MockLogger() 1738 defer restoreLogger() 1739 1740 expected := []string{ 1741 "conditional-auto-refresh", 1742 "run-hook [snap-a;gate-auto-refresh]", 1743 // snap-b hook is triggered because of base-snap-b refresh 1744 "run-hook [snap-b;gate-auto-refresh]", 1745 "prerequisites", 1746 "download-snap", 1747 "validate-snap", 1748 "mount-snap", 1749 "run-hook [snap-a;pre-refresh]", 1750 "stop-snap-services", 1751 "remove-aliases", 1752 "unlink-current-snap", 1753 "copy-snap-data", 1754 "setup-profiles", 1755 "link-snap", 1756 "auto-connect", 1757 "set-auto-aliases", 1758 "setup-aliases", 1759 "run-hook [snap-a;post-refresh]", 1760 "start-snap-services", 1761 "cleanup", 1762 "run-hook [snap-a;configure]", 1763 "run-hook [snap-a;check-health]", 1764 "check-rerefresh", 1765 } 1766 1767 chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) { 1768 if snapName == "snap-b" { 1769 // pretend than snap-b calls snapctl --hold to hold refresh of base-snap-b 1770 c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil) 1771 } 1772 }, expected) 1773 1774 s.state.Lock() 1775 defer s.state.Unlock() 1776 1777 c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`) 1778 tasks := chg.Tasks() 1779 // no re-refresh for base-snap-b because it was held. 1780 c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "snap-a" as needed`) 1781 } 1782 1783 func (s *snapmgrTestSuite) TestAutoRefreshPhase2Proceed(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 s.testAutoRefreshPhase2(c, func() { 1815 // pretend that snap-a and base-snap-b are initially held 1816 c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil) 1817 c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil) 1818 }, func(snapName string) { 1819 if snapName == "snap-a" { 1820 // pretend than snap-a calls snapctl --proceed 1821 c.Assert(snapstate.ProceedWithRefresh(s.state, "snap-a"), IsNil) 1822 } 1823 // note, do nothing about snap-b which just keeps its hold state in 1824 // the test, but if we were using real gate-auto-refresh hook 1825 // handler, the default behavior for snap-b if it doesn't call --hold 1826 // would be to proceed (hook handler would take care of that). 1827 }, expected) 1828 1829 c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`) 1830 } 1831 1832 func (s *snapmgrTestSuite) TestAutoRefreshPhase2AllHeld(c *C) { 1833 logbuf, restoreLogger := logger.MockLogger() 1834 defer restoreLogger() 1835 1836 expected := []string{ 1837 "conditional-auto-refresh", 1838 "run-hook [snap-a;gate-auto-refresh]", 1839 // snap-b hook is triggered because of base-snap-b refresh 1840 "run-hook [snap-b;gate-auto-refresh]", 1841 } 1842 1843 s.testAutoRefreshPhase2(c, nil, func(snapName string) { 1844 switch snapName { 1845 case "snap-b": 1846 // pretend that snap-b calls snapctl --hold to hold refresh of base-snap-b 1847 c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil) 1848 case "snap-a": 1849 // pretend that snap-a calls snapctl --hold to hold itself 1850 c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil) 1851 default: 1852 c.Fatalf("unexpected snap %q", snapName) 1853 } 1854 }, expected) 1855 1856 c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b,snap-a`) 1857 } 1858 1859 func (s *snapmgrTestSuite) testAutoRefreshPhase2DiskSpaceCheck(c *C, fail bool) { 1860 st := s.state 1861 st.Lock() 1862 defer st.Unlock() 1863 1864 restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error { 1865 c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123)) 1866 if fail { 1867 return &osutil.NotEnoughDiskSpaceError{} 1868 } 1869 return nil 1870 }) 1871 defer restore() 1872 1873 var installSizeCalled bool 1874 restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) { 1875 installSizeCalled = true 1876 seen := map[string]bool{} 1877 for _, sn := range snaps { 1878 seen[sn.InstanceName()] = true 1879 } 1880 c.Check(seen, DeepEquals, map[string]bool{ 1881 "base-snap-b": true, 1882 "snap-a": true, 1883 }) 1884 return 123, nil 1885 }) 1886 defer restoreInstallSize() 1887 1888 restoreModel := snapstatetest.MockDeviceModel(DefaultModel()) 1889 defer restoreModel() 1890 1891 tr := config.NewTransaction(s.state) 1892 tr.Set("core", "experimental.check-disk-space-refresh", true) 1893 tr.Commit() 1894 1895 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 1896 fakeStore: s.fakeStore, 1897 refreshedSnaps: []*snap.Info{{ 1898 Architectures: []string{"all"}, 1899 SnapType: snap.TypeApp, 1900 SideInfo: snap.SideInfo{ 1901 RealName: "snap-a", 1902 Revision: snap.R(8), 1903 }, 1904 }, { 1905 Architectures: []string{"all"}, 1906 SnapType: snap.TypeBase, 1907 SideInfo: snap.SideInfo{ 1908 RealName: "base-snap-b", 1909 Revision: snap.R(3), 1910 }, 1911 }}}) 1912 1913 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1914 mockInstalledSnap(c, s.state, snapByaml, useHook) 1915 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1916 1917 snapstate.MockSnapReadInfo(fakeReadInfo) 1918 1919 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1920 c.Assert(err, IsNil) 1921 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 1922 1923 chg := s.state.NewChange("refresh", "...") 1924 for _, ts := range tss { 1925 chg.AddAll(ts) 1926 } 1927 1928 s.state.Unlock() 1929 defer s.se.Stop() 1930 s.settle(c) 1931 s.state.Lock() 1932 1933 c.Check(installSizeCalled, Equals, true) 1934 if fail { 1935 c.Check(chg.Status(), Equals, state.ErrorStatus) 1936 c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n- Run auto-refresh for ready snaps \(insufficient space.*`) 1937 } else { 1938 c.Check(chg.Status(), Equals, state.DoneStatus) 1939 c.Check(chg.Err(), IsNil) 1940 } 1941 } 1942 1943 func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceError(c *C) { 1944 fail := true 1945 s.testAutoRefreshPhase2DiskSpaceCheck(c, fail) 1946 } 1947 1948 func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceHappy(c *C) { 1949 var nofail bool 1950 s.testAutoRefreshPhase2DiskSpaceCheck(c, nofail) 1951 } 1952 1953 // XXX: this case is probably artificial; with proper conflict prevention 1954 // we shouldn't get conflicts from doInstall in phase2. 1955 func (s *snapmgrTestSuite) TestAutoRefreshPhase2Conflict(c *C) { 1956 st := s.state 1957 st.Lock() 1958 defer st.Unlock() 1959 1960 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 1961 fakeStore: s.fakeStore, 1962 refreshedSnaps: []*snap.Info{{ 1963 Architectures: []string{"all"}, 1964 SnapType: snap.TypeApp, 1965 SideInfo: snap.SideInfo{ 1966 RealName: "snap-a", 1967 Revision: snap.R(8), 1968 }, 1969 }, { 1970 Architectures: []string{"all"}, 1971 SnapType: snap.TypeBase, 1972 SideInfo: snap.SideInfo{ 1973 RealName: "base-snap-b", 1974 Revision: snap.R(3), 1975 }, 1976 }}}) 1977 1978 mockInstalledSnap(c, s.state, snapAyaml, useHook) 1979 mockInstalledSnap(c, s.state, snapByaml, useHook) 1980 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 1981 1982 snapstate.MockSnapReadInfo(fakeReadInfo) 1983 1984 restore := snapstatetest.MockDeviceModel(DefaultModel()) 1985 defer restore() 1986 1987 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 1988 c.Assert(err, IsNil) 1989 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 1990 1991 chg := s.state.NewChange("refresh", "...") 1992 for _, ts := range tss { 1993 chg.AddAll(ts) 1994 } 1995 1996 conflictChange := st.NewChange("conflicting change", "") 1997 conflictTask := st.NewTask("conflicting task", "") 1998 si := &snap.SideInfo{ 1999 RealName: "snap-a", 2000 Revision: snap.R(1), 2001 } 2002 sup := snapstate.SnapSetup{SideInfo: si} 2003 conflictTask.Set("snap-setup", sup) 2004 conflictChange.AddTask(conflictTask) 2005 conflictTask.WaitFor(tss[0].Tasks()[0]) 2006 2007 s.state.Unlock() 2008 defer s.se.Stop() 2009 s.settle(c) 2010 s.state.Lock() 2011 2012 c.Assert(chg.Status(), Equals, state.DoneStatus) 2013 c.Check(chg.Err(), IsNil) 2014 2015 // no refresh of snap-a because of the conflict. 2016 expected := []string{ 2017 "conditional-auto-refresh", 2018 "run-hook [snap-a;gate-auto-refresh]", 2019 // snap-b hook is triggered because of base-snap-b refresh 2020 "run-hook [snap-b;gate-auto-refresh]", 2021 "prerequisites", 2022 "download-snap", 2023 "validate-snap", 2024 "mount-snap", 2025 "run-hook [base-snap-b;pre-refresh]", 2026 "stop-snap-services", 2027 "remove-aliases", 2028 "unlink-current-snap", 2029 "copy-snap-data", 2030 "setup-profiles", 2031 "link-snap", 2032 "auto-connect", 2033 "set-auto-aliases", 2034 "setup-aliases", 2035 "run-hook [base-snap-b;post-refresh]", 2036 "start-snap-services", 2037 "cleanup", 2038 "run-hook [base-snap-b;check-health]", 2039 "check-rerefresh", 2040 } 2041 verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected) 2042 } 2043 2044 func (s *snapmgrTestSuite) TestAutoRefreshPhase2ConflictOtherSnapOp(c *C) { 2045 st := s.state 2046 st.Lock() 2047 defer st.Unlock() 2048 2049 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 2050 fakeStore: s.fakeStore, 2051 refreshedSnaps: []*snap.Info{{ 2052 Architectures: []string{"all"}, 2053 SnapType: snap.TypeApp, 2054 SideInfo: snap.SideInfo{ 2055 RealName: "snap-a", 2056 Revision: snap.R(8), 2057 }, 2058 }}}) 2059 2060 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2061 2062 snapstate.MockSnapReadInfo(fakeReadInfo) 2063 2064 restore := snapstatetest.MockDeviceModel(DefaultModel()) 2065 defer restore() 2066 2067 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 2068 c.Assert(err, IsNil) 2069 c.Check(names, DeepEquals, []string{"snap-a"}) 2070 2071 chg := s.state.NewChange("fake-auto-refresh", "...") 2072 for _, ts := range tss { 2073 chg.AddAll(ts) 2074 } 2075 2076 s.state.Unlock() 2077 // run first task 2078 s.se.Ensure() 2079 s.se.Wait() 2080 2081 s.state.Lock() 2082 2083 _, err = snapstate.Remove(s.state, "snap-a", snap.R(8), nil) 2084 c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ 2085 ChangeKind: "fake-auto-refresh", 2086 Snap: "snap-a", 2087 }) 2088 2089 _, err = snapstate.Update(s.state, "snap-a", nil, 0, snapstate.Flags{}) 2090 c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ 2091 ChangeKind: "fake-auto-refresh", 2092 Snap: "snap-a", 2093 }) 2094 2095 // only 2 tasks because we don't run settle() so conditional-auto-refresh 2096 // doesn't run and no new tasks get created. 2097 expected := []string{ 2098 "conditional-auto-refresh", 2099 "run-hook [snap-a;gate-auto-refresh]", 2100 } 2101 verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected) 2102 } 2103 2104 func (s *snapmgrTestSuite) TestAutoRefreshPhase2GatedSnaps(c *C) { 2105 st := s.state 2106 st.Lock() 2107 defer st.Unlock() 2108 2109 restore := snapstate.MockSnapsToRefresh(func(gatingTask *state.Task) ([]*snapstate.RefreshCandidate, error) { 2110 c.Assert(gatingTask.Kind(), Equals, "conditional-auto-refresh") 2111 var candidates map[string]*snapstate.RefreshCandidate 2112 c.Assert(gatingTask.Get("snaps", &candidates), IsNil) 2113 seenSnaps := make(map[string]bool) 2114 var filteredByGatingHooks []*snapstate.RefreshCandidate 2115 for _, cand := range candidates { 2116 seenSnaps[cand.InstanceName()] = true 2117 if cand.InstanceName() == "snap-a" { 2118 continue 2119 } 2120 filteredByGatingHooks = append(filteredByGatingHooks, cand) 2121 } 2122 c.Check(seenSnaps, DeepEquals, map[string]bool{ 2123 "snap-a": true, 2124 "base-snap-b": true, 2125 }) 2126 return filteredByGatingHooks, nil 2127 }) 2128 defer restore() 2129 2130 snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{ 2131 fakeStore: s.fakeStore, 2132 refreshedSnaps: []*snap.Info{ 2133 { 2134 Architectures: []string{"all"}, 2135 SnapType: snap.TypeApp, 2136 SideInfo: snap.SideInfo{ 2137 RealName: "snap-a", 2138 Revision: snap.R(8), 2139 }, 2140 }, { 2141 Architectures: []string{"all"}, 2142 SnapType: snap.TypeBase, 2143 SideInfo: snap.SideInfo{ 2144 RealName: "base-snap-b", 2145 Revision: snap.R(3), 2146 }, 2147 }, 2148 }}) 2149 2150 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2151 mockInstalledSnap(c, s.state, snapByaml, useHook) 2152 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 2153 2154 snapstate.MockSnapReadInfo(fakeReadInfo) 2155 2156 restoreModel := snapstatetest.MockDeviceModel(DefaultModel()) 2157 defer restoreModel() 2158 2159 names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") 2160 c.Assert(err, IsNil) 2161 c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"}) 2162 2163 chg := s.state.NewChange("refresh", "...") 2164 for _, ts := range tss { 2165 chg.AddAll(ts) 2166 } 2167 2168 s.state.Unlock() 2169 defer s.se.Stop() 2170 s.settle(c) 2171 s.state.Lock() 2172 2173 c.Assert(chg.Status(), Equals, state.DoneStatus) 2174 c.Check(chg.Err(), IsNil) 2175 2176 expected := []string{ 2177 "conditional-auto-refresh", 2178 "run-hook [snap-a;gate-auto-refresh]", 2179 // snap-b hook is triggered because of base-snap-b refresh 2180 "run-hook [snap-b;gate-auto-refresh]", 2181 "prerequisites", 2182 "download-snap", 2183 "validate-snap", 2184 "mount-snap", 2185 "run-hook [base-snap-b;pre-refresh]", 2186 "stop-snap-services", 2187 "remove-aliases", 2188 "unlink-current-snap", 2189 "copy-snap-data", 2190 "setup-profiles", 2191 "link-snap", 2192 "auto-connect", 2193 "set-auto-aliases", 2194 "setup-aliases", 2195 "run-hook [base-snap-b;post-refresh]", 2196 "start-snap-services", 2197 "cleanup", 2198 "run-hook [base-snap-b;check-health]", 2199 "check-rerefresh", 2200 } 2201 tasks := chg.Tasks() 2202 verifyPhasedAutorefreshTasks(c, tasks, expected) 2203 // no re-refresh for snap-a because it was held. 2204 c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b" as needed`) 2205 2206 // only snap-a remains in refresh-candidates because it was held; 2207 // base-snap-b got pruned (was refreshed). 2208 var candidates map[string]*snapstate.RefreshCandidate 2209 c.Assert(st.Get("refresh-candidates", &candidates), IsNil) 2210 c.Assert(candidates, HasLen, 1) 2211 c.Check(candidates["snap-a"], NotNil) 2212 } 2213 2214 func (s *snapmgrTestSuite) TestAutoRefreshForGatingSnapErrorAutoRefreshInProgress(c *C) { 2215 st := s.state 2216 st.Lock() 2217 defer st.Unlock() 2218 2219 chg := st.NewChange("auto-refresh", "...") 2220 task := st.NewTask("foo", "...") 2221 chg.AddTask(task) 2222 2223 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-a"), ErrorMatches, `there is an auto-refresh in progress`) 2224 } 2225 2226 func (s *snapmgrTestSuite) TestAutoRefreshForGatingSnapErrorNothingHeld(c *C) { 2227 st := s.state 2228 st.Lock() 2229 defer st.Unlock() 2230 2231 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-a"), ErrorMatches, `no snaps are held by snap "snap-a"`) 2232 } 2233 2234 func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnap(c *C) { 2235 s.store.refreshedSnaps = []*snap.Info{{ 2236 Architectures: []string{"all"}, 2237 SnapType: snap.TypeApp, 2238 SideInfo: snap.SideInfo{ 2239 RealName: "snap-a", 2240 Revision: snap.R(8), 2241 }, 2242 }, { 2243 Architectures: []string{"all"}, 2244 SnapType: snap.TypeApp, 2245 Base: "base-snap-b", 2246 SideInfo: snap.SideInfo{ 2247 RealName: "snap-b", 2248 Revision: snap.R(2), 2249 }, 2250 }, { 2251 Architectures: []string{"all"}, 2252 SnapType: snap.TypeBase, 2253 SideInfo: snap.SideInfo{ 2254 RealName: "base-snap-b", 2255 Revision: snap.R(3), 2256 }, 2257 }} 2258 2259 st := s.state 2260 st.Lock() 2261 defer st.Unlock() 2262 2263 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2264 mockInstalledSnap(c, s.state, snapByaml, useHook) 2265 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 2266 2267 restore := snapstatetest.MockDeviceModel(DefaultModel()) 2268 defer restore() 2269 2270 // pretend some snaps are held 2271 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil) 2272 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil) 2273 2274 lastRefreshTime := time.Now().Add(-99 * time.Hour) 2275 st.Set("last-refresh", lastRefreshTime) 2276 2277 // pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed) 2278 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil) 2279 2280 changes := st.Changes() 2281 c.Assert(changes, HasLen, 1) 2282 chg := changes[0] 2283 c.Assert(chg.Kind(), Equals, "auto-refresh") 2284 c.Check(chg.Summary(), Equals, `Auto-refresh snaps "base-snap-b", "snap-b"`) 2285 var snapNames []string 2286 var apiData map[string]interface{} 2287 c.Assert(chg.Get("snap-names", &snapNames), IsNil) 2288 c.Check(snapNames, DeepEquals, []string{"base-snap-b", "snap-b"}) 2289 c.Assert(chg.Get("api-data", &apiData), IsNil) 2290 c.Check(apiData, DeepEquals, map[string]interface{}{ 2291 "snap-names": []interface{}{"base-snap-b", "snap-b"}, 2292 }) 2293 2294 tasks := chg.Tasks() 2295 c.Assert(tasks, HasLen, 2) 2296 conditionalRefreshTask := tasks[0] 2297 checkGatingTask(c, conditionalRefreshTask, map[string]*snapstate.RefreshCandidate{ 2298 "base-snap-b": { 2299 SnapSetup: snapstate.SnapSetup{ 2300 Type: "base", 2301 PlugsOnly: true, 2302 Flags: snapstate.Flags{ 2303 IsAutoRefresh: true, 2304 }, 2305 SideInfo: &snap.SideInfo{ 2306 RealName: "base-snap-b", 2307 Revision: snap.R(3), 2308 }, 2309 DownloadInfo: &snap.DownloadInfo{}, 2310 }, 2311 }, 2312 "snap-b": { 2313 SnapSetup: snapstate.SnapSetup{ 2314 Type: "app", 2315 Base: "base-snap-b", 2316 PlugsOnly: true, 2317 Flags: snapstate.Flags{ 2318 IsAutoRefresh: true, 2319 }, 2320 SideInfo: &snap.SideInfo{ 2321 RealName: "snap-b", 2322 Revision: snap.R(2), 2323 }, 2324 DownloadInfo: &snap.DownloadInfo{}, 2325 }, 2326 }, 2327 }) 2328 2329 // the gate-auto-refresh hook task for snap-b is present 2330 c.Check(tasks[1].Kind(), Equals, "run-hook") 2331 var hs hookstate.HookSetup 2332 c.Assert(tasks[1].Get("hook-setup", &hs), IsNil) 2333 c.Check(hs.Hook, Equals, "gate-auto-refresh") 2334 c.Check(hs.Snap, Equals, "snap-b") 2335 c.Check(hs.Optional, Equals, true) 2336 2337 var data interface{} 2338 c.Assert(tasks[1].Get("hook-context", &data), IsNil) 2339 c.Check(data, DeepEquals, map[string]interface{}{ 2340 "base": true, 2341 "restart": false, 2342 "affecting-snaps": []interface{}{"base-snap-b", "snap-b"}, 2343 }) 2344 2345 // last-refresh wasn't modified 2346 var lr time.Time 2347 st.Get("last-refresh", &lr) 2348 c.Check(lr.Equal(lastRefreshTime), Equals, true) 2349 } 2350 2351 func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnapMoreAffectedSnaps(c *C) { 2352 s.store.refreshedSnaps = []*snap.Info{{ 2353 Architectures: []string{"all"}, 2354 SnapType: snap.TypeApp, 2355 SideInfo: snap.SideInfo{ 2356 RealName: "snap-a", 2357 Revision: snap.R(8), 2358 }, 2359 }, { 2360 Architectures: []string{"all"}, 2361 SnapType: snap.TypeBase, 2362 SideInfo: snap.SideInfo{ 2363 RealName: "base-snap-b", 2364 Revision: snap.R(3), 2365 }, 2366 }, { 2367 Architectures: []string{"all"}, 2368 SnapType: snap.TypeApp, 2369 Base: "base-snap-b", 2370 SideInfo: snap.SideInfo{ 2371 RealName: "snap-b", 2372 Revision: snap.R(2), 2373 }, 2374 }} 2375 2376 st := s.state 2377 st.Lock() 2378 defer st.Unlock() 2379 2380 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2381 mockInstalledSnap(c, s.state, snapByaml, useHook) 2382 mockInstalledSnap(c, s.state, snapBByaml, useHook) 2383 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 2384 2385 restore := snapstatetest.MockDeviceModel(DefaultModel()) 2386 defer restore() 2387 2388 // pretend snap-b holds base-snap-b. 2389 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil) 2390 2391 // pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed) 2392 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil) 2393 2394 changes := st.Changes() 2395 c.Assert(changes, HasLen, 1) 2396 chg := changes[0] 2397 c.Assert(chg.Kind(), Equals, "auto-refresh") 2398 c.Check(chg.Summary(), Equals, `Auto-refresh snaps "base-snap-b", "snap-b"`) 2399 var snapNames []string 2400 var apiData map[string]interface{} 2401 c.Assert(chg.Get("snap-names", &snapNames), IsNil) 2402 c.Check(snapNames, DeepEquals, []string{"base-snap-b", "snap-b"}) 2403 c.Assert(chg.Get("api-data", &apiData), IsNil) 2404 c.Check(apiData, DeepEquals, map[string]interface{}{ 2405 "snap-names": []interface{}{"base-snap-b", "snap-b"}, 2406 }) 2407 2408 tasks := chg.Tasks() 2409 c.Assert(tasks, HasLen, 3) 2410 conditionalRefreshTask := tasks[0] 2411 checkGatingTask(c, conditionalRefreshTask, map[string]*snapstate.RefreshCandidate{ 2412 "base-snap-b": { 2413 SnapSetup: snapstate.SnapSetup{ 2414 Type: "base", 2415 PlugsOnly: true, 2416 Flags: snapstate.Flags{ 2417 IsAutoRefresh: true, 2418 }, 2419 SideInfo: &snap.SideInfo{ 2420 RealName: "base-snap-b", 2421 Revision: snap.R(3), 2422 }, 2423 DownloadInfo: &snap.DownloadInfo{}, 2424 }, 2425 }, 2426 "snap-b": { 2427 SnapSetup: snapstate.SnapSetup{ 2428 Type: "app", 2429 Base: "base-snap-b", 2430 PlugsOnly: true, 2431 Flags: snapstate.Flags{ 2432 IsAutoRefresh: true, 2433 }, 2434 SideInfo: &snap.SideInfo{ 2435 RealName: "snap-b", 2436 Revision: snap.R(2), 2437 }, 2438 DownloadInfo: &snap.DownloadInfo{}, 2439 }, 2440 }, 2441 }) 2442 2443 seenSnaps := make(map[string]bool) 2444 2445 // check that the gate-auto-refresh hooks are run. 2446 // snap-bb's hook is triggered because it is affected by base-snap-b refresh 2447 // (and intersects with affecting snap of snap-b). Note, snap-a is not here 2448 // because it is not affected by snaps affecting snap-b. 2449 for i := 1; i <= 2; i++ { 2450 c.Assert(tasks[i].Kind(), Equals, "run-hook") 2451 var hs hookstate.HookSetup 2452 c.Assert(tasks[i].Get("hook-setup", &hs), IsNil) 2453 c.Check(hs.Hook, Equals, "gate-auto-refresh") 2454 c.Check(hs.Optional, Equals, true) 2455 seenSnaps[hs.Snap] = true 2456 var data interface{} 2457 c.Assert(tasks[i].Get("hook-context", &data), IsNil) 2458 switch hs.Snap { 2459 case "snap-b": 2460 c.Check(data, DeepEquals, map[string]interface{}{ 2461 "base": true, 2462 "restart": false, 2463 "affecting-snaps": []interface{}{"base-snap-b", "snap-b"}, 2464 }) 2465 case "snap-bb": 2466 c.Check(data, DeepEquals, map[string]interface{}{ 2467 "base": true, 2468 "restart": false, 2469 "affecting-snaps": []interface{}{"base-snap-b"}, 2470 }) 2471 default: 2472 c.Fatalf("unexpected snap %q", hs.Snap) 2473 } 2474 } 2475 c.Check(seenSnaps, DeepEquals, map[string]bool{ 2476 "snap-b": true, 2477 "snap-bb": true, 2478 }) 2479 } 2480 2481 func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnapNoCandidatesAnymore(c *C) { 2482 // only snap-a will have a refresh available 2483 s.store.refreshedSnaps = []*snap.Info{{ 2484 Architectures: []string{"all"}, 2485 SnapType: snap.TypeApp, 2486 SideInfo: snap.SideInfo{ 2487 RealName: "snap-a", 2488 Revision: snap.R(8), 2489 }, 2490 }} 2491 2492 logbuf, restoreLogger := logger.MockLogger() 2493 defer restoreLogger() 2494 2495 st := s.state 2496 st.Lock() 2497 defer st.Unlock() 2498 2499 mockInstalledSnap(c, s.state, snapAyaml, useHook) 2500 mockInstalledSnap(c, s.state, snapByaml, useHook) 2501 mockInstalledSnap(c, s.state, baseSnapByaml, noHook) 2502 2503 restore := snapstatetest.MockDeviceModel(DefaultModel()) 2504 defer restore() 2505 2506 // pretend some snaps are held 2507 c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil) 2508 c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil) 2509 2510 // pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed) 2511 c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil) 2512 c.Assert(st.Changes(), HasLen, 0) 2513 2514 // but base-snap-b has no update anymore. 2515 c.Check(logbuf.String(), testutil.Contains, `auto-refresh: all snaps previously held by "snap-b" are up-to-date`) 2516 } 2517 2518 func verifyPhasedAutorefreshTasks(c *C, tasks []*state.Task, expected []string) { 2519 c.Assert(len(tasks), Equals, len(expected)) 2520 for i, t := range tasks { 2521 var got string 2522 if t.Kind() == "run-hook" { 2523 var hsup hookstate.HookSetup 2524 c.Assert(t.Get("hook-setup", &hsup), IsNil) 2525 got = fmt.Sprintf("%s [%s;%s]", t.Kind(), hsup.Snap, hsup.Hook) 2526 } else { 2527 got = t.Kind() 2528 } 2529 c.Assert(got, Equals, expected[i], Commentf("#%d", i)) 2530 } 2531 }