Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Adds complete Slate documentation infrastructure to generated projects:
- docs/ directory with Gemfile, config.rb, and source templates
- Dockerfile for building docs site
- Dockerfile.nginx for serving static docs
- generate-docs.sh script for CI integration
- Claude command for AI-assisted docs generation
- OpenAPI → Slate markdown conversion via widdershins
Also includes:
- --export-openapi flag for service binaries
- DNS provisioning for docs.{domain} subdomain
- Updated project_infra for docs DNS records
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
210 lines
6.1 KiB
Go
210 lines
6.1 KiB
Go
// Package service provides business logic services.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/port"
|
|
)
|
|
|
|
// ValidateProjectName validates that a project name is safe for use as
|
|
// a DNS subdomain, K8s resource name, and git repository name.
|
|
// Delegates to domain.ValidateProjectName for centralized validation.
|
|
func ValidateProjectName(name string) error {
|
|
return domain.ValidateProjectName(name)
|
|
}
|
|
|
|
// ProjectInfraService orchestrates project infrastructure operations.
|
|
// It coordinates git repo creation, DNS, CI activation, template seeding, deployment,
|
|
// and database/cache provisioning.
|
|
type ProjectInfraService struct {
|
|
db *sql.DB
|
|
gitRepo port.GitRepository
|
|
dns port.DNSProvider
|
|
deployer port.Deployer
|
|
ciProvider port.CIProvider
|
|
templateProvider port.TemplateProvider
|
|
domainRepo port.ProjectDomainRepository
|
|
slugGenerator port.SlugGenerator
|
|
credentialStore port.CredentialStore
|
|
dbProvisioner port.DatabaseProvisioner
|
|
cacheProvisioner port.CacheProvisioner
|
|
|
|
// Config
|
|
defaultGitOwner string
|
|
defaultDomain string
|
|
clusterIP string
|
|
registryURL string // e.g., "registry.threesix.ai"
|
|
}
|
|
|
|
// ProjectInfraConfig configures the project infrastructure service.
|
|
type ProjectInfraConfig struct {
|
|
DefaultGitOwner string // e.g., "threesix"
|
|
DefaultDomain string // e.g., "threesix.ai"
|
|
ClusterIP string // e.g., "208.122.204.172"
|
|
RegistryURL string // e.g., "registry.threesix.ai"
|
|
}
|
|
|
|
// NewProjectInfraService creates a new project infrastructure service.
|
|
func NewProjectInfraService(
|
|
db *sql.DB,
|
|
gitRepo port.GitRepository,
|
|
dns port.DNSProvider,
|
|
deployer port.Deployer,
|
|
ciProvider port.CIProvider,
|
|
templateProvider port.TemplateProvider,
|
|
domainRepo port.ProjectDomainRepository,
|
|
slugGenerator port.SlugGenerator,
|
|
cfg ProjectInfraConfig,
|
|
) *ProjectInfraService {
|
|
registryURL := cfg.RegistryURL
|
|
if registryURL == "" {
|
|
registryURL = "registry.threesix.ai" // Default for backward compatibility
|
|
}
|
|
return &ProjectInfraService{
|
|
db: db,
|
|
gitRepo: gitRepo,
|
|
dns: dns,
|
|
deployer: deployer,
|
|
ciProvider: ciProvider,
|
|
templateProvider: templateProvider,
|
|
domainRepo: domainRepo,
|
|
slugGenerator: slugGenerator,
|
|
defaultGitOwner: cfg.DefaultGitOwner,
|
|
defaultDomain: cfg.DefaultDomain,
|
|
clusterIP: cfg.ClusterIP,
|
|
registryURL: registryURL,
|
|
}
|
|
}
|
|
|
|
// WithCredentialStore sets the credential store for storing provisioned credentials.
|
|
func (s *ProjectInfraService) WithCredentialStore(cs port.CredentialStore) *ProjectInfraService {
|
|
s.credentialStore = cs
|
|
return s
|
|
}
|
|
|
|
// WithDatabaseProvisioner sets the database provisioner for project databases.
|
|
func (s *ProjectInfraService) WithDatabaseProvisioner(dp port.DatabaseProvisioner) *ProjectInfraService {
|
|
s.dbProvisioner = dp
|
|
return s
|
|
}
|
|
|
|
// WithCacheProvisioner sets the cache provisioner for project cache access.
|
|
func (s *ProjectInfraService) WithCacheProvisioner(cp port.CacheProvisioner) *ProjectInfraService {
|
|
s.cacheProvisioner = cp
|
|
return s
|
|
}
|
|
|
|
// CreateProjectRequest contains parameters for creating a new project.
|
|
type CreateProjectRequest struct {
|
|
Name string
|
|
Description string
|
|
Private bool
|
|
Template string // Template to seed the repo with (default: "skeleton" for composable monorepos)
|
|
CustomSubdomain string // Optional: custom subdomain (e.g., "my-app" for my-app.threesix.ai)
|
|
}
|
|
|
|
// CreateProjectResult contains the result of project creation.
|
|
type CreateProjectResult struct {
|
|
ProjectID string
|
|
Name string
|
|
Description string
|
|
Slug string // Auto-generated unique identifier
|
|
|
|
// Git info
|
|
GitRepoOwner string
|
|
GitRepoName string
|
|
CloneSSH string
|
|
CloneHTTP string
|
|
HTMLURL string
|
|
|
|
// Domain info (primary domain for backward compatibility)
|
|
Domain string
|
|
URL string
|
|
|
|
// API documentation URL (docs.{slug}.{domain})
|
|
DocsURL string
|
|
|
|
// All domains associated with the project
|
|
Domains []*domain.ProjectDomain
|
|
|
|
// Next steps
|
|
NextSteps []string
|
|
}
|
|
|
|
// ProjectStatus represents the current status of a project.
|
|
type ProjectStatus struct {
|
|
ProjectID string
|
|
Name string
|
|
Description string
|
|
Slug string
|
|
|
|
// Git
|
|
GitRepoOwner string
|
|
GitRepoName string
|
|
CloneSSH string
|
|
CloneHTTP string
|
|
HTMLURL string
|
|
|
|
// Domain (primary for backward compatibility)
|
|
Domain string
|
|
CustomDomain string
|
|
URL string
|
|
|
|
// All domains associated with the project
|
|
Domains []*domain.ProjectDomain
|
|
|
|
// Deployment
|
|
DeploymentImage string
|
|
DeploymentStatus string
|
|
DeploymentReplicas int
|
|
ReadyReplicas int
|
|
|
|
// Site health
|
|
SiteLive bool // True if the site responds with HTTP 200
|
|
SiteError string // Error message if site check failed
|
|
}
|
|
|
|
// AddDomainRequest contains parameters for adding a domain to a project.
|
|
type AddDomainRequest struct {
|
|
ProjectID string
|
|
Domain string // Full domain (e.g., "my-app.threesix.ai" or "custom.example.com")
|
|
Type domain.DomainType // DomainTypePrimaryCustom or DomainTypeAlias
|
|
RecordType string // "A" or "CNAME" (default: "A")
|
|
Proxied bool // Cloudflare proxy enabled
|
|
}
|
|
|
|
// checkSiteHealth performs an HTTP GET request to check if a site is live.
|
|
// Returns (true, "") if the site returns HTTP 200, otherwise (false, errorMessage).
|
|
func checkSiteHealth(ctx context.Context, url string) (bool, string) {
|
|
client := &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return false, fmt.Sprintf("failed to create request: %v", err)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return false, fmt.Sprintf("request failed: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Accept 2xx and 3xx status codes as "live"
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
|
return true, ""
|
|
}
|
|
|
|
return false, fmt.Sprintf("HTTP %d", resp.StatusCode)
|
|
}
|