Skip to content

Commit c037630

Browse files
committed
o/snapstate: prevent components in the seed from being removed
1 parent 1a9523a commit c037630

5 files changed

Lines changed: 95 additions & 6 deletions

File tree

overlord/devicestate/devicestate.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ func delayedCrossMgrInit() {
298298
snapstate.SeedRefreshTasks = SeedRefreshTasks
299299
snapstate.UpdateSeedRefreshChange = UpdateSeedRefreshChange
300300
snapstate.CheckSeedRefreshRemove = CheckSeedRefreshRemove
301+
snapstate.CheckComponentSeedRefreshRemove = CheckComponentSeedRefreshRemove
301302
}
302303

303304
// proxyStore returns the store assertion for the proxy store if one is set.
@@ -1881,6 +1882,24 @@ func CheckSeedRefreshRemove(st *state.State, si *snap.Info, dctx snapstate.Devic
18811882
return nil
18821883
}
18831884

1885+
// CheckComponentSeedRefreshRemove is set by devicestate to prevent removal of
1886+
// components that must remain present for seed-refresh.
1887+
var CheckComponentSeedRefreshRemove = func(st *state.State, si *snap.Info, componentName string, dctx snapstate.DeviceContext) error {
1888+
filter := seedRefreshFilter(st, dctx)
1889+
_, seedRefreshTriggered, err := filter(snapstate.SeedRefreshCandidate{
1890+
InstanceName: si.SnapName(),
1891+
ComponentSetupTaskIDs: map[string]string{componentName: ""},
1892+
})
1893+
if err != nil {
1894+
return err
1895+
}
1896+
1897+
if seedRefreshTriggered {
1898+
return errors.New("cannot remove component present in the current seed while seed-refresh is enabled")
1899+
}
1900+
return nil
1901+
}
1902+
18841903
// seedRefreshFilter returns a closure that filters the given
18851904
// SeedRefreshCandidate so that it only contains the tasks
18861905
// of the snaps and components that can trigger a seed refresh

overlord/snapstate/component.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -796,27 +796,49 @@ func RemoveComponents(st *state.State, snapName string, compName []string, opts
796796
return tss, nil
797797
}
798798

