Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
115 changes: 112 additions & 3 deletions internal/overlord/ifacestate/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func (m *InterfaceManager) remountSources(projectId, w, s string) map[string]str
return candidates
}

func (m *InterfaceManager) batchAutoConnectTasks(wp *workshop.Workshop, info *sdk.Info, refs []*interfaces.ConnRef, plugDynamic, slotDynamic map[string]map[string]any) *state.TaskSet {
func (m *InterfaceManager) batchAutoConnectTasks(wp *workshop.Workshop, info *sdk.Info, refs []*interfaces.ConnRef, autoConns map[string]bool, plugDynamic, slotDynamic map[string]map[string]any) *state.TaskSet {

connectTs := state.NewTaskSet()
var affected = map[sdk.Ref]bool{}
Expand All @@ -149,7 +149,7 @@ func (m *InterfaceManager) batchAutoConnectTasks(wp *workshop.Workshop, info *sd

connect.Set("plug", ref.PlugRef)
connect.Set("slot", ref.SlotRef)
connect.Set("auto", true)
connect.Set("auto", autoConns[ref.ID()])
connect.Set("delayed-setup-profile", true)

if plugDynamic != nil {
Expand Down Expand Up @@ -201,11 +201,90 @@ func (m *InterfaceManager) connectAuto(task *state.Task, wp *workshop.Workshop,
return err
}

// Retrieve previously stored connections from the disconnect phase
var prevConns map[string]map[string]*schema.ConnState
chg := task.Change()
if err := chg.Get("prev-conns", &prevConns); err != nil && !errors.Is(err, state.ErrNoState) {
return err
}

var connectRefs = []*interfaces.ConnRef{}
var autoConns = make(map[string]bool)
var wconns = workshopConns(wp)
var plugDynamic = make(map[string]map[string]any)
var slotDynamic = make(map[string]map[string]any)

// Restore previously connected interfaces that were disconnected during refresh
for _, sdkConns := range prevConns {
for connID, connState := range sdkConns {
// Only restore manually created connections (auto=false) that were
// not previously marked as undesired. Auto-connected pairs will be
// re-discovered by the auto-connect logic.
if connState.Auto || connState.Undesired {
continue
}

connRef, err := interfaces.ParseConnRef(connID)
if err != nil {
continue
}

// Skip if connection already exists in conns state
if _, ok := conns[connID]; ok {
continue
}

// Only restore connections for the current SDK
if connRef.PlugRef.Sdk != info.Name && connRef.SlotRef.Sdk != info.Name {
continue
}

// Check if plug and slot still exist in the repository
plug := m.repo.Plug(connRef.PlugRef.ProjectId, connRef.PlugRef.Workshop, connRef.PlugRef.Sdk, connRef.PlugRef.Name)
slot := m.repo.Slot(connRef.SlotRef.ProjectId, connRef.SlotRef.Workshop, connRef.SlotRef.Sdk, connRef.SlotRef.Name)
if plug == nil || slot == nil {
continue
}

// Restore dynamic attributes from remounts for mount interfaces
if connState.Interface == "mount" {
if src, ok := remounts[connID]; ok {
if slotDynamic[connID] == nil {
slotDynamic[connID] = make(map[string]any)
}
slotDynamic[connID]["host-source"] = src
}
}

// Restore dynamic attributes from the previous connection state
if connState.DynamicPlugAttrs != nil {
if plugDynamic[connID] == nil {
plugDynamic[connID] = make(map[string]any)
}
maps.Copy(plugDynamic[connID], connState.DynamicPlugAttrs)
}
if connState.DynamicSlotAttrs != nil {
if slotDynamic[connID] == nil {
slotDynamic[connID] = make(map[string]any)
}
maps.Copy(slotDynamic[connID], connState.DynamicSlotAttrs)
}

connectRefs = append(connectRefs, connRef)
autoConns[connID] = connState.Auto

// Mark this connection as restored so it won't be processed again
conns[connID] = &schema.ConnState{
Auto: connState.Auto,
Interface: connState.Interface,
StaticPlugAttrs: connState.StaticPlugAttrs,
DynamicPlugAttrs: connState.DynamicPlugAttrs,
StaticSlotAttrs: connState.StaticSlotAttrs,
DynamicSlotAttrs: connState.DynamicSlotAttrs,
}
}
}

for _, plug := range info.Plugs {
ref := plug.Ref()
master, slaves := MaybeBound(wp, ref)
Expand Down Expand Up @@ -239,6 +318,7 @@ func (m *InterfaceManager) connectAuto(task *state.Task, wp *workshop.Workshop,
slref := &interfaces.ConnRef{PlugRef: slave, SlotRef: slotRef}
if _, ok := conns[slref.ID()]; !ok {
connectRefs = append(connectRefs, slref)
autoConns[slref.ID()] = true
plugDynamic[slref.ID()] = make(map[string]any)
plugDynamic[slref.ID()]["bind"] = connRef.ID()
}
Expand All @@ -251,6 +331,7 @@ func (m *InterfaceManager) connectAuto(task *state.Task, wp *workshop.Workshop,
continue
}
connectRefs = append(connectRefs, connRef)
autoConns[connRef.ID()] = true
}
}

Expand Down Expand Up @@ -287,6 +368,7 @@ func (m *InterfaceManager) connectAuto(task *state.Task, wp *workshop.Workshop,
slref := &interfaces.ConnRef{PlugRef: slave, SlotRef: slotRef}
if _, ok := conns[slref.ID()]; !ok {
connectRefs = append(connectRefs, slref)
autoConns[slref.ID()] = true
plugDynamic[slref.ID()] = make(map[string]any)
plugDynamic[slref.ID()]["bind"] = connRef.ID()
}
Expand All @@ -299,6 +381,7 @@ func (m *InterfaceManager) connectAuto(task *state.Task, wp *workshop.Workshop,
continue
}
connectRefs = append(connectRefs, connRef)
autoConns[connRef.ID()] = true
}
}

Expand All @@ -312,7 +395,7 @@ func (m *InterfaceManager) connectAuto(task *state.Task, wp *workshop.Workshop,
return a.ID() == b.ID()
})

ts := m.batchAutoConnectTasks(wp, info, connectRefs, plugDynamic, slotDynamic)
ts := m.batchAutoConnectTasks(wp, info, connectRefs, autoConns, plugDynamic, slotDynamic)
handlersetup.InjectTasks(task, ts)
m.state.EnsureBefore(0)
task.SetStatus(state.DoneStatus)
Expand Down Expand Up @@ -391,6 +474,32 @@ func (m *InterfaceManager) doDisconnectInterfaces(task *state.Task, tomb *tomb.T
return err
}

// Store connection states before disconnect so they can be restored during refresh.
conns, err := getConns(st)
if err != nil {
return err
}
var prevConns map[string]*schema.ConnState
for _, cref := range connections {
if connState, ok := conns[cref.ID()]; ok {
if prevConns == nil {
prevConns = make(map[string]*schema.ConnState)
}
prevConns[cref.ID()] = connState
}
}
if prevConns != nil {
chg := task.Change()
var storedConns map[string]map[string]*schema.ConnState
if err = chg.Get("prev-conns", &storedConns); errors.Is(err, state.ErrNoState) {
storedConns = make(map[string]map[string]*schema.ConnState)
} else if err != nil {
return err
}
storedConns[s] = prevConns
chg.Set("prev-conns", storedConns)
}

ts := m.batchDisconnectTasks(*project, w, s, connections)

handlersetup.InjectTasks(task, ts)
Expand Down
120 changes: 120 additions & 0 deletions internal/overlord/ifacestate/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,70 @@ func (s *interfaceHandlersSuite) TestAutoconnectRemountedPlugsOnRefresh(c *check
c.Assert(s.secBackend.RemoveCalls, check.HasLen, 0)
}

func (s *interfaceHandlersSuite) TestAutoconnectRestoresPreviousConnectionsOnRefresh(c *check.C) {
// Setup
repo := s.mgr.Repository()
s.launchWorkshop(c, "ws-producer", []sdk.Meta{producer})
c.Assert(repo.AddSdk(sdk.MockInfo(c, producer.SdkYAML, s.prj.ProjectId, "ws-producer")), check.IsNil)

s.launchWorkshop(c, "ws", []sdk.Meta{consumer})
c.Assert(repo.AddSdk(sdk.MockInfo(c, consumer.SdkYAML, s.prj.ProjectId, "ws")), check.IsNil)

connRefKey := "42424242/ws/consumer:plug 42424242/ws-producer/producer:slot"

// Execute
s.state.Lock()
chg := s.state.NewChange("refresh", "...")
chg.Set("prev-conns", map[string]map[string]*schema.ConnState{
"consumer": {
connRefKey: {
Auto: false,
Interface: "mock-network",
StaticPlugAttrs: map[string]any{"attribute": "one"},
DynamicPlugAttrs: map[string]any{"test-dynamic-attr": "new-dynamic-value"},
},
},
})
t1 := s.state.NewTask("resolve-interfaces", "...")
t2 := s.state.NewTask("auto-connect", "...")
t2.Set("sdk", "consumer")
t2.WaitFor(t1)
setWorkshopProject("ws", s.prj, t1, t2)
chg.Set("user", "testuser")
chg.AddTask(t1)
chg.AddTask(t2)
s.state.Unlock()

s.settle(c)

s.state.Lock()
defer s.state.Unlock()
c.Check(chg.Err(), check.IsNil)

// Validate
ref, err := repo.Connected(s.prj.ProjectId, "ws", "consumer", "plug")
c.Assert(ref, check.HasLen, 1)
c.Assert(err, check.IsNil)

ref, err = repo.Connected(s.prj.ProjectId, "ws-producer", "producer", "slot")
c.Assert(ref, check.HasLen, 1)
c.Assert(err, check.IsNil)

var conns map[string]*schema.ConnState
c.Assert(s.state.Get("conns", &conns), check.IsNil)
c.Assert(conns, check.DeepEquals, map[string]*schema.ConnState{
connRefKey: {
Auto: false,
Interface: "mock-network",
StaticPlugAttrs: map[string]any{"attribute": "one"},
DynamicPlugAttrs: map[string]any{"test-dynamic-attr": "new-dynamic-value"},
},
})

c.Assert(s.secBackend.SetupCalls, check.HasLen, 2)
c.Assert(s.secBackend.RemoveCalls, check.HasLen, 0)
}

func (s *interfaceHandlersSuite) TestUndoAutoConnect(c *check.C) {
// Setup
// Create an already installed workshop with a candidate SDK/slot
Expand Down Expand Up @@ -1159,6 +1223,62 @@ func (s *interfaceHandlersSuite) TestAutoDisconnectSavesRemounts(c *check.C) {
c.Assert(s.secBackend.RemoveCalls, check.HasLen, 1)
}

func (s *interfaceHandlersSuite) TestAutoDisconnectSavesPreviousConnections(c *check.C) {
// Setup
repo := s.mgr.Repository()
s.launchWorkshop(c, "ws-consumer", []sdk.Meta{consumer, producer})
c.Assert(repo.AddSdk(sdk.MockInfo(c, consumer.SdkYAML, s.prj.ProjectId, "ws-consumer")), check.IsNil)
c.Assert(repo.AddSdk(sdk.MockInfo(c, producer.SdkYAML, s.prj.ProjectId, "ws-consumer")), check.IsNil)

connRef := &interfaces.ConnRef{
PlugRef: sdk.PlugRef{ProjectId: "42424242", Workshop: "ws-consumer", Sdk: "consumer", Name: "plug"},
SlotRef: sdk.SlotRef{ProjectId: "42424242", Workshop: "ws-consumer", Sdk: "producer", Name: "slot"},
}

s.state.Lock()
s.state.Set("conns", map[string]any{
connRef.ID(): map[string]any{
"interface": "mock-network",
"auto": false,
"plug-static": map[string]any{"attribute": "one"},
"plug-dynamic": map[string]any{"test-dynamic-attr": "new-dynamic-value"},
},
})
_, err := repo.Connect(connRef, map[string]any{"attribute": "one"}, map[string]any{"test-dynamic-attr": "new-dynamic-value"}, nil, nil, nil)
c.Assert(err, check.IsNil)
_, err = ifacestate.ReloadConnections(s.mgr, "", "", "")
c.Assert(err, check.IsNil)
chg := s.newDisconnectInterfacesChange("consumer")
s.state.Unlock()

s.settle(c)

s.state.Lock()
defer s.state.Unlock()
c.Check(chg.Err(), check.IsNil)

// Validate
conns, err := repo.Connections(s.prj.ProjectId, "ws-consumer", "consumer")
c.Assert(err, check.IsNil)
c.Assert(conns, check.HasLen, 0)

var prevConns map[string]map[string]*schema.ConnState
c.Assert(chg.Get("prev-conns", &prevConns), check.IsNil)
c.Assert(prevConns, check.DeepEquals, map[string]map[string]*schema.ConnState{
"consumer": {
connRef.ID(): {
Auto: false,
Interface: "mock-network",
StaticPlugAttrs: map[string]any{"attribute": "one"},
DynamicPlugAttrs: map[string]any{"test-dynamic-attr": "new-dynamic-value"},
},
},
})

c.Assert(s.secBackend.SetupCalls, check.HasLen, 2)
c.Assert(s.secBackend.RemoveCalls, check.HasLen, 1)
}

func (s *interfaceHandlersSuite) TestAutoDisconnectIgnoresAutoConnections(c *check.C) {
// Setup
// Create an already installed workshop with an auto-connected mount plug
Expand Down
Loading