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 }