Repository: skupperproject/skupper
Pull request #1677 (2024-09-23 20:34:02)

Update nonkube to include router Site config
Exposes router configuration for including a site vanflow record to be emitted from the router in lieu of a dedicated controller to share this information.

c-kruse: There's some work to do yet end-to-end before this is perfect:

Shows a kube site with 3 nonkube sites attached (podman, docker and systemd)
![image](https://github.com/user-attachments/assets/a63eac92-6de6-4c75-8ff4-ac86e4c4048a)

* version field is tbd - also kind of awkward since we usually expect a control plane version there.
* collector api work to add systemd platform
* the "island" site ls1 there has to do with https://github.com/skupperproject/skupper/issues/1676 - where we do not correctly correlate the router with the site record without a consistent SKUPPER_SITE_ID.
* misc frontend enhancements for adding logos and such

Patch:
diff --git a/cmd/network-console-collector/internal/api/types_gen.go b/cmd/network-console-collector/internal/api/types_gen.go
index dc01fe8a..60b931f0 100644
--- a/cmd/network-console-collector/internal/api/types_gen.go
+++ b/cmd/network-console-collector/internal/api/types_gen.go
@@ -474,10 +474,16 @@ type RouterLinkRecord struct {
 	// Cost When connected, cost will be set to the link cost attribute
 	Cost *uint64 `json:"cost"`
 
+	// DestinationRouterId When connected, the identity of the destitation (peer) router.
+	DestinationRouterId *string `json:"destinationRouterId"`
+
+	// DestinationRouterName When connected, the name of the destitation (peer) router.
+	DestinationRouterName *string `json:"destinationRouterName"`
+
 	// DestinationSiteId When connected, the identity of the destitation (peer) site.
 	DestinationSiteId *string `json:"destinationSiteId"`
 
-	// DestinationSiteName When connected, the identity of the destitation (peer) site.
+	// DestinationSiteName When connected, the name of the destitation (peer) site.
 	DestinationSiteName *string `json:"destinationSiteName"`
 
 	// EndTime The end time in microseconds of the record in Unix timestamp format.
@@ -489,14 +495,15 @@ type RouterLinkRecord struct {
 	Octets        uint64 `json:"octets"`
 	OctetsReverse uint64 `json:"octetsReverse"`
 
-	// Peer When connected, peer will be set to the identity of a peer router access
-	Peer *string `json:"peer"`
-
 	// Role The class of skupper link
-	Role           LinkRoleType `json:"role"`
-	RouterId       string       `json:"routerId"`
-	SourceSiteId   string       `json:"sourceSiteId"`
-	SourceSiteName string       `json:"sourceSiteName"`
+	Role LinkRoleType `json:"role"`
+
+	// RouterAccessId When connected, the identity of the destitation (peer) router access.
+	RouterAccessId *string `json:"routerAccessId"`
+	RouterId       string  `json:"routerId"`
+	RouterName     string  `json:"routerName"`
+	SourceSiteId   string  `json:"sourceSiteId"`
+	SourceSiteName string  `json:"sourceSiteName"`
 
 	// StartTime The creation time in microseconds of the record in Unix timestamp format. The value 0 means that the record is not terminated
 	StartTime uint64         `json:"startTime"`
diff --git a/cmd/network-console-collector/internal/collector/connections.go b/cmd/network-console-collector/internal/collector/connections.go
index 80ae2ae3..691e99cc 100644
--- a/cmd/network-console-collector/internal/collector/connections.go
+++ b/cmd/network-console-collector/internal/collector/connections.go
@@ -580,38 +580,6 @@ func (c *connectionManager) run(ctx context.Context) {
 						continue
 					}
 
-					sn := c.graph.Process(pair.Source)
-					sSiteID := sn.Parent().ID()
-					dn := c.graph.Process(pair.Dest)
-					dSiteID := dn.Parent().ID()
-
-					sourceProc, ok := sn.GetRecord()
-					if !ok {
-						return
-					}
-					destProc, ok := dn.GetRecord()
-					if !ok {
-						return
-					}
-
-					var (
-						sourceGroupID string
-						destGroupID   string
-					)
-					sGroup, dGroup := dref(sourceProc.Group), dref(destProc.Group)
-					sGroupEntries := c.records.Index(IndexByTypeName, store.Entry{Record: ProcessGroupRecord{
-						Name: sGroup,
-					}})
-					if len(sGroupEntries) > 0 {
-						sourceGroupID = sGroupEntries[0].Record.Identity()
-					}
-					dGroupEntries := c.records.Index(IndexByTypeName, store.Entry{Record: ProcessGroupRecord{
-						Name: dGroup,
-					}})
-					if len(dGroupEntries) > 0 {
-						destGroupID = dGroupEntries[0].Record.Identity()
-					}
-
 					id := c.idp.ID("processpair", pair.Source, pair.Dest, pair.Protocol)
 					if _, ok := c.records.Get(id); !ok {
 						record := ProcPairRecord{
@@ -624,37 +592,6 @@ func (c *connectionManager) run(ctx context.Context) {
 						c.logger.Info("Adding process pairs", slog.Any("id", id))
 						c.records.Add(record, store.SourceRef{ID: "self"})
 					}
-
-					if sSiteID != "" && dSiteID != "" {
-						id := c.idp.ID("sitepair", sSiteID, dSiteID, pair.Protocol)
-						if _, ok := c.records.Get(id); !ok {
-							record := SitePairRecord{
-								ID:       id,
-								Source:   sSiteID,
-								Dest:     dSiteID,
-								Protocol: pair.Protocol,
-								Start:    time.Now(),
-							}
-							c.logger.Info("Adding site pairs", slog.Any("id", id))
-							c.records.Add(record, store.SourceRef{ID: "self"})
-						}
-					}
-
-					if sourceGroupID != "" && destGroupID != "" {
-						id := c.idp.ID("processgrouppair", sourceGroupID, destGroupID, pair.Protocol)
-						if _, ok := c.records.Get(id); !ok {
-							record := ProcGroupPairRecord{
-								ID:       id,
-								Source:   sourceGroupID,
-								Dest:     destGroupID,
-								Protocol: pair.Protocol,
-								Start:    time.Now(),
-							}
-							c.logger.Info("Adding process group pairs", slog.Any("id", id))
-							c.records.Add(record, store.SourceRef{ID: "self"})
-						}
-					}
-
 					c.processPairs[pair] = false
 				}
 			}()
diff --git a/cmd/network-console-collector/internal/collector/pairs.go b/cmd/network-console-collector/internal/collector/pairs.go
index ef12db50..e8c0a9cb 100644
--- a/cmd/network-console-collector/internal/collector/pairs.go
+++ b/cmd/network-console-collector/internal/collector/pairs.go
@@ -38,14 +38,29 @@ func newPairManager(logger *slog.Logger, stor store.Interface, graph *graph) *pa
 
 func (m *pairManager) run(ctx context.Context) func() error {
 	return func() error {
+		refresh := time.NewTicker(time.Second * 20)
+		defer refresh.Stop()
 		for {
 			select {
 			case <-ctx.Done():
 				return nil
+			case <-refresh.C:
+				go func() {
+					processPairs := m.stor.Index(store.TypeIndex, store.Entry{Record: ProcPairRecord{}})
+					for _, entry := range processPairs {
+						if pair, ok := entry.Record.(ProcPairRecord); ok {
+							m.updatePairs <- pair
+						}
+					}
+				}()
 			case pair := <-m.updatePairs:
 				func() {
 					m.mu.Lock()
 					defer m.mu.Unlock()
+					var (
+						hasSitePair  bool
+						hasGroupPair bool
+					)
 					if _, ok := m.pairs[pair.ID]; ok {
 						return
 					}
@@ -55,8 +70,9 @@ func (m *pairManager) run(ctx context.Context) func() error {
 
 					// add site and group pairs
 					if sourceID, destID := sourceN.ID(), destN.ID(); sourceID != "" && destID != "" {
-						spid := m.idp.ID("sp", sourceID, destID, pair.Protocol)
-						m.stor.Add(SitePairRecord{
+						hasSitePair = true
+						spid := m.idp.ID("sitepair", sourceID, destID, pair.Protocol)
+						added := m.stor.Add(SitePairRecord{
 							ID:       spid,
 							Start:    time.Now(),
 							Protocol: pair.Protocol,
@@ -65,34 +81,52 @@ func (m *pairManager) run(ctx context.Context) func() error {
 						},
 							m.source,
 						)
+						if added {
+							m.logger.Info("Added site pair", slog.String("id", spid), slog.String("source", sourceID), slog.String("dest", destID))
+						}
 					}
 
-					pgIDByProcess := func(node Process) (string, bool) {
+					pgIDByProcess := func(node Process) (string, string, bool) {
 						record, ok := node.GetRecord()
 						if !ok {
-							return "", false
+							return "", "", false
 						}
-						groups := m.stor.Index(IndexByTypeName, store.Entry{Record: ProcessGroupRecord{Name: dref(record.Name)}})
+						groups := m.stor.Index(IndexByTypeName, store.Entry{Record: ProcessGroupRecord{Name: dref(record.Group)}})
 						if len(groups) > 0 {
-							return groups[0].Record.Identity(), true
+							if pg, ok := groups[0].Record.(ProcessGroupRecord); ok {
+								return pg.ID, pg.Name, true
+							}
 						}
-						return "", false
+						return "", "", false
 					}
-					sourcePG, sourceFound := pgIDByProcess(m.graph.Process(pair.Source))
-					destPG, destFound := pgIDByProcess(m.graph.Process(pair.Dest))
+					sourcePG, sourcePGName, sourceFound := pgIDByProcess(m.graph.Process(pair.Source))
+					destPG, destPGName, destFound := pgIDByProcess(m.graph.Process(pair.Dest))
 					if sourceFound && destFound {
-						pgpid := m.idp.ID("sp", sourcePG, destPG, pair.Protocol)
-						m.stor.Add(SitePairRecord{
-							ID:       pgpid,
-							Start:    time.Now(),
-							Protocol: pair.Protocol,
-							Source:   sourcePG,
-							Dest:     destPG,
+						hasGroupPair = true
+						pgpid := m.idp.ID("grouppair", sourcePG, destPG, pair.Protocol)
+						added := m.stor.Add(ProcGroupPairRecord{
+							ID:         pgpid,
+							Start:      time.Now(),
+							Protocol:   pair.Protocol,
+							Source:     sourcePG,
+							SourceName: sourcePGName,
+							Dest:       destPG,
+							DestName:   destPGName,
 						},
 							m.source,
 						)
+						if added {
+							m.logger.Info(
+								"Added process group pair",
+								slog.String("id", pgpid),
+								slog.String("source", sourcePG),
+								slog.String("dest", destPG))
+						}
 
 					}
+					if hasSitePair && hasGroupPair {
+						m.pairs[pair.ID] = struct{}{}
+					}
 				}()
 			}
 		}
diff --git a/cmd/network-console-collector/internal/collector/records.go b/cmd/network-console-collector/internal/collector/records.go
index 9f6399b7..380f4c3b 100644
--- a/cmd/network-console-collector/internal/collector/records.go
+++ b/cmd/network-console-collector/internal/collector/records.go
@@ -62,11 +62,13 @@ func (r SitePairRecord) GetTypeMeta() vanflow.TypeMeta {
 }
 
 type ProcGroupPairRecord struct {
-	ID       string
-	Protocol string
-	Source   string
-	Dest     string
-	Start    time.Time
+	ID         string
+	Protocol   string
+	Source     string
+	SourceName string
+	Dest       string
+	DestName   string
+	Start      time.Time
 }
 
 func (r ProcGroupPairRecord) Identity() string {
diff --git a/cmd/network-console-collector/internal/server/query.go b/cmd/network-console-collector/internal/server/query.go
index a5477378..8ae7f309 100644
--- a/cmd/network-console-collector/internal/server/query.go
+++ b/cmd/network-console-collector/internal/server/query.go
@@ -159,10 +159,9 @@ func (m fieldIndex[T]) Compare(x, y T) int {
 		}
 		vx, vy = vx.Elem(), vy.Elem()
 	}
-	xx, yy := vx.Interface(), vy.Interface()
-	switch xx := xx.(type) {
-	case string:
-		yy := yy.(string)
+	switch vx.Kind() {
+	case reflect.String:
+		xx, yy := vx.String(), vy.String()
 		if xx == yy {
 			return 0
 		}
@@ -170,14 +169,18 @@ func (m fieldIndex[T]) Compare(x, y T) int {
 			return -1
 		}
 		return 1
-	case uint64:
-		return int(xx - yy.(uint64))
-	case int32:
-		return int(xx - yy.(int32))
-	case int64:
-		return int(xx - yy.(int64))
-	case int:
-		return int(xx - yy.(int))
+	case reflect.Uint32:
+		fallthrough
+	case reflect.Uint64:
+		xx, yy := vx.Uint(), vy.Uint()
+		return int(xx - yy)
+	case reflect.Int32:
+		fallthrough
+	case reflect.Int:
+		fallthrough
+	case reflect.Int64:
+		xx, yy := vx.Int(), vy.Int()
+		return int(xx - yy)
 	}
 	return 0
 }
diff --git a/cmd/network-console-collector/internal/server/views/views.go b/cmd/network-console-collector/internal/server/views/views.go
index dcc45742..06aa3a75 100644
--- a/cmd/network-console-collector/internal/server/views/views.go
+++ b/cmd/network-console-collector/internal/server/views/views.go
@@ -105,7 +105,9 @@ func NewProcessGroupPairProvider() func(collector.ProcGroupPairRecord) api.FlowA
 		out.StartTime = uint64(record.Start.UnixMicro())
 		out.PairType = api.PROCESSGROUP
 		out.SourceId = record.Source
+		out.SourceName = record.SourceName
 		out.DestinationId = record.Dest
+		out.DestinationName = record.DestName
 		out.Protocol = record.Protocol
 		return out
 	}
@@ -394,14 +396,21 @@ func NewRouterLinkProvider(graph collector.Graph) func(vanflow.LinkRecord) (api.
 		if link.Parent == nil {
 			return out, false
 		}
-		siteNode := graph.Link(link.ID).Parent().Parent()
+		routerNode := graph.Link(link.ID).Parent()
+		sourceRouter, found := routerNode.GetRecord()
+		if !found {
+			return out, false
+		}
+		siteNode := routerNode.Parent()
 		sourceSite, found := siteNode.GetRecord()
 		if !found {
 			return out, false
 		}
+
+		out.RouterId = sourceRouter.ID
 		out.SourceSiteId = sourceSite.ID
+		setOpt(&out.RouterName, sourceRouter.Name)
 		setOpt(&out.SourceSiteName, sourceSite.Name)
-		setOpt(&out.RouterId, link.Parent)
 		setOpt(&out.Name, link.Name)
 
 		if link.Role != nil {
@@ -420,11 +429,20 @@ func NewRouterLinkProvider(graph collector.Graph) func(vanflow.LinkRecord) (api.
 		if link.Peer == nil {
 			return out, true
 		}
-		out.Peer = link.Peer
+		out.RouterAccessId = link.Peer
 		out.Cost = link.LinkCost
 
 		raN := graph.RouterAccess(*link.Peer)
-		raSiteN := raN.Parent().Parent()
+		raRouterN := raN.Parent()
+		raSiteN := raRouterN.Parent()
+		if destRouter, found := raRouterN.GetRecord(); found {
+			out.DestinationRouterId = &destRouter.ID
+			if destRouter.Name == nil {
+				tmp := unknownStr
+				destRouter.Name = &tmp
+			}
+			out.DestinationRouterName = destRouter.Name
+		}
 		if destSite, found := raSiteN.GetRecord(); found {
 			out.DestinationSiteId = &destSite.ID
 			if destSite.Name == nil {
@@ -445,6 +463,7 @@ func defaultRouterLink(id string) api.RouterLinkRecord {
 		Name:           unknownStr,
 		Role:           api.LinkRoleTypeUnknown,
 		RouterId:       unknownStr,
+		RouterName:     unknownStr,
 		SourceSiteId:   unknownStr,
 		SourceSiteName: unknownStr,
 		Status:         api.Down,
@@ -553,6 +572,7 @@ func Router(record vanflow.RouterRecord) api.RouterRecord {
 	out := defaultRouter(record.ID)
 	out.StartTime, out.EndTime = vanflowTimes(record.BaseRecord)
 	out.Namespace = record.Namespace
+	setOpt(&out.Parent, record.Parent)
 	setOpt(&out.HostName, record.Hostname)
 	setOpt(&out.ImageName, record.ImageName)
 	setOpt(&out.ImageVersion, record.ImageVersion)
diff --git a/cmd/network-console-collector/spec/openapi.yaml b/cmd/network-console-collector/spec/openapi.yaml
index fa8463cb..b6ca594c 100644
--- a/cmd/network-console-collector/spec/openapi.yaml
+++ b/cmd/network-console-collector/spec/openapi.yaml
@@ -1138,20 +1138,25 @@ components:
         - type: object
           required:
             - routerId
+            - routerName
             - name
             - cost
             - role
-            - peer
             - status
             - octets
             - octetsReverse
             - sourceSiteId
-            - destinationSiteId
             - sourceSiteName
+            - destinationSiteId
             - destinationSiteName
+            - routerAccessId
+            - destinationRouterId
+            - destinationRouterName
           properties:
             routerId:
               type: string
+            routerName:
+              type: string
             name:
               type: string
             cost:
@@ -1161,10 +1166,6 @@ components:
               description: When connected, cost will be set to the link cost attribute
             role:
               $ref: '#/components/schemas/linkRoleType'
-            peer:
-              type: string
-              nullable: true
-              description: When connected, peer will be set to the identity of a peer router access
             status:
               $ref: '#/components/schemas/operStatusType'
             octets:
@@ -1173,6 +1174,18 @@ components:
             octetsReverse:
               type: integer
               format: uint64
+            routerAccessId:
+              type: string
+              nullable: true
+              description: When connected, the identity of the destitation (peer) router access.
+            destinationRouterId:
+              type: string
+              nullable: true
+              description: When connected, the identity of the destitation (peer) router.
+            destinationRouterName:
+              type: string
+              nullable: true
+              description: When connected, the name of the destitation (peer) router.
             sourceSiteId:
               type: string
             sourceSiteName:
@@ -1184,7 +1197,7 @@ components:
             destinationSiteName:
               type: string
               nullable: true
-              description: When connected, the identity of the destitation (peer) site.
+              description: When connected, the name of the destitation (peer) site.
     RouterAccessRecord:
       allOf:
         - $ref: '#/components/schemas/baseRecord'
diff --git a/internal/nonkube/client/compat/container.go b/internal/nonkube/client/compat/container.go
index 5f967850..6ec7cc64 100644
--- a/internal/nonkube/client/compat/container.go
+++ b/internal/nonkube/client/compat/container.go
@@ -8,6 +8,7 @@ import (
 	"os/user"
 	"strconv"
 	"strings"
+	"syscall"
 	"time"
 
 	"github.com/go-openapi/runtime"
@@ -243,13 +244,10 @@ func (c *CompatClient) ContainerCreate(container *container.Container) error {
 }
 
 func (c *CompatClient) ToSpecGenerator(newContainer *container.Container) *models.CreateContainerConfig {
-	curUser, err := user.Current()
+	uid := syscall.Getuid()
 	var userNs models.UsernsMode
-
-	if err == nil {
-		if curUser.Uid != "0" {
-			userNs = "keep-id"
-		}
+	if uid != 0 {
+		userNs = "keep-id"
 	}
 	spec := &models.CreateContainerConfig{
 		HostConfig: &models.HostConfig{
diff --git a/pkg/nonkube/api/site_state.go b/pkg/nonkube/api/site_state.go
index 386a00af..33712fca 100644
--- a/pkg/nonkube/api/site_state.go
+++ b/pkg/nonkube/api/site_state.go
@@ -254,7 +254,7 @@ func (s *SiteState) bindings() *site.Bindings {
 	return b
 }
 
-func (s *SiteState) ToRouterConfig(sslProfileBasePath string) qdr.RouterConfig {
+func (s *SiteState) ToRouterConfig(sslProfileBasePath string, platform string) qdr.RouterConfig {
 	if s.SiteId == "" {
 		s.SiteId = uuid.New().String()
 	}
@@ -263,6 +263,12 @@ func (s *SiteState) ToRouterConfig(sslProfileBasePath string) qdr.RouterConfig {
 		routerName = fmt.Sprintf("%s-%d", s.Site.Name, time.Now().Unix())
 	}
 	routerConfig := qdr.InitialConfig(routerName, s.SiteId, version.Version, !s.IsInterior(), 3)
+	routerConfig.SiteConfig = &qdr.SiteConfig{
+		Name:      routerName,
+		Namespace: s.GetNamespace(),
+		Platform:  platform,
+	}
+
 	// override metadata
 	if s.bundle {
 		routerConfig.Metadata.Id += "-{{.SiteNameSuffix}}"
@@ -273,6 +279,8 @@ func (s *SiteState) ToRouterConfig(sslProfileBasePath string) qdr.RouterConfig {
 		}
 		metadataJson, _ := encodingjson.Marshal(metadata)
 		routerConfig.Metadata.Metadata = string(metadataJson)
+		routerConfig.SiteConfig.Namespace = "{{.Namespace}}"
+		routerConfig.SiteConfig.Platform = "{{.Platform}}"
 	}
 	// LinkAccess
 	s.linkAccessMap().DesiredConfig(nil, path.Join(sslProfileBasePath, "certificates/server")).Apply(&routerConfig)
diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go
index 109fb345..7b2ad3f4 100644
--- a/pkg/nonkube/common/fs_config_renderer.go
+++ b/pkg/nonkube/common/fs_config_renderer.go
@@ -265,7 +265,7 @@ func (c *FileSystemConfigurationRenderer) createTokens(siteState *api.SiteState)
 }
 
 func (c *FileSystemConfigurationRenderer) createRouterConfig(siteState *api.SiteState) error {
-	c.RouterConfig = siteState.ToRouterConfig(c.SslProfileBasePath)
+	c.RouterConfig = siteState.ToRouterConfig(c.SslProfileBasePath, c.Platform)
 
 	// Saving router config
 	routerConfigJson, err := qdr.MarshalRouterConfig(c.RouterConfig)
diff --git a/pkg/qdr/qdr.go b/pkg/qdr/qdr.go
index 7b0ad4d5..cd1446de 100644
--- a/pkg/qdr/qdr.go
+++ b/pkg/qdr/qdr.go
@@ -22,6 +22,7 @@ type RouterConfig struct {
 	Connectors  map[string]Connector
 	Addresses   map[string]Address
 	LogConfig   map[string]LogConfig
+	SiteConfig  *SiteConfig
 	Bridges     BridgeConfig
 }
 
@@ -587,6 +588,14 @@ type HttpEndpoint struct {
 	VerifyHostname  *bool  `json:"verifyHostname,omitempty"`
 }
 
+type SiteConfig struct {
+	Name      string `json:"name,omitempty"`
+	Location  string `json:"location,omitempty"`
+	Provider  string `json:"provider,omitempty"`
+	Platform  string `json:"platform,omitempty"`
+	Namespace string `json:"namespace,omitempty"`
+}
+
 func convert(from interface{}, to interface{}) error {
 	data, err := json.Marshal(from)
 	if err != nil {
@@ -687,6 +696,13 @@ func UnmarshalRouterConfig(config string) (RouterConfig, error) {
 				return result, fmt.Errorf("Invalid %s element got %#v", entityType, element[1])
 			}
 			result.LogConfig[logConfig.Module] = logConfig
+		case "site":
+			siteConfig := &SiteConfig{}
+			err = convert(element[1], siteConfig)
+			if err != nil {
+				return result, fmt.Errorf("Invalid %s element got %#v", entityType, element[1])
+			}
+			result.SiteConfig = siteConfig
 		case "tcpConnector":
 			connector := TcpEndpoint{}
 			err = convert(element[1], &connector)
@@ -791,6 +807,13 @@ func MarshalRouterConfig(config RouterConfig) (string, error) {
 		}
 		elements = append(elements, tuple)
 	}
+	if config.SiteConfig != nil {
+		tuple := []interface{}{
+			"site",
+			*config.SiteConfig,
+		}
+		elements = append(elements, tuple)
+	}
 	data, err := json.MarshalIndent(elements, "", "    ")
 	if err != nil {
 		return "", err


Test patch:
diff --git a/cmd/network-console-collector/internal/server/links_test.go b/cmd/network-console-collector/internal/server/links_test.go
index c799265c..ee44ff01 100644
--- a/cmd/network-console-collector/internal/server/links_test.go
+++ b/cmd/network-console-collector/internal/server/links_test.go
@@ -57,6 +57,7 @@ func TestRouterlinks(t *testing.T) {
 						Name:       ptrTo("linkb2"),
 						Status:     ptrTo("up"),
 						Peer:       ptrTo("routeraccess-a-1"),
+						LinkCost:   ptrTo(uint64(3)),
 					},
 				)...,
 			),
@@ -69,16 +70,20 @@ func TestRouterlinks(t *testing.T) {
 				assert.Equal(t, string(link1.Status), "down")
 				assert.Equal(t, link2.SourceSiteName, "site b")
 				assert.DeepEqual(t, link2, api.RouterLinkRecord{
-					Identity:            "link2",
-					Status:              api.Up,
-					Name:                "linkb2",
-					Role:                "unknown",
-					RouterId:            "router-b-2",
-					SourceSiteId:        "site-b",
-					SourceSiteName:      "site b",
-					Peer:                ptrTo("routeraccess-a-1"),
-					DestinationSiteId:   ptrTo("site-a"),
-					DestinationSiteName: ptrTo("site a"),
+					Identity:              "link2",
+					Cost:                  ptrTo(uint64(3)),
+					Status:                api.Up,
+					Name:                  "linkb2",
+					Role:                  "unknown",
+					RouterId:              "router-b-2",
+					RouterName:            "router b.2",
+					SourceSiteId:          "site-b",
+					SourceSiteName:        "site b",
+					RouterAccessId:        ptrTo("routeraccess-a-1"),
+					DestinationSiteId:     ptrTo("site-a"),
+					DestinationSiteName:   ptrTo("site a"),
+					DestinationRouterId:   ptrTo("router-a-1"),
+					DestinationRouterName: ptrTo("router a.1"),
 				})
 			},
 		},
diff --git a/cmd/network-console-collector/internal/server/process_test.go b/cmd/network-console-collector/internal/server/process_test.go
index b480b18d..4f243891 100644
--- a/cmd/network-console-collector/internal/server/process_test.go
+++ b/cmd/network-console-collector/internal/server/process_test.go
@@ -75,6 +75,17 @@ func TestProcesses(t *testing.T) {
 					SourceHost:     "10.99.99.2",
 				})
 			},
+		}, {
+			Records:     append(exProcessWithAddresses(), wrapRecords(vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("0")})...),
+			ExpectOK:    true,
+			ExpectCount: 2,
+			Parameters: map[string][]string{
+				"sortBy": {"processBinding.asc"},
+			},
+			ExpectResults: func(t *testing.T, results []api.ProcessRecord) {
+				assert.Equal(t, results[0].Identity, "1")
+				assert.Equal(t, results[1].Identity, "0")
+			},
 		}, {
 			Records: exProcessWithAddresses(),
 			Parameters: map[string][]string{
diff --git a/cmd/network-console-collector/internal/server/router_test.go b/cmd/network-console-collector/internal/server/router_test.go
new file mode 100644
index 00000000..8473ef0e
--- /dev/null
+++ b/cmd/network-console-collector/internal/server/router_test.go
@@ -0,0 +1,94 @@
+package server
+
+import (
+	"context"
+	"log/slog"
+	"testing"
+
+	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/api"
+	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/collector"
+	"github.com/skupperproject/skupper/pkg/vanflow"
+	"github.com/skupperproject/skupper/pkg/vanflow/store"
+	"gotest.tools/assert"
+)
+
+func TestRouters(t *testing.T) {
+	tlog := slog.Default()
+	stor := store.NewSyncMapStore(store.SyncMapStoreConfig{Indexers: collector.RecordIndexers()})
+	graph := collector.NewGraph(stor)
+	srv, c := requireTestClient(t, New(tlog, stor, graph))
+	defer srv.Close()
+
+	van := []vanflow.Record{
+		vanflow.SiteRecord{BaseRecord: vanflow.NewBase("site-a"), Name: ptrTo("site a")},
+		vanflow.RouterRecord{BaseRecord: vanflow.NewBase("router-a-1"), Name: ptrTo("router a.1"), Parent: ptrTo("site-a"),
+			Mode: ptrTo("inter-router"),
+		},
+
+		vanflow.SiteRecord{BaseRecord: vanflow.NewBase("site-b"), Name: ptrTo("site b")},
+		vanflow.RouterRecord{BaseRecord: vanflow.NewBase("router-b-1"), Name: ptrTo("router b.1"), Parent: ptrTo("site-b")},
+		vanflow.RouterRecord{BaseRecord: vanflow.NewBase("router-b-2"), Name: ptrTo("router b.2"), Parent: ptrTo("site-b")},
+	}
+
+	testcases := []struct {
+		Records              []store.Entry
+		Parameters           map[string][]string
+		ExpectOK             bool
+		ExpectCount          int
+		ExpectTimeRangeCount int
+		ExpectResults        func(t *testing.T, results []api.RouterRecord)
+	}{
+		{ExpectOK: true},
+		{
+			ExpectOK:    true,
+			Records:     wrapRecords(van...),
+			ExpectCount: 3,
+			ExpectResults: func(t *testing.T, results []api.RouterRecord) {
+				for _, result := range results {
+					switch result.Identity {
+					case "router-a-1":
+						assert.DeepEqual(
+							t, result, api.RouterRecord{
+								Identity:     "router-a-1",
+								Name:         "router a.1",
+								HostName:     "unknown",
+								ImageName:    "unknown",
+								ImageVersion: "unknown",
+								Mode:         "inter-router",
+								Parent:       "site-a",
+							},
+						)
+
+					}
+
+				}
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run("", func(t *testing.T) {
+			stor.Replace(tc.Records)
+			graph.(reset).Reset()
+			for _, r := range tc.Records {
+				graph.(reset).Reindex(r.Record)
+			}
+			resp, err := c.RoutersWithResponse(context.TODO(), WithParameters(tc.Parameters))
+			assert.Check(t, err)
+			if tc.ExpectOK {
+				assert.Equal(t, resp.StatusCode(), 200)
+				assert.Equal(t, resp.JSON200.Count, int64(tc.ExpectCount))
+				assert.Assert(t, resp.JSON200.Results != nil)
+				assert.Equal(t, len(resp.JSON200.Results), tc.ExpectCount)
+				if tc.ExpectTimeRangeCount != 0 {
+					assert.Equal(t, resp.JSON200.TimeRangeCount, int64(tc.ExpectTimeRangeCount))
+				}
+				if tc.ExpectResults != nil {
+					tc.ExpectResults(t, resp.JSON200.Results)
+				}
+			} else {
+				assert.Check(t, resp.JSON400 != nil)
+			}
+		})
+	}
+}
diff --git a/pkg/nonkube/api/site_state_test.go b/pkg/nonkube/api/site_state_test.go
index e0b5d7a4..6bf8571c 100644
--- a/pkg/nonkube/api/site_state_test.go
+++ b/pkg/nonkube/api/site_state_test.go
@@ -94,7 +94,7 @@ func TestSiteState_ToRouterConfig(t *testing.T) {
 			ss := fakeSiteState()
 			ss.bundle = test.bundle
 			sslProfileBasePath := "${SSL_PROFILE_BASE_PATH}"
-			routerConfig := ss.ToRouterConfig(sslProfileBasePath)
+			routerConfig := ss.ToRouterConfig(sslProfileBasePath, "podman")
 			if test.bundle {
 				assert.Assert(t, strings.HasSuffix(routerConfig.Metadata.Id, "-{{.SiteNameSuffix}}"))
 				assert.Assert(t, strings.Contains(routerConfig.Metadata.Metadata, `"id":"{{.SiteId}}"`), routerConfig.Metadata.Metadata)
@@ -116,6 +116,16 @@ func TestSiteState_ToRouterConfig(t *testing.T) {
 			assert.Assert(t, strings.HasPrefix(routerConfig.SslProfiles["local-access-one"].CaCertFile, sslProfileBasePath))
 			assert.Equal(t, len(routerConfig.Bridges.TcpListeners), 2)
 			assert.Equal(t, len(routerConfig.Bridges.TcpConnectors), 1)
+			assert.Assert(t, routerConfig.SiteConfig != nil)
+			expectedPlatform := "podman"
+			expectedNamespace := "default"
+			if test.bundle {
+				expectedPlatform = "{{.Platform}}"
+				expectedNamespace = "{{.Namespace}}"
+			}
+			assert.Equal(t, routerConfig.SiteConfig.Platform, expectedPlatform)
+			assert.Equal(t, routerConfig.SiteConfig.Namespace, expectedNamespace)
+
 		})
 	}
 }
diff --git a/pkg/qdr/qdr_test.go b/pkg/qdr/qdr_test.go
index d790501b..a958a9f4 100644
--- a/pkg/qdr/qdr_test.go
+++ b/pkg/qdr/qdr_test.go
@@ -1,6 +1,7 @@
 package qdr
 
 import (
+	"encoding/json"
 	"reflect"
 	"testing"
 
@@ -328,6 +329,12 @@ func TestMarshalUnmarshalRouterConfig(t *testing.T) {
 				Distribution: "balanced",
 			},
 		},
+		SiteConfig: &SiteConfig{
+			Name:      "razzle",
+			Namespace: "dazzle",
+			Location:  "pizzazz",
+			Provider:  "azure",
+		},
 	}
 	data, err := MarshalRouterConfig(input)
 	if err != nil {
@@ -355,6 +362,9 @@ func TestMarshalUnmarshalRouterConfig(t *testing.T) {
 	if !reflect.DeepEqual(input.Bridges, output.Bridges) {
 		t.Errorf("Incorrect bridges. Expected %#v got %#v", input.Bridges, output.Bridges)
 	}
+	if !reflect.DeepEqual(input.SiteConfig, output.SiteConfig) {
+		t.Errorf("Incorrect siteconfig. Expected %#v got %#v", input.SiteConfig, output.SiteConfig)
+	}
 }
 
 func TestUnmarshalErrorInvalidJson(t *testing.T) {
@@ -677,3 +687,31 @@ func TestGetSslProfilesDifference(t *testing.T) {
 	assert.Assert(t, utils.StringSlicesEqual(deletedSslProfiles, expectedDeletedSslProfiles), "Expected %v but got %v", expectedDeletedSslProfiles, deletedSslProfiles)
 
 }
+
+func TestSiteConfig(t *testing.T) {
+	config := InitialConfig("foo", "bar", "1.2.3", true, 10)
+	if config.SiteConfig != nil {
+		t.Error("expected no site configuration by default")
+	}
+
+	data, err := MarshalRouterConfig(config)
+	if err != nil {
+		t.Fatalf("Failed to marshal: %v", err)
+	}
+
+	var entities [][]json.RawMessage
+	if err := json.Unmarshal([]byte(data), &entities); err != nil {
+		t.Fatalf("Failed to unmarshal: %v", err)
+	}
+	for _, entity := range entities {
+		if len(entity) > 1 {
+			var entityType string
+			if err := json.Unmarshal(entity[0], &entityType); err != nil {
+				t.Fatalf("Failed to unmarshal entity type: %v", err)
+			}
+			if entityType == "site" {
+				t.Errorf("expected no site config entity but got %q", string(entity[1]))
+			}
+		}
+	}
+}