omni/internal/backend/grpc/dependency_graph.go
Artem Chernyshev b0f7634310
feat: implement the API for reading resources and their dependency graph
Then another Tool can represent it in a nice way.
This will be a good education tool for the start, and then we can add
monitoring there too.

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
2025-07-21 20:50:47 +03:00

260 lines
6.0 KiB
Go

// Copyright (c) 2025 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//go:build sidero.debug
package grpc
import (
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"reflect"
"slices"
"strings"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/protobuf"
"github.com/cosi-project/runtime/pkg/state"
"github.com/siderolabs/gen/xslices"
"github.com/siderolabs/talos/pkg/machinery/proto"
"github.com/siderolabs/omni/client/api/omni/resources"
"github.com/siderolabs/omni/internal/pkg/auth"
"github.com/siderolabs/omni/internal/pkg/auth/actor"
"github.com/siderolabs/omni/internal/pkg/auth/role"
)
func (s *ResourceServer) Controllers(ctx context.Context, _ *resources.ControllersRequest) (*resources.ControllersResponse, error) {
_, err := auth.CheckGRPC(ctx, auth.WithRole(role.Admin))
if err != nil {
return nil, err
}
graph, err := s.runtime.GetDependencyGraph()
if err != nil {
return nil, err
}
resp := &resources.ControllersResponse{}
visited := map[string]struct{}{}
for _, edge := range graph.Edges {
if _, ok := visited[edge.ControllerName]; ok {
continue
}
resp.Controllers = append(resp.Controllers, edge.ControllerName)
visited[edge.ControllerName] = struct{}{}
}
slices.Sort(resp.Controllers)
return resp, nil
}
func (s *ResourceServer) DependencyGraph(ctx context.Context, req *resources.DependencyGraphRequest) (*resources.DependencyGraphResponse, error) {
_, err := auth.CheckGRPC(ctx, auth.WithRole(role.Admin))
if err != nil {
return nil, err
}
graph, err := s.runtime.GetDependencyGraph()
if err != nil {
return nil, err
}
filterControllers := xslices.ToSet(req.Controllers)
res := &resources.DependencyGraphResponse{}
nodesMap := map[string]string{}
genNodeID := func(label string) {
id := fmt.Sprintf("n_%s", label)
nodesMap[label] = id
}
addNode := func(edge controller.DependencyEdge, t resources.DependencyGraphResponse_Node_Type) error {
var label string
switch t {
case resources.DependencyGraphResponse_Node_RESOURCE:
label = edge.ResourceType
case resources.DependencyGraphResponse_Node_CONTROLLER:
label = edge.ControllerName
case resources.DependencyGraphResponse_Node_UNKNOWN:
return errors.New("unknown node type")
}
id, ok := nodesMap[label]
if !ok {
return nil
}
var (
labels []string
fields []string
)
if t == resources.DependencyGraphResponse_Node_RESOURCE {
ctx := actor.MarkContextAsInternalActor(ctx)
var resources []resource.Resource
if edge.ResourceID != "" {
res, err := s.state.Get(ctx, resource.NewMetadata(edge.ResourceNamespace, edge.ResourceType, edge.ResourceID, resource.VersionUndefined))
if err != nil && !state.IsNotFoundError(err) {
return err
}
if err == nil {
resources = append(resources, res)
}
} else {
response, err := s.state.List(ctx, resource.NewMetadata(edge.ResourceNamespace, edge.ResourceType, "", resource.VersionUndefined))
if err != nil {
return err
}
resources = response.Items
}
labelsMap := map[string]struct{}{}
for _, res := range resources {
for _, key := range res.Metadata().Labels().Keys() {
labelsMap[key] = struct{}{}
}
}
labels = slices.Sorted(maps.Keys(labelsMap))
res, err := protobuf.CreateResource(edge.ResourceType)
if err != nil {
return err
}
if reflect.ValueOf(res.Spec()).Kind() == reflect.Ptr {
err = json.Unmarshal([]byte("{}"), res.Spec())
if err != nil {
return err
}
type protobufWrapper interface {
GetValue() proto.Message
}
if message, ok := res.Spec().(protobufWrapper); ok {
v := reflect.TypeOf(message.GetValue()).Elem()
for i := range v.NumField() {
field := v.Field(i)
f := field.Tag.Get("json")
if f == "" {
continue
}
fields = append(fields, strings.Split(f, ",")[0])
}
}
}
}
res.Nodes = append(res.Nodes, &resources.DependencyGraphResponse_Node{
Id: id,
Label: label,
Type: t,
Labels: labels,
Fields: fields,
})
return nil
}
addEdge := func(edge controller.DependencyEdge) error {
var (
source string
target string
edgeType controller.DependencyEdgeType
)
switch edge.EdgeType {
case controller.EdgeOutputExclusive:
source = nodesMap[edge.ControllerName]
target = nodesMap[edge.ResourceType]
edgeType = edge.EdgeType
case controller.EdgeOutputShared:
source = nodesMap[edge.ControllerName]
target = nodesMap[edge.ResourceType]
edgeType = edge.EdgeType
case controller.EdgeInputStrong, controller.EdgeInputQPrimary:
source = nodesMap[edge.ResourceType]
target = nodesMap[edge.ControllerName]
edgeType = edge.EdgeType
case controller.EdgeInputWeak, controller.EdgeInputQMapped:
source = nodesMap[edge.ResourceType]
target = nodesMap[edge.ControllerName]
edgeType = edge.EdgeType
case controller.EdgeInputDestroyReady, controller.EdgeInputQMappedDestroyReady:
if req.ShowDestroyReady {
source = nodesMap[edge.ResourceType]
target = nodesMap[edge.ControllerName]
edgeType = edge.EdgeType
}
}
if source == "" || target == "" {
return nil
}
res.Edges = append(res.Edges,
&resources.DependencyGraphResponse_Edge{
Id: fmt.Sprintf("e_%s->%s", source, target),
Source: source,
Target: target,
EdgeType: int32(edgeType),
},
)
if err = addNode(edge, resources.DependencyGraphResponse_Node_CONTROLLER); err != nil {
return err
}
if err = addNode(edge, resources.DependencyGraphResponse_Node_RESOURCE); err != nil {
return err
}
return nil
}
for _, edge := range graph.Edges {
if len(filterControllers) != 0 {
if _, ok := filterControllers[edge.ControllerName]; !ok {
continue
}
}
genNodeID(edge.ControllerName)
genNodeID(edge.ResourceType)
}
for _, edge := range graph.Edges {
if err = addEdge(edge); err != nil {
return nil, err
}
}
return res, nil
}