carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "sort"
10 "strings"
11
12 "github.com/aws/aws-sdk-go-v2/aws"
13 "github.com/aws/aws-sdk-go-v2/credentials"
14 awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
15
16 "github.com/carlosframework/platform/internal/lease"
17 "github.com/carlosframework/platform/internal/moves"
18 "github.com/carlosframework/platform/internal/objstore"
19)
20
21// The storage probes of the target's report: T4's probe of every store
22// bound in the composed environment, and T10's return lane in the source's
23// bucket. armPhase1 wires them as moveCfg.ProbeStore and LaneProbe.
24
25// probeStores is T4 in production (§7.3): for each store bound in the
26// composed environment (objstore.EnvNames), with that store's own
27// credentials, list one key, then put, get and delete
28// .carlos-move-probe/<move id>. It returns the first failing binding's
29// prefix with its error. The client is built the way a member app builds
30// it from the delivered variables (objstore's delivered contract): static
31// credentials, the delivered region, and the endpoint path-style when one
32// is delivered.
33func probeStores(ctx context.Context, env map[string]string, id string) (string, error) {
34 for _, p := range storePrefixes(env) {
35 if err := probeStore(ctx, env, p, id); err != nil {
36 return p, err
37 }
38 }
39 return "", nil
40}
41
42func probeStore(ctx context.Context, env map[string]string, prefix, id string) error {
43 bn, rn, en, ak, sk := objstore.EnvNames(prefix)
44 region := env[rn]
45 if region == "" {
46 region = "us-east-1"
47 }
48 opts := awss3.Options{
49 Region: region,
50 Credentials: credentials.NewStaticCredentialsProvider(env[ak], env[sk], ""),
51 RequestChecksumCalculation: aws.RequestChecksumCalculationWhenRequired,
52 ResponseChecksumValidation: aws.ResponseChecksumValidationWhenRequired,
53 }
54 if ep := env[en]; ep != "" {
55 opts.BaseEndpoint = aws.String(ep)
56 opts.UsePathStyle = true
57 }
58 c := awss3.New(opts)
59 bucket := aws.String(env[bn])
60 key := aws.String(".carlos-move-probe/" + id)
61 body := []byte("carlos move probe " + id)
62 if _, err := c.ListObjectsV2(ctx, &awss3.ListObjectsV2Input{Bucket: bucket, MaxKeys: aws.Int32(1)}); err != nil {
63 return fmt.Errorf("list: %w", err)
64 }
65 if _, err := c.PutObject(ctx, &awss3.PutObjectInput{Bucket: bucket, Key: key, Body: bytes.NewReader(body)}); err != nil {
66 return fmt.Errorf("put: %w", err)
67 }
68 out, err := c.GetObject(ctx, &awss3.GetObjectInput{Bucket: bucket, Key: key})
69 if err != nil {
70 return fmt.Errorf("get: %w", err)
71 }
72 got, err := io.ReadAll(io.LimitReader(out.Body, 1<<16))
73 out.Body.Close()
74 if err != nil {
75 return fmt.Errorf("get: %w", err)
76 }
77 if !bytes.Equal(got, body) {
78 return errors.New("get: the probe object read back different bytes")
79 }
80 if _, err := c.DeleteObject(ctx, &awss3.DeleteObjectInput{Bucket: bucket, Key: key}); err != nil {
81 return fmt.Errorf("delete: %w", err)
82 }
83 return nil
84}
85
86// laneProbe is T10 in production: put-if-absent then delete-by-version of
87// moves/_lane/<id> in the source's bucket. A probe object left by an earlier
88// pass whose delete failed is removed by version and the check fails this
89// pass; the next one starts clean.
90func laneProbe(versioned func(ctx context.Context, bucket, region string) (lease.VersionedStore, error)) func(context.Context, string, string, string) error {
91 return func(ctx context.Context, bucket, region, id string) error {
92 vs, err := versioned(ctx, bucket, region)
93 if err != nil {
94 return err
95 }
96 key := moves.LaneKey(id)
97 ver, err := vs.PutIfAbsent(ctx, bucket, key, []byte(`{"probe":"`+id+`"}`))
98 if errors.Is(err, lease.ErrExists) {
99 vers, lerr := vs.ListVersions(ctx, bucket, key)
100 if lerr != nil {
101 return fmt.Errorf("a probe object is left and cannot be listed: %w", lerr)
102 }
103 for _, v := range vers {
104 if v.Key == key {
105 if derr := vs.DeleteVersion(ctx, bucket, key, v.VersionID); derr != nil {
106 return derr
107 }
108 }
109 }
110 return errors.New("a probe object was left by an earlier pass; removed")
111 }
112 if err != nil {
113 return err
114 }
115 return vs.DeleteVersion(ctx, bucket, key, ver)
116 }
117}
118
119// storePrefixes are the store bindings in an environment: every prefix P
120// with P_BUCKET, P_ACCESS_KEY and P_SECRET_KEY set (objstore.EnvNames).
121func storePrefixes(env map[string]string) []string {
122 var out []string
123 for name := range env {
124 p, ok := strings.CutSuffix(name, "_BUCKET")
125 if !ok || p == "" {
126 continue
127 }
128 _, _, _, ak, sk := objstore.EnvNames(p)
129 if env[ak] != "" && env[sk] != "" {
130 out = append(out, p)
131 }
132 }
133 sort.Strings(out)
134 return out
135}
136