Skip to content

Commit

Permalink
o/devicestate: handle components during a remodel
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewphelpsj committed Jan 6, 2025
1 parent 94af399 commit 22c77ca
Show file tree
Hide file tree
Showing 2 changed files with 681 additions and 25 deletions.
180 changes: 155 additions & 25 deletions overlord/devicestate/devicestate.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,20 @@ const (
remodelChannelSwitch
remodelInstallAction
remodelUpdateAction
remodelAddComponentsAction
)

func (r *remodeler) maybeInstallOrUpdate(ctx context.Context, st *state.State, rt remodelTarget) (remodelAction, []*state.TaskSet, error) {
var components []string
if ms := rt.newModelSnap; ms != nil {
components = make([]string, 0, len(ms.Components))
for c := range ms.Components {
if ms.Presence == "required" {
components = append(components, c)
}
}
}

var snapst snapstate.SnapState
if err := snapstate.Get(st, rt.name, &snapst); err != nil {
if !errors.Is(err, state.ErrNoState) {
Expand All @@ -493,7 +504,7 @@ func (r *remodeler) maybeInstallOrUpdate(ctx context.Context, st *state.State, r
return remodelNoAction, nil, nil
}

goal, err := r.installGoal(rt)
goal, err := r.installGoal(rt, components)
if err != nil {
return 0, nil, err
}
Expand Down Expand Up @@ -539,21 +550,39 @@ func (r *remodeler) maybeInstallOrUpdate(ctx context.Context, st *state.State, r
}

// we need to change the revision if either the incoming model's validation
// sets require a specific revision that we don't have installed
//
// TODO: if the current revision doesn't support the components that we
// need, will also need to change the revision here
needsRevisionChange := (!constraints.Revision.Unset() && constraints.Revision != snapst.Current)

// TODO: we don't properly handle snaps and components that are invalid in
// the incoming model and required by the previous model. this would require
// removing things during a remodel, which isn't something we do at the
// moment. afaict, there it is impossible to remodel from a model that
// sets require a specific revision that we don't have installed, or if the
// current revision doesn't support the components that we need.
needsRevisionChange := (!constraints.Revision.Unset() && constraints.Revision != snapst.Current) || !revisionSupportsComponents(currentInfo, components)

// check if any components are either missing, or installed at the wrong
// revision. note that we will only explicitly handle these needed changes
// if the snap itself, and its channel, are already valid in the incoming
// model
needsComponentChanges := false
for _, c := range components {
csi := snapst.CurrentComponentSideInfo(naming.NewComponentRef(rt.name, c))
if csi == nil {
needsComponentChanges = true
break
}

compConstraints := constraints.Component(c)

if !compConstraints.Revision.Unset() && compConstraints.Revision != csi.Revision {
needsComponentChanges = true
break
}
}

// TODO: we don't properly handle snaps (and now components) that are
// invalid in the incoming model and required by the previous model. this
// would require removing things during a remodel, which isn't something we
// do at the moment. afaict, it is impossible to remodel from a model that
// requires a snap that is invalid in the incoming model.

switch {
case needsRevisionChange || needsChannelChange:
if r.shouldJustSwitch(rt, needsRevisionChange) {
if r.shouldSwitchWithoutRefresh(rt, needsRevisionChange, needsComponentChanges) {
ts, err := snapstate.Switch(st, rt.name, &snapstate.RevisionOptions{
Channel: rt.channel,
})
Expand All @@ -564,7 +593,18 @@ func (r *remodeler) maybeInstallOrUpdate(ctx context.Context, st *state.State, r
return remodelChannelSwitch, []*state.TaskSet{ts}, nil
}

goal, err := r.updateGoal(st, rt, constraints)
// right now, we don't properly handle switching a channel and
// installing components at the same time. in the meantime, we can use
// snapstate.UpdateOne to add additional components and switch the
// channel for us. this method is suboptimal, since we're creating tasks
// for essentially re-installing the snap.
//
// this also will not work well for offline remodeling, since it
// prevents us from using a combination of locally provided components
// and an already installed snap. for that case,
// snapstate.InstallComponents would need to support switching channels
// at the same time as installing components.
goal, err := r.updateGoal(st, rt, components, constraints)
if err != nil {
return 0, nil, err
}
Expand All @@ -586,19 +626,25 @@ func (r *remodeler) maybeInstallOrUpdate(ctx context.Context, st *state.State, r
}

return remodelChannelSwitch, []*state.TaskSet{ts}, nil
case needsComponentChanges:
tss, err := r.installComponents(ctx, st, currentInfo, rt, components)
if err != nil {
return 0, nil, err
}
return remodelAddComponentsAction, tss, nil
default:
// nothing to do but add the snap to the prereq tracker
r.tracker.Add(currentInfo)
return remodelNoAction, nil, nil
}
}

func (r *remodeler) shouldJustSwitch(rt remodelTarget, needsRevisionChange bool) bool {
func (r *remodeler) shouldSwitchWithoutRefresh(rt remodelTarget, needsRevisionChange bool, needsComponentChanges bool) bool {
if !r.offline {
return false
}

if needsRevisionChange {
if needsRevisionChange || needsComponentChanges {
return false
}

Expand All @@ -618,7 +664,16 @@ func (r *remodeler) shouldJustSwitch(rt remodelTarget, needsRevisionChange bool)
return true
}

func (r *remodeler) installGoal(sn remodelTarget) (snapstate.InstallGoal, error) {
func revisionSupportsComponents(info *snap.Info, components []string) bool {
for _, c := range components {
if _, ok := info.Components[c]; !ok {
return false
}
}
return true
}

func (r *remodeler) installGoal(sn remodelTarget, components []string) (snapstate.InstallGoal, error) {
if r.offline {
if sn.newModelSnap == nil {
return nil, errors.New("offline remodeling requires that new model snap is provided")
Expand All @@ -630,16 +685,27 @@ func (r *remodeler) installGoal(sn remodelTarget) (snapstate.InstallGoal, error)
return nil, fmt.Errorf("no snap file provided for %q", sn.name)
}

comps := make(map[*snap.ComponentSideInfo]string, len(components))
for _, c := range components {
lc, ok := r.localContainers.Components[naming.NewComponentRef(sn.name, c).String()]
if !ok {
return nil, fmt.Errorf("no component file provided for %q", c)
}

comps[lc.SideInfo] = lc.Path
}

opts := snapstate.RevisionOptions{
Channel: sn.channel,
ValidationSets: r.vsets,
}

return snapstatePathInstallGoal("", ls.Path, ls.SideInfo, nil, opts), nil
return snapstatePathInstallGoal("", ls.Path, ls.SideInfo, comps, opts), nil
}

return snapstateStoreInstallGoal(snapstate.StoreSnap{
InstanceName: sn.name,
Components: components,
RevOpts: snapstate.RevisionOptions{
Channel: sn.channel,
ValidationSets: r.vsets,
Expand All @@ -650,10 +716,15 @@ func (r *remodeler) installGoal(sn remodelTarget) (snapstate.InstallGoal, error)
func (r *remodeler) installedRevisionUpdateGoal(
st *state.State,
sn remodelTarget,
components []string,
constraints snapasserts.SnapPresenceConstraints,
) (snapstate.UpdateGoal, error) {
if len(components) > 0 {
return nil, errors.New("internal error: falling back to previous snap with components not supported during remodel")
}

if constraints.Revision.Unset() {
return nil, errors.New("internal error: falling back to a previous revision requires that we have a speicifc revision to pick")
return nil, errors.New("internal error: falling back to a previous revision requires that we have a specific revision to pick")
}

var snapst snapstate.SnapState
Expand All @@ -666,6 +737,10 @@ func (r *remodeler) installedRevisionUpdateGoal(
return nil, fmt.Errorf("installed snap %q does not have the required revision in its sequence to be used for offline remodel: %s", sn.name, constraints.Revision)
}

if snapst.Sequence.HasComponents(index) {
return nil, errors.New("TODO: snapstate currently reaches out to the store during a refresh if the snap has components already installed, regardless if the snap is already installed or not")
}

return snapstateStoreUpdateGoal(snapstate.StoreUpdate{
InstanceName: sn.name,
RevOpts: snapstate.RevisionOptions{
Expand All @@ -676,22 +751,36 @@ func (r *remodeler) installedRevisionUpdateGoal(
}), nil
}

func (r *remodeler) updateGoal(st *state.State, sn remodelTarget, constraints snapasserts.SnapPresenceConstraints) (snapstate.UpdateGoal, error) {
func (r *remodeler) updateGoal(st *state.State, sn remodelTarget, components []string, constraints snapasserts.SnapPresenceConstraints) (snapstate.UpdateGoal, error) {
if r.offline {
if sn.newModelSnap == nil {
return nil, errors.New("offline remodeling requires that new model snap is provided")
return nil, errors.New("internal error: offline remodeling requires that new model snap is provided")
}

snapID := sn.newModelSnap.SnapID
ls, ok := r.localContainers.Snaps[snapID]
if !ok {
g, err := r.installedRevisionUpdateGoal(st, sn, constraints)
g, err := r.installedRevisionUpdateGoal(st, sn, components, constraints)
if err != nil {
return nil, err
}
return g, nil
}

// we assume that all of the component revisions are valid with the
// given snap revision. the code in daemon that calls Remodel verifies
// this against the assertions db, and the task handlers in snapstate
// also double check this while installing the snap/components.
comps := make(map[*snap.ComponentSideInfo]string, len(components))
for _, c := range components {
lc, ok := r.localContainers.Components[naming.NewComponentRef(sn.name, c).String()]
if !ok {
return nil, fmt.Errorf("internal error: cannot find local component for %q", c)
}

comps[lc.SideInfo] = lc.Path
}

opts := snapstate.RevisionOptions{
Channel: sn.channel,
ValidationSets: r.vsets,
Expand All @@ -701,9 +790,10 @@ func (r *remodeler) updateGoal(st *state.State, sn remodelTarget, constraints sn
// snapstate for by-path installs (why don't we?)

return snapstatePathUpdateGoal(snapstate.PathSnap{
Path: ls.Path,
SideInfo: ls.SideInfo,
RevOpts: opts,
Path: ls.Path,
SideInfo: ls.SideInfo,
RevOpts: opts,
Components: comps,
}), nil
}

Expand All @@ -713,9 +803,49 @@ func (r *remodeler) updateGoal(st *state.State, sn remodelTarget, constraints sn
Channel: sn.channel,
ValidationSets: r.vsets,
},
// components will be the full list of components needed by the new
// model, and it might already contain any of the components that are
// already installed. the snapstate code handles this case correctly.
AdditionalComponents: components,
}), nil
}

func (r *remodeler) installComponents(ctx context.Context, st *state.State, info *snap.Info, rt remodelTarget, components []string) ([]*state.TaskSet, error) {
r.tracker.Add(info)

if r.offline {
var tss []*state.TaskSet
for _, c := range components {
ref := naming.NewComponentRef(rt.name, c)

lc, ok := r.localContainers.Components[ref.String()]
if !ok {
return nil, fmt.Errorf("internal error: cannot find local component: %q", ref)
}

ts, err := snapstate.InstallComponentPath(st, lc.SideInfo, info, lc.Path, snapstate.Options{
DeviceCtx: r.deviceCtx,
FromChange: r.fromChange,
PrereqTracker: r.tracker,
})
if err != nil {
return nil, err
}
tss = append(tss, ts)

// TODO: verify against validation sets, since we don't do that in
// snapstate for by-path installs (why don't we?)
}
return tss, nil
}

return snapstate.InstallComponents(ctx, st, components, info, r.vsets, snapstate.Options{
DeviceCtx: r.deviceCtx,
FromChange: r.fromChange,
PrereqTracker: r.tracker,
})
}

func remodelEssentialSnapTasks(
ctx context.Context,
st *state.State,
Expand Down Expand Up @@ -786,7 +916,7 @@ func remodelEssentialSnapTasks(
// if we're updating or installing a new essential snap, everything will
// already be handled
return tss, nil
case remodelNoAction:
case remodelNoAction, remodelAddComponentsAction:
ts, err := switchEssentialTasks(ms.newSnap, rm.fromChange)
if err != nil {
return nil, err
Expand Down
Loading

0 comments on commit 22c77ca

Please sign in to comment.