Skip to content

Commit

Permalink
many: add mount/unmount/prepare handlers for components (canonical#13423
Browse files Browse the repository at this point in the history
)

* s/backend: add method to remove component diretories

* o/snapstate: add functions that create tasks to install local components

* snap: add method to compare two ComponentSideInfo

* overlord,snap: replace makeTestComponent with snaptest.MakeTestComponent

* o/snapstate: implement mount and prepare handlers for components

* NEWS.md: start of snap components implementation has started

* o/snapmgr: register component tasks

* snap,overlord: change name to ComponentSideInfo.Equal

and add unit test for it.

* o/snapstate: pass directly snap.Info to InstallComponentPath

* o/snapstate: include SnapSetup in ComponentSetup

* o/snapstate: do not set restart boundaries for components yet

* o/snapsate: have SnapSetup as separate task attribute for component tasks

* overlord,snap: address review comments

* o/snapstate: some clean-ups

* o/snapstate: address review comments

* o/snapstate: address review comments
  • Loading branch information
alfonsosanchezbeato committed Dec 16, 2023
1 parent 45e0361 commit e5d50be
Show file tree
Hide file tree
Showing 17 changed files with 1,447 additions and 12 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
3 changes: 2 additions & 1 deletion overlord/snapstate/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 1 addition & 7 deletions overlord/snapstate/backend/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)

Expand Down
10 changes: 10 additions & 0 deletions overlord/snapstate/backend/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/<snap_instance>/components/<snap_rev>/<comp_name>
os.Remove(compMountDir)
// and /snap/<snap_instance>/components/<snap_rev> (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)
Expand Down
26 changes: 24 additions & 2 deletions overlord/snapstate/backend/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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)
}
25 changes: 23 additions & 2 deletions overlord/snapstate/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
183 changes: 183 additions & 0 deletions overlord/snapstate/component.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

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
}
Loading

0 comments on commit e5d50be

Please sign in to comment.