diff --git a/internal/api.go b/internal/api.go index dc7d220..3a68af2 100644 --- a/internal/api.go +++ b/internal/api.go @@ -1,6 +1,7 @@ package internal import ( + "context" "net/http" "github.com/envoyproxy/go-control-plane/pkg/cache/types" @@ -30,7 +31,12 @@ mux.HandleFunc("/add-cluster", func(w http.ResponseWriter, r *http.Request) { api.addResourceHandler(w, r, resourcev3.ClusterType, func(req interface{}) types.Resource { cr := req.(*internalapi.AddClusterRequest) - return &cr.Cluster + cl, er := snapshot.LoadClusterFromYAML(context.TODO(), cr.YAML) + if er != nil { + http.Error(w, "failed to load cluster", http.StatusBadRequest) + return nil + } + return cl }) }) mux.HandleFunc("/disable-cluster", func(w http.ResponseWriter, r *http.Request) { @@ -47,7 +53,12 @@ mux.HandleFunc("/add-listener", func(w http.ResponseWriter, r *http.Request) { api.addResourceHandler(w, r, resourcev3.ListenerType, func(req interface{}) types.Resource { lr := req.(*internalapi.AddListenerRequest) - return &lr.Listener + ls, err := snapshot.LoadListenerFromYAML(context.TODO(), lr.YAML) + if err != nil { + http.Error(w, "failed to load listener", http.StatusBadRequest) + return nil + } + return ls }) }) mux.HandleFunc("/disable-listener", func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/types.go b/internal/api/types.go index ddb1bff..fb68177 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -1,15 +1,13 @@ package internal import ( - clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" - listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" ) // AddClusterRequest defines payload to add a cluster type AddClusterRequest struct { - Name string `json:"name"` - Cluster clusterv3.Cluster `json:"cluster"` + Name string `json:"name"` + YAML string `json:"yaml"` } // RemoveClusterRequest defines payload to remove a cluster (Not explicitly used in handlers, but included for completeness) @@ -36,8 +34,8 @@ // AddListenerRequest defines payload to add a listener type AddListenerRequest struct { - Name string `json:"name"` - Listener listenerv3.Listener `json:"listener"` + Name string `json:"name"` + YAML string `json:"yaml"` } // RemoveListenerRequest defines payload to remove a listener (Not explicitly used in handlers, but included for completeness) diff --git a/internal/api_handlers.go b/internal/api_handlers.go index b33919a..b1b1ddc 100644 --- a/internal/api_handlers.go +++ b/internal/api_handlers.go @@ -12,6 +12,7 @@ "google.golang.org/protobuf/reflect/protoreflect" internalapi "envoy-control-plane/internal/api" + "envoy-control-plane/internal/snapshot" "envoy-control-plane/internal/storage" ) @@ -79,7 +80,7 @@ return } - resources, err := api.Manager.LoadSnapshotFromFile(context.Background(), req.Path) + resources, err := snapshot.LoadSnapshotFromFile(context.Background(), req.Path) if err != nil { http.Error(w, fmt.Sprintf("failed to load snapshot from file: %v", err), http.StatusInternalServerError) return @@ -108,8 +109,12 @@ http.Error(w, "path required in request body", http.StatusBadRequest) return } - - if err := api.Manager.SaveSnapshotToFile(req.Path); err != nil { + snap, err := api.Manager.Cache.GetSnapshot(api.Manager.NodeID) + if err != nil { + http.Error(w, fmt.Sprintf("failed to get snapshot: %v", err), http.StatusInternalServerError) + return + } + if err := snapshot.SaveSnapshotToFile(snap, req.Path); err != nil { http.Error(w, fmt.Sprintf("failed to save snapshot to file: %v", err), http.StatusInternalServerError) return } diff --git a/internal/snapshot/manager.go b/internal/snapshot/manager.go index 6c71a30..e13691c 100644 --- a/internal/snapshot/manager.go +++ b/internal/snapshot/manager.go @@ -28,7 +28,7 @@ type SnapshotManager struct { Cache cachev3.SnapshotCache NodeID string - DB *storage.Storage // Assuming Storage is defined elsewhere (DB persistence layer) + DB *storage.Storage } // NewSnapshotManager creates a new instance of SnapshotManager. diff --git a/internal/snapshot/resource_io.go b/internal/snapshot/resource_io.go index 73e1fef..3e5a090 100644 --- a/internal/snapshot/resource_io.go +++ b/internal/snapshot/resource_io.go @@ -9,6 +9,7 @@ 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" @@ -41,7 +42,7 @@ } // LoadSnapshotFromFile reads a YAML/JSON file, parses it, and returns a map of xDS resources. -func (sm *SnapshotManager) LoadSnapshotFromFile(context context.Context, filePath string) (map[resourcev3.Type][]types.Resource, error) { +func LoadSnapshotFromFile(context context.Context, filePath string) (map[resourcev3.Type][]types.Resource, error) { log := internallog.LogFromContext(context) // Read the file @@ -115,7 +116,7 @@ // LoadFilterChainFromYAML unmarshals a YAML string representing an Envoy Listener FilterChain // configuration into a listenerv3.FilterChain protobuf message using protojson pipeline. -func (sm *SnapshotManager) LoadFilterChainFromYAML(ctx context.Context, yamlStr string) (*listenerv3.FilterChain, error) { +func LoadFilterChainFromYAML(ctx context.Context, yamlStr string) (*listenerv3.FilterChain, error) { log := internallog.LogFromContext(ctx) // 1. Unmarshal YAML into a generic Go map @@ -144,11 +145,7 @@ } // SaveSnapshotToFile marshals the current cache snapshot to JSON and writes it to a file. -func (sm *SnapshotManager) SaveSnapshotToFile(filePath string) error { - snap, err := sm.Cache.GetSnapshot(sm.NodeID) - if err != nil { - return err - } +func SaveSnapshotToFile(snap cache.ResourceSnapshot, filePath string) error { out := make(map[string][]interface{}) @@ -169,3 +166,63 @@ 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 +} diff --git a/internal/snapshot/resource_io_test.go b/internal/snapshot/resource_io_test.go index 829b66b..40f591d 100644 --- a/internal/snapshot/resource_io_test.go +++ b/internal/snapshot/resource_io_test.go @@ -11,7 +11,6 @@ // TestLoadFilterChainFromYAML_ComplexInput tests the functionality of LoadFilterChainFromYAML func TestLoadFilterChainFromYAML_ComplexInput(t *testing.T) { - sm := &SnapshotManager{} ctx := context.Background() // The user's provided, valid YAML for a single FilterChain object @@ -162,7 +161,7 @@ for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - chain, err := sm.LoadFilterChainFromYAML(ctx, tt.yamlStr) + chain, err := LoadFilterChainFromYAML(ctx, tt.yamlStr) if tt.expectError { if err == nil { diff --git a/main.go b/main.go index e50591e..f36a717 100644 --- a/main.go +++ b/main.go @@ -81,7 +81,7 @@ filePath := filepath.Join(dir, fileName) log.Infof(" -> loading config file: %s", filePath) - rf, err := manager.LoadSnapshotFromFile(ctx, filePath) + rf, err := snapshot.LoadSnapshotFromFile(ctx, filePath) if err != nil { return fmt.Errorf("failed to load snapshot from file %s: %w", filePath, err) } @@ -188,7 +188,7 @@ loadedConfigs = true } else if snapshotFile != "" { if _, err := os.Stat(snapshotFile); err == nil { - resources, err := manager.LoadSnapshotFromFile(ctx, snapshotFile) + resources, err := snapshot.LoadSnapshotFromFile(ctx, snapshotFile) if err != nil { log.Errorf("failed to load snapshot from file: %v", err) os.Exit(1)