carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package lease
2
3// CARLOS_S3_ENDPOINT support for the hibernation lease.
4//
5// release.NewS3Store has honoured CARLOS_S3_ENDPOINT since the first
6// S3-compatible deployment; this package did not, and every S3 client it
7// builds dialled real AWS regardless. That gap is not cosmetic: an activator
8// route is minted with Hibernate:true, the activator only arms when
9// CARLOS_HIBERNATE_BUCKET is set, and arming means acquiring the lease here.
10// On a non-AWS store (Hetzner Object Storage, R2, MinIO) the acquire could
11// never succeed, so the activator stayed nil and an exec-backed route with
12// nothing to spawn it 502'd forever — which took `carlos instances create`,
13// the whole console-driven provisioning path, down with it. The same clients
14// back the agent's status-publisher election (internal/host/agent.go), so the
15// override has to reach all three construction sites, not just the one.
16
17import (
18 "context"
19 "errors"
20 "fmt"
21 "os"
22
23 "github.com/aws/aws-sdk-go-v2/aws"
24 awsconfig "github.com/aws/aws-sdk-go-v2/config"
25 awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
26 lss3 "github.com/benbjohnson/litestream/s3"
27 "github.com/carlosframework/platform/internal/rootaws"
28
29 "github.com/carlosframework/platform/internal/s3compat"
30)
31
32// s3Endpoint reports the CARLOS_S3_ENDPOINT override, or "" for stock AWS.
33// Delegated to s3compat so there is exactly one reader of that variable.
34func s3Endpoint() string {
35 return s3compat.Endpoint()
36}
37
38// resolveRegion decides the region every client in this package signs for, in
39// strict order of authority:
40//
41// 1. the caller's explicit region (CARLOS_HIBERNATE_REGION), if any;
42// 2. whatever the AWS default chain yields — AWS_REGION, or a profile. This
43// step is why the resolution cannot be a pure function of the argument: a
44// compatible-store deployment sets AWS_REGION to its own location (the
45// private CARLOS uses nbg1), and that is the region release.NewS3Store
46// already signs for against the same bucket. Substituting a constant here
47// would make the lease sign differently from every other S3 client in the
48// process, against a store that may well check;
49// 3. only if BOTH are empty and an endpoint override is in play, litestream's
50// non-AWS default — better a working guess than a client signing for no
51// region at all.
52//
53// Step 3 is the one an override adds. Without it the AWS chain is authoritative
54// and an empty result is fine: stock AWS resolves the region per-request from
55// the bucket's own endpoint, which a compatible store cannot do.
56//
57// The resolved value is returned, not just applied, because it has to travel:
58// litestream's replica client does its OWN region defaulting (Init substitutes
59// the same non-AWS constant for an empty Region), so handing it the resolved
60// region is what stops the two halves of one lease — lock.json and the LTX
61// files beside it under the same prefix — from signing differently.
62func resolveRegion(ctx context.Context, region string) (string, error) {
63 if region != "" {
64 return region, nil // no config load needed: step 1 already decided
65 }
66 cfg, err := rootaws.LoadDefaultConfig(ctx)
67 if err != nil {
68 return "", fmt.Errorf("load aws config: %w", err)
69 }
70 return regionFor(region, cfg.Region), nil
71}
72
73// regionFor is resolveRegion's decision with the chain lookup already done —
74// the shared rule, so the leaser's path (which loads a config anyway, and must
75// not load a second one just to re-ask the same question) and Restore's cannot
76// drift apart.
77func regionFor(explicit, chainResolved string) string {
78 if explicit != "" {
79 return explicit
80 }
81 if chainResolved == "" && s3Endpoint() != "" {
82 return lss3.DefaultRegion
83 }
84 return chainResolved
85}
86
87// newLeaserClient builds the S3 client the litestream leaser writes lock.json
88// with. Unlike the replica client, which builds its own from its Endpoint
89// field, the leaser takes a ready-made client (lss3.Leaser.SetClient) — so the
90// override has to be applied right here or the lock lands on AWS while the
91// data it guards lands on the compatible store.
92//
93// The override, path-style addressing, when-required checksums and every
94// per-provider quirk come from internal/s3compat, which is a no-op on stock
95// AWS — so the flagship's path stays byte-identical while this client picks up
96// the unquoted-If-Match fix that litestream's leaser needs to renew a lease on
97// Ceph RGW. Passing that client to lss3.Leaser.SetClient is the ONLY way the
98// fix reaches litestream, which has no provider handling in its leaser at all.
99//
100// It returns the region it resolved alongside the client, so AcquireTTL can
101// carry that exact value onto the Held for Replicate's replica client rather
102// than letting litestream substitute a different default of its own.
103func newLeaserClient(ctx context.Context, region string) (*awss3.Client, string, error) {
104 var opts []func(*awsconfig.LoadOptions) error
105 if region != "" {
106 opts = append(opts, awsconfig.WithRegion(region))
107 }
108 cfg, err := rootaws.LoadDefaultConfig(ctx, opts...)
109 if err != nil {
110 return nil, "", fmt.Errorf("load aws config: %w", err)
111 }
112 cfg.Region = regionFor(region, cfg.Region)
113 return awss3.NewFromConfig(cfg, s3compat.Options()), cfg.Region, nil
114}
115
116// newReplicaClient builds litestream's replica client for the LTX files at
117// s3://bucket/prefix. litestream reads its OWN env var (LITESTREAM_S3_ENDPOINT)
118// only on the URL-parsing path, which this package never takes — it sets the
119// fields directly — so the override is applied here too. Endpoint/ForcePathStyle
120// is all it takes: litestream's Init derives the rest (the checksum knobs and
121// the non-AWS default region) from a non-empty Endpoint on its own, and the
122// signed-payload and content-MD5 defaults NewReplicaClient already ships are
123// the ones every compatible in litestream's provider table wants.
124func newReplicaClient(bucket, prefix, region string) (*lss3.ReplicaClient, error) {
125 return newReplicaClientForUID(bucket, prefix, region, os.Geteuid(), rootaws.OffcloudCredentials)
126}
127
128var errRootReplicaSessionToken = errors.New("root Litestream cannot use a session token from /etc/carlos/credentials. set the keys in the environment instead, or use an EC2 instance profile")
129
130func newReplicaClientForUID(bucket, prefix, region string, euid int, offcloud func() (aws.Credentials, error)) (*lss3.ReplicaClient, error) {
131 c := lss3.NewReplicaClient()
132 if euid == 0 {
133 creds, err := offcloud()
134 if err != nil {
135 return nil, err
136 }
137 if creds.SessionToken != "" {
138 return nil, errRootReplicaSessionToken
139 }
140 c.AccessKeyID = creds.AccessKeyID
141 c.SecretAccessKey = creds.SecretAccessKey
142 }
143 c.Bucket = bucket
144 c.Path = prefix
145 c.Region = region
146 if ep := s3Endpoint(); ep != "" {
147 c.Endpoint = ep
148 c.ForcePathStyle = true
149 }
150 return c, nil
151}
152
153// ReadReplicaClient is newReplicaClient for READERS outside this package —
154// the console's db door (spec 2026-09-02 §6). Identical construction on
155// purpose, region resolution included (Restore's resolveRegion): a second
156// hand-rolled client is how the endpoint override, the path-style flag and
157// the signing region drift apart between the writer and the reader of the
158// same bucket.
159//
160// It returns the same type the writer uses, so nothing here can enforce
161// read-only by construction; that guarantee lives in internal/dbdoor's
162// client, which has no write methods at all, and in the door only ever
163// calling LTXFiles and OpenLTXFile on what this returns.
164func ReadReplicaClient(ctx context.Context, bucket, prefix, region string) (*lss3.ReplicaClient, error) {
165 region, err := resolveRegion(ctx, region)
166 if err != nil {
167 return nil, err
168 }
169 return newReplicaClient(bucket, prefix, region)
170}
171