Skip to content
Prev Previous commit
Next Next commit
fixup! overlord/ifacestate: use state as source of truth in disconnec…
…t handlers
  • Loading branch information
bboozzoo committed Sep 22, 2026
commit 0030e9a2f2a4dc795e072d7b56f95115e1e1d25e
29 changes: 27 additions & 2 deletions overlord/ifacestate/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,40 @@ func (m *InterfaceManager) setupAffectedSnaps(task *state.Task, affectingSnap st
if !snapst.Active && (snapst.PendingSecurity == nil || snapst.PendingSecurity.SideInfo == nil) {
continue
Comment thread
Copilot marked this conversation as resolved.
}
affectedSnapInfo, err := snapst.CurrentInfo()

var affectedSnapInfo *snap.Info
var err error
if !snapst.Active {
// The snap is inactive, but likely mid-refresh, so use its
// pending security info.
affectedSnapInfo, err = snap.ReadInfo(affectedInstanceName, snapst.PendingSecurity.SideInfo)
} else {
affectedSnapInfo, err = snapst.CurrentInfo()
}
if err != nil {
return err
}
if err := addImplicitInterfaces(st, affectedSnapInfo); err != nil {
return err
}

affectedAppSet, err := appSetForSnapRevision(st, affectedSnapInfo)
var affectedAppSet *interfaces.SnapAppSet
var comps []*snap.ComponentInfo
if !snapst.Active {
for _, csi := range snapst.PendingSecurity.Components {
ci, err := snapstate.ReadComponentInfo(affectedSnapInfo, csi)
if err != nil {
return fmt.Errorf("cannot read component info when building app set %q: %v", affectedInstanceName, err)
}
comps = append(comps, ci)
}
} else {
comps, err = snapst.ComponentInfosForRevision(affectedSnapInfo.Revision)
if err != nil {
return err
}
}
affectedAppSet, err = interfaces.NewSnapAppSet(affectedSnapInfo, comps)
if err != nil {
return fmt.Errorf("building app set for snap %q: %v", affectingSnap, err)
}
Expand Down
79 changes: 79 additions & 0 deletions overlord/ifacestate/ifacestate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6144,6 +6144,85 @@ slots:
c.Check(s.secBackend.SetupCalls[0].AppSet.InstanceName(), Equals, naming.InstanceName("producer"))
}

// TestDoRemoveMidRefreshPeerUsesPendingRevision checks that when an inactive
// peer is processed because it has a real PendingSecurity.SideInfo (e.g.
// mid-refresh), its app set is built from that pinned pending revision, not
// from snapst.CurrentInfo(): Current can still point at the stale,
// about-to-be-replaced revision until link-snap actually runs, since the
// refreshing snap's own setup-profiles(prepare-mode) task pins
// PendingSecurity at the new candidate revision well before that.
func (s *interfaceManagerSuite) TestDoRemoveMidRefreshPeerUsesPendingRevision(c *C) {
s.mockIfaces(&ifacetest.TestInterface{InterfaceName: "test"}, &ifacetest.TestInterface{InterfaceName: "test2"})
var consumerYaml = `
name: consumer
version: 1
plugs:
plug:
interface: test
`
var producerYaml = `
name: producer
version: 1
slots:
slot:
interface: test
`
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
// mock the "candidate" mid-refresh revision 2 of producer, already
// unpacked on disk (as mount-snap/copy-data would have done), appended
// to the Sequence but NOT yet Current.
producerInfo2 := s.mockUpdatedSnap(c, producerYaml, 2)

s.state.Lock()
s.state.Set("conns", map[string]any{
"consumer:plug producer:slot": map[string]any{"interface": "test"},
})
s.state.Unlock()

s.manager(c)

// Simulate producer mid-refresh: Current is still rev1, Active=false
// (unlinked), but its own setup-profiles(prepare-mode) task for the rev2
// candidate already ran and pinned PendingSecurity at rev2.
s.state.Lock()
var snapst snapstate.SnapState
c.Assert(snapstate.Get(s.state, "producer", &snapst), IsNil)
snapst.Active = false
snapst.PendingSecurity = &snapstate.PendingSecurityState{
SideInfo: &producerInfo2.SideInfo,
}
snapstate.Set(s.state, "producer", &snapst)
c.Assert(snapst.Current, Equals, snap.R(1))
s.state.Unlock()

// Now disable/remove consumer.
func() {
s.state.Lock()
defer s.state.Unlock()
var snapst snapstate.SnapState
c.Assert(snapstate.Get(s.state, "consumer", &snapst), IsNil)
snapst.Active = false
snapstate.Set(s.state, "consumer", &snapst)
c.Check(ifacestate.OnSnapLinkageChanged(s.state, &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "consumer"}}), IsNil)
}()

change := s.addRemoveSnapSecurityChange("consumer")
s.se.Ensure()
s.se.Wait()
s.se.Stop()

s.state.Lock()
defer s.state.Unlock()
c.Check(change.Status(), Equals, state.DoneStatus)

// producer's app set must be built from the pending (rev2) revision, not
// the stale Current (rev1) one.
c.Assert(s.secBackend.SetupCalls, HasLen, 1)
c.Check(s.secBackend.SetupCalls[0].AppSet.InstanceName(), Equals, naming.InstanceName("producer"))
c.Check(s.secBackend.SetupCalls[0].AppSet.Info().Revision, Equals, snap.R(2))
}

func (s *interfaceManagerSuite) TestConnectTracksConnectionsInState(c *C) {
s.MockModel(c, nil)

Expand Down