| index 49d4d016d..fa7da235a 100644 |
| --- a/internal/host/agent.go |
| +++ b/internal/host/agent.go |
| @@ -84,6 +84,7 @@ import ( |
| "github.com/carlosframework/platform/internal/domains" |
| "github.com/carlosframework/platform/internal/edge" |
| "github.com/carlosframework/platform/internal/lease" |
| + "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/status" |
| @@ -147,6 +148,7 @@ func RunAgent(args []string) { |
| go canaryAdoptLoop(reg) |
| go statusLoop(reg) |
| go domainsLoop(reg, peekAcmeCache(args)) |
| + go instancesLoop(reg) |
| go metricsLoop() |
| go configSyncLoop(reg, svc, dd) |
| go socketAdvisoryLoop(reg, svc) |
| @@ -422,6 +424,35 @@ func domainsTickConfig(getenv func(string) string, publicIP string) domains.Conf |
| return cfg |
| } |
| |
| +// provisionTickConfig builds one instances reconcile pass's Config from the |
| +// environment — pure (getenv-injected), mirroring domainsTickConfig's shape. |
| +// Unlike domainsTickConfig, there is no inert zero-Config case: an unset |
| +// CARLOS_REGION/CARLOS_POOLS is a LEGAL state, not a "feature not |
| +// configured" one — see provision.Reconcile's doc comment — a box that |
| +// hasn't been told its region/pools still serves only ""-scoped records, |
| +// never someone else's residency promise. So this always returns a |
| +// populated Config, and CARLOS_DATA_DIR is honored (default "/data") |
| +// regardless of whether region/pools are set. |
| +func provisionTickConfig(getenv func(string) string) provision.ReconcileConfig { |
| + region := strings.TrimSpace(getenv("CARLOS_REGION")) |
| + var pools []string |
| + for _, p := range strings.Split(getenv("CARLOS_POOLS"), ",") { |
| + if p = strings.TrimSpace(p); p != "" { |
| + pools = append(pools, p) |
| + } |
| + } |
| + dd := getenv("CARLOS_DATA_DIR") |
| + if dd == "" { |
| + dd = "/data" |
| + } |
| + return provision.ReconcileConfig{ |
| + Region: region, |
| + Pools: pools, |
| + DataDir: dd, |
| + Now: time.Now, |
| + } |
| +} |
| + |
| // resolveHost is the production DNS seam: the default resolver with a hard |
| // 3s timeout, so one dead nameserver can't stall a whole reconcile pass. |
| func resolveHost(host string) ([]string, error) { |
| @@ -516,6 +547,37 @@ func domainsLoop(reg *registry.Registry, acmeCacheFlag string) { |
| } |
| } |
| |
| +// instancesLoop runs the instance-provisioning reconcile pass forever on the |
| +// same cadence as domainsLoop (5min + jitter, first tick after 1 minute) — |
| +// turning bucket instance records into registry routes for THIS agent's |
| +// region/pool scope (provision.Reconcile). Unlike domainsLoop there is NO |
| +// apps-domain-style inert gate: an unset CARLOS_REGION/CARLOS_POOLS is a |
| +// legal, still-armed state (see provisionTickConfig and |
| +// provision.Reconcile's doc comment — a box with no region deliberately |
| +// still serves ""-scoped records), so this loop arms whenever the deploy |
| +// store opens, full stop. Missing deployment config disables the loop with |
| +// one log line, the same "inert until configured" shape adoptLoop and |
| +// domainsLoop use for their own missing config. Per-pass errors are logged; |
| +// the loop lives on. |
| +func instancesLoop(reg *registry.Registry) { |
| + ctx := context.Background() |
| + store, err := openDeployStore(ctx) |
| + if err != nil { |
| + log.Printf("agent: instances loop disabled: %v", err) |
| + return |
| + } |
| + cfg := provisionTickConfig(os.Getenv) |
| + log.Printf("agent: instances loop armed (5min + jitter), region %q pools %v", cfg.Region, cfg.Pools) |
| + delay := time.Minute |
| + for { |
| + time.Sleep(delay) |
| + delay = 5*time.Minute + rand.N(time.Minute) |
| + if err := provision.Reconcile(ctx, store, reg, cfg); err != nil { |
| + log.Printf("agent: instances pass: %v", err) |
| + } |
| + } |
| +} |
| + |
| // resolveStatusBucket applies the status/deployment bucket defaulting |
| // documented in this file's package comment: an explicit CARLOS_STATUS_BUCKET |
| // always wins, and when it's unset the status loop falls back to whatever |