diff --git a/data/config.db b/data/config.db index bfbb3ed..b2f562d 100644 --- a/data/config.db +++ b/data/config.db Binary files differ diff --git a/internal/api_handlers.go b/internal/api_handlers.go index 0569963..b5c4bcc 100644 --- a/internal/api_handlers.go +++ b/internal/api_handlers.go @@ -78,7 +78,7 @@ return } - resources, err := api.Manager.LoadSnapshotFromFile(req.Path) + resources, err := api.Manager.LoadSnapshotFromFile(context.Background(), req.Path) if err != nil { http.Error(w, fmt.Sprintf("failed to load snapshot from file: %v", err), http.StatusInternalServerError) return diff --git a/internal/log/log.go b/internal/log/log.go index 50a57de..31f8307 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -1,9 +1,24 @@ package internal import ( - "k8s.io/klog/v2" // Import klog + "context" + + "k8s.io/klog/v2" ) +// Logger defines the interface for logging operations. +// The DefaultLogger must satisfy this interface. +// This is necessary so we can store and retrieve the interface type in the context. +type Logger interface { + Debugf(format string, args ...interface{}) + Infof(format string, args ...interface{}) + Warnf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) +} + +// Ensure DefaultLogger satisfies the Logger interface. +var _ Logger = (*DefaultLogger)(nil) + // DefaultLogger is enabled when no consuming clients provide // a logger to the server/cache subsystem. type DefaultLogger struct { @@ -39,3 +54,38 @@ func (l *DefaultLogger) Errorf(format string, args ...interface{}) { klog.Errorf(format, args...) } + +// ----------------------------------------------------------------------------- +// Context Key and Functions +// ----------------------------------------------------------------------------- + +// loggerKey is an unexported type for context keys. +// Using an unexported, unique type prevents collisions with other packages' keys. +type loggerKey struct{} + +var ( + // defaultLog is a singleton instance of the default logger + defaultLog = NewDefaultLogger() +) + +// WithLogger returns a new context derived from ctx with the provided Logger +// injected into it. +func WithLogger(ctx context.Context, log Logger) context.Context { + return context.WithValue(ctx, loggerKey{}, log) +} + +// LogFromContext extracts the Logger from the context. +// If no Logger is present, it returns the default Logger. +func LogFromContext(ctx context.Context) Logger { + if ctx == nil { + return defaultLog + } + + // Retrieve the value stored with loggerKey{} + if log, ok := ctx.Value(loggerKey{}).(Logger); ok { + return log + } + + // If no logger is found in the context, return the default logger + return defaultLog +} diff --git a/internal/snapshot.go b/internal/snapshot.go index 055d819..015b221 100644 --- a/internal/snapshot.go +++ b/internal/snapshot.go @@ -8,9 +8,7 @@ "time" clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" - endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/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" // Ensure all standard filters are imported for proto unmarshalling _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" @@ -27,6 +25,7 @@ yaml "gopkg.in/yaml.v3" internalapi "envoy-control-plane/internal/api" + internallog "envoy-control-plane/internal/log" ) // ResourceNamer is an interface implemented by all xDS resources with a GetName() method. @@ -54,7 +53,31 @@ Resources []yaml.Node `yaml:"resources"` } -func (sm *SnapshotManager) LoadSnapshotFromFile(filePath string) (map[resourcev3.Type][]types.Resource, error) { +// unmarshalYamlNodeToProto takes a single YAML/JSON object (represented as a map[string]interface{}) +// and unmarshals it into the given Protobuf resource pointer using protojson. +func unmarshalYamlNodeToProto(node map[string]interface{}, resource types.Resource) error { + // 1. We must remove the standard Protobuf type marker (if present) before marshaling to JSON, + // as it can interfere with direct Go struct marshaling. + delete(node, "@type") + + // 2. Marshal the generic map into JSON bytes. + jsonBytes, err := json.Marshal(node) + if err != nil { + return fmt.Errorf("failed to marshal resource node to JSON: %w", err) + } + + // 3. Unmarshal the JSON bytes into the target Protobuf struct. + // protojson correctly handles field names (snake_case vs camelCase) and Any fields. + if err := protojson.Unmarshal(jsonBytes, resource); err != nil { + return fmt.Errorf("failed to unmarshal into proto: %w", err) + } + return nil +} + +func (sm *SnapshotManager) LoadSnapshotFromFile(context context.Context, filePath string) (map[resourcev3.Type][]types.Resource, error) { + log := internallog.LogFromContext(context) + + // Read the file data, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) @@ -85,30 +108,16 @@ case resourcev3.ListenerType: resource = &listenerv3.Listener{} newResource = true - case resourcev3.EndpointType: - resource = &endpointv3.ClusterLoadAssignment{} - newResource = true - case resourcev3.SecretType: - resource = &secretv3.Secret{} - newResource = true - case resourcev3.RuntimeType: - // resource = &runtimev3.Runtime{} // Placeholder, assuming it's correctly imported - // newResource = true + // ... other types ... default: + log.Warnf("unsupported resource type: %s", typ) // Skip nested or unsupported types } if newResource { - // Remove @type before unmarshalling - delete(v, "@type") - - jsonBytes, err := json.Marshal(v) - if err != nil { - return fmt.Errorf("failed to marshal resource node to JSON: %w", err) - } - - if err := protojson.Unmarshal(jsonBytes, resource); err != nil { - return fmt.Errorf("failed to unmarshal %s: %w", typ, err) + // *** REPLACED: json.Marshal and protojson.Unmarshal logic *** + if err := unmarshalYamlNodeToProto(v, resource); err != nil { + return fmt.Errorf("failed to unmarshal %s from file: %w", typ, err) } resources[typ] = append(resources[typ], resource) } @@ -138,6 +147,101 @@ return resources, nil } +// 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) { + log := internallog.LogFromContext(ctx) + + // 1. Unmarshal YAML into a generic Go map + var rawChainMap map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &rawChainMap); err != nil { + log.Errorf("Failed to unmarshal YAML: %v", err) + return nil, fmt.Errorf("failed to unmarshal YAML into generic map: %w", err) + } + if rawChainMap == nil { + return nil, fmt.Errorf("failed to unmarshal YAML: input was empty or invalid") + } + + // 2. Unmarshal the generic map into the Protobuf struct using the helper + rawChain := &listenerv3.FilterChain{} + if err := unmarshalYamlNodeToProto(rawChainMap, rawChain); err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML into FilterChain using protojson: %w", err) + } + + // Check if the FilterChain contains any filters (optional but good sanity check) + if len(rawChain.Filters) == 0 { + return nil, fmt.Errorf("filter chain loaded but contains no network filters") + } + + // Return the single FilterChain object. + return rawChain, nil +} + +// AppendFilterChainToListener loads the current listener from the cache, appends the provided +// FilterChain to its list of FilterChains, and updates the cache with the new snapshot. +func (sm *SnapshotManager) AppendFilterChainToListener(ctx context.Context, listenerName string, newFilterChain *listenerv3.FilterChain) error { + log := internallog.LogFromContext(ctx) + + // 1. Get the current Listener from the cache + resource, err := sm.GetResourceFromCache(listenerName, resourcev3.ListenerType) + if err != nil { + return fmt.Errorf("failed to get listener '%s' from cache: %w", listenerName, err) + } + + listener, ok := resource.(*listenerv3.Listener) + if !ok { + return fmt.Errorf("resource '%s' is not a Listener type", listenerName) + } + + // 2. Append the new FilterChain to the listener's list of filter chains. + // This is the core change: appending a *FilterChain, not just []*Filter. + listener.FilterChains = append(listener.FilterChains, newFilterChain) + log.Infof("Appended new filter chain (match: %v) to listener '%s'", newFilterChain.FilterChainMatch, listenerName) + + // 3. Create a new snapshot with the modified listener (rest of logic remains similar) + snap, err := sm.Cache.GetSnapshot(sm.NodeID) + if err != nil { + return fmt.Errorf("failed to get snapshot for modification: %w", err) + } + + // Get all current resources + resources := sm.getAllResourcesFromSnapshot(snap) + + // Replace the old listener with the modified one + listenerList, ok := resources[resourcev3.ListenerType] + if !ok { + return fmt.Errorf("listener resource type not present in snapshot") + } + + foundAndReplaced := false + for i, res := range listenerList { + if namer, ok := res.(interface{ GetName() string }); ok && namer.GetName() == listenerName { + listenerList[i] = listener + foundAndReplaced = true + break + } + } + + if !foundAndReplaced { + return fmt.Errorf("failed to locate listener '%s' in current resource list for replacement", listenerName) + } + + // Create and set the new snapshot + version := fmt.Sprintf("listener-update-%s-%d", listenerName, time.Now().UnixNano()) + newSnap, err := cachev3.NewSnapshot(version, resources) + if err != nil { + return fmt.Errorf("failed to create new snapshot: %w", err) + } + + if err := sm.Cache.SetSnapshot(ctx, sm.NodeID, newSnap); err != nil { + return fmt.Errorf("failed to set new snapshot: %w", err) + } + + log.Infof("Successfully updated listener '%s' in cache with new filter chain.", listenerName) + + return nil +} + // SetSnapshotFromConfig sets a snapshot from an aggregated SnapshotConfig func (sm *SnapshotManager) SetSnapshotFromConfig(ctx context.Context, version string, cfg *SnapshotConfig) error { if cfg == nil { diff --git a/internal/snapshot_test.go b/internal/snapshot_test.go new file mode 100644 index 0000000..2c35f96 --- /dev/null +++ b/internal/snapshot_test.go @@ -0,0 +1,199 @@ +package internal + +import ( + "context" + "testing" +) + +// NOTE: Assume MockLogger and SnapshotManager are defined for the test to run. +// The actual implementation of LoadFilterChainFromYAML is assumed to be available +// to the test file. + +// 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 + validComplexYAML := ` + filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: AUTO + upgrade_configs: + - upgrade_type: websocket + stream_idle_timeout: 0s + normalize_path: true + merge_slashes: true + route_config: + virtual_hosts: + - name: printer_service + domains: ["printer.jerxie.com"] + routes: + - match: { prefix: "/webcam" } + route: { prefix_rewrite: "/", cluster: "_3d_printer_camera", max_stream_duration: {grpc_timeout_header_max: 0s} } + - match: { prefix: "/" } + route: { cluster: "_3d_printer_console"} + http_filters: + - name: envoy.filters.http.oauth2 + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2 + config: + token_endpoint: + cluster: _auth_server + uri: auth.jerxie.com/token + timeout: 3s + authorization_endpoint: https://auth.jerxie.com/auth + redirect_uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/callback" + redirect_path_matcher: + path: + exact: /callback + signout_path: + path: + exact: /signout + forward_bearer_token: true + credentials: + client_id: octoprint-portal + token_secret: + name: token + sds_config: + path: "/etc/envoy/token-secret.yaml" + hmac_secret: + name: hmac + sds_config: + path: "/etc/envoy/hmac-secret.yaml" + auth_scopes: + - openid + - email + - name: envoy.filters.http.jwt_authn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + providers: + provider1: + remote_jwks: + http_uri: + uri: "https://auth.jerxie.com/keys" + cluster: _auth_server + timeout: 5s + cache_duration: 600s + from_headers: + - name: Authorization + value_prefix: "Bearer " + payload_in_metadata: jwt_payload + rules: + - match: + prefix: / + requires: + provider_name: provider1 + - name: envoy.filters.http.lua + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + inline_code: | + email = "" + function envoy_on_request(request_handle) + email = "" + local meta = request_handle:streamInfo():dynamicMetadata() + for key, value in pairs(meta:get("envoy.filters.http.jwt_authn")) do + if key == "jwt_payload" then + for k, v in pairs(value) do + if k == "email" then + print("login octoprint: "..v) + email = v + request_handle:headers():add("ENVOY_AUTHENTICATED_USER", v) + end + end + end + end + end + + function envoy_on_response(response_handle) + if email ~="" and email ~= "axieyangb@gmail.com" then + response_handle:logInfo("Got unauthorized user, return 403 for user " ..email) + response_handle:headers():add("set-cookie", "BearerToken=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") + response_handle:headers():add("set-cookie", "OauthHMAC=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") + response_handle:headers():add("set-cookie", "IdToken=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") + response_handle:headers():add("set-cookie", "OauthExpires=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT") + end + email = "" + end + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + filter_chain_match: + server_names: ["printer.jerxie.com", "printer.local"] + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + common_tls_context: + tls_certificates: + - certificate_chain: { filename: "/etc/certs/downstream/printer.jerxie.com/fullchain.pem" } + private_key: { filename: "/etc/certs/downstream/printer.jerxie.com/privkey.pem" } +` + + tests := []struct { + name string + yamlStr string + expectError bool + expectedLen int // Expected number of network filters (top-level filters array) + }{ + { + name: "Success_ComplexSingleFilterChain", + yamlStr: validComplexYAML, + expectError: false, + expectedLen: 1, // Only one top-level network filter: http_connection_manager + }, + // Re-include sanity checks for robust testing + { + name: "Error_NoFiltersInChain", + yamlStr: `filter_chain_match: { server_names: ["empty"] }`, + expectError: true, + expectedLen: 0, + }, + { + name: "Error_InputIsAList", + yamlStr: `- filters: []`, + expectError: true, // Should fail unmarshaling a list into a single struct + expectedLen: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + chain, err := sm.LoadFilterChainFromYAML(ctx, tt.yamlStr) + + if tt.expectError { + if err == nil { + t.Errorf("Expected an error but got nil") + } + if chain != nil { + t.Errorf("Expected nil chain on error, but got non-nil") + } + } else { + if err != nil { + t.Fatalf("Expected no error but got: %v", err) + } + if chain == nil { + t.Fatal("Expected non-nil filter chain, but got nil") + } + + // 1. Check top-level filter count + if len(chain.Filters) != tt.expectedLen { + t.Errorf("Top-level filter count mismatch. Got %d, want %d", len(chain.Filters), tt.expectedLen) + } + + // 2. Check a deeply nested value to ensure complex unmarshaling worked + if len(chain.FilterChainMatch.ServerNames) == 0 || chain.FilterChainMatch.ServerNames[0] != "printer.jerxie.com" { + t.Errorf("FilterChainMatch assertion failed. Expected server name 'printer.jerxie.com'") + } + + // 3. Check the name of the top-level filter + if chain.Filters[0].Name != "envoy.filters.network.http_connection_manager" { + t.Errorf("Top-level filter name mismatch. Got %s", chain.Filters[0].Name) + } + } + }) + } +} diff --git a/main.go b/main.go index c280210..dc2453d 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,6 @@ "github.com/envoyproxy/go-control-plane/pkg/cache/types" cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" - "github.com/envoyproxy/go-control-plane/pkg/log" resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/envoyproxy/go-control-plane/pkg/server/v3" "github.com/envoyproxy/go-control-plane/pkg/test/v3" @@ -22,10 +21,13 @@ "k8s.io/klog/v2" "envoy-control-plane/internal" + internallog "envoy-control-plane/internal/log" ) var ( - logger *log.DefaultLogger + // The logger variable should now be of the internal.Logger interface type + // to use the custom context functions. + logger internallog.Logger port uint nodeID string restPort uint @@ -36,7 +38,8 @@ ) func init() { - logger = log.NewDefaultLogger() + // Initialize the default logger (which implements the internal.Logger interface) + logger = internallog.NewDefaultLogger() klog.InitFlags(nil) flag.UintVar(&port, "port", 18000, "xDS management server port") @@ -55,9 +58,11 @@ return "sqlite3" } -// loadConfigFiles iterates over a directory and loads all .yaml/.json files -func loadConfigFiles(manager *internal.SnapshotManager, dir string) error { - logger.Infof("loading configuration files from directory: %s", dir) +// loadConfigFiles now accepts and uses a context +func loadConfigFiles(ctx context.Context, manager *internal.SnapshotManager, dir string) error { + log := internallog.LogFromContext(ctx) // Use the logger from context + + log.Infof("loading configuration files from directory: %s", dir) files, err := os.ReadDir(dir) if err != nil { @@ -72,20 +77,20 @@ fileName := file.Name() if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") || strings.HasSuffix(fileName, ".json") { filePath := filepath.Join(dir, fileName) - logger.Infof(" -> loading config file: %s", filePath) + log.Infof(" -> loading config file: %s", filePath) - rf, err := manager.LoadSnapshotFromFile(filePath) + rf, err := manager.LoadSnapshotFromFile(ctx, filePath) if err != nil { return fmt.Errorf("failed to load snapshot from file %s: %w", filePath, err) } for k, v := range rf { resourceFiles[k] = append(resourceFiles[k], v...) } - logger.Infof("loaded %d resources from %s", len(rf), filePath) + log.Infof("loaded %d resources from %s", len(rf), filePath) } } - if err := manager.SetSnapshot(context.TODO(), "snap-from-file", resourceFiles); err != nil { + if err := manager.SetSnapshot(ctx, "snap-from-file", resourceFiles); err != nil { return fmt.Errorf("failed to set combined snapshot from files: %w", err) } return nil @@ -93,6 +98,7 @@ // CORS is a middleware that sets the Access-Control-Allow-Origin header to * (all origins). func CORS(next http.Handler) http.Handler { + // ... (CORS implementation remains unchanged) ... return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Set CORS headers for all domains w.Header().Set("Access-Control-Allow-Origin", "*") @@ -113,6 +119,10 @@ flag.Parse() defer klog.Flush() + // 1. Create the root context and inject the logger + ctx := internallog.WithLogger(context.Background(), logger) + log := internallog.LogFromContext(ctx) // Now 'log' is the context-aware logger + // Default DB to SQLite file if none provided if dbConnStr == "" { defaultDBPath := "data/config.db" @@ -128,93 +138,106 @@ // --- Database initialization --- db, err := sql.Open(dbDriver, dbConnStr) if err != nil { - logger.Errorf("failed to connect to DB: %v", err) + log.Errorf("failed to connect to DB: %v", err) os.Exit(1) } defer db.Close() + // internal.NewStorage likely needs to be updated to accept a logger as well + // if its methods don't accept context, but we will pass context to its methods below. storage := internal.NewStorage(db, dbDriver) - if err := storage.InitSchema(context.Background()); err != nil { - logger.Errorf("failed to initialize DB schema: %v", err) + // Pass the context with the logger down + if err := storage.InitSchema(ctx); err != nil { + log.Errorf("failed to initialize DB schema: %v", err) os.Exit(1) } // Create snapshot cache and manager + // NOTE: The Envoy cachev3.NewSnapshotCache takes a `log.Logger` from go-control-plane, + // which is likely a different interface. For now, we continue to use the global 'logger' + // variable (which is an internal.Logger that wraps klog, matching the go-control-plane + // logger behavior you previously set up) as a bridge, since it was initialized + // to log.NewDefaultLogger(). cache := cachev3.NewSnapshotCache(false, cachev3.IDHash{}, logger) manager := internal.NewSnapshotManager(cache, nodeID, storage) loadedConfigs := false // Step 1: Try to load snapshot from DB - snapCfg, err := storage.RebuildSnapshot(context.Background()) + // Pass the context with the logger down + snapCfg, err := storage.RebuildSnapshot(ctx) if err == nil && len(snapCfg.EnabledClusters)+len(snapCfg.EnabledListeners) > 0 { - if err := manager.SetSnapshotFromConfig(context.Background(), "snap-from-db", snapCfg); err != nil { - logger.Errorf("failed to set DB snapshot: %v", err) + if err := manager.SetSnapshotFromConfig(ctx, "snap-from-db", snapCfg); err != nil { + log.Errorf("failed to set DB snapshot: %v", err) os.Exit(1) } loadedConfigs = true - logger.Infof("loaded snapshot from database") + log.Infof("loaded snapshot from database") } // Step 2: If DB empty, load from files and persist into DB if !loadedConfigs { if configDir != "" { - if err := loadConfigFiles(manager, configDir); err != nil { - logger.Errorf("failed to load configs from directory: %v", err) + // Pass the context with the logger down + if err := loadConfigFiles(ctx, manager, configDir); err != nil { + log.Errorf("failed to load configs from directory: %v", err) os.Exit(1) } loadedConfigs = true } else if snapshotFile != "" { if _, err := os.Stat(snapshotFile); err == nil { - resources, err := manager.LoadSnapshotFromFile(snapshotFile) + resources, err := manager.LoadSnapshotFromFile(ctx, snapshotFile) if err != nil { - logger.Errorf("failed to load snapshot from file: %v", err) + log.Errorf("failed to load snapshot from file: %v", err) os.Exit(1) } - if err := manager.SetSnapshot(context.TODO(), "snap-from-file", resources); err != nil { - logger.Errorf("failed to set loaded snapshot: %v", err) + if err := manager.SetSnapshot(ctx, "snap-from-file", resources); err != nil { + log.Errorf("failed to set loaded snapshot: %v", err) os.Exit(1) } loadedConfigs = true } else { - logger.Warnf("snapshot file not found: %s", snapshotFile) + log.Warnf("snapshot file not found: %s", snapshotFile) } } // Persist loaded snapshot into DB if loadedConfigs { - snapCfg, err := manager.SnapshotToConfig(context.Background(), nodeID) + // Pass the context with the logger down + snapCfg, err := manager.SnapshotToConfig(ctx, nodeID) if err != nil { - logger.Errorf("failed to convert snapshot to DB config: %v", err) + log.Errorf("failed to convert snapshot to DB config: %v", err) os.Exit(1) } - if err := storage.SaveSnapshot(context.Background(), snapCfg, internal.DeleteLogical); err != nil { - logger.Errorf("failed to save initial snapshot into DB: %v", err) + // Pass the context with the logger down + if err := storage.SaveSnapshot(ctx, snapCfg, internal.DeleteLogical); err != nil { + log.Errorf("failed to save initial snapshot into DB: %v", err) os.Exit(1) } - logger.Infof("initial snapshot written into database") + log.Infof("initial snapshot written into database") } } // Step 3: Ensure snapshot exists in cache snap, err := manager.Cache.GetSnapshot(nodeID) if err != nil || !loadedConfigs { - logger.Warnf("no valid snapshot found, creating empty snapshot") + log.Warnf("no valid snapshot found, creating empty snapshot") snap, _ = cachev3.NewSnapshot("snap-init", map[resourcev3.Type][]types.Resource{ resourcev3.ClusterType: {}, resourcev3.RouteType: {}, resourcev3.ListenerType: {}, }) - if err := cache.SetSnapshot(context.Background(), nodeID, snap); err != nil { - logger.Errorf("failed to set initial snapshot: %v", err) + // Pass the context with the logger down + if err := cache.SetSnapshot(ctx, nodeID, snap); err != nil { + log.Errorf("failed to set initial snapshot: %v", err) os.Exit(1) } } - logger.Infof("xDS snapshot ready: version %s", snap.GetVersion(string(resourcev3.ClusterType))) + log.Infof("xDS snapshot ready: version %s", snap.GetVersion(string(resourcev3.ClusterType))) // --- Start xDS gRPC server --- - ctx := context.Background() + // The root context with logger is used here cb := &test.Callbacks{Debug: true} srv := server.NewServer(ctx, cache, cb) go internal.RunServer(srv, port) @@ -222,6 +245,7 @@ // --- Start REST API server --- api := internal.NewAPI(manager) mux := http.NewServeMux() + // NOTE: If api.RegisterRoutes uses a context to log, it should be updated. api.RegisterRoutes(mux) // Wrap the main multiplexer with the CORS handler @@ -231,9 +255,9 @@ mux.Handle("/", http.FileServer(http.Dir("./static"))) // Assuming 'web' is the folder restAddr := fmt.Sprintf(":%d", restPort) - logger.Infof("starting REST API server on %s", restAddr) + log.Infof("starting REST API server on %s", restAddr) if err := http.ListenAndServe(restAddr, corsHandler); err != nil { // Use corsHandler - logger.Errorf("REST server error: %v", err) + log.Errorf("REST server error: %v", err) os.Exit(1) } }