diff --git a/Dockerfile b/Dockerfile index 7ee72b6..2c1b0e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,37 @@ +# syntax=docker/dockerfile:1.4 + FROM golang:1.24-alpine AS builder -# Install necessary runtime dependencies for static binaries if needed -# For standard Go apps, this is usually just ca-certificates for HTTPS/TLS. -RUN apk add --no-cache ca-certificates +# --- STAGE 1: Builder --- + +# 1. Install dependencies for CGO and runtime +# gcc and musl-dev are required on Alpine when CGO_ENABLED=1 (necessary for go-sqlite3). +# ca-certificates is needed for HTTPS/TLS connections during module download and runtime. +RUN apk add --no-cache ca-certificates gcc musl-dev # Set the working directory for the build WORKDIR /app # Copy the dependency files first for better build caching -# If go.mod/go.sum don't change, this layer is reused, speeding up subsequent builds. COPY go.mod go.sum ./ # Download all dependencies RUN go mod download -# Copy the rest of the source code, including internal/ +# Copy the rest of the source code +# NOTE: Ensure you have a .dockerignore file that excludes 'tempCodeRunnerFile.go' here COPY . . # Build the final executable -# CGO_ENABLED=0 creates a statically linked binary (no libc dependency). -# -ldflags="-s -w" strips debug information to minimize the binary size. -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o /xds-server . +# CGO_ENABLED=1 creates a statically linked binary. +RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o /xds-server . + +# 2. Clean up build dependencies +# Remove packages not needed for the final binary to keep the intermediate layer smaller. +RUN apk del gcc musl-dev # --- STAGE 2: Create the minimal runtime image --- -# Use a minimal base image, like alpine or gcr.io/distroless/static, for the final image. -# Alpine is a good choice as it includes basic shell commands (useful for debugging). FROM alpine:latest # Install ca-certificates again for the final image to handle HTTPS/TLS connections @@ -39,16 +45,16 @@ USER appuser # Copy the built binary from the 'builder' stage -# The binary is the only thing needed to run the Go application. COPY --from=builder --chown=appuser:appuser /xds-server /usr/local/bin/xds-server +# Copy the static assets (e.g., HTML files) and assign ownership to the non-root user. +# The source is the build context (EnvoyControlPlane/static/) and the destination is inside the container (/app/static/). +COPY --chown=appuser:appuser static/ /app/static/ + # Expose the ports for the xDS server (18000) and the REST API (8080) EXPOSE 18000 EXPOSE 8080 # Define the command to run the application -# We use the new flags to listen on all interfaces and point to a config directory. ENTRYPOINT ["/usr/local/bin/xds-server"] -# CMD is for default arguments. Here, we specify the default configuration to load. -# The container will run with nodeID 'proxy', listening on 18000/8080, and loading configs from the /configs directory inside the container. -CMD ["--nodeID", "proxy", "--config-dir", "/app/configs"] \ No newline at end of file +CMD ["--nodeID", "proxy", "--config-dir", "/app/configs"] diff --git a/docker-compose.yml b/docker-compose.yml index 48b0157..70f0e7e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,13 @@ restart: unless-stopped # Mount the named volume (defined below) to the /app directory in the container ports: - - "8888:8080" + - "8090:8080" - "18000:18000" volumes: + # Ensure this is mounted read/write for the container - data_volume:/app/data:rw - command: ["--nodeID", "home", "--config-dir", "app/data/config"] + command: ["--nodeID", "home", "--config-dir", "/app/data/config","--db","file:/app/data/data.db?_foreign_keys=on"] + # Define the volumes used by the services volumes: # Define a named volume for your code @@ -25,7 +27,8 @@ driver_opts: # Set the filesystem type to NFS type: "nfs" - # Set the NFS options, including the server address - o: "addr=192.168.68.90,rw" + # IMPORTANT: YOU MUST replace YOUR_UID and YOUR_GID below with the numeric IDs + # that own the data directory on your NFS server (e.g., 1000). + o: "addr=192.168.68.90,rw,nfsvers=4,uid=1026,gid=100" # Specify the remote path (device) on the NFS server to mount device: ":/volume1/docker/envoy-control-plane/data" \ No newline at end of file diff --git a/main.go b/main.go index d5b014e..738001d 100644 --- a/main.go +++ b/main.go @@ -24,194 +24,216 @@ ) var ( - logger *internal.DefaultLogger - port uint - nodeID string - restPort uint - snapshotFile string - configDir string - dbConnStr string - dbDriver string + logger *internal.DefaultLogger + port uint + nodeID string + restPort uint + snapshotFile string + configDir string + dbConnStr string + dbDriver string ) func init() { - logger = internal.NewDefaultLogger() - klog.InitFlags(nil) + logger = internal.NewDefaultLogger() + klog.InitFlags(nil) - flag.UintVar(&port, "port", 18000, "xDS management server port") - flag.StringVar(&nodeID, "nodeID", "test-id", "Node ID") - flag.UintVar(&restPort, "rest-port", 8080, "REST API server port") - flag.StringVar(&snapshotFile, "snapshot-file", "", "Optional initial snapshot JSON/YAML file") - flag.StringVar(&configDir, "config-dir", "data/", "Optional directory containing multiple config files") - flag.StringVar(&dbConnStr, "db", "", "Optional database connection string for config persistence") + flag.UintVar(&port, "port", 18000, "xDS management server port") + flag.StringVar(&nodeID, "nodeID", "test-id", "Node ID") + flag.UintVar(&restPort, "rest-port", 8080, "REST API server port") + flag.StringVar(&snapshotFile, "snapshot-file", "", "Optional initial snapshot JSON/YAML file") + flag.StringVar(&configDir, "config-dir", "data/", "Optional directory containing multiple config files") + flag.StringVar(&dbConnStr, "db", "", "Optional database connection string for config persistence") } // determineDriver returns driver name from connection string func determineDriver(dsn string) string { - if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { - return "postgres" - } - return "sqlite3" + if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { + return "postgres" + } + 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) + logger.Infof("loading configuration files from directory: %s", dir) - files, err := os.ReadDir(dir) - if err != nil { - return fmt.Errorf("failed to read directory %s: %w", dir, err) - } + files, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("failed to read directory %s: %w", dir, err) + } - resourceFiles := make(map[string][]types.Resource) - for _, file := range files { - if file.IsDir() { - continue - } - 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) + resourceFiles := make(map[string][]types.Resource) + for _, file := range files { + if file.IsDir() { + continue + } + 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) - rf, err := manager.LoadSnapshotFromFile(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) - } - } + rf, err := manager.LoadSnapshotFromFile(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) + } + } - if err := manager.SetSnapshot(context.TODO(), "snap-from-file", resourceFiles); err != nil { - return fmt.Errorf("failed to set combined snapshot from files: %w", err) - } - return nil + if err := manager.SetSnapshot(context.TODO(), "snap-from-file", resourceFiles); err != nil { + return fmt.Errorf("failed to set combined snapshot from files: %w", err) + } + return nil } +// CORS is a middleware that sets the Access-Control-Allow-Origin header to * (all origins). +func CORS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CORS headers for all domains + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With") + + // Handle preflight requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} + + func main() { - flag.Parse() - defer klog.Flush() + flag.Parse() + defer klog.Flush() - // Default DB to SQLite file if none provided - if dbConnStr == "" { - defaultDBPath := "data/config.db" - if err := os.MkdirAll(filepath.Dir(defaultDBPath), 0755); err != nil { - fmt.Fprintf(os.Stderr, "failed to create data directory: %v\n", err) - os.Exit(1) - } - dbConnStr = fmt.Sprintf("file:%s?_foreign_keys=on", defaultDBPath) - dbDriver = "sqlite3" - } else { - dbDriver = determineDriver(dbConnStr) - } - // --- Database initialization --- - db, err := sql.Open(dbDriver, dbConnStr) - if err != nil { - logger.Errorf("failed to connect to DB: %v", err) - os.Exit(1) - } - defer db.Close() + // Default DB to SQLite file if none provided + if dbConnStr == "" { + defaultDBPath := "data/config.db" + if err := os.MkdirAll(filepath.Dir(defaultDBPath), 0755); err != nil { + fmt.Fprintf(os.Stderr, "failed to create data directory: %v\n", err) + os.Exit(1) + } + dbConnStr = fmt.Sprintf("file:%s?_foreign_keys=on", defaultDBPath) + dbDriver = "sqlite3" + } else { + dbDriver = determineDriver(dbConnStr) + } + // --- Database initialization --- + db, err := sql.Open(dbDriver, dbConnStr) + if err != nil { + logger.Errorf("failed to connect to DB: %v", err) + os.Exit(1) + } + defer db.Close() - storage := internal.NewStorage(db, dbDriver) - if err := storage.InitSchema(context.Background()); err != nil { - logger.Errorf("failed to initialize DB schema: %v", err) - os.Exit(1) - } + storage := internal.NewStorage(db, dbDriver) + if err := storage.InitSchema(context.Background()); err != nil { + logger.Errorf("failed to initialize DB schema: %v", err) + os.Exit(1) + } - // Create snapshot cache and manager - cache := cachev3.NewSnapshotCache(false, cachev3.IDHash{}, logger) - manager := internal.NewSnapshotManager(cache, nodeID, storage) + // Create snapshot cache and manager + cache := cachev3.NewSnapshotCache(false, cachev3.IDHash{}, logger) + manager := internal.NewSnapshotManager(cache, nodeID, storage) - loadedConfigs := false + loadedConfigs := false - // Step 1: Try to load snapshot from DB - snapCfg, err := storage.RebuildSnapshot(context.Background()) - 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) - os.Exit(1) - } - loadedConfigs = true - logger.Infof("loaded snapshot from database") - } + // Step 1: Try to load snapshot from DB + snapCfg, err := storage.RebuildSnapshot(context.Background()) + 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) + os.Exit(1) + } + loadedConfigs = true + logger.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) - os.Exit(1) - } - loadedConfigs = true - } else if snapshotFile != "" { - if _, err := os.Stat(snapshotFile); err == nil { - resources, err := manager.LoadSnapshotFromFile(snapshotFile) - if err != nil { - logger.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) - os.Exit(1) - } - loadedConfigs = true - } else { - logger.Warnf("snapshot file not found: %s", snapshotFile) - } - } + // 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) + os.Exit(1) + } + loadedConfigs = true + } else if snapshotFile != "" { + if _, err := os.Stat(snapshotFile); err == nil { + resources, err := manager.LoadSnapshotFromFile(snapshotFile) + if err != nil { + logger.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) + os.Exit(1) + } + loadedConfigs = true + } else { + logger.Warnf("snapshot file not found: %s", snapshotFile) + } + } - // Persist loaded snapshot into DB - if loadedConfigs { - snapCfg, err := manager.SnapshotToConfig(context.Background(), nodeID) - if err != nil { - logger.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) - os.Exit(1) - } - logger.Infof("initial snapshot written into database") - } - } + // Persist loaded snapshot into DB + if loadedConfigs { + snapCfg, err := manager.SnapshotToConfig(context.Background(), nodeID) + if err != nil { + logger.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) + os.Exit(1) + } + logger.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") - 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) - os.Exit(1) - } - } + // 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") + 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) + os.Exit(1) + } + } - logger.Infof("xDS snapshot ready: version %s", snap.GetVersion(string(resourcev3.ClusterType))) + logger.Infof("xDS snapshot ready: version %s", snap.GetVersion(string(resourcev3.ClusterType))) - // --- Start xDS gRPC server --- - ctx := context.Background() - cb := &test.Callbacks{Debug: true} - srv := server.NewServer(ctx, cache, cb) - go internal.RunServer(srv, port) + // --- Start xDS gRPC server --- + ctx := context.Background() + cb := &test.Callbacks{Debug: true} + srv := server.NewServer(ctx, cache, cb) + go internal.RunServer(srv, port) - // --- Start REST API server --- - api := internal.NewAPI(manager) - mux := http.NewServeMux() - api.RegisterRoutes(mux) + // --- Start REST API server --- + api := internal.NewAPI(manager) + mux := http.NewServeMux() + api.RegisterRoutes(mux) - // NEW: Serve the index.html file and any other static assets - mux.Handle("/", http.FileServer(http.Dir("./static"))) // Assuming 'web' is the folder + // Wrap the main multiplexer with the CORS handler + corsHandler := CORS(mux) - restAddr := fmt.Sprintf(":%d", restPort) - logger.Infof("starting REST API server on %s", restAddr) - if err := http.ListenAndServe(restAddr, mux); err != nil { - logger.Errorf("REST server error: %v", err) - os.Exit(1) - } -} + // NEW: Serve the index.html file and any other static assets + 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) + if err := http.ListenAndServe(restAddr, corsHandler); err != nil { // Use corsHandler + logger.Errorf("REST server error: %v", err) + os.Exit(1) + } +} \ No newline at end of file diff --git a/static/script.js b/static/script.js index 80cb89c..3bc4419 100644 --- a/static/script.js +++ b/static/script.js @@ -1,5 +1,8 @@ -// --- Configuration --- -const API_BASE_URL = 'http://localhost:8080'; +// Step 1: Initialize the variable with the current full URL +let API_BASE_URL = window.location.href; + +// Step 2: Trim the last slash if it exists and reassign to the variable +API_BASE_URL = API_BASE_URL.replace(/\/$/, ""); // ========================================================================= // GLOBAL IN-MEMORY STORE