You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							404 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							404 lines
						
					
					
						
							12 KiB
						
					
					
				| package command | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"crypto/tls" | |
| 	"encoding/json" | |
| 	"fmt" | |
| 	"os" | |
| 	"os/signal" | |
| 	"strings" | |
| 	"syscall" | |
| 	"time" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/server/postgres" | |
| 	"github.com/seaweedfs/seaweedfs/weed/util" | |
| ) | |
| 
 | |
| var ( | |
| 	dbOptions DBOptions | |
| ) | |
| 
 | |
| type DBOptions struct { | |
| 	host        *string | |
| 	port        *int | |
| 	masterAddr  *string | |
| 	authMethod  *string | |
| 	users       *string | |
| 	database    *string | |
| 	maxConns    *int | |
| 	idleTimeout *string | |
| 	tlsCert     *string | |
| 	tlsKey      *string | |
| } | |
| 
 | |
| func init() { | |
| 	cmdDB.Run = runDB // break init cycle | |
| 	dbOptions.host = cmdDB.Flag.String("host", "localhost", "Database server host") | |
| 	dbOptions.port = cmdDB.Flag.Int("port", 5432, "Database server port") | |
| 	dbOptions.masterAddr = cmdDB.Flag.String("master", "localhost:9333", "SeaweedFS master server address") | |
| 	dbOptions.authMethod = cmdDB.Flag.String("auth", "trust", "Authentication method: trust, password, md5") | |
| 	dbOptions.users = cmdDB.Flag.String("users", "", "User credentials for auth (JSON format '{\"user1\":\"pass1\",\"user2\":\"pass2\"}' or file '@/path/to/users.json')") | |
| 	dbOptions.database = cmdDB.Flag.String("database", "default", "Default database name") | |
| 	dbOptions.maxConns = cmdDB.Flag.Int("max-connections", 100, "Maximum concurrent connections per server") | |
| 	dbOptions.idleTimeout = cmdDB.Flag.String("idle-timeout", "1h", "Connection idle timeout") | |
| 	dbOptions.tlsCert = cmdDB.Flag.String("tls-cert", "", "TLS certificate file path") | |
| 	dbOptions.tlsKey = cmdDB.Flag.String("tls-key", "", "TLS private key file path") | |
| } | |
| 
 | |
