-
Notifications
You must be signed in to change notification settings - Fork 146
/
state_store.go
340 lines (286 loc) · 9.12 KB
/
state_store.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package store
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"sync"
"github.com/elastic/elastic-agent/internal/pkg/agent/storage"
"github.com/elastic/elastic-agent/internal/pkg/fleetapi"
"github.com/elastic/elastic-agent/internal/pkg/fleetapi/acker"
"github.com/elastic/elastic-agent/pkg/core/logger"
)
// Version is the current StateStore version. If any breaking change is
// introduced, it should be increased and a migration added.
const Version = "1"
type saver interface {
Save(io.Reader) error
}
type saveLoader interface {
saver
Load() (io.ReadCloser, error)
}
// StateStore is a combined agent state storage initially derived from the former actionStore
// and modified to allow persistence of additional agent specific state information.
// The following is the original actionStore implementation description:
// receives multiples actions to persist to disk, the implementation of the store only
// take care of action policy change every other action are discarded. The store will only keep the
// last good action on disk, we assume that the action is added to the store after it was ACK with
// Fleet. The store is not thread safe.
type StateStore struct {
log *logger.Logger
store saveLoader
dirty bool
state state
mx sync.RWMutex
}
type state struct {
Version string `json:"version"`
ActionSerializer actionSerializer `json:"action,omitempty"`
AckToken string `json:"ack_token,omitempty"`
Queue actionQueue `json:"action_queue,omitempty"`
}
// actionSerializer is JSON Marshaler/Unmarshaler for fleetapi.Action.
type actionSerializer struct {
json.Marshaler
json.Unmarshaler
Action fleetapi.Action
}
// actionQueue stores scheduled actions to be executed and the type is needed
// to make it possible to marshal and unmarshal fleetapi.ScheduledActions.
// The fleetapi package marshal/unmarshal fleetapi.Actions, therefore it does
// not need to handle fleetapi.ScheduledAction separately. However, the store does,
// therefore the need for this type to do so.
type actionQueue []fleetapi.ScheduledAction
// NewStateStoreWithMigration creates a new state store and migrates the old ones.
func NewStateStoreWithMigration(
ctx context.Context,
log *logger.Logger,
actionStorePath,
stateStorePath string,
storageOpts ...storage.EncryptedOptionFunc) (*StateStore, error) {
stateDiskStore, err := storage.NewEncryptedDiskStore(
ctx, stateStorePath, storageOpts...)
if err != nil {
return nil, fmt.Errorf(
"could not create EncryptedDiskStore when creating StateStoreWithMigration: %w",
err)
}
return newStateStoreWithMigration(log, actionStorePath, stateDiskStore)
}
func newStateStoreWithMigration(
log *logger.Logger,
actionStorePath string,
stateStore storage.Storage) (*StateStore, error) {
err := migrateActionStoreToStateStore(log, actionStorePath, stateStore)
if err != nil {
return nil, fmt.Errorf("failed migrating action store to YAML state store: %w",
err)
}
err = migrateYAMLStateStoreToStateStoreV1(log, stateStore)
if err != nil {
return nil, fmt.Errorf("failed migrating YAML store JSON store: %w",
err)
}
return NewStateStore(log, stateStore)
}
// NewStateStoreActionAcker creates a new state store backed action acker.
func NewStateStoreActionAcker(acker acker.Acker, store *StateStore) *StateStoreActionAcker {
return &StateStoreActionAcker{acker: acker, store: store}
}
// NewStateStore creates a new state store.
func NewStateStore(log *logger.Logger, store saveLoader) (*StateStore, error) {
// If the store exists we will read it, if an error is returned we log it
// and return an empty store.
reader, err := store.Load()
if err != nil {
log.Warnf("failed to load state store, returning empty contents: %v", err.Error())
return &StateStore{log: log, store: store}, nil
}
defer reader.Close()
st, err := readState(reader)
if err != nil {
return nil, fmt.Errorf("could not parse store content: %w", err)
}
if st.Version != Version {
return nil, fmt.Errorf(
"invalid state store version, got %q isntead of %s",
st.Version, Version)
}
return &StateStore{
log: log,
store: store,
state: st,
}, nil
}
// readState parsed the content from reader as JSON to state.
// It's mostly to abstract the parsing of the date so different functions can
// reuse this.
func readState(reader io.ReadCloser) (state, error) {
st := state{}
data, err := io.ReadAll(reader)
if err != nil {
return state{}, fmt.Errorf("could not read store state: %w", err)
}
if len(data) == 0 {
// empty file
return state{Version: "1"}, nil
}
err = json.Unmarshal(data, &st)
if err != nil {
return state{}, fmt.Errorf("could not parse JSON: %w", err)
}
return st, nil
}
// SetAction sets the current action. It accepts ActionPolicyChange or
// ActionUnenroll. Any other type will be silently discarded.
func (s *StateStore) SetAction(a fleetapi.Action) {
s.mx.Lock()
defer s.mx.Unlock()
switch v := a.(type) {
case *fleetapi.ActionPolicyChange, *fleetapi.ActionUnenroll:
// Only persist the action if the action is different.
if s.state.ActionSerializer.Action != nil &&
s.state.ActionSerializer.Action.ID() == v.ID() {
return
}
s.dirty = true
s.state.ActionSerializer.Action = a
}
}
// SetAckToken set ack token to the agent state
func (s *StateStore) SetAckToken(ackToken string) {
s.mx.Lock()
defer s.mx.Unlock()
if s.state.AckToken == ackToken {
return
}
s.dirty = true
s.state.AckToken = ackToken
}
// SetQueue sets the action_queue to agent state
func (s *StateStore) SetQueue(q []fleetapi.ScheduledAction) {
s.mx.Lock()
defer s.mx.Unlock()
s.state.Queue = q
s.dirty = true
}
// Save saves the actions into a state store.
func (s *StateStore) Save() error {
s.mx.Lock()
defer s.mx.Unlock()
defer func() { s.dirty = false }()
if !s.dirty {
return nil
}
var reader io.Reader
switch a := s.state.ActionSerializer.Action.(type) {
case *fleetapi.ActionPolicyChange,
*fleetapi.ActionUnenroll,
nil:
// ok
default:
return fmt.Errorf("incompatible type, expected ActionPolicyChange, "+
"ActionUnenroll or nil, but received %T", a)
}
reader, err := jsonToReader(&s.state)
if err != nil {
return err
}
if err := s.store.Save(reader); err != nil {
return err
}
s.log.Debugf("save state on disk : %+v", s.state)
return nil
}
// Queue returns a copy of the queue
func (s *StateStore) Queue() []fleetapi.ScheduledAction {
s.mx.RLock()
defer s.mx.RUnlock()
q := make([]fleetapi.ScheduledAction, len(s.state.Queue))
copy(q, s.state.Queue)
return q
}
// Action the action to execute. See SetAction for the possible action types.
func (s *StateStore) Action() fleetapi.Action {
s.mx.RLock()
defer s.mx.RUnlock()
if s.state.ActionSerializer.Action == nil {
return nil
}
return s.state.ActionSerializer.Action
}
// AckToken return the agent state persisted ack_token
func (s *StateStore) AckToken() string {
s.mx.RLock()
defer s.mx.RUnlock()
return s.state.AckToken
}
// StateStoreActionAcker wraps an existing acker and will set any acked event
// in the state store. It's up to the state store to decide if we need to
// persist the event for future replay or just discard the event.
type StateStoreActionAcker struct {
acker acker.Acker
store *StateStore
}
// Ack acks the action using underlying acker.
// After the action is acked it is stored in the StateStore. The StateStore
// decides if the action needs to be persisted or not.
func (a *StateStoreActionAcker) Ack(ctx context.Context, action fleetapi.Action) error {
if err := a.acker.Ack(ctx, action); err != nil {
return err
}
a.store.SetAction(action)
return a.store.Save()
}
// Commit commits acks.
func (a *StateStoreActionAcker) Commit(ctx context.Context) error {
return a.acker.Commit(ctx)
}
func (as *actionSerializer) MarshalJSON() ([]byte, error) {
return json.Marshal(as.Action)
}
func (as *actionSerializer) UnmarshalJSON(data []byte) error {
var typeUnmarshaler struct {
Type string `json:"type,omitempty" yaml:"type,omitempty"`
}
err := json.Unmarshal(data, &typeUnmarshaler)
if err != nil {
return err
}
as.Action = fleetapi.NewAction(typeUnmarshaler.Type)
err = json.Unmarshal(data, &as.Action)
if err != nil {
return err
}
return nil
}
func (aq *actionQueue) UnmarshalJSON(data []byte) error {
actions := fleetapi.Actions{}
err := json.Unmarshal(data, &actions)
if err != nil {
return fmt.Errorf("actionQueue failed to unmarshal: %w", err)
}
var scheduledActions []fleetapi.ScheduledAction
for _, a := range actions {
sa, ok := a.(fleetapi.ScheduledAction)
if !ok {
return fmt.Errorf("actionQueue: action %s isn't a ScheduledAction,"+
"cannot unmarshal it to actionQueue", a.Type())
}
scheduledActions = append(scheduledActions, sa)
}
*aq = scheduledActions
return nil
}
func jsonToReader(in interface{}) (io.Reader, error) {
data, err := json.Marshal(in)
if err != nil {
return nil, fmt.Errorf("could not marshal to JSON: %w", err)
}
return bytes.NewReader(data), nil
}