-
Notifications
You must be signed in to change notification settings - Fork 0
feat(api): Add multi-domain support with unified domain management #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
nfebe
commented
Jan 31, 2026
- Add DomainConfig type and Domains field to ServiceMetadata
- Add GetEffectiveDomains helper for backward compatibility
- Add domain CRUD API endpoints (list, add, update, delete)
- Support "default" ID for legacy networking.domain config
- Add SSL manager support for multi-domain certificates
- Add nginx manager support for multi-domain virtual hosts
- Add comprehensive domain API tests
Code Review SummaryThis review covers a significant feature addition introducing multi-domain and multi-database support for deployments. The changes involve updates to service metadata models, API handlers for domain and database management, and extensive refactoring of the Nginx proxy orchestration to support multiple domain configurations and SSL certificates. The new test cases are well-structured and provide good coverage for the added functionality. While many aspects of the implementation are well-considered, a critical architectural flaw and some areas for improved clarity have been identified. 🚀 Key Improvements
💡 Minor Suggestions
🚨 Critical Issues
|
🔍 Code Review💡 1. **internal/api/server.go** (Lines 3462-3471) - REFACTORThe logic here implicitly adds a "default" domain based on Suggested Code: Current Code: if deployment.Metadata == nil {
deployment.Metadata = &models.ServiceMetadata{}
}
if len(deployment.Metadata.Domains) == 0 && deployment.Metadata.Networking.Expose {
existingDomain := models.DomainConfig{
ID: "default",
Service: deployment.Metadata.Name,
ContainerPort: deployment.Metadata.Networking.ContainerPort,
Domain: deployment.Metadata.Networking.Domain,
SSL: deployment.Metadata.SSL,
}
deployment.Metadata.Domains = []models.DomainConfig{existingDomain}
}
deployment.Metadata.Domains = append(deployment.Metadata.Domains, domain)💡 2. **internal/api/server.go** (Lines 3551-3590) - REFACTORThe logic for handling the 'default' domain deletion and subsequent proxy teardown/setup is complex. It involves checking Suggested Code: Current Code: // Handle legacy "default" domain from networking config
if domainID == "default" && len(deployment.Metadata.Domains) == 0 {
if !deployment.Metadata.Networking.Expose || deployment.Metadata.Networking.Domain == "" {
c.JSON(http.StatusNotFound, gin.H{"error": "Domain not found"})
return
}
// Clear the legacy networking config
deployment.Metadata.Networking.Expose = false
deployment.Metadata.Networking.Domain = ""
deployment.Metadata.SSL.Enabled = false
deployment.Metadata.SSL.AutoCert = false
if err := s.manager.SaveMetadata(name, deployment.Metadata); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save metadata: " + err.Error()})
return
}
if s.proxyOrchestrator != nil {
if err := s.proxyOrchestrator.TeardownDeployment(name); err != nil {
log.Printf("Warning: failed to teardown proxy for %s: %v", name, err)
}
}
c.JSON(http.StatusOK, gin.H{"message": "Domain deleted successfully"})
return
}
if len(deployment.Metadata.Domains) == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Domain not found"})
return
}
found := false
newDomains := make([]models.DomainConfig, 0)
for _, d := range deployment.Metadata.Domains {
if d.ID == domainID {
found = true
continue
}
newDomains = append(newDomains, d)
}
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "Domain not found"})
return
}
if len(newDomains) == 0 {
deployment.Metadata.Domains = nil
} else {
deployment.Metadata.Domains = newDomains
}
if err := s.manager.SaveMetadata(name, deployment.Metadata); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save metadata: " + err.Error()})
return
}
if s.proxyOrchestrator != nil {
if len(newDomains) == 0 {
if deployment.Metadata.Networking.Expose {
if _, err := s.proxyOrchestrator.SetupDeployment(deployment); err != nil {
log.Printf("Warning: failed to setup legacy proxy for %s: %v", name, err)
}
} else {
if err := s.proxyOrchestrator.TeardownDeployment(name); err != nil {
log.Printf("Warning: failed to teardown proxy for %s: %v", name, err)
}
}
} else {
if _, err := s.proxyOrchestrator.SetupDeployment(deployment); err != nil {
log.Printf("Warning: failed to update proxy for %s: %v", name, err)
}
}
}
c.JSON(http.StatusOK, gin.H{
"message": "Domain deleted successfully",
})💡 3. **internal/nginx/manager.go** (Line 19) - REFACTORThe Suggested Code: Current Code: type Manager struct {
config *config.CertbotConfig
certsPath string
webRoot string
containerWebRoot string
mu sync.RWMutex
}💡 4. **internal/nginx/manager.go** (Lines 26-48) - REFACTORThe Suggested Code: Current Code: func NewManager(cfg *config.CertbotConfig, deploymentsPath string) *Manager {
certsPath := cfg.CertsPath
if certsPath == "" {
certsPath = filepath.Join(deploymentsPath, "nginx", "certs", "live")
}
webRoot := cfg.WebrootPath
if webRoot == "" {
webRoot = filepath.Join(deploymentsPath, "nginx", "html")
}
containerWebRoot := cfg.ContainerWebrootPath
if containerWebRoot == "" {
containerWebRoot = "/var/www/certbot"
}
return &Manager{
config: cfg,
certsPath: certsPath,
webRoot: webRoot,
containerWebRoot: containerWebRoot,
}
}💡 5. **internal/nginx/manager.go** (Lines 54-77) - REFACTORThe Suggested Code: Current Code: func (m *Manager) UpdateConfig(cfg *config.CertbotConfig, deploymentsPath string) {
m.mu.Lock()
defer m.mu.Unlock()
m.config = cfg
certsPath := cfg.CertsPath
if certsPath == "" {
certsPath = filepath.Join(deploymentsPath, "nginx", "certs", "live")
}
m.certsPath = certsPath
webRoot := cfg.WebrootPath
if webRoot == "" {
webRoot = filepath.Join(deploymentsPath, "nginx", "html")
}
m.webRoot = webRoot
containerWebRoot := cfg.ContainerWebrootPath
if containerWebRoot == "" {
containerWebRoot = "/var/www/certbot"
}
m.containerWebRoot = containerWebRoot
}💡 6. **internal/proxy/orchestrator.go** (Line 20) - REFACTORThe Suggested Code: Current Code: func NewOrchestrator(cfg *config.Config) *Orchestrator {
return &Orchestrator{
nginx: nginx.NewManager(&cfg.Nginx, cfg.DeploymentsPath, cfg.Certbot.WebrootPath),
ssl: ssl.NewManager(&cfg.Certbot, cfg.DeploymentsPath),
}
}💡 7. **internal/proxy/orchestrator.go** (Lines 42-43) - REFACTORThe Suggested Code: Current Code: func (o *Orchestrator) UpdateConfig(cfg *config.Config) {
o.nginx.UpdateConfig(&cfg.Nginx, cfg.DeploymentsPath, cfg.Certbot.WebrootPath)
o.ssl.UpdateConfig(&cfg.Certbot, cfg.DeploymentsPath)
}💡 8. **pkg/models/deployment.go** (Line 33) - IMPROVEMENTThe addition of Suggested Code: Current Code: CredentialID string `yaml:"credential_id,omitempty" json:"credential_id,omitempty"`💡 9. **pkg/models/deployment.go** (Lines 36-64) - IMPROVEMENTThe new Suggested Code: Verdict: APPROVE Posted as a comment because posting a review failed. |
44f27c0 to
407ca02
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review complete. See the overview comment for a summary.
407ca02 to
9f7c44c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review complete. See the overview comment for a summary.
9f7c44c to
d0fe206
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review complete. See the overview comment for a summary.
d0fe206 to
8a58ead
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review complete. See the overview comment for a summary.
dead86f to
16309c9
Compare
- Add DomainConfig type and Domains field to ServiceMetadata - Add GetDomains helper as single source of truth - Use GetDomains consistently in nginx and proxy managers - Remove duplicate legacy/multi-domain branching logic - Add rate limit support to multi-domain nginx templates - Add domain CRUD API endpoints (list, add, update, delete) - Support "default" ID for legacy networking.domain config - Add SSL manager support for multi-domain certificates - Add comprehensive domain API tests Signed-off-by: nfebe <fenn25.fn@gmail.com>
- Add DatabaseConfig struct to models with alias, type, mode fields - Add Databases field to ServiceMetadata with helper methods - Add createDatabasesForDeployment() for multiple database provisioning - Generate alias-prefixed env vars (e.g., PRIMARY_HOST, CACHE_HOST) - Maintain backward compatibility with legacy DB_* env vars - Fix deletion to iterate all domains and databases - Add deleteDatabaseByAlias() for multi-database cleanup - Add comprehensive unit tests for models and API Signed-off-by: nfebe <fenn25.fn@gmail.com>
Add Validate() method to DatabaseConfigRequest that validates: - Database type (mysql, postgres, mariadb, mongodb, redis) - Mode (shared, create, existing, external) - Required fields based on mode: - existing mode requires existing_container - external mode requires external_host and positive external_port Validation is called in createDeployment handler before processing databases array. Signed-off-by: nfebe <fenn25.fn@gmail.com>
16309c9 to
4d4ef08
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review complete. See the overview comment for a summary.