799-
func removeComponentTasks(st *state.State, snapst *SnapState, compst *sequence.ComponentState, info *snap.Info, setupSecurity *state.Task, copts ConflictOptions) (*state.TaskSet, error) {
799+
func canRemoveComponent(st *state.State, compst *sequence.ComponentState, info *snap.Info, copts ConflictOptions) error {
800+
deviceCtx, err := DeviceCtxFromState(st, nil)
801+
if err != nil {
802+
return err
803+
}
800804
instName := info.InstanceName()
801-
802805
// For the moment we consider the same conflicts as if the component
803806
// was actually the snap.
804807
if err := checkChangeConflictIgnoringOneChange(st, instName, nil, copts); err != nil {
805-
return nil, err
808+
return err
806809
}
807810

808811
// check if this component is required by any validation set in enforcing mode
809812
enforcedSets, err := EnforcedValidationSets(st)
810813
if err != nil {
811-
return nil, err
814+
return err
812815
}
813816
pres, err := enforcedSets.Presence(info)
814817
if err != nil {
815-
return nil, err
818+
return err
816819
}
817820
compPres := pres.Component(compst.SideInfo.Component.ComponentName)
818821
if compPres.Presence == asserts.PresenceRequired {
819-
return nil, fmt.Errorf("cannot remove component %q as it is required by an enforcing validation set", compst.SideInfo.Component)
822+
return fmt.Errorf("cannot remove component %q as it is required by an enforcing validation set", compst.SideInfo.Component)
823+
}
824+
825+
seedRefresh, err := seedRefreshEnabled(st)
826+
if err != nil {
827+
return err
828+
}
829+
if seedRefresh {
830+
if err := CheckComponentSeedRefreshRemove(st, info, compst.SideInfo.Component.ComponentName, deviceCtx); err != nil {
831+
return err
832+
}
833+
}
834+
835+
return nil
836+
}
837+
838+
func removeComponentTasks(st *state.State, snapst *SnapState, compst *sequence.ComponentState, info *snap.Info, setupSecurity *state.Task, copts ConflictOptions) (*state.TaskSet, error) {
839+
instName := info.InstanceName()
840+
if err := canRemoveComponent(st, compst, info, copts); err != nil {
841+
return nil, err
820842
}
821843

822844
snapSup := &SnapSetup{

overlord/snapstate/component_remove_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ package snapstate_test
2121

2222
import (
2323
"errors"
24+
"fmt"
2425

26+
"github.com/snapcore/snapd/overlord/configstate/config"
2527
"github.com/snapcore/snapd/overlord/snapstate"
2628
"github.com/snapcore/snapd/overlord/snapstate/sequence"
2729
"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
@@ -460,3 +462,35 @@ func (s *snapmgrTestSuite) TestRemoveComponentUpdateNoConflict(c *C) {
460462
c.Assert(len(tss), Equals, 1)
461463
verifyComponentRemoveTasks(c, compCurrentIsDiscarded, tss[0])
462464
}
465+
466+
func (s *snapmgrTestSuite) TestRemoveComponentInSeedRefresh(c *C) {
467+
const snapName = "some-snap"
468+
const compName = "mycomp"
469+
snapRev := snap.R(1)
470+
info := createTestSnapInfoForComponent(c, snapName, snapRev, compName)
471+
ci, _ := createTestComponent(c, snapName, compName, info)
472+
s.AddCleanup(snapstate.MockReadComponentInfo(func(compMntDir string,
473+
snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) {
474+
return ci, nil
475+
}))
476+
477+
s.state.Lock()
478+
defer s.state.Unlock()
479+
tr := config.NewTransaction(s.state)
480+
c.Assert(tr.Set("core", "experimental.seed-refresh", true), IsNil)
481+
tr.Commit()
482+
483+
s.AddCleanup(snapstate.MockCheckComponentSeedRefreshRemove(func(st *state.State,
484+
si *snap.Info, componentName string, dctx snapstate.DeviceContext) error {
485+
return fmt.Errorf("blocked by seed refresh")
486+
}))
487+
488+
csi1 := snap.NewComponentSideInfo(naming.NewComponentRef(snapName, compName), snap.R(1))
489+
cs1 := sequence.NewComponentState(csi1, snap.StandardComponent)
490+
setStateWithComponents(s.state, snapName, snapRev, []*sequence.ComponentState{cs1})
491+
492+
_, err := snapstate.RemoveComponents(s.state, snapName, []string{compName},
493+
snapstate.RemoveComponentsOpts{})
494+
495+
c.Assert(err, ErrorMatches, "blocked by seed refresh")
496+
}

overlord/snapstate/export_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ func MockReadComponentInfo(mock func(compMntDir string, snapInfo *snap.Info, csi
8080
return func() { readComponentInfoAt = old }
8181
}
8282

83+
func MockCheckComponentSeedRefreshRemove(mock func(st *state.State, si *snap.Info, componentName string, dctx DeviceContext) error) (restore func()) {
84+
old := CheckComponentSeedRefreshRemove
85+
CheckComponentSeedRefreshRemove = mock
86+
return func() {
87+
CheckComponentSeedRefreshRemove = old
88+
}
89+
}
90+
8391
func MockMountPollInterval(intv time.Duration) (restore func()) {
8492
old := mountPollInterval
8593
mountPollInterval = intv

overlord/snapstate/seed.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ var CheckSeedRefreshRemove = func(st *state.State, si *snap.Info, dctx DeviceCon
8080
panic("internal error: snapstate.CheckSeedRefreshRemove is unset")
8181
}
8282

83+
// CheckComponentSeedRefreshRemove is set by devicestate to prevent removal of
84+
// components that must remain present for seed-refresh.
85+
var CheckComponentSeedRefreshRemove = func(st *state.State, si *snap.Info, componentName string, dctx DeviceContext) error {
86+
panic("internal error: snapstate.CheckComponentSeedRefreshRemove is unset")
87+
}
88+
8389
func seedRefreshCandidateForTaskSet(ts *state.TaskSet) (SeedRefreshCandidate, error) {
8490
t, err := ts.Edge(SnapSetupEdge)
8591
if err != nil {

0 commit comments

Comments
 (0)