This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user