github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/snapstate/storehelpers.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2018 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 21 22 import ( 23 "context" 24 "fmt" 25 "sort" 26 27 "github.com/snapcore/snapd/logger" 28 "github.com/snapcore/snapd/overlord/auth" 29 "github.com/snapcore/snapd/overlord/state" 30 "github.com/snapcore/snapd/snap" 31 "github.com/snapcore/snapd/store" 32 "github.com/snapcore/snapd/strutil" 33 ) 34 35 var currentSnaps = currentSnapsImpl 36 37 func userIDForSnap(st *state.State, snapst *SnapState, fallbackUserID int) (int, error) { 38 userID := snapst.UserID 39 _, err := auth.User(st, userID) 40 if err == nil { 41 return userID, nil 42 } 43 if err != auth.ErrInvalidUser { 44 return 0, err 45 } 46 return fallbackUserID, nil 47 } 48 49 // userFromUserID returns the first valid user from a series of userIDs 50 // used as successive fallbacks. 51 func userFromUserID(st *state.State, userIDs ...int) (*auth.UserState, error) { 52 var user *auth.UserState 53 var err error 54 for _, userID := range userIDs { 55 if userID == 0 { 56 err = nil 57 continue 58 } 59 user, err = auth.User(st, userID) 60 if err != auth.ErrInvalidUser { 61 break 62 } 63 } 64 return user, err 65 } 66 67 func refreshOptions(st *state.State, origOpts *store.RefreshOptions) (*store.RefreshOptions, error) { 68 var opts store.RefreshOptions 69 70 if origOpts != nil { 71 if origOpts.PrivacyKey != "" { 72 // nothing to add 73 return origOpts, nil 74 } 75 opts = *origOpts 76 } 77 78 if err := st.Get("refresh-privacy-key", &opts.PrivacyKey); err != nil && err != state.ErrNoState { 79 return nil, fmt.Errorf("cannot obtain store request salt: %v", err) 80 } 81 if opts.PrivacyKey == "" { 82 return nil, fmt.Errorf("internal error: request salt is unset") 83 } 84 return &opts, nil 85 } 86 87 // installSize returns total download size of snaps and their prerequisites 88 // (bases and default content providers), querying the store as neccessarry, 89 // potentially more than once. It assumes the initial list of snaps already has 90 // download infos set. 91 // The state must be locked by the caller. 92 var installSize = func(st *state.State, snaps []minimalInstallInfo, userID int) (uint64, error) { 93 curSnaps, err := currentSnaps(st) 94 if err != nil { 95 return 0, err 96 } 97 98 user, err := userFromUserID(st, userID) 99 if err != nil { 100 return 0, err 101 } 102 103 accountedSnaps := map[string]bool{} 104 for _, snap := range curSnaps { 105 accountedSnaps[snap.InstanceName] = true 106 } 107 108 var prereqs []string 109 110 resolveBaseAndContentProviders := func(inst minimalInstallInfo) { 111 if inst.Type() != snap.TypeApp { 112 return 113 } 114 if inst.SnapBase() != "none" { 115 base := defaultCoreSnapName 116 if inst.SnapBase() != "" { 117 base = inst.SnapBase() 118 } 119 if !accountedSnaps[base] { 120 prereqs = append(prereqs, base) 121 accountedSnaps[base] = true 122 } 123 } 124 for _, snapName := range inst.Prereq(st) { 125 if !accountedSnaps[snapName] { 126 prereqs = append(prereqs, snapName) 127 accountedSnaps[snapName] = true 128 } 129 } 130 } 131 132 snapSizes := map[string]uint64{} 133 for _, inst := range snaps { 134 if inst.DownloadSize() == 0 { 135 return 0, fmt.Errorf("internal error: download info missing for %q", inst.InstanceName()) 136 } 137 snapSizes[inst.InstanceName()] = uint64(inst.DownloadSize()) 138 resolveBaseAndContentProviders(inst) 139 } 140 141 opts, err := refreshOptions(st, nil) 142 if err != nil { 143 return 0, err 144 } 145 146 theStore := Store(st, nil) 147 channel := defaultPrereqSnapsChannel() 148 149 // this can potentially be executed multiple times if we (recursively) 150 // find new prerequisites or bases. 151 for len(prereqs) > 0 { 152 actions := []*store.SnapAction{} 153 for _, prereq := range prereqs { 154 action := &store.SnapAction{ 155 Action: "install", 156 InstanceName: prereq, 157 Channel: channel, 158 } 159 actions = append(actions, action) 160 } 161 162 // calls to the store should be done without holding the state lock 163 st.Unlock() 164 results, _, err := theStore.SnapAction(context.TODO(), curSnaps, actions, nil, user, opts) 165 st.Lock() 166 if err != nil { 167 return 0, err 168 } 169 prereqs = []string{} 170 for _, res := range results { 171 snapSizes[res.InstanceName()] = uint64(res.Size) 172 // results may have new base or content providers 173 resolveBaseAndContentProviders(installSnapInfo{res.Info}) 174 } 175 } 176 177 // state is locked at this point 178 179 // since we unlock state above when querying store, other changes may affect 180 // same snaps, therefore obtain current snaps again and only compute total 181 // size of snaps that would actually need to be installed. 182 curSnaps, err = currentSnaps(st) 183 if err != nil { 184 return 0, err 185 } 186 for _, snap := range curSnaps { 187 delete(snapSizes, snap.InstanceName) 188 } 189 190 var total uint64 191 for _, sz := range snapSizes { 192 total += sz 193 } 194 195 return total, nil 196 } 197 198 func installInfo(ctx context.Context, st *state.State, name string, revOpts *RevisionOptions, userID int, deviceCtx DeviceContext) (store.SnapActionResult, error) { 199 // TODO: support ignore-validation? 200 201 curSnaps, err := currentSnaps(st) 202 if err != nil { 203 return store.SnapActionResult{}, err 204 } 205 206 user, err := userFromUserID(st, userID) 207 if err != nil { 208 return store.SnapActionResult{}, err 209 } 210 211 opts, err := refreshOptions(st, nil) 212 if err != nil { 213 return store.SnapActionResult{}, err 214 } 215 216 action := &store.SnapAction{ 217 Action: "install", 218 InstanceName: name, 219 } 220 221 // cannot specify both with the API 222 if revOpts.Revision.Unset() { 223 // the desired channel 224 action.Channel = revOpts.Channel 225 // the desired cohort key 226 action.CohortKey = revOpts.CohortKey 227 } else { 228 // the desired revision 229 action.Revision = revOpts.Revision 230 } 231 232 theStore := Store(st, deviceCtx) 233 st.Unlock() // calls to the store should be done without holding the state lock 234 res, _, err := theStore.SnapAction(ctx, curSnaps, []*store.SnapAction{action}, nil, user, opts) 235 st.Lock() 236 237 return singleActionResult(name, action.Action, res, err) 238 } 239 240 func updateInfo(st *state.State, snapst *SnapState, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (*snap.Info, error) { 241 curSnaps, err := currentSnaps(st) 242 if err != nil { 243 return nil, err 244 } 245 246 refreshOpts, err := refreshOptions(st, nil) 247 if err != nil { 248 return nil, err 249 } 250 251 curInfo, user, err := preUpdateInfo(st, snapst, flags.Amend, userID) 252 if err != nil { 253 return nil, err 254 } 255 256 var storeFlags store.SnapActionFlags 257 if flags.IgnoreValidation { 258 storeFlags = store.SnapActionIgnoreValidation 259 } else { 260 storeFlags = store.SnapActionEnforceValidation 261 } 262 263 action := &store.SnapAction{ 264 Action: "refresh", 265 InstanceName: curInfo.InstanceName(), 266 SnapID: curInfo.SnapID, 267 // the desired channel 268 Channel: opts.Channel, 269 CohortKey: opts.CohortKey, 270 Flags: storeFlags, 271 } 272 273 if curInfo.SnapID == "" { // amend 274 action.Action = "install" 275 action.Epoch = curInfo.Epoch 276 } 277 278 theStore := Store(st, deviceCtx) 279 st.Unlock() // calls to the store should be done without holding the state lock 280 res, _, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, nil, user, refreshOpts) 281 st.Lock() 282 283 sar, err := singleActionResult(curInfo.InstanceName(), action.Action, res, err) 284 return sar.Info, err 285 } 286 287 func preUpdateInfo(st *state.State, snapst *SnapState, amend bool, userID int) (*snap.Info, *auth.UserState, error) { 288 user, err := userFromUserID(st, snapst.UserID, userID) 289 if err != nil { 290 return nil, nil, err 291 } 292 293 curInfo, err := snapst.CurrentInfo() 294 if err != nil { 295 return nil, nil, err 296 } 297 298 if curInfo.SnapID == "" { // covers also trymode 299 if !amend { 300 return nil, nil, store.ErrLocalSnap 301 } 302 } 303 304 return curInfo, user, nil 305 } 306 307 var ErrMissingExpectedResult = fmt.Errorf("unexpectedly empty response from the server (try again later)") 308 309 func singleActionResult(name, action string, results []store.SnapActionResult, e error) (store.SnapActionResult, error) { 310 if len(results) > 1 { 311 return store.SnapActionResult{}, fmt.Errorf("internal error: multiple store results for a single snap op") 312 } 313 if len(results) > 0 { 314 // TODO: if we also have an error log/warn about it 315 return results[0], nil 316 } 317 318 if saErr, ok := e.(*store.SnapActionError); ok { 319 if len(saErr.Other) != 0 { 320 return store.SnapActionResult{}, saErr 321 } 322 323 var snapErr error 324 switch action { 325 case "refresh": 326 snapErr = saErr.Refresh[name] 327 case "install": 328 snapErr = saErr.Install[name] 329 } 330 if snapErr != nil { 331 return store.SnapActionResult{}, snapErr 332 } 333 334 // no result, atypical case 335 if saErr.NoResults { 336 return store.SnapActionResult{}, ErrMissingExpectedResult 337 } 338 } 339 340 return store.SnapActionResult{}, e 341 } 342 343 func updateToRevisionInfo(st *state.State, snapst *SnapState, revision snap.Revision, userID int, deviceCtx DeviceContext) (*snap.Info, error) { 344 // TODO: support ignore-validation? 345 346 curSnaps, err := currentSnaps(st) 347 if err != nil { 348 return nil, err 349 } 350 351 curInfo, user, err := preUpdateInfo(st, snapst, false, userID) 352 if err != nil { 353 return nil, err 354 } 355 356 opts, err := refreshOptions(st, nil) 357 if err != nil { 358 return nil, err 359 } 360 361 action := &store.SnapAction{ 362 Action: "refresh", 363 SnapID: curInfo.SnapID, 364 InstanceName: curInfo.InstanceName(), 365 // the desired revision 366 Revision: revision, 367 } 368 369 theStore := Store(st, deviceCtx) 370 st.Unlock() // calls to the store should be done without holding the state lock 371 res, _, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, nil, user, opts) 372 st.Lock() 373 374 sar, err := singleActionResult(curInfo.InstanceName(), action.Action, res, err) 375 return sar.Info, err 376 } 377 378 func currentSnapsImpl(st *state.State) ([]*store.CurrentSnap, error) { 379 snapStates, err := All(st) 380 if err != nil { 381 return nil, err 382 } 383 384 if len(snapStates) == 0 { 385 // no snaps installed, do not bother any further 386 return nil, nil 387 } 388 389 curSnaps := collectCurrentSnaps(snapStates, nil) 390 return curSnaps, nil 391 } 392 393 func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store.CurrentSnap, *SnapState)) (curSnaps []*store.CurrentSnap) { 394 curSnaps = make([]*store.CurrentSnap, 0, len(snapStates)) 395 396 for _, snapst := range snapStates { 397 if snapst.TryMode { 398 // try mode snaps are completely local and 399 // irrelevant for the operation 400 continue 401 } 402 403 snapInfo, err := snapst.CurrentInfo() 404 if err != nil { 405 continue 406 } 407 408 if snapInfo.SnapID == "" { 409 // the store won't be able to tell what this 410 // is and so cannot include it in the 411 // operation 412 continue 413 } 414 415 installed := &store.CurrentSnap{ 416 InstanceName: snapInfo.InstanceName(), 417 SnapID: snapInfo.SnapID, 418 // the desired channel (not snapInfo.Channel!) 419 TrackingChannel: snapst.TrackingChannel, 420 Revision: snapInfo.Revision, 421 RefreshedDate: revisionDate(snapInfo), 422 IgnoreValidation: snapst.IgnoreValidation, 423 Epoch: snapInfo.Epoch, 424 CohortKey: snapst.CohortKey, 425 } 426 curSnaps = append(curSnaps, installed) 427 428 if consider != nil { 429 consider(installed, snapst) 430 } 431 } 432 433 return curSnaps 434 } 435 436 func refreshCandidates(ctx context.Context, st *state.State, names []string, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, map[string]*SnapState, map[string]bool, error) { 437 snapStates, err := All(st) 438 if err != nil { 439 return nil, nil, nil, err 440 } 441 442 opts, err = refreshOptions(st, opts) 443 if err != nil { 444 return nil, nil, nil, err 445 } 446 447 // check if we have this name at all 448 for _, name := range names { 449 if _, ok := snapStates[name]; !ok { 450 return nil, nil, nil, snap.NotInstalledError{Snap: name} 451 } 452 } 453 454 sort.Strings(names) 455 456 var fallbackID int 457 // normalize fallback user 458 if !user.HasStoreAuth() { 459 user = nil 460 } else { 461 fallbackID = user.ID 462 } 463 464 actionsByUserID := make(map[int][]*store.SnapAction) 465 stateByInstanceName := make(map[string]*SnapState, len(snapStates)) 466 ignoreValidationByInstanceName := make(map[string]bool) 467 nCands := 0 468 469 addCand := func(installed *store.CurrentSnap, snapst *SnapState) { 470 // FIXME: snaps that are not active are skipped for now 471 // until we know what we want to do 472 if !snapst.Active { 473 return 474 } 475 476 if len(names) == 0 && snapst.DevMode { 477 // no auto-refresh for devmode 478 return 479 } 480 481 if len(names) > 0 && !strutil.SortedListContains(names, installed.InstanceName) { 482 return 483 } 484 485 stateByInstanceName[installed.InstanceName] = snapst 486 487 if len(names) == 0 { 488 installed.Block = snapst.Block() 489 } 490 491 userID := snapst.UserID 492 if userID == 0 { 493 userID = fallbackID 494 } 495 actionsByUserID[userID] = append(actionsByUserID[userID], &store.SnapAction{ 496 Action: "refresh", 497 SnapID: installed.SnapID, 498 InstanceName: installed.InstanceName, 499 }) 500 if snapst.IgnoreValidation { 501 ignoreValidationByInstanceName[installed.InstanceName] = true 502 } 503 nCands++ 504 } 505 // determine current snaps and collect candidates for refresh 506 curSnaps := collectCurrentSnaps(snapStates, addCand) 507 508 actionsForUser := make(map[*auth.UserState][]*store.SnapAction, len(actionsByUserID)) 509 noUserActions := actionsByUserID[0] 510 for userID, actions := range actionsByUserID { 511 if userID == 0 { 512 continue 513 } 514 u, err := userFromUserID(st, userID, 0) 515 if err != nil { 516 return nil, nil, nil, err 517 } 518 if u.HasStoreAuth() { 519 actionsForUser[u] = actions 520 } else { 521 noUserActions = append(noUserActions, actions...) 522 } 523 } 524 // coalesce if possible 525 if len(noUserActions) != 0 { 526 if len(actionsForUser) == 0 { 527 actionsForUser[nil] = noUserActions 528 } else { 529 // coalesce no user actions with one other user's 530 for u1, actions := range actionsForUser { 531 actionsForUser[u1] = append(actions, noUserActions...) 532 break 533 } 534 } 535 } 536 537 // TODO: possibly support a deviceCtx 538 theStore := Store(st, nil) 539 540 updates := make([]*snap.Info, 0, nCands) 541 for u, actions := range actionsForUser { 542 st.Unlock() 543 sarsForUser, _, err := theStore.SnapAction(ctx, curSnaps, actions, nil, u, opts) 544 st.Lock() 545 if err != nil { 546 saErr, ok := err.(*store.SnapActionError) 547 if !ok { 548 return nil, nil, nil, err 549 } 550 // TODO: use the warning infra here when we have it 551 logger.Noticef("%v", saErr) 552 } 553 554 for _, sar := range sarsForUser { 555 updates = append(updates, sar.Info) 556 } 557 } 558 559 return updates, stateByInstanceName, ignoreValidationByInstanceName, nil 560 } 561 562 func installCandidates(st *state.State, names []string, channel string, user *auth.UserState) ([]store.SnapActionResult, error) { 563 curSnaps, err := currentSnaps(st) 564 if err != nil { 565 return nil, err 566 } 567 568 opts, err := refreshOptions(st, nil) 569 if err != nil { 570 return nil, err 571 } 572 573 actions := make([]*store.SnapAction, len(names)) 574 for i, name := range names { 575 actions[i] = &store.SnapAction{ 576 Action: "install", 577 InstanceName: name, 578 // the desired channel 579 Channel: channel, 580 } 581 } 582 583 // TODO: possibly support a deviceCtx 584 theStore := Store(st, nil) 585 st.Unlock() // calls to the store should be done without holding the state lock 586 defer st.Lock() 587 results, _, err := theStore.SnapAction(context.TODO(), curSnaps, actions, nil, user, opts) 588 return results, err 589 }