diff --git a/overlord/devicestate/devicestate_test.go b/overlord/devicestate/devicestate_test.go index c2b79b73ffc..e7787e9f65d 100644 --- a/overlord/devicestate/devicestate_test.go +++ b/overlord/devicestate/devicestate_test.go @@ -24,6 +24,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "testing" "time" @@ -385,6 +386,36 @@ func (s *deviceMgrBaseSuite) setupSnapDeclForNameAndID(c *C, name, snapID, publi assertstatetest.AddMany(s.state, snapDecl) } +func (s *deviceMgrBaseSuite) setupSnapResourcePair(c *C, comp, snapID, publisherID string, resRev, snapRev snap.Revision) { + assertion, err := s.storeSigning.Sign(asserts.SnapResourcePairType, map[string]interface{}{ + "snap-id": snapID, + "resource-name": comp, + "resource-revision": strconv.Itoa(resRev.N), + "snap-revision": strconv.Itoa(snapRev.N), + "developer-id": publisherID, + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + assertstatetest.AddMany(s.state, assertion) +} + +func (s *deviceMgrBaseSuite) setupSnapResourceRevision(c *C, file string, comp, snapID, publisherID string, rev snap.Revision) { + sha, size, err := asserts.SnapFileSHA3_384(file) + c.Assert(err, IsNil) + + assertion, err := s.storeSigning.Sign(asserts.SnapResourceRevisionType, map[string]interface{}{ + "snap-id": snapID, + "resource-name": comp, + "resource-sha3-384": sha, + "resource-size": fmt.Sprint(size), + "resource-revision": strconv.Itoa(rev.N), + "developer-id": publisherID, + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + assertstatetest.AddMany(s.state, assertion) +} + func (s *deviceMgrBaseSuite) setupSnapDecl(c *C, info *snap.Info, publisherID string) { s.setupSnapDeclForNameAndID(c, info.SnapName(), info.SnapID, publisherID) } diff --git a/overlord/devicestate/handlers_systems.go b/overlord/devicestate/handlers_systems.go index 9a1e1ea85eb..d88bd874c92 100644 --- a/overlord/devicestate/handlers_systems.go +++ b/overlord/devicestate/handlers_systems.go @@ -40,8 +40,6 @@ import ( "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/seed" - "github.com/snapcore/snapd/snap" - "github.com/snapcore/snapd/snap/snapfile" "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/timings" ) @@ -307,79 +305,7 @@ func (m *DeviceManager) doCreateRecoverySystem(t *state.Task, _ *tomb.Tomb) (err label := setup.Label systemDirectory := setup.Directory - // get all infos - infoGetter := func(name string) (info *snap.Info, path string, present bool, err error) { - // snaps will come from one of these places: - // * passed into the task via a list of side infos (these would have - // come from a user posting snaps via the API) - // * have just been downloaded by a task in setup.SnapSetupTasks - // * already installed on the system - - for _, l := range setup.LocalSnaps { - if l.SideInfo.RealName != name { - continue - } - - snapf, err := snapfile.Open(l.Path) - if err != nil { - return nil, "", false, err - } - - info, err := snap.ReadInfoFromSnapFile(snapf, l.SideInfo) - if err != nil { - return nil, "", false, err - } - - return info, l.Path, true, nil - } - - // in a remodel scenario, the snaps may need to be fetched and thus - // their content can be different from what we have in already installed - // snaps, so we should first check the download tasks before consulting - // snapstate - logger.Debugf("requested info for snap %q being installed during remodel", name) - for _, tskID := range setup.SnapSetupTasks { - taskWithSnapSetup := st.Task(tskID) - snapsup, err := snapstate.TaskSnapSetup(taskWithSnapSetup) - if err != nil { - return nil, "", false, err - } - if snapsup.SnapName() != name { - continue - } - // by the time this task runs, the file has already been - // downloaded and validated - snapFile, err := snapfile.Open(snapsup.MountFile()) - if err != nil { - return nil, "", false, err - } - info, err = snap.ReadInfoFromSnapFile(snapFile, snapsup.SideInfo) - if err != nil { - return nil, "", false, err - } - - return info, info.MountFile(), true, nil - } - - // either a remodel scenario, in which case the snap is not - // among the ones being fetched, or just creating a recovery - // system, in which case we use the snaps that are already - // installed - - info, err = snapstate.CurrentInfo(st, name) - if err == nil { - hash, _, err := asserts.SnapFileSHA3_384(info.MountFile()) - if err != nil { - return nil, "", true, fmt.Errorf("cannot compute SHA3 of snap file: %v", err) - } - info.Sha3_384 = hash - return info, info.MountFile(), true, nil - } - if _, ok := err.(*snap.NotInstalledError); !ok { - return nil, "", false, err - } - return nil, "", false, nil - } + infoGetter := setupInfoGetter{setup: setup} observeSnapFileWrite := func(recoverySystemDir, where string) error { if recoverySystemDir != systemDirectory { @@ -434,7 +360,7 @@ func (m *DeviceManager) doCreateRecoverySystem(t *state.Task, _ *tomb.Tomb) (err // creation could have been interrupted by an unexpected reboot; // consider clearing the recovery system directory and restarting from // scratch - _, err = createSystemForModelFromValidatedSnaps(model, label, db, infoGetter, observeSnapFileWrite) + _, err = createSystemForModelFromValidatedSnaps(st, model, label, db, &infoGetter, observeSnapFileWrite) if err != nil { return fmt.Errorf("cannot create a recovery system with label %q for %v: %v", label, model.Model(), err) } diff --git a/overlord/devicestate/systems.go b/overlord/devicestate/systems.go index 8a4755e3859..ad5993b819c 100644 --- a/overlord/devicestate/systems.go +++ b/overlord/devicestate/systems.go @@ -35,6 +35,8 @@ import ( "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/seed/seedwriter" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/snap/snapfile" "github.com/snapcore/snapd/strutil" ) @@ -208,12 +210,107 @@ func seededSystemFromModeenv() (*seededSystem, error) { return seededSys, nil } -// getInfoFunc is expected to return for a given snap name a snap.Info for that -// snap, a path on disk where the snap file can be found, and whether the snap -// is present. The last bit is relevant for non-essential snaps mentioned in the -// model, which if present and having an 'optional' presence in the model, will -// be added to the recovery system. -type getSnapInfoFunc func(name string) (info *snap.Info, path string, snapIsPresent bool, err error) +// infoGetter is an interface that helps us get information about snaps and +// components that are being installed in a new recovery system. +type infoGetter interface { + // SnapInfo is expected to return for a given snap name a snap.Info for that + // snap, a path on disk where the snap file can be found, and whether the + // snap is present. The last bit is relevant for non-essential snaps + // mentioned in the model, which if present and having an 'optional' + // presence in the model, will be added to the recovery system. + SnapInfo(st *state.State, name string) (info *snap.Info, path string, snapIsPresent bool, err error) + // ComponentInfo is expected to return for a given component ref a + // snap.ComponentInfo for that component, a path on disk where the component + // file can be found, and whether the component is present. The last bit is + // relevant for non-essential components mentioned in the model, which if + // present and having an 'optional' presence in the model, will be added to + // the recovery system. + ComponentInfo(st *state.State, cref naming.ComponentRef, snapInfo *snap.Info) (info *snap.ComponentInfo, path string, present bool, err error) +} + +// setupInfoGetter is an infoGetter that uses a recoverySystemSetup to get +// information about snaps and components that are being installed in a new +// recovery system. +type setupInfoGetter struct { + setup *recoverySystemSetup +} + +func (ig *setupInfoGetter) ComponentInfo(st *state.State, cref naming.ComponentRef, snapInfo *snap.Info) (info *snap.ComponentInfo, path string, present bool, err error) { + return nil, "", false, fmt.Errorf("internal error: creating a recovery system with components from recoverySystemSetup not yet supported") +} + +func (ig *setupInfoGetter) SnapInfo(st *state.State, name string) (info *snap.Info, path string, present bool, err error) { + // snaps will come from one of these places: + // * passed into the task via a list of side infos (these would have + // come from a user posting snaps via the API) + // * have just been downloaded by a task in setup.SnapSetupTasks + // * already installed on the system + + for _, l := range ig.setup.LocalSnaps { + if l.SideInfo.RealName != name { + continue + } + + snapf, err := snapfile.Open(l.Path) + if err != nil { + return nil, "", false, err + } + + info, err := snap.ReadInfoFromSnapFile(snapf, l.SideInfo) + if err != nil { + return nil, "", false, err + } + + return info, l.Path, true, nil + } + + // in a remodel scenario, the snaps may need to be fetched and thus + // their content can be different from what we have in already installed + // snaps, so we should first check the download tasks before consulting + // snapstate + logger.Debugf("requested info for snap %q being installed during remodel", name) + for _, tskID := range ig.setup.SnapSetupTasks { + taskWithSnapSetup := st.Task(tskID) + snapsup, err := snapstate.TaskSnapSetup(taskWithSnapSetup) + if err != nil { + return nil, "", false, err + } + if snapsup.SnapName() != name { + continue + } + // by the time this task runs, the file has already been + // downloaded and validated + snapFile, err := snapfile.Open(snapsup.MountFile()) + if err != nil { + return nil, "", false, err + } + info, err = snap.ReadInfoFromSnapFile(snapFile, snapsup.SideInfo) + if err != nil { + return nil, "", false, err + } + + return info, info.MountFile(), true, nil + } + + // either a remodel scenario, in which case the snap is not + // among the ones being fetched, or just creating a recovery + // system, in which case we use the snaps that are already + // installed + + info, err = snapstate.CurrentInfo(st, name) + if err == nil { + hash, _, err := asserts.SnapFileSHA3_384(info.MountFile()) + if err != nil { + return nil, "", true, fmt.Errorf("cannot compute SHA3 of snap file: %v", err) + } + info.Sha3_384 = hash + return info, info.MountFile(), true, nil + } + if _, ok := err.(*snap.NotInstalledError); !ok { + return nil, "", false, err + } + return nil, "", false, nil +} // snapWriteObserveFunc is called with the recovery system directory and the // path to a snap file being written. The snap file may be written to a location @@ -229,7 +326,14 @@ type snapWriteObserveFunc func(systemDir, where string) error // recovery system - some snaps may be in the recovery system directory while // others may be in the common snaps directory shared between multiple recovery // systems on ubuntu-seed. -func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, db asserts.RODatabase, getInfo getSnapInfoFunc, observeWrite snapWriteObserveFunc) (dir string, err error) { +func createSystemForModelFromValidatedSnaps( + st *state.State, + model *asserts.Model, + label string, + db asserts.RODatabase, + getInfo infoGetter, + observeWrite snapWriteObserveFunc, +) (dir string, err error) { if model.Grade() == asserts.ModelGradeUnset { return "", fmt.Errorf("cannot create a system for pre-UC20 model") } @@ -254,57 +358,85 @@ func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, optsSnaps := make([]*seedwriter.OptionsSnap, 0, len(model.RequiredWithEssentialSnaps())) // collect all snaps that are present modelSnaps := make(map[string]*snap.Info) + // mapping of snap names to map of component names to component infos. + modelComponents := make(map[string]map[string]*snap.ComponentInfo) - getModelSnap := func(name string, essential bool, nonEssentialPresence string) error { + getModelSnap := func(sn *asserts.ModelSnap, essential bool) error { kind := "essential" if !essential { kind = "non-essential" - if nonEssentialPresence != "" { - kind = fmt.Sprintf("non-essential but %v", nonEssentialPresence) + if sn.Presence != "" { + kind = fmt.Sprintf("non-essential but %v", sn.Presence) } } - info, snapPath, present, err := getInfo(name) + snapInfo, snapPath, present, err := getInfo.SnapInfo(st, sn.Name) if err != nil { return fmt.Errorf("cannot obtain %v snap information: %v", kind, err) } - if !essential && !present && nonEssentialPresence == "optional" { + if !essential && !present && sn.Presence == "optional" { // non-essential snap which is declared as optionally // present in the model return nil } // grab those - logger.Debugf("%v snap: %v", kind, name) + logger.Debugf("%v snap: %v", kind, sn.Name) if !present { - return fmt.Errorf("internal error: %v snap %q not present", kind, name) + return fmt.Errorf("internal error: %v snap %q not present", kind, sn.Name) } if _, ok := modelSnaps[snapPath]; ok { // we've already seen this snap return nil } + + var comps []seedwriter.OptionsComponent + modelComponents[sn.Name] = make(map[string]*snap.ComponentInfo) + for compName, comp := range sn.Components { + cref := naming.NewComponentRef(sn.Name, compName) + compInfo, compPath, present, err := getInfo.ComponentInfo(st, cref, snapInfo) + if err != nil { + return fmt.Errorf("cannot obtain component %q information: %v", cref, err) + } + + if !present { + if comp.Presence == "optional" { + continue + } + return fmt.Errorf("internal error: required component %q not present", cref) + } + + // since everything here is done by path, we omit the component + // names. this is what the seedwriter code wants. + comps = append(comps, seedwriter.OptionsComponent{ + Path: compPath, + }) + modelComponents[sn.Name][compPath] = compInfo + } + // present locally // TODO: for grade dangerous we could have a channel here which is not // the model channel, handle that here optsSnaps = append(optsSnaps, &seedwriter.OptionsSnap{ - Path: snapPath, + Path: snapPath, + Components: comps, }) - modelSnaps[snapPath] = info + modelSnaps[snapPath] = snapInfo return nil } for _, sn := range model.EssentialSnaps() { const essential = true - if err := getModelSnap(sn.SnapName(), essential, ""); err != nil { + if err := getModelSnap(sn, essential); err != nil { return "", err } } // snapd is implicitly needed const snapdIsEssential = true - if err := getModelSnap("snapd", snapdIsEssential, ""); err != nil { + if err := getModelSnap(&asserts.ModelSnap{Name: "snapd"}, snapdIsEssential); err != nil { return "", err } for _, sn := range model.SnapsWithoutEssential() { const essential = false - if err := getModelSnap(sn.SnapName(), essential, sn.Presence); err != nil { + if err := getModelSnap(sn, essential); err != nil { return "", err } } @@ -335,44 +467,67 @@ func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, if err := w.Start(db, sf); err != nil { return "", err } + // past this point the system directory is present // TODO:COMPS: take into account local components localSnaps, err := w.LocalSnaps() if err != nil { - return recoverySystemDir, err + return "", err } localARefs := make(map[*seedwriter.SeedSnap][]*asserts.Ref) for _, sn := range localSnaps { info, ok := modelSnaps[sn.Path] if !ok { - return recoverySystemDir, fmt.Errorf("internal error: no snap info for %q", sn.Path) + return "", fmt.Errorf("internal error: no snap info for %q", sn.Path) } + + asserted := info.ID() != "" + // TODO: the side info derived here can be different from what // we have in snap.Info, but getting it this way can be // expensive as we need to compute the hash, try to find a // better way - _, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, model, sf, db) + _, assertions, err := seedwriter.DeriveSideInfo(sn.Path, model, sf, db) if err != nil { if !errors.Is(err, &asserts.NotFoundError{}) { - return recoverySystemDir, err - } else if info.SnapID != "" { - // snap info from state must have come - // from the store, so it is unexpected - // if no assertions for it were found - return recoverySystemDir, fmt.Errorf("internal error: no assertions for asserted snap with ID: %v", info.SnapID) + return "", err + } + + // snap info from state must have come from the store, so it is + // unexpected if no assertions for it were found + if asserted { + return "", fmt.Errorf("internal error: no assertions for asserted snap with ID: %v", info.SnapID) + } + } + + seedComps := make(map[string]*seedwriter.SeedComponent, len(sn.Components)) + for compPath, comp := range modelComponents[info.SnapName()] { + if asserted { + _, compAssertions, err := seedwriter.DeriveComponentSideInfo(compPath, comp, info, model, sf, db) + if err != nil { + return "", err + } + + assertions = append(assertions, compAssertions...) + } + + seedComps[comp.Component.ComponentName] = &seedwriter.SeedComponent{ + ComponentRef: comp.Component, + Path: compPath, + Info: comp, } } - // TODO:COMPS: consider components - if err := w.SetInfo(sn, info, nil); err != nil { - return recoverySystemDir, err + + if err := w.SetInfo(sn, info, seedComps); err != nil { + return "", err } - localARefs[sn] = aRefs + localARefs[sn] = assertions } if err := w.InfoDerived(); err != nil { - return recoverySystemDir, err + return "", err } retrieveAsserts := func(sn, _, _ *seedwriter.SeedSnap) ([]*asserts.Ref, error) { @@ -383,7 +538,7 @@ func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, // get the list of snaps we need in this iteration toDownload, err := w.SnapsToDownload() if err != nil { - return recoverySystemDir, err + return "", err } // which should be empty as all snaps should be accounted for // already @@ -392,12 +547,12 @@ func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, for _, sn := range toDownload { which = append(which, sn.SnapName()) } - return recoverySystemDir, fmt.Errorf("internal error: need to download snaps: %v", strings.Join(which, ", ")) + return "", fmt.Errorf("internal error: need to download snaps: %v", strings.Join(which, ", ")) } complete, err := w.Downloaded(retrieveAsserts) if err != nil { - return recoverySystemDir, err + return "", err } if complete { logger.Debugf("snap processing for creating %q complete", label) @@ -411,7 +566,7 @@ func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, unassertedSnaps, err := w.UnassertedSnaps() if err != nil { - return recoverySystemDir, err + return "", err } if len(unassertedSnaps) > 0 { locals := make([]string, len(unassertedSnaps)) @@ -439,15 +594,15 @@ func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, return osutil.CopyFile(src, dst, 0) } if err := w.SeedSnaps(copySnap); err != nil { - return recoverySystemDir, err + return "", err } if err := w.WriteMeta(); err != nil { - return recoverySystemDir, err + return "", err } bootSnaps, err := w.BootSnaps() if err != nil { - return recoverySystemDir, err + return "", err } bootWith := &boot.RecoverySystemBootableSet{} for _, sn := range bootSnaps { @@ -460,7 +615,7 @@ func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, } } if err := boot.MakeRecoverySystemBootable(model, boot.InitramfsUbuntuSeedDir, recoverySystemDirInRootDir, bootWith); err != nil { - return recoverySystemDir, fmt.Errorf("cannot make candidate recovery system %q bootable: %v", label, err) + return "", fmt.Errorf("cannot make candidate recovery system %q bootable: %v", label, err) } logger.Noticef("created recovery system %q", label) diff --git a/overlord/devicestate/systems_test.go b/overlord/devicestate/systems_test.go index ef694deff85..df16dd97996 100644 --- a/overlord/devicestate/systems_test.go +++ b/overlord/devicestate/systems_test.go @@ -36,8 +36,11 @@ import ( "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/devicestate" + "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/seed/seedtest" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/snap/snapfile" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" ) @@ -52,18 +55,47 @@ type createSystemSuite struct { var _ = Suite(&createSystemSuite{}) +func withComponents(yaml string, comps map[string]snap.ComponentType) string { + if len(comps) == 0 { + return yaml + } + + var b strings.Builder + b.WriteString(yaml) + b.WriteString("\ncomponents:") + for name, typ := range comps { + fmt.Fprintf(&b, "\n %s:\n type: %s", name, typ) + } + return b.String() +} + var ( genericSnapYaml = "name: %s\nversion: 1.0\n%s" snapYamls = map[string]string{ - "pc-kernel": "name: pc-kernel\nversion: 1.0\ntype: kernel", - "pc": "name: pc\nversion: 1.0\ntype: gadget\nbase: core20", - "core20": "name: core20\nversion: 20.1\ntype: base", - "core18": "name: core18\nversion: 18.1\ntype: base", - "snapd": "name: snapd\nversion: 2.2.2\ntype: snapd", - "other-required": fmt.Sprintf(genericSnapYaml, "other-required", "base: core20"), - "other-present": fmt.Sprintf(genericSnapYaml, "other-present", "base: core20"), - "other-core18": fmt.Sprintf(genericSnapYaml, "other-present", "base: core18"), - "other-unasserted": fmt.Sprintf(genericSnapYaml, "other-unasserted", "base: core20"), + "pc-kernel": "name: pc-kernel\nversion: 1.0\ntype: kernel", + "pc": "name: pc\nversion: 1.0\ntype: gadget\nbase: core20", + "core20": "name: core20\nversion: 20.1\ntype: base", + "core18": "name: core18\nversion: 18.1\ntype: base", + "snapd": "name: snapd\nversion: 2.2.2\ntype: snapd", + "other-required": fmt.Sprintf(genericSnapYaml, "other-required", "base: core20"), + "other-present": fmt.Sprintf(genericSnapYaml, "other-present", "base: core20"), + "other-core18": fmt.Sprintf(genericSnapYaml, "other-present", "base: core18"), + "pc-kernel-with-kmods": withComponents("name: pc-kernel-with-kmods\nversion: 1.0\ntype: kernel", map[string]snap.ComponentType{ + "kmod": snap.KernelModulesComponent, + }), + "other-unasserted": withComponents(fmt.Sprintf(genericSnapYaml, "other-unasserted", "base: core20"), map[string]snap.ComponentType{ + "comp": snap.StandardComponent, + }), + "snap-with-components": withComponents(fmt.Sprintf(genericSnapYaml, "snap-with-components", "base: core20"), map[string]snap.ComponentType{ + "comp-1": snap.StandardComponent, + "comp-2": snap.StandardComponent, + }), + } + componentYamls = map[string]string{ + "pc-kernel-with-kmods+kmod": "component: pc-kernel-with-kmods+kmod\ntype: kernel-modules\nversion: 1.0", + "other-unasserted+comp": "component: other-unasserted+comp\ntype: standard\nversion: 10.0", + "snap-with-components+comp-1": "component: snap-with-components+comp-1\ntype: standard\nversion: 22.0", + "snap-with-components+comp-2": "component: snap-with-components+comp-2\ntype: standard\nversion: 33.0", } snapFiles = map[string][][]string{ "pc": { @@ -109,6 +141,52 @@ func (s *createSystemSuite) makeSnap(c *C, name string, rev snap.Revision) *snap return info } +func (s *createSystemSuite) makeSnapWithComponents( + c *C, + name string, + rev snap.Revision, + comps map[string]snap.Revision, +) (*snap.Info, map[string]*snap.ComponentInfo) { + info := s.makeSnap(c, name, rev) + compInfos := make(map[string]*snap.ComponentInfo, len(comps)) + for comp, compRev := range comps { + if compRev.Local() { + c.Assert(rev.Local(), Equals, true, Commentf("component revision cannot be set if snap revision is not set; %q", comp)) + } else { + c.Assert(rev.Store(), Equals, true, Commentf("component revision must be from the store if snap's revision is: %q", comp)) + } + + compPath := snaptest.MakeTestComponent(c, componentYamls[naming.NewComponentRef(name, comp).String()]) + + cpi := snap.MinimalComponentContainerPlaceInfo( + comp, + compRev, + name, + ) + err := os.Rename(compPath, cpi.MountFile()) + c.Assert(err, IsNil) + + if !compRev.Local() { + s.setupSnapResourceRevision(c, cpi.MountFile(), comp, info.SnapID, "my-brand", compRev) + s.setupSnapResourcePair(c, comp, info.SnapID, "my-brand", compRev, rev) + } + + cont, err := snapfile.Open(cpi.MountFile()) + c.Assert(err, IsNil) + + csi := &snap.ComponentSideInfo{ + Component: naming.NewComponentRef(name, comp), + Revision: compRev, + } + + compInfo, err := snap.ReadComponentInfoFromContainer(cont, info, csi) + c.Assert(err, IsNil) + + compInfos[csi.Component.String()] = compInfo + } + return info, compInfos +} + func (s *createSystemSuite) makeEssentialSnapInfos(c *C) map[string]*snap.Info { infos := map[string]*snap.Info{} infos["pc-kernel"] = s.makeSnap(c, "pc-kernel", snap.R(1)) @@ -139,6 +217,50 @@ func validateCore20Seed(c *C, name string, expectedModel *asserts.Model, trusted c.Assert(sd.Model(), DeepEquals, expectedModel) } +func infoGetterFromMaps(c *C, snaps map[string]*snap.Info, comps map[string]*snap.ComponentInfo) testInfoGetter { + snapInfoFn := func(st *state.State, name string) (info *snap.Info, path string, present bool, err error) { + c.Logf("called for: %q", name) + info, present = snaps[name] + if !present { + return info, "", false, nil + } + return info, info.MountFile(), true, nil + } + + componentInfoFn := func(st *state.State, cref naming.ComponentRef, snapInfo *snap.Info) (info *snap.ComponentInfo, path string, present bool, err error) { + c.Logf("called for: %q", cref) + info, present = comps[cref.String()] + if !present { + return info, "", false, nil + } + cpi := snap.MinimalComponentContainerPlaceInfo( + cref.ComponentName, + info.Revision, + snapInfo.SnapName(), + ) + + return info, cpi.MountFile(), true, nil + } + + return testInfoGetter{ + snapInfoFn: snapInfoFn, + componentInfoFn: componentInfoFn, + } +} + +type testInfoGetter struct { + snapInfoFn func(st *state.State, name string) (info *snap.Info, path string, present bool, err error) + componentInfoFn func(st *state.State, cref naming.ComponentRef, snapInfo *snap.Info) (info *snap.ComponentInfo, path string, present bool, err error) +} + +func (ig *testInfoGetter) SnapInfo(st *state.State, name string) (info *snap.Info, path string, present bool, err error) { + return ig.snapInfoFn(st, name) +} + +func (ig *testInfoGetter) ComponentInfo(st *state.State, cref naming.ComponentRef, snapInfo *snap.Info) (info *snap.ComponentInfo, path string, present bool, err error) { + return ig.componentInfoFn(st, cref, snapInfo) +} + func (s *createSystemSuite) TestCreateSystemFromAssertedSnaps(c *C) { bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets() // make it simple for now, no assets @@ -211,14 +333,8 @@ func (s *createSystemSuite) TestCreateSystemFromAssertedSnaps(c *C) { }) expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234") - infoGetter := func(name string) (*snap.Info, string, bool, error) { - c.Logf("called for: %q", name) - info, present := infos[name] - if !present { - return info, "", false, nil - } - return info, info.MountFile(), true, nil - } + infoGetter := infoGetterFromMaps(c, infos, nil) + var newFiles []string snapWriteObserver := func(dir, where string) error { c.Check(dir, Equals, expectedDir) @@ -227,7 +343,7 @@ func (s *createSystemSuite) TestCreateSystemFromAssertedSnaps(c *C) { return nil } - dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, infoGetter, snapWriteObserver) + dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, &infoGetter, snapWriteObserver) c.Assert(err, IsNil) c.Check(newFiles, DeepEquals, []string{ filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"), @@ -258,6 +374,177 @@ func (s *createSystemSuite) TestCreateSystemFromAssertedSnaps(c *C) { "other-core18", "core18", "other-present", "other-required") } +func (s *createSystemSuite) TestCreateSystemFromAssertedSnapsComponents(c *C) { + bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets() + // make it simple for now, no assets + bl.TrustedAssetsMap = nil + bl.StaticCommandLine = "mock static" + bl.CandidateStaticCommandLine = "unused" + bootloader.Force(bl) + + s.state.Lock() + defer s.state.Unlock() + s.setupBrands() + infos := map[string]*snap.Info{ + "pc": s.makeSnap(c, "pc", snap.R(2)), + "core20": s.makeSnap(c, "core20", snap.R(3)), + "snapd": s.makeSnap(c, "snapd", snap.R(4)), + "other-present": s.makeSnap(c, "other-present", snap.R(5)), + "other-required": s.makeSnap(c, "other-required", snap.R(6)), + "other-core18": s.makeSnap(c, "other-core18", snap.R(7)), + "core18": s.makeSnap(c, "core18", snap.R(8)), + } + compInfos := make(map[string]*snap.ComponentInfo) + + // make the kernel snap with components + info, comps := s.makeSnapWithComponents(c, "pc-kernel-with-kmods", snap.R(1), map[string]snap.Revision{ + "kmod": snap.R(11), + }) + for k, v := range comps { + compInfos[k] = v + } + infos["pc-kernel-with-kmods"] = info + + // make another snap that is missing comp-2, but since it is optional in the + // model nothing should go wrong. + info, comps = s.makeSnapWithComponents(c, "snap-with-components", snap.R(2), map[string]snap.Revision{ + "comp-1": snap.R(22), + }) + for k, v := range comps { + compInfos[k] = v + } + infos["snap-with-components"] = info + + model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{ + "architecture": "amd64", + "grade": "dangerous", + "base": "core20", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel-with-kmods", + "id": s.ss.AssertedSnapID("pc-kernel-with-kmods"), + "type": "kernel", + "default-channel": "20", + "components": map[string]interface{}{ + "kmod": map[string]interface{}{ + "presence": "required", + }, + }, + }, + map[string]interface{}{ + "name": "pc", + "id": s.ss.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "snapd", + "id": s.ss.AssertedSnapID("snapd"), + "type": "snapd", + }, + // optional but not present + map[string]interface{}{ + "name": "other-not-present", + "id": s.ss.AssertedSnapID("other-not-present"), + "presence": "optional", + }, + // optional and present + map[string]interface{}{ + "name": "other-present", + "id": s.ss.AssertedSnapID("other-present"), + "presence": "optional", + }, + // required + map[string]interface{}{ + "name": "other-required", + "id": s.ss.AssertedSnapID("other-required"), + "presence": "required", + }, + // different base + map[string]interface{}{ + "name": "other-core18", + "id": s.ss.AssertedSnapID("other-core18"), + }, + // and the actual base for that snap + map[string]interface{}{ + "name": "core18", + "id": s.ss.AssertedSnapID("core18"), + "type": "base", + }, + map[string]interface{}{ + "name": "snap-with-components", + "id": s.ss.AssertedSnapID("snap-with-components"), + "type": "app", + "components": map[string]interface{}{ + "comp-1": map[string]interface{}{ + "presence": "required", + }, + "comp-2": map[string]interface{}{ + "presence": "optional", + }, + }, + }, + }, + }) + expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234") + + infoGetter := infoGetterFromMaps(c, infos, compInfos) + + var newFiles []string + snapWriteObserver := func(dir, where string) error { + c.Check(dir, Equals, expectedDir) + c.Check(where, testutil.FileAbsent) + newFiles = append(newFiles, where) + return nil + } + + dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, &infoGetter, snapWriteObserver) + c.Assert(err, IsNil) + c.Check(newFiles, DeepEquals, []string{ + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc-kernel-with-kmods_1.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc-kernel-with-kmods+kmod_11.comp"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/core20_3.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc_2.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/other-present_5.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/other-required_6.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/other-core18_7.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/core18_8.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snap-with-components_2.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snap-with-components+comp-1_22.comp"), + }) + c.Check(dir, Equals, expectedDir) + + // naive check for files being present + for _, info := range infos { + c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps", filepath.Base(info.MountFile())), + testutil.FileEquals, + testutil.FileContentRef(info.MountFile())) + } + for _, compInfo := range compInfos { + cpi := snap.MinimalComponentContainerPlaceInfo( + compInfo.Component.ComponentName, + compInfo.Revision, + compInfo.Component.SnapName, + ) + + c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps", filepath.Base(cpi.MountFile())), + testutil.FileEquals, + testutil.FileContentRef(cpi.MountFile())) + } + + // recovery system bootenv was set + c.Check(bl.RecoverySystemDir, Equals, "/systems/1234") + c.Check(bl.RecoverySystemBootVars, DeepEquals, map[string]string{ + "snapd_full_cmdline_args": "mock static args from gadget", + "snapd_extra_cmdline_args": "", + "snapd_recovery_kernel": "/snaps/pc-kernel-with-kmods_1.snap", + }) + // load the seed + validateCore20Seed(c, "1234", model, s.storeSigning.Trusted, + "other-core18", "core18", "other-present", "other-required", "snap-with-components") +} + func (s *createSystemSuite) TestCreateSystemFromUnassertedSnaps(c *C) { bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets() // make it simple for now, no assets @@ -304,14 +591,102 @@ func (s *createSystemSuite) TestCreateSystemFromUnassertedSnaps(c *C) { }) expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234") - infoGetter := func(name string) (*snap.Info, string, bool, error) { - c.Logf("called for: %q", name) - info, present := infos[name] - if !present { - return info, "", false, nil + infoGetter := infoGetterFromMaps(c, infos, nil) + + var newFiles []string + snapWriteObserver := func(dir, where string) error { + c.Check(dir, Equals, expectedDir) + c.Check(where, testutil.FileAbsent) + newFiles = append(newFiles, where) + return nil + } + + dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, &infoGetter, snapWriteObserver) + c.Assert(err, IsNil) + c.Check(newFiles, DeepEquals, []string{ + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc-kernel_1.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/core20_3.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc_2.snap"), + // this snap unasserted and lands under the system + filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234/snaps/other-unasserted_1.0.snap"), + }) + c.Check(dir, Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234")) + // naive check for files being present + for _, info := range infos { + if info.Revision.Store() { + c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps", filepath.Base(info.MountFile())), + testutil.FileEquals, + testutil.FileContentRef(info.MountFile())) + } else { + fileName := fmt.Sprintf("%s_%s.snap", info.SnapName(), info.Version) + c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234/snaps", fileName), + testutil.FileEquals, + testutil.FileContentRef(info.MountFile())) } - return info, info.MountFile(), true, nil } + // load the seed + validateCore20Seed(c, "1234", model, s.storeSigning.Trusted, "other-unasserted") + // we have unasserted snaps, so a warning should have been logged + c.Check(s.logbuf.String(), testutil.Contains, `system "1234" contains unasserted snaps "other-unasserted"`) +} + +func (s *createSystemSuite) TestCreateSystemFromUnassertedSnapsComponents(c *C) { + bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets() + // make it simple for now, no assets + bl.TrustedAssetsMap = nil + bl.StaticCommandLine = "mock static" + bl.CandidateStaticCommandLine = "unused" + bootloader.Force(bl) + + s.state.Lock() + defer s.state.Unlock() + s.setupBrands() + infos := s.makeEssentialSnapInfos(c) + // unasserted with local revision + unassertedInfo, compInfos := s.makeSnapWithComponents(c, "other-unasserted", snap.R(-1), map[string]snap.Revision{ + "comp": snap.R(-11), + }) + infos["other-unasserted"] = unassertedInfo + + model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{ + "architecture": "amd64", + "grade": "dangerous", + "base": "core20", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": s.ss.AssertedSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": s.ss.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "snapd", + "id": s.ss.AssertedSnapID("snapd"), + "type": "snapd", + }, + // required + map[string]interface{}{ + "name": "other-unasserted", + "presence": "required", + "components": map[string]interface{}{ + "comp": map[string]interface{}{ + "presence": "required", + }, + }, + }, + }, + }) + expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234") + + infoGetter := infoGetterFromMaps(c, infos, compInfos) + var newFiles []string snapWriteObserver := func(dir, where string) error { c.Check(dir, Equals, expectedDir) @@ -320,7 +695,7 @@ func (s *createSystemSuite) TestCreateSystemFromUnassertedSnaps(c *C) { return nil } - dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, infoGetter, snapWriteObserver) + dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, &infoGetter, snapWriteObserver) c.Assert(err, IsNil) c.Check(newFiles, DeepEquals, []string{ filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"), @@ -329,6 +704,7 @@ func (s *createSystemSuite) TestCreateSystemFromUnassertedSnaps(c *C) { filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc_2.snap"), // this snap unasserted and lands under the system filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234/snaps/other-unasserted_1.0.snap"), + filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234/snaps/other-unasserted+comp_10.0.comp"), }) c.Check(dir, Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234")) // naive check for files being present @@ -344,6 +720,25 @@ func (s *createSystemSuite) TestCreateSystemFromUnassertedSnaps(c *C) { testutil.FileContentRef(info.MountFile())) } } + for _, compInfo := range compInfos { + cpi := snap.MinimalComponentContainerPlaceInfo( + compInfo.Component.ComponentName, + compInfo.Revision, + compInfo.Component.SnapName, + ) + + if compInfo.Revision.Store() { + c.Fatal("unexpected store revision for component") + } + + filename := fmt.Sprintf("%s_%s.comp", compInfo.Component, compInfo.Version("")) + c.Check( + filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234/snaps", filename), + testutil.FileEquals, + testutil.FileContentRef(cpi.MountFile()), + ) + } + // load the seed validateCore20Seed(c, "1234", model, s.storeSigning.Trusted, "other-unasserted") // we have unasserted snaps, so a warning should have been logged @@ -384,14 +779,8 @@ func (s *createSystemSuite) TestCreateSystemWithSomeSnapsAlreadyExisting(c *C) { }) expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234") - infoGetter := func(name string) (*snap.Info, string, bool, error) { - c.Logf("called for: %q", name) - info, present := infos[name] - if !present { - return info, "", false, nil - } - return info, info.MountFile(), true, nil - } + infoGetter := infoGetterFromMaps(c, infos, nil) + var newFiles []string snapWriteObserver := func(dir, where string) error { c.Check(dir, Equals, expectedDir) @@ -409,7 +798,7 @@ func (s *createSystemSuite) TestCreateSystemWithSomeSnapsAlreadyExisting(c *C) { // when a given snap in asserted snaps directory already exists, it is // not copied over - dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, infoGetter, snapWriteObserver) + dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, &infoGetter, snapWriteObserver) c.Assert(err, IsNil) c.Check(newFiles, DeepEquals, []string{ filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"), @@ -475,8 +864,8 @@ func (s *createSystemSuite) TestCreateSystemWithSomeSnapsAlreadyExisting(c *C) { // the unasserted snap goes into the snaps directory under the system // directory, which triggers the error in creating the directory by // seed writer - dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(modelWithUnasserted, "1234unasserted", s.db, - infoGetter, snapWriteObserver) + dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(s.state, modelWithUnasserted, "1234unasserted", s.db, + &infoGetter, snapWriteObserver) c.Assert(err, ErrorMatches, `system "1234unasserted" already exists`) // we failed early, no files were written yet @@ -496,6 +885,7 @@ func (s *createSystemSuite) TestCreateSystemInfoAndAssertsChecks(c *C) { infos["pc-kernel"] = s.makeSnap(c, "pc-kernel", snap.R(1)) infos["core20"] = s.makeSnap(c, "core20", snap.R(3)) infos["snapd"] = s.makeSnap(c, "snapd", snap.R(4)) + infos["snap-with-components"] = s.makeSnap(c, "snap-with-components", snap.R(2)) model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{ "architecture": "amd64", "grade": "dangerous", @@ -524,17 +914,22 @@ func (s *createSystemSuite) TestCreateSystemInfoAndAssertsChecks(c *C) { "id": s.ss.AssertedSnapID("other-required"), "presence": "required", }, + map[string]interface{}{ + "name": "snap-with-components", + "id": s.ss.AssertedSnapID("snap-with-components"), + "presence": "optional", + "components": map[string]interface{}{ + "comp-1": map[string]interface{}{ + "presence": "required", + }, + }, + }, }, }) - infoGetter := func(name string) (*snap.Info, string, bool, error) { - c.Logf("called for: %q", name) - info, present := infos[name] - if !present { - return info, "", false, nil - } - return info, info.MountFile(), true, nil - } + compInfos := make(map[string]*snap.ComponentInfo) + infoGetter := infoGetterFromMaps(c, infos, compInfos) + var observerCalls int snapWriteObserver := func(dir, where string) error { observerCalls++ @@ -545,8 +940,8 @@ func (s *createSystemSuite) TestCreateSystemInfoAndAssertsChecks(c *C) { // when a given snap in asserted snaps directory already exists, it is // not copied over - dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, - infoGetter, snapWriteObserver) + dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, + &infoGetter, snapWriteObserver) c.Assert(err, ErrorMatches, `internal error: essential snap "pc" not present`) c.Check(dir, Equals, "") c.Check(observerCalls, Equals, 0) @@ -558,8 +953,8 @@ func (s *createSystemSuite) TestCreateSystemInfoAndAssertsChecks(c *C) { infos["pc"] = s.makeSnap(c, "pc", snap.R(2)) // and try with with a non essential snap - dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, - infoGetter, snapWriteObserver) + dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, + &infoGetter, snapWriteObserver) c.Assert(err, ErrorMatches, `internal error: non-essential but required snap "other-required" not present`) c.Check(dir, Equals, "") c.Check(observerCalls, Equals, 0) @@ -569,15 +964,24 @@ func (s *createSystemSuite) TestCreateSystemInfoAndAssertsChecks(c *C) { // create the info now infos["other-required"] = s.makeSnap(c, "other-required", snap.R(5)) + _, err = devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, + &infoGetter, snapWriteObserver) + c.Assert(err, ErrorMatches, `internal error: required component "snap-with-components\+comp-1" not present`) + + info, comps := s.makeSnapWithComponents(c, "snap-with-components", snap.R(2), map[string]snap.Revision{ + "comp-1": snap.R(22), + }) + infos["snap-with-components"] = info + compInfos["snap-with-components+comp-1"] = comps["snap-with-components+comp-1"] + // but change the file contents of 'pc' snap so that deriving side info fails randomSnap := snaptest.MakeTestSnapWithFiles(c, `name: random version: 1`, nil) c.Assert(osutil.CopyFile(randomSnap, infos["pc"].MountFile(), osutil.CopyFlagOverwrite), IsNil) - dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, - infoGetter, snapWriteObserver) + _, err = devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, + &infoGetter, snapWriteObserver) c.Assert(err, ErrorMatches, `internal error: no assertions for asserted snap with ID: pcididididididididididididididid`) // we're past the start, so the system directory is there - c.Check(dir, Equals, systemDir) c.Check(osutil.IsDirectory(systemDir), Equals, true) // but no files were copied c.Check(observerCalls, Equals, 0) @@ -626,7 +1030,7 @@ func (s *createSystemSuite) TestCreateSystemGetInfoErr(c *C) { failOn := map[string]bool{} - infoGetter := func(name string) (*snap.Info, string, bool, error) { + snapInfoFn := func(st *state.State, name string) (*snap.Info, string, bool, error) { c.Logf("called for: %q", name) if failOn[name] { return nil, "", false, fmt.Errorf("mock failure for snap %q", name) @@ -637,6 +1041,7 @@ func (s *createSystemSuite) TestCreateSystemGetInfoErr(c *C) { } return info, info.MountFile(), true, nil } + infoGetter := testInfoGetter{snapInfoFn: snapInfoFn} var observerCalls int snapWriteObserver := func(dir, where string) error { observerCalls++ @@ -649,8 +1054,8 @@ func (s *createSystemSuite) TestCreateSystemGetInfoErr(c *C) { // not copied over failOn["pc"] = true - dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, - infoGetter, snapWriteObserver) + dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, + &infoGetter, snapWriteObserver) c.Assert(err, ErrorMatches, `cannot obtain essential snap information: mock failure for snap "pc"`) c.Check(dir, Equals, "") c.Check(observerCalls, Equals, 0) @@ -658,8 +1063,8 @@ func (s *createSystemSuite) TestCreateSystemGetInfoErr(c *C) { failOn["pc"] = false failOn["other-required"] = true - dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, - infoGetter, snapWriteObserver) + dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, + &infoGetter, snapWriteObserver) c.Assert(err, ErrorMatches, `cannot obtain non-essential but required snap information: mock failure for snap "other-required"`) c.Check(dir, Equals, "") c.Check(observerCalls, Equals, 0) @@ -680,16 +1085,17 @@ func (s *createSystemSuite) TestCreateSystemNonUC20(c *C) { "gadget": "pc", }) - infoGetter := func(name string) (*snap.Info, string, bool, error) { + snapInfoFn := func(st *state.State, name string) (*snap.Info, string, bool, error) { c.Fatalf("unexpected call") return nil, "", false, fmt.Errorf("unexpected call") } + infoGetter := testInfoGetter{snapInfoFn: snapInfoFn} snapWriteObserver := func(dir, where string) error { c.Fatalf("unexpected call") return fmt.Errorf("unexpected call") } - dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, - infoGetter, snapWriteObserver) + dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, + &infoGetter, snapWriteObserver) c.Assert(err, ErrorMatches, `cannot create a system for pre-UC20 model`) c.Check(dir, Equals, "") } @@ -726,14 +1132,7 @@ func (s *createSystemSuite) TestCreateSystemImplicitSnaps(c *C) { }) expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234") - infoGetter := func(name string) (*snap.Info, string, bool, error) { - c.Logf("called for: %q", name) - info, present := infos[name] - if !present { - return info, "", false, nil - } - return info, info.MountFile(), true, nil - } + infoGetter := infoGetterFromMaps(c, infos, nil) var newFiles []string snapWriteObserver := func(dir, where string) error { c.Check(dir, Equals, expectedDir) @@ -741,8 +1140,8 @@ func (s *createSystemSuite) TestCreateSystemImplicitSnaps(c *C) { return nil } - dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, - infoGetter, snapWriteObserver) + dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, + &infoGetter, snapWriteObserver) c.Assert(err, IsNil) c.Check(newFiles, DeepEquals, []string{ filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"), @@ -786,13 +1185,7 @@ func (s *createSystemSuite) TestCreateSystemObserverErr(c *C) { }, }) - infoGetter := func(name string) (*snap.Info, string, bool, error) { - info, present := infos[name] - if !present { - return info, "", false, nil - } - return info, info.MountFile(), true, nil - } + infoGetter := infoGetterFromMaps(c, infos, nil) var newFiles []string snapWriteObserver := func(dir, where string) error { newFiles = append(newFiles, where) @@ -802,8 +1195,8 @@ func (s *createSystemSuite) TestCreateSystemObserverErr(c *C) { return nil } - dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, - infoGetter, snapWriteObserver) + _, err := devicestate.CreateSystemForModelFromValidatedSnaps(s.state, model, "1234", s.db, + &infoGetter, snapWriteObserver) c.Assert(err, ErrorMatches, "mocked observer failure") c.Check(newFiles, DeepEquals, []string{ filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"), @@ -811,5 +1204,4 @@ func (s *createSystemSuite) TestCreateSystemObserverErr(c *C) { // we failed on this one filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/core20_3.snap"), }) - c.Check(dir, Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234")) }