| var cmdDB = &Command{ | |
| 	UsageLine: "db -port=5432 -master=<master_server>", | |
| 	Short:     "start a PostgreSQL-compatible database server for SQL queries", | |
| 	Long: `Start a PostgreSQL wire protocol compatible database server that provides SQL query access to SeaweedFS. | |
|  | |
| This database server enables any PostgreSQL client, tool, or application to connect to SeaweedFS | |
| and execute SQL queries against MQ topics. It implements the PostgreSQL wire protocol for maximum | |
| compatibility with the existing PostgreSQL ecosystem. | |
|  | |
| Examples: | |
|  | |
| 	# Start database server on default port 5432 | |
| 	weed db | |
| 	 | |
| 	# Start with MD5 authentication using JSON format (recommended) | |
| 	weed db -auth=md5 -users='{"admin":"secret","readonly":"view123"}' | |
| 	 | |
| 	# Start with complex passwords using JSON format | |
| 	weed db -auth=md5 -users='{"admin":"pass;with;semicolons","user":"password:with:colons"}' | |
| 	 | |
| 	# Start with credentials from JSON file (most secure) | |
| 	weed db -auth=md5 -users="@/etc/seaweedfs/users.json" | |
| 	 | |
| 	# Start with custom port and master | |
| 	weed db -port=5433 -master=master1:9333 | |
| 	 | |
| 	# Allow connections from any host | |
| 	weed db -host=0.0.0.0 -port=5432 | |
| 	 | |
| 	# Start with TLS encryption | |
| 	weed db -tls-cert=server.crt -tls-key=server.key | |
|  | |
| Client Connection Examples: | |
|  | |
| 	# psql command line client | |
| 	psql "host=localhost port=5432 dbname=default user=seaweedfs" | |
| 	psql -h localhost -p 5432 -U seaweedfs -d default | |
| 	 | |
| 	# With password | |
| 	PGPASSWORD=secret psql -h localhost -p 5432 -U admin -d default | |
| 	 | |
| 	# Connection string | |
| 	psql "postgresql://admin:secret@localhost:5432/default" | |
|  | |
| Programming Language Examples: | |
|  | |
| 	# Python (psycopg2) | |
| 	import psycopg2 | |
| 	conn = psycopg2.connect( | |
| 		host="localhost", port=5432,  | |
| 		user="seaweedfs", database="default" | |
| 	) | |
| 	 | |
| 	# Java JDBC | |
| 	String url = "jdbc:postgresql://localhost:5432/default"; | |
| 	Connection conn = DriverManager.getConnection(url, "seaweedfs", ""); | |
| 	 | |
| 	# Go (lib/pq) | |
| 	db, err := sql.Open("postgres", "host=localhost port=5432 user=seaweedfs dbname=default sslmode=disable") | |
| 	 | |
| 	# Node.js (pg) | |
| 	const client = new Client({ | |
| 		host: 'localhost', port: 5432, | |
| 		user: 'seaweedfs', database: 'default' | |
| 	}); | |
|  | |
| Supported SQL Operations: | |
| 	- SELECT queries on MQ topics | |
| 	- DESCRIBE/DESC table_name commands | |
| 	- EXPLAIN query execution plans | |
| 	- SHOW DATABASES/TABLES commands   | |
| 	- Aggregation functions (COUNT, SUM, AVG, MIN, MAX) | |
| 	- WHERE clauses with filtering | |
| 	- System columns (_timestamp_ns, _key, _source) | |
| 	- Basic PostgreSQL system queries (version(), current_database(), current_user) | |
|  | |
| Authentication Methods: | |
| 	- trust: No authentication required (default) | |
| 	- password: Clear text password authentication | |
| 	- md5: MD5 password authentication | |
|  | |
| User Credential Formats: | |
| 	- JSON format: '{"user1":"pass1","user2":"pass2"}' (supports any special characters) | |
| 	- File format: "@/path/to/users.json" (JSON file) | |
| 	 | |
| 	Note: JSON format supports passwords with semicolons, colons, and any other special characters. | |
| 	      File format is recommended for production to keep credentials secure. | |
|  | |
| Compatible Tools: | |
| 	- psql (PostgreSQL command line client) | |
| 	- Any PostgreSQL JDBC/ODBC compatible tool | |
|  | |
| Security Features: | |
| 	- Multiple authentication methods | |
| 	- TLS encryption support | |
| 	- Read-only access (no data modification) | |
|  | |
| Performance Features: | |
| 	- Fast path aggregation optimization (COUNT, MIN, MAX without WHERE clauses) | |
| 	- Hybrid data scanning (parquet files + live logs) | |
| 	- PostgreSQL wire protocol | |
| 	- Query result streaming | |
|  | |
| `, | |
| } | |
| 
 | |
