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/envoy/extensions/filters/network/http_connection_manager/v3"
_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/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
}
// LoadAndUnmarshal takes raw data (e.g., from a file or string) and unmarshals
// it into the target interface using YAML/JSON rules.
func LoadAndUnmarshal(data []byte, target interface{}) error {
if err := yaml.Unmarshal(data, target); err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}
return nil
}
// ProcessFn is a function type that defines the processing logic to be applied
// to a potential resource node found during the walk.
// It returns a non-nil error if processing fails and should stop the walk.
type ProcessFn func(ctx context.Context, node map[string]interface{}) error
// WalkAndProcess traverses an arbitrary Go data structure (unmarshaled from YAML/JSON)
// and applies the provided ProcessFn to every map[string]interface{} node it finds.
func WalkAndProcess(ctx context.Context, raw interface{}, processFn ProcessFn) error {
var walk func(node interface{}) error
walk = func(node interface{}) error {
switch v := node.(type) {
case map[string]interface{}:
// Apply the custom processing function to the current map node
if err := processFn(ctx, v); err != nil {
return err
}
// 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
}
return walk(raw)
}
// makeResourceProcessor creates the specific ProcessFn needed for the xDS resource logic.
func makeResourceProcessor(
log internallog.Logger,
resources map[resourcev3.Type][]types.Resource,
) ProcessFn {
return func(_ context.Context, v map[string]interface{}) error {
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 {
// NOTE: unmarshalYamlNodeToProto must be available in this scope
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)
}
}
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 (Step 1: Read)
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
// Unmarshal data (Step 2: Generic Unmarshal)
var raw interface{}
if err := LoadAndUnmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("failed to unmarshal file %s: %w", filePath, err)
}
resources := make(map[resourcev3.Type][]types.Resource)
processor := makeResourceProcessor(log, resources)
// Walk and Process (Step 3: Generic Walk with specific processor)
if err := WalkAndProcess(context, raw, processor); err != nil {
return nil, err
}
return resources, 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)
}
// 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
}
// LoadResourceFromYAML unmarshals a YAML string representing a single xDS resource
// (like a Cluster or a Listener) into the appropriate Protobuf message.
//
// It leverages the existing WalkAndProcess logic to find and unmarshal the resource.
func LoadResourceFromYAML(ctx context.Context, yamlStr string, expectedType resourcev3.Type) ([]types.Resource, error) {
log := internallog.LogFromContext(ctx)
// 1. Unmarshal YAML into a generic Go map
var raw interface{}
if err := LoadAndUnmarshal([]byte(yamlStr), &raw); err != nil {
return nil, fmt.Errorf("failed to unmarshal YAML into generic structure: %w", err)
}
// This map will hold the one resource we expect to find.
resources := make(map[resourcev3.Type][]types.Resource)
// 2. Use the standard resource processor to walk the structure.
processor := makeResourceProcessor(log, resources)
// 3. Walk and Process
if err := WalkAndProcess(ctx, raw, processor); err != nil {
return nil, fmt.Errorf("failed to walk and process YAML: %w", err)
}
// 4. Validate and return the resource
resourceList, ok := resources[expectedType]
if !ok || len(resourceList) == 0 {
// Only return an error if the expected type was not found.
return nil, fmt.Errorf("no resource of expected type %s found in YAML", expectedType)
}
if len(resourceList) > 1 {
// Warn if multiple resources were found, but return the first one as expected.
log.Warnf("found %d resources of type %s, expected 1; returning the first", len(resourceList), expectedType)
}
return resourceList, nil
}