diff --git a/Makefile b/Makefile index 40f503b..50978c9 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ .PHONY: run run: @echo "==> Running $(APP_NAME)..." - @$(GO) run . --port=$${PORT:-18000} --nodeID=test-id + @$(GO) run . --port=$${PORT:-18000} --node-id=test-id ## Run tests .PHONY: test diff --git a/data/config.db b/data/config.db index f4453c4..62493c1 100644 --- a/data/config.db +++ b/data/config.db Binary files differ diff --git a/internal/api.go b/internal/api.go index aefd901..5585567 100644 --- a/internal/api.go +++ b/internal/api.go @@ -33,14 +33,14 @@ // Cluster Handlers mux.HandleFunc("/add-cluster", func(w http.ResponseWriter, r *http.Request) { - api.addResourcesHandler(w, r, resourcev3.ClusterType, func(req interface{}) []types.Resource { + api.addResourcesHandler(w, r, resourcev3.ClusterType, func(req interface{}) ([]types.Resource, bool) { cr := req.(*internalapi.AddClusterRequest) cls, er := snapshot.LoadResourceFromYAML(context.TODO(), cr.YAML, resourcev3.ClusterType) if er != nil { http.Error(w, "failed to load cluster", http.StatusBadRequest) - return nil + return nil, false } - return cls + return cls, cr.Upsert }) }) mux.HandleFunc("/disable-cluster", func(w http.ResponseWriter, r *http.Request) { @@ -55,14 +55,14 @@ // Listener Handlers mux.HandleFunc("/add-listener", func(w http.ResponseWriter, r *http.Request) { - api.addResourcesHandler(w, r, resourcev3.ListenerType, func(req interface{}) []types.Resource { + api.addResourcesHandler(w, r, resourcev3.ListenerType, func(req interface{}) ([]types.Resource, bool) { lr := req.(*internalapi.AddListenerRequest) lss, err := snapshot.LoadResourceFromYAML(context.TODO(), lr.YAML, resourcev3.ListenerType) if err != nil { http.Error(w, "failed to load listener", http.StatusBadRequest) - return nil + return nil, false } - return lss + return lss, lr.Upsert }) }) mux.HandleFunc("/disable-listener", func(w http.ResponseWriter, r *http.Request) { @@ -91,14 +91,14 @@ // Secret Handlers (ADDED) // ------------------------------------------------------------------------- mux.HandleFunc("/add-secret", func(w http.ResponseWriter, r *http.Request) { - api.addResourcesHandler(w, r, resourcev3.SecretType, func(req interface{}) []types.Resource { + api.addResourcesHandler(w, r, resourcev3.SecretType, func(req interface{}) ([]types.Resource, bool) { sr := req.(*internalapi.AddSecretRequest) srs, err := snapshot.LoadResourceFromYAML(context.TODO(), sr.YAML, resourcev3.SecretType) if err != nil { http.Error(w, "failed to load secret", http.StatusBadRequest) - return nil + return nil, false } - return srs + return srs, sr.Upsert }) }) mux.HandleFunc("/disable-secret", func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/types.go b/internal/api/types.go index 435b8b8..efc45a3 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -8,6 +8,8 @@ type AddClusterRequest struct { Name string `json:"name"` YAML string `json:"yaml"` + // If true, performs an 'upsert' (update if exists, insert if new). + Upsert bool `json:"upsert"` } // RemoveClusterRequest defines payload to remove a cluster (Not explicitly used in handlers, but included for completeness) @@ -36,12 +38,16 @@ type AddListenerRequest struct { Name string `json:"name"` YAML string `json:"yaml"` + // If true, performs an 'upsert' (update if exists, insert if new). + Upsert bool `json:"upsert"` } // AddSecretRequest defines payload to add a secret type AddSecretRequest struct { Name string `json:"name"` YAML string `json:"yaml"` + // If true, performs an 'upsert' (update if exists, insert if new). + Upsert bool `json:"upsert"` } // AppendFilterChainRequest defines payload to append a filter chain to a given listener diff --git a/internal/api_handlers.go b/internal/api_handlers.go index 1c730cb..f93157a 100644 --- a/internal/api_handlers.go +++ b/internal/api_handlers.go @@ -132,7 +132,7 @@ // ---------------- 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) { +func (api *API) addResourcesHandler(w http.ResponseWriter, r *http.Request, typ resourcev3.Type, createFn func(interface{}) ([]types.Resource, bool)) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return @@ -160,7 +160,7 @@ return } - resources := createFn(req) + resources, upsert := createFn(req) // Check if any resources were created if len(resources) == 0 { http.Error(w, "create function returned no resources", http.StatusInternalServerError) @@ -171,7 +171,7 @@ addedNames := make([]string, 0, len(resources)) for _, r := range resources { - if err := api.Manager.AddResourceToSnapshot(r, typ); err != nil { + if err := api.Manager.AddResourceToSnapshot(r, typ, upsert); err != nil { http.Error(w, fmt.Sprintf("failed to add resource: %v", err), http.StatusInternalServerError) return } diff --git a/internal/pkg/snapshot/resource_crud.go b/internal/pkg/snapshot/resource_crud.go index 088a54c..6d878b5 100644 --- a/internal/pkg/snapshot/resource_crud.go +++ b/internal/pkg/snapshot/resource_crud.go @@ -13,6 +13,7 @@ "github.com/envoyproxy/go-control-plane/pkg/cache/types" cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + "github.com/go-acme/lego/v4/log" internallog "envoy-control-plane/internal/log" internalcertapi "envoy-control-plane/internal/pkg/cert/api" @@ -313,7 +314,7 @@ // ----------------------------------------------------------------------------- // AddResourceToSnapshot adds any resource to the snapshot dynamically -func (sm *SnapshotManager) AddResourceToSnapshot(resource types.Resource, typ resourcev3.Type) error { +func (sm *SnapshotManager) AddResourceToSnapshot(resource types.Resource, typ resourcev3.Type, upsert bool) error { snap, err := sm.Cache.GetSnapshot(sm.NodeID) if err != nil { return fmt.Errorf("failed to get snapshot from cache: %w", err) @@ -326,10 +327,14 @@ return fmt.Errorf("resource of type %s does not implement GetName()", typ) } - // Check for duplicates before adding + // Check for duplicates before adding. if existingResources, ok := resources[typ]; ok { for _, r := range existingResources { if namer, ok := r.(interface{ GetName() string }); ok && namer.GetName() == resourceNamer.GetName() { + if upsert { + log.Infof("Resource '%s' of type %s already exists. Performing update (upsert).", resourceNamer.GetName(), typ) + continue + } return fmt.Errorf("resource '%s' of type %s already exists in cache", resourceNamer.GetName(), typ) } } @@ -592,7 +597,7 @@ } // 3. Add the resource to the snapshot using the generic function - if err := sm.AddResourceToSnapshot(newSecret, resourcev3.SecretType); err != nil { + if err := sm.AddResourceToSnapshot(newSecret, resourcev3.SecretType, true /*upsert flag*/); err != nil { return fmt.Errorf("failed to add SDS Secret '%s' to snapshot: %w", secretName, err) } diff --git a/static/clusters.js b/static/clusters.js index c34d516..071a556 100644 --- a/static/clusters.js +++ b/static/clusters.js @@ -2,7 +2,7 @@ import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js'; // ========================================================================= -// CLUSTER UTILITIES +// CLUSTER UTILITIES (Unchanged) // ========================================================================= function getClusterEndpointDetails(cluster) { @@ -25,7 +25,7 @@ } // ========================================================================= -// CLUSTER CORE LOGIC +// CLUSTER CORE LOGIC (Unchanged) // ========================================================================= export async function listClusters() { @@ -100,7 +100,7 @@ // ========================================================================= -// CLUSTER ENABLE/DISABLE/REMOVE LOGIC +// CLUSTER ENABLE/DISABLE/REMOVE LOGIC (Unchanged) // ========================================================================= async function toggleClusterStatus(clusterName, action) { @@ -160,6 +160,11 @@ // document.getElementById('add-cluster-modal-title').textContent = // `Add New Cluster`; document.getElementById('add-cluster-yaml-input').value = ''; + // Clear checkbox on show + const upsertCheckbox = document.getElementById('add-cluster-upsert-flag'); + if (upsertCheckbox) { + upsertCheckbox.checked = false; + } document.getElementById('addClusterModal').style.display = 'block'; } @@ -171,15 +176,24 @@ if (modal) { modal.style.display = 'none'; document.getElementById('add-cluster-yaml-input').value = ''; + // Clear checkbox on hide + const upsertCheckbox = document.getElementById('add-cluster-upsert-flag'); + if (upsertCheckbox) { + upsertCheckbox.checked = false; + } } } /** * Submits the new cluster YAML to the /add-cluster endpoint. + * + * MODIFIED: Now checks for an 'allow-upsert' checkbox and adds 'upsert: true' to the payload. */ export async function submitNewCluster() { const yamlInput = document.getElementById('add-cluster-yaml-input'); + // Get the checkbox element and its state + const upsertCheckbox = document.getElementById('add-cluster-upsert-flag'); const clusterYaml = yamlInput.value.trim(); if (!clusterYaml) { @@ -189,6 +203,12 @@ try { const payload = { yaml: clusterYaml }; + + // Add upsert flag to payload if checkbox is checked + if (upsertCheckbox && upsertCheckbox.checked) { + payload.upsert = true; + } + const url = `${API_BASE_URL}/add-cluster`; const response = await fetch(url, { @@ -206,6 +226,10 @@ alert('Cluster successfully added! The dashboard will now refresh.'); yamlInput.value = ''; + // Uncheck the box upon success/closing + if (upsertCheckbox) { + upsertCheckbox.checked = false; + } hideAddClusterModal(); cleanupConfigStore(); diff --git a/static/index.html b/static/index.html index 222baf6..d3eea8e 100644 --- a/static/index.html +++ b/static/index.html @@ -192,7 +192,7 @@ -