Major changes: - Add internal/logging package with field constants, context propagation, sensitive data auto-redaction, and per-component log levels - Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.) - Extend SDLC with callback handlers, generate endpoints, and executor - Add new cookbook trees for aeries and slackpath progression - Add skeleton templates for queue, realtime, and microservices - Add worker component template with async job processing - Refactor services and handlers to use new logging infrastructure - Split component.go into component_infra.go and component_listing.go Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
174 lines
5.4 KiB
Go
174 lines
5.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/logging"
|
|
)
|
|
|
|
// AddDomain adds a new domain to a project.
|
|
// For threesix.ai subdomains, it creates the DNS record automatically.
|
|
// For external domains, it returns instructions for the user to configure their DNS.
|
|
func (s *ProjectInfraService) AddDomain(ctx context.Context, req AddDomainRequest) (*domain.ProjectDomain, error) {
|
|
if s.domainRepo == nil {
|
|
return nil, fmt.Errorf("domain repository not configured")
|
|
}
|
|
|
|
// Validate domain format
|
|
if err := domain.ValidateFQDN(req.Domain); err != nil {
|
|
return nil, fmt.Errorf("invalid domain: %w", err)
|
|
}
|
|
|
|
// Check if domain already exists
|
|
exists, err := s.domainRepo.Exists(ctx, req.Domain)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check domain: %w", err)
|
|
}
|
|
if exists {
|
|
return nil, domain.ErrDuplicateDomain
|
|
}
|
|
|
|
// Default record type
|
|
recordType := req.RecordType
|
|
if recordType == "" {
|
|
recordType = "A"
|
|
}
|
|
|
|
// Default domain type
|
|
domainType := req.Type
|
|
if domainType == "" {
|
|
domainType = domain.DomainTypeAlias
|
|
}
|
|
|
|
pd := &domain.ProjectDomain{
|
|
ProjectID: req.ProjectID,
|
|
Domain: req.Domain,
|
|
Type: domainType,
|
|
DNSRecordType: recordType,
|
|
Verified: false,
|
|
}
|
|
|
|
// Create DNS record for threesix.ai subdomains
|
|
if domain.IsSubdomainOf(req.Domain, s.defaultDomain) && s.dns != nil {
|
|
subdomain := domain.ExtractSubdomain(req.Domain, s.defaultDomain)
|
|
content := s.clusterIP
|
|
if recordType == "CNAME" {
|
|
// CNAME points to the project's primary auto domain
|
|
status, err := s.GetStatus(ctx, req.ProjectID)
|
|
if err == nil && status.Slug != "" {
|
|
content = status.Slug + "." + s.defaultDomain
|
|
}
|
|
}
|
|
|
|
dnsRecord, err := s.dns.UpsertRecord(ctx, domain.DNSRecord{
|
|
Type: recordType,
|
|
Name: subdomain,
|
|
Content: content,
|
|
TTL: 1,
|
|
Proxied: req.Proxied,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to upsert DNS record: %w", err)
|
|
}
|
|
pd.DNSRecordID = dnsRecord.ID
|
|
pd.Verified = true
|
|
}
|
|
|
|
// Store in database
|
|
if err := s.domainRepo.Create(ctx, pd); err != nil {
|
|
// Rollback DNS record if database insert fails
|
|
if pd.DNSRecordID != "" && s.dns != nil {
|
|
_ = s.dns.DeleteRecord(ctx, pd.DNSRecordID)
|
|
}
|
|
return nil, fmt.Errorf("failed to store domain: %w", err)
|
|
}
|
|
|
|
// Add host to K8s ingress (for both threesix.ai and external domains)
|
|
log := logging.FromContext(ctx).WithService("project_infra")
|
|
if s.deployer != nil {
|
|
if err := s.deployer.AddIngressHost(ctx, req.ProjectID, req.Domain); err != nil {
|
|
log.Warn("failed to add ingress host", logging.FieldError, err, logging.FieldProjectID, req.ProjectID, "domain", req.Domain)
|
|
// Don't fail the request - DNS and DB are already set up
|
|
}
|
|
}
|
|
|
|
log.Info("domain added", logging.FieldProjectID, req.ProjectID, "domain", req.Domain, "type", domainType)
|
|
return pd, nil
|
|
}
|
|
|
|
// ListDomains returns all domains for a project.
|
|
func (s *ProjectInfraService) ListDomains(ctx context.Context, projectID string) ([]*domain.ProjectDomain, error) {
|
|
if s.domainRepo == nil {
|
|
return nil, fmt.Errorf("domain repository not configured")
|
|
}
|
|
return s.domainRepo.ListByProject(ctx, projectID)
|
|
}
|
|
|
|
// RemoveDomain removes a domain from a project.
|
|
// Returns error if attempting to remove the primary_auto domain.
|
|
func (s *ProjectInfraService) RemoveDomain(ctx context.Context, projectID, fqdn string) error {
|
|
if s.domainRepo == nil {
|
|
return fmt.Errorf("domain repository not configured")
|
|
}
|
|
|
|
// Get the domain record
|
|
pd, err := s.domainRepo.GetByDomain(ctx, fqdn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get domain: %w", err)
|
|
}
|
|
if pd == nil {
|
|
return domain.ErrDomainNotFound
|
|
}
|
|
|
|
// Verify it belongs to this project
|
|
if pd.ProjectID != projectID {
|
|
return fmt.Errorf("domain does not belong to project")
|
|
}
|
|
|
|
// Prevent deletion of primary_auto domain
|
|
if pd.Type == domain.DomainTypePrimaryAuto {
|
|
return fmt.Errorf("cannot remove auto-generated primary domain; delete the project instead")
|
|
}
|
|
|
|
log := logging.FromContext(ctx).WithService("project_infra")
|
|
|
|
// Delete DNS record if we have the record ID
|
|
if pd.DNSRecordID != "" && s.dns != nil {
|
|
if err := s.dns.DeleteRecord(ctx, pd.DNSRecordID); err != nil {
|
|
log.Warn("failed to delete DNS record", logging.FieldError, err, "domain", fqdn)
|
|
}
|
|
} else if domain.IsSubdomainOf(fqdn, s.defaultDomain) && s.dns != nil {
|
|
// Fallback: try to delete by name
|
|
subdomain := domain.ExtractSubdomain(fqdn, s.defaultDomain)
|
|
if subdomain != "" {
|
|
_ = s.dns.DeleteRecordByName(ctx, pd.DNSRecordType, subdomain)
|
|
}
|
|
}
|
|
|
|
// Remove host from K8s ingress
|
|
if s.deployer != nil {
|
|
if err := s.deployer.RemoveIngressHost(ctx, projectID, fqdn); err != nil {
|
|
log.Warn("failed to remove ingress host", logging.FieldError, err, logging.FieldProjectID, projectID, "domain", fqdn)
|
|
// Continue anyway - DNS record is already deleted
|
|
}
|
|
}
|
|
|
|
// Delete from database
|
|
if err := s.domainRepo.Delete(ctx, pd.ID); err != nil {
|
|
return fmt.Errorf("failed to delete domain: %w", err)
|
|
}
|
|
|
|
log.Info("domain removed", logging.FieldProjectID, projectID, "domain", fqdn)
|
|
return nil
|
|
}
|
|
|
|
// GetPrimaryDomain returns the primary domain for a project.
|
|
func (s *ProjectInfraService) GetPrimaryDomain(ctx context.Context, projectID string) (*domain.ProjectDomain, error) {
|
|
if s.domainRepo == nil {
|
|
return nil, fmt.Errorf("domain repository not configured")
|
|
}
|
|
return s.domainRepo.GetPrimary(ctx, projectID)
|
|
}
|