// internal/app/app.go package app import ( "context" "database/sql" "fmt" "net/http" "os" "path/filepath" "strings" "github.com/envoyproxy/go-control-plane/pkg/cache/types" cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/envoyproxy/go-control-plane/pkg/server/v3" "github.com/envoyproxy/go-control-plane/pkg/test/v3" "envoy-control-plane/internal" "envoy-control-plane/internal/config" internallog "envoy-control-plane/internal/log" "envoy-control-plane/internal/pkg/api" "envoy-control-plane/internal/pkg/cert" rtspserver "envoy-control-plane/internal/pkg/server" "envoy-control-plane/internal/pkg/snapshot" internalstorage "envoy-control-plane/internal/pkg/storage" ) // loadConfigFiles has been moved or recreated to accept and use a context // It is the same logic extracted from your original main.go func loadConfigFiles(ctx context.Context, dir string) (map[string][]types.Resource, error) { log := internallog.LogFromContext(ctx) log.Infof("loading configuration files from directory: %s", dir) files, err := os.ReadDir(dir) if err != nil { return nil, 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) log.Infof(" -> loading config file: %s", filePath) rf, err := snapshot.LoadSnapshotFromFile(ctx, filePath) if err != nil { return nil, fmt.Errorf("failed to load snapshot from file %s: %w", filePath, err) } for k, v := range rf { resourceFiles[k] = append(resourceFiles[k], v...) } log.Infof("loaded %d resources from %s", len(rf), filePath) } } return resourceFiles, nil } // loadInitialConfig attempts to load the configuration from DB, config dir, or snapshot file. // It also ensures the loaded config is saved to the DB if it came from files. func loadInitialConfig( ctx context.Context, manager *snapshot.SnapshotManager, storage *internalstorage.Storage, cfg *config.Config, ) error { log := internallog.LogFromContext(ctx) loadedConfigs := false // --- 1. Attempt to load from Database --- snapCfg, err := storage.RebuildSnapshot(ctx) if err == nil && (len(snapCfg.EnabledClusters) > 0 || len(snapCfg.EnabledListeners) > 0) { if err := manager.SetSnapshotFromConfig(ctx, "snap-from-db", snapCfg); err != nil { return fmt.Errorf("failed to set DB snapshot: %w", err) } loadedConfigs = true log.Infof("loaded snapshot from database") } if !loadedConfigs { var resources map[string][]types.Resource snapSource := "" // --- 2. Attempt to load from Config Directory --- if cfg.ConfigDir != "" { resources, err = loadConfigFiles(ctx, cfg.ConfigDir) if err != nil { return fmt.Errorf("failed to load configs from directory: %w", err) } snapSource = "snap-from-dir" loadedConfigs = len(resources) > 0 if loadedConfigs { log.Infof("loaded snapshot from directory: %s", cfg.ConfigDir) } } // --- 3. Attempt to load from Snapshot File (only if dir didn't load any) --- if !loadedConfigs && cfg.SnapshotFile != "" { if _, err := os.Stat(cfg.SnapshotFile); err == nil { resources, err = snapshot.LoadSnapshotFromFile(ctx, cfg.SnapshotFile) if err != nil { return fmt.Errorf("failed to load snapshot from file: %w", err) } snapSource = "snap-from-file" loadedConfigs = len(resources) > 0 if loadedConfigs { log.Infof("loaded snapshot from file: %s", cfg.SnapshotFile) } } else { log.Warnf("snapshot file not found: %s", cfg.SnapshotFile) } } // --- 4. Apply and Save Loaded Config (if any) --- if loadedConfigs { if err := manager.SetSnapshot(ctx, snapSource, resources); err != nil { return fmt.Errorf("failed to set loaded snapshot: %w", err) } // Save the newly loaded snapshot to the DB for persistence snapCfgToSave, err := manager.SnapshotToConfig(ctx, cfg.NodeID) if err != nil { return fmt.Errorf("failed to convert snapshot to DB config: %w", err) } if err := storage.SaveSnapshot(ctx, snapCfgToSave, internalstorage.DeleteLogical); err != nil { return fmt.Errorf("failed to save initial snapshot into DB: %w", err) } log.Infof("initial snapshot written into database from %s", snapSource) } } // --- 5. Ensure an initial snapshot exists (even an empty one) --- snap, err := manager.Cache.GetSnapshot(cfg.NodeID) if err != nil || !loadedConfigs { 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: {}, // Add other resource types if needed (e.g., resourcev3.SecretType) }) if err := manager.Cache.SetSnapshot(ctx, cfg.NodeID, snap); err != nil { return fmt.Errorf("failed to set initial empty snapshot: %w", err) } } log.Infof("xDS snapshot ready: version %s", snap.GetVersion(string(resourcev3.ClusterType))) return nil } // Run encapsulates the entire application startup logic. func Run(ctx context.Context) error { log := internallog.LogFromContext(ctx) cfg := config.GetConfig() // Use the globally available config // 1. Conditional Certificate Issuance (Non-blocking) cert.RunCertIssuance(ctx) // 2. Database Initialization dbConnStr, dbDriver, err := internalstorage.SetupDBConnection(ctx, cfg.DBConnStr) if err != nil { return fmt.Errorf("database setup failed: %w", err) } db, err := sql.Open(dbDriver, dbConnStr) if err != nil { return fmt.Errorf("failed to connect to DB: %w", err) } defer db.Close() // 3. Storage and Snapshot Manager Setup storage := internalstorage.NewStorage(db, dbDriver) if err := storage.InitSchema(ctx); err != nil { return fmt.Errorf("failed to initialize DB schema: %w", err) } cache := cachev3.NewSnapshotCache(false, cachev3.IDHash{}, log) manager := snapshot.NewSnapshotManager(cache, cfg.NodeID, storage) // 4. Load Initial Configuration (DB, Dir, or File) if err := loadInitialConfig(ctx, manager, storage, cfg); err != nil { return fmt.Errorf("failed to load initial configuration: %w", err) } // 5. Start xDS gRPC Server cb := &test.Callbacks{Debug: true} srv := server.NewServer(ctx, cache, cb) go internal.RunServer(srv, cfg.Port) // Assuming internal.RunServer is correct // 6. Start REST API Server mux := http.NewServeMux() api.RegisterRoutes(mux, manager, cfg.EnableCertIssuance, cfg.WebrootPath) // Pass needed dependencies return rtspserver.RunRESTServer(ctx, mux, cfg.RESTPort, cfg.WebrootPath, cfg.EnableCertIssuance) }