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 }