package snapshot
import (
"context"
"encoding/json"
"fmt"
"os"
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
"github.com/envoyproxy/go-control-plane/pkg/cache/types"
"github.com/envoyproxy/go-control-plane/pkg/cache/v3"
resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
"google.golang.org/protobuf/encoding/protojson"
yaml "gopkg.in/yaml.v3"
internallog "envoy-control-plane/internal/log"
)
// YamlResources is a helper struct to unmarshal the common Envoy YAML file structure
type YamlResources struct {
Resources []yaml.Node `yaml:"resources"`
}
// unmarshalYamlNodeToProto takes a generic map representation of a YAML/JSON object
// and unmarshals it into the given Protobuf resource pointer using protojson.
func unmarshalYamlNodeToProto(node map[string]interface{}, resource types.Resource) error {
// 1. Remove the standard Protobuf type marker (if present) before marshaling to JSON.
delete(node, "@type")
// 2. Marshal the generic map into JSON bytes.
jsonBytes, err := json.Marshal(node)
if err != nil {
return fmt.Errorf("failed to marshal resource node to JSON: %w", err)
}
// 3. Unmarshal the JSON bytes into the target Protobuf struct.
if err := protojson.Unmarshal(jsonBytes, resource); err != nil {
return fmt.Errorf("failed to unmarshal into proto: %w", err)
}
return nil
}
// LoadSnapshotFromFile reads a YAML/JSON file, parses it, and returns a map of xDS resources.
func LoadSnapshotFromFile(context context.Context, filePath string) (map[resourcev3.Type][]types.Resource, error) {
log := internallog.LogFromContext(context)
// Read the file
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
var raw interface{}
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("failed to unmarshal YAML/JSON file %s: %w", filePath, err)
}
resources := make(map[resourcev3.Type][]types.Resource)
var walk func(node interface{}) error
walk = func(node interface{}) error {
switch v := node.(type) {
case map[string]interface{}:
if typStr, ok := v["@type"].(string); ok {
typ := resourcev3.Type(typStr)
// only process known top-level xDS resources
var resource types.Resource
var newResource bool
switch typ {
case resourcev3.ClusterType:
resource = &clusterv3.Cluster{}
newResource = true
case resourcev3.ListenerType:
resource = &listenerv3.Listener{}
newResource = true
// ... other types ...
default:
log.Warnf("unsupported resource type: %s", typ)
// Skip nested or unsupported types
}
if newResource {
if err := unmarshalYamlNodeToProto(v, resource); err != nil {
return fmt.Errorf("failed to unmarshal %s from file: %w", typ, err)
}
resources[typ] = append(resources[typ], resource)
}
}
// recurse into children
for _, child := range v {
if err := walk(child); err != nil {
return err
}
}
case []interface{}:
for _, item := range v {
if err := walk(item); err != nil {
return err
}
}
}
return nil
}
if err := walk(raw); err != nil {
return nil, err
}
return resources, nil
}
// LoadFilterChainFromYAML unmarshals a YAML string representing an Envoy Listener FilterChain
// configuration into a listenerv3.FilterChain protobuf message using protojson pipeline.
func LoadFilterChainFromYAML(ctx context.Context, yamlStr string) (*listenerv3.FilterChain, error) {
log := internallog.LogFromContext(ctx)
// 1. Unmarshal YAML into a generic Go map
var rawChainMap map[string]interface{}
if err := yaml.Unmarshal([]byte(yamlStr), &rawChainMap); err != nil {
log.Errorf("Failed to unmarshal YAML: %v", err)
return nil, fmt.Errorf("failed to unmarshal YAML into generic map: %w", err)
}
if rawChainMap == nil {
return nil, fmt.Errorf("failed to unmarshal YAML: input was empty or invalid")
}
// 2. Unmarshal the generic map into the Protobuf struct using the helper
rawChain := &listenerv3.FilterChain{}
if err := unmarshalYamlNodeToProto(rawChainMap, rawChain); err != nil {
return nil, fmt.Errorf("failed to unmarshal YAML into FilterChain using protojson: %w", err)
}
// Check if the FilterChain contains any filters (optional but good sanity check)
if len(rawChain.Filters) == 0 {
return nil, fmt.Errorf("filter chain loaded but contains no network filters")
}
// Return the single FilterChain object.
return rawChain, nil
}
// SaveSnapshotToFile marshals the current cache snapshot to JSON and writes it to a file.
func SaveSnapshotToFile(snap cache.ResourceSnapshot, filePath string) error {
out := make(map[string][]interface{})
// Iterate over all known types
clusterTypeResources := snap.GetResources(resourcev3.ClusterType)
for _, r := range clusterTypeResources {
out[resourcev3.ClusterType] = append(out[resourcev3.ClusterType], r)
}
listenerTypeResources := snap.GetResources(resourcev3.ListenerType)
for _, r := range listenerTypeResources {
out[resourcev3.ListenerType] = append(out[resourcev3.ListenerType], r)
}
data, err := json.MarshalIndent(out, "", " ")
if err != nil {
return err
}
return os.WriteFile(filePath, data, 0644)
}
// The package imports and existing helper functions (unmarshalYamlNodeToProto, LoadSnapshotFromFile, etc.) are assumed to be present.
// LoadClusterFromYAML unmarshals a YAML string representing an Envoy Cluster
// configuration into a clusterv3.Cluster protobuf message.
func LoadClusterFromYAML(ctx context.Context, yamlStr string) (*clusterv3.Cluster, error) {
log := internallog.LogFromContext(ctx)
// 1. Unmarshal YAML into a generic Go map
var rawClusterMap map[string]interface{}
if err := yaml.Unmarshal([]byte(yamlStr), &rawClusterMap); err != nil {
log.Errorf("Failed to unmarshal YAML for Cluster: %v", err)
return nil, fmt.Errorf("failed to unmarshal YAML into generic map for Cluster: %w", err)
}
if rawClusterMap == nil {
return nil, fmt.Errorf("failed to unmarshal YAML: input for Cluster was empty or invalid")
}
// 2. Unmarshal the generic map into the Protobuf struct using the helper
cluster := &clusterv3.Cluster{}
if err := unmarshalYamlNodeToProto(rawClusterMap, cluster); err != nil {
return nil, fmt.Errorf("failed to unmarshal YAML into Cluster using protojson: %w", err)
}
// Sanity check: ensure the cluster has a name
if cluster.Name == "" {
return nil, fmt.Errorf("cluster loaded but has an empty name")
}
return cluster, nil
}
// LoadListenerFromYAML unmarshals a YAML string representing an Envoy Listener
// configuration into a listenerv3.Listener protobuf message.
func LoadListenerFromYAML(ctx context.Context, yamlStr string) (*listenerv3.Listener, error) {
log := internallog.LogFromContext(ctx)
// 1. Unmarshal YAML into a generic Go map
var rawListenerMap map[string]interface{}
if err := yaml.Unmarshal([]byte(yamlStr), &rawListenerMap); err != nil {
log.Errorf("Failed to unmarshal YAML for Listener: %v", err)
return nil, fmt.Errorf("failed to unmarshal YAML into generic map for Listener: %w", err)
}
if rawListenerMap == nil {
return nil, fmt.Errorf("failed to unmarshal YAML: input for Listener was empty or invalid")
}
// 2. Unmarshal the generic map into the Protobuf struct using the helper
listener := &listenerv3.Listener{}
if err := unmarshalYamlNodeToProto(rawListenerMap, listener); err != nil {
return nil, fmt.Errorf("failed to unmarshal YAML into Listener using protojson: %w", err)
}
// Sanity check: ensure the listener has a name
if listener.Name == "" {
return nil, fmt.Errorf("listener loaded but has an empty name")
}
return listener, nil
}