diff --git a/NEWS.md b/NEWS.md index 5a739a747c3..2cd5b7d50b7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,6 @@ +# In progress: +* Installation of local snap components + # Next: * state: add support for notices (from pebble) * daemon: add notices to the snapd API under `/v2/notices` and `/v2/notice` diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go index b549bfc9171..94442195a07 100644 --- a/overlord/snapstate/backend.go +++ b/overlord/snapstate/backend.go @@ -98,7 +98,8 @@ type managerBackend interface { RemoveSnapCommonData(info *snap.Info, opts *dirs.SnapDirOptions) error RemoveSnapSaveData(info *snap.Info, dev snap.Device) error RemoveSnapDataDir(info *snap.Info, hasOtherInstances bool) error - RemoveContainerMountUnits(s snap.ContainerPlaceInfo, meter progress.Meter) error + RemoveComponentDir(cpi snap.ContainerPlaceInfo) error + RemoveContainerMountUnits(cpi snap.ContainerPlaceInfo, meter progress.Meter) error DiscardSnapNamespace(snapName string) error RemoveSnapInhibitLock(snapName string) error RemoveAllSnapAppArmorProfiles() error diff --git a/overlord/snapstate/backend/backend_test.go b/overlord/snapstate/backend/backend_test.go index 49ab0735e57..4616a9cee79 100644 --- a/overlord/snapstate/backend/backend_test.go +++ b/overlord/snapstate/backend/backend_test.go @@ -43,12 +43,6 @@ func makeTestSnap(c *C, snapYamlContent string) string { return snaptest.MakeTestSnapWithFiles(c, snapYamlContent, files) } -func makeTestComponent(c *C, compYaml string) string { - compInfo, err := snap.InfoFromComponentYaml([]byte(compYaml)) - c.Assert(err, IsNil) - return snaptest.MakeTestComponentWithFiles(c, compInfo.FullName()+".comp", compYaml, nil) -} - type backendSuite struct { testutil.BaseTest } @@ -118,7 +112,7 @@ type: test version: 33 ` - compPath := makeTestComponent(c, componentYaml) + compPath := snaptest.MakeTestComponent(c, componentYaml) compInfo, cont, err := backend.OpenComponentFile(compPath) c.Assert(err, IsNil) diff --git a/overlord/snapstate/backend/setup.go b/overlord/snapstate/backend/setup.go index 7ad070d5603..6a3aabd47a1 100644 --- a/overlord/snapstate/backend/setup.go +++ b/overlord/snapstate/backend/setup.go @@ -240,6 +240,16 @@ func (b Backend) RemoveSnapDir(s snap.PlaceInfo, hasOtherInstances bool) error { return nil } +func (b Backend) RemoveComponentDir(cpi snap.ContainerPlaceInfo) error { + compMountDir := cpi.MountDir() + // Remove /snap//components// + os.Remove(compMountDir) + // and /snap//components/ (might fail + // if there are other components installed for this revision) + os.Remove(filepath.Dir(compMountDir)) + return nil +} + // UndoSetupSnap undoes the work of SetupSnap using RemoveSnapFiles. func (b Backend) UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, installRecord *InstallRecord, dev snap.Device, meter progress.Meter) error { return b.RemoveSnapFiles(s, typ, installRecord, dev, meter) diff --git a/overlord/snapstate/backend/setup_test.go b/overlord/snapstate/backend/setup_test.go index 0b5010fcec1..1451924f320 100644 --- a/overlord/snapstate/backend/setup_test.go +++ b/overlord/snapstate/backend/setup_test.go @@ -461,7 +461,7 @@ type: test version: 1.0 `, snapName, compName) - compPath := makeTestComponent(c, componentYaml) + compPath := snaptest.MakeTestComponent(c, componentYaml) cpi := snap.MinimalComponentContainerPlaceInfo(compName, compRev, instanceName, snapRev) installRecord, err := s.be.SetupComponent(compPath, cpi, mockDev, progress.Null) @@ -521,7 +521,7 @@ type: test version: 1.0 `, snapName, compName) - compPath := makeTestComponent(c, componentYaml) + compPath := snaptest.MakeTestComponent(c, componentYaml) cpi := snap.MinimalComponentContainerPlaceInfo(compName, compRev, snapName, snapRev) @@ -545,3 +545,25 @@ version: 1.0 c.Assert(osutil.FileExists(cpi.MountDir()), Equals, false) c.Assert(osutil.FileExists(cpi.MountFile()), Equals, false) } + +func (s *setupSuite) TestSetupComponentFilesDir(c *C) { + snapRev := snap.R(11) + compRev := snap.R(33) + compName := "mycomp" + snapInstance := "mysnap_inst" + cpi := snap.MinimalComponentContainerPlaceInfo(compName, compRev, snapInstance, snapRev) + + installRecord := s.testSetupComponentDo(c, compName, "mysnap", snapInstance, compRev, snapRev) + + err := s.be.RemoveComponentFiles(cpi, installRecord, mockDev, progress.Null) + c.Assert(err, IsNil) + l, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, "*.mount")) + c.Assert(l, HasLen, 0) + c.Assert(osutil.FileExists(cpi.MountDir()), Equals, false) + c.Assert(osutil.FileExists(cpi.MountFile()), Equals, false) + + err = s.be.RemoveComponentDir(cpi) + c.Assert(err, IsNil) + // Directory for the snap revision should be gone + c.Assert(osutil.FileExists(filepath.Dir(cpi.MountDir())), Equals, false) +} diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index 9573f5e3050..9759c808fde 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -902,11 +902,32 @@ func (f *fakeSnappyBackend) SetupSnap(snapFilePath, instanceName string, si *sna } func (f *fakeSnappyBackend) SetupComponent(compFilePath string, compPi snap.ContainerPlaceInfo, dev snap.Device, meter progress.Meter) (installRecord *backend.InstallRecord, err error) { - panic("not used yet in tests") + meter.Notify("setup-component") + f.appendOp(&fakeOp{ + op: "setup-component", + }) + if strings.HasSuffix(compPi.ContainerName(), "+broken") { + return nil, fmt.Errorf("cannot set-up component %q", compPi.ContainerName()) + } + return &backend.InstallRecord{}, nil } func (f *fakeSnappyBackend) UndoSetupComponent(cpi snap.ContainerPlaceInfo, installRecord *backend.InstallRecord, dev snap.Device, meter progress.Meter) error { - panic("not used yet in tests") + meter.Notify("undo-setup-component") + f.appendOp(&fakeOp{ + op: "undo-setup-component", + }) + if strings.HasSuffix(cpi.ContainerName(), "+brokenundo") { + return fmt.Errorf("cannot undo set-up of component %q", cpi.ContainerName()) + } + return nil +} + +func (f *fakeSnappyBackend) RemoveComponentDir(cpi snap.ContainerPlaceInfo) error { + f.appendOp(&fakeOp{ + op: "remove-component-dir", + }) + return nil } func (f *fakeSnappyBackend) ReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) { diff --git a/overlord/snapstate/component.go b/overlord/snapstate/component.go new file mode 100644 index 00000000000..896a0e99a86 --- /dev/null +++ b/overlord/snapstate/component.go @@ -0,0 +1,183 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2023 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package snapstate + +import ( + "errors" + "fmt" + "os" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/overlord/snapstate/backend" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" +) + +// InstallComponentPath returns a set of tasks for installing a snap component +// from a file path. +// +// Note that the state must be locked by the caller. The provided SideInfo can +// contain just a name which results in local sideloading of the component, or +// full metadata in which case the component will appear as installed from the +// store. +func InstallComponentPath(st *state.State, csi *snap.ComponentSideInfo, info *snap.Info, + path string, flags Flags) (*state.TaskSet, error) { + var snapst SnapState + // owner snap must be already installed + err := Get(st, info.InstanceName(), &snapst) + if err != nil { + if errors.Is(err, state.ErrNoState) { + return nil, &snap.NotInstalledError{Snap: info.InstanceName()} + } + return nil, err + } + + // Read ComponentInfo + compInfo, _, err := backend.OpenComponentFile(path) + if err != nil { + return nil, err + } + + // Check snap name matches + if compInfo.Component.SnapName != info.SnapName() { + return nil, fmt.Errorf( + "component snap name %q does not match snap name %q", + compInfo.Component.SnapName, info.RealName) + } + + snapsup := &SnapSetup{ + Base: info.Base, + SideInfo: &info.SideInfo, + Channel: info.Channel, + Flags: flags.ForSnapSetup(), + Type: info.Type(), + Version: info.Version, + PlugsOnly: len(info.Slots) == 0, + InstanceKey: info.InstanceKey, + } + compSetup := &ComponentSetup{ + CompSideInfo: csi, + CompPath: path, + } + // The file passed around is temporary, make sure it gets removed. + // TODO probably this should be part of a flags type in the future. + removeComponentPath := true + return doInstallComponent(st, &snapst, compSetup, snapsup, path, removeComponentPath, "") +} + +// doInstallComponent might be called with the owner snap installed or not. +func doInstallComponent(st *state.State, snapst *SnapState, compSetup *ComponentSetup, + snapsup *SnapSetup, path string, removeComponentPath bool, fromChange string) (*state.TaskSet, error) { + + // TODO check for experimental flag that will hide temporarily components + + snapSi := snapsup.SideInfo + compSi := compSetup.CompSideInfo + + if snapst.IsInstalled() && !snapst.Active { + return nil, fmt.Errorf("cannot install component %q for disabled snap %q", + compSi.Component, snapSi.RealName) + } + + // For the moment we consider the same conflicts as if the component + // was actually the snap. + if err := checkChangeConflictIgnoringOneChange(st, snapsup.InstanceName(), + snapst, fromChange); err != nil { + return nil, err + } + + // Check if we already have the revision in the snaps folder (alters tasks). + // Note that this will search for all snap revisions in the system. + revisionIsPresent := snapst.IsComponentRevPresent(compSi) == true + revisionStr := fmt.Sprintf(" (%s)", compSi.Revision) + + var prepare, prev *state.Task + // if we have a local revision here we go back to that + if path != "" || revisionIsPresent { + prepare = st.NewTask("prepare-component", + fmt.Sprintf(i18n.G("Prepare component %q%s"), + path, revisionStr)) + } else { + // TODO implement download-component + return nil, fmt.Errorf("download-component not implemented yet") + } + prepare.Set("component-setup", compSetup) + prepare.Set("snap-setup", snapsup) + + tasks := []*state.Task{prepare} + prev = prepare + + addTask := func(t *state.Task) { + t.Set("component-setup-task", prepare.ID()) + t.Set("snap-setup-task", prepare.ID()) + t.WaitFor(prev) + tasks = append(tasks, t) + prev = t + } + + // TODO task to fetch and check assertions for component if from store + // (equivalent to "validate-snap") + + // Task that copies the file and creates mount units + if !revisionIsPresent { + mount := st.NewTask("mount-component", + fmt.Sprintf(i18n.G("Mount component %q%s"), + compSi.Component, revisionStr)) + addTask(mount) + } else { + if removeComponentPath { + // If the revision is local, we will not need the + // temporary snap. This can happen when e.g. + // side-loading a local revision again. The path is + // only needed in the "mount-snap" handler and that is + // skipped for local revisions. + if err := os.Remove(path); err != nil { + return nil, err + } + } + } + + // TODO hooks for components + + // We might be replacing a component if a local install, otherwise + // this is not really possible. + compInstalled := snapst.IsComponentInCurrentSeq(compSi.Component) + if compInstalled { + unlink := st.NewTask("unlink-current-component", fmt.Sprintf(i18n.G( + "Make current revision for component %q unavailable"), + compSi.Component)) + addTask(unlink) + } + + // finalize (sets SnapState) + linkSnap := st.NewTask("link-component", + fmt.Sprintf(i18n.G("Make component %q%s available to the system"), + compSi.Component, revisionStr)) + addTask(linkSnap) + + installSet := state.NewTaskSet(tasks...) + installSet.MarkEdge(prepare, BeginEdge) + installSet.MarkEdge(linkSnap, MaybeRebootEdge) + + // TODO do we need to set restart boundaries here? (probably + // for kernel-modules components if installed along the kernel) + + return installSet, nil +} diff --git a/overlord/snapstate/component_install_test.go b/overlord/snapstate/component_install_test.go new file mode 100644 index 00000000000..44d31634472 --- /dev/null +++ b/overlord/snapstate/component_install_test.go @@ -0,0 +1,433 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2023 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package snapstate_test + +import ( + "fmt" + + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/snap/snapfile" + "github.com/snapcore/snapd/snap/snaptest" + . "gopkg.in/check.v1" +) + +const ( + // Install from local file + compOptIsLocal = 1 << iota + // Component revision is already in snaps folder and mounted + compOptRevisionPresent + // Component revision is used by the currently active snap revision + compOptIsActive +) + +// opts is a bitset with compOpt* as possible values. +func expectedComponentInstallTasks(opts int) []string { + var startTasks []string + // Installation of a local component container + if opts&compOptIsLocal != 0 { + startTasks = []string{"prepare-component"} + } else { + startTasks = []string{"download-component"} + } + // Revision is not the same as the current one installed + if opts&compOptRevisionPresent == 0 { + startTasks = append(startTasks, "mount-component") + } + // Component is installed (implicit if compOptRevisionPresent is set) + if opts&compOptIsActive != 0 { + startTasks = append(startTasks, "unlink-current-component") + } + // link-component is always present + startTasks = append(startTasks, "link-component") + + return startTasks +} + +func verifyComponentInstallTasks(c *C, opts int, ts *state.TaskSet) { + kinds := taskKinds(ts.Tasks()) + + expected := expectedComponentInstallTasks(opts) + + c.Assert(kinds, DeepEquals, expected) + + // Check presence of attributes + var firstTaskID string + var compSetup snapstate.ComponentSetup + var snapsup snapstate.SnapSetup + for i, t := range ts.Tasks() { + switch i { + case 0: + if t.Change() == nil { + // Add to a change so we can call snapstate.TaskComponentSetup + chg := t.State().NewChange("install", "install...") + chg.AddAll(ts) + } + c.Assert(t.Get("component-setup", &compSetup), IsNil) + c.Assert(t.Get("snap-setup", &snapsup), IsNil) + firstTaskID = t.ID() + default: + var storedTaskID string + c.Assert(t.Get("component-setup-task", &storedTaskID), IsNil) + c.Assert(storedTaskID, Equals, firstTaskID) + c.Assert(t.Get("snap-setup-task", &storedTaskID), IsNil) + c.Assert(storedTaskID, Equals, firstTaskID) + } + // ComponentSetup/SnapSetup found must match the ones from the first task + csup, ssup, err := snapstate.TaskComponentSetup(t) + c.Assert(err, IsNil) + c.Assert(csup, DeepEquals, &compSetup) + c.Assert(ssup, DeepEquals, &snapsup) + } +} + +func createTestComponent(c *C, snapName, compName string) (*snap.ComponentInfo, string) { + componentYaml := fmt.Sprintf(`component: %s+%s +type: test +version: 1.0 +`, snapName, compName) + compPath := snaptest.MakeTestComponent(c, componentYaml) + compf, err := snapfile.Open(compPath) + c.Assert(err, IsNil) + + ci, err := snap.ReadComponentInfoFromContainer(compf) + c.Assert(err, IsNil) + + return ci, compPath +} + +func createTestSnapInfoForComponent(c *C, snapName string, snapRev snap.Revision, compName string) *snap.Info { + snapYaml := fmt.Sprintf(`name: %s +type: app +version: 1.1 +components: + %s: + type: test +`, snapName, compName) + info, err := snap.InfoFromSnapYaml([]byte(snapYaml)) + c.Assert(err, IsNil) + info.SideInfo = snap.SideInfo{RealName: snapName, Revision: snapRev} + + return info +} + +func createTestSnapSetup(info *snap.Info, flags snapstate.Flags) *snapstate.SnapSetup { + return &snapstate.SnapSetup{ + Base: info.Base, + SideInfo: &info.SideInfo, + Channel: info.Channel, + Flags: flags.ForSnapSetup(), + Type: info.Type(), + Version: info.Version, + PlugsOnly: len(info.Slots) == 0, + InstanceKey: info.InstanceKey, + } +} + +func (s *snapmgrTestSuite) setStateWithOneSnap(c *C, snapName string, snapRev snap.Revision) { + ssi := &snap.SideInfo{RealName: snapName, Revision: snapRev, + SnapID: "some-snap-id"} + snapstate.Set(s.state, snapName, &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos( + []*snapstate.RevisionSideState{ + snapstate.NewRevisionSideInfo(ssi, nil)}), + Current: snapRev, + }) +} + +func (s *snapmgrTestSuite) setStateWithOneComponent(c *C, snapName string, + snapRev snap.Revision, compName string, compRev snap.Revision) { + ssi := &snap.SideInfo{RealName: snapName, Revision: snapRev, + SnapID: "some-snap-id"} + csi := snap.NewComponentSideInfo(naming.NewComponentRef(snapName, compName), compRev) + snapstate.Set(s.state, snapName, &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos( + []*snapstate.RevisionSideState{ + snapstate.NewRevisionSideInfo(ssi, + []*snap.ComponentSideInfo{csi})}), + Current: snapRev, + }) +} + +func (s *snapmgrTestSuite) TestInstallComponentPath(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + _, compPath := createTestComponent(c, snapName, compName) + info := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + + s.state.Lock() + defer s.state.Unlock() + + s.setStateWithOneSnap(c, snapName, snapRev) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, ComponentName: compName}, snap.R(33)) + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(err, IsNil) + + verifyComponentInstallTasks(c, compOptIsLocal, ts) + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + // File is not deleted + c.Assert(osutil.FileExists(compPath), Equals, true) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathForParallelInstall(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + const snapKey = "key" + snapRev := snap.R(1) + _, compPath := createTestComponent(c, snapName, compName) + info := createTestSnapInfoForComponent(c, snapName, snap.R(1), compName) + info.InstanceKey = snapKey + + s.state.Lock() + defer s.state.Unlock() + + // The instance is already installed to make sure it is checked + instanceName := snap.InstanceName(snapName, snapKey) + ssi := &snap.SideInfo{RealName: snapName, Revision: snapRev} + snapstate.Set(s.state, instanceName, &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos( + []*snapstate.RevisionSideState{ + snapstate.NewRevisionSideInfo(ssi, nil)}), + Current: snapRev, + InstanceKey: snapKey, + }) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, ComponentName: compName}, snap.R(33)) + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(err, IsNil) + + verifyComponentInstallTasks(c, compOptIsLocal, ts) + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + // File is not deleted + c.Assert(osutil.FileExists(compPath), Equals, true) + + var snapsup snapstate.SnapSetup + c.Assert(ts.Tasks()[0].Get("snap-setup", &snapsup), IsNil) + c.Assert(snapsup.InstanceKey, Equals, snapKey) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathWrongSnap(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + _, compPath := createTestComponent(c, snapName, compName) + info := createTestSnapInfoForComponent(c, "other-snap", snapRev, compName) + + s.state.Lock() + defer s.state.Unlock() + + s.setStateWithOneSnap(c, "other-snap", snapRev) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, ComponentName: compName}, snap.R(33)) + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(ts, IsNil) + c.Assert(err.Error(), Equals, + `component snap name "mysnap" does not match snap name "other-snap"`) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathCompRevisionPresent(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + compRev := snap.R(7) + _, compPath := createTestComponent(c, snapName, compName) + info := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + + s.state.Lock() + defer s.state.Unlock() + + // Current component same revision to the one we install + s.setStateWithOneComponent(c, snapName, snapRev, compName, compRev) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, ComponentName: compName}, compRev) + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(err, IsNil) + + verifyComponentInstallTasks(c, compOptIsLocal|compOptRevisionPresent|compOptIsActive, ts) + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + // Temporary file is deleted as component file is already in the system + c.Assert(osutil.FileExists(compPath), Equals, false) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathCompRevisionPresentDiffSnapRev(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev1 := snap.R(1) + snapRev2 := snap.R(2) + compRev := snap.R(7) + _, compPath := createTestComponent(c, snapName, compName) + info := createTestSnapInfoForComponent(c, snapName, snapRev1, compName) + + s.state.Lock() + defer s.state.Unlock() + + // There is a component with the same revision to the one we install + // (but it is not for the currently active snap revision). + ssi1 := &snap.SideInfo{RealName: snapName, Revision: snapRev1, + SnapID: "some-snap-id"} + ssi2 := &snap.SideInfo{RealName: snapName, Revision: snapRev2, + SnapID: "some-snap-id"} + csi := snap.NewComponentSideInfo(naming.NewComponentRef(snapName, compName), compRev) + snapstate.Set(s.state, snapName, &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos( + []*snapstate.RevisionSideState{ + snapstate.NewRevisionSideInfo(ssi1, nil), + snapstate.NewRevisionSideInfo(ssi2, + []*snap.ComponentSideInfo{csi}), + }), + Current: snapRev1, + }) + + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(err, IsNil) + + // In this case there is no unlink-current-component, as the component + // is not installed for the active snap revision. + verifyComponentInstallTasks(c, compOptIsLocal|compOptRevisionPresent, ts) + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + // Temporary file is deleted as component file is already in the system + c.Assert(osutil.FileExists(compPath), Equals, false) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathCompAlreadyInstalled(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + compRev := snap.R(33) + _, compPath := createTestComponent(c, snapName, compName) + info := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + + s.state.Lock() + defer s.state.Unlock() + + // Current component revision different to the one we install + s.setStateWithOneComponent(c, snapName, snapRev, compName, snap.R(7)) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, ComponentName: compName}, compRev) + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(err, IsNil) + + verifyComponentInstallTasks(c, compOptIsLocal|compOptIsActive, ts) + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + c.Assert(osutil.FileExists(compPath), Equals, true) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathSnapNotActive(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + compRev := snap.R(7) + _, compPath := createTestComponent(c, snapName, compName) + info := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + + s.state.Lock() + defer s.state.Unlock() + + ssi := &snap.SideInfo{RealName: snapName, Revision: snapRev} + csi := snap.NewComponentSideInfo(naming.NewComponentRef(snapName, compName), compRev) + snapstate.Set(s.state, snapName, &snapstate.SnapState{ + Active: false, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos( + []*snapstate.RevisionSideState{ + snapstate.NewRevisionSideInfo(ssi, + []*snap.ComponentSideInfo{csi})}), + Current: snapRev, + }) + + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(err.Error(), Equals, `cannot install component "mysnap+mycomp" for disabled snap "mysnap"`) + c.Assert(ts, IsNil) + c.Assert(osutil.FileExists(compPath), Equals, true) +} + +func (s *snapmgrTestSuite) TestInstallComponentRemodelConflict(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + _, compPath := createTestComponent(c, snapName, compName) + info := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + + s.state.Lock() + defer s.state.Unlock() + + s.setStateWithOneSnap(c, snapName, snapRev) + + tugc := s.state.NewTask("update-managed-boot-config", "update managed boot config") + chg := s.state.NewChange("remodel", "remodel") + chg.AddTask(tugc) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, ComponentName: compName}, snap.R(33)) + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(ts, IsNil) + c.Assert(err.Error(), Equals, + `remodeling in progress, no other changes allowed until this is done`) +} + +func (s *snapmgrTestSuite) TestInstallComponentUpdateConflict(c *C) { + const snapName = "some-snap" + const compName = "mycomp" + snapRev := snap.R(1) + _, compPath := createTestComponent(c, snapName, compName) + info := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + + s.state.Lock() + defer s.state.Unlock() + + s.setStateWithOneSnap(c, snapName, snapRev) + + tupd, err := snapstate.Update(s.state, snapName, + &snapstate.RevisionOptions{Channel: ""}, s.user.ID, + snapstate.Flags{}) + c.Assert(err, IsNil) + chg := s.state.NewChange("update", "update a snap") + chg.AddAll(tupd) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, ComponentName: compName}, snap.R(33)) + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, + snapstate.Flags{}) + c.Assert(ts, IsNil) + c.Assert(err.Error(), Equals, + `snap "some-snap" has "update" change in progress`) +} diff --git a/overlord/snapstate/component_test.go b/overlord/snapstate/component_test.go new file mode 100644 index 00000000000..ea454af2868 --- /dev/null +++ b/overlord/snapstate/component_test.go @@ -0,0 +1,90 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2023 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package snapstate_test + +import ( + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + . "gopkg.in/check.v1" +) + +func (s *snapmgrTestSuite) TestIsComponentHelpers(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + const compName2 = "mycomp2" + snapRev := snap.R(1) + snapRev2 := snap.R(2) + compRev := snap.R(33) + + s.state.Lock() + defer s.state.Unlock() + + ssi := &snap.SideInfo{RealName: snapName, Revision: snapRev, + SnapID: "some-snap-id"} + ssi2 := &snap.SideInfo{RealName: snapName, Revision: snapRev2, + SnapID: "some-snap-id"} + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + cref2 := naming.NewComponentRef(snapName, compName2) + csi2 := snap.NewComponentSideInfo(cref2, compRev) + + snapSt := &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos( + []*snapstate.RevisionSideState{ + snapstate.NewRevisionSideInfo(ssi, + []*snap.ComponentSideInfo{csi2, csi})}), + Current: snapRev, + } + snapstate.Set(s.state, snapName, snapSt) + + c.Check(snapSt.IsComponentInCurrentSeq(cref), Equals, true) + c.Check(snapSt.IsComponentRevPresent(csi), Equals, true) + + snapSt = &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos( + []*snapstate.RevisionSideState{ + snapstate.NewRevisionSideInfo(ssi2, nil), + snapstate.NewRevisionSideInfo(ssi, []*snap.ComponentSideInfo{csi}), + }), + Current: snapRev2, + } + snapstate.Set(s.state, snapName, snapSt) + + c.Check(snapSt.IsComponentInCurrentSeq(cref), Equals, false) + c.Check(snapSt.IsComponentRevPresent(csi), Equals, true) + + snapSt = &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromRevisionSideInfos( + []*snapstate.RevisionSideState{ + snapstate.NewRevisionSideInfo(ssi2, nil), + snapstate.NewRevisionSideInfo(ssi, nil), + }), + Current: snapRev2, + } + snapstate.Set(s.state, snapName, snapSt) + + c.Check(snapSt.IsComponentInCurrentSeq(cref), Equals, false) + c.Check(snapSt.IsComponentRevPresent(csi), Equals, false) +} diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go index 890404e6479..e67acec617e 100644 --- a/overlord/snapstate/export_test.go +++ b/overlord/snapstate/export_test.go @@ -67,6 +67,12 @@ func MockSnapReadInfo(mock func(name string, si *snap.SideInfo) (*snap.Info, err return func() { snapReadInfo = old } } +func MockReadComponentInfo(mock func(compMntDir string) (*snap.ComponentInfo, error)) (restore func()) { + old := readComponentInfo + readComponentInfo = mock + return func() { readComponentInfo = old } +} + func MockMountPollInterval(intv time.Duration) (restore func()) { old := mountPollInterval mountPollInterval = intv diff --git a/overlord/snapstate/handlers_components.go b/overlord/snapstate/handlers_components.go new file mode 100644 index 00000000000..f91f56665bf --- /dev/null +++ b/overlord/snapstate/handlers_components.go @@ -0,0 +1,229 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2023 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package snapstate + +import ( + "errors" + "fmt" + "time" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/snapstate/backend" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapdir" + "github.com/snapcore/snapd/timings" + "gopkg.in/tomb.v2" +) + +// TaskComponentSetup returns the ComponentSetup and SnapSetup with task params hold +// by or referred to by the task. +func TaskComponentSetup(t *state.Task) (*ComponentSetup, *SnapSetup, error) { + snapsup, err := TaskSnapSetup(t) + if err != nil { + return nil, nil, err + } + + var compSetup ComponentSetup + err = t.Get("component-setup", &compSetup) + if err != nil && !errors.Is(err, state.ErrNoState) { + return nil, nil, err + } + if err == nil { + return &compSetup, snapsup, nil + } + + var id string + err = t.Get("component-setup-task", &id) + if err != nil { + return nil, nil, err + } + + ts := t.State().Task(id) + if ts == nil { + return nil, nil, fmt.Errorf("internal error: tasks are being pruned") + } + if err := ts.Get("component-setup", &compSetup); err != nil { + return nil, nil, err + } + return &compSetup, snapsup, nil +} + +func (m *SnapManager) doPrepareComponent(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + + compSetup, _, err := TaskComponentSetup(t) + if err != nil { + return err + } + + if compSetup.Revision().Unset() { + // This is a local installation, revision is -1 (there + // is no history of local revisions for components). + compSetup.CompSideInfo.Revision = snap.R(-1) + } + + t.Set("component-setup", compSetup) + return nil +} + +func (m *SnapManager) doMountComponent(t *state.Task, _ *tomb.Tomb) (err error) { + st := t.State() + st.Lock() + perfTimings := state.TimingsForTask(t) + compSetup, snapsup, err := TaskComponentSetup(t) + st.Unlock() + if err != nil { + return err + } + + st.Lock() + deviceCtx, err := DeviceCtx(t.State(), t, nil) + st.Unlock() + if err != nil { + return err + } + + // TODO we might want a checkComponents doing checks for some + // component types (see checkSnap and checkSnapCallbacks slice) + + csi := compSetup.CompSideInfo + cpi := snap.MinimalComponentContainerPlaceInfo(csi.Component.ComponentName, + csi.Revision, snapsup.InstanceName(), snapsup.Revision()) + + defer func() { + st.Lock() + defer st.Unlock() + + if err == nil { + return + } + + // RemoveComponentDir is idempotent so it's ok to always + // call it in the cleanup path. + if err := m.backend.RemoveComponentDir(cpi); err != nil { + t.Errorf("cannot cleanup partial setup component %q: %v", + csi.Component, err) + } + }() + + pm := NewTaskProgressAdapterUnlocked(t) + var installRecord *backend.InstallRecord + timings.Run(perfTimings, "setup-component", + fmt.Sprintf("setup component %q", csi.Component), + func(timings.Measurer) { + installRecord, err = m.backend.SetupComponent( + compSetup.CompPath, + cpi, + deviceCtx, + pm) + }) + if err != nil { + return err + } + + // double check that the component is mounted + var readInfoErr error + for i := 0; i < 10; i++ { + compMntDir := cpi.MountDir() + _, readInfoErr = readComponentInfo(compMntDir) + if readInfoErr == nil { + logger.Debugf("component %q (%v) available at %q", + csi.Component, compSetup.Revision(), compMntDir) + break + } + // snap not found, seems is not mounted yet + time.Sleep(mountPollInterval) + } + if readInfoErr != nil { + timings.Run(perfTimings, "undo-setup-component", + fmt.Sprintf("Undo setup of component %q", csi.Component), + func(timings.Measurer) { + err = m.backend.UndoSetupComponent(cpi, + installRecord, deviceCtx, pm) + }) + if err != nil { + st.Lock() + t.Errorf("cannot undo partial setup of component %q: %v", + csi.Component, err) + st.Unlock() + } + + return fmt.Errorf("expected component %q revision %v to be mounted but is not: %w", + csi.Component, compSetup.Revision(), readInfoErr) + } + + st.Lock() + if installRecord != nil { + t.Set("install-record", installRecord) + } + perfTimings.Save(st) + st.Unlock() + + return nil +} + +// Maybe we will need flags as in readInfo +var readComponentInfo = func(compMntDir string) (*snap.ComponentInfo, error) { + cont := snapdir.New(compMntDir) + return snap.ReadComponentInfoFromContainer(cont) +} + +func (m *SnapManager) undoMountComponent(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + compSetup, snapsup, err := TaskComponentSetup(t) + st.Unlock() + if err != nil { + return err + } + + st.Lock() + deviceCtx, err := DeviceCtx(st, t, nil) + st.Unlock() + if err != nil { + return err + } + + var installRecord backend.InstallRecord + st.Lock() + // install-record is optional + err = t.Get("install-record", &installRecord) + st.Unlock() + if err != nil && !errors.Is(err, state.ErrNoState) { + return err + } + + csi := compSetup.CompSideInfo + cpi := snap.MinimalComponentContainerPlaceInfo(csi.Component.ComponentName, + csi.Revision, snapsup.InstanceName(), snapsup.Revision()) + + pm := NewTaskProgressAdapterUnlocked(t) + if err := m.backend.UndoSetupComponent(cpi, &installRecord, deviceCtx, pm); err != nil { + return err + } + + st.Lock() + defer st.Unlock() + + return m.backend.RemoveComponentDir(cpi) +} diff --git a/overlord/snapstate/handlers_components_mount_test.go b/overlord/snapstate/handlers_components_mount_test.go new file mode 100644 index 00000000000..7fcb5c1828c --- /dev/null +++ b/overlord/snapstate/handlers_components_mount_test.go @@ -0,0 +1,290 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2023 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package snapstate_test + +import ( + "fmt" + + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + . "gopkg.in/check.v1" +) + +type mountCompSnapSuite struct { + baseHandlerSuite +} + +var _ = Suite(&mountCompSnapSuite{}) + +func (s *mountCompSnapSuite) SetUpTest(c *C) { + s.baseHandlerSuite.SetUpTest(c) + s.AddCleanup(snapstatetest.MockDeviceModel(DefaultModel())) +} + +func (s *mountCompSnapSuite) TestDoMountComponent(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + compRev := snap.R(7) + ci, compPath := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + + s.state.Lock() + + t := s.state.NewTask("mount-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", snapstate.NewComponentSetup(csi, compPath)) + t.Set("snap-setup", ssu) + chg := s.state.NewChange("test change", "change desc") + chg.AddTask(t) + + s.state.Unlock() + + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + c.Check(chg.Err(), IsNil) + s.state.Unlock() + + // Ensure backend calls have happened with the expected data + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "setup-component", + }, + }) + // File not removed + c.Assert(osutil.FileExists(compPath), Equals, true) +} + +func (s *mountCompSnapSuite) TestDoUndoMountComponent(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + compRev := snap.R(7) + ci, compPath := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + + s.state.Lock() + defer s.state.Unlock() + + t := s.state.NewTask("mount-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", snapstate.NewComponentSetup(csi, compPath)) + t.Set("snap-setup", ssu) + + chg := s.state.NewChange("sample", "...") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + + s.state.Lock() + + c.Check(chg.Err().Error(), Equals, "cannot perform the following tasks:\n"+ + "- provoking total undo (error out)") + + // ensure undo was called the right way + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "setup-component", + }, + { + op: "undo-setup-component", + }, + { + op: "remove-component-dir", + }, + }) +} + +func (s *mountCompSnapSuite) TestDoMountComponentSetupFails(c *C) { + const snapName = "mysnap" + // fakeSnappyBackend.SetupComponent will fail for this component name + const compName = "broken" + snapRev := snap.R(1) + compRev := snap.R(7) + ci, compPath := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + + s.state.Lock() + defer s.state.Unlock() + + t := s.state.NewTask("mount-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", snapstate.NewComponentSetup(csi, compPath)) + t.Set("snap-setup", ssu) + + chg := s.state.NewChange("sample", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + + c.Check(chg.Err().Error(), Equals, "cannot perform the following tasks:\n"+ + "- task desc (cannot set-up component \"mysnap+broken\")") + + // ensure undo was called the right way + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "setup-component", + }, + { + op: "remove-component-dir", + }, + }) +} + +func (s *mountCompSnapSuite) TestDoUndoMountComponentFails(c *C) { + const snapName = "mysnap" + // fakeSnappyBackend.UndoSetupComponent will fail for this component name + const compName = "brokenundo" + snapRev := snap.R(1) + compRev := snap.R(7) + ci, compPath := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, nil + })) + + s.state.Lock() + defer s.state.Unlock() + + t := s.state.NewTask("mount-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", snapstate.NewComponentSetup(csi, compPath)) + t.Set("snap-setup", ssu) + + chg := s.state.NewChange("sample", "...") + chg.AddTask(t) + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(t) + chg.AddTask(terr) + + s.state.Unlock() + + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + + s.state.Lock() + + c.Check(chg.Err().Error(), Equals, "cannot perform the following tasks:\n"+ + "- task desc (cannot undo set-up of component \"mysnap+brokenundo\")\n"+ + "- provoking total undo (error out)") + + // ensure undo was called the right way + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "setup-component", + }, + { + op: "undo-setup-component", + }, + }) +} + +func (s *mountCompSnapSuite) TestDoMountComponentMountFails(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + compRev := snap.R(7) + ci, compPath := createTestComponent(c, snapName, compName) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + s.AddCleanup(snapstate.MockReadComponentInfo(func( + compMntDir string) (*snap.ComponentInfo, error) { + return ci, fmt.Errorf("cannot read component") + })) + + s.state.Lock() + defer s.state.Unlock() + + t := s.state.NewTask("mount-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", snapstate.NewComponentSetup(csi, compPath)) + t.Set("snap-setup", ssu) + + chg := s.state.NewChange("sample", "...") + chg.AddTask(t) + + s.state.Unlock() + + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + + c.Check(chg.Err().Error(), Equals, "cannot perform the following tasks:\n"+ + "- task desc (expected component \"mysnap+mycomp\" revision 7 "+ + "to be mounted but is not: cannot read component)") + + // ensure undo was called the right way + c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{ + { + op: "setup-component", + }, + { + op: "undo-setup-component", + }, + { + op: "remove-component-dir", + }, + }) +} diff --git a/overlord/snapstate/handlers_components_prepare_test.go b/overlord/snapstate/handlers_components_prepare_test.go new file mode 100644 index 00000000000..6fe04181671 --- /dev/null +++ b/overlord/snapstate/handlers_components_prepare_test.go @@ -0,0 +1,70 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2023 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package snapstate_test + +import ( + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + . "gopkg.in/check.v1" +) + +type prepareCompSnapSuite struct { + baseHandlerSuite +} + +var _ = Suite(&prepareCompSnapSuite{}) + +func (s *prepareSnapSuite) TestDoPrepareComponentSimple(c *C) { + const snapName = "mysnap" + const compName = "mycomp" + snapRev := snap.R(1) + // Unset component revision + compRev := snap.R(0) + si := createTestSnapInfoForComponent(c, snapName, snapRev, compName) + ssu := createTestSnapSetup(si, snapstate.Flags{}) + + s.state.Lock() + + t := s.state.NewTask("prepare-component", "task desc") + cref := naming.NewComponentRef(snapName, compName) + csi := snap.NewComponentSideInfo(cref, compRev) + t.Set("component-setup", snapstate.NewComponentSetup(csi, "path-to-component")) + t.Set("snap-setup", ssu) + + s.state.NewChange("test change", "change desc").AddTask(t) + + s.state.Unlock() + + s.se.Ensure() + s.se.Wait() + + s.state.Lock() + defer s.state.Unlock() + + var csup snapstate.ComponentSetup + t.Get("component-setup", &csup) + // Revision should have been set to x1 (-1) + c.Check(csup.CompSideInfo, DeepEquals, snap.NewComponentSideInfo( + cref, snap.R(-1), + )) + c.Check(t.Status(), Equals, state.DoneStatus) +} diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go index 68bcd7568ad..85d74603f2d 100644 --- a/overlord/snapstate/snapmgr.go +++ b/overlord/snapstate/snapmgr.go @@ -43,6 +43,7 @@ import ( "github.com/snapcore/snapd/sandbox" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/channel" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/snapdenv" "github.com/snapcore/snapd/snapdtool" "github.com/snapcore/snapd/store" @@ -183,6 +184,26 @@ func (snapsup *SnapSetup) MountFile() string { return snap.MountFileInDir(blobDir, snapsup.InstanceName(), snapsup.Revision()) } +// ComponentSetup holds the necessary component details to perform +// most component related tasks. +type ComponentSetup struct { + // CompSideInfo for metadata not coming from the component + CompSideInfo *snap.ComponentSideInfo `json:"comp-side-info,omitempty"` + // CompPath is the path to the file + CompPath string `json:"comp-path,omitempty"` +} + +func NewComponentSetup(csi *snap.ComponentSideInfo, compPath string) *ComponentSetup { + return &ComponentSetup{ + CompSideInfo: csi, + CompPath: compPath, + } +} + +func (compsu *ComponentSetup) Revision() snap.Revision { + return compsu.CompSideInfo.Revision +} + // RevertStatus is a status of a snap revert; anything other than DefaultStatus // denotes a reverted snap revision that needs special handling in terms of // refresh blocking. @@ -380,6 +401,23 @@ func (snapst *SnapState) IsInstalled() bool { return true } +// IsComponentInCurrentSeq returns whether a given component is present for the +// snap represented by snapst in the active or last active revision. +func (snapst *SnapState) IsComponentInCurrentSeq(cref naming.ComponentRef) bool { + if !snapst.IsInstalled() { + return false + } + + idx := snapst.LastIndex(snapst.Current) + for _, seqComp := range snapst.Sequence.Revisions[idx].Components { + if seqComp.Component == cref { + return true + } + } + + return false +} + // LocalRevision returns the "latest" local revision. Local revisions // start at -1 and are counted down. func (snapst *SnapState) LocalRevision() snap.Revision { @@ -427,6 +465,19 @@ func (snapst *SnapState) LastIndex(revision snap.Revision) int { return -1 } +// IsComponentRevPresent tells us if a given component revision is +// present in the system for this snap. +func (snapst *SnapState) IsComponentRevPresent(compSi *snap.ComponentSideInfo) bool { + for _, rev := range snapst.Sequence.Revisions { + for _, csi := range rev.Components { + if csi.Equal(compSi) { + return true + } + } + } + return false +} + // Block returns revisions that should be blocked on refreshes, // computed from Sequence[currentRevisionIndex+1:] and considering // special casing resulting from snapst.RevertStatus map. @@ -645,6 +696,9 @@ func Manager(st *state.State, runner *state.TaskRunner) (*SnapManager, error) { runner.AddHandler("enforce-validation-sets", m.doEnforceValidationSets, nil) runner.AddHandler("pre-download-snap", m.doPreDownloadSnap, nil) + // component tasks + runner.AddHandler("prepare-component", m.doPrepareComponent, nil) + runner.AddHandler("mount-component", m.doMountComponent, m.undoMountComponent) // control serialisation runner.AddBlocked(m.blockedTask) diff --git a/snap/component.go b/snap/component.go index 194b30cb1e7..78b3a4df677 100644 --- a/snap/component.go +++ b/snap/component.go @@ -62,6 +62,11 @@ func NewComponentSideInfo(cref naming.ComponentRef, rev Revision) *ComponentSide } } +// Equal compares two ComponentSideInfo. +func (csi *ComponentSideInfo) Equal(other *ComponentSideInfo) bool { + return *csi == *other +} + // componentPlaceInfo holds information about where to put a component in the // system. It implements ContainerPlaceInfo and should be used only via this // interface. diff --git a/snap/component_test.go b/snap/component_test.go index 60ec7298cfe..d1c1c9f1d04 100644 --- a/snap/component_test.go +++ b/snap/component_test.go @@ -289,3 +289,21 @@ func (s *componentSuite) TestComponentContainerPlaceInfoImpl(c *C) { filepath.Join(dirs.GlobalRootDir, "var/lib/snapd/snaps/mysnap_instance+test-info_25.comp")) c.Check(contPi.MountDescription(), Equals, "Mount unit for mysnap_instance+test-info, revision 25") } + +func (s *componentSuite) TestComponentSideInfoEqual(c *C) { + cref := naming.NewComponentRef("snap", "comp") + csi := snap.NewComponentSideInfo(cref, snap.R(1)) + + for _, tc := range []struct { + csi *snap.ComponentSideInfo + equal bool + }{ + {snap.NewComponentSideInfo(naming.NewComponentRef("snap", "comp"), snap.R(1)), true}, + {snap.NewComponentSideInfo(naming.NewComponentRef("other", "comp"), snap.R(1)), false}, + {snap.NewComponentSideInfo(naming.NewComponentRef("snap", "other"), snap.R(1)), false}, + {snap.NewComponentSideInfo(naming.NewComponentRef("snap", "comp"), snap.R(5)), false}, + {snap.NewComponentSideInfo(naming.NewComponentRef("", ""), snap.R(0)), false}, + } { + c.Check(csi.Equal(tc.csi), Equals, tc.equal) + } +} diff --git a/snap/snaptest/snaptest.go b/snap/snaptest/snaptest.go index 71408c7e19b..48edaa9ba5f 100644 --- a/snap/snaptest/snaptest.go +++ b/snap/snaptest/snaptest.go @@ -220,6 +220,12 @@ func MakeTestComponentWithFiles(c *check.C, componentName, componentYaml string, return filepath.Join(compSource, componentName) } +func MakeTestComponent(c *check.C, compYaml string) string { + compInfo, err := snap.InfoFromComponentYaml([]byte(compYaml)) + c.Assert(err, check.IsNil) + return MakeTestComponentWithFiles(c, compInfo.FullName()+".comp", compYaml, nil) +} + func populateContainer(c *check.C, yamlFile, yamlContent string, files [][]string) string { tmpdir := c.MkDir() snapSource := filepath.Join(tmpdir, "snapsrc")