136 lines
3.0 KiB
Go
136 lines
3.0 KiB
Go
package graph
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// yamlNode is the intermediate YAML representation of a node.
|
|
type yamlNode struct {
|
|
Services map[string]yamlService `yaml:"services"`
|
|
}
|
|
|
|
// yamlService is the intermediate YAML representation of a service.
|
|
type yamlService struct {
|
|
DependsOn yamlDependsOn `yaml:"depends_on"`
|
|
}
|
|
|
|
// yamlDependsOn supports both short form (list of strings) and long form (map with conditions).
|
|
type yamlDependsOn struct {
|
|
simple []string
|
|
detail map[string]yamlDepCondition
|
|
}
|
|
|
|
type yamlDepCondition struct {
|
|
Condition DepCondition `yaml:"condition"`
|
|
}
|
|
|
|
func (d *yamlDependsOn) UnmarshalYAML(value *yaml.Node) error {
|
|
switch value.Kind {
|
|
case yaml.SequenceNode:
|
|
var names []string
|
|
if err := value.Decode(&names); err != nil {
|
|
return err
|
|
}
|
|
d.simple = names
|
|
return nil
|
|
case yaml.MappingNode:
|
|
d.detail = make(map[string]yamlDepCondition)
|
|
if err := value.Decode(&d.detail); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("depends_on must be a list or mapping, got %v", value.Kind)
|
|
}
|
|
}
|
|
|
|
// parseServiceRef parses a reference like "redis" or "infra:redis".
|
|
func parseServiceRef(ref string) ServiceRef {
|
|
parts := strings.SplitN(ref, ":", 2)
|
|
if len(parts) == 2 {
|
|
return ServiceRef{NodeID: parts[0], Name: parts[1]}
|
|
}
|
|
return ServiceRef{Name: parts[0]}
|
|
}
|
|
|
|
// ParseYAML parses a node/service dependency graph from YAML bytes.
|
|
//
|
|
// Example:
|
|
//
|
|
// nodes:
|
|
// server1:
|
|
// services:
|
|
// web:
|
|
// agent_id: agent-1
|
|
// depends_on:
|
|
// - redis
|
|
// - infra:cache
|
|
// api:
|
|
// depends_on:
|
|
// redis:
|
|
// condition: healthy
|
|
// infra:
|
|
// services:
|
|
// cache:
|
|
// db:
|
|
func ParseYAML(data []byte) (*Graph, error) {
|
|
var raw struct {
|
|
Nodes map[string]yamlNode `yaml:"nodes"`
|
|
}
|
|
|
|
if err := yaml.Unmarshal(data, &raw); err != nil {
|
|
return nil, fmt.Errorf("parse yaml: %w", err)
|
|
}
|
|
|
|
g := New()
|
|
|
|
// Phase 1: register all nodes and services
|
|
for nodeID, yn := range raw.Nodes {
|
|
g.AddNode(nodeID)
|
|
for svcName := range yn.Services {
|
|
g.AddService(nodeID, &Service{Name: svcName})
|
|
}
|
|
}
|
|
|
|
// Phase 2: wire dependencies
|
|
for nodeID, yn := range raw.Nodes {
|
|
for svcName, ys := range yn.Services {
|
|
// Short form
|
|
for _, ref := range ys.DependsOn.simple {
|
|
target := parseServiceRef(ref)
|
|
if err := g.AddDependency(nodeID, svcName, Dependency{
|
|
Target: target,
|
|
Condition: Started,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
// Long form
|
|
for ref, cond := range ys.DependsOn.detail {
|
|
target := parseServiceRef(ref)
|
|
if err := g.AddDependency(nodeID, svcName, Dependency{
|
|
Target: target,
|
|
Condition: cond.Condition,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return g, nil
|
|
}
|
|
|
|
// ParseYAMLFile reads and parses from a file.
|
|
func ParseYAMLFile(path string) (*Graph, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ParseYAML(data)
|
|
}
|