| func runDB(cmd *Command, args []string) bool { | |
| 
 | |
| 	util.LoadConfiguration("security", false) | |
| 
 | |
| 	// Validate options | |
| 	if *dbOptions.masterAddr == "" { | |
| 		fmt.Fprintf(os.Stderr, "Error: master address is required\n") | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Parse authentication method | |
| 	authMethod, err := parseAuthMethod(*dbOptions.authMethod) | |
| 	if err != nil { | |
| 		fmt.Fprintf(os.Stderr, "Error: %v\n", err) | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Parse user credentials | |
| 	users, err := parseUsers(*dbOptions.users, authMethod) | |
| 	if err != nil { | |
| 		fmt.Fprintf(os.Stderr, "Error: %v\n", err) | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Parse idle timeout | |
| 	idleTimeout, err := time.ParseDuration(*dbOptions.idleTimeout) | |
| 	if err != nil { | |
| 		fmt.Fprintf(os.Stderr, "Error parsing idle timeout: %v\n", err) | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Validate port number | |
| 	if err := validatePortNumber(*dbOptions.port); err != nil { | |
| 		fmt.Fprintf(os.Stderr, "Error: %v\n", err) | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Setup TLS if requested | |
| 	var tlsConfig *tls.Config | |
| 	if *dbOptions.tlsCert != "" && *dbOptions.tlsKey != "" { | |
| 		cert, err := tls.LoadX509KeyPair(*dbOptions.tlsCert, *dbOptions.tlsKey) | |
| 		if err != nil { | |
| 			fmt.Fprintf(os.Stderr, "Error loading TLS certificates: %v\n", err) | |
| 			return false | |
| 		} | |
| 		tlsConfig = &tls.Config{ | |
| 			Certificates: []tls.Certificate{cert}, | |
| 		} | |
| 	} | |
| 
 | |
| 	// Create server configuration | |
| 	config := &postgres.PostgreSQLServerConfig{ | |
| 		Host:        *dbOptions.host, | |
| 		Port:        *dbOptions.port, | |
| 		AuthMethod:  authMethod, | |
| 		Users:       users, | |
| 		Database:    *dbOptions.database, | |
| 		MaxConns:    *dbOptions.maxConns, | |
| 		IdleTimeout: idleTimeout, | |
| 		TLSConfig:   tlsConfig, | |
| 	} | |
| 
 | |
| 	// Create database server | |
| 	dbServer, err := postgres.NewPostgreSQLServer(config, *dbOptions.masterAddr) | |
| 	if err != nil { | |
| 		fmt.Fprintf(os.Stderr, "Error creating database server: %v\n", err) | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Print startup information | |
| 	fmt.Printf("Starting SeaweedFS Database Server...\n") | |
| 	fmt.Printf("Host: %s\n", *dbOptions.host) | |
| 	fmt.Printf("Port: %d\n", *dbOptions.port) | |
| 	fmt.Printf("Master: %s\n", *dbOptions.masterAddr) | |
| 	fmt.Printf("Database: %s\n", *dbOptions.database) | |
| 	fmt.Printf("Auth Method: %s\n", *dbOptions.authMethod) | |
| 	fmt.Printf("Max Connections: %d\n", *dbOptions.maxConns) | |
| 	fmt.Printf("Idle Timeout: %s\n", *dbOptions.idleTimeout) | |
| 	if tlsConfig != nil { | |
| 		fmt.Printf("TLS: Enabled\n") | |
| 	} else { | |
| 		fmt.Printf("TLS: Disabled\n") | |
| 	} | |
| 	if len(users) > 0 { | |
| 		fmt.Printf("Users: %d configured\n", len(users)) | |
| 	} | |
| 
 | |
| 	fmt.Printf("\nDatabase Connection Examples:\n") | |
| 	fmt.Printf("  psql -h %s -p %d -U seaweedfs -d %s\n", *dbOptions.host, *dbOptions.port, *dbOptions.database) | |
| 	if len(users) > 0 { | |
| 		// Show first user as example | |
| 		for username := range users { | |
| 			fmt.Printf("  psql -h %s -p %d -U %s -d %s\n", *dbOptions.host, *dbOptions.port, username, *dbOptions.database) | |
| 			break | |
| 		} | |
| 	} | |
| 	fmt.Printf("  postgresql://%s:%d/%s\n", *dbOptions.host, *dbOptions.port, *dbOptions.database) | |
| 
 | |
| 	fmt.Printf("\nSupported Operations:\n") | |
| 	fmt.Printf("  - SELECT queries on MQ topics\n") | |
| 	fmt.Printf("  - DESCRIBE/DESC table_name\n") | |
| 	fmt.Printf("  - EXPLAIN query execution plans\n") | |
| 	fmt.Printf("  - SHOW DATABASES/TABLES\n") | |
| 	fmt.Printf("  - Aggregations: COUNT, SUM, AVG, MIN, MAX\n") | |
| 	fmt.Printf("  - System columns: _timestamp_ns, _key, _source\n") | |
| 	fmt.Printf("  - Basic PostgreSQL system queries\n") | |
| 
 | |
| 	fmt.Printf("\nReady for database connections!\n\n") | |
| 
 | |
| 	// Start the server | |
| 	err = dbServer.Start() | |
| 	if err != nil { | |
| 		fmt.Fprintf(os.Stderr, "Error starting database server: %v\n", err) | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Set up signal handling for graceful shutdown | |
| 	sigChan := make(chan os.Signal, 1) | |
| 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | |
| 
 | |
| 	// Wait for shutdown signal | |
| 	<-sigChan | |
| 	fmt.Printf("\nReceived shutdown signal, stopping database server...\n") | |
| 
 | |
| 	// Create context with timeout for graceful shutdown | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | |
| 	defer cancel() | |
| 
 | |
| 	// Stop the server with timeout | |
| 	done := make(chan error, 1) | |
| 	go func() { | |
| 		done <- dbServer.Stop() | |
| 	}() | |
| 
 | |
| 	select { | |
| 	case err := <-done: | |
| 		if err != nil { | |
| 			fmt.Fprintf(os.Stderr, "Error stopping database server: %v\n", err) | |
| 			return false | |
| 		} | |
| 		fmt.Printf("Database server stopped successfully\n") | |
| 	case <-ctx.Done(): | |
| 		fmt.Fprintf(os.Stderr, "Timeout waiting for database server to stop\n") | |
| 		return false | |
| 	} | |
| 
 | |
| 	return true | |
| } | |
| 
 | |
| // parseAuthMethod parses the authentication method string | |
| func parseAuthMethod(method string) (postgres.AuthMethod, error) { | |
| 	switch strings.ToLower(method) { | |
| 	case "trust": | |
| 		return postgres.AuthTrust, nil | |
| 	case "password": | |
| 		return postgres.AuthPassword, nil | |
| 	case "md5": | |
| 		return postgres.AuthMD5, nil | |
| 	default: | |
| 		return postgres.AuthTrust, fmt.Errorf("unsupported auth method '%s'. Supported: trust, password, md5", method) | |
| 	} | |
| } | |
| 
 | |
| // parseUsers parses the user credentials string with support for secure formats only | |
| // Supported formats: | |
| // 1. JSON format: {"username":"password","username2":"password2"} | |
| // 2. File format: /path/to/users.json or @/path/to/users.json | |
| func parseUsers(usersStr string, authMethod postgres.AuthMethod) (map[string]string, error) { | |
| 	users := make(map[string]string) | |
| 
 | |
| 	if usersStr == "" { | |
| 		// No users specified | |
| 		if authMethod != postgres.AuthTrust { | |
| 			return nil, fmt.Errorf("users must be specified when auth method is not 'trust'") | |
| 		} | |
| 		return users, nil | |
| 	} | |
| 
 | |
| 	// Trim whitespace | |
| 	usersStr = strings.TrimSpace(usersStr) | |
| 
 | |
| 	// Determine format and parse accordingly | |
| 	if strings.HasPrefix(usersStr, "{") && strings.HasSuffix(usersStr, "}") { | |
| 		// JSON format | |
| 		return parseUsersJSON(usersStr, authMethod) | |
| 	} | |
| 
 | |
| 	// Check if it's a file path (with or without @ prefix) before declaring invalid format | |
| 	filePath := strings.TrimPrefix(usersStr, "@") | |
| 	if _, err := os.Stat(filePath); err == nil { | |
| 		// File format | |
| 		return parseUsersFile(usersStr, authMethod) // Pass original string to preserve @ handling | |
| 	} | |
| 
 | |
| 	// Invalid format | |
| 	return nil, fmt.Errorf("invalid user credentials format. Use JSON format '{\"user\":\"pass\"}' or file format '@/path/to/users.json' or 'path/to/users.json'. Legacy semicolon-separated format is no longer supported") | |
| } | |
| 
 | |
| // parseUsersJSON parses user credentials from JSON format | |
| func parseUsersJSON(jsonStr string, authMethod postgres.AuthMethod) (map[string]string, error) { | |
| 	var users map[string]string | |
| 	if err := json.Unmarshal([]byte(jsonStr), &users); err != nil { | |
| 		return nil, fmt.Errorf("invalid JSON format for users: %v", err) | |
| 	} | |
| 
 | |
| 	// Validate users | |
| 	for username, password := range users { | |
| 		if username == "" { | |
| 			return nil, fmt.Errorf("empty username in JSON user specification") | |
| 		} | |
| 		if authMethod != postgres.AuthTrust && password == "" { | |
| 			return nil, fmt.Errorf("empty password for user '%s' with auth method", username) | |
| 		} | |
| 	} | |
| 
 | |
| 	return users, nil | |
| } | |
| 
 | |
| // parseUsersFile parses user credentials from a JSON file | |
| func parseUsersFile(filePath string, authMethod postgres.AuthMethod) (map[string]string, error) { | |
| 	// Remove @ prefix if present | |
| 	filePath = strings.TrimPrefix(filePath, "@") | |
| 
 | |
| 	// Read file content | |
| 	content, err := os.ReadFile(filePath) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to read users file '%s': %v", filePath, err) | |
| 	} | |
| 
 | |
| 	contentStr := strings.TrimSpace(string(content)) | |
| 
 | |
| 	// File must contain JSON format | |
| 	if !strings.HasPrefix(contentStr, "{") || !strings.HasSuffix(contentStr, "}") { | |
| 		return nil, fmt.Errorf("users file '%s' must contain JSON format: {\"user\":\"pass\"}. Legacy formats are no longer supported", filePath) | |
| 	} | |
| 
 | |
| 	// Parse as JSON | |
| 	return parseUsersJSON(contentStr, authMethod) | |
| } | |
| 
 | |
| // validatePortNumber validates that the port number is reasonable | |
| func validatePortNumber(port int) error { | |
| 	if port < 1 || port > 65535 { | |
| 		return fmt.Errorf("port number must be between 1 and 65535, got %d", port) | |
| 	} | |
| 	if port < 1024 { | |
| 		fmt.Fprintf(os.Stderr, "Warning: port number %d may require root privileges\n", port) | |
| 	} | |
| 	return nil | |
| }
 |