mirror of
https://github.com/DeBrosOfficial/network.git
synced 2026-01-30 06:53:03 +00:00
fixed deployments
This commit is contained in:
parent
84c9b9ab9b
commit
fc0b958b1e
6
migrations/embed.go
Normal file
6
migrations/embed.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var FS embed.FS
|
||||||
@ -19,7 +19,7 @@ type HTTPGatewayConfig struct {
|
|||||||
IPFSClusterAPIURL string `yaml:"ipfs_cluster_api_url"` // IPFS Cluster API URL
|
IPFSClusterAPIURL string `yaml:"ipfs_cluster_api_url"` // IPFS Cluster API URL
|
||||||
IPFSAPIURL string `yaml:"ipfs_api_url"` // IPFS API URL
|
IPFSAPIURL string `yaml:"ipfs_api_url"` // IPFS API URL
|
||||||
IPFSTimeout time.Duration `yaml:"ipfs_timeout"` // Timeout for IPFS operations
|
IPFSTimeout time.Duration `yaml:"ipfs_timeout"` // Timeout for IPFS operations
|
||||||
BaseDomain string `yaml:"base_domain"` // Base domain for deployments (e.g., "dbrs.space", defaults to "orama.network")
|
BaseDomain string `yaml:"base_domain"` // Base domain for deployments (e.g., "dbrs.space"). Defaults to "dbrs.space"
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPSConfig contains HTTPS/TLS configuration for the gateway
|
// HTTPSConfig contains HTTPS/TLS configuration for the gateway
|
||||||
|
|||||||
@ -237,7 +237,8 @@ WantedBy=multi-user.target
|
|||||||
func (m *Manager) getStartCommand(deployment *deployments.Deployment, workDir string) string {
|
func (m *Manager) getStartCommand(deployment *deployments.Deployment, workDir string) string {
|
||||||
switch deployment.Type {
|
switch deployment.Type {
|
||||||
case deployments.DeploymentTypeNextJS:
|
case deployments.DeploymentTypeNextJS:
|
||||||
return "/usr/bin/node server.js"
|
// Next.js standalone output places server at .next/standalone/server.js
|
||||||
|
return "/usr/bin/node .next/standalone/server.js"
|
||||||
case deployments.DeploymentTypeNodeJSBackend:
|
case deployments.DeploymentTypeNodeJSBackend:
|
||||||
// Check if ENTRY_POINT is set in environment
|
// Check if ENTRY_POINT is set in environment
|
||||||
if entryPoint, ok := deployment.Environment["ENTRY_POINT"]; ok {
|
if entryPoint, ok := deployment.Environment["ENTRY_POINT"]; ok {
|
||||||
|
|||||||
@ -211,6 +211,13 @@ func (ps *ProductionSetup) Phase2ProvisionEnvironment() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up deployment sudoers (allows debros user to manage orama-deploy-* services)
|
||||||
|
if err := ps.userProvisioner.SetupDeploymentSudoers(); err != nil {
|
||||||
|
ps.logf(" ⚠️ Failed to setup deployment sudoers: %v", err)
|
||||||
|
} else {
|
||||||
|
ps.logf(" ✓ Deployment sudoers configured")
|
||||||
|
}
|
||||||
|
|
||||||
// Create directory structure (unified structure)
|
// Create directory structure (unified structure)
|
||||||
if err := ps.fsProvisioner.EnsureDirectoryStructure(); err != nil {
|
if err := ps.fsProvisioner.EnsureDirectoryStructure(); err != nil {
|
||||||
return fmt.Errorf("failed to create directory structure: %w", err)
|
return fmt.Errorf("failed to create directory structure: %w", err)
|
||||||
|
|||||||
@ -182,6 +182,48 @@ func (up *UserProvisioner) SetupSudoersAccess(invokerUser string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetupDeploymentSudoers configures the debros user with permissions needed for
|
||||||
|
// managing user deployments via systemd services.
|
||||||
|
func (up *UserProvisioner) SetupDeploymentSudoers() error {
|
||||||
|
sudoersFile := "/etc/sudoers.d/debros-deployments"
|
||||||
|
|
||||||
|
// Check if already configured
|
||||||
|
if _, err := os.Stat(sudoersFile); err == nil {
|
||||||
|
return nil // Already configured
|
||||||
|
}
|
||||||
|
|
||||||
|
sudoersContent := `# DeBros Network - Deployment Management Permissions
|
||||||
|
# Allows debros user to manage systemd services for user deployments
|
||||||
|
|
||||||
|
# Systemd service management for orama-deploy-* services
|
||||||
|
debros ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload
|
||||||
|
debros ALL=(ALL) NOPASSWD: /usr/bin/systemctl start orama-deploy-*
|
||||||
|
debros ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop orama-deploy-*
|
||||||
|
debros ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart orama-deploy-*
|
||||||
|
debros ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable orama-deploy-*
|
||||||
|
debros ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable orama-deploy-*
|
||||||
|
debros ALL=(ALL) NOPASSWD: /usr/bin/systemctl status orama-deploy-*
|
||||||
|
|
||||||
|
# Service file management (tee to write, rm to remove)
|
||||||
|
debros ALL=(ALL) NOPASSWD: /usr/bin/tee /etc/systemd/system/orama-deploy-*.service
|
||||||
|
debros ALL=(ALL) NOPASSWD: /bin/rm -f /etc/systemd/system/orama-deploy-*.service
|
||||||
|
`
|
||||||
|
|
||||||
|
// Write sudoers rule
|
||||||
|
if err := os.WriteFile(sudoersFile, []byte(sudoersContent), 0440); err != nil {
|
||||||
|
return fmt.Errorf("failed to create deployment sudoers rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate sudoers file
|
||||||
|
cmd := exec.Command("visudo", "-c", "-f", sudoersFile)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
os.Remove(sudoersFile) // Clean up on validation failure
|
||||||
|
return fmt.Errorf("deployment sudoers rule validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// StateDetector checks for existing production state
|
// StateDetector checks for existing production state
|
||||||
type StateDetector struct {
|
type StateDetector struct {
|
||||||
oramaDir string
|
oramaDir string
|
||||||
|
|||||||
@ -231,18 +231,13 @@ StandardOutput=append:%[4]s
|
|||||||
StandardError=append:%[4]s
|
StandardError=append:%[4]s
|
||||||
SyslogIdentifier=debros-node
|
SyslogIdentifier=debros-node
|
||||||
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
||||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
|
||||||
|
|
||||||
PrivateTmp=yes
|
PrivateTmp=yes
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=read-only
|
ProtectHome=read-only
|
||||||
ProtectKernelTunables=yes
|
ProtectKernelTunables=yes
|
||||||
ProtectKernelModules=yes
|
ProtectKernelModules=yes
|
||||||
ProtectControlGroups=yes
|
ProtectControlGroups=yes
|
||||||
RestrictRealtime=yes
|
RestrictRealtime=yes
|
||||||
RestrictSUIDSGID=yes
|
ReadWritePaths=%[2]s /etc/systemd/system
|
||||||
ReadWritePaths=%[2]s
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@ -19,7 +19,7 @@ type Config struct {
|
|||||||
TLSCacheDir string // Directory to cache TLS certificates (default: ~/.orama/tls-cache)
|
TLSCacheDir string // Directory to cache TLS certificates (default: ~/.orama/tls-cache)
|
||||||
|
|
||||||
// Domain routing configuration
|
// Domain routing configuration
|
||||||
BaseDomain string // Base domain for deployment routing (e.g., "dbrs.space"). Defaults to "orama.network"
|
BaseDomain string // Base domain for deployment routing. Set via node config http_gateway.base_domain. Defaults to "dbrs.space"
|
||||||
|
|
||||||
// Data directory configuration
|
// Data directory configuration
|
||||||
DataDir string // Base directory for node-local data (SQLite databases, deployments). Defaults to ~/.orama
|
DataDir string // Base directory for node-local data (SQLite databases, deployments). Defaults to ~/.orama
|
||||||
|
|||||||
@ -106,8 +106,8 @@ func (h *GoHandler) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create DNS records
|
// Create DNS records (use background context since HTTP context will be cancelled)
|
||||||
go h.service.CreateDNSRecords(ctx, deployment)
|
go h.service.CreateDNSRecords(context.Background(), deployment)
|
||||||
|
|
||||||
// Build response
|
// Build response
|
||||||
urls := h.service.BuildDeploymentURLs(deployment)
|
urls := h.service.BuildDeploymentURLs(deployment)
|
||||||
|
|||||||
@ -65,8 +65,9 @@ func (h *ListHandler) HandleList(w http.ResponseWriter, r *http.Request) {
|
|||||||
baseDomain := h.service.BaseDomain()
|
baseDomain := h.service.BaseDomain()
|
||||||
deployments := make([]map[string]interface{}, len(rows))
|
deployments := make([]map[string]interface{}, len(rows))
|
||||||
for i, row := range rows {
|
for i, row := range rows {
|
||||||
|
shortNodeID := GetShortNodeID(row.HomeNodeID)
|
||||||
urls := []string{
|
urls := []string{
|
||||||
"https://" + row.Name + "." + row.HomeNodeID + "." + baseDomain,
|
"https://" + row.Name + "." + shortNodeID + "." + baseDomain,
|
||||||
}
|
}
|
||||||
if row.Subdomain != "" {
|
if row.Subdomain != "" {
|
||||||
urls = append(urls, "https://"+row.Subdomain+"."+baseDomain)
|
urls = append(urls, "https://"+row.Subdomain+"."+baseDomain)
|
||||||
|
|||||||
@ -110,8 +110,8 @@ func (h *NextJSHandler) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create DNS records
|
// Create DNS records (use background context since HTTP context will be cancelled)
|
||||||
go h.service.CreateDNSRecords(ctx, deployment)
|
go h.service.CreateDNSRecords(context.Background(), deployment)
|
||||||
|
|
||||||
// Build response
|
// Build response
|
||||||
urls := h.service.BuildDeploymentURLs(deployment)
|
urls := h.service.BuildDeploymentURLs(deployment)
|
||||||
@ -186,6 +186,12 @@ func (h *NextJSHandler) deploySSR(ctx context.Context, namespace, name, subdomai
|
|||||||
}
|
}
|
||||||
|
|
||||||
deployment.Status = deployments.DeploymentStatusActive
|
deployment.Status = deployments.DeploymentStatusActive
|
||||||
|
|
||||||
|
// Update status in database
|
||||||
|
if err := h.service.UpdateDeploymentStatus(ctx, deployment.ID, deployment.Status); err != nil {
|
||||||
|
h.logger.Warn("Failed to update deployment status", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
return deployment, nil
|
return deployment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -107,8 +107,8 @@ func (h *NodeJSHandler) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create DNS records
|
// Create DNS records (use background context since HTTP context will be cancelled)
|
||||||
go h.service.CreateDNSRecords(ctx, deployment)
|
go h.service.CreateDNSRecords(context.Background(), deployment)
|
||||||
|
|
||||||
// Build response
|
// Build response
|
||||||
urls := h.service.BuildDeploymentURLs(deployment)
|
urls := h.service.BuildDeploymentURLs(deployment)
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func NewDeploymentService(
|
|||||||
homeNodeManager: homeNodeManager,
|
homeNodeManager: homeNodeManager,
|
||||||
portAllocator: portAllocator,
|
portAllocator: portAllocator,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
baseDomain: "orama.network", // default
|
baseDomain: "dbrs.space", // default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,11 +47,26 @@ func (s *DeploymentService) SetBaseDomain(domain string) {
|
|||||||
// BaseDomain returns the configured base domain
|
// BaseDomain returns the configured base domain
|
||||||
func (s *DeploymentService) BaseDomain() string {
|
func (s *DeploymentService) BaseDomain() string {
|
||||||
if s.baseDomain == "" {
|
if s.baseDomain == "" {
|
||||||
return "orama.network"
|
return "dbrs.space"
|
||||||
}
|
}
|
||||||
return s.baseDomain
|
return s.baseDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetShortNodeID extracts a short node ID from a full peer ID for domain naming.
|
||||||
|
// e.g., "12D3KooWGqyuQR8N..." -> "node-GqyuQR"
|
||||||
|
// If the ID is already short (starts with "node-"), returns it as-is.
|
||||||
|
func GetShortNodeID(peerID string) string {
|
||||||
|
// If already a short ID, return as-is
|
||||||
|
if len(peerID) < 20 {
|
||||||
|
return peerID
|
||||||
|
}
|
||||||
|
// Skip "12D3KooW" prefix (8 chars) and take next 6 chars
|
||||||
|
if len(peerID) > 14 {
|
||||||
|
return "node-" + peerID[8:14]
|
||||||
|
}
|
||||||
|
return "node-" + peerID[:6]
|
||||||
|
}
|
||||||
|
|
||||||
// CreateDeployment creates a new deployment
|
// CreateDeployment creates a new deployment
|
||||||
func (s *DeploymentService) CreateDeployment(ctx context.Context, deployment *deployments.Deployment) error {
|
func (s *DeploymentService) CreateDeployment(ctx context.Context, deployment *deployments.Deployment) error {
|
||||||
// Assign home node if not already assigned
|
// Assign home node if not already assigned
|
||||||
@ -273,24 +288,31 @@ func (s *DeploymentService) UpdateDeploymentStatus(ctx context.Context, deployme
|
|||||||
|
|
||||||
// CreateDNSRecords creates DNS records for a deployment
|
// CreateDNSRecords creates DNS records for a deployment
|
||||||
func (s *DeploymentService) CreateDNSRecords(ctx context.Context, deployment *deployments.Deployment) error {
|
func (s *DeploymentService) CreateDNSRecords(ctx context.Context, deployment *deployments.Deployment) error {
|
||||||
// Get node IP
|
// Get node IP using the full node ID
|
||||||
nodeIP, err := s.getNodeIP(ctx, deployment.HomeNodeID)
|
nodeIP, err := s.getNodeIP(ctx, deployment.HomeNodeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to get node IP", zap.Error(err))
|
s.logger.Error("Failed to get node IP", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create node-specific record
|
// Use short node ID for the domain (e.g., node-kv4la8 instead of full peer ID)
|
||||||
nodeFQDN := fmt.Sprintf("%s.%s.%s.", deployment.Name, deployment.HomeNodeID, s.BaseDomain())
|
shortNodeID := GetShortNodeID(deployment.HomeNodeID)
|
||||||
|
|
||||||
|
// Create node-specific record: {name}.node-{shortID}.{baseDomain}
|
||||||
|
nodeFQDN := fmt.Sprintf("%s.%s.%s.", deployment.Name, shortNodeID, s.BaseDomain())
|
||||||
if err := s.createDNSRecord(ctx, nodeFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil {
|
if err := s.createDNSRecord(ctx, nodeFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil {
|
||||||
s.logger.Error("Failed to create node-specific DNS record", zap.Error(err))
|
s.logger.Error("Failed to create node-specific DNS record", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
s.logger.Info("Created node-specific DNS record", zap.String("fqdn", nodeFQDN), zap.String("ip", nodeIP))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create load-balanced record if subdomain is set
|
// Create load-balanced record if subdomain is set: {subdomain}.{baseDomain}
|
||||||
if deployment.Subdomain != "" {
|
if deployment.Subdomain != "" {
|
||||||
lbFQDN := fmt.Sprintf("%s.%s.", deployment.Subdomain, s.BaseDomain())
|
lbFQDN := fmt.Sprintf("%s.%s.", deployment.Subdomain, s.BaseDomain())
|
||||||
if err := s.createDNSRecord(ctx, lbFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil {
|
if err := s.createDNSRecord(ctx, lbFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil {
|
||||||
s.logger.Error("Failed to create load-balanced DNS record", zap.Error(err))
|
s.logger.Error("Failed to create load-balanced DNS record", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
s.logger.Info("Created load-balanced DNS record", zap.String("fqdn", lbFQDN), zap.String("ip", nodeIP))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,30 +332,47 @@ func (s *DeploymentService) createDNSRecord(ctx context.Context, fqdn, recordTyp
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getNodeIP retrieves the IP address for a node
|
// getNodeIP retrieves the IP address for a node.
|
||||||
|
// It tries to find the node by full peer ID first, then by short node ID.
|
||||||
func (s *DeploymentService) getNodeIP(ctx context.Context, nodeID string) (string, error) {
|
func (s *DeploymentService) getNodeIP(ctx context.Context, nodeID string) (string, error) {
|
||||||
type nodeRow struct {
|
type nodeRow struct {
|
||||||
IPAddress string `db:"ip_address"`
|
IPAddress string `db:"ip_address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows []nodeRow
|
var rows []nodeRow
|
||||||
|
|
||||||
|
// Try full node ID first
|
||||||
query := `SELECT ip_address FROM dns_nodes WHERE id = ? LIMIT 1`
|
query := `SELECT ip_address FROM dns_nodes WHERE id = ? LIMIT 1`
|
||||||
err := s.db.Query(ctx, &rows, query, nodeID)
|
err := s.db.Query(ctx, &rows, query, nodeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rows) == 0 {
|
// If found, return it
|
||||||
return "", fmt.Errorf("node not found: %s", nodeID)
|
if len(rows) > 0 {
|
||||||
|
return rows[0].IPAddress, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try with short node ID if the original was a full peer ID
|
||||||
|
shortID := GetShortNodeID(nodeID)
|
||||||
|
if shortID != nodeID {
|
||||||
|
err = s.db.Query(ctx, &rows, query, shortID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(rows) > 0 {
|
||||||
return rows[0].IPAddress, nil
|
return rows[0].IPAddress, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("node not found: %s (tried: %s, %s)", nodeID, nodeID, shortID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildDeploymentURLs builds all URLs for a deployment
|
// BuildDeploymentURLs builds all URLs for a deployment
|
||||||
func (s *DeploymentService) BuildDeploymentURLs(deployment *deployments.Deployment) []string {
|
func (s *DeploymentService) BuildDeploymentURLs(deployment *deployments.Deployment) []string {
|
||||||
|
shortNodeID := GetShortNodeID(deployment.HomeNodeID)
|
||||||
urls := []string{
|
urls := []string{
|
||||||
fmt.Sprintf("https://%s.%s.%s", deployment.Name, deployment.HomeNodeID, s.BaseDomain()),
|
fmt.Sprintf("https://%s.%s.%s", deployment.Name, shortNodeID, s.BaseDomain()),
|
||||||
}
|
}
|
||||||
|
|
||||||
if deployment.Subdomain != "" {
|
if deployment.Subdomain != "" {
|
||||||
|
|||||||
@ -154,8 +154,8 @@ func (h *StaticDeploymentHandler) HandleUpload(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create DNS records
|
// Create DNS records (use background context since HTTP context will be cancelled)
|
||||||
go h.service.CreateDNSRecords(ctx, deployment)
|
go h.service.CreateDNSRecords(context.Background(), deployment)
|
||||||
|
|
||||||
// Build URLs
|
// Build URLs
|
||||||
urls := h.service.BuildDeploymentURLs(deployment)
|
urls := h.service.BuildDeploymentURLs(deployment)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package gateway
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@ -198,7 +199,7 @@ func isPublicPath(p string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch p {
|
switch p {
|
||||||
case "/health", "/v1/health", "/status", "/v1/status", "/v1/auth/jwks", "/.well-known/jwks.json", "/v1/version", "/v1/auth/login", "/v1/auth/challenge", "/v1/auth/verify", "/v1/auth/register", "/v1/auth/refresh", "/v1/auth/logout", "/v1/auth/api-key", "/v1/auth/simple-key", "/v1/network/status", "/v1/network/peers":
|
case "/health", "/v1/health", "/status", "/v1/status", "/v1/auth/jwks", "/.well-known/jwks.json", "/v1/version", "/v1/auth/login", "/v1/auth/challenge", "/v1/auth/verify", "/v1/auth/register", "/v1/auth/refresh", "/v1/auth/logout", "/v1/auth/api-key", "/v1/auth/simple-key", "/v1/network/status", "/v1/network/peers", "/v1/internal/tls/check":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@ -491,6 +492,10 @@ func (g *Gateway) domainRoutingMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getDeploymentByDomain looks up a deployment by its domain
|
// getDeploymentByDomain looks up a deployment by its domain
|
||||||
|
// Supports formats like:
|
||||||
|
// - {name}.node-{shortID}.{baseDomain} (e.g., myapp.node-kv4la8.dbrs.space)
|
||||||
|
// - {name}.{baseDomain} (e.g., myapp.dbrs.space for load-balanced/custom subdomain)
|
||||||
|
// - custom domains via deployment_domains table
|
||||||
func (g *Gateway) getDeploymentByDomain(ctx context.Context, domain string) (*deployments.Deployment, error) {
|
func (g *Gateway) getDeploymentByDomain(ctx context.Context, domain string) (*deployments.Deployment, error) {
|
||||||
if g.deploymentService == nil {
|
if g.deploymentService == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -499,44 +504,47 @@ func (g *Gateway) getDeploymentByDomain(ctx context.Context, domain string) (*de
|
|||||||
// Strip trailing dot if present
|
// Strip trailing dot if present
|
||||||
domain = strings.TrimSuffix(domain, ".")
|
domain = strings.TrimSuffix(domain, ".")
|
||||||
|
|
||||||
// Get base domain from config (default to orama.network)
|
// Get base domain from config (default to dbrs.space)
|
||||||
baseDomain := "orama.network"
|
baseDomain := "dbrs.space"
|
||||||
if g.cfg != nil && g.cfg.BaseDomain != "" {
|
if g.cfg != nil && g.cfg.BaseDomain != "" {
|
||||||
baseDomain = g.cfg.BaseDomain
|
baseDomain = g.cfg.BaseDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query deployment by domain (node-specific subdomain or custom domain)
|
// Query deployment by domain
|
||||||
|
// We need to match:
|
||||||
|
// 1. {name}.node-{shortID}.{baseDomain} - extract shortID and find deployment where
|
||||||
|
// 'node-' || substr(home_node_id, 9, 6) matches the node part
|
||||||
|
// 2. {subdomain}.{baseDomain} - match by subdomain field
|
||||||
|
// 3. Custom verified domain from deployment_domains table
|
||||||
db := g.client.Database()
|
db := g.client.Database()
|
||||||
internalCtx := client.WithInternalAuth(ctx)
|
internalCtx := client.WithInternalAuth(ctx)
|
||||||
|
|
||||||
|
// First, try to parse the domain to extract deployment name and node ID
|
||||||
|
// Format: {name}.node-{shortID}.{baseDomain}
|
||||||
|
suffix := "." + baseDomain
|
||||||
|
if strings.HasSuffix(domain, suffix) {
|
||||||
|
subdomain := strings.TrimSuffix(domain, suffix)
|
||||||
|
parts := strings.Split(subdomain, ".")
|
||||||
|
|
||||||
|
// If we have 2 parts and second starts with "node-", it's a node-specific domain
|
||||||
|
if len(parts) == 2 && strings.HasPrefix(parts[1], "node-") {
|
||||||
|
deploymentName := parts[0]
|
||||||
|
shortNodeID := parts[1] // e.g., "node-kv4la8"
|
||||||
|
|
||||||
|
// Query by name and matching short node ID
|
||||||
|
// Short ID is derived from peer ID: 'node-' + chars 9-14 of home_node_id
|
||||||
query := `
|
query := `
|
||||||
SELECT d.id, d.namespace, d.name, d.type, d.port, d.content_cid, d.status
|
SELECT id, namespace, name, type, port, content_cid, status, home_node_id
|
||||||
FROM deployments d
|
FROM deployments
|
||||||
LEFT JOIN deployment_domains dd ON d.id = dd.deployment_id
|
WHERE name = ?
|
||||||
WHERE (d.name || '.' || d.home_node_id || '.' || ? = ?
|
AND ('node-' || substr(home_node_id, 9, 6) = ? OR home_node_id = ?)
|
||||||
OR d.name || '.node-' || d.home_node_id || '.' || ? = ?
|
AND status = 'active'
|
||||||
OR d.name || '.' || ? = ?
|
|
||||||
OR dd.domain = ? AND dd.verified_at IS NOT NULL)
|
|
||||||
AND d.status = 'active'
|
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
result, err := db.Query(internalCtx, query, deploymentName, shortNodeID, shortNodeID)
|
||||||
result, err := db.Query(internalCtx, query, baseDomain, domain, baseDomain, domain, baseDomain, domain, domain)
|
if err == nil && len(result.Rows) > 0 {
|
||||||
if err != nil || result.Count == 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result.Rows) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
row := result.Rows[0]
|
row := result.Rows[0]
|
||||||
if len(row) < 7 {
|
return &deployments.Deployment{
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create deployment object
|
|
||||||
deployment := &deployments.Deployment{
|
|
||||||
ID: getString(row[0]),
|
ID: getString(row[0]),
|
||||||
Namespace: getString(row[1]),
|
Namespace: getString(row[1]),
|
||||||
Name: getString(row[2]),
|
Name: getString(row[2]),
|
||||||
@ -544,19 +552,90 @@ func (g *Gateway) getDeploymentByDomain(ctx context.Context, domain string) (*de
|
|||||||
Port: getInt(row[4]),
|
Port: getInt(row[4]),
|
||||||
ContentCID: getString(row[5]),
|
ContentCID: getString(row[5]),
|
||||||
Status: deployments.DeploymentStatus(getString(row[6])),
|
Status: deployments.DeploymentStatus(getString(row[6])),
|
||||||
|
HomeNodeID: getString(row[7]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return deployment, nil
|
// Single subdomain: match by subdomain field (e.g., myapp.dbrs.space)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
query := `
|
||||||
|
SELECT id, namespace, name, type, port, content_cid, status, home_node_id
|
||||||
|
FROM deployments
|
||||||
|
WHERE subdomain = ?
|
||||||
|
AND status = 'active'
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
result, err := db.Query(internalCtx, query, parts[0])
|
||||||
|
if err == nil && len(result.Rows) > 0 {
|
||||||
|
row := result.Rows[0]
|
||||||
|
return &deployments.Deployment{
|
||||||
|
ID: getString(row[0]),
|
||||||
|
Namespace: getString(row[1]),
|
||||||
|
Name: getString(row[2]),
|
||||||
|
Type: deployments.DeploymentType(getString(row[3])),
|
||||||
|
Port: getInt(row[4]),
|
||||||
|
ContentCID: getString(row[5]),
|
||||||
|
Status: deployments.DeploymentStatus(getString(row[6])),
|
||||||
|
HomeNodeID: getString(row[7]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try custom domain from deployment_domains table
|
||||||
|
query := `
|
||||||
|
SELECT d.id, d.namespace, d.name, d.type, d.port, d.content_cid, d.status, d.home_node_id
|
||||||
|
FROM deployments d
|
||||||
|
JOIN deployment_domains dd ON d.id = dd.deployment_id
|
||||||
|
WHERE dd.domain = ? AND dd.verified_at IS NOT NULL
|
||||||
|
AND d.status = 'active'
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
result, err := db.Query(internalCtx, query, domain)
|
||||||
|
if err == nil && len(result.Rows) > 0 {
|
||||||
|
row := result.Rows[0]
|
||||||
|
return &deployments.Deployment{
|
||||||
|
ID: getString(row[0]),
|
||||||
|
Namespace: getString(row[1]),
|
||||||
|
Name: getString(row[2]),
|
||||||
|
Type: deployments.DeploymentType(getString(row[3])),
|
||||||
|
Port: getInt(row[4]),
|
||||||
|
ContentCID: getString(row[5]),
|
||||||
|
Status: deployments.DeploymentStatus(getString(row[6])),
|
||||||
|
HomeNodeID: getString(row[7]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// proxyToDynamicDeployment proxies requests to a dynamic deployment's local port
|
// proxyToDynamicDeployment proxies requests to a dynamic deployment's local port
|
||||||
|
// If the deployment is on a different node, it forwards the request to that node
|
||||||
func (g *Gateway) proxyToDynamicDeployment(w http.ResponseWriter, r *http.Request, deployment *deployments.Deployment) {
|
func (g *Gateway) proxyToDynamicDeployment(w http.ResponseWriter, r *http.Request, deployment *deployments.Deployment) {
|
||||||
if deployment.Port == 0 {
|
if deployment.Port == 0 {
|
||||||
http.Error(w, "Deployment has no assigned port", http.StatusServiceUnavailable)
|
http.Error(w, "Deployment has no assigned port", http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a simple reverse proxy
|
// Check if request was already forwarded by another node (loop prevention)
|
||||||
|
proxyNode := r.Header.Get("X-Orama-Proxy-Node")
|
||||||
|
|
||||||
|
// Check if this deployment is on the current node
|
||||||
|
if g.nodePeerID != "" && deployment.HomeNodeID != "" &&
|
||||||
|
deployment.HomeNodeID != g.nodePeerID && proxyNode == "" {
|
||||||
|
// Need to proxy to home node
|
||||||
|
if g.proxyCrossNode(w, r, deployment) {
|
||||||
|
return // Request was proxied successfully
|
||||||
|
}
|
||||||
|
// Fall through if cross-node proxy failed - try local anyway
|
||||||
|
g.logger.Warn("Cross-node proxy failed, attempting local fallback",
|
||||||
|
zap.String("deployment", deployment.Name),
|
||||||
|
zap.String("home_node", deployment.HomeNodeID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a simple reverse proxy to localhost
|
||||||
target := "http://localhost:" + strconv.Itoa(deployment.Port)
|
target := "http://localhost:" + strconv.Itoa(deployment.Port)
|
||||||
|
|
||||||
// Set proxy headers
|
// Set proxy headers
|
||||||
@ -584,8 +663,8 @@ func (g *Gateway) proxyToDynamicDeployment(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute proxy request
|
// Execute proxy request
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
httpClient := &http.Client{Timeout: 30 * time.Second}
|
||||||
resp, err := client.Do(proxyReq)
|
resp, err := httpClient.Do(proxyReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.logger.ComponentError(logging.ComponentGeneral, "proxy request failed",
|
g.logger.ComponentError(logging.ComponentGeneral, "proxy request failed",
|
||||||
zap.String("target", target),
|
zap.String("target", target),
|
||||||
@ -610,6 +689,94 @@ func (g *Gateway) proxyToDynamicDeployment(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// proxyCrossNode forwards a request to the home node of a deployment
|
||||||
|
// Returns true if the request was successfully forwarded, false otherwise
|
||||||
|
func (g *Gateway) proxyCrossNode(w http.ResponseWriter, r *http.Request, deployment *deployments.Deployment) bool {
|
||||||
|
// Get home node IP from dns_nodes table
|
||||||
|
db := g.client.Database()
|
||||||
|
internalCtx := client.WithInternalAuth(r.Context())
|
||||||
|
|
||||||
|
query := "SELECT ip_address FROM dns_nodes WHERE id = ? LIMIT 1"
|
||||||
|
result, err := db.Query(internalCtx, query, deployment.HomeNodeID)
|
||||||
|
if err != nil || result == nil || len(result.Rows) == 0 {
|
||||||
|
g.logger.Warn("Failed to get home node IP",
|
||||||
|
zap.String("home_node_id", deployment.HomeNodeID),
|
||||||
|
zap.Error(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
homeIP := getString(result.Rows[0][0])
|
||||||
|
if homeIP == "" {
|
||||||
|
g.logger.Warn("Home node IP is empty", zap.String("home_node_id", deployment.HomeNodeID))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.logger.Info("Proxying request to home node",
|
||||||
|
zap.String("deployment", deployment.Name),
|
||||||
|
zap.String("home_node_id", deployment.HomeNodeID),
|
||||||
|
zap.String("home_ip", homeIP),
|
||||||
|
zap.String("current_node", g.nodePeerID),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Proxy to home node via HTTPS
|
||||||
|
// Use the original Host header so the home node's TLS works correctly
|
||||||
|
targetURL := "https://" + homeIP + r.URL.Path
|
||||||
|
if r.URL.RawQuery != "" {
|
||||||
|
targetURL += "?" + r.URL.RawQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
|
||||||
|
if err != nil {
|
||||||
|
g.logger.Error("Failed to create cross-node proxy request", zap.Error(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy headers and set Host header to original host
|
||||||
|
for key, values := range r.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
proxyReq.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxyReq.Host = r.Host // Keep original host for TLS SNI
|
||||||
|
proxyReq.Header.Set("X-Forwarded-For", getClientIP(r))
|
||||||
|
proxyReq.Header.Set("X-Orama-Proxy-Node", g.nodePeerID) // Prevent loops
|
||||||
|
|
||||||
|
// Skip TLS verification since we're connecting by IP with a Host header
|
||||||
|
// The home node has the correct certificate for the domain
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
ServerName: r.Host, // Use original host for SNI
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(proxyReq)
|
||||||
|
if err != nil {
|
||||||
|
g.logger.Error("Cross-node proxy request failed",
|
||||||
|
zap.String("target_ip", homeIP),
|
||||||
|
zap.String("host", r.Host),
|
||||||
|
zap.Error(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Copy response headers
|
||||||
|
for key, values := range resp.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
w.Header().Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write status code and body
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions for type conversion
|
// Helper functions for type conversion
|
||||||
func getString(v interface{}) string {
|
func getString(v interface{}) string {
|
||||||
if s, ok := v.(string); ok {
|
if s, ok := v.(string); ok {
|
||||||
|
|||||||
@ -15,6 +15,9 @@ func (g *Gateway) Routes() http.Handler {
|
|||||||
mux.HandleFunc("/v1/version", g.versionHandler)
|
mux.HandleFunc("/v1/version", g.versionHandler)
|
||||||
mux.HandleFunc("/v1/status", g.statusHandler)
|
mux.HandleFunc("/v1/status", g.statusHandler)
|
||||||
|
|
||||||
|
// TLS check endpoint for Caddy on-demand TLS
|
||||||
|
mux.HandleFunc("/v1/internal/tls/check", g.tlsCheckHandler)
|
||||||
|
|
||||||
// auth endpoints
|
// auth endpoints
|
||||||
mux.HandleFunc("/v1/auth/jwks", g.authService.JWKSHandler)
|
mux.HandleFunc("/v1/auth/jwks", g.authService.JWKSHandler)
|
||||||
mux.HandleFunc("/.well-known/jwks.json", g.authService.JWKSHandler)
|
mux.HandleFunc("/.well-known/jwks.json", g.authService.JWKSHandler)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/DeBrosOfficial/network/pkg/client"
|
"github.com/DeBrosOfficial/network/pkg/client"
|
||||||
@ -86,3 +87,29 @@ func (g *Gateway) versionHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"uptime": time.Since(g.startedAt).String(),
|
"uptime": time.Since(g.startedAt).String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tlsCheckHandler validates if a domain should receive a TLS certificate
|
||||||
|
// Used by Caddy's on-demand TLS feature to prevent abuse
|
||||||
|
func (g *Gateway) tlsCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
domain := r.URL.Query().Get("domain")
|
||||||
|
if domain == "" {
|
||||||
|
http.Error(w, "domain parameter required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get base domain from config
|
||||||
|
baseDomain := "dbrs.space"
|
||||||
|
if g.cfg != nil && g.cfg.BaseDomain != "" {
|
||||||
|
baseDomain = g.cfg.BaseDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow any subdomain of our base domain
|
||||||
|
if strings.HasSuffix(domain, "."+baseDomain) || domain == baseDomain {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain not allowed - only allow subdomains of our base domain
|
||||||
|
// Custom domains would need to be verified separately
|
||||||
|
http.Error(w, "domain not allowed", http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|||||||
@ -422,21 +422,93 @@ func splitSQLStatements(in string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional helper to load embedded migrations if you later decide to embed.
|
// ApplyEmbeddedMigrations applies migrations from an embedded filesystem.
|
||||||
// Keep for future use; currently unused.
|
// This is the preferred method as it doesn't depend on filesystem paths.
|
||||||
func readDirFS(fsys fs.FS, root string) ([]string, error) {
|
func ApplyEmbeddedMigrations(ctx context.Context, db *sql.DB, fsys fs.FS, logger *zap.Logger) error {
|
||||||
var files []string
|
if logger == nil {
|
||||||
err := fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureMigrationsTable(ctx, db); err != nil {
|
||||||
|
return fmt.Errorf("ensure schema_migrations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := readMigrationFilesFromFS(fsys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("read embedded migration files: %w", err)
|
||||||
}
|
}
|
||||||
if d.IsDir() {
|
if len(files) == 0 {
|
||||||
|
logger.Info("No embedded migrations found")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(strings.ToLower(d.Name()), ".sql") {
|
|
||||||
files = append(files, path)
|
applied, err := loadAppliedVersions(ctx, db)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load applied versions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, mf := range files {
|
||||||
|
if applied[mf.Version] {
|
||||||
|
logger.Debug("Migration already applied; skipping", zap.Int("version", mf.Version), zap.String("name", mf.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlBytes, err := fs.ReadFile(fsys, mf.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read embedded migration %s: %w", mf.Path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Applying migration", zap.Int("version", mf.Version), zap.String("name", mf.Name))
|
||||||
|
if err := applySQL(ctx, db, string(sqlBytes)); err != nil {
|
||||||
|
return fmt.Errorf("apply migration %d (%s): %w", mf.Version, mf.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.ExecContext(ctx, `INSERT OR IGNORE INTO schema_migrations(version) VALUES (?)`, mf.Version); err != nil {
|
||||||
|
return fmt.Errorf("record migration %d: %w", mf.Version, err)
|
||||||
|
}
|
||||||
|
logger.Info("Migration applied", zap.Int("version", mf.Version), zap.String("name", mf.Name))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
}
|
||||||
return files, err
|
|
||||||
|
// ApplyEmbeddedMigrations is a convenience helper bound to RQLiteManager.
|
||||||
|
func (r *RQLiteManager) ApplyEmbeddedMigrations(ctx context.Context, fsys fs.FS) error {
|
||||||
|
db, err := sql.Open("rqlite", fmt.Sprintf("http://localhost:%d", r.config.RQLitePort))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open rqlite db: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
return ApplyEmbeddedMigrations(ctx, db, fsys, r.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readMigrationFilesFromFS reads migration files from an embedded filesystem.
|
||||||
|
func readMigrationFilesFromFS(fsys fs.FS) ([]migrationFile, error) {
|
||||||
|
entries, err := fs.ReadDir(fsys, ".")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []migrationFile
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := e.Name()
|
||||||
|
if !strings.HasSuffix(strings.ToLower(name), ".sql") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ver, ok := parseVersionPrefix(name)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, migrationFile{
|
||||||
|
Version: ver,
|
||||||
|
Name: name,
|
||||||
|
Path: name, // In embedded FS, path is just the filename
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Version < out[j].Version })
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/DeBrosOfficial/network/migrations"
|
||||||
"github.com/DeBrosOfficial/network/pkg/config"
|
"github.com/DeBrosOfficial/network/pkg/config"
|
||||||
"github.com/rqlite/gorqlite"
|
"github.com/rqlite/gorqlite"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -73,8 +74,14 @@ func (r *RQLiteManager) Start(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
migrationsDir, _ := r.resolveMigrationsDir()
|
// Apply embedded migrations - these are compiled into the binary
|
||||||
_ = r.ApplyMigrations(ctx, migrationsDir)
|
if err := r.ApplyEmbeddedMigrations(ctx, migrations.FS); err != nil {
|
||||||
|
r.logger.Error("Failed to apply embedded migrations", zap.Error(err))
|
||||||
|
// Don't fail startup - migrations may have already been applied by another node
|
||||||
|
// or we may be joining an existing cluster
|
||||||
|
} else {
|
||||||
|
r.logger.Info("Database migrations applied successfully")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
testdata/apps/go-backend/app
vendored
Executable file
BIN
testdata/apps/go-backend/app
vendored
Executable file
Binary file not shown.
37
testdata/apps/nodejs-backend/index.js
vendored
Normal file
37
testdata/apps/nodejs-backend/index.js
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = req.url;
|
||||||
|
|
||||||
|
if (url === '/health' || url === '/health/') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
status: 'healthy',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
service: 'nodejs-backend-test'
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === '/' || url === '/api') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
message: 'Hello from Node.js backend!',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
environment: {
|
||||||
|
port: PORT,
|
||||||
|
nodeVersion: process.version
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Not found' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Node.js backend listening on port ${PORT}`);
|
||||||
|
});
|
||||||
9
testdata/apps/nodejs-backend/package.json
vendored
Normal file
9
testdata/apps/nodejs-backend/package.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "nodejs-backend-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user