Documentation
¶
Index ¶
- Constants
- Variables
- func ExtractCaddyBinary(p *paths.Paths) (string, error)
- func GetCaddyBinaryPath(p *paths.Paths) (string, error)
- func HasTLSRules(ingresses []Ingress) bool
- func ParseCaddyError(caddyError string) error
- func SupportedDNSProviders() string
- type ACMEConfig
- type APIIngressConfig
- type CaddyConfigGenerator
- type CaddyDaemon
- func (d *CaddyDaemon) AdminPort() int
- func (d *CaddyDaemon) AdminURL() string
- func (d *CaddyDaemon) DiscoverRunning() (int, bool)
- func (d *CaddyDaemon) GetPID() int
- func (d *CaddyDaemon) IsRunning() bool
- func (d *CaddyDaemon) ReloadConfig(config []byte) error
- func (d *CaddyDaemon) Start(ctx context.Context) (int, error)
- func (d *CaddyDaemon) Stop(ctx context.Context) error
- func (d *CaddyDaemon) StopOnShutdown() bool
- type CaddyLogForwarder
- type Config
- type CreateIngressRequest
- type DNSProvider
- type HostnamePattern
- type Ingress
- type IngressMatch
- type IngressRule
- type IngressTarget
- type InstanceResolver
- type Manager
- type ValidationError
Constants ¶
const CaddyVersion = "v2.10.2"
CaddyVersion is the version of Caddy to use.
const DefaultDNSPort = dns.DefaultPort
DefaultDNSPort is the default port for the internal DNS server.
Variables ¶
var ( // ErrNotFound is returned when an ingress is not found. ErrNotFound = errors.New("ingress not found") // ErrAlreadyExists is returned when trying to create an ingress that already exists. ErrAlreadyExists = errors.New("ingress already exists") // ErrInvalidRequest is returned when the request is invalid. ErrInvalidRequest = errors.New("invalid request") // ErrInstanceNotFound is returned when the target instance is not found. ErrInstanceNotFound = errors.New("target instance not found") // ErrInstanceNoNetwork is returned when the target instance has no network. ErrInstanceNoNetwork = errors.New("target instance has no network configured") // ErrHostnameInUse is returned when a hostname is already in use by another ingress. ErrHostnameInUse = errors.New("hostname already in use by another ingress") // ErrConfigValidationFailed is returned when Caddy config validation fails. // This indicates the config was rejected by Caddy's admin API. ErrConfigValidationFailed = errors.New("config validation failed") // ErrPortInUse is returned when the requested port is already in use by another process. ErrPortInUse = errors.New("port already in use") // ErrDomainNotAllowed is returned when a TLS ingress is requested for a domain not in the allowed list. ErrDomainNotAllowed = errors.New("domain not allowed for TLS") // ErrAmbiguousName is returned when a lookup matches multiple ingresses. ErrAmbiguousName = errors.New("ambiguous ingress identifier matches multiple ingresses") )
Common errors returned by the ingress package.
var ErrCaddyNotEmbedded = fmt.Errorf("caddy binary is not embedded on macOS; install via: brew install caddy")
ErrCaddyNotEmbedded indicates Caddy is not embedded on macOS. Users should install Caddy via Homebrew or download from caddyserver.com.
Functions ¶
func ExtractCaddyBinary ¶
ExtractCaddyBinary on macOS attempts to find Caddy in PATH. Unlike Linux, we don't embed the binary on macOS.
func GetCaddyBinaryPath ¶
GetCaddyBinaryPath returns path to Caddy, looking in PATH on macOS.
func HasTLSRules ¶
HasTLSRules checks if any ingress has TLS enabled.
func ParseCaddyError ¶
ParseCaddyError parses a Caddy error response and returns a more specific error if possible.
func SupportedDNSProviders ¶
func SupportedDNSProviders() string
SupportedDNSProviders returns a comma-separated list of supported DNS provider names. Used in error messages to keep them in sync as new providers are added.
Types ¶
type ACMEConfig ¶
type ACMEConfig struct {
// Email is the ACME account email (required for TLS).
Email string
// DNSProvider is the DNS provider for ACME challenges.
DNSProvider DNSProvider
// CA is the ACME CA URL. Empty means Let's Encrypt production.
CA string
// DNS propagation settings (applies to all providers)
DNSPropagationTimeout string // Max time to wait for DNS propagation (e.g., "2m")
DNSResolvers string // Comma-separated DNS resolvers to use for checking propagation
// AllowedDomains is a comma-separated list of domain patterns allowed for TLS ingresses.
// Supports wildcards like "*.example.com" and exact matches like "api.example.com".
// If empty, no TLS domains are allowed.
AllowedDomains string
// Cloudflare API token (if DNSProvider=cloudflare).
CloudflareAPIToken string
}
ACMEConfig holds ACME/TLS configuration for Caddy.
func (*ACMEConfig) IsDomainAllowed ¶
func (c *ACMEConfig) IsDomainAllowed(hostname string) bool
IsDomainAllowed checks if a hostname is allowed for TLS based on the AllowedDomains config. Returns true if the hostname matches any of the allowed patterns.
Supported pattern types:
- Exact match: "api.example.com" matches only "api.example.com"
- Global wildcard: "*" matches any hostname (use with caution)
- Subdomain wildcard: "*.example.com" matches single-level subdomains only
Wildcard behavior for "*.example.com":
- Matches: "foo.example.com", "bar.example.com"
- Does NOT match: "example.com" (apex domain)
- Does NOT match: "foo.bar.example.com" (multi-level subdomain)
func (*ACMEConfig) IsTLSConfigured ¶
func (c *ACMEConfig) IsTLSConfigured() bool
IsTLSConfigured returns true if ACME/TLS is properly configured.
type APIIngressConfig ¶ added in v0.0.6
type APIIngressConfig struct {
// Hostname is the hostname for API access (e.g., "hypeman.hostname.kernel.sh").
// Empty means API ingress is disabled.
Hostname string
// Port is the local port where the Hypeman API is running.
Port int
// TLS enables TLS for the API hostname.
TLS bool
// RedirectHTTP enables HTTP to HTTPS redirect for the API hostname.
RedirectHTTP bool
}
APIIngressConfig holds configuration for exposing the Hypeman API via Caddy.
func (*APIIngressConfig) IsEnabled ¶ added in v0.0.6
func (c *APIIngressConfig) IsEnabled() bool
IsEnabled returns true if API ingress is configured.
type CaddyConfigGenerator ¶
type CaddyConfigGenerator struct {
// contains filtered or unexported fields
}
CaddyConfigGenerator generates Caddy configuration from ingress resources.
func NewCaddyConfigGenerator ¶
func NewCaddyConfigGenerator(p *paths.Paths, listenAddress string, adminAddress string, adminPort int, acme ACMEConfig, apiIngress APIIngressConfig, dnsResolverPort int) *CaddyConfigGenerator
NewCaddyConfigGenerator creates a new Caddy config generator.
func (*CaddyConfigGenerator) GenerateConfig ¶
func (g *CaddyConfigGenerator) GenerateConfig(ctx context.Context, ingresses []Ingress) ([]byte, error)
GenerateConfig generates the Caddy JSON configuration.
func (*CaddyConfigGenerator) WriteConfig ¶
func (g *CaddyConfigGenerator) WriteConfig(ctx context.Context, ingresses []Ingress) error
WriteConfig writes the Caddy configuration to disk.
type CaddyDaemon ¶
type CaddyDaemon struct {
// contains filtered or unexported fields
}
CaddyDaemon manages the Caddy proxy daemon lifecycle. Caddy uses its admin API for configuration updates - no restart needed.
func NewCaddyDaemon ¶
func NewCaddyDaemon(p *paths.Paths, adminAddress string, adminPort int, stopOnShutdown bool) *CaddyDaemon
NewCaddyDaemon creates a new CaddyDaemon manager. If adminPort is 0, it will be resolved later from existing config or picked fresh.
func (*CaddyDaemon) AdminPort ¶
func (d *CaddyDaemon) AdminPort() int
AdminPort returns the admin port. If it was configured as 0 (random), this returns the actual port after it's been resolved.
func (*CaddyDaemon) AdminURL ¶
func (d *CaddyDaemon) AdminURL() string
AdminURL returns the admin API URL.
func (*CaddyDaemon) DiscoverRunning ¶
func (d *CaddyDaemon) DiscoverRunning() (int, bool)
DiscoverRunning checks if Caddy is already running and returns its PID.
func (*CaddyDaemon) GetPID ¶
func (d *CaddyDaemon) GetPID() int
GetPID returns the PID of the running Caddy process, or 0 if not running.
func (*CaddyDaemon) IsRunning ¶
func (d *CaddyDaemon) IsRunning() bool
IsRunning returns true if Caddy is currently running.
func (*CaddyDaemon) ReloadConfig ¶
func (d *CaddyDaemon) ReloadConfig(config []byte) error
ReloadConfig reloads Caddy configuration by posting to the admin API.
func (*CaddyDaemon) Start ¶
func (d *CaddyDaemon) Start(ctx context.Context) (int, error)
Start starts the Caddy daemon. If Caddy is already running (discovered via PID file or admin API), this is a no-op and returns the existing PID. Note: adminPort must be resolved (non-zero) before calling Start.
func (*CaddyDaemon) Stop ¶
func (d *CaddyDaemon) Stop(ctx context.Context) error
Stop gracefully stops the Caddy daemon.
func (*CaddyDaemon) StopOnShutdown ¶
func (d *CaddyDaemon) StopOnShutdown() bool
StopOnShutdown returns whether Caddy should be stopped when hypeman shuts down.
type CaddyLogForwarder ¶
type CaddyLogForwarder struct {
// contains filtered or unexported fields
}
CaddyLogForwarder tails Caddy's system log and forwards to OTEL.
func NewCaddyLogForwarder ¶
func NewCaddyLogForwarder(p *paths.Paths, logger *slog.Logger) *CaddyLogForwarder
NewCaddyLogForwarder creates a new log forwarder.
type Config ¶
type Config struct {
// ListenAddress is the address Caddy should listen on (default: 0.0.0.0).
ListenAddress string
// AdminAddress is the address for Caddy admin API (default: 127.0.0.1).
AdminAddress string
// AdminPort is the port for Caddy admin API (default: 2019).
AdminPort int
// DNSPort is the port for the internal DNS server used for dynamic upstream resolution.
// Default: 5353. Set to 0 to use a random available port.
DNSPort int
// StopOnShutdown determines whether to stop Caddy when hypeman shuts down (default: false).
// When false, Caddy continues running independently.
StopOnShutdown bool
// ACME configuration for TLS certificates
ACME ACMEConfig
// APIIngress configuration for exposing Hypeman API via Caddy
APIIngress APIIngressConfig
}
Config holds configuration for the ingress manager.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns the default ingress configuration.
type CreateIngressRequest ¶
type CreateIngressRequest struct {
// Name is a human-readable name for the ingress.
Name string `json:"name"`
// Tags are optional user-defined key-value tags.
Tags tags.Tags `json:"tags,omitempty"`
// Rules define the routing rules for this ingress.
Rules []IngressRule `json:"rules"`
}
CreateIngressRequest is the request body for creating a new ingress.
func (*CreateIngressRequest) Validate ¶
func (r *CreateIngressRequest) Validate() error
Validate validates the CreateIngressRequest.
type DNSProvider ¶
type DNSProvider string
DNSProvider represents supported DNS providers for ACME challenges.
const ( // DNSProviderNone indicates no DNS provider is configured. DNSProviderNone DNSProvider = "" // DNSProviderCloudflare uses Cloudflare for DNS challenges. DNSProviderCloudflare DNSProvider = "cloudflare" )
func ParseDNSProvider ¶
func ParseDNSProvider(s string) (DNSProvider, error)
ParseDNSProvider parses a string into a DNSProvider, returning an error for unknown values.
type HostnamePattern ¶
type HostnamePattern struct {
// Original is the original pattern string (e.g., "{instance}.example.com")
Original string
// Wildcard is the Caddy wildcard pattern (e.g., "*.example.com")
Wildcard string
// Captures is the list of capture names in order (e.g., ["instance"])
Captures []string
// CaddyLabels maps capture names to Caddy placeholder expressions
// e.g., {"instance": "{http.request.host.labels.2}"}
CaddyLabels map[string]string
}
HostnamePattern represents a parsed hostname pattern with captures.
func (*HostnamePattern) ResolveInstance ¶
func (p *HostnamePattern) ResolveInstance(targetInstance string) string
ResolveInstance resolves the target instance expression using the pattern's captures. For a target like "{instance}" and captures {"instance": "{http.request.host.labels.2}"}, returns "{http.request.host.labels.2}". For a literal target like "my-api", returns "my-api".
type Ingress ¶
type Ingress struct {
// ID is the unique identifier for this ingress (auto-generated).
ID string `json:"id"`
// Name is a human-readable name for the ingress.
Name string `json:"name"`
// Tags are optional user-defined key-value tags.
Tags tags.Tags `json:"tags,omitempty"`
// Rules define the routing rules for this ingress.
Rules []IngressRule `json:"rules"`
// CreatedAt is the timestamp when this ingress was created.
CreatedAt time.Time `json:"created_at"`
}
Ingress represents an ingress resource that defines how external traffic should be routed to VM instances.
type IngressMatch ¶
type IngressMatch struct {
// Hostname is the hostname to match. Can be:
// - Literal: "api.example.com" (exact match on Host header)
// - Pattern: "{instance}.example.com" (dynamic, extracts subdomain as instance name)
// This is required.
Hostname string `json:"hostname"`
// Port is the host port to listen on for this rule.
// If not specified, defaults to 80.
Port int `json:"port,omitempty"`
}
IngressMatch specifies the conditions for matching incoming requests.
func (*IngressMatch) GetPort ¶
func (m *IngressMatch) GetPort() int
GetPort returns the port for this match, defaulting to 80 if not specified.
func (*IngressMatch) IsPattern ¶
func (m *IngressMatch) IsPattern() bool
IsPattern returns true if the hostname contains {name} captures.
func (*IngressMatch) ParsePattern ¶
func (m *IngressMatch) ParsePattern() (*HostnamePattern, error)
ParsePattern parses the hostname pattern and returns a HostnamePattern. For "{instance}.example.com":
- Wildcard: "*.example.com"
- Captures: ["instance"]
- CaddyLabels: {"instance": "{http.request.host.labels.2}"}
Caddy labels are indexed from the right (TLD first): - foo.bar.example.com → labels.0=com, labels.1=example, labels.2=bar, labels.3=foo
type IngressRule ¶
type IngressRule struct {
// Match specifies the conditions for matching incoming requests.
Match IngressMatch `json:"match"`
// Target specifies where matching requests should be routed.
Target IngressTarget `json:"target"`
// TLS enables TLS termination for this rule.
// When enabled, a certificate will be automatically issued via ACME.
TLS bool `json:"tls,omitempty"`
// RedirectHTTP creates an automatic HTTP to HTTPS redirect for this hostname.
// Only applies when TLS is enabled.
RedirectHTTP bool `json:"redirect_http,omitempty"`
}
IngressRule defines a single routing rule within an ingress.
type IngressTarget ¶
type IngressTarget struct {
// Instance is the name or ID of the target instance.
Instance string `json:"instance"`
// Port is the port on the target instance.
Port int `json:"port"`
}
IngressTarget specifies the target for routing matched requests.
type InstanceResolver ¶
type InstanceResolver interface {
// ResolveInstanceIP resolves an instance name or ID to its IP address.
// Returns the IP address and nil error if found, or an error if the instance
// doesn't exist, isn't running, or has no network.
ResolveInstanceIP(ctx context.Context, nameOrID string) (string, error)
// InstanceExists checks if an instance with the given name or ID exists.
InstanceExists(ctx context.Context, nameOrID string) (bool, error)
// ResolveInstance resolves an instance name, ID, or ID prefix to its canonical name and ID.
// Returns (name, id, nil) if found, or an error if the instance doesn't exist.
ResolveInstance(ctx context.Context, nameOrID string) (name string, id string, err error)
}
InstanceResolver provides instance resolution capabilities. This interface is implemented by the instance manager.
type Manager ¶
type Manager interface {
// Initialize starts the ingress subsystem.
// This should be called during server startup.
Initialize(ctx context.Context) error
// Create creates a new ingress resource.
Create(ctx context.Context, req CreateIngressRequest) (*Ingress, error)
// Get retrieves an ingress by ID, name, or ID prefix.
// Lookup order: exact ID match -> exact name match -> ID prefix match.
// Returns ErrAmbiguousName if prefix matches multiple ingresses.
Get(ctx context.Context, idOrName string) (*Ingress, error)
// List returns all ingress resources.
List(ctx context.Context) ([]Ingress, error)
// Delete removes an ingress resource by ID, name, or ID prefix.
// Lookup order: exact ID match -> exact name match -> ID prefix match.
// Returns ErrAmbiguousName if prefix matches multiple ingresses.
Delete(ctx context.Context, idOrName string) error
// Shutdown gracefully stops the ingress subsystem.
Shutdown(ctx context.Context) error
// AdminURL returns the Caddy admin API URL.
// Only valid after Initialize() has been called.
AdminURL() string
}
Manager is the interface for managing ingress resources.
func NewManager ¶
func NewManager(p *paths.Paths, config Config, instanceResolver InstanceResolver, otelLogger *slog.Logger) Manager
NewManager creates a new ingress manager. If otelLogger is non-nil, Caddy system logs will be forwarded to OTEL.
type ValidationError ¶
ValidationError represents a validation error.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string