carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "crypto/rand"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "io"
10
11 "github.com/carlosframework/platform/internal/hostops"
12 "github.com/carlosframework/platform/internal/objstore"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// Request-local per-call effect adapters for the enabled create path. Each
17// backend or store call is its own durable effect under the request's
18// registration; the effect's permit is what lets the call through the
19// boundary handles beneath. Named private fields, every method explicit.
20
21type claimEffectContextKey struct{}
22
23type claimStoreEffects struct {
24 Runner hostops.EffectRunner
25 OperationID string
26}
27
28func claimEffectID() (string, error) {
29 var b [16]byte
30 if _, err := rand.Read(b[:]); err != nil {
31 return "", err
32 }
33 return hex.EncodeToString(b[:]), nil
34}
35
36func (e claimStoreEffects) run(ctx context.Context, resource, action string, inputs any, call func(context.Context) (string, error)) (string, error) {
37 id, err := claimEffectID()
38 if err != nil {
39 return "", err
40 }
41 body, err := json.Marshal(inputs)
42 if err != nil {
43 return "", err
44 }
45 return e.Runner.Run(ctx, hostops.Effect{ID: id, Resource: resource, Action: action, Inputs: map[string]string{"request": string(body)}}, call)
46}
47
48type claimEffectProvisioner struct {
49 base objstore.Provisioner
50 effects claimStoreEffects
51}
52
53var _ objstore.Provisioner = claimEffectProvisioner{}
54
55func (p claimEffectProvisioner) mutation(ctx context.Context, resource, action string, inputs any, call func(context.Context) error) error {
56 _, err := p.effects.run(ctx, resource, action, inputs, func(ctx context.Context) (string, error) {
57 if err := call(ctx); err != nil {
58 return "", err
59 }
60 return "joined", nil
61 })
62 return err
63}
64func (p claimEffectProvisioner) EnsureBucket(ctx context.Context, name string) (string, error) {
65 return p.effects.run(ctx, "bucket:"+name, "ensure-bucket", name, func(ctx context.Context) (string, error) { return p.base.EnsureBucket(ctx, name) })
66}
67func (p claimEffectProvisioner) EnsureUser(ctx context.Context, name, path, arn string, deny *objstore.DenyTag) (string, error) {
68 return p.effects.run(ctx, "iam-user:"+name, "ensure-user", []any{name, path, arn, deny}, func(ctx context.Context) (string, error) {
69 return p.base.EnsureUser(ctx, name, path, arn, deny)
70 })
71}
72func (p claimEffectProvisioner) MintKey(ctx context.Context, user string) (string, string, error) {
73 var id, secret string
74 _, err := p.effects.run(ctx, "iam-user:"+user, "mint-key", user, func(ctx context.Context) (string, error) {
75 var callErr error
76 id, secret, callErr = p.base.MintKey(ctx, user)
77 return id, callErr
78 })
79 // Only the key ID is journaled; the secret travels through the sealed
80 // config walk, never through admission.
81 if err != nil {
82 return "", "", err
83 }
84 return id, secret, nil
85}
86func (p claimEffectProvisioner) DeleteKey(ctx context.Context, user, id string) error {
87 return p.mutation(ctx, "iam-user:"+user, "delete-key", []string{user, id}, func(ctx context.Context) error { return p.base.DeleteKey(ctx, user, id) })
88}
89func (p claimEffectProvisioner) ReapKeys(ctx context.Context, user string, keep []string) error {
90 return p.mutation(ctx, "iam-user:"+user, "reap-keys", []any{user, keep}, func(ctx context.Context) error { return p.base.ReapKeys(ctx, user, keep) })
91}
92func (p claimEffectProvisioner) EmptyBucket(ctx context.Context, name string) error {
93 return p.mutation(ctx, "bucket:"+name, "empty-bucket", name, func(ctx context.Context) error { return p.base.EmptyBucket(ctx, name) })
94}
95func (p claimEffectProvisioner) DeleteBucket(ctx context.Context, name string) error {
96 return p.mutation(ctx, "bucket:"+name, "delete-bucket", name, func(ctx context.Context) error { return p.base.DeleteBucket(ctx, name) })
97}
98func (p claimEffectProvisioner) DeleteUser(ctx context.Context, name string) error {
99 return p.mutation(ctx, "iam-user:"+name, "delete-user", name, func(ctx context.Context) error { return p.base.DeleteUser(ctx, name) })
100}
101
102// claimEffectStore wraps the data boundary for one request: every Put is an
103// effect. Delete is refused (the walk never needs a bare delete).
104type claimEffectStore struct {
105 data release.Store
106 effects claimStoreEffects
107}
108
109var (
110 _ release.Store = claimEffectStore{}
111 _ release.ConditionalDeleter = claimEffectStore{}
112 _ release.Opener = claimEffectStore{}
113)
114
115func (s claimEffectStore) Get(ctx context.Context, key string) ([]byte, string, error) {
116 return s.data.Get(ctx, key)
117}
118func (s claimEffectStore) List(ctx context.Context, prefix string) ([]string, error) {
119 return s.data.List(ctx, prefix)
120}
121func (s claimEffectStore) ListDirs(ctx context.Context, prefix string) ([]string, error) {
122 return s.data.ListDirs(ctx, prefix)
123}
124func (s claimEffectStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) {
125 return release.OpenObject(ctx, s.data, key)
126}
127func (s claimEffectStore) Put(ctx context.Context, key string, body []byte, etag string) (string, error) {
128 // Hash and coordinates only: config payloads can carry sealed values.
129 sum := sha256.Sum256(body)
130 return s.effects.run(ctx, "control-object:"+key, "put", map[string]string{"key": key, "expected_etag": etag, "sha256": hex.EncodeToString(sum[:])}, func(ctx context.Context) (string, error) {
131 return s.data.Put(ctx, key, body, etag)
132 })
133}
134func (s claimEffectStore) Delete(context.Context, string) error { return hostops.ErrContract }
135func (s claimEffectStore) DeleteIfMatch(ctx context.Context, key, etag string) error {
136 _, err := s.effects.run(ctx, "control-object:"+key, "delete", map[string]string{"key": key, "expected_etag": etag}, func(ctx context.Context) (string, error) {
137 if err := release.DeleteIfMatch(ctx, s.data, key, etag); err != nil {
138 return "", err
139 }
140 return "joined", nil
141 })
142 return err
143}
144
145func claimStoreAdapters(ctx context.Context, store release.Store, p objstore.Provisioner) (release.Store, objstore.Provisioner) {
146 e, ok := ctx.Value(claimEffectContextKey{}).(claimStoreEffects)
147 if !ok {
148 return store, p
149 }
150 var prov objstore.Provisioner
151 if p != nil {
152 prov = claimEffectProvisioner{base: p, effects: e}
153 }
154 return claimEffectStore{data: store, effects: e}, prov
155}
156