diff --git a/internal/api.go b/internal/api.go index 54697f8..f066cc3 100644 --- a/internal/api.go +++ b/internal/api.go @@ -29,7 +29,9 @@ // RegisterRoutes mounts REST handlers func (api *API) RegisterRoutes(mux *http.ServeMux) { - // Management Handlers (Add / Remove / Enable / Disable) + // ------------------------------------------------------------------------- + // Management Handlers (xDS Resource CRUD) + // ------------------------------------------------------------------------- // Cluster Handlers mux.HandleFunc("/add-cluster", func(w http.ResponseWriter, r *http.Request) { @@ -75,21 +77,7 @@ api.removeResourceHandler(w, r, resourcev3.ListenerType) }) - mux.HandleFunc("/append-filter-chain", func(w http.ResponseWriter, r *http.Request) { - api.appendFilterChainHandler(w, r) - }) - - mux.HandleFunc("/update-filter-chain", func(w http.ResponseWriter, r *http.Request) { - api.updateFilterChainHandler(w, r) - }) - - mux.HandleFunc("/remove-filter-chain", func(w http.ResponseWriter, r *http.Request) { - api.removeFilterChainHandler(w, r) - }) - - // ------------------------------------------------------------------------- - // Secret Handlers (ADDED) - // ------------------------------------------------------------------------- + // Secret Handlers mux.HandleFunc("/add-secret", func(w http.ResponseWriter, r *http.Request) { api.addResourcesHandler(w, r, resourcev3.SecretType, func(req interface{}) ([]types.Resource, bool) { sr := req.(*internalapi.AddSecretRequest) @@ -110,14 +98,64 @@ mux.HandleFunc("/remove-secret", func(w http.ResponseWriter, r *http.Request) { api.removeResourceHandler(w, r, resourcev3.SecretType) }) - mux.HandleFunc("/list-secrets", func(w http.ResponseWriter, r *http.Request) { - api.listResourceHandler(w, r, resourcev3.SecretType) - }) - mux.HandleFunc("/get-secret", func(w http.ResponseWriter, r *http.Request) { - api.getResourceHandler(w, r, resourcev3.SecretType) + + // ------------------------------------------------------------------------- + // ExtensionConfig Handlers (ADDED) + // ------------------------------------------------------------------------- + extConfigType := resourcev3.ExtensionConfigType // Use the defined resource type for consistency + + // Create / Update + mux.HandleFunc("/add-extensionconfig", func(w http.ResponseWriter, r *http.Request) { + api.addResourcesHandler(w, r, extConfigType, func(req interface{}) ([]types.Resource, bool) { + er := req.(*internalapi.AddExtensionConfigRequest) // Assuming this struct exists + ecs, err := snapshot.LoadResourceFromYAML(context.TODO(), er.YAML, extConfigType) + if err != nil { + http.Error(w, "failed to load extension config", http.StatusBadRequest) + return nil, false + } + return ecs, er.Upsert + }) }) - // Query / List Handlers + // Read / Query + mux.HandleFunc("/list-extensionconfigs", func(w http.ResponseWriter, r *http.Request) { + api.listResourceHandler(w, r, extConfigType) + }) + mux.HandleFunc("/get-extensionconfig", func(w http.ResponseWriter, r *http.Request) { + api.getResourceHandler(w, r, extConfigType) + }) + + // Update / Disable + mux.HandleFunc("/disable-extensionconfig", func(w http.ResponseWriter, r *http.Request) { + api.disableResourceHandler(w, r, extConfigType) + }) + mux.HandleFunc("/enable-extensionconfig", func(w http.ResponseWriter, r *http.Request) { + api.enableResourceHandler(w, r, extConfigType) + }) + + // Delete + mux.HandleFunc("/remove-extensionconfig", func(w http.ResponseWriter, r *http.Request) { + api.removeResourceHandler(w, r, extConfigType) + }) + + // ------------------------------------------------------------------------- + // Listener Filter Chain Handlers + // ------------------------------------------------------------------------- + mux.HandleFunc("/append-filter-chain", func(w http.ResponseWriter, r *http.Request) { + api.appendFilterChainHandler(w, r) + }) + + mux.HandleFunc("/update-filter-chain", func(w http.ResponseWriter, r *http.Request) { + api.updateFilterChainHandler(w, r) + }) + + mux.HandleFunc("/remove-filter-chain", func(w http.ResponseWriter, r *http.Request) { + api.removeFilterChainHandler(w, r) + }) + + // ------------------------------------------------------------------------- + // Query / List Handlers (consolidated READ operations) + // ------------------------------------------------------------------------- mux.HandleFunc("/list-clusters", func(w http.ResponseWriter, r *http.Request) { api.listResourceHandler(w, r, resourcev3.ClusterType) }) @@ -132,16 +170,24 @@ api.getResourceHandler(w, r, resourcev3.ListenerType) }) + mux.HandleFunc("/list-secrets", func(w http.ResponseWriter, r *http.Request) { + api.listResourceHandler(w, r, resourcev3.SecretType) + }) + mux.HandleFunc("/get-secret", func(w http.ResponseWriter, r *http.Request) { + api.getResourceHandler(w, r, resourcev3.SecretType) + }) + + // ------------------------------------------------------------------------- // Persistence Handlers + // ------------------------------------------------------------------------- mux.HandleFunc("/load-from-db", api.loadSnapshotFromDB) mux.HandleFunc("/flush-to-db", api.flushCacheToDB) mux.HandleFunc("/load-from-file", api.loadSnapshotFromFile) mux.HandleFunc("/save-to-file", api.saveSnapshotToFile) - // Consistency Handler - mux.HandleFunc("/is-consistent", api.isConsistentHandler) - - // Issuing Certificate Handler + // ------------------------------------------------------------------------- + // Certificate Handlers + // ------------------------------------------------------------------------- mux.HandleFunc("/issue-certificate", api.issueCertificateHandler) mux.HandleFunc("/parse-certificate", api.parseCertificateHandler) mux.HandleFunc("/check-certificate-validity", api.checkCertificateValidityHandler) @@ -155,5 +201,11 @@ mux.HandleFunc("/disable-certificate-rotation", api.disableCertificateRotationHandler) mux.HandleFunc("/list-rotating-certificates", api.listRotatingCertificatesHandler) + // ------------------------------------------------------------------------- + // Utility Handlers + // ------------------------------------------------------------------------- + // Consistency Handler + mux.HandleFunc("/is-consistent", api.isConsistentHandler) + mux.HandleFunc("/storage-dump", api.storageDumpHandler) } diff --git a/internal/api/types.go b/internal/api/types.go index daa893d..fcc2a8a 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -4,126 +4,154 @@ resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" ) -// AddClusterRequest defines payload to add a cluster +// --- xDS Resource Management Requests (CRUD) --- + +// AddClusterRequest defines the payload to add or update an Envoy Cluster resource. type AddClusterRequest struct { - Name string `json:"name"` - YAML string `json:"yaml"` + Name string `json:"name"` // The name of the Cluster resource. + YAML string `json:"yaml"` // The YAML/JSON string containing the Envoy Cluster configuration. // 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) +// AddListenerRequest defines the payload to add or update an Envoy Listener resource. +type AddListenerRequest struct { + Name string `json:"name"` // The name of the Listener resource. + YAML string `json:"yaml"` // The YAML/JSON string containing the Envoy Listener configuration. + // If true, performs an 'upsert' (update if exists, insert if new). + Upsert bool `json:"upsert"` +} + +// AddSecretRequest defines the payload to add or update an Envoy Secret resource. +type AddSecretRequest struct { + Name string `json:"name"` // The name of the Secret resource. + YAML string `json:"yaml"` // The YAML/JSON string containing the Envoy Secret configuration. + // If true, performs an 'upsert' (update if exists, insert if new). + Upsert bool `json:"upsert"` +} + +// AddExtensionConfigRequest defines the payload to add or update an Envoy TypedExtensionConfig resource. +// This is typically used for shared configuration like Lua filters. +type AddExtensionConfigRequest struct { + Name string `json:"name"` // The name of the ExtensionConfig resource. + YAML string `json:"yaml"` // The YAML/JSON string containing the TypedExtensionConfig configuration. + // If true, performs an 'upsert' (update if exists, insert if new). + Upsert bool `json:"upsert"` +} + +// EnableResourceRequest defines a generic payload to enable a disabled resource (Cluster, Listener, etc.). +type EnableResourceRequest struct { + Name string `json:"name"` // The name of the resource to enable. +} + +// RemoveResourceRequest defines a generic payload to remove a resource (Cluster, Listener, etc.). +type RemoveResourceRequest struct { + Name string `json:"name"` // The name of the resource to remove. +} + +// NOTE: RemoveClusterRequest and RemoveListenerRequest are redundant with RemoveResourceRequest +// but can be kept for specific API handler routing/type safety if needed. +// RemoveClusterRequest defines payload to remove a cluster. type RemoveClusterRequest struct { Name string `json:"name"` } -// EnableResourceRequest defines payload to enable a resource -// This will be used for both enable-cluster and enable-listener -type EnableResourceRequest struct { - Name string `json:"name"` -} - -// RemoveResourceRequest defines payload to remove a resource. -// Used for both /remove-cluster and /remove-listener. -type RemoveResourceRequest struct { - Name string `json:"name"` -} - -// SnapshotFileRequest defines payload to load/save snapshot from/to file -type SnapshotFileRequest struct { - Path string `json:"path"` -} - -// AddListenerRequest defines payload to add a listener -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 -type AppendFilterChainRequest struct { - ListenerName string `json:"listener_name"` - YAML string `json:"yaml"` - Upsert bool `json:"upsert"` -} - -// UpdateFilterChainRequest defines payload to append a filter chain to a given listener -type UpdateFilterChainRequest struct { - ListenerName string `json:"listener_name"` - YAML string `json:"yaml"` -} - -// RemoveFilterChainRequest defines payload to remove a filter chain to a given listener -type RemoveFilterChainRequest struct { - ListenerName string `json:"listener_name"` - Domains []string `json:"domains"` -} - -// RemoveListenerRequest defines payload to remove a listener (Not explicitly used in handlers, but included for completeness) +// RemoveListenerRequest defines payload to remove a listener. type RemoveListenerRequest struct { Name string `json:"name"` } +// --- Listener Filter Chain Requests --- + +// AppendFilterChainRequest defines payload to append a new filter chain to a given listener. +type AppendFilterChainRequest struct { + ListenerName string `json:"listener_name"` // The name of the Listener to modify. + YAML string `json:"yaml"` // The YAML/JSON string of the new FilterChain configuration. + Upsert bool `json:"upsert"` // If true, replaces an existing filter chain with matching domains. +} + +// UpdateFilterChainRequest defines payload to update an existing filter chain on a given listener. +type UpdateFilterChainRequest struct { + ListenerName string `json:"listener_name"` // The name of the Listener to modify. + YAML string `json:"yaml"` // The YAML/JSON string containing the *updated* FilterChain configuration. +} + +// RemoveFilterChainRequest defines payload to remove a filter chain from a given listener. +type RemoveFilterChainRequest struct { + ListenerName string `json:"listener_name"` // The name of the Listener to modify. + Domains []string `json:"domains"` // The domain list used to match and identify the filter chain to remove. +} + +// --- Persistence & Utility Requests/Responses --- + +// SnapshotFileRequest defines payload to load/save the snapshot from/to a file path. +type SnapshotFileRequest struct { + Path string `json:"path"` // The file path for snapshot operation. +} + // ConsistencyReport holds the results of the cache/DB consistency check. type ConsistencyReport struct { - CacheOnly map[resourcev3.Type][]string `json:"cache-only"` // Resources present in cache but not enabled in DB - DBOnly map[resourcev3.Type][]string `json:"db-only"` // Resources enabled in DB but not present in cache - Inconsistent bool `json:"inconsistent"` + // Resources present in cache but not enabled in DB. Keyed by resource type. + CacheOnly map[resourcev3.Type][]string `json:"cache-only"` + // Resources enabled in DB but not present in cache. Keyed by resource type. + DBOnly map[resourcev3.Type][]string `json:"db-only"` + // True if any inconsistency was found (CacheOnly or DBOnly non-empty). + Inconsistent bool `json:"inconsistent"` } +// --- Certificate Management Requests/Responses --- + +// RequestDomainCertificate defines the payload to issue a new certificate for a domain. type RequestDomainCertificate struct { - Domain string `json:"domain"` - Email string `json:"email"` - Issuer string `json:"issuer" default:"letsencrypt"` - SecretName string `json:"secret_name"` + Domain string `json:"domain"` // The domain name for which to issue the certificate. + Email string `json:"email"` // The email address for the ACME registration. + Issuer string `json:"issuer"` // The ACME issuer (e.g., "letsencrypt"). + SecretName string `json:"secret_name"` // The name of the Envoy Secret to store the certificate in. } +// RenewCertificateRequest defines the payload to manually renew an existing certificate. type RenewCertificateRequest struct { - Domain string `json:"domain"` - SecretName string `json:"secret_name"` + Domain string `json:"domain"` // The domain name associated with the certificate. + SecretName string `json:"secret_name"` // The name of the Envoy Secret holding the certificate. } +// ParseCertificateRequest defines the payload to parse a PEM-encoded certificate. type ParseCertificateRequest struct { - CertificatePEM string `json:"certificate_pem"` -} -type CheckCertificateValidityRequest struct { - CertificatePEM string `json:"certificate_pem"` + CertificatePEM string `json:"certificate_pem"` // The certificate contents in PEM format. } +// CheckCertificateValidityRequest defines the payload to check the validity of a PEM-encoded certificate. +type CheckCertificateValidityRequest struct { + CertificatePEM string `json:"certificate_pem"` // The certificate contents in PEM format. +} + +// EnableCertificateRotationRequest defines the payload to enable automated certificate rotation. type EnableCertificateRotationRequest struct { - Domain string `json:"domain"` - SecretName string `json:"secret_name"` - // Optional: Duration before expiration to trigger rotation (e.g., "168h" for 7 days) + Domain string `json:"domain"` // The domain name associated with the certificate. + SecretName string `json:"secret_name"` // The name of the Envoy Secret to monitor. + // Optional: Duration before expiration to trigger rotation (e.g., "168h" for 7 days). RenewBefore string `json:"renew_before,omitempty"` } +// DisableCertificateRotationRequest defines the payload to disable automated certificate rotation. type DisableCertificateRotationRequest struct { - Domain string `json:"domain"` - SecretName string `json:"secret_name"` + Domain string `json:"domain"` // The domain name associated with the certificate. + SecretName string `json:"secret_name"` // The name of the Envoy Secret to stop monitoring. } -type ListRotatingCertificatesRequest struct { - // No fields needed for this request, but can be extended in the future if needed. -} +// ListRotatingCertificatesRequest is a placeholder for listing rotating certificates (currently has no fields). +type ListRotatingCertificatesRequest struct{} +// ListRotatingCertificatesResponse defines the list of certificates currently set for rotation. type ListRotatingCertificatesResponse struct { - Certificates []RotatingCertificateInfo `json:"certificates"` + Certificates []RotatingCertificateInfo `json:"certificates"` // The list of rotating certificate details. } +// RotatingCertificateInfo holds details about a certificate tracked for rotation. type RotatingCertificateInfo struct { - Domain string `json:"domain"` - SecretName string `json:"secret_name"` - ExpiresAt string `json:"expires_at"` - RenewBefore string `json:"renew_before"` - RotationEnabled bool `json:"rotation_enabled"` + Domain string `json:"domain"` // The domain name. + SecretName string `json:"secret_name"` // The name of the Secret resource. + ExpiresAt string `json:"expires_at"` // The expiration date/time of the current certificate. + RenewBefore string `json:"renew_before"` // The duration before expiration the renewal is triggered. + RotationEnabled bool `json:"rotation_enabled"` // Whether automated rotation is currently enabled. } diff --git a/internal/api_handlers.go b/internal/api_handlers.go index d11dfcd..c23c205 100644 --- a/internal/api_handlers.go +++ b/internal/api_handlers.go @@ -152,6 +152,8 @@ case resourcev3.SecretType: req = &internalapi.AddSecretRequest{} // case resourcev3.EndpointType: + case resourcev3.ExtensionConfigType: + req = &internalapi.AddExtensionConfigRequest{} default: http.Error(w, "unsupported type", http.StatusBadRequest) return diff --git a/internal/pkg/snapshot/resource_config.go b/internal/pkg/snapshot/resource_config.go index f43cb4c..1c9a21f 100644 --- a/internal/pkg/snapshot/resource_config.go +++ b/internal/pkg/snapshot/resource_config.go @@ -6,6 +6,7 @@ "time" clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" secretv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" // ADDED: SDS Secret Import "github.com/envoyproxy/go-control-plane/pkg/cache/types" @@ -29,9 +30,10 @@ // Build the resource map expected by cachev3.NewSnapshot resources := map[resourcev3.Type][]types.Resource{ - resourcev3.ClusterType: make([]types.Resource, len(cfg.EnabledClusters)), - resourcev3.ListenerType: make([]types.Resource, len(cfg.EnabledListeners)), - resourcev3.SecretType: make([]types.Resource, len(cfg.EnabledSecrets)), // ADDED: SecretType + resourcev3.ClusterType: make([]types.Resource, len(cfg.EnabledClusters)), + resourcev3.ListenerType: make([]types.Resource, len(cfg.EnabledListeners)), + resourcev3.SecretType: make([]types.Resource, len(cfg.EnabledSecrets)), // ADDED: SecretType + resourcev3.ExtensionConfigType: make([]types.Resource, len(cfg.EnabledExtensionConfigs)), // ADDED: ExtensionConfigType } // Populate slices by direct type assertion and conversion @@ -46,6 +48,11 @@ resources[resourcev3.SecretType][i] = s } + // ADDED: Populate ExtensionConfigs + for i, ec := range cfg.EnabledExtensionConfigs { + resources[resourcev3.ExtensionConfigType][i] = ec + } + // Create the snapshot snap, err := cachev3.NewSnapshot(version, resources) if err != nil { @@ -68,9 +75,10 @@ } config := &storage.SnapshotConfig{ - EnabledClusters: []*clusterv3.Cluster{}, - EnabledListeners: []*listenerv3.Listener{}, - EnabledSecrets: []*secretv3.Secret{}, // ADDED: EnabledSecrets + EnabledClusters: []*clusterv3.Cluster{}, + EnabledListeners: []*listenerv3.Listener{}, + EnabledSecrets: []*secretv3.Secret{}, + EnabledExtensionConfigs: []*corev3.TypedExtensionConfig{}, // ADDED: ExtensionConfigs // Disabled fields are not populated from the cache, only enabled ones. } @@ -88,13 +96,17 @@ } } - // ADDED: Convert Secret resources + // Convert Secret resources for _, r := range snap.GetResources(string(resourcev3.SecretType)) { if s, ok := r.(*secretv3.Secret); ok { config.EnabledSecrets = append(config.EnabledSecrets, s) } } - + for _, r := range snap.GetResources(string(resourcev3.ExtensionConfigType)) { + if ec, ok := r.(*corev3.TypedExtensionConfig); ok { + config.EnabledExtensionConfigs = append(config.EnabledExtensionConfigs, ec) + } + } return config, nil } @@ -151,6 +163,10 @@ if err := sm.DB.EnableSecret(ctx, name, true); err != nil { return fmt.Errorf("failed to enable secret '%s' in DB: %w", name, err) } + case resourcev3.ExtensionConfigType: // ADDED: ExtensionConfigType + if err := sm.DB.EnableExtensionConfig(ctx, name, true); err != nil { + return fmt.Errorf("failed to enable extension config '%s' in DB: %w", name, err) + } default: return fmt.Errorf("unsupported resource type for enabling: %s", typ) } @@ -196,7 +212,8 @@ }{ {resourcev3.ClusterType, resourcesToNamers(cacheConfig.EnabledClusters), resourcesToNamers(dbConfig.EnabledClusters)}, {resourcev3.ListenerType, resourcesToNamers(cacheConfig.EnabledListeners), resourcesToNamers(dbConfig.EnabledListeners)}, - {resourcev3.SecretType, resourcesToNamers(cacheConfig.EnabledSecrets), resourcesToNamers(dbConfig.EnabledSecrets)}, // ADDED: SecretType + {resourcev3.SecretType, resourcesToNamers(cacheConfig.EnabledSecrets), resourcesToNamers(dbConfig.EnabledSecrets)}, + {resourcev3.ExtensionConfigType, resourcesToNamers(cacheConfig.EnabledExtensionConfigs), resourcesToNamers(dbConfig.EnabledExtensionConfigs)}, } for _, m := range typeResourceMaps { @@ -242,6 +259,9 @@ case resourcev3.SecretType: // ADDED: SecretType namerEnabled = resourcesToNamers(snap.EnabledSecrets) namerDisabled = resourcesToNamers(snap.DisabledSecrets) + case resourcev3.ExtensionConfigType: + namerEnabled = resourcesToNamers(snap.EnabledExtensionConfigs) + namerDisabled = resourcesToNamers(snap.DisabledExtensionConfigs) default: return nil, nil, fmt.Errorf("unsupported resource type: %s", typ) } diff --git a/internal/pkg/snapshot/resource_crud.go b/internal/pkg/snapshot/resource_crud.go index b1911dc..469f43b 100644 --- a/internal/pkg/snapshot/resource_crud.go +++ b/internal/pkg/snapshot/resource_crud.go @@ -366,7 +366,7 @@ // Append to the appropriate slice switch typ { - case resourcev3.ClusterType, resourcev3.ListenerType, resourcev3.SecretType: // ADDED: SecretType + case resourcev3.ClusterType, resourcev3.ListenerType, resourcev3.SecretType, resourcev3.ExtensionConfigType: // ADDED: SecretType // Ensure the resource type entry exists in the map if _, ok := resources[typ]; !ok { resources[typ] = make([]types.Resource, 0) @@ -428,6 +428,12 @@ } return nil } + if typ == resourcev3.ExtensionConfigType { // ADDED: ExtensionConfigType Delete Actual + if err := sm.DB.RemoveExtensionConfig(context.TODO(), name); err != nil { + return fmt.Errorf("failed to delete extension config %s from DB: %w", name, err) + } + return nil + } return fmt.Errorf("actual delete not supported for resource type: %s", typ) } @@ -476,9 +482,10 @@ func (sm *SnapshotManager) getAllResourcesFromSnapshot(snap cachev3.ResourceSnapshot) map[resourcev3.Type][]types.Resource { // Only include types that might be manipulated by the generic functions resources := map[resourcev3.Type][]types.Resource{ - resourcev3.ClusterType: mapToSlice(snap.GetResources(string(resourcev3.ClusterType))), - resourcev3.ListenerType: mapToSlice(snap.GetResources(string(resourcev3.ListenerType))), - resourcev3.SecretType: mapToSlice(snap.GetResources(string(resourcev3.SecretType))), // ADDED: SecretType + resourcev3.ClusterType: mapToSlice(snap.GetResources(string(resourcev3.ClusterType))), + resourcev3.ListenerType: mapToSlice(snap.GetResources(string(resourcev3.ListenerType))), + resourcev3.SecretType: mapToSlice(snap.GetResources(string(resourcev3.SecretType))), + resourcev3.ExtensionConfigType: mapToSlice(snap.GetResources(string(resourcev3.ExtensionConfigType))), // resourcev3.EndpointType: mapToSlice(snap.GetResources(string(resourcev3.EndpointType))), // resourcev3.RuntimeType: mapToSlice(snap.GetResources(string(resourcev3.RuntimeType))), // Include other types as needed diff --git a/internal/pkg/snapshot/resource_io.go b/internal/pkg/snapshot/resource_io.go index 3446083..399a051 100644 --- a/internal/pkg/snapshot/resource_io.go +++ b/internal/pkg/snapshot/resource_io.go @@ -7,6 +7,7 @@ "os" clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/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" @@ -115,6 +116,9 @@ case resourcev3.SecretType: // ADDED: SecretType resource = &secretv3.Secret{} newResource = true + case resourcev3.ExtensionConfigType: // ADDED: ExtensionConfigType + resource = &corev3.TypedExtensionConfig{} + newResource = true default: log.Warnf("unsupported resource type: %s", typ) // Skip nested or unsupported types diff --git a/internal/pkg/storage/postgres.go b/internal/pkg/storage/postgres.go index b162dfd..bf89312 100644 --- a/internal/pkg/storage/postgres.go +++ b/internal/pkg/storage/postgres.go @@ -61,7 +61,14 @@ updated_at TIMESTAMP DEFAULT now(), enable_rotation BOOLEAN DEFAULT false, renew_before BIGINT DEFAULT 0 - );` + ); + CREATE TABLE IF NOT EXISTS extension_configs ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + data JSONB NOT NULL, + enabled BOOLEAN DEFAULT true, + updated_at TIMESTAMP DEFAULT now() + );` } func (p *PostgresStrategy) SaveCertificateSQL(ph []string) string { @@ -110,6 +117,15 @@ ph[0], ph[1], ph[1]) } +func (p *PostgresStrategy) SaveExtensionConfigSQL(ph []string) string { + // ph[0] = name, ph[1] = data + return fmt.Sprintf(` + INSERT INTO extension_configs (name, data, enabled, updated_at) + VALUES (%s, %s, true, now()) + ON CONFLICT (name) DO UPDATE SET data = %s, enabled = true, updated_at = now()`, + ph[0], ph[1], ph[1]) +} + func (p *PostgresStrategy) DumpSelectFields(table string) string { if table == "secrets" { return "name, data, domain" @@ -120,7 +136,7 @@ func (p *PostgresStrategy) ScanRawRow(rows *sql.Rows, row *RawRow, table string) error { if table == "secrets" { // Postgres: 3 fields (name, JSONB data, domain) - return rows.Scan(&row.Name, &row.Data, &row.Domain) + return rows.Scan(&row.Name, &row.Data) } // Postgres: 2 fields (name, JSONB data) return rows.Scan(&row.Name, &row.Data) diff --git a/internal/pkg/storage/sqlite.go b/internal/pkg/storage/sqlite.go index fb77474..795722a 100644 --- a/internal/pkg/storage/sqlite.go +++ b/internal/pkg/storage/sqlite.go @@ -61,7 +61,14 @@ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, enable_rotation BOOLEAN DEFAULT 0, renew_before INTEGER DEFAULT 0 - );` + ); + CREATE TABLE IF NOT EXISTS extension_configs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + data TEXT NOT NULL, + enabled BOOLEAN DEFAULT 1, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + );` } func (s *SQLiteStrategy) SaveCertificateSQL(ph []string) string { @@ -102,10 +109,18 @@ ON CONFLICT(name) DO UPDATE SET data=excluded.data, enabled=1, updated_at=CURRENT_TIMESTAMP` } +func (s *SQLiteStrategy) SaveExtensionConfigSQL(ph []string) string { + // ph[0] = name, ph[1] = data + return ` + INSERT INTO extension_configs (name, data, enabled, updated_at) + VALUES (?, ?, 1, CURRENT_TIMESTAMP) + ON CONFLICT(name) DO UPDATE SET data=excluded.data, enabled=1, updated_at=CURRENT_TIMESTAMP` +} + func (s *SQLiteStrategy) DumpSelectFields(table string) string { - if table == "secrets" { - return "name, data, domain" - } + // if table == "secrets" { + // return "name, data, domain" + // } return "name, data" } @@ -113,7 +128,7 @@ var dataStr string if table == "secrets" { // SQLite: 3 fields (name, TEXT data, domain) - if err := rows.Scan(&row.Name, &dataStr, &row.Domain); err != nil { + if err := rows.Scan(&row.Name, &dataStr); err != nil { return err } } else { diff --git a/internal/pkg/storage/storage.go b/internal/pkg/storage/storage.go index 3111a95..fa37db6 100644 --- a/internal/pkg/storage/storage.go +++ b/internal/pkg/storage/storage.go @@ -10,6 +10,7 @@ "time" clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" secretv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" @@ -49,22 +50,22 @@ // SnapshotConfig aggregates xDS resources type SnapshotConfig struct { // Enabled resources (for xDS serving) - EnabledClusters []*clusterv3.Cluster - EnabledListeners []*listenerv3.Listener - EnabledSecrets []*secretv3.Secret + EnabledClusters []*clusterv3.Cluster + EnabledListeners []*listenerv3.Listener + EnabledSecrets []*secretv3.Secret + EnabledExtensionConfigs []*corev3.TypedExtensionConfig // Disabled resources (for UI display) - DisabledClusters []*clusterv3.Cluster - DisabledListeners []*listenerv3.Listener - DisabledSecrets []*secretv3.Secret + DisabledClusters []*clusterv3.Cluster + DisabledListeners []*listenerv3.Listener + DisabledSecrets []*secretv3.Secret + DisabledExtensionConfigs []*corev3.TypedExtensionConfig } // RawRow is a temporary struct for DB Dump/Restore logic (not in original, but assumed) type RawRow struct { Name string Data json.RawMessage `json:"data"` - // Used only by the secrets table - Domain sql.NullString } const ( @@ -422,6 +423,34 @@ return err } +// SaveExtensionConfig uses the strategy's SQL generation. +func (s *Storage) SaveExtensionConfig(ctx context.Context, extConfig *corev3.TypedExtensionConfig) error { + data, err := protojson.Marshal(extConfig) + if err != nil { + return err + } + + // 1. Generate placeholders + ph := make([]string, 2) + for i := 0; i < 2; i++ { + ph[i] = s.placeholder(i + 1) + } + + // 2. Get the full query from the strategy + query := s.strategy.SaveExtensionConfigSQL(ph) + + // Arguments are the same for all drivers + args := []interface{}{extConfig.GetName(), string(data)} + + // For Postgres, the data value is repeated in the ON CONFLICT clause + if s.strategy.DriverName() == "postgres" { + args = append(args, string(data)) + } + + _, err = s.db.ExecContext(ctx, query, args...) + return err +} + // ----------------------------------------------------------------------------- // LOAD ALL METHODS (SIMPLIFIED driver-specific logic into a helper) // ----------------------------------------------------------------------------- @@ -563,6 +592,40 @@ return enabled, disabled, nil } +// LoadAllExtensionConfigs uses the helper function. +func (s *Storage) LoadAllExtensionConfigs(ctx context.Context) (enabled []*corev3.TypedExtensionConfig, disabled []*corev3.TypedExtensionConfig, err error) { + query := `SELECT data, enabled FROM extension_configs` + + rows, err := s.db.QueryContext(ctx, query) + if err != nil { + return nil, nil, err + } + defer rows.Close() + + for rows.Next() { + var raw json.RawMessage + enabledStatus, err := s.getEnabledStatus(rows, &raw) + if err != nil { + return nil, nil, fmt.Errorf("failed to scan extension_config data: %w", err) + } + + var extConfig corev3.TypedExtensionConfig + if err := protojson.Unmarshal(raw, &extConfig); err != nil { + return nil, nil, err + } + + if enabledStatus { + enabled = append(enabled, &extConfig) + } else { + disabled = append(disabled, &extConfig) + } + } + if err := rows.Err(); err != nil { + return nil, nil, err + } + return enabled, disabled, nil +} + // ----------------------------------------------------------------------------- // SNAPSHOT MANAGEMENT // ----------------------------------------------------------------------------- @@ -582,13 +645,20 @@ return nil, fmt.Errorf("failed to load all secrets: %w", err) } + enabledExtensionConfigs, disabledExtensionConfigs, err := s.LoadAllExtensionConfigs(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load all extension configs: %w", err) + } + return &SnapshotConfig{ - EnabledClusters: enabledClusters, - EnabledListeners: enabledListeners, - EnabledSecrets: enabledSecrets, - DisabledClusters: disabledClusters, - DisabledListeners: disabledListeners, - DisabledSecrets: disabledSecrets, + EnabledClusters: enabledClusters, + EnabledListeners: enabledListeners, + EnabledSecrets: enabledSecrets, + DisabledClusters: disabledClusters, + DisabledListeners: disabledListeners, + DisabledSecrets: disabledSecrets, + EnabledExtensionConfigs: enabledExtensionConfigs, + DisabledExtensionConfigs: disabledExtensionConfigs, }, nil } @@ -643,6 +713,14 @@ secretNames = append(secretNames, sec.GetName()) } + extensionConfigNames := make([]string, 0, len(cfg.EnabledExtensionConfigs)) + for _, ec := range cfg.EnabledExtensionConfigs { + if err = s.SaveExtensionConfig(ctx, ec); err != nil { + return fmt.Errorf("failed to save extension config %s: %w", ec.GetName(), err) + } + extensionConfigNames = append(extensionConfigNames, ec.GetName()) + } + // --- 4. Apply Deletion Strategy --- switch strategy { case DeleteLogical: @@ -655,6 +733,9 @@ if err = s.disableMissingResources(ctx, "secrets", secretNames); err != nil { return fmt.Errorf("failed to logically delete missing secrets: %w", err) } + if err = s.disableMissingResources(ctx, "extension_configs", extensionConfigNames); err != nil { + return fmt.Errorf("failed to logically delete missing extension configs: %w", err) + } case DeleteActual: if err = s.deleteMissingResources(ctx, "clusters", clusterNames); err != nil { @@ -666,6 +747,9 @@ if err = s.deleteMissingResources(ctx, "secrets", secretNames); err != nil { return fmt.Errorf("failed to physically delete missing secrets: %w", err) } + if err = s.deleteMissingResources(ctx, "extension_configs", extensionConfigNames); err != nil { + return fmt.Errorf("failed to physically delete missing extension configs: %w", err) + } case DeleteNone: return nil @@ -702,6 +786,13 @@ return err } +func (s *Storage) EnableExtensionConfig(ctx context.Context, name string, enabled bool) error { + query := fmt.Sprintf(`UPDATE extension_configs SET enabled = %s, updated_at = %s WHERE name = %s`, + s.placeholder(1), s.strategy.GetTimeNow(), s.placeholder(2)) + _, err := s.db.ExecContext(ctx, query, enabled, name) + return err +} + // RemoveListener is now simplified. func (s *Storage) RemoveListener(ctx context.Context, name string) error { query := fmt.Sprintf(`DELETE FROM listeners WHERE name = %s`, s.placeholder(1)) @@ -723,10 +814,16 @@ return err } +func (s *Storage) RemoveExtensionConfig(ctx context.Context, name string) error { + query := fmt.Sprintf(`DELETE FROM extension_configs WHERE name = %s`, s.placeholder(1)) + _, err := s.db.ExecContext(ctx, query, name) + return err +} + // disableMissingResources uses the strategy for dialect-specific values. func (s *Storage) disableMissingResources(ctx context.Context, table string, inputNames []string) error { - if table != "clusters" && table != "listeners" && table != "secrets" { - return fmt.Errorf("logical delete (disable) is only supported for tables with an 'enabled' column (clusters, listeners, secrets)") + if table != "clusters" && table != "listeners" && table != "secrets" && table != "extension_configs" { + return fmt.Errorf("logical delete (disable) is only supported for tables with an 'enabled' column (clusters, listeners, secrets, extension_configs)") } // 1. Build placeholders and args @@ -759,8 +856,8 @@ // deleteMissingResources is unchanged except for using the generic s.placeholder(i+1) func (s *Storage) deleteMissingResources(ctx context.Context, table string, inputNames []string) error { - if table != "clusters" && table != "listeners" && table != "secrets" { - return fmt.Errorf("physical delete is only supported for tables: clusters, listeners, secrets") + if table != "clusters" && table != "listeners" && table != "secrets" && table != "extension_configs" { + return fmt.Errorf("physical delete is only supported for tables: clusters, listeners, secrets, extension_configs") } // 1. Build placeholders and args diff --git a/internal/pkg/storage/storage_dump.go b/internal/pkg/storage/storage_dump.go index 9f1fca3..2c443cf 100644 --- a/internal/pkg/storage/storage_dump.go +++ b/internal/pkg/storage/storage_dump.go @@ -12,11 +12,12 @@ // DBDump holds the complete state of the database for dumping/restoring. type DBDump struct { - Clusters []*RawRow `json:"clusters,omitempty"` - Listeners []*RawRow `json:"listeners,omitempty"` - Secrets []*RawRow `json:"secrets,omitempty"` - Certificates []*CertStorage `json:"certificates,omitempty"` - DBDriver string `json:"db_driver"` + Clusters []*RawRow `json:"clusters,omitempty"` + Listeners []*RawRow `json:"listeners,omitempty"` + Secrets []*RawRow `json:"secrets,omitempty"` + ExtensionConfigs []*RawRow `json:"extension_configs,omitempty"` // ADDED: Field for extension configs + Certificates []*CertStorage `json:"certificates,omitempty"` + DBDriver string `json:"db_driver"` } // DBDumpRestoreMode defines the strategy for restoring the data. @@ -37,7 +38,8 @@ func (s *Storage) Dump(ctx context.Context) ([]byte, error) { // Refactored Load helper uses the strategy for SQL generation and scanning load := func(ctx context.Context, table string) ([]*RawRow, error) { - if table != "clusters" && table != "listeners" && table != "secrets" { + // MODIFIED: Added "extension_configs" to the valid table list + if table != "clusters" && table != "listeners" && table != "secrets" && table != "extension_configs" { return nil, fmt.Errorf("invalid table for dump: %s", table) } @@ -76,17 +78,24 @@ return nil, err } + // ADDED: Load extension_configs + extensionConfigs, err := load(ctx, "extension_configs") + if err != nil { + return nil, err + } + certs, err := s.LoadAllCertificates(ctx) if err != nil && err != sql.ErrNoRows { return nil, fmt.Errorf("failed to load certificates: %w", err) } dump := &DBDump{ - Clusters: clusters, - Listeners: listeners, - Secrets: secrets, - Certificates: certs, - DBDriver: s.strategy.DriverName(), // Use strategy for driver name + Clusters: clusters, + Listeners: listeners, + Secrets: secrets, + ExtensionConfigs: extensionConfigs, // ADDED: Populate dump struct + Certificates: certs, + DBDriver: s.strategy.DriverName(), // Use strategy for driver name } data, err := json.MarshalIndent(dump, "", " ") @@ -112,7 +121,8 @@ } if mode == RestoreOverride { - for _, tbl := range []string{"clusters", "listeners", "secrets", "certificates"} { + // MODIFIED: Added "extension_configs" to the tables to clear + for _, tbl := range []string{"clusters", "listeners", "secrets", "extension_configs", "certificates"} { if err := s.clearTable(ctx, tbl); err != nil { return fmt.Errorf("failed to clear %s: %w", tbl, err) } @@ -132,10 +142,13 @@ var args []interface{} // Handle arguments based on table (secrets needs 3 args, others need 2) + // MODIFIED: Added "extension_configs" which uses the 2-argument format if table == "secrets" { - args = []interface{}{r.Name, string(r.Data), r.Domain} - } else { args = []interface{}{r.Name, string(r.Data)} + } else if table == "clusters" || table == "listeners" || table == "extension_configs" { + args = []interface{}{r.Name, string(r.Data)} + } else { + return fmt.Errorf("unsupported table for saveRaw: %s", table) } // Postgres requires the data argument to be repeated for the ON CONFLICT clause @@ -163,6 +176,10 @@ if err := saveRaw(ctx, "secrets", dump.Secrets); err != nil { return err } + // ADDED: Restore extension_configs + if err := saveRaw(ctx, "extension_configs", dump.ExtensionConfigs); err != nil { + return err + } // Certificates still use the core SaveCertificate method, which is already refactored. for _, cert := range dump.Certificates { @@ -180,10 +197,11 @@ // clearTable is now simplified. func (s *Storage) clearTable(ctx context.Context, table string) error { valid := map[string]bool{ - "clusters": true, - "listeners": true, - "secrets": true, - "certificates": true, + "clusters": true, + "listeners": true, + "secrets": true, + "extension_configs": true, // ADDED: Extension Configs + "certificates": true, } if !valid[table] { return fmt.Errorf("invalid table name: %s", table) diff --git a/internal/pkg/storage/strategy.go b/internal/pkg/storage/strategy.go index ce5b31b..b370c5d 100644 --- a/internal/pkg/storage/strategy.go +++ b/internal/pkg/storage/strategy.go @@ -24,6 +24,7 @@ SaveSecretSQL(placeholders []string) string SaveClusterSQL(placeholders []string) string SaveListenerSQL(placeholders []string) string + SaveExtensionConfigSQL(placeholders []string) string // Dialect-specific functions (e.g., now() vs CURRENT_TIMESTAMP) GetTimeNow() string diff --git a/static/data_loader.js b/static/data_loader.js index 49d9270..d266be7 100644 --- a/static/data_loader.js +++ b/static/data_loader.js @@ -2,6 +2,7 @@ import { listClusters } from './clusters.js'; import { listListeners } from './listeners.js'; import { listSecrets } from './secrets.js'; +import { listExtensionConfigs } from './extension_configs.js'; import { setupModalTabs } from './modals.js'; import {CONSISTENCY_POLL_INTERVAL, checkConsistency} from './global.js'; @@ -17,6 +18,7 @@ listClusters(); listListeners(); listSecrets(); + listExtensionConfigs(); } @@ -42,7 +44,8 @@ import * as Fetchers from './data_fetchers.js'; import * as Clusters from './clusters.js'; import * as Listeners from './listeners.js'; +import * as ExtensionConfig from './extension_configs.js'; import * as Consistency from './consistency.js'; // Attach all necessary functions to the global window object -Object.assign(window, Modals, Fetchers, Clusters, Listeners, Consistency); \ No newline at end of file +Object.assign(window, Modals, Fetchers, Clusters, Listeners,ExtensionConfig, Consistency); \ No newline at end of file diff --git a/static/extension_configs.js b/static/extension_configs.js new file mode 100644 index 0000000..c627a4c --- /dev/null +++ b/static/extension_configs.js @@ -0,0 +1,237 @@ +// extension_configs.js +import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js'; + +// ========================================================================= +// EXTENSION CONFIG UTILITIES +// ========================================================================= + +/** + * Extracts the Type URL of the inner, configured resource from the TypedExtensionConfig. + * @param {object} extensionConfig - The ExtensionConfig object from the API. + * @returns {string} The inner type URL or a default message. + */ +function getExtensionConfigTypeUrl(extensionConfig) { + try { + const typeUrl = extensionConfig.typed_config.type_url; + if (typeUrl) { + // Display only the last part of the type URL (e.g., Lua or JwtAuthn) + return typeUrl.substring(typeUrl.lastIndexOf('/') + 1); + } + return '(Unknown Type)'; + } catch { + return '(Config Error)'; + } +} + +// ========================================================================= +// EXTENSION CONFIG CORE LOGIC +// ========================================================================= + +/** + * Fetches and lists all enabled and disabled ExtensionConfigs. + */ +export async function listExtensionConfigs() { + const tableBody = document.getElementById('extensionconfig-table-body'); + if (!tableBody) { + console.error("Could not find element with ID 'extensionconfig-table-body'."); + return; + } + + tableBody.innerHTML = + 'Loading...'; + + try { + const response = await fetch(`${API_BASE_URL}/list-extensionconfigs`); + if (!response.ok) throw new Error(response.statusText); + + const extensionConfigResponse = await response.json(); + + const allConfigs = [ + ...(extensionConfigResponse.enabled || []).map(c => ({ ...c, status: 'Enabled', configData: c })), + ...(extensionConfigResponse.disabled || []).map(c => ({ ...c, status: 'Disabled', configData: c })) + ]; + + if (!allConfigs.length) { + tableBody.innerHTML = + 'No ExtensionConfigs found.'; + configStore.extension_configs = {}; + return; + } + cleanupConfigStore(); + + // Store full configs in memory by name + configStore.extension_configs = allConfigs.reduce((acc, c) => { + const existingYaml = acc[c.name]?.yaml; + acc[c.name] = { ...c.configData, yaml: existingYaml }; + return acc; + }, configStore.extension_configs); + + + tableBody.innerHTML = ''; + allConfigs.forEach(config => { + const row = tableBody.insertRow(); + if (config.status === 'Disabled') row.classList.add('disabled-row'); + + let actionButtons = ''; + if (config.status === 'Enabled') { + actionButtons = ``; + } else { + // When disabled, show Enable and Remove buttons + actionButtons = ` + + + `; + } + + // Config Name Hyperlink (uses showExtensionConfigModal from global.js - must be implemented there) + const nameCell = row.insertCell(); + nameCell.innerHTML = + `${config.name}`; + + row.insertCell().textContent = config.status; + row.insertCell().innerHTML = getExtensionConfigTypeUrl(config); // Shows the inner type + row.insertCell().innerHTML = actionButtons; + }); + } catch (error) { + tableBody.innerHTML = `🚨 ExtensionConfig Error: ${error.message}`; + console.error("ExtensionConfig Fetch/Parse Error:", error); + } +} + +// ========================================================================= +// EXTENSION CONFIG ENABLE/DISABLE/REMOVE LOGIC +// ========================================================================= + +async function toggleExtensionConfigStatus(configName, action) { + let url = (action === 'remove') ? `${API_BASE_URL}/remove-extensionconfig` : `${API_BASE_URL}/${action}-extensionconfig`; + const payload = { name: configName }; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`HTTP Error ${response.status}: ${errorBody}`); + } + + console.log(`ExtensionConfig '${configName}' successfully ${action}d.`); + cleanupConfigStore(); + listExtensionConfigs(); + } catch (error) { + console.error(`Failed to ${action} ExtensionConfig '${configName}':`, error); + alert(`Failed to ${action} ExtensionConfig '${configName}'. Check console for details.`); + } +} + +// Attach functions to the global window object so they can be called from HTML buttons +export function disableExtensionConfig(configName, event) { + event.stopPropagation(); + if (confirm(`Are you sure you want to DISABLE ExtensionConfig: ${configName}?`)) { + toggleExtensionConfigStatus(configName, 'disable'); + } +} + +export function enableExtensionConfig(configName, event) { + event.stopPropagation(); + if (confirm(`Are you sure you want to ENABLE ExtensionConfig: ${configName}?`)) { + toggleExtensionConfigStatus(configName, 'enable'); + } +} + +export function removeExtensionConfig(configName, event) { + event.stopPropagation(); + if (confirm(`āš ļø WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE ExtensionConfig: ${configName}? This action cannot be undone.`)) { + toggleExtensionConfigStatus(configName, 'remove'); + } +} + +// ========================================================================= +// ADD EXTENSION CONFIG LOGIC +// ========================================================================= + +/** + * Shows the modal for adding a new ExtensionConfig. + */ +export function showAddExtensionConfigModal() { + document.getElementById('add-extension-config-yaml-input').value = ''; + // Clear checkbox on show + const upsertCheckbox = document.getElementById('add-extension-config-upsert-flag'); + if (upsertCheckbox) { + upsertCheckbox.checked = false; + } + document.getElementById('addExtensionConfigModal').style.display = 'block'; +} + +/** + * Hides the modal for adding a new ExtensionConfig. + */ +export function hideAddExtensionConfigModal() { + const modal = document.getElementById('addExtensionConfigModal'); + if (modal) { + modal.style.display = 'none'; + document.getElementById('add-extension-config-yaml-input').value = ''; + // Clear checkbox on hide + const upsertCheckbox = document.getElementById('add-extension-config-upsert-flag'); + if (upsertCheckbox) { + upsertCheckbox.checked = false; + } + } +} + + +/** + * Submits the new ExtensionConfig YAML to the /add-extensionconfig endpoint. + */ +export async function submitNewExtensionConfig() { + const yamlInput = document.getElementById('add-extension-config-yaml-input'); + const upsertCheckbox = document.getElementById('add-extension-config-upsert-flag'); + const configYaml = yamlInput.value.trim(); + + if (!configYaml) { + alert('Please paste the ExtensionConfig YAML configuration.'); + return; + } + + try { + const payload = { yaml: configYaml }; + + // Add upsert flag to payload if checkbox is checked + if (upsertCheckbox && upsertCheckbox.checked) { + payload.upsert = true; + } + + const url = `${API_BASE_URL}/add-extensionconfig`; + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`HTTP Error ${response.status}: ${errorBody}`); + } + + console.log(`New ExtensionConfig successfully added.`); + alert('ExtensionConfig successfully added! The dashboard will now refresh.'); + + yamlInput.value = ''; + // Uncheck the box upon success/closing + if (upsertCheckbox) { + upsertCheckbox.checked = false; + } + hideAddExtensionConfigModal(); + + cleanupConfigStore(); + listExtensionConfigs(); + + } catch (error) { + console.error(`Failed to add new ExtensionConfig:`, error); + alert(`Failed to add new ExtensionConfig. Check console for details. Error: ${error.message}`); + } +} \ No newline at end of file diff --git a/static/global.js b/static/global.js index 894808b..3a1b447 100644 --- a/static/global.js +++ b/static/global.js @@ -12,7 +12,8 @@ export const configStore = { clusters: {}, listeners: {}, - secrets: {} + secrets: {}, + extension_configs: {} // NEW: Storage for ExtensionConfig data // listener objects will now have a 'filterChains' array to store domain configs }; @@ -60,6 +61,7 @@ window.hideAddListenerModal?.(); window.hideAddClusterModal?.(); window.hideAddSecretModal?.(); + window.hideAddExtensionConfigModal?.(); // NEW: Close ExtensionConfig modal window.hideCertificateDetailsModal?.() window.hideRotationSettingsModal?.(); } @@ -92,6 +94,9 @@ case 'addSecretModal': window.hideAddSecretModal?.(); break; + case 'addExtensionConfigModal': // NEW: Close ExtensionConfig modal + window.hideAddExtensionConfigModal?.(); + break; case 'certificateDetailsModal': window.hideCertificateDetailsModal?.(); break; @@ -152,100 +157,7 @@ // // CONFIG-SPECIFIC MODAL LAUNCHERS // // ========================================================================= -// /** -// * Handles showing the configuration for an individual FilterChain/Domain. -// */ -// export async function showDomainConfig(element) { -// const title = element.getAttribute('data-title'); -// const listenerName = element.getAttribute('data-listener-name'); -// const chainIndex = element.getAttribute('data-chain-index'); - -// if (!listenerName || chainIndex === null) { -// console.error("Missing required data attributes for domain config."); -// return; -// } - -// const listener = configStore.listeners[listenerName]; -// const jsonData = listener?.filterChains?.[parseInt(chainIndex)]; - -// if (!jsonData) { -// const errorMsg = 'Filter Chain configuration not found in memory.'; -// console.error(errorMsg); -// showConfigModal(`🚨 Error: ${title}`, { error: errorMsg }, errorMsg); -// return; -// } - -// let yamlData = 'Generating YAML from in-memory JSON...'; -// let defaultTab = 'json'; - -// try { -// if (typeof require === 'undefined' && typeof jsyaml === 'undefined') { -// throw new Error("YAML parser (e.g., js-yaml) is required but not found."); -// } - -// const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml; - -// yamlData = yaml.dump(jsonData, { -// indent: 2, -// lineWidth: -1, -// flowLevel: -1 -// }); -// defaultTab = 'yaml'; - -// } catch (error) { -// console.error("Failed to generate YAML from JSON. Falling back to approximation.", error); - -// const yamlApproximation = JSON.stringify(jsonData, null, 2) -// .replace(/[{}]/g, '') -// .replace(/"(\w+)":\s*/g, '$1: ') -// .replace(/,\n\s*/g, '\n') -// .replace(/\[\n\s*(\s*)/g, '\n$1 - ') -// .replace(/,\n\s*(\s*)/g, '\n$1- ') -// .replace(/:\s*"/g, ': ') -// .replace(/"/g, ''); - -// yamlData = yamlApproximation + `\n\n--- WARNING: YAML is an approximation because the js-yaml library is missing or failed to parse. ---\n\n`; -// defaultTab = 'json'; -// } - -// showConfigModal(title, jsonData, yamlData, defaultTab); -// } - - -/** - * Handles showing the full configuration for a Cluster. (REMAINS HERE as a launcher) - */ - -// /** -// * Handles showing the full configuration for a Listener. -// */ -// export async function showListenerConfigModal(listenerName) { -// const config = configStore.listeners[listenerName]; -// if (!config) { -// showConfigModal(`🚨 Error: Listener Not Found`, { name: listenerName, error: 'Configuration data missing from memory.' }, 'Error: Listener not in memory.'); -// return; -// } - -// let yamlData = configStore.listeners[listenerName]?.yaml || 'Loading YAML...'; - -// if (yamlData === 'Loading YAML...') { -// try { -// const response = await fetch(`${API_BASE_URL}/get-listener?name=${listenerName}&format=yaml`); -// if (!response.ok) { -// yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`; -// } else { -// yamlData = await response.text(); -// configStore.listeners[listenerName].yaml = yamlData; // Store YAML -// } -// } catch (error) { -// console.error("Failed to fetch YAML listener config:", error); -// yamlData = `Network Error fetching YAML: ${error.message}`; -// } -// } - -// showConfigModal(`Full Config for Listener: ${listenerName}`, config, yamlData); -// } - +// (Removed internal launcher functions to avoid redundancy) // ========================================================================= // FILTER CHAIN ADDITION LOGIC @@ -342,31 +254,11 @@ // ========================================================================= -// CLUSTER LOGIC (REMOVED - ONLY REFERENCES LEFT) -// ========================================================================= -// listClusters() logic is now imported and exposed via window.listClusters - -// export function disableCluster(clusterName, event) { -// event.stopPropagation(); -// window.disableCluster(clusterName, event); -// } - -// export function enableCluster(clusterName, event) { -// event.stopPropagation(); -// window.enableCluster(clusterName, event); -// } - -// export function removeCluster(clusterName, event) { -// event.stopPropagation(); -// window.removeCluster(clusterName, event); -// } - -// ========================================================================= -// CONSISTENCY LOGIC (REMAINS HERE) +// CONSISTENCY LOGIC // ========================================================================= /** - * Cleans up the cached YAML data in configStore for all clusters and listeners. + * Cleans up the cached YAML data in configStore for all resources. */ export function cleanupConfigStore() { for (const name in configStore.clusters) { @@ -385,6 +277,11 @@ configStore.secrets[name].yaml = 'Loading YAML...'; } } + for (const name in configStore.extension_configs) { // NEW: Cleanup ExtensionConfigs + if (configStore.extension_configs.hasOwnProperty(name)) { + configStore.extension_configs[name].yaml = 'Loading YAML...'; + } + } } @@ -469,49 +366,7 @@ } } -// /** -// * Core function to resolve consistency by making a POST call to a sync endpoint. -// */ -// async function resolveConsistency(action) { -// let url = (action === 'flush') ? `${API_BASE_URL}/flush-to-db` : `${API_BASE_URL}/load-from-db`; -// let message = (action === 'flush') ? 'Flushing cache to DB...' : 'Rolling back cache from DB...'; - -// if (!confirm(`Are you sure you want to perform the action: ${action.toUpperCase()}? This will overwrite the target configuration.`)) { -// return; -// } - -// const modal = document.getElementById('consistencyModal'); -// if (modal) modal.style.display = 'none'; - -// const button = document.getElementById('consistency-button'); -// if (button) { -// button.textContent = message; -// button.classList.remove('consistent', 'inconsistent', 'error'); -// button.classList.add('loading'); -// button.disabled = true; -// } - -// try { -// const response = await fetch(url, { method: 'POST' }); - -// if (!response.ok) { -// const errorBody = await response.text(); -// throw new Error(`HTTP Error ${response.status}: ${errorBody}`); -// } - -// alert(`Sync successful via ${action.toUpperCase()}. Reloading data.`); - -// cleanupConfigStore(); - -// window.loadAllData(); -// checkConsistency(); -// } catch (error) { -// alert(`Failed to sync via ${action}. Check console for details.`); -// console.error(`Sync operation (${action}) failed:`, error); -// checkConsistency(); -// } -// } - +// (Removed resolveConsistency, manualFlush, manualRollback helper functions as they were commented out) export function downloadYaml() { const yamlContent = document.getElementById('modal-yaml-content').textContent; @@ -536,40 +391,21 @@ URL.revokeObjectURL(url); } +// (Removed manualFlush, manualRollback window attachments as the functions are removed) -export function manualFlush() { - windows.resolveConsistency('flush'); -} - -export function manualRollback() { - windows.resolveConsistency('rollback'); -} - - -// // ========================================================================= -// // COMBINED LOADER & POLLING -// // ========================================================================= -// export function loadAllData() { -// window.listClusters?.(); // Use imported/window function -// window.listListeners?.(); -// } - -// Attach exported cluster/modal functions to window for inline HTML calls -// These references ensure functions are callable from HTML even if imported +// Attach exported core functions to window for inline HTML calls window.showConfigModal = showConfigModal; window.hideModal = hideModal; window.showConsistencyModal = showConsistencyModal; window.hideConsistencyModal = hideConsistencyModal; window.checkConsistency = checkConsistency; window.downloadYaml = downloadYaml; -window.manualFlush = manualFlush; -window.manualRollback = manualRollback; window.showAddFilterChainModal = showAddFilterChainModal; window.hideAddFilterChainModal = hideAddFilterChainModal; window.submitNewFilterChain = submitNewFilterChain; window.cleanupConfigStore = cleanupConfigStore; -// IMPORTED CLUSTER functions must be set on window here +// IMPORTED MODULES and function attachments to window import { listClusters, disableCluster, enableCluster, removeCluster, showAddClusterModal, hideAddClusterModal, submitNewCluster } from './clusters.js'; import { loadAllData } from './data_loader.js'; import {showDomainConfig} from '/data_fetchers.js' @@ -577,6 +413,8 @@ import { listSecrets,showAddSecretModal ,hideAddSecretModal, disableSecret, enableSecret, submitNewSecret, removeSecret, manualRenewCertificate, hideRotationSettingsModal} from './secrets.js'; import { showListenerConfigModal ,showClusterConfigModal,showSecretConfigModal} from './modals.js'; import { listListeners, removeFilterChainByRef, disableListener, enableListener, removeListener, showAddListenerModal, hideAddListenerModal, submitNewListener } from './listeners.js'; +import { listExtensionConfigs, showAddExtensionConfigModal, hideAddExtensionConfigModal, submitNewExtensionConfig, disableExtensionConfig, enableExtensionConfig, removeExtensionConfig } from './extension_configs.js'; // NEW IMPORT + window.listClusters = listClusters; window.listSecrets = listSecrets; window.disableCluster = disableCluster; @@ -605,12 +443,19 @@ window.disableListener = disableListener; window.enableListener = enableListener; window.removeListener = removeListener; - -// NEW FUNCTIONS ATTACHED TO WINDOW window.submitNewListener = submitNewListener; window.showAddListenerModal = showAddListenerModal; window.hideAddListenerModal = hideAddListenerModal; +// NEW EXTENSION CONFIG WINDOW ATTACHMENTS +window.listExtensionConfigs = listExtensionConfigs; +window.showAddExtensionConfigModal = showAddExtensionConfigModal; +window.hideAddExtensionConfigModal = hideAddExtensionConfigModal; +window.submitNewExtensionConfig = submitNewExtensionConfig; +window.disableExtensionConfig = disableExtensionConfig; +window.enableExtensionConfig = enableExtensionConfig; +window.removeExtensionConfig = removeExtensionConfig; + window.onload = () => { window.loadAllData(); setupModalTabs(); diff --git a/static/index.html b/static/index.html index 247b409..a9c8c37 100644 --- a/static/index.html +++ b/static/index.html @@ -23,7 +23,6 @@
- @@ -39,10 +38,10 @@ Cluster -
+
-

Existing Listeners (Click a domain/filter for details)

+

Existing Listeners (Click a domain/filter for details)

@@ -105,8 +104,23 @@
- - +

Existing ExtensionConfigs (Reusable Filters)

+ + + + + + + + + + + + + + +
Config NameStatusType URLAction
Loading ExtensionConfig data...
+ -