master
go 450 lines 10 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package secretstore
4
5 import (
6 "context"
7 "fmt"
8 "maps"
9 "slices"
10 "sync"
11 "sync/atomic"
12 "time"
13
14 "gopkg.in/yaml.v2"
15 )
16
17 type storeRecord struct {
18 rawConfig Config
19 configHash uint64
20 status StoreStatus
21 published PublishedStore
22 }
23
24 type preparedStore struct {
25 key string
26 rawConfig Config
27 configHash uint64
28 status StoreStatus
29 published PublishedStore
30 }
31
32 type serviceState struct {
33 snapshot *Snapshot
34 records map[string]storeRecord
35 }
36
37 type creatorRegistry struct {
38 kinds []StoreKind
39 byKind map[StoreKind]Creator
40 }
41
42 type inMemoryService struct {
43 mu sync.Mutex
44 state atomic.Pointer[serviceState]
45 now func() time.Time
46 resolver *runtimeResolver
47 registry creatorRegistry
48 }
49
50 func NewService(creators ...Creator) Service {
51 return newInMemoryService(creators...)
52 }
53
54 func newInMemoryService(creators ...Creator) Service {
55 s := &inMemoryService{
56 now: time.Now,
57 resolver: newRuntimeResolver(),
58 registry: newCreatorRegistry(creators...),
59 }
60 s.state.Store(&serviceState{
61 snapshot: &Snapshot{
62 generation: 0,
63 publishedAt: s.now().UTC(),
64 stores: map[string]publishedRecord{},
65 },
66 records: map[string]storeRecord{},
67 })
68 return s
69 }
70
71 func (s *inMemoryService) Capture() *Snapshot {
72 state := s.state.Load()
73 if state == nil {
74 return &Snapshot{stores: map[string]publishedRecord{}}
75 }
76 return cloneSnapshot(state.snapshot)
77 }
78
79 func (s *inMemoryService) Resolve(ctx context.Context, snapshot *Snapshot, ref, original string) (string, error) {
80 if ctx != nil {
81 select {
82 case <-ctx.Done():
83 return "", ctx.Err()
84 default:
85 }
86 }
87 return s.resolver.resolveContext(ctx, snapshot, ref, original)
88 }
89
90 func (s *inMemoryService) Kinds() []StoreKind {
91 return append([]StoreKind(nil), s.registry.kinds...)
92 }
93
94 func (s *inMemoryService) DisplayName(kind StoreKind) (string, bool) {
95 creator, ok := s.registry.byKind[kind]
96 if !ok {
97 return "", false
98 }
99 return creator.DisplayName, true
100 }
101
102 func (s *inMemoryService) Schema(kind StoreKind) (string, bool) {
103 creator, ok := s.registry.byKind[kind]
104 if !ok {
105 return "", false
106 }
107 return creator.Schema, true
108 }
109
110 func (s *inMemoryService) New(kind StoreKind) (Store, bool) {
111 creator, ok := s.registry.byKind[kind]
112 if !ok || creator.Create == nil {
113 return nil, false
114 }
115 store := creator.Create()
116 if store == nil {
117 return nil, false
118 }
119 return store, true
120 }
121
122 func (s *inMemoryService) GetStatus(key string) (StoreStatus, bool) {
123 key, err := normalizeStoreKey(key)
124 if err != nil {
125 return StoreStatus{}, false
126 }
127
128 state := s.state.Load()
129 if state == nil {
130 return StoreStatus{}, false
131 }
132 record, ok := state.records[key]
133 if !ok {
134 return StoreStatus{}, false
135 }
136 return cloneStoreStatus(record.status), true
137 }
138
139 func (s *inMemoryService) Validate(ctx context.Context, cfg Config) error {
140 _, err := s.prepareConfig(ctx, cfg)
141 return err
142 }
143
144 func (s *inMemoryService) ValidateStored(ctx context.Context, key string) error {
145 key, err := normalizeStoreKey(key)
146 if err != nil {
147 return err
148 }
149
150 state := s.state.Load()
151 if state == nil {
152 return storeNotConfiguredError(key)
153 }
154 record, ok := state.records[key]
155 if !ok {
156 return storeNotConfiguredError(key)
157 }
158 validatedHash := record.configHash
159
160 _, err = s.prepareConfig(ctx, record.rawConfig)
161
162 validation := &ValidationStatus{
163 CheckedAt: s.now().UTC(),
164 OK: err == nil,
165 }
166
167 s.mu.Lock()
168 defer s.mu.Unlock()
169
170 current := s.state.Load()
171 if current == nil {
172 return storeNotConfiguredError(key)
173 }
174 updated, ok := current.records[key]
175 if !ok {
176 return storeNotConfiguredError(key)
177 }
178 if updated.configHash != validatedHash {
179 return fmt.Errorf("store '%s' changed during validation; retry", key)
180 }
181 updated.status.LastValidation = validation
182 if err != nil {
183 updated.status.LastErrorSummary = err.Error()
184 } else {
185 updated.status.LastErrorSummary = ""
186 }
187
188 records := cloneRecords(current.records)
189 records[key] = updated
190 s.state.Store(&serviceState{
191 snapshot: current.snapshot,
192 records: records,
193 })
194
195 return err
196 }
197
198 func (s *inMemoryService) Add(ctx context.Context, cfg Config) error {
199 prepared, err := s.prepareConfig(ctx, cfg)
200 if err != nil {
201 return err
202 }
203
204 s.mu.Lock()
205 defer s.mu.Unlock()
206
207 state := s.state.Load()
208 if state == nil {
209 state = &serviceState{
210 snapshot: &Snapshot{stores: map[string]publishedRecord{}},
211 records: map[string]storeRecord{},
212 }
213 }
214 if _, ok := state.records[prepared.key]; ok {
215 return storeAlreadyExistsError(prepared.key)
216 }
217
218 records := cloneRecords(state.records)
219 records[prepared.key] = prepared.record()
220 snapshot := newSnapshot(state.snapshot.Generation()+1, s.now().UTC(), records)
221 s.state.Store(&serviceState{snapshot: snapshot, records: records})
222 return nil
223 }
224
225 func (s *inMemoryService) Update(ctx context.Context, key string, cfg Config) error {
226 key, err := normalizeStoreKey(key)
227 if err != nil {
228 return err
229 }
230
231 state := s.state.Load()
232 if state == nil {
233 return storeNotConfiguredError(key)
234 }
235 before, ok := state.records[key]
236 if !ok {
237 return storeNotConfiguredError(key)
238 }
239
240 prepared, err := s.prepareConfig(ctx, cfg)
241 if err != nil {
242 return err
243 }
244 if prepared.key != key {
245 return fmt.Errorf("store key mismatch: path key '%s' differs from config key '%s'", key, prepared.key)
246 }
247
248 s.mu.Lock()
249 defer s.mu.Unlock()
250
251 current := s.state.Load()
252 if current == nil {
253 return storeNotConfiguredError(key)
254 }
255 before, ok = current.records[key]
256 if !ok {
257 return storeNotConfiguredError(key)
258 }
259
260 if before.configHash == prepared.configHash {
261 return nil
262 }
263
264 records := cloneRecords(current.records)
265 records[key] = prepared.record()
266 snapshot := newSnapshot(current.snapshot.Generation()+1, s.now().UTC(), records)
267 s.state.Store(&serviceState{snapshot: snapshot, records: records})
268 return nil
269 }
270
271 func (s *inMemoryService) Remove(key string) error {
272 key, err := normalizeStoreKey(key)
273 if err != nil {
274 return err
275 }
276
277 s.mu.Lock()
278 defer s.mu.Unlock()
279
280 state := s.state.Load()
281 if state == nil {
282 return storeNotConfiguredError(key)
283 }
284 if _, ok := state.records[key]; !ok {
285 return storeNotConfiguredError(key)
286 }
287
288 records := cloneRecords(state.records)
289 delete(records, key)
290 snapshot := newSnapshot(state.snapshot.Generation()+1, s.now().UTC(), records)
291 s.state.Store(&serviceState{snapshot: snapshot, records: records})
292 return nil
293 }
294
295 func newCreatorRegistry(creators ...Creator) creatorRegistry {
296 reg := creatorRegistry{
297 byKind: make(map[StoreKind]Creator, len(creators)),
298 }
299 for _, creator := range creators {
300 if creator.Kind == "" || creator.Create == nil {
301 continue
302 }
303 reg.byKind[creator.Kind] = creator
304 }
305 reg.kinds = make([]StoreKind, 0, len(reg.byKind))
306 for kind := range reg.byKind {
307 reg.kinds = append(reg.kinds, kind)
308 }
309 slices.Sort(reg.kinds)
310 return reg
311 }
312
313 func (s *inMemoryService) prepareConfig(ctx context.Context, cfg Config) (preparedStore, error) {
314 if cfg == nil {
315 return preparedStore{}, fmt.Errorf("store config is nil")
316 }
317 if ctx == nil {
318 ctx = context.Background()
319 }
320
321 raw := cloneConfig(cfg)
322 if raw == nil {
323 return preparedStore{}, fmt.Errorf("store config is nil")
324 }
325 if err := raw.Validate(); err != nil {
326 return preparedStore{}, err
327 }
328 rawConfig := cloneConfig(raw)
329 rawHash := raw.Hash()
330 resolvedPayload, err := resolveProviderPayload(ctx, raw)
331 if err != nil {
332 return preparedStore{}, err
333 }
334
335 kind := raw.Kind()
336 name := raw.Name()
337 key := raw.ExposedKey()
338
339 store, ok := s.New(kind)
340 if !ok {
341 return preparedStore{}, fmt.Errorf("store kind '%s' is not supported", kind)
342 }
343 if store.Configuration() == nil {
344 return preparedStore{}, fmt.Errorf("store '%s': configuration is nil", key)
345 }
346
347 bs, err := yaml.Marshal(raw)
348 if err != nil {
349 return preparedStore{}, fmt.Errorf("store '%s': marshaling raw config: %w", key, err)
350 }
351 if len(resolvedPayload) != 0 {
352 maps.Copy(raw, resolvedPayload)
353 bs, err = yaml.Marshal(raw)
354 if err != nil {
355 return preparedStore{}, fmt.Errorf("store '%s': marshaling resolved config: %w", key, err)
356 }
357 }
358 if err := yaml.Unmarshal(bs, store.Configuration()); err != nil {
359 return preparedStore{}, fmt.Errorf("store '%s': invalid provider payload: %w", key, err)
360 }
361
362 if err := store.Init(ctx); err != nil {
363 return preparedStore{}, err
364 }
365
366 published := store.Publish()
367 if published == nil {
368 return preparedStore{}, fmt.Errorf("store '%s': published resolver state is nil", key)
369 }
370
371 return preparedStore{
372 key: key,
373 rawConfig: rawConfig,
374 configHash: rawHash,
375 status: StoreStatus{
376 Name: name,
377 Kind: kind,
378 },
379 published: published,
380 }, nil
381 }
382
383 func newSnapshot(generation uint64, publishedAt time.Time, records map[string]storeRecord) *Snapshot {
384 stores := make(map[string]publishedRecord, len(records))
385 for key, record := range records {
386 stores[key] = publishedRecord{
387 published: record.published,
388 }
389 }
390 return &Snapshot{
391 generation: generation,
392 publishedAt: publishedAt,
393 stores: stores,
394 }
395 }
396
397 func cloneRecords(in map[string]storeRecord) map[string]storeRecord {
398 if len(in) == 0 {
399 return map[string]storeRecord{}
400 }
401 out := make(map[string]storeRecord, len(in))
402 for key, record := range in {
403 out[key] = storeRecord{
404 rawConfig: cloneConfig(record.rawConfig),
405 configHash: record.configHash,
406 status: cloneStoreStatus(record.status),
407 published: record.published,
408 }
409 }
410 return out
411 }
412
413 func normalizeStoreKey(key string) (string, error) {
414 kind, name, err := ParseStoreKey(key)
415 if err != nil {
416 return "", err
417 }
418 return StoreKey(kind, name), nil
419 }
420
421 func (p preparedStore) record() storeRecord {
422 return storeRecord{
423 rawConfig: cloneConfig(p.rawConfig),
424 configHash: p.configHash,
425 status: cloneStoreStatus(p.status),
426 published: p.published,
427 }
428 }
429
430 type wrappedStoreError struct {
431 msg string
432 err error
433 }
434
435 func (e wrappedStoreError) Error() string { return e.msg }
436 func (e wrappedStoreError) Unwrap() error { return e.err }
437
438 func storeAlreadyExistsError(key string) error {
439 return wrappedStoreError{
440 msg: fmt.Sprintf("store '%s' already exists", key),
441 err: ErrStoreExists,
442 }
443 }
444
445 func storeNotConfiguredError(key string) error {
446 return wrappedStoreError{
447 msg: fmt.Sprintf("store '%s' is not configured", key),
448 err: ErrStoreNotFound,
449 }
450 }