From 8f966bcd4fa7dab8935205e94ae4fd95a35fee67 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 19 Jan 2026 12:59:19 +0530 Subject: [PATCH 1/3] Clean up shared package dependencies + env vars in PDP --- .github/workflows/integration-tests.yml | 11 +- .../.choreo/component.yaml | 45 --- exchange/policy-decision-point/Dockerfile | 14 +- exchange/policy-decision-point/README.md | 66 ++-- exchange/policy-decision-point/go.mod | 7 - .../{shared => internal}/config/config.go | 103 +++--- .../internal/utils/utils.go | 160 +++++++++ exchange/policy-decision-point/main.go | 82 +++-- .../shared/config/go.mod | 3 - .../shared/constants/constants.go | 8 - .../shared/constants/go.mod | 3 - .../policy-decision-point/shared/utils/go.mod | 3 - .../shared/utils/utils.go | 320 ------------------ exchange/policy-decision-point/v1/database.go | 28 +- .../policy-decision-point/v1/database_test.go | 58 ++-- exchange/policy-decision-point/v1/handler.go | 2 +- .../policy-decision-point/v1/test_helpers.go | 30 +- 17 files changed, 367 insertions(+), 576 deletions(-) delete mode 100644 exchange/policy-decision-point/.choreo/component.yaml rename exchange/policy-decision-point/{shared => internal}/config/config.go (56%) create mode 100644 exchange/policy-decision-point/internal/utils/utils.go delete mode 100644 exchange/policy-decision-point/shared/config/go.mod delete mode 100644 exchange/policy-decision-point/shared/constants/constants.go delete mode 100644 exchange/policy-decision-point/shared/constants/go.mod delete mode 100644 exchange/policy-decision-point/shared/utils/go.mod delete mode 100644 exchange/policy-decision-point/shared/utils/utils.go diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 493e208d..4e61e399 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -112,15 +112,14 @@ jobs: mkdir -p logs echo "🚀 Starting Policy Decision Point..." - # Running with env vars mapping to what the code expects (based on docker-compose.test.yml analysis) PORT=8082 \ LOG_LEVEL=debug \ + DB_HOST=localhost \ + DB_PORT=5433 \ + DB_USERNAME=postgres \ + DB_PASSWORD=password \ + DB_NAME=policy_db \ DB_SSLMODE=disable \ - CHOREO_OPENDIF_DATABASE_HOSTNAME=localhost \ - CHOREO_OPENDIF_DATABASE_PORT=5433 \ - CHOREO_OPENDIF_DATABASE_USERNAME=postgres \ - CHOREO_OPENDIF_DATABASE_PASSWORD=password \ - CHOREO_OPENDIF_DATABASE_DATABASENAME=policy_db \ RUN_MIGRATION=true \ ./bin/policy-decision-point > logs/pdp.log 2>&1 & PDP_PID=$! diff --git a/exchange/policy-decision-point/.choreo/component.yaml b/exchange/policy-decision-point/.choreo/component.yaml deleted file mode 100644 index f5ca52d8..00000000 --- a/exchange/policy-decision-point/.choreo/component.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# +required The configuration file schema version -schemaVersion: 1.2 - -# +optional Incoming connection details for the component -endpoints: - # REST API Endpoint for Policy Decision Point - - name: policy-decision-point - displayName: Policy Decision Point - service: - # Base path for the REST API endpoint - basePath: / - port: 8082 - # +required Type of traffic that the endpoint is accepting. - # Allowed values: REST, GraphQL, WS, GRPC, TCP, UDP. - type: REST - # Network visibility: Public for external API access - networkVisibilities: - - Public - - Organization - # +optional Path to the schema definition file. Defaults to wild card route if not provided - # This is only applicable to REST or WS endpoint types. - # The path should be relative to the docker context. - schemaFilePath: openapi.yaml - -dependencies: - connectionReferences: - - name: OpenDIF Database - resourceRef: database:opendif-db/testdb2 - -configurations: - # +optional List of environment variables to be injected into the component. - env: - - name: PORT - # +required value source - # Allowed value sources: connectionRef, configForm - valueFrom: - # +required config form value source - configForm: - # +optional display name inside the config form, name will be shown in config form if not specified - displayName: Port - # +optional default value is true if not specified - required: true - # +optional default value is string if not specified - # Allowed types - string, number, boolean, secret - type: number \ No newline at end of file diff --git a/exchange/policy-decision-point/Dockerfile b/exchange/policy-decision-point/Dockerfile index 0f0cf100..2fd8c9bc 100644 --- a/exchange/policy-decision-point/Dockerfile +++ b/exchange/policy-decision-point/Dockerfile @@ -13,16 +13,16 @@ ARG GIT_COMMIT RUN apk add --no-cache git ca-certificates tzdata # Copy go mod files and source code -COPY exchange/policy-decision-point/go.mod exchange/policy-decision-point/go.sum ./ -COPY exchange/policy-decision-point/ . - -# Copy shared packages from exchange/shared -COPY exchange/shared/ /shared/ +COPY exchange/policy-decision-point/go.mod exchange/policy-decision-point/go.sum ./exchange/policy-decision-point/ +COPY exchange/policy-decision-point/ ./exchange/policy-decision-point/ -# Download dependencies +WORKDIR /app/exchange/policy-decision-point/ RUN go mod download -# Build the application with build info +# Copy source code +COPY exchange/policy-decision-point/ . + +# Build the application with build info from policy-decision-point directory RUN CGO_ENABLED=0 GOOS=linux go build \ -ldflags="-w -s -X main.Version=${BUILD_VERSION} -X main.BuildTime=${BUILD_TIME} -X main.GitCommit=${GIT_COMMIT}" \ -o /app/service_binary . diff --git a/exchange/policy-decision-point/README.md b/exchange/policy-decision-point/README.md index 208913de..73fa5ca3 100644 --- a/exchange/policy-decision-point/README.md +++ b/exchange/policy-decision-point/README.md @@ -22,7 +22,7 @@ The PDP provides attribute-based access control (ABAC) with field-level permissi ### Prerequisites -- Go 1.21+ +- Go 1.24+ - PostgreSQL 13+ ### Run the Service @@ -31,6 +31,16 @@ The PDP provides attribute-based access control (ABAC) with field-level permissi # Install dependencies go mod download +# Copy environment template +cp .env.template .env + +# Edit .env with your database configuration +# DB_HOST=localhost +# DB_PORT=5432 +# DB_USERNAME=postgres +# DB_PASSWORD=password +# DB_NAME=pdp + # Run locally go run main.go @@ -45,21 +55,26 @@ The service runs on port 8082 by default. ### Environment Variables +All configuration is done via environment variables. See `.env.template` for a complete list. + +| Variable | Description | Default | +|----------|-------------|---------| +| `PORT` | Service port | `8082` | +| `ENVIRONMENT` | `production` or `local` | `local` | +| `IDP_ORG_NAME` | IDP organization name | - | +| `IDP_ISSUER` | JWT issuer URL | - | +| `IDP_AUDIENCE` | JWT audience | - | +| `IDP_JWKS_URL` | JWKS endpoint URL | - | +| `DB_HOST` | Database host | `localhost` | +| `DB_PORT` | Database port | `5432` | +| `DB_USERNAME` | Database username | `postgres` | +| `DB_PASSWORD` | Database password | - | +| `DB_NAME` | Database name | `pdp` | +| `DB_SSLMODE` | SSL mode | `require` | + +**Optional:** ```bash -# Database Configuration (Choreo) -CHOREO_DB_PDP_HOSTNAME=your-db-host -CHOREO_DB_PDP_PORT=your-db-port -CHOREO_DB_PDP_USERNAME=your-db-username -CHOREO_DB_PDP_PASSWORD=your-db-password -CHOREO_DB_PDP_DATABASENAME=your-db-name - -# Or use standard DB variables -DB_HOST=localhost -DB_PORT=5432 -DB_USER=postgres -DB_PASSWORD=your_password -DB_NAME=pdp -DB_SSLMODE=disable +RUN_MIGRATION=false # Set to "true" to run migrations on startup ``` ## API Endpoints @@ -70,6 +85,8 @@ DB_SSLMODE=disable | `/api/v1/policy/metadata` | POST | Create policy metadata for fields | | `/api/v1/policy/update-allowlist` | POST | Update allow list for applications | | `/health` | GET | Health check | +| `/debug` | GET | Debug information | +| `/debug/db` | GET | Database connection status | ### Authorization Request @@ -118,7 +135,13 @@ DB_SSLMODE=disable ```json { "schema_id": "schema-123", - "sdl": "type Person { fullName: String }" + "records": [ + { + "field_name": "person.fullName", + "display_name": "Full Name", + "access_control_type": "public" + } + ] } ``` @@ -176,9 +199,13 @@ go test ./... -cover curl -X POST http://localhost:8082/api/v1/policy/decide \ -H "Content-Type: application/json" \ -d '{ - "consumer_id": "passport-app", - "app_id": "passport-app", - "required_fields": ["person.fullName"] + "applicationId": "passport-app", + "requiredFields": [ + { + "fieldName": "person.fullName", + "schemaId": "schema-123" + } + ] }' ``` @@ -188,6 +215,7 @@ curl -X POST http://localhost:8082/api/v1/policy/decide \ **`policy_metadata` Table:** - `id` (UUID) - Primary key +- `schema_id` (TEXT) - Schema identifier - `field_name` (TEXT) - Data field name - `display_name` (TEXT) - Human-readable name - `access_control_type` (ENUM) - public/restricted diff --git a/exchange/policy-decision-point/go.mod b/exchange/policy-decision-point/go.mod index fcf256b8..bc0224de 100644 --- a/exchange/policy-decision-point/go.mod +++ b/exchange/policy-decision-point/go.mod @@ -4,7 +4,6 @@ go 1.24.6 require ( github.com/google/uuid v1.6.0 - github.com/gov-dx-sandbox/exchange/shared/utils v0.0.0 github.com/joho/godotenv v1.5.1 github.com/stretchr/testify v1.10.0 gorm.io/driver/postgres v1.6.0 @@ -29,9 +28,3 @@ require ( golang.org/x/text v0.27.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/gov-dx-sandbox/exchange/shared/config => ./shared/config - -replace github.com/gov-dx-sandbox/exchange/shared/constants => ./shared/constants - -replace github.com/gov-dx-sandbox/exchange/shared/utils => ./shared/utils diff --git a/exchange/policy-decision-point/shared/config/config.go b/exchange/policy-decision-point/internal/config/config.go similarity index 56% rename from exchange/policy-decision-point/shared/config/config.go rename to exchange/policy-decision-point/internal/config/config.go index ab866cdd..8413bd21 100644 --- a/exchange/policy-decision-point/shared/config/config.go +++ b/exchange/policy-decision-point/internal/config/config.go @@ -3,8 +3,9 @@ package config import ( "flag" - "os" "time" + + "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/utils" ) // Config holds all configuration for a service @@ -13,6 +14,8 @@ type Config struct { Service ServiceConfig Logging LoggingConfig Security SecurityConfig + IDPConfig IDPConfig + DBConfigs DBConfigs } // ServiceConfig holds service-specific configuration @@ -31,29 +34,60 @@ type LoggingConfig struct { // SecurityConfig holds security configuration type SecurityConfig struct { - JWTSecret string EnableCORS bool RateLimit int } +// IDPConfig holds IDP configuration +type IDPConfig struct { + Issuer string + JwksUrl string + Audience string + OrgName string +} + +// DBConfigs holds database configuration +type DBConfigs struct { + Host string + Port string + Username string + Password string + Database string + SSLMode string +} + // LoadConfig loads configuration from flags and environment variables func LoadConfig(serviceName string) *Config { // Get environment first to determine defaults - env := getEnvOrDefault("ENVIRONMENT", "local") + env := utils.GetEnvOrDefault("ENVIRONMENT", "local") // Define flags envFlag := flag.String("env", env, "Environment: local or production") - port := flag.String("port", getDefaultPort(serviceName), "Service port") - host := flag.String("host", getEnvOrDefault("HOST", "0.0.0.0"), "Host address") + port := flag.String("port", utils.GetEnvOrDefault("PORT", "8082"), "Service port") + host := flag.String("host", utils.GetEnvOrDefault("HOST", "0.0.0.0"), "Host address") timeout := flag.Duration("timeout", 10*time.Second, "Request timeout") logLevel := flag.String("log-level", getDefaultLogLevel(env), "Log level") logFormat := flag.String("log-format", getDefaultLogFormat(env), "Log format") - jwtSecret := flag.String("jwt-secret", getDefaultJWTSecret(env), "JWT secret") enableCORS := flag.Bool("cors", getDefaultCORS(env), "Enable CORS") rateLimit := flag.Int("rate-limit", getDefaultRateLimit(env), "Rate limit per minute") + // Parse flags flag.Parse() + // Reading IDP Configs + orgName := utils.GetEnvOrDefault("IDP_ORG_NAME", "YOUR_ORG_NAME") + userIssuer := utils.GetEnvOrDefault("IDP_ISSUER", "https://api.asgardeo.io/t/"+orgName+"/oauth2/token") + userAudience := utils.GetEnvOrDefault("IDP_AUDIENCE", "YOUR_AUDIENCE") + userJwksURL := utils.GetEnvOrDefault("IDP_JWKS_URL", "https://api.asgardeo.io/t/"+orgName+"/oauth2/jwks") + + // Reading DB Configs + dbHost := utils.GetEnvOrDefault("DB_HOST", "localhost") + dbPort := utils.GetEnvOrDefault("DB_PORT", "5432") + dbUsername := utils.GetEnvOrDefault("DB_USERNAME", "postgres") + dbPassword := utils.GetEnvOrDefault("DB_PASSWORD", "") + dbName := utils.GetEnvOrDefault("DB_NAME", "pdp") + dbSslMode := utils.GetEnvOrDefault("DB_SSLMODE", "require") + // Use flag value if provided, otherwise use environment default finalEnv := *envFlag @@ -70,39 +104,28 @@ func LoadConfig(serviceName string) *Config { Format: *logFormat, }, Security: SecurityConfig{ - JWTSecret: *jwtSecret, EnableCORS: *enableCORS, RateLimit: *rateLimit, }, + IDPConfig: IDPConfig{ + Issuer: userIssuer, + JwksUrl: userJwksURL, + Audience: userAudience, + OrgName: orgName, + }, + DBConfigs: DBConfigs{ + Host: dbHost, + Port: dbPort, + Username: dbUsername, + Password: dbPassword, + Database: dbName, + SSLMode: dbSslMode, + }, } - // Validate configuration - validateConfig(config) - return config } -// Helper functions -func getEnvOrDefault(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} - -func getDefaultPort(serviceName string) string { - ports := map[string]string{ - "consent-engine": "8081", - "policy-decision-point": "8082", - "orchestration-engine": "4000", - } - if port, exists := ports[serviceName]; exists { - return port - } - // Fallback to environment variable or default - return getEnvOrDefault("PORT", "8081") -} - func getDefaultLogLevel(env string) string { if env == "production" { return "warn" @@ -117,14 +140,6 @@ func getDefaultLogFormat(env string) string { return "text" } -func getDefaultJWTSecret(env string) string { - if env == "production" { - // In production, require JWT secret to be set via environment variable - return getEnvOrDefault("JWT_SECRET", "") - } - return "local-secret-key" -} - func getDefaultCORS(env string) bool { return env != "production" } @@ -135,13 +150,3 @@ func getDefaultRateLimit(env string) int { } return 1000 } - -// validateConfig validates the configuration and logs warnings for production -func validateConfig(cfg *Config) { - if cfg.Environment == "production" { - if cfg.Security.JWTSecret == "" { - // Log warning but don't fail - let the service handle it - // This allows for graceful degradation - } - } -} diff --git a/exchange/policy-decision-point/internal/utils/utils.go b/exchange/policy-decision-point/internal/utils/utils.go new file mode 100644 index 00000000..7a21fcb7 --- /dev/null +++ b/exchange/policy-decision-point/internal/utils/utils.go @@ -0,0 +1,160 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" +) + +// ErrorResponse represents a standard error response structure +type ErrorResponse struct { + Error string `json:"error"` +} + +// RespondWithJSON sends a JSON response with the given status code and data +func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + + if err := json.NewEncoder(w).Encode(data); err != nil { + slog.Error("Failed to encode JSON response", "error", err) + } +} + +// RespondWithError sends a JSON error response +func RespondWithError(w http.ResponseWriter, statusCode int, message string) { + RespondWithJSON(w, statusCode, ErrorResponse{Error: message}) +} + +// RespondWithSuccess sends a JSON success response +func RespondWithSuccess(w http.ResponseWriter, statusCode int, data interface{}) { + RespondWithJSON(w, statusCode, data) +} + +// HealthHandler creates a health check handler +func HealthHandler(serviceName string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + response := map[string]string{ + "service": serviceName, + "status": "healthy", + } + RespondWithJSON(w, http.StatusOK, response) + } +} + +// PanicRecoveryMiddleware provides panic recovery for HTTP handlers +func PanicRecoveryMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + slog.Error("Handler panicked", "error", err, "path", r.URL.Path) + RespondWithError(w, http.StatusInternalServerError, "Internal server error") + } + }() + next.ServeHTTP(w, r) + }) +} + +// ServerConfig holds configuration for HTTP servers +type ServerConfig struct { + Port string + ReadTimeout time.Duration + WriteTimeout time.Duration + IdleTimeout time.Duration +} + +// DefaultServerConfig returns a default server configuration +func DefaultServerConfig() *ServerConfig { + return &ServerConfig{ + Port: GetEnvOrDefault("PORT", "8080"), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + } +} + +// StartServerWithGracefulShutdown starts an HTTP server with graceful shutdown +func StartServerWithGracefulShutdown(server *http.Server, serviceName string) error { + // Graceful shutdown + go func() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + slog.Info("Shutting down server...", "service", serviceName) + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer shutdownCancel() + + if err := server.Shutdown(shutdownCtx); err != nil { + slog.Error("Server shutdown error", "error", err, "service", serviceName) + } + }() + + slog.Info("Server starting", "service", serviceName, "address", server.Addr) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slog.Error("Server failed to start", "error", err, "service", serviceName) + return err + } + return nil +} + +// CreateServer creates an HTTP server with the given configuration +func CreateServer(config *ServerConfig, handler http.Handler) *http.Server { + return &http.Server{ + Addr: fmt.Sprintf(":%s", config.Port), + Handler: handler, + ReadTimeout: config.ReadTimeout, + WriteTimeout: config.WriteTimeout, + IdleTimeout: config.IdleTimeout, + } +} + +// GetEnvOrDefault returns the environment variable value or a default +func GetEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// SetupLogging configures logging based on the configuration +func SetupLogging(format, level string) { + var handler slog.Handler + + switch format { + case "json": + handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: getLogLevel(level), + }) + default: + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: getLogLevel(level), + }) + } + + slog.SetDefault(slog.New(handler)) +} + +// getLogLevel converts string level to slog.Level +func getLogLevel(level string) slog.Level { + switch strings.ToLower(level) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn", "warning": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + diff --git a/exchange/policy-decision-point/main.go b/exchange/policy-decision-point/main.go index 958fbee5..8d30de64 100644 --- a/exchange/policy-decision-point/main.go +++ b/exchange/policy-decision-point/main.go @@ -7,8 +7,9 @@ import ( "os" "time" + "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/config" + "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/utils" v1 "github.com/gov-dx-sandbox/exchange/policy-decision-point/v1" - "github.com/gov-dx-sandbox/exchange/shared/utils" "github.com/joho/godotenv" ) @@ -23,30 +24,63 @@ func main() { // Load .env file if it exists (optional - fails silently if not found) _ = godotenv.Load() + // Load configuration using flags + cfg := config.LoadConfig("policy-decision-point") + // Setup logging - utils.SetupLogging("json", getEnvOrDefault("LOG_LEVEL", "info")) + utils.SetupLogging(cfg.Logging.Format, cfg.Logging.Level) - slog.Info("Starting policy decision point (V1)", + slog.Info("Starting policy decision point", + "environment", cfg.Environment, + "port", cfg.Service.Port, "version", Version, "build_time", BuildTime, "git_commit", GitCommit) // Log database configuration being used slog.Info("Database configuration", - "choreo_host", os.Getenv("CHOREO_OPENDIF_DATABASE_HOSTNAME"), - "choreo_port", os.Getenv("CHOREO_OPENDIF_DATABASE_PORT"), - "choreo_user", os.Getenv("CHOREO_OPENDIF_DATABASE_USERNAME"), - "choreo_database", os.Getenv("CHOREO_OPENDIF_DATABASE_DATABASENAME"), - "sslmode", os.Getenv("DB_SSLMODE"), - ) + "host", cfg.DBConfigs.Host, + "port", cfg.DBConfigs.Port, + "username", cfg.DBConfigs.Username, + "database", cfg.DBConfigs.Database, + "sslmode", cfg.DBConfigs.SSLMode) + + // Log IDP configuration (for future use) + slog.Info("IDP configuration", + "org_name", cfg.IDPConfig.OrgName, + "issuer", cfg.IDPConfig.Issuer, + "audience", cfg.IDPConfig.Audience, + "jwks_url", cfg.IDPConfig.JwksUrl) // Initialize V1 GORM database connection - v1DbConfig := v1.NewDatabaseConfig() + v1DbConfig := v1.NewDatabaseConfig(&v1.DatabaseConfigs{ + Host: cfg.DBConfigs.Host, + Port: cfg.DBConfigs.Port, + Username: cfg.DBConfigs.Username, + Password: cfg.DBConfigs.Password, + Database: cfg.DBConfigs.Database, + SSLMode: cfg.DBConfigs.SSLMode, + }) gormDB, err := v1.ConnectGormDB(v1DbConfig) if err != nil { slog.Error("Failed to connect to GORM database", "error", err) os.Exit(1) } + slog.Info("V1 database connected successfully") + + // Get underlying SQL DB for proper cleanup + v1SqlDB, err := gormDB.DB() + if err != nil { + slog.Error("Failed to get V1 database connection", "error", err) + os.Exit(1) + } + defer func() { + if err := v1SqlDB.Close(); err != nil { + slog.Error("Failed to close V1 database connection", "error", err) + } else { + slog.Info("V1 database connection closed successfully") + } + }() // Initialize V1 handlers v1Handler := v1.NewHandler(gormDB) @@ -128,12 +162,11 @@ func main() { utils.RespondWithJSON(w, http.StatusOK, debugInfo) }))) - // Create server using utils - port := getEnvOrDefault("PORT", "8082") + // Create server configuration serverConfig := &utils.ServerConfig{ - Port: port, - ReadTimeout: 15 * time.Second, - WriteTimeout: 15 * time.Second, + Port: cfg.Service.Port, + ReadTimeout: cfg.Service.Timeout, + WriteTimeout: cfg.Service.Timeout, IdleTimeout: 60 * time.Second, } server := utils.CreateServer(serverConfig, mux) @@ -143,23 +176,4 @@ func main() { slog.Error("Server failed", "error", err) os.Exit(1) } - - // Cleanup database connection on shutdown - defer func() { - if gormDB != nil { - if sqlDB, err := gormDB.DB(); err == nil { - if err := sqlDB.Close(); err != nil { - slog.Error("Failed to close database connection", "error", err) - } - } - } - }() -} - -// getEnvOrDefault gets an environment variable with a default value -func getEnvOrDefault(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue } diff --git a/exchange/policy-decision-point/shared/config/go.mod b/exchange/policy-decision-point/shared/config/go.mod deleted file mode 100644 index e1c50b2f..00000000 --- a/exchange/policy-decision-point/shared/config/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/gov-dx-sandbox/exchange/shared/config - -go 1.21 diff --git a/exchange/policy-decision-point/shared/constants/constants.go b/exchange/policy-decision-point/shared/constants/constants.go deleted file mode 100644 index a919e2aa..00000000 --- a/exchange/policy-decision-point/shared/constants/constants.go +++ /dev/null @@ -1,8 +0,0 @@ -package constants - -// Common constants -const ( - StatusMethodNotAllowed = "Method not allowed" - StatusIDRequired = "ID is required" - StatusConsentIDRequired = "consent_id is required" -) diff --git a/exchange/policy-decision-point/shared/constants/go.mod b/exchange/policy-decision-point/shared/constants/go.mod deleted file mode 100644 index 99b43fa1..00000000 --- a/exchange/policy-decision-point/shared/constants/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/gov-dx-sandbox/exchange/shared/constants - -go 1.24.6 diff --git a/exchange/policy-decision-point/shared/utils/go.mod b/exchange/policy-decision-point/shared/utils/go.mod deleted file mode 100644 index 79f0dabf..00000000 --- a/exchange/policy-decision-point/shared/utils/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/gov-dx-sandbox/exchange/shared/utils - -go 1.24.6 diff --git a/exchange/policy-decision-point/shared/utils/utils.go b/exchange/policy-decision-point/shared/utils/utils.go deleted file mode 100644 index 1ea5ae1a..00000000 --- a/exchange/policy-decision-point/shared/utils/utils.go +++ /dev/null @@ -1,320 +0,0 @@ -package utils - -import ( - "context" - "encoding/json" - "fmt" - "io" - "log/slog" - "net/http" - "os" - "os/signal" - "strings" - "syscall" - "time" -) - -// ErrorResponse represents a standard error response structure -type ErrorResponse struct { - Error string `json:"error"` -} - -// SuccessResponse represents a standard success response structure -type SuccessResponse struct { - Message string `json:"message,omitempty"` - Data interface{} `json:"data,omitempty"` -} - -// RespondWithJSON sends a JSON response with the given status code and data -func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - - if err := json.NewEncoder(w).Encode(data); err != nil { - slog.Error("Failed to encode JSON response", "error", err) - } -} - -// RespondWithError sends a JSON error response -func RespondWithError(w http.ResponseWriter, statusCode int, message string) { - RespondWithJSON(w, statusCode, ErrorResponse{Error: message}) -} - -// RespondWithSuccess sends a JSON success response -func RespondWithSuccess(w http.ResponseWriter, statusCode int, data interface{}) { - RespondWithJSON(w, statusCode, data) -} - -// HealthHandler creates a health check handler -func HealthHandler(serviceName string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - response := map[string]string{ - "service": serviceName, - "status": "healthy", - } - RespondWithJSON(w, http.StatusOK, response) - } -} - -// PanicRecoveryMiddleware provides panic recovery for HTTP handlers -func PanicRecoveryMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if err := recover(); err != nil { - slog.Error("Handler panicked", "error", err, "path", r.URL.Path) - RespondWithError(w, http.StatusInternalServerError, "Internal server error") - } - }() - next.ServeHTTP(w, r) - }) -} - -// JSONHandler handles JSON request/response with error handling -func JSONHandler(w http.ResponseWriter, r *http.Request, req interface{}, handler func() (interface{}, int, error)) { - if err := json.NewDecoder(r.Body).Decode(req); err != nil { - slog.Error("Failed to decode request body", "error", err) - RespondWithError(w, http.StatusBadRequest, "Invalid JSON input") - return - } - - data, statusCode, err := handler() - if err != nil { - slog.Error("Handler failed", "error", err) - RespondWithError(w, statusCode, err.Error()) - return - } - - RespondWithJSON(w, statusCode, data) -} - -// PathHandler handles path-based requests with parameter extraction -func PathHandler(w http.ResponseWriter, r *http.Request, prefix string, handler func(string) (interface{}, int, error)) { - param := r.URL.Path[len(prefix):] - if param == "" { - RespondWithError(w, http.StatusBadRequest, "Parameter is required") - return - } - - data, statusCode, err := handler(param) - if err != nil { - slog.Error("Handler failed", "error", err) - RespondWithError(w, statusCode, err.Error()) - return - } - - RespondWithJSON(w, statusCode, data) -} - -// GenericHandler handles generic requests without specific parameter extraction -func GenericHandler(w http.ResponseWriter, r *http.Request, handler func() (interface{}, int, error)) { - data, statusCode, err := handler() - if err != nil { - slog.Error("Handler failed", "error", err) - RespondWithError(w, statusCode, err.Error()) - return - } - - RespondWithJSON(w, statusCode, data) -} - -// Helper functions for common patterns -func HandleError(w http.ResponseWriter, err error, statusCode int, operation string) { - slog.Error("Operation failed", "operation", operation, "error", err) - RespondWithError(w, statusCode, fmt.Sprintf("failed to %s: %v", operation, err)) -} - -func HandleSuccess(w http.ResponseWriter, data interface{}, statusCode int, operation string, logData map[string]interface{}) { - // Convert map to key-value pairs for slog - args := make([]interface{}, 0, len(logData)*2+2) - args = append(args, "operation", operation) - for k, v := range logData { - args = append(args, k, v) - } - slog.Info("Operation successful", args...) - RespondWithSuccess(w, statusCode, data) -} - -func ValidateMethod(w http.ResponseWriter, r *http.Request, allowedMethod string) bool { - if r.Method != allowedMethod { - w.Header().Set("Allow", allowedMethod) - RespondWithError(w, http.StatusMethodNotAllowed, "Method not allowed") - return false - } - return true -} - -func ExtractIDFromPath(r *http.Request, prefix string) (string, error) { - id := r.URL.Path[len(prefix):] - if id == "" { - return "", fmt.Errorf("ID is required") - } - return id, nil -} - -func ExtractQueryParam(r *http.Request, param string) (string, error) { - value := r.URL.Query().Get(param) - if value == "" { - return "", fmt.Errorf("%s is required", param) - } - return value, nil -} - -// Additional utility functions from individual utils packages - -// ExtractIDFromPathString extracts the ID from a URL path by taking the last segment -func ExtractIDFromPathString(path string) string { - path = strings.TrimSuffix(path, "/") - parts := strings.Split(path, "/") - if len(parts) > 0 { - return parts[len(parts)-1] - } - return "" -} - -// ParseJSONRequest parses a JSON request body into the target struct -func ParseJSONRequest(r *http.Request, target interface{}) error { - defer r.Body.Close() - return json.NewDecoder(r.Body).Decode(target) -} - -// CreateCollectionResponse creates a standardized collection response with count -func CreateCollectionResponse(items interface{}, count int) map[string]interface{} { - return map[string]interface{}{ - "items": items, - "count": count, - } -} - -// ServerConfig holds configuration for HTTP servers -type ServerConfig struct { - Port string - ReadTimeout time.Duration - WriteTimeout time.Duration - IdleTimeout time.Duration -} - -// DefaultServerConfig returns a default server configuration -func DefaultServerConfig() *ServerConfig { - return &ServerConfig{ - Port: GetEnvOrDefault("PORT", "8080"), - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 60 * time.Second, - } -} - -// StartServerWithGracefulShutdown starts an HTTP server with graceful shutdown -func StartServerWithGracefulShutdown(server *http.Server, serviceName string) error { - // Graceful shutdown - go func() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - <-sigChan - - slog.Info("Shutting down server...", "service", serviceName) - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) - defer shutdownCancel() - - if err := server.Shutdown(shutdownCtx); err != nil { - slog.Error("Server shutdown error", "error", err, "service", serviceName) - } - }() - - slog.Info("Server starting", "service", serviceName, "address", server.Addr) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - slog.Error("Server failed to start", "error", err, "service", serviceName) - return err - } - return nil -} - -// CreateServer creates an HTTP server with the given configuration -func CreateServer(config *ServerConfig, handler http.Handler) *http.Server { - return &http.Server{ - Addr: fmt.Sprintf(":%s", config.Port), - Handler: handler, - ReadTimeout: config.ReadTimeout, - WriteTimeout: config.WriteTimeout, - IdleTimeout: config.IdleTimeout, - } -} - -// GetEnvOrDefault returns the environment variable value or a default -func GetEnvOrDefault(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} - -// ParseExpiryTime parses expiry time strings like "30d", "1h", "7d" -func ParseExpiryTime(expiryStr string) (time.Duration, error) { - if len(expiryStr) < 2 { - return 0, fmt.Errorf("invalid expiry time format") - } - - unit := expiryStr[len(expiryStr)-1:] - value := expiryStr[:len(expiryStr)-1] - - var duration time.Duration - switch unit { - case "d": - duration = 24 * time.Hour - case "h": - duration = time.Hour - case "m": - duration = time.Minute - case "s": - duration = time.Second - default: - return 0, fmt.Errorf("unsupported time unit: %s", unit) - } - - // Parse the numeric value - var multiplier int - if _, err := fmt.Sscanf(value, "%d", &multiplier); err != nil { - return 0, fmt.Errorf("invalid numeric value: %s", value) - } - - return time.Duration(multiplier) * duration, nil -} - -// SetupLogging configures logging based on the configuration -func SetupLogging(format, level string) { - var handler slog.Handler - - switch format { - case "json": - handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - Level: getLogLevel(level), - }) - default: - handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: getLogLevel(level), - }) - } - - slog.SetDefault(slog.New(handler)) -} - -// getLogLevel converts string level to slog.Level -func getLogLevel(level string) slog.Level { - switch strings.ToLower(level) { - case "debug": - return slog.LevelDebug - case "info": - return slog.LevelInfo - case "warn", "warning": - return slog.LevelWarn - case "error": - return slog.LevelError - default: - return slog.LevelInfo - } -} - -// ReadRequestBody reads the entire request body as bytes -func ReadRequestBody(r *http.Request) ([]byte, error) { - defer r.Body.Close() - return io.ReadAll(r.Body) -} diff --git a/exchange/policy-decision-point/v1/database.go b/exchange/policy-decision-point/v1/database.go index 1ff9ce9c..cc7d8ce0 100644 --- a/exchange/policy-decision-point/v1/database.go +++ b/exchange/policy-decision-point/v1/database.go @@ -27,14 +27,14 @@ type DatabaseConfig struct { } // NewDatabaseConfig creates a new GORM database configuration for V1 -func NewDatabaseConfig() *DatabaseConfig { +func NewDatabaseConfig(dbConfigs *DatabaseConfigs) *DatabaseConfig { return &DatabaseConfig{ - Host: getEnvOrDefault("CHOREO_OPENDIF_DATABASE_HOSTNAME", "localhost"), - Port: getEnvOrDefault("CHOREO_OPENDIF_DATABASE_PORT", "5432"), - Username: getEnvOrDefault("CHOREO_OPENDIF_DATABASE_USERNAME", "postgres"), - Password: getEnvOrDefault("CHOREO_OPENDIF_DATABASE_PASSWORD", "password"), - Database: getEnvOrDefault("CHOREO_OPENDIF_DATABASE_DATABASENAME", "testdb"), - SSLMode: getEnvOrDefault("DB_SSLMODE", "require"), + Host: dbConfigs.Host, + Port: dbConfigs.Port, + Username: dbConfigs.Username, + Password: dbConfigs.Password, + Database: dbConfigs.Database, + SSLMode: dbConfigs.SSLMode, MaxOpenConns: 25, MaxIdleConns: 5, ConnMaxLifetime: time.Hour, @@ -42,12 +42,14 @@ func NewDatabaseConfig() *DatabaseConfig { } } -// getEnvOrDefault gets environment variable or returns default value -func getEnvOrDefault(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue +// DatabaseConfigs holds database configuration values (matches internal/config.DBConfigs) +type DatabaseConfigs struct { + Host string + Port string + Username string + Password string + Database string + SSLMode string } // ConnectGormDB establishes a GORM connection to PostgreSQL diff --git a/exchange/policy-decision-point/v1/database_test.go b/exchange/policy-decision-point/v1/database_test.go index f06a4694..22a9f7ce 100644 --- a/exchange/policy-decision-point/v1/database_test.go +++ b/exchange/policy-decision-point/v1/database_test.go @@ -1,7 +1,6 @@ package v1 import ( - "os" "testing" "time" @@ -12,13 +11,21 @@ import ( ) func TestNewDatabaseConfig(t *testing.T) { - config := NewDatabaseConfig() + dbConfigs := &DatabaseConfigs{ + Host: "localhost", + Port: "5432", + Username: "postgres", + Password: "password", + Database: "pdp", + SSLMode: "require", + } + config := NewDatabaseConfig(dbConfigs) assert.NotNil(t, config) assert.Equal(t, "localhost", config.Host) assert.Equal(t, "5432", config.Port) assert.Equal(t, "postgres", config.Username) assert.Equal(t, "password", config.Password) - assert.Equal(t, "testdb", config.Database) + assert.Equal(t, "pdp", config.Database) assert.Equal(t, "require", config.SSLMode) assert.Equal(t, 25, config.MaxOpenConns) assert.Equal(t, 5, config.MaxIdleConns) @@ -26,47 +33,24 @@ func TestNewDatabaseConfig(t *testing.T) { assert.Equal(t, 30*time.Minute, config.ConnMaxIdleTime) } -func TestNewDatabaseConfig_WithEnvVars(t *testing.T) { - cleanup := WithEnvVars(t, TestEnvVarsChoreo()) - defer cleanup() - - config := NewDatabaseConfig() +func TestNewDatabaseConfig_WithConfig(t *testing.T) { + dbConfigs := &DatabaseConfigs{ + Host: "test-host", + Port: "5432", + Username: "test-user", + Password: "test-pass", + Database: "test-db", + SSLMode: "disable", + } + config := NewDatabaseConfig(dbConfigs) assert.Equal(t, "test-host", config.Host) - assert.Equal(t, "5433", config.Port) + assert.Equal(t, "5432", config.Port) assert.Equal(t, "test-user", config.Username) assert.Equal(t, "test-pass", config.Password) assert.Equal(t, "test-db", config.Database) assert.Equal(t, "disable", config.SSLMode) } -func TestGetEnvOrDefault(t *testing.T) { - t.Run("Returns env var when set", func(t *testing.T) { - key := "TEST_ENV_VAR_12345" - os.Setenv(key, "test-value") - defer os.Unsetenv(key) - - result := getEnvOrDefault(key, "default") - assert.Equal(t, "test-value", result) - }) - - t.Run("Returns default when not set", func(t *testing.T) { - key := "TEST_ENV_VAR_NONEXISTENT_12345" - os.Unsetenv(key) - - result := getEnvOrDefault(key, "default-value") - assert.Equal(t, "default-value", result) - }) - - t.Run("Returns default when empty string", func(t *testing.T) { - key := "TEST_ENV_VAR_EMPTY_12345" - os.Setenv(key, "") - defer os.Unsetenv(key) - - result := getEnvOrDefault(key, "default") - assert.Equal(t, "default", result) - }) -} - func TestConnectGormDB_WithSQLite(t *testing.T) { // Use SQLite for testing instead of PostgreSQL db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) diff --git a/exchange/policy-decision-point/v1/handler.go b/exchange/policy-decision-point/v1/handler.go index c6989bb6..16da7e9d 100644 --- a/exchange/policy-decision-point/v1/handler.go +++ b/exchange/policy-decision-point/v1/handler.go @@ -5,9 +5,9 @@ import ( "net/http" "strings" + "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/utils" "github.com/gov-dx-sandbox/exchange/policy-decision-point/v1/models" "github.com/gov-dx-sandbox/exchange/policy-decision-point/v1/services" - "github.com/gov-dx-sandbox/exchange/shared/utils" "gorm.io/gorm" ) diff --git a/exchange/policy-decision-point/v1/test_helpers.go b/exchange/policy-decision-point/v1/test_helpers.go index 4d10e7cc..cfc34924 100644 --- a/exchange/policy-decision-point/v1/test_helpers.go +++ b/exchange/policy-decision-point/v1/test_helpers.go @@ -6,7 +6,7 @@ import ( "time" ) -// TestConstants contains shared test constants +// TestConstants contains test constants const ( TestHost = "localhost" TestPort = "5432" @@ -50,26 +50,14 @@ func WithEnvVars(t *testing.T, vars map[string]string) func() { } } -// TestEnvVarsChoreo returns Choreo environment variables for testing -func TestEnvVarsChoreo() map[string]string { +// TestEnvVars returns standard environment variables for testing +func TestEnvVars() map[string]string { return map[string]string{ - "CHOREO_OPENDIF_DATABASE_HOSTNAME": "test-host", - "CHOREO_OPENDIF_DATABASE_PORT": "5433", - "CHOREO_OPENDIF_DATABASE_USERNAME": "test-user", - "CHOREO_OPENDIF_DATABASE_PASSWORD": "test-pass", - "CHOREO_OPENDIF_DATABASE_DATABASENAME": "test-db", - "DB_SSLMODE": "disable", - } -} - -// TestEnvVarsStandard returns standard environment variables for testing -func TestEnvVarsStandard() map[string]string { - return map[string]string{ - "DB_HOST": "standard-host", - "DB_PORT": "5434", - "DB_USER": "standard-user", - "DB_PASSWORD": "standard-password", - "DB_NAME": "standard-db", - "DB_SSLMODE": "prefer", + "DB_HOST": "test-host", + "DB_PORT": "5432", + "DB_USERNAME": "test-user", + "DB_PASSWORD": "test-pass", + "DB_NAME": "test-db", + "DB_SSLMODE": "disable", } } From 041245f1f3b65c84221a3d2d2977b012dae4c6d9 Mon Sep 17 00:00:00 2001 From: ginaxu1 <167130561+ginaxu1@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:14:30 +0530 Subject: [PATCH 2/3] Update exchange/policy-decision-point/internal/config/config.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- exchange/policy-decision-point/Dockerfile | 7 +- exchange/policy-decision-point/go.mod | 4 +- exchange/policy-decision-point/go.sum | 2 - .../internal/config/config.go | 6 +- .../internal/utils/utils.go | 160 ------------------ exchange/policy-decision-point/main.go | 17 +- exchange/policy-decision-point/v1/database.go | 13 +- .../policy-decision-point/v1/database_test.go | 43 ++--- exchange/policy-decision-point/v1/handler.go | 2 +- 9 files changed, 38 insertions(+), 216 deletions(-) delete mode 100644 exchange/policy-decision-point/internal/utils/utils.go diff --git a/exchange/policy-decision-point/Dockerfile b/exchange/policy-decision-point/Dockerfile index 2fd8c9bc..f6a04856 100644 --- a/exchange/policy-decision-point/Dockerfile +++ b/exchange/policy-decision-point/Dockerfile @@ -12,14 +12,15 @@ ARG GIT_COMMIT # Install build dependencies RUN apk add --no-cache git ca-certificates tzdata -# Copy go mod files and source code +# Copy go mod files first for better layer caching COPY exchange/policy-decision-point/go.mod exchange/policy-decision-point/go.sum ./exchange/policy-decision-point/ -COPY exchange/policy-decision-point/ ./exchange/policy-decision-point/ +# Copy shared dependencies +COPY exchange/shared/utils/ ./exchange/shared/utils/ WORKDIR /app/exchange/policy-decision-point/ RUN go mod download -# Copy source code +# Copy source code (this layer will be invalidated only on code changes) COPY exchange/policy-decision-point/ . # Build the application with build info from policy-decision-point directory diff --git a/exchange/policy-decision-point/go.mod b/exchange/policy-decision-point/go.mod index bc0224de..13ba55a5 100644 --- a/exchange/policy-decision-point/go.mod +++ b/exchange/policy-decision-point/go.mod @@ -4,13 +4,15 @@ go 1.24.6 require ( github.com/google/uuid v1.6.0 - github.com/joho/godotenv v1.5.1 + github.com/gov-dx-sandbox/exchange/shared/utils v0.0.0 github.com/stretchr/testify v1.10.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.0 ) +replace github.com/gov-dx-sandbox/exchange/shared/utils => ../shared/utils + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/exchange/policy-decision-point/go.sum b/exchange/policy-decision-point/go.sum index 7426b59c..07691979 100644 --- a/exchange/policy-decision-point/go.sum +++ b/exchange/policy-decision-point/go.sum @@ -16,8 +16,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/exchange/policy-decision-point/internal/config/config.go b/exchange/policy-decision-point/internal/config/config.go index 8413bd21..60bfa116 100644 --- a/exchange/policy-decision-point/internal/config/config.go +++ b/exchange/policy-decision-point/internal/config/config.go @@ -5,7 +5,7 @@ import ( "flag" "time" - "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/utils" + "github.com/gov-dx-sandbox/exchange/shared/utils" ) // Config holds all configuration for a service @@ -41,7 +41,7 @@ type SecurityConfig struct { // IDPConfig holds IDP configuration type IDPConfig struct { Issuer string - JwksUrl string + JwksURL string Audience string OrgName string } @@ -109,7 +109,7 @@ func LoadConfig(serviceName string) *Config { }, IDPConfig: IDPConfig{ Issuer: userIssuer, - JwksUrl: userJwksURL, + JwksURL: userJwksURL, Audience: userAudience, OrgName: orgName, }, diff --git a/exchange/policy-decision-point/internal/utils/utils.go b/exchange/policy-decision-point/internal/utils/utils.go deleted file mode 100644 index 7a21fcb7..00000000 --- a/exchange/policy-decision-point/internal/utils/utils.go +++ /dev/null @@ -1,160 +0,0 @@ -package utils - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "net/http" - "os" - "os/signal" - "strings" - "syscall" - "time" -) - -// ErrorResponse represents a standard error response structure -type ErrorResponse struct { - Error string `json:"error"` -} - -// RespondWithJSON sends a JSON response with the given status code and data -func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - - if err := json.NewEncoder(w).Encode(data); err != nil { - slog.Error("Failed to encode JSON response", "error", err) - } -} - -// RespondWithError sends a JSON error response -func RespondWithError(w http.ResponseWriter, statusCode int, message string) { - RespondWithJSON(w, statusCode, ErrorResponse{Error: message}) -} - -// RespondWithSuccess sends a JSON success response -func RespondWithSuccess(w http.ResponseWriter, statusCode int, data interface{}) { - RespondWithJSON(w, statusCode, data) -} - -// HealthHandler creates a health check handler -func HealthHandler(serviceName string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - response := map[string]string{ - "service": serviceName, - "status": "healthy", - } - RespondWithJSON(w, http.StatusOK, response) - } -} - -// PanicRecoveryMiddleware provides panic recovery for HTTP handlers -func PanicRecoveryMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if err := recover(); err != nil { - slog.Error("Handler panicked", "error", err, "path", r.URL.Path) - RespondWithError(w, http.StatusInternalServerError, "Internal server error") - } - }() - next.ServeHTTP(w, r) - }) -} - -// ServerConfig holds configuration for HTTP servers -type ServerConfig struct { - Port string - ReadTimeout time.Duration - WriteTimeout time.Duration - IdleTimeout time.Duration -} - -// DefaultServerConfig returns a default server configuration -func DefaultServerConfig() *ServerConfig { - return &ServerConfig{ - Port: GetEnvOrDefault("PORT", "8080"), - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 60 * time.Second, - } -} - -// StartServerWithGracefulShutdown starts an HTTP server with graceful shutdown -func StartServerWithGracefulShutdown(server *http.Server, serviceName string) error { - // Graceful shutdown - go func() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - <-sigChan - - slog.Info("Shutting down server...", "service", serviceName) - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) - defer shutdownCancel() - - if err := server.Shutdown(shutdownCtx); err != nil { - slog.Error("Server shutdown error", "error", err, "service", serviceName) - } - }() - - slog.Info("Server starting", "service", serviceName, "address", server.Addr) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - slog.Error("Server failed to start", "error", err, "service", serviceName) - return err - } - return nil -} - -// CreateServer creates an HTTP server with the given configuration -func CreateServer(config *ServerConfig, handler http.Handler) *http.Server { - return &http.Server{ - Addr: fmt.Sprintf(":%s", config.Port), - Handler: handler, - ReadTimeout: config.ReadTimeout, - WriteTimeout: config.WriteTimeout, - IdleTimeout: config.IdleTimeout, - } -} - -// GetEnvOrDefault returns the environment variable value or a default -func GetEnvOrDefault(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} - -// SetupLogging configures logging based on the configuration -func SetupLogging(format, level string) { - var handler slog.Handler - - switch format { - case "json": - handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - Level: getLogLevel(level), - }) - default: - handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: getLogLevel(level), - }) - } - - slog.SetDefault(slog.New(handler)) -} - -// getLogLevel converts string level to slog.Level -func getLogLevel(level string) slog.Level { - switch strings.ToLower(level) { - case "debug": - return slog.LevelDebug - case "info": - return slog.LevelInfo - case "warn", "warning": - return slog.LevelWarn - case "error": - return slog.LevelError - default: - return slog.LevelInfo - } -} - diff --git a/exchange/policy-decision-point/main.go b/exchange/policy-decision-point/main.go index 8d30de64..f050a79a 100644 --- a/exchange/policy-decision-point/main.go +++ b/exchange/policy-decision-point/main.go @@ -8,9 +8,8 @@ import ( "time" "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/config" - "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/utils" v1 "github.com/gov-dx-sandbox/exchange/policy-decision-point/v1" - "github.com/joho/godotenv" + "github.com/gov-dx-sandbox/exchange/shared/utils" ) // Build information - set during build @@ -21,9 +20,6 @@ var ( ) func main() { - // Load .env file if it exists (optional - fails silently if not found) - _ = godotenv.Load() - // Load configuration using flags cfg := config.LoadConfig("policy-decision-point") @@ -50,17 +46,10 @@ func main() { "org_name", cfg.IDPConfig.OrgName, "issuer", cfg.IDPConfig.Issuer, "audience", cfg.IDPConfig.Audience, - "jwks_url", cfg.IDPConfig.JwksUrl) + "jwks_url", cfg.IDPConfig.JwksURL) // Initialize V1 GORM database connection - v1DbConfig := v1.NewDatabaseConfig(&v1.DatabaseConfigs{ - Host: cfg.DBConfigs.Host, - Port: cfg.DBConfigs.Port, - Username: cfg.DBConfigs.Username, - Password: cfg.DBConfigs.Password, - Database: cfg.DBConfigs.Database, - SSLMode: cfg.DBConfigs.SSLMode, - }) + v1DbConfig := v1.NewDatabaseConfig(&cfg.DBConfigs) gormDB, err := v1.ConnectGormDB(v1DbConfig) if err != nil { slog.Error("Failed to connect to GORM database", "error", err) diff --git a/exchange/policy-decision-point/v1/database.go b/exchange/policy-decision-point/v1/database.go index cc7d8ce0..22d2ac07 100644 --- a/exchange/policy-decision-point/v1/database.go +++ b/exchange/policy-decision-point/v1/database.go @@ -6,6 +6,7 @@ import ( "os" "time" + "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/config" "github.com/gov-dx-sandbox/exchange/policy-decision-point/v1/models" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -27,7 +28,7 @@ type DatabaseConfig struct { } // NewDatabaseConfig creates a new GORM database configuration for V1 -func NewDatabaseConfig(dbConfigs *DatabaseConfigs) *DatabaseConfig { +func NewDatabaseConfig(dbConfigs *config.DBConfigs) *DatabaseConfig { return &DatabaseConfig{ Host: dbConfigs.Host, Port: dbConfigs.Port, @@ -42,16 +43,6 @@ func NewDatabaseConfig(dbConfigs *DatabaseConfigs) *DatabaseConfig { } } -// DatabaseConfigs holds database configuration values (matches internal/config.DBConfigs) -type DatabaseConfigs struct { - Host string - Port string - Username string - Password string - Database string - SSLMode string -} - // ConnectGormDB establishes a GORM connection to PostgreSQL func ConnectGormDB(config *DatabaseConfig) (*gorm.DB, error) { dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", diff --git a/exchange/policy-decision-point/v1/database_test.go b/exchange/policy-decision-point/v1/database_test.go index 22a9f7ce..90096e55 100644 --- a/exchange/policy-decision-point/v1/database_test.go +++ b/exchange/policy-decision-point/v1/database_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" @@ -11,7 +12,7 @@ import ( ) func TestNewDatabaseConfig(t *testing.T) { - dbConfigs := &DatabaseConfigs{ + dbConfigs := &config.DBConfigs{ Host: "localhost", Port: "5432", Username: "postgres", @@ -19,22 +20,22 @@ func TestNewDatabaseConfig(t *testing.T) { Database: "pdp", SSLMode: "require", } - config := NewDatabaseConfig(dbConfigs) - assert.NotNil(t, config) - assert.Equal(t, "localhost", config.Host) - assert.Equal(t, "5432", config.Port) - assert.Equal(t, "postgres", config.Username) - assert.Equal(t, "password", config.Password) - assert.Equal(t, "pdp", config.Database) - assert.Equal(t, "require", config.SSLMode) - assert.Equal(t, 25, config.MaxOpenConns) - assert.Equal(t, 5, config.MaxIdleConns) - assert.Equal(t, time.Hour, config.ConnMaxLifetime) - assert.Equal(t, 30*time.Minute, config.ConnMaxIdleTime) + dbConfig := NewDatabaseConfig(dbConfigs) + assert.NotNil(t, dbConfig) + assert.Equal(t, "localhost", dbConfig.Host) + assert.Equal(t, "5432", dbConfig.Port) + assert.Equal(t, "postgres", dbConfig.Username) + assert.Equal(t, "password", dbConfig.Password) + assert.Equal(t, "pdp", dbConfig.Database) + assert.Equal(t, "require", dbConfig.SSLMode) + assert.Equal(t, 25, dbConfig.MaxOpenConns) + assert.Equal(t, 5, dbConfig.MaxIdleConns) + assert.Equal(t, time.Hour, dbConfig.ConnMaxLifetime) + assert.Equal(t, 30*time.Minute, dbConfig.ConnMaxIdleTime) } func TestNewDatabaseConfig_WithConfig(t *testing.T) { - dbConfigs := &DatabaseConfigs{ + dbConfigs := &config.DBConfigs{ Host: "test-host", Port: "5432", Username: "test-user", @@ -42,13 +43,13 @@ func TestNewDatabaseConfig_WithConfig(t *testing.T) { Database: "test-db", SSLMode: "disable", } - config := NewDatabaseConfig(dbConfigs) - assert.Equal(t, "test-host", config.Host) - assert.Equal(t, "5432", config.Port) - assert.Equal(t, "test-user", config.Username) - assert.Equal(t, "test-pass", config.Password) - assert.Equal(t, "test-db", config.Database) - assert.Equal(t, "disable", config.SSLMode) + dbConfig := NewDatabaseConfig(dbConfigs) + assert.Equal(t, "test-host", dbConfig.Host) + assert.Equal(t, "5432", dbConfig.Port) + assert.Equal(t, "test-user", dbConfig.Username) + assert.Equal(t, "test-pass", dbConfig.Password) + assert.Equal(t, "test-db", dbConfig.Database) + assert.Equal(t, "disable", dbConfig.SSLMode) } func TestConnectGormDB_WithSQLite(t *testing.T) { diff --git a/exchange/policy-decision-point/v1/handler.go b/exchange/policy-decision-point/v1/handler.go index 16da7e9d..c6989bb6 100644 --- a/exchange/policy-decision-point/v1/handler.go +++ b/exchange/policy-decision-point/v1/handler.go @@ -5,9 +5,9 @@ import ( "net/http" "strings" - "github.com/gov-dx-sandbox/exchange/policy-decision-point/internal/utils" "github.com/gov-dx-sandbox/exchange/policy-decision-point/v1/models" "github.com/gov-dx-sandbox/exchange/policy-decision-point/v1/services" + "github.com/gov-dx-sandbox/exchange/shared/utils" "gorm.io/gorm" ) From 16c09b5592ad983218c556d54eb11624b63a52ff Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 20 Jan 2026 00:22:38 +0530 Subject: [PATCH 3/3] update .env.template --- exchange/policy-decision-point/.env.template | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/exchange/policy-decision-point/.env.template b/exchange/policy-decision-point/.env.template index 1ea5cc49..5ec18e7d 100644 --- a/exchange/policy-decision-point/.env.template +++ b/exchange/policy-decision-point/.env.template @@ -1,14 +1,14 @@ -# DataBase Configuration -CHOREO_OPENDIF_DATABASE_HOSTNAME={your_database_host} -CHOREO_OPENDIF_DATABASE_PORT={your_database_host_port} -CHOREO_OPENDIF_DATABASE_NAME={your_database_name} -CHOREO_OPENDIF_DATABASE_USERNAME={your_database_username} -CHOREO_OPENDIF_DATABASE_PASSWORD={your_database_password} +# Database Configuration +DB_HOST={your_database_host} +DB_PORT={your_database_port} +DB_USERNAME={your_database_username} +DB_PASSWORD={your_database_password} +DB_NAME={your_database_name} DB_SSLMODE={disable|require|verify-ca|verify-full} +# Migration Configuration RUN_MIGRATION=false -# Local Server Configuration -PORT=8081 -LOG_LEVEL=debug - +# Server Configuration +PORT=8082 +LOG_LEVEL=info