Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions overlord/ifacestate/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,18 @@ func (m *InterfaceManager) refreshAppSetConnections(task *state.Task, appSet *in
task.Logf("%s", snap.BadInterfacesSummary(snapInfo))
}

affectedConnections, err := m.reloadConnections(snapName)
affectedConnections, changedConns, err := m.reloadConnections(snapName)
Comment thread
andrewphelpsj marked this conversation as resolved.
Outdated
if err != nil {
return nil, nil, err
}

// if this task modified any connection states, we take a snapshot of the
// original connections so that we can restore them on the undo path, if
// needed
if err := snapshotChangedConnectionsForUndo(task, snapName, changedConns); err != nil {
return nil, nil, err
}
Comment on lines +383 to +387

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I know this is super convenient place to put this code, it creates some implicit behaviour of this function that is really not obvious, and should as a minimum be documented. Ideally I'd rather see this as a return value an handled on a task level instead, but I recognize the annoying thing about changing the call chain that also goes through setupProfilesForAppSet

@Meulengracht Meulengracht May 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just a completely naive thought: is there any reason we don't just cache the all the refreshed snaps connections from "conns" state at the start of Do and restore this during undo?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This isn't wrong, it would be much more explicit. The catch is we don't know which ones are relevant until quite late, so we'd need to collect all of the connections of a snap, along with information on how they were established. Not great but doable. For some snaps, like snapd there's quite a lot of data to be stored this way.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I pushed a commit renaming some things and adding some comments. I agree that making the task handler itself store this data on the task would be nice, but that would require a pretty large refactor I think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think it's that large a refactor, but it's also something we can do at another stage.


return disconnectedSnaps, affectedConnections, nil
}

Expand Down Expand Up @@ -696,6 +704,9 @@ func (m *InterfaceManager) undoSetupProfiles(task *state.Task, tomb *tomb.Tomb)
if err := snapstateFinishRestart(task, snapsup, finishOpts); err != nil {
return err
}
if err := restoreConnectionsForSetupProfiles(task); err != nil {
return err
}
Comment on lines +716 to +718

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this should mention that it relies on the task variable being set by refreshAppSetConnections during the do path.


// Get the name from SnapSetup and use it to find the current SideInfo
// about the snap, if there is one.
Expand Down Expand Up @@ -2003,7 +2014,7 @@ func (m *InterfaceManager) transitionConnectionsCoreMigration(st *state.State, o
// on disk are rewritten. This is ok because core/ubuntu-core have
// exactly the same profiles and nothing in the generated policies
// has the core snap-name encoded.
if _, err := m.reloadConnections(newName); err != nil {
if _, _, err := m.reloadConnections(newName); err != nil {
return err
}

Expand Down
116 changes: 104 additions & 12 deletions overlord/ifacestate/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,15 +426,104 @@ func isBroken(st *state.State, snapName string) (bool, error) {
return false, nil
}

func cloneConnState(connState *schema.ConnState) *schema.ConnState {
Comment thread
andrewphelpsj marked this conversation as resolved.
clone := *connState

cloneAttrs := func(attrs map[string]any) map[string]any {
if attrs == nil {
return nil
}
return utils.CopyAttributes(attrs)
}

clone.StaticPlugAttrs = cloneAttrs(connState.StaticPlugAttrs)
clone.DynamicPlugAttrs = cloneAttrs(connState.DynamicPlugAttrs)
clone.StaticSlotAttrs = cloneAttrs(connState.StaticSlotAttrs)
clone.DynamicSlotAttrs = cloneAttrs(connState.DynamicSlotAttrs)

return &clone
}

// snapshotModifiedConnectionsForUndo records a snapshot of the connection
Comment thread
andrewphelpsj marked this conversation as resolved.
Outdated
// states, prior to modification. This enables the undo of setup-profiles to
// restore them, if needed.
func snapshotChangedConnectionsForUndo(task *state.Task, instanceName string, changedConns map[string]*schema.ConnState) error {
if len(changedConns) == 0 {
return nil
}

// undo setup-profiles also calls this code while rebuilding old profiles,
// but restoration data should only come from the original do path
if task.Status() == state.UndoingStatus {
Comment thread
andrewphelpsj marked this conversation as resolved.
Outdated
return nil
}

// if this isn't the setup-profiles task that is going to handle the undo,
// then we don't need to keep track of these on the task
if !shouldUndoSetupProfiles(task, instanceName) {
return nil
}

var connectionSnapshot map[string]*schema.ConnState
err := task.Get("changed-connection-snapshot", &connectionSnapshot)
if err != nil && !errors.Is(err, state.ErrNoState) {
return err
}
if connectionSnapshot == nil {
connectionSnapshot = make(map[string]*schema.ConnState)
}

for connID, connState := range changedConns {
if connectionSnapshot[connID] != nil {
// a setup-profiles task can be retried after saving the connection
// states and unlocking for backend setup. keep the first snapshot.
continue
}
connectionSnapshot[connID] = connState
}

task.Set("changed-connection-snapshot", connectionSnapshot)

return nil
}

// restoreConnectionsForSetupProfiles restores connection states saved on a
// setup-profiles task.
func restoreConnectionsForSetupProfiles(task *state.Task) error {
var connectionSnapshot map[string]*schema.ConnState
err := task.Get("changed-connection-snapshot", &connectionSnapshot)
if errors.Is(err, state.ErrNoState) {
return nil
}
if err != nil {
return err
}

st := task.State()

conns, err := getConns(st)
if err != nil {
return err
}

for connID, connState := range connectionSnapshot {
conns[connID] = connState
}
setConns(st, conns)

return nil
}

// reloadConnections reloads connections stored in the state in the repository.
// Using non-empty snapName the operation can be scoped to connections
// affecting a given snap.
//
// The return value is the list of affected snap names and their connection IDs.
func (m *InterfaceManager) reloadConnections(snapName string) (reloadedConnectionIDs []string, err error) {
// The return value is the list of affected snap names and their connection IDs,
Comment thread
andrewphelpsj marked this conversation as resolved.
Outdated
// plus the original connection states that were changed.
func (m *InterfaceManager) reloadConnections(snapName string) (reloadedConnectionIDs []string, changedConns map[string]*schema.ConnState, err error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suppose changedConns includes also dropped connections?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@andrewphelpsj andrewphelpsj May 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, it contains both deleted connections and connections whose static attributes were updated.

conns, err := getConns(m.state)
if err != nil {
return nil, err
return nil, nil, err
}

var policyChecker interfaces.PolicyFunc
Expand All @@ -445,21 +534,22 @@ func (m *InterfaceManager) reloadConnections(snapName string) (reloadedConnectio
if errors.Is(err, state.ErrNoState) {
// everything else is a noop, as no model means no connections
// to reload
return nil, nil
return nil, nil, nil
} else if err != nil {
return nil, err
return nil, nil, err
}
autoChecker, err = newAutoConnectChecker(m.state, m.repo, deviceCtx)
if err != nil {
return nil, err
return nil, nil, err
}

connChecker, err = newConnectChecker(m.state, deviceCtx)
if err != nil {
return nil, err
return nil, nil, err
}

connStateChanged := false
changedConns = make(map[string]*schema.ConnState)

var reloadedConnections []string
ConnsLoop:
Expand All @@ -473,7 +563,7 @@ ConnsLoop:
}
connRef, err := interfaces.ParseConnRef(connId)
if err != nil {
return nil, err
return nil, nil, err
}
// Apply filtering, this allows us to reload only a subset of
// connections (and similarly, refresh the static attributes of only a
Expand All @@ -497,13 +587,14 @@ ConnsLoop:
for _, snapName := range []string{connRef.PlugRef.Snap, connRef.SlotRef.Snap} {
broken, err := isBroken(m.state, snapName)
if err != nil {
return nil, err
return nil, nil, err
}
if broken {
logger.Noticef("Snap %q is broken, ignored by reloadConnections", snapName)
continue ConnsLoop
}
}
changedConns[connId] = cloneConnState(connState)
delete(conns, connId)
connStateChanged = true
}
Expand Down Expand Up @@ -540,11 +631,11 @@ ConnsLoop:

plugAppSet, err := interfaces.NewSnapAppSet(plugInfo.Snap, nil)
if err != nil {
return nil, err
return nil, nil, err
}
slotAppSet, err := interfaces.NewSnapAppSet(slotInfo.Snap, nil)
if err != nil {
return nil, err
return nil, nil, err
}

cplug := interfaces.NewConnectedPlug(plugInfo, plugAppSet, newStaticPlugAttrs, connState.DynamicPlugAttrs)
Expand All @@ -568,6 +659,7 @@ ConnsLoop:
reloadedConnections = append(reloadedConnections, connId)

if updateStaticAttrs {
changedConns[connId] = cloneConnState(connState)
connState.StaticPlugAttrs = staticPlugAttrs
connState.StaticSlotAttrs = staticSlotAttrs
connStateChanged = true
Expand All @@ -578,7 +670,7 @@ ConnsLoop:
setConns(m.state, conns)
}

return reloadedConnections, nil
return reloadedConnections, changedConns, nil
}

// removeConnections disconnects all connections of the snap in the repo. It should only be used if the snap
Expand Down
2 changes: 1 addition & 1 deletion overlord/ifacestate/ifacemgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func (m *InterfaceManager) StartUp() error {
if err := removeStaleConnections(m.state); err != nil {
return err
}
if _, err := m.reloadConnections(""); err != nil {
if _, _, err := m.reloadConnections(""); err != nil {
return err
}

Expand Down
Loading
Loading