package internal
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/envoyproxy/go-control-plane/pkg/cache/types"
resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
internalapi "envoy-control-plane/internal/api"
"envoy-control-plane/internal/snapshot"
"envoy-control-plane/internal/storage"
)
// ---------------- Persistence Handlers ----------------
// loadSnapshotFromDB loads the full configuration from the persistent database
// into the SnapshotManager's Envoy Cache.
func (api *API) loadSnapshotFromDB(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
// Use context.Background() since this is a top-level operation
if err := api.Manager.LoadSnapshotFromDB(context.Background()); err != nil {
http.Error(w, fmt.Sprintf("failed to load from DB: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "message": "Configuration loaded from DB and applied to cache."})
}
// flushCacheToDB saves the current configuration from the Envoy Cache (source of truth)
// to the persistent database.
func (api *API) flushCacheToDB(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
// Default to DeleteLogical (no physical deletion)
deleteStrategy := storage.DeleteLogical // DeleteLogical is assumed to be defined elsewhere
// Check for 'deleteMissing' query parameter. If "true" or "1", switch to DeleteActual.
deleteMissingStr := r.URL.Query().Get("deleteMissing")
if deleteMissingStr == "true" || deleteMissingStr == "1" {
deleteStrategy = storage.DeleteActual // DeleteActual is assumed to be defined elsewhere
}
// Use context.Background() since this is a top-level operation
// Pass the determined DeleteStrategy
if err := api.Manager.FlushCacheToDB(context.Background(), deleteStrategy); err != nil {
http.Error(w, fmt.Sprintf("failed to flush to DB: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "message": "Configuration saved from cache to DB."})
}
// loadSnapshotFromFile loads a snapshot from a local file and applies it to the cache.
func (api *API) loadSnapshotFromFile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
var req internalapi.SnapshotFileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Path == "" {
http.Error(w, "path required in request body", http.StatusBadRequest)
return
}
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
}
// Use context.Background()
if err := api.Manager.SetSnapshot(context.Background(), req.Path, resources); err != nil {
http.Error(w, fmt.Sprintf("failed to set snapshot: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "message": fmt.Sprintf("Snapshot loaded from %s and applied.", req.Path)})
}
// saveSnapshotToFile saves the current cache snapshot to a local file.
func (api *API) saveSnapshotToFile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
var req internalapi.SnapshotFileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Path == "" {
http.Error(w, "path required in request body", http.StatusBadRequest)
return
}
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
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "message": fmt.Sprintf("Snapshot saved to %s.", req.Path)})
}
// ---------------- Generic REST Handlers ----------------
// ---------------- Generic REST Handlers ----------------
// addResourceHandler handles adding a resource (Cluster, Listener) to the cache and persisting it.
func (api *API) addResourcesHandler(w http.ResponseWriter, r *http.Request, typ resourcev3.Type, createFn func(interface{}) []types.Resource) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
var req interface{}
switch typ {
case resourcev3.ClusterType:
req = &internalapi.AddClusterRequest{}
// case resourcev3.RouteType:
// req = &AddRouteRequest{}
case resourcev3.ListenerType:
req = &internalapi.AddListenerRequest{}
default:
http.Error(w, "unsupported type", http.StatusBadRequest)
return
}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
resources := createFn(req)
// Check if any resources were created
if len(resources) == 0 {
http.Error(w, "create function returned no resources", http.StatusInternalServerError)
return
}
// --- FIX: Initialize array to store names ---
addedNames := make([]string, 0, len(resources))
for _, r := range resources {
if err := api.Manager.AddResourceToSnapshot(r, typ); err != nil {
http.Error(w, fmt.Sprintf("failed to add resource: %v", err), http.StatusInternalServerError)
return
}
// --- FIX: Collect name during iteration ---
if nameable, ok := r.(interface{ GetName() string }); ok {
addedNames = append(addedNames, nameable.GetName())
}
}
// Persist immediately using DeleteLogical (mark as disabled in DB)
if err := api.Manager.FlushCacheToDB(context.Background(), storage.DeleteLogical); err != nil {
http.Error(w, fmt.Sprintf("failed to persist resource to DB: %v", err), http.StatusInternalServerError)
return
}
// --- FIX: Encode the array of names in the response ---
w.WriteHeader(http.StatusCreated)
response := map[string]interface{}{
"status": "created",
"names": addedNames,
}
// Fallback if no names were collected (e.g., resource type doesn't implement GetName())
if len(addedNames) == 0 {
response["names"] = fmt.Sprintf("failed to collect names for %d resources", len(resources))
}
json.NewEncoder(w).Encode(response)
}
// disableResourceHandler handles disabling a resource (logical removal from cache/DB).
func (api *API) disableResourceHandler(w http.ResponseWriter, r *http.Request, typ resourcev3.Type) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
var req struct{ Name string }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
http.Error(w, "name required", http.StatusBadRequest)
return
}
// Use DeleteLogical to remove from cache and mark as disabled in DB
if err := api.Manager.RemoveResource(req.Name, typ, storage.DeleteLogical); err != nil {
http.Error(w, fmt.Sprintf("failed to disable resource: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "message": fmt.Sprintf("Resource '%s' disabled.", req.Name)})
}
// enableResourceHandler fetches a disabled resource from the DB and enables it (adds to cache).
func (api *API) enableResourceHandler(w http.ResponseWriter, r *http.Request, typ resourcev3.Type) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
var req internalapi.EnableResourceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
http.Error(w, "name required", http.StatusBadRequest)
return
}
// Call the Manager function to pull the resource from DB, enable it, and add to the cache.
if err := api.Manager.EnableResourceFromDB(req.Name, typ); err != nil {
// NOTE: Exact error string check is brittle but necessary to replicate original logic's status code.
if err.Error() == fmt.Sprintf("disabled resource %s not found in DB for type %s", req.Name, typ) {
http.Error(w, fmt.Sprintf("disabled resource '%s' not found or already enabled: %v", req.Name, err), http.StatusNotFound)
} else {
http.Error(w, fmt.Sprintf("failed to enable resource '%s' from DB: %v", req.Name, err), http.StatusInternalServerError)
}
return
}
// Reload the cache again from DB to ensure consistency.
if err := api.Manager.LoadSnapshotFromDB(context.Background()); err != nil {
http.Error(w, fmt.Sprintf("failed to reload snapshot from DB after enabling resource: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "message": fmt.Sprintf("Resource '%s' enabled and applied to cache.", req.Name)})
}
// removeResourceHandler removes a resource completely from the DB (DeleteActual).
// It requires the resource to be disabled (not in the cache) before actual deletion.
func (api *API) removeResourceHandler(w http.ResponseWriter, r *http.Request, typ resourcev3.Type) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
var req internalapi.RemoveResourceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
http.Error(w, "name required", http.StatusBadRequest)
return
}
// Use DeleteActual strategy for permanent removal from the DB.
if err := api.Manager.RemoveResource(req.Name, typ, storage.DeleteActual); err != nil {
// NOTE: Exact error string check is brittle but necessary to replicate original logic's status code.
if err.Error() == fmt.Sprintf("resource %s for type %s is enabled and must be disabled before removal", req.Name, typ) {
http.Error(w, fmt.Sprintf("resource '%s' must be disabled first before permanent removal: %v", req.Name, err), http.StatusBadRequest)
} else {
http.Error(w, fmt.Sprintf("failed to permanently remove resource: %v", err), http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "message": fmt.Sprintf("Resource '%s' permanently removed.", req.Name)})
}
// appendFilterChainHandler defines the append filter handler.
func (api *API) appendFilterChainHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
var req internalapi.AppendFilterChainRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.ListenerName == "" || req.YAML == "" {
http.Error(w, "listener name and YAML required", http.StatusBadRequest)
return
}
ctx := context.Background()
chain, err := snapshot.LoadFilterChainFromYAML(ctx, req.YAML)
if err != nil {
http.Error(w, "failed to load filter chain", http.StatusBadRequest)
return
}
if err := api.Manager.AppendFilterChainToListener(ctx, req.ListenerName, chain); err != nil {
http.Error(w, fmt.Sprintf("failed to append filter chain: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (api *API) updateFilterChainHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req internalapi.UpdateFilterChainRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.ListenerName == "" || req.YAML == "" {
http.Error(w, "listener name and YAML required", http.StatusBadRequest)
return
}
ctx := context.Background()
chain, err := snapshot.LoadFilterChainFromYAML(ctx, req.YAML)
if err != nil {
http.Error(w, "failed to load filter chain", http.StatusBadRequest)
return
}
if err := api.Manager.UpdateFilterChainOfListener(ctx, req.ListenerName, chain); err != nil {
http.Error(w, fmt.Sprintf("failed to update filter chain: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (api *API) removeFilterChainHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req internalapi.RemoveFilterChainRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.ListenerName == "" {
http.Error(w, "listener name required", http.StatusBadRequest)
return
}
ctx := context.Background()
if err := api.Manager.RemoveFilterChainFromListener(ctx, req.ListenerName, req.Domains); err != nil {
http.Error(w, fmt.Sprintf("failed to remove filter chain: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
// ---------------- Query / List Handlers ----------------
// listResourceHandler returns a list of enabled and disabled resources of a given type.
func (api *API) listResourceHandler(w http.ResponseWriter, r *http.Request, typ resourcev3.Type) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
enabledResources, disabledResources, err := api.Manager.ListResources(typ)
if err != nil {
http.Error(w, fmt.Sprintf("failed to list resources: %v", err), http.StatusInternalServerError)
return
}
// Create the final response object structure
response := struct {
Enabled []types.Resource `json:"enabled"`
Disabled []types.Resource `json:"disabled"`
}{
Enabled: enabledResources,
Disabled: disabledResources,
}
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "failed to encode response", http.StatusInternalServerError)
}
}
// getResourceHandler returns a single resource by name, allowing for different output formats.
func (api *API) getResourceHandler(w http.ResponseWriter, r *http.Request, typ resourcev3.Type) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
name := r.URL.Query().Get("name")
if name == "" {
http.Error(w, "name query parameter required", http.StatusBadRequest)
return
}
format := r.URL.Query().Get("format")
if format == "" {
format = "json" // default format
}
res, err := api.Manager.GetResourceFromCache(name, typ)
if err != nil {
http.Error(w, fmt.Sprintf("resource not found: %v", err), http.StatusNotFound)
return
}
pb, ok := res.(interface{ ProtoReflect() protoreflect.Message })
if !ok {
http.Error(w, "resource is not a protobuf message", http.StatusInternalServerError)
return
}
var output []byte
switch format {
case "yaml":
// ConvertProtoToYAML is assumed to be defined elsewhere
yamlStr, err := ConvertProtoToYAML(pb)
if err != nil {
http.Error(w, fmt.Sprintf("failed to convert resource to YAML: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/x-yaml")
output = []byte(yamlStr)
default: // json
data, err := protojson.Marshal(pb)
if err != nil {
http.Error(w, fmt.Sprintf("failed to marshal protobuf to JSON: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
output = data
}
w.WriteHeader(http.StatusOK)
w.Write(output)
}
// ---------------- Consistency Handler ----------------
// isConsistentHandler checks whether the current in-memory cache is consistent with the database.
func (api *API) isConsistentHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
// ConsistencyReport is assumed to be defined elsewhere
consistent, err := api.Manager.CheckCacheDBConsistency(context.TODO())
if err != nil {
http.Error(w, fmt.Sprintf("failed to check consistency: %v", err), http.StatusInternalServerError)
return
}
response := struct {
Consistent *internalapi.ConsistencyReport `json:"consistent"` // ConsistencyReport is assumed to be defined elsewhere
}{
Consistent: consistent,
}
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "failed to encode response", http.StatusInternalServerError)
}
}