Newer
Older
EnvoyControlPlane / internal / snapshot / resource_io.go
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
}