Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

o/snapstate: properly handle components when refreshing to revision that has been on the system before #14498

21 changes: 21 additions & 0 deletions overlord/snapstate/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,8 @@ func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) {
base = "some-base"
case "provenance-snap-id":
name = "provenance-snap"
case "snap-with-components-id":
name = "snap-with-components"
default:
panic(fmt.Sprintf("refresh: unknown snap-id: %s", cand.snapID))
}
Expand Down Expand Up @@ -533,6 +535,14 @@ func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) {
Type: snap.KernelModulesComponent,
Name: "kernel-modules-component",
},
"test-component-extra": {
Type: snap.TestComponent,
Name: "test-component-extra",
},
"test-component-present-in-sequence": {
Type: snap.TestComponent,
Name: "test-component-present-in-sequence",
},
}
}
if name == "some-snap-now-classic" {
Expand Down Expand Up @@ -1117,6 +1127,17 @@ func (f *fakeSnappyBackend) ReadInfo(name string, si *snap.SideInfo) (*snap.Info
info.SnapType = snap.TypeOS
case "snapd":
info.SnapType = snap.TypeSnapd
case "snap-with-components":
info.Components = map[string]*snap.Component{
"test-component": {
Type: snap.TestComponent,
Name: "test-component",
},
"kernel-modules-component": {
Type: snap.KernelModulesComponent,
Name: "kernel-modules-component",
},
}
case "services-snap":
var err error
// fix services after/before so that there is only one solution
Expand Down
33 changes: 17 additions & 16 deletions overlord/snapstate/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func InstallComponents(ctx context.Context, st *state.State, names []string, inf
}
}

compsups, err := componentSetupsForInstall(ctx, st, names, info, opts)
compsups, err := componentSetupsForInstall(ctx, st, names, snapst, snapst.Current, snapst.TrackingChannel, opts)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -111,14 +111,9 @@ func InstallComponents(ctx context.Context, st *state.State, names []string, inf
return append(tss, ts), nil
}

func componentSetupsForInstall(ctx context.Context, st *state.State, names []string, info *snap.Info, opts Options) ([]ComponentSetup, error) {
var snapst SnapState
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
func componentSetupsForInstall(ctx context.Context, st *state.State, names []string, snapst SnapState, snapRev snap.Revision, channel string, opts Options) ([]ComponentSetup, error) {
if len(names) == 0 {
return nil, nil
}

current, err := currentSnaps(st)
Expand All @@ -132,7 +127,7 @@ func componentSetupsForInstall(ctx context.Context, st *state.State, names []str
return nil, err
}

action, err := installComponentAction(st, snapst, opts)
action, err := installComponentAction(st, snapst, snapRev, channel, opts)
if err != nil {
return nil, err
}
Expand All @@ -159,10 +154,16 @@ func componentSetupsForInstall(ctx context.Context, st *state.State, names []str
return componentTargetsFromActionResult("install", sars[0], names)
}

func installComponentAction(st *state.State, snapst SnapState, opts Options) (*store.SnapAction, error) {
si := snapst.CurrentSideInfo()
if si == nil {
return nil, errors.New("internal error: cannot install components for a snap that is not installed")
func installComponentAction(st *state.State, snapst SnapState, snapRev snap.Revision, channel string, opts Options) (*store.SnapAction, error) {
var si *snap.SideInfo
if snapRev.Unset() {
si = snapst.CurrentSideInfo()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmh, test don't seem to hit this case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code, we can't reach that case. Removed.

} else {
index := snapst.LastIndex(snapRev)
if index == -1 {
return nil, fmt.Errorf("internal error: cannot find snap revision %s in sequence", snapRev)
}
si = snapst.Sequence.SideInfos()[index]
}

if si.SnapID == "" {
Expand All @@ -184,8 +185,8 @@ func installComponentAction(st *state.State, snapst SnapState, opts Options) (*s
// that we make sure to get back components that are compatible with the
// currently installed snap
revOpts := RevisionOptions{
Channel: snapst.TrackingChannel,
Revision: snapst.Current,
Revision: si.Revision,
Channel: channel,
}

// TODO:COMPS: handle validation sets here
Expand Down
95 changes: 95 additions & 0 deletions overlord/snapstate/snapstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import (
"github.com/snapcore/snapd/release"
"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/store"
"github.com/snapcore/snapd/strutil"
Expand Down Expand Up @@ -273,6 +274,89 @@ func isCoreSnap(snapName string) bool {
return snapName == defaultCoreSnapName
}

// removeExtraComponentsTasks creates tasks that will remove unwanted components
// that are currently installed alongside the snap revision that is about to be
// installed. If the new snap revision is not in the sequence, then we don't
// have anything to do. If the revision is in the sequence, then we generate
// tasks that will unlink components that are not in compsups.
//
// This is mostly relevant when we're moving from one snap revision to another
// snap revision that has already been installed on the system. The target snap
// might have components that are installed that we don't want any more.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this comment, aren't the component linked to a given snap revision, why would we remove components when switch around by revision?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they are linked by snap revision. Consider this case:

snap revision 1, components a, b
snap revision 2, components b, c

When we move from snap revision 2 -> 1, we need to unlink component a (and discard it, if nothing else references it) and install component c.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused about the comment too, should "target snap might have components that are installed that we don't want any more" be "target revision might have components that were installed in the past but that we don't want any more"? Or maybe state more clearly that this is about checking a revision that was active in the past and still around but not currently. I always find misleading when we talk about installed revisions for non-active but present revisions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean, I'll update it to be explicit when I'm talking about something that was installed in the past.

func removeExtraComponentsTasks(st *state.State, snapst *SnapState, newRevision snap.Revision, compsups []ComponentSetup) (
andrewphelpsj marked this conversation as resolved.
Show resolved Hide resolved
unlinkTasks, discardTasks []*state.Task, err error,
) {
idx := snapst.LastIndex(newRevision)
if idx < 0 {
return nil, nil, nil
}
si := snapst.Sequence.Revisions[idx].Snap

keep := make(map[naming.ComponentRef]bool, len(compsups))
for _, compsup := range compsups {
keep[compsup.CompSideInfo.Component] = true
}

snapst.CurrentComponentSideInfos()
andrewphelpsj marked this conversation as resolved.
Show resolved Hide resolved

linkedForRevision, err := snapst.ComponentInfosForRevision(newRevision)
if err != nil {
return nil, nil, err
}

// this is just a throwaway SnapSetup we create here so that
// unlink-component knows which snap revision we're unlinking the component
// from. note that we don't need to worry about kernel module components
// here, since the components that we are removing are not associated with
// the currently installed snap revision.
snapsup := SnapSetup{
SideInfo: &snap.SideInfo{
RealName: si.RealName,
SnapID: si.SnapID,
Revision: si.Revision,
},
InstanceKey: snapst.InstanceKey,
Type: snap.Type(snapst.SnapType),
}

for _, ci := range linkedForRevision {
if keep[ci.Component] {
continue
}

// note that we shouldn't ever be able to lose components during a
// refresh without a snap revision change. this might be able to happen
// once we introduce components and validation sets? if that is the
// case, we'll need to take care here to use "unlink-current-component"
// and point it to the correct snap setup task.
if snapst.Current == newRevision {
return nil, nil, errors.New("internal error: cannot lose a component during a refresh without a snap revision change")
}

unlink := st.NewTask("unlink-component", fmt.Sprintf(
i18n.G("Unlink component %q for snap revision %s"), ci.Component, snapsup.Revision(),
))

unlink.Set("snap-setup", snapsup)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this confuse findSnapSetupTask ? that's one example but I fear we might have a few bits of logic that assume there is mainly one task in a snap lane with snap-setup attached to it vs id?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've change this in 6014886.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know why I didn't do that in the first place.

unlink.Set("component-setup", ComponentSetup{
CompSideInfo: &ci.ComponentSideInfo,
CompType: ci.Type,
})
unlinkTasks = append(unlinkTasks, unlink)

if !snapst.Sequence.IsComponentRevInRefSeqPtInAnyOtherSeqPt(ci.Component, idx) {
discard := st.NewTask("discard-component", fmt.Sprintf(
i18n.G("Discard previous revision for component %q"), ci.Component,
))
discard.Set("snap-setup-task", unlink.ID())
discard.Set("component-setup-task", unlink.ID())
discardTasks = append(discardTasks, discard)
}
}

return unlinkTasks, discardTasks, nil
}

func doInstall(st *state.State, snapst *SnapState, snapsup SnapSetup, compsups []ComponentSetup, flags int, fromChange string, inUseCheck func(snap.Type) (boot.InUseFunc, error)) (*state.TaskSet, error) {
tr := config.NewTransaction(st)
experimentalRefreshAppAwareness, err := features.Flag(tr, features.RefreshAppAwareness)
Expand Down Expand Up @@ -442,13 +526,24 @@ func doInstall(st *state.State, snapst *SnapState, snapsup SnapSetup, compsups [
}
}

removeExtraComps, discardExtraComps, err := removeExtraComponentsTasks(st, snapst, snapsup.Revision(), compsups)
if err != nil {
return nil, err
}

for _, t := range removeExtraComps {
addTask(t)
}

tasksBeforePreRefreshHook, tasksAfterLinkSnap, tasksAfterPostOpHook, tasksBeforeDiscard, compSetupIDs, err := splitComponentTasksForInstall(
compsups, st, snapst, snapsup, prepare.ID(), fromChange,
)
if err != nil {
return nil, err
}

tasksBeforeDiscard = append(tasksBeforeDiscard, discardExtraComps...)

for _, t := range tasksBeforePreRefreshHook {
addTask(t)
}
Expand Down
Loading
Loading