336 lines
9.2 KiB
Go
336 lines
9.2 KiB
Go
// Package infra provides infrastructure configuration and API clients
|
|
// for managing the Host UK production environment.
|
|
package infra
|
|
|
|
import (
|
|
core "dappco.re/go/core"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Config is the top-level infrastructure configuration parsed from infra.yaml.
|
|
// Usage: cfg := infra.Config{}
|
|
type Config struct {
|
|
Hosts map[string]*Host `yaml:"hosts"`
|
|
LoadBalancer LoadBalancer `yaml:"load_balancer"`
|
|
Network Network `yaml:"network"`
|
|
DNS DNS `yaml:"dns"`
|
|
SSL SSL `yaml:"ssl"`
|
|
Database Database `yaml:"database"`
|
|
Cache Cache `yaml:"cache"`
|
|
Containers map[string]*Container `yaml:"containers"`
|
|
S3 S3Config `yaml:"s3"`
|
|
CDN CDN `yaml:"cdn"`
|
|
CICD CICD `yaml:"cicd"`
|
|
Monitoring Monitoring `yaml:"monitoring"`
|
|
Backups Backups `yaml:"backups"`
|
|
}
|
|
|
|
// Host represents a server in the infrastructure.
|
|
// Usage: host := infra.Host{}
|
|
type Host struct {
|
|
FQDN string `yaml:"fqdn"`
|
|
IP string `yaml:"ip"`
|
|
PrivateIP string `yaml:"private_ip,omitempty"`
|
|
Type string `yaml:"type"` // hcloud, hrobot
|
|
Role string `yaml:"role"` // bastion, app, builder
|
|
SSH SSHConf `yaml:"ssh"`
|
|
Services []string `yaml:"services"`
|
|
}
|
|
|
|
// SSHConf holds SSH connection details for a host.
|
|
// Usage: ssh := infra.SSHConf{}
|
|
type SSHConf struct {
|
|
User string `yaml:"user"`
|
|
Key string `yaml:"key"`
|
|
Port int `yaml:"port"`
|
|
}
|
|
|
|
// LoadBalancer represents a Hetzner managed load balancer.
|
|
// Usage: lb := infra.LoadBalancer{}
|
|
type LoadBalancer struct {
|
|
Name string `yaml:"name"`
|
|
FQDN string `yaml:"fqdn"`
|
|
Provider string `yaml:"provider"`
|
|
Type string `yaml:"type"`
|
|
Location string `yaml:"location"`
|
|
Algorithm string `yaml:"algorithm"`
|
|
Backends []Backend `yaml:"backends"`
|
|
Health HealthCheck `yaml:"health_check"`
|
|
Listeners []Listener `yaml:"listeners"`
|
|
SSL LBCert `yaml:"ssl"`
|
|
}
|
|
|
|
// Backend is a load balancer backend target.
|
|
// Usage: backend := infra.Backend{}
|
|
type Backend struct {
|
|
Host string `yaml:"host"`
|
|
Port int `yaml:"port"`
|
|
}
|
|
|
|
// HealthCheck configures load balancer health checking.
|
|
// Usage: check := infra.HealthCheck{}
|
|
type HealthCheck struct {
|
|
Protocol string `yaml:"protocol"`
|
|
Path string `yaml:"path"`
|
|
Interval int `yaml:"interval"`
|
|
}
|
|
|
|
// Listener maps a frontend port to a backend port.
|
|
// Usage: listener := infra.Listener{}
|
|
type Listener struct {
|
|
Frontend int `yaml:"frontend"`
|
|
Backend int `yaml:"backend"`
|
|
Protocol string `yaml:"protocol"`
|
|
ProxyProtocol bool `yaml:"proxy_protocol"`
|
|
}
|
|
|
|
// LBCert holds the SSL certificate configuration for the load balancer.
|
|
// Usage: cert := infra.LBCert{}
|
|
type LBCert struct {
|
|
Certificate string `yaml:"certificate"`
|
|
SAN []string `yaml:"san"`
|
|
}
|
|
|
|
// Network describes the private network.
|
|
// Usage: network := infra.Network{}
|
|
type Network struct {
|
|
CIDR string `yaml:"cidr"`
|
|
Name string `yaml:"name"`
|
|
}
|
|
|
|
// DNS holds DNS provider configuration and zone records.
|
|
// Usage: dns := infra.DNS{}
|
|
type DNS struct {
|
|
Provider string `yaml:"provider"`
|
|
Nameservers []string `yaml:"nameservers"`
|
|
Zones map[string]*Zone `yaml:"zones"`
|
|
}
|
|
|
|
// Zone is a DNS zone with its records.
|
|
// Usage: zone := infra.Zone{}
|
|
type Zone struct {
|
|
Records []DNSRecord `yaml:"records"`
|
|
}
|
|
|
|
// DNSRecord is a single DNS record.
|
|
// Usage: record := infra.DNSRecord{}
|
|
type DNSRecord struct {
|
|
Name string `yaml:"name"`
|
|
Type string `yaml:"type"`
|
|
Value string `yaml:"value"`
|
|
TTL int `yaml:"ttl"`
|
|
}
|
|
|
|
// SSL holds SSL certificate configuration.
|
|
// Usage: ssl := infra.SSL{}
|
|
type SSL struct {
|
|
Wildcard WildcardCert `yaml:"wildcard"`
|
|
}
|
|
|
|
// WildcardCert describes a wildcard SSL certificate.
|
|
// Usage: cert := infra.WildcardCert{}
|
|
type WildcardCert struct {
|
|
Domains []string `yaml:"domains"`
|
|
Method string `yaml:"method"`
|
|
DNSProvider string `yaml:"dns_provider"`
|
|
Termination string `yaml:"termination"`
|
|
}
|
|
|
|
// Database describes the database cluster.
|
|
// Usage: db := infra.Database{}
|
|
type Database struct {
|
|
Engine string `yaml:"engine"`
|
|
Version string `yaml:"version"`
|
|
Cluster string `yaml:"cluster"`
|
|
Nodes []DBNode `yaml:"nodes"`
|
|
SSTMethod string `yaml:"sst_method"`
|
|
Backup BackupConfig `yaml:"backup"`
|
|
}
|
|
|
|
// DBNode is a database cluster node.
|
|
// Usage: node := infra.DBNode{}
|
|
type DBNode struct {
|
|
Host string `yaml:"host"`
|
|
Port int `yaml:"port"`
|
|
}
|
|
|
|
// BackupConfig describes automated backup settings.
|
|
// Usage: backup := infra.BackupConfig{}
|
|
type BackupConfig struct {
|
|
Schedule string `yaml:"schedule"`
|
|
Destination string `yaml:"destination"`
|
|
Bucket string `yaml:"bucket"`
|
|
Prefix string `yaml:"prefix"`
|
|
}
|
|
|
|
// Cache describes the cache/session cluster.
|
|
// Usage: cache := infra.Cache{}
|
|
type Cache struct {
|
|
Engine string `yaml:"engine"`
|
|
Version string `yaml:"version"`
|
|
Sentinel bool `yaml:"sentinel"`
|
|
Nodes []CacheNode `yaml:"nodes"`
|
|
}
|
|
|
|
// CacheNode is a cache cluster node.
|
|
// Usage: node := infra.CacheNode{}
|
|
type CacheNode struct {
|
|
Host string `yaml:"host"`
|
|
Port int `yaml:"port"`
|
|
}
|
|
|
|
// Container describes a container deployment.
|
|
// Usage: container := infra.Container{}
|
|
type Container struct {
|
|
Image string `yaml:"image"`
|
|
Port int `yaml:"port,omitempty"`
|
|
Runtime string `yaml:"runtime,omitempty"`
|
|
Command string `yaml:"command,omitempty"`
|
|
Replicas int `yaml:"replicas,omitempty"`
|
|
DependsOn []string `yaml:"depends_on,omitempty"`
|
|
}
|
|
|
|
// S3Config describes object storage.
|
|
// Usage: s3 := infra.S3Config{}
|
|
type S3Config struct {
|
|
Endpoint string `yaml:"endpoint"`
|
|
Buckets map[string]*S3Bucket `yaml:"buckets"`
|
|
}
|
|
|
|
// S3Bucket is an S3 bucket configuration.
|
|
// Usage: bucket := infra.S3Bucket{}
|
|
type S3Bucket struct {
|
|
Purpose string `yaml:"purpose"`
|
|
Paths []string `yaml:"paths"`
|
|
}
|
|
|
|
// CDN describes CDN configuration.
|
|
// Usage: cdn := infra.CDN{}
|
|
type CDN struct {
|
|
Provider string `yaml:"provider"`
|
|
Origin string `yaml:"origin"`
|
|
Zones []string `yaml:"zones"`
|
|
}
|
|
|
|
// CICD describes CI/CD configuration.
|
|
// Usage: cicd := infra.CICD{}
|
|
type CICD struct {
|
|
Provider string `yaml:"provider"`
|
|
URL string `yaml:"url"`
|
|
Runner string `yaml:"runner"`
|
|
Registry string `yaml:"registry"`
|
|
DeployHook string `yaml:"deploy_hook"`
|
|
}
|
|
|
|
// Monitoring describes monitoring configuration.
|
|
// Usage: monitoring := infra.Monitoring{}
|
|
type Monitoring struct {
|
|
HealthEndpoints []HealthEndpoint `yaml:"health_endpoints"`
|
|
Alerts map[string]int `yaml:"alerts"`
|
|
}
|
|
|
|
// HealthEndpoint is a URL to monitor.
|
|
// Usage: endpoint := infra.HealthEndpoint{}
|
|
type HealthEndpoint struct {
|
|
URL string `yaml:"url"`
|
|
Interval int `yaml:"interval"`
|
|
}
|
|
|
|
// Backups describes backup schedules.
|
|
// Usage: backups := infra.Backups{}
|
|
type Backups struct {
|
|
Daily []BackupJob `yaml:"daily"`
|
|
Weekly []BackupJob `yaml:"weekly"`
|
|
}
|
|
|
|
// BackupJob is a scheduled backup task.
|
|
// Usage: job := infra.BackupJob{}
|
|
type BackupJob struct {
|
|
Name string `yaml:"name"`
|
|
Type string `yaml:"type"`
|
|
Destination string `yaml:"destination,omitempty"`
|
|
Hosts []string `yaml:"hosts,omitempty"`
|
|
}
|
|
|
|
// Load reads and parses an infra.yaml file.
|
|
// Usage: cfg, err := infra.Load("/srv/project/infra.yaml")
|
|
func Load(path string) (*Config, error) {
|
|
read := localFS.Read(path)
|
|
if !read.OK {
|
|
return nil, core.E("infra.Load", "read infra config", coreResultErr(read, "infra.Load"))
|
|
}
|
|
|
|
var cfg Config
|
|
if err := yaml.Unmarshal([]byte(read.Value.(string)), &cfg); err != nil {
|
|
return nil, core.E("infra.Load", "parse infra config", err)
|
|
}
|
|
|
|
// Expand SSH key paths
|
|
for _, h := range cfg.Hosts {
|
|
if h.SSH.Key != "" {
|
|
h.SSH.Key = expandPath(h.SSH.Key)
|
|
}
|
|
if h.SSH.Port == 0 {
|
|
h.SSH.Port = 22
|
|
}
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
// Discover searches for infra.yaml in the given directory and parent directories.
|
|
// Usage: cfg, path, err := infra.Discover(".")
|
|
func Discover(startDir string) (*Config, string, error) {
|
|
dir := startDir
|
|
for {
|
|
path := core.JoinPath(dir, "infra.yaml")
|
|
if localFS.Exists(path) {
|
|
cfg, err := Load(path)
|
|
return cfg, path, err
|
|
}
|
|
|
|
parent := core.PathDir(dir)
|
|
if parent == dir {
|
|
break
|
|
}
|
|
dir = parent
|
|
}
|
|
return nil, "", core.E("infra.Discover", core.Concat("infra.yaml not found (searched from ", startDir, ")"), nil)
|
|
}
|
|
|
|
// HostsByRole returns all hosts matching the given role.
|
|
// Usage: apps := cfg.HostsByRole("app")
|
|
func (c *Config) HostsByRole(role string) map[string]*Host {
|
|
result := make(map[string]*Host)
|
|
for name, h := range c.Hosts {
|
|
if h.Role == role {
|
|
result[name] = h
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// AppServers returns hosts with role "app".
|
|
// Usage: apps := cfg.AppServers()
|
|
func (c *Config) AppServers() map[string]*Host {
|
|
return c.HostsByRole("app")
|
|
}
|
|
|
|
// expandPath expands ~ to home directory.
|
|
func expandPath(path string) string {
|
|
if core.HasPrefix(path, "~") {
|
|
home := core.Env("DIR_HOME")
|
|
if home == "" {
|
|
return path
|
|
}
|
|
suffix := core.TrimPrefix(path, "~")
|
|
if suffix == "" {
|
|
return home
|
|
}
|
|
if core.HasPrefix(suffix, "/") {
|
|
return core.Concat(home, suffix)
|
|
}
|
|
return core.JoinPath(home, suffix)
|
|
}
|
|
return path
|
|
}
|