From db6c7a6fe7a891f615f0d057a985566fa75c3ee9 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 15 Feb 2024 14:22:29 +0100 Subject: [PATCH 01/39] Fix action store -> state store migration tests (#4235) * use golden file for store migration test * remove withFile function * migrate take in a storage.Store instead of the storage's path. It's needed so we can set the encrypted store vault's path --- .../pkg/agent/storage/store/state_store.go | 17 +- .../agent/storage/store/state_store_test.go | 561 ++++++++++-------- .../store/testdata/7.17.18-action_store.yml | 6 + 3 files changed, 319 insertions(+), 265 deletions(-) create mode 100644 internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index 3e794c3547b..1955d479e67 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -76,7 +76,9 @@ type stateSerializer struct { // NewStateStoreWithMigration creates a new state store and migrates the old one. func NewStateStoreWithMigration(ctx context.Context, log *logger.Logger, actionStorePath, stateStorePath string) (*StateStore, error) { - err := migrateStateStore(ctx, log, actionStorePath, stateStorePath) + + stateDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath) + err := migrateStateStore(log, actionStorePath, stateDiskStore) if err != nil { return nil, err } @@ -143,20 +145,23 @@ func NewStateStore(log *logger.Logger, store storeLoad) (*StateStore, error) { }, nil } -func migrateStateStore(ctx context.Context, log *logger.Logger, actionStorePath, stateStorePath string) (err error) { +func migrateStateStore( + log *logger.Logger, + actionStorePath string, + stateDiskStore storage.Storage) (err error) { + log = log.Named("state_migration") actionDiskStore := storage.NewDiskStore(actionStorePath) - stateDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath) stateStoreExits, err := stateDiskStore.Exists() if err != nil { - log.Errorf("failed to check if state store %s exists: %v", stateStorePath, err) + log.Errorf("failed to check if state store exists: %v", err) return err } // do not migrate if the state store already exists if stateStoreExits { - log.Debugf("state store %s already exists", stateStorePath) + log.Debugf("state store already exists") return nil } @@ -204,7 +209,7 @@ func migrateStateStore(ctx context.Context, log *logger.Logger, actionStorePath, err = stateStore.Save() if err != nil { - log.Debugf("failed to save agent state store %s, err: %v", stateStorePath, err) + log.Debugf("failed to save agent state store, err: %v", err) } return err } diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 0e969a7525e..a31cb9006db 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -6,26 +6,31 @@ package store import ( "context" + "io" + "os" "path/filepath" + "runtime" "sync" "testing" "time" - "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/elastic/elastic-agent/internal/pkg/agent/application/secret" "github.com/elastic/elastic-agent/internal/pkg/agent/storage" + "github.com/elastic/elastic-agent/internal/pkg/agent/vault" "github.com/elastic/elastic-agent/internal/pkg/fleetapi" "github.com/elastic/elastic-agent/pkg/core/logger" ) func TestStateStore(t *testing.T) { t.Run("ack token", func(t *testing.T) { - runTestStateStore(t, "") + runTestStateStore(t, "czlV93YBwdkt5lYhBY7S") }) t.Run("no ack token", func(t *testing.T) { - runTestStateStore(t, "czlV93YBwdkt5lYhBY7S") + runTestStateStore(t, "") }) } @@ -35,264 +40,302 @@ func runTestStateStore(t *testing.T, ackToken string) { ctx, cn := context.WithCancel(context.Background()) defer cn() - withFile := func(fn func(t *testing.T, file string)) func(*testing.T) { - return func(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "state.yml") - fn(t, file) + t.Run("action returns empty when no action is saved on disk", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + require.Empty(t, store.Actions()) + require.Empty(t, store.Queue()) + }) + + t.Run("will discard silently unknown action", func(t *testing.T) { + actionPolicyChange := &fleetapi.ActionUnknown{ + ActionID: "abc123", } - } - t.Run("action returns empty when no action is saved on disk", - withFile(func(t *testing.T, file string) { - s := storage.NewDiskStore(file) - store, err := NewStateStore(log, s) - require.NoError(t, err) - require.Empty(t, store.Actions()) - require.Empty(t, store.Queue()) - })) - - t.Run("will discard silently unknown action", - withFile(func(t *testing.T, file string) { - actionPolicyChange := &fleetapi.ActionUnknown{ - ActionID: "abc123", - } - - s := storage.NewDiskStore(file) - store, err := NewStateStore(log, s) - require.NoError(t, err) - - require.Equal(t, 0, len(store.Actions())) - store.Add(actionPolicyChange) - store.SetAckToken(ackToken) - err = store.Save() - require.NoError(t, err) - require.Empty(t, store.Actions()) - require.Empty(t, store.Queue()) - require.Equal(t, ackToken, store.AckToken()) - })) - - t.Run("can save to disk known action type", - withFile(func(t *testing.T, file string) { - ActionPolicyChange := &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Policy: map[string]interface{}{ - "hello": "world", - }, - } - - s := storage.NewDiskStore(file) - store, err := NewStateStore(log, s) - require.NoError(t, err) - - require.Empty(t, store.Actions()) - require.Empty(t, store.Queue()) - store.Add(ActionPolicyChange) - store.SetAckToken(ackToken) - err = store.Save() - require.NoError(t, err) - require.Len(t, store.Actions(), 1) - require.Empty(t, store.Queue()) - require.Equal(t, ackToken, store.AckToken()) - - s = storage.NewDiskStore(file) - store1, err := NewStateStore(log, s) - require.NoError(t, err) - - actions := store1.Actions() - require.Len(t, actions, 1) - require.Empty(t, store1.Queue()) - - require.Equal(t, ActionPolicyChange, actions[0]) - require.Equal(t, ackToken, store.AckToken()) - })) - - t.Run("can save a queue with one upgrade action", - withFile(func(t *testing.T, file string) { - ts := time.Now().UTC().Round(time.Second) - queue := []action{&fleetapi.ActionUpgrade{ - ActionID: "test", - ActionType: fleetapi.ActionTypeUpgrade, - ActionStartTime: ts.Format(time.RFC3339), - Version: "1.2.3", - SourceURI: "https://example.com", - }} - - s := storage.NewDiskStore(file) - store, err := NewStateStore(log, s) - require.NoError(t, err) - - require.Empty(t, store.Actions()) - store.SetQueue(queue) - err = store.Save() - require.NoError(t, err) - require.Empty(t, store.Actions()) - require.Len(t, store.Queue(), 1) - - s = storage.NewDiskStore(file) - store1, err := NewStateStore(log, s) - require.NoError(t, err) - require.Empty(t, store1.Actions()) - require.Len(t, store1.Queue(), 1) - require.Equal(t, "test", store1.Queue()[0].ID()) - scheduledAction, ok := store1.Queue()[0].(fleetapi.ScheduledAction) - require.True(t, ok, "expected to be able to cast Action as ScheduledAction") - start, err := scheduledAction.StartTime() - require.NoError(t, err) - require.Equal(t, ts, start) - })) - - t.Run("can save a queue with two actions", - withFile(func(t *testing.T, file string) { - ts := time.Now().UTC().Round(time.Second) - queue := []action{&fleetapi.ActionUpgrade{ - ActionID: "test", - ActionType: fleetapi.ActionTypeUpgrade, - ActionStartTime: ts.Format(time.RFC3339), - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Policy: map[string]interface{}{ - "hello": "world", - }, - }} - - s := storage.NewDiskStore(file) - store, err := NewStateStore(log, s) - require.NoError(t, err) - - require.Empty(t, store.Actions()) - store.SetQueue(queue) - err = store.Save() - require.NoError(t, err) - require.Empty(t, store.Actions()) - require.Len(t, store.Queue(), 2) - - s = storage.NewDiskStore(file) - store1, err := NewStateStore(log, s) - require.NoError(t, err) - require.Empty(t, store1.Actions()) - require.Len(t, store1.Queue(), 2) - - require.Equal(t, "test", store1.Queue()[0].ID()) - scheduledAction, ok := store1.Queue()[0].(fleetapi.ScheduledAction) - require.True(t, ok, "expected to be able to cast Action as ScheduledAction") - start, err := scheduledAction.StartTime() - require.NoError(t, err) - require.Equal(t, ts, start) - retryableAction, ok := store1.Queue()[0].(fleetapi.RetryableAction) - require.True(t, ok, "expected to be able to cast Action as RetryableAction") - require.Equal(t, 1, retryableAction.RetryAttempt()) - - require.Equal(t, "abc123", store1.Queue()[1].ID()) - _, ok = store1.Queue()[1].(fleetapi.ScheduledAction) - require.False(t, ok, "expected cast to ScheduledAction to fail") - })) - - t.Run("can save to disk unenroll action type", - withFile(func(t *testing.T, file string) { - action := &fleetapi.ActionUnenroll{ - ActionID: "abc123", - ActionType: "UNENROLL", - } - - s := storage.NewDiskStore(file) - store, err := NewStateStore(log, s) - require.NoError(t, err) - - require.Empty(t, store.Actions()) - require.Empty(t, store.Queue()) - store.Add(action) - store.SetAckToken(ackToken) - err = store.Save() - require.NoError(t, err) - require.Len(t, store.Actions(), 1) - require.Empty(t, store.Queue()) - require.Equal(t, ackToken, store.AckToken()) - - s = storage.NewDiskStore(file) - store1, err := NewStateStore(log, s) - require.NoError(t, err) - - actions := store1.Actions() - require.Len(t, actions, 1) - require.Empty(t, store1.Queue()) - require.Equal(t, action, actions[0]) - require.Equal(t, ackToken, store.AckToken()) - })) - - t.Run("when we ACK we save to disk", - withFile(func(t *testing.T, file string) { - ActionPolicyChange := &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - } - - s := storage.NewDiskStore(file) - store, err := NewStateStore(log, s) - require.NoError(t, err) - store.SetAckToken(ackToken) - - acker := NewStateStoreActionAcker(&testAcker{}, store) - require.Empty(t, store.Actions()) - - require.NoError(t, acker.Ack(context.Background(), ActionPolicyChange)) - require.Len(t, store.Actions(), 1) - require.Empty(t, store.Queue()) - require.Equal(t, ackToken, store.AckToken()) - })) - - t.Run("migrate actions file does not exists", - withFile(func(t *testing.T, actionStorePath string) { - withFile(func(t *testing.T, stateStorePath string) { - err := migrateStateStore(ctx, log, actionStorePath, stateStorePath) - require.NoError(t, err) - stateStore, err := NewStateStore(log, storage.NewDiskStore(stateStorePath)) - require.NoError(t, err) - stateStore.SetAckToken(ackToken) - require.Empty(t, stateStore.Actions()) - require.Equal(t, ackToken, stateStore.AckToken()) - require.Empty(t, stateStore.Queue()) - }) - })) - - t.Run("migrate", - withFile(func(t *testing.T, actionStorePath string) { - ActionPolicyChange := &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Policy: map[string]interface{}{ - "hello": "world", - }, - } - - actionStore, err := newActionStore(log, storage.NewDiskStore(actionStorePath)) - require.NoError(t, err) - - require.Empty(t, actionStore.actions()) - actionStore.add(ActionPolicyChange) - err = actionStore.save() - require.NoError(t, err) - require.Len(t, actionStore.actions(), 1) - - withFile(func(t *testing.T, stateStorePath string) { - err = migrateStateStore(ctx, log, actionStorePath, stateStorePath) - require.NoError(t, err) - - stateStore, err := NewStateStore(log, storage.NewDiskStore(stateStorePath)) - require.NoError(t, err) - stateStore.SetAckToken(ackToken) - diff := cmp.Diff(actionStore.actions(), stateStore.Actions()) - if diff != "" { - t.Error(diff) - } - require.Equal(t, ackToken, stateStore.AckToken()) - require.Empty(t, stateStore.Queue()) - }) - })) + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + + require.Equal(t, 0, len(store.Actions())) + store.Add(actionPolicyChange) + store.SetAckToken(ackToken) + err = store.Save() + require.NoError(t, err) + require.Empty(t, store.Actions()) + require.Empty(t, store.Queue()) + require.Equal(t, ackToken, store.AckToken()) + }) + + t.Run("can save to disk known action type", func(t *testing.T) { + ActionPolicyChange := &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Policy: map[string]interface{}{ + "hello": "world", + }, + } + + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + + require.Empty(t, store.Actions()) + require.Empty(t, store.Queue()) + store.Add(ActionPolicyChange) + store.SetAckToken(ackToken) + err = store.Save() + require.NoError(t, err) + require.Len(t, store.Actions(), 1) + require.Empty(t, store.Queue()) + require.Equal(t, ackToken, store.AckToken()) + + s = storage.NewDiskStore(storePath) + store1, err := NewStateStore(log, s) + require.NoError(t, err) + + actions := store1.Actions() + require.Len(t, actions, 1) + require.Empty(t, store1.Queue()) + + require.Equal(t, ActionPolicyChange, actions[0]) + require.Equal(t, ackToken, store.AckToken()) + }) + + t.Run("can save a queue with one upgrade action", func(t *testing.T) { + ts := time.Now().UTC().Round(time.Second) + queue := []action{&fleetapi.ActionUpgrade{ + ActionID: "test", + ActionType: fleetapi.ActionTypeUpgrade, + ActionStartTime: ts.Format(time.RFC3339), + Version: "1.2.3", + SourceURI: "https://example.com", + }} + + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + + require.Empty(t, store.Actions()) + store.SetQueue(queue) + err = store.Save() + require.NoError(t, err) + require.Empty(t, store.Actions()) + require.Len(t, store.Queue(), 1) + + s = storage.NewDiskStore(storePath) + store1, err := NewStateStore(log, s) + require.NoError(t, err) + require.Empty(t, store1.Actions()) + require.Len(t, store1.Queue(), 1) + require.Equal(t, "test", store1.Queue()[0].ID()) + scheduledAction, ok := store1.Queue()[0].(fleetapi.ScheduledAction) + require.True(t, ok, "expected to be able to cast Action as ScheduledAction") + start, err := scheduledAction.StartTime() + require.NoError(t, err) + require.Equal(t, ts, start) + }) + + t.Run("can save a queue with two actions", func(t *testing.T) { + ts := time.Now().UTC().Round(time.Second) + queue := []action{&fleetapi.ActionUpgrade{ + ActionID: "test", + ActionType: fleetapi.ActionTypeUpgrade, + ActionStartTime: ts.Format(time.RFC3339), + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Policy: map[string]interface{}{ + "hello": "world", + }, + }} + + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + + require.Empty(t, store.Actions()) + store.SetQueue(queue) + err = store.Save() + require.NoError(t, err) + require.Empty(t, store.Actions()) + require.Len(t, store.Queue(), 2) + + s = storage.NewDiskStore(storePath) + store1, err := NewStateStore(log, s) + require.NoError(t, err) + require.Empty(t, store1.Actions()) + require.Len(t, store1.Queue(), 2) + + require.Equal(t, "test", store1.Queue()[0].ID()) + scheduledAction, ok := store1.Queue()[0].(fleetapi.ScheduledAction) + require.True(t, ok, "expected to be able to cast Action as ScheduledAction") + start, err := scheduledAction.StartTime() + require.NoError(t, err) + require.Equal(t, ts, start) + retryableAction, ok := store1.Queue()[0].(fleetapi.RetryableAction) + require.True(t, ok, "expected to be able to cast Action as RetryableAction") + require.Equal(t, 1, retryableAction.RetryAttempt()) + + require.Equal(t, "abc123", store1.Queue()[1].ID()) + _, ok = store1.Queue()[1].(fleetapi.ScheduledAction) + require.False(t, ok, "expected cast to ScheduledAction to fail") + }) + + t.Run("can save to disk unenroll action type", func(t *testing.T) { + action := &fleetapi.ActionUnenroll{ + ActionID: "abc123", + ActionType: "UNENROLL", + } + + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + + require.Empty(t, store.Actions()) + require.Empty(t, store.Queue()) + store.Add(action) + store.SetAckToken(ackToken) + err = store.Save() + require.NoError(t, err) + require.Len(t, store.Actions(), 1) + require.Empty(t, store.Queue()) + require.Equal(t, ackToken, store.AckToken()) + + s = storage.NewDiskStore(storePath) + store1, err := NewStateStore(log, s) + require.NoError(t, err) + + actions := store1.Actions() + require.Len(t, actions, 1) + require.Empty(t, store1.Queue()) + require.Equal(t, action, actions[0]) + require.Equal(t, ackToken, store.AckToken()) + }) + + t.Run("when we ACK we save to disk", func(t *testing.T) { + ActionPolicyChange := &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + } + + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + store.SetAckToken(ackToken) + + acker := NewStateStoreActionAcker(&testAcker{}, store) + require.Empty(t, store.Actions()) + + require.NoError(t, acker.Ack(context.Background(), ActionPolicyChange)) + require.Len(t, store.Actions(), 1) + require.Empty(t, store.Queue()) + require.Equal(t, ackToken, store.AckToken()) + }) + + t.Run("migrate actions file does not exists", func(t *testing.T) { + if runtime.GOOS == "darwin" { + // the original test never actually run, so with this at least + // there is coverage for linux and windows. + t.Skipf("needs https://github.com/elastic/elastic-agent/issues/3866" + + "to be merged so this test can work on darwin") + } + + tempDir := t.TempDir() + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + newStateStorePath := filepath.Join(tempDir, "state_store.yml") + + newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath) + err := migrateStateStore(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "migration action store -> state store failed") + + // to load from disk a new store needs to be created, it loads the file + // to memory during the store creation. + stateStore, err := NewStateStore(log, storage.NewDiskStore(newStateStorePath)) + require.NoError(t, err) + stateStore.SetAckToken(ackToken) + require.Empty(t, stateStore.Actions()) + require.Equal(t, ackToken, stateStore.AckToken()) + require.Empty(t, stateStore.Queue()) + }) + + t.Run("migrate", func(t *testing.T) { + if runtime.GOOS == "darwin" { + // the original migrate never actually run, so with this at least + // there is coverage for linux and windows. + t.Skipf("needs https://github.com/elastic/elastic-agent/issues/3866" + + "to be merged so this test can work on darwin") + } + + want := &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42, + }, + } + + tempDir := t.TempDir() + vaultPath := filepath.Join(tempDir, "vault") + err := os.MkdirAll(vaultPath, 0o750) + require.NoError(t, err, + "could not create directory for the agent's vault") + _, err = vault.New(ctx, vaultPath) + require.NoError(t, err, "could not create agent's vault") + err = secret.CreateAgentSecret( + context.Background(), secret.WithVaultPath(vaultPath)) + require.NoError(t, err, "could not create agent secret") + + // Copy the golden file as the migration deletes the old store. + goldenActionStoreFile, err := os.Open( + filepath.Join("testdata", "7.17.18-action_store.yml")) + require.NoError(t, err, "could not open action store golden file") + defer goldenActionStoreFile.Close() + + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + storeFile, err := os.Create(oldActionStorePath) + require.NoError(t, err, "could not create action store file") + + _, err = io.Copy(storeFile, goldenActionStoreFile) + require.NoError(t, err, "could not copy action store golden file") + err = storeFile.Close() + // It needs to be closed now otherwise on windows the store will fail to + // open the file. + require.NoError(t, err, "could not close store file") + + newStateStorePath := filepath.Join(tempDir, "state_store.yaml") + newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath, + storage.WithVaultPath(vaultPath)) + err = migrateStateStore(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "migration action store -> state store failed") + + // to load from disk a new store needs to be created, it loads the file + // to memory during the store creation. + newStateStore = storage.NewEncryptedDiskStore(ctx, newStateStorePath, + storage.WithVaultPath(vaultPath)) + stateStore, err := NewStateStore(log, newStateStore) + require.NoError(t, err, "could not create state store") + + actions := stateStore.Actions() + require.Len(t, actions, 1, "state store should load exactly 1 action") + got := actions[0] + + assert.Equalf(t, want, got, + "loaded action differs from action on the old action store") + assert.Empty(t, stateStore.Queue(), + "queue should be empty, old action store did not have a queue") + }) } diff --git a/internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml b/internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml new file mode 100644 index 00000000000..8f559ec80d9 --- /dev/null +++ b/internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml @@ -0,0 +1,6 @@ +action_id: abc123 +action_type: POLICY_CHANGE +policy: + answer: 42 + hello: world + phi: 1.618 From 19c8fd0d5bcdb81170d4fba53862d649e7f7b05d Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 23 Feb 2024 07:03:54 +0100 Subject: [PATCH 02/39] update action model to match fleet-server schema (#4240) * simplify fleetapi.Actions.UnmarshalJSON * add test to ensure the state store is correctly loaded from disk * skip state store migration tests, they will be fixes on a follow-up PR as part of https://github.com/elastic/elastic-agent/issues/3912 --- .../actions/handlers/handler_action_cancel.go | 8 +- .../handlers/handler_action_policy_change.go | 2 +- .../handler_action_policy_change_test.go | 8 +- .../handlers/handler_action_settings.go | 9 +- .../handlers/handler_action_upgrade.go | 15 +- .../handlers/handler_action_upgrade_test.go | 12 +- .../application/dispatcher/dispatcher.go | 4 +- .../application/dispatcher/dispatcher_test.go | 20 +- .../agent/application/upgrade/step_mark.go | 10 +- .../pkg/agent/storage/store/action_store.go | 29 +- .../agent/storage/store/action_store_test.go | 5 +- .../pkg/agent/storage/store/state_store.go | 107 ++++-- .../agent/storage/store/state_store_test.go | 225 ++++++++++- internal/pkg/config/operations/inspector.go | 2 +- internal/pkg/fleetapi/ack_cmd_test.go | 7 +- .../fleetapi/acker/fleet/fleet_acker_test.go | 15 +- internal/pkg/fleetapi/action.go | 353 ++++++------------ internal/pkg/fleetapi/action_test.go | 22 +- internal/pkg/fleetapi/checkin_cmd_test.go | 2 +- 19 files changed, 489 insertions(+), 366 deletions(-) diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_cancel.go b/internal/pkg/agent/application/actions/handlers/handler_action_cancel.go index bb48b2bd753..3086fe8e7a1 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_cancel.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_cancel.go @@ -37,11 +37,13 @@ func (h *Cancel) Handle(ctx context.Context, a fleetapi.Action, acker acker.Acke if !ok { return fmt.Errorf("invalid type, expected ActionCancel and received %T", a) } - n := h.c.Cancel(action.TargetID) + n := h.c.Cancel(action.Data.TargetID) if n == 0 { - h.log.Debugf("Cancel action id: %s target id: %s found no actions in queue.", action.ActionID, action.TargetID) + h.log.Debugf("Cancel action id: %s target id: %s found no actions in queue.", + action.ActionID, action.Data.TargetID) return nil } - h.log.Infof("Cancel action id: %s target id: %s removed %d action(s) from queue.", action.ActionID, action.TargetID, n) + h.log.Infof("Cancel action id: %s target id: %s removed %d action(s) from queue.", + action.ActionID, action.Data.TargetID, n) return nil } diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_policy_change.go b/internal/pkg/agent/application/actions/handlers/handler_action_policy_change.go index af76e83f6d1..7ce4660531b 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_policy_change.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_policy_change.go @@ -98,7 +98,7 @@ func (h *PolicyChangeHandler) Handle(ctx context.Context, a fleetapi.Action, ack // // Cache signature validation key for the next policy handling // h.signatureValidationKey = signatureValidationKey - c, err := config.NewConfigFrom(action.Policy) + c, err := config.NewConfigFrom(action.Data.Policy) if err != nil { return errors.New(err, "could not parse the configuration from the policy", errors.TypeConfig) } diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_policy_change_test.go b/internal/pkg/agent/application/actions/handlers/handler_action_policy_change_test.go index b56bb1a1ba4..72cb77d491f 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_policy_change_test.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_policy_change_test.go @@ -44,7 +44,9 @@ func TestPolicyChange(t *testing.T) { action := &fleetapi.ActionPolicyChange{ ActionID: "abc123", ActionType: "POLICY_CHANGE", - Policy: conf, + Data: fleetapi.ActionPolicyChangeData{ + Policy: conf, + }, } cfg := configuration.DefaultConfiguration() @@ -73,7 +75,9 @@ func TestPolicyAcked(t *testing.T) { action := &fleetapi.ActionPolicyChange{ ActionID: actionID, ActionType: "POLICY_CHANGE", - Policy: config, + Data: fleetapi.ActionPolicyChangeData{ + Policy: config, + }, } cfg := configuration.DefaultConfiguration() diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_settings.go b/internal/pkg/agent/application/actions/handlers/handler_action_settings.go index 68be5715dab..ed59fbe6266 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_settings.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_settings.go @@ -45,17 +45,18 @@ func (h *Settings) Handle(ctx context.Context, a fleetapi.Action, acker acker.Ac return fmt.Errorf("invalid type, expected ActionSettings and received %T", a) } - if !isSupportedLogLevel(action.LogLevel) { - return fmt.Errorf("invalid log level, expected debug|info|warning|error and received '%s'", action.LogLevel) + if !isSupportedLogLevel(action.Data.LogLevel) { + return fmt.Errorf("invalid log level, expected debug|info|warning|error and received '%s'", + action.Data.LogLevel) } lvl := logp.InfoLevel - err := lvl.Unpack(action.LogLevel) + err := lvl.Unpack(action.Data.LogLevel) if err != nil { return fmt.Errorf("failed to unpack log level: %w", err) } - if err := h.agentInfo.SetLogLevel(ctx, action.LogLevel); err != nil { + if err := h.agentInfo.SetLogLevel(ctx, action.Data.LogLevel); err != nil { return fmt.Errorf("failed to update log level: %w", err) } diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_upgrade.go b/internal/pkg/agent/application/actions/handlers/handler_action_upgrade.go index 6d58797f37e..ce24f297168 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_upgrade.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_upgrade.go @@ -74,9 +74,9 @@ func (h *Upgrade) Handle(ctx context.Context, a fleetapi.Action, ack acker.Acker } go func() { - h.log.Infof("starting upgrade to version %s in background", action.Version) - if err := h.coord.Upgrade(asyncCtx, action.Version, action.SourceURI, action, false, false); err != nil { - h.log.Errorf("upgrade to version %s failed: %v", action.Version, err) + h.log.Infof("starting upgrade to version %s in background", action.Data.Version) + if err := h.coord.Upgrade(asyncCtx, action.Data.Version, action.Data.SourceURI, action, false, false); err != nil { + h.log.Errorf("upgrade to version %s failed: %v", action.Data.Version, err) // If context is cancelled in getAsyncContext, the actions are acked there if !errors.Is(asyncCtx.Err(), context.Canceled) { h.bkgMutex.Lock() @@ -125,14 +125,17 @@ func (h *Upgrade) getAsyncContext(ctx context.Context, action fleetapi.Action, a h.log.Errorf("invalid type, expected ActionUpgrade and received %T", action) return nil, false } - if (upgradeAction.Version == bkgAction.Version) && (upgradeAction.SourceURI == bkgAction.SourceURI) { - h.log.Infof("Duplicate upgrade to version %s received", bkgAction.Version) + if (upgradeAction.Data.Version == bkgAction.Data.Version) && + (upgradeAction.Data.SourceURI == bkgAction.Data.SourceURI) { + h.log.Infof("Duplicate upgrade to version %s received", + bkgAction.Data.Version) h.bkgActions = append(h.bkgActions, action) return nil, false } // Versions must be different, cancel the first upgrade and run the new one - h.log.Infof("Canceling upgrade to version %s received", bkgAction.Version) + h.log.Infof("Canceling upgrade to version %s received", + bkgAction.Data.Version) h.bkgCancel() // Ack here because we have the lock, and we need to clear out the saved actions diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_upgrade_test.go b/internal/pkg/agent/application/actions/handlers/handler_action_upgrade_test.go index 14f4a02c571..e331e477253 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_upgrade_test.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_upgrade_test.go @@ -81,7 +81,8 @@ func TestUpgradeHandler(t *testing.T) { go c.Run(ctx) u := NewUpgrade(log, c) - a := fleetapi.ActionUpgrade{Version: "8.3.0", SourceURI: "http://localhost"} + a := fleetapi.ActionUpgrade{Data: fleetapi.ActionUpgradeData{ + Version: "8.3.0", SourceURI: "http://localhost"}} ack := noopacker.New() err := u.Handle(ctx, &a, ack) require.NoError(t, err) @@ -114,7 +115,8 @@ func TestUpgradeHandlerSameVersion(t *testing.T) { go c.Run(ctx) u := NewUpgrade(log, c) - a := fleetapi.ActionUpgrade{Version: "8.3.0", SourceURI: "http://localhost"} + a := fleetapi.ActionUpgrade{Data: fleetapi.ActionUpgradeData{ + Version: "8.3.0", SourceURI: "http://localhost"}} ack := noopacker.New() err1 := u.Handle(ctx, &a, ack) err2 := u.Handle(ctx, &a, ack) @@ -149,8 +151,10 @@ func TestUpgradeHandlerNewVersion(t *testing.T) { go c.Run(ctx) u := NewUpgrade(log, c) - a1 := fleetapi.ActionUpgrade{Version: "8.2.0", SourceURI: "http://localhost"} - a2 := fleetapi.ActionUpgrade{Version: "8.5.0", SourceURI: "http://localhost"} + a1 := fleetapi.ActionUpgrade{Data: fleetapi.ActionUpgradeData{ + Version: "8.2.0", SourceURI: "http://localhost"}} + a2 := fleetapi.ActionUpgrade{Data: fleetapi.ActionUpgradeData{ + Version: "8.5.0", SourceURI: "http://localhost"}} ack := noopacker.New() err1 := u.Handle(ctx, &a1, ack) require.NoError(t, err1) diff --git a/internal/pkg/agent/application/dispatcher/dispatcher.go b/internal/pkg/agent/application/dispatcher/dispatcher.go index aef7bff5cdb..b7b97d7e69d 100644 --- a/internal/pkg/agent/application/dispatcher/dispatcher.go +++ b/internal/pkg/agent/application/dispatcher/dispatcher.go @@ -304,7 +304,7 @@ func (ad *ActionDispatcher) handleExpired( version := "unknown" expiration := "unknown" if upgrade, ok := e.(*fleetapi.ActionUpgrade); ok { - version = upgrade.Version + version = upgrade.Data.Version expiration = upgrade.ActionExpiration } ad.lastUpgradeDetails = details.NewDetails(version, details.StateFailed, e.ID()) @@ -356,7 +356,7 @@ func (ad *ActionDispatcher) reportNextScheduledUpgrade(input []fleetapi.Action, } upgradeDetails := details.NewDetails( - nextUpgrade.Version, + nextUpgrade.Data.Version, details.StateScheduled, nextUpgrade.ID()) startTime, err := nextUpgrade.StartTime() diff --git a/internal/pkg/agent/application/dispatcher/dispatcher_test.go b/internal/pkg/agent/application/dispatcher/dispatcher_test.go index 1e63e73e6e9..176f12f7b35 100644 --- a/internal/pkg/agent/application/dispatcher/dispatcher_test.go +++ b/internal/pkg/agent/application/dispatcher/dispatcher_test.go @@ -675,7 +675,9 @@ func TestReportNextScheduledUpgrade(t *testing.T) { actions: []fleetapi.Action{ &fleetapi.ActionUpgrade{ ActionID: "action1", - Version: "8.12.3", + Data: fleetapi.ActionUpgradeData{ + Version: "8.12.3", + }, }, }, expectedErrLogMsg: "failed to get start time for scheduled upgrade action [id = action1]", @@ -685,7 +687,9 @@ func TestReportNextScheduledUpgrade(t *testing.T) { &fleetapi.ActionUpgrade{ ActionID: "action2", ActionStartTime: later.Format(time.RFC3339), - Version: "8.13.0", + Data: fleetapi.ActionUpgradeData{ + Version: "8.13.0", + }, }, }, expectedDetails: &details.Details{ @@ -702,12 +706,16 @@ func TestReportNextScheduledUpgrade(t *testing.T) { &fleetapi.ActionUpgrade{ ActionID: "action3", ActionStartTime: muchLater.Format(time.RFC3339), - Version: "8.14.1", + Data: fleetapi.ActionUpgradeData{ + Version: "8.14.1", + }, }, &fleetapi.ActionUpgrade{ ActionID: "action4", ActionStartTime: later.Format(time.RFC3339), - Version: "8.13.5", + Data: fleetapi.ActionUpgradeData{ + Version: "8.13.5", + }, }, }, expectedDetails: &details.Details{ @@ -723,8 +731,10 @@ func TestReportNextScheduledUpgrade(t *testing.T) { actions: []fleetapi.Action{ &fleetapi.ActionUpgrade{ ActionID: "action1", - Version: "8.13.2", ActionStartTime: "invalid", + Data: fleetapi.ActionUpgradeData{ + Version: "8.13.2", + }, }, }, expectedErrLogMsg: "failed to get start time for scheduled upgrade action [id = action1]", diff --git a/internal/pkg/agent/application/upgrade/step_mark.go b/internal/pkg/agent/application/upgrade/step_mark.go index 44a869ee2dd..23ae5f59948 100644 --- a/internal/pkg/agent/application/upgrade/step_mark.go +++ b/internal/pkg/agent/application/upgrade/step_mark.go @@ -70,8 +70,8 @@ func convertToMarkerAction(a *fleetapi.ActionUpgrade) *MarkerActionUpgrade { return &MarkerActionUpgrade{ ActionID: a.ActionID, ActionType: a.ActionType, - Version: a.Version, - SourceURI: a.SourceURI, + Version: a.Data.Version, + SourceURI: a.Data.SourceURI, } } @@ -82,8 +82,10 @@ func convertToActionUpgrade(a *MarkerActionUpgrade) *fleetapi.ActionUpgrade { return &fleetapi.ActionUpgrade{ ActionID: a.ActionID, ActionType: a.ActionType, - Version: a.Version, - SourceURI: a.SourceURI, + Data: fleetapi.ActionUpgradeData{ + Version: a.Version, + SourceURI: a.SourceURI, + }, } } diff --git a/internal/pkg/agent/storage/store/action_store.go b/internal/pkg/agent/storage/store/action_store.go index 4fc9df8b485..9f40dd678e3 100644 --- a/internal/pkg/agent/storage/store/action_store.go +++ b/internal/pkg/agent/storage/store/action_store.go @@ -9,7 +9,7 @@ import ( "fmt" "io" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" "github.com/elastic/elastic-agent/internal/pkg/fleetapi" "github.com/elastic/elastic-agent/pkg/core/logger" @@ -19,7 +19,8 @@ import ( // 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 threadsafe. -// ATTN!!!: THE actionStore is deprecated, please use and extend the stateStore instead. The actionStore will be eventually removed. +// The actionStore is deprecated, please use and extend the stateStore instead. The actionStore will be eventually removed. +// Deprecated. type actionStore struct { log *logger.Logger store storeLoad @@ -86,7 +87,7 @@ func (s *actionStore) save() error { if apc, ok := s.action.(*fleetapi.ActionPolicyChange); ok { serialize := actionPolicyChangeSerializer(*apc) - r, err := yamlToReader(&serialize) + r, err := jsonToReader(&serialize) if err != nil { return err } @@ -95,7 +96,7 @@ func (s *actionStore) save() error { } else if aun, ok := s.action.(*fleetapi.ActionUnenroll); ok { serialize := actionUnenrollSerializer(*aun) - r, err := yamlToReader(&serialize) + r, err := jsonToReader(&serialize) if err != nil { return err } @@ -130,26 +131,26 @@ func (s *actionStore) actions() []action { // There are four ways to achieve the same results: // 1. We create a second struct that map the existing field. // 2. We add the serialization in the fleetapi. -// 3. We move the actual action type outside of the actual fleetapi package. +// 3. We move the actual action type outside the actual fleetapi package. // 4. We have two sets of type. // // This could be done in a refactoring. type actionPolicyChangeSerializer struct { - ActionID string `yaml:"action_id"` - ActionType string `yaml:"action_type"` - Policy map[string]interface{} `yaml:"policy"` + ActionID string `json:"id" yaml:"id"` + ActionType string `json:"type" yaml:"type"` + Data fleetapi.ActionPolicyChangeData `json:"data,omitempty" yaml:"data,omitempty"` } // add a guards between the serializer structs and the original struct. -var _ actionPolicyChangeSerializer = actionPolicyChangeSerializer(fleetapi.ActionPolicyChange{}) +var _ = actionPolicyChangeSerializer(fleetapi.ActionPolicyChange{}) // actionUnenrollSerializer is a struct that adds a YAML serialization, type actionUnenrollSerializer struct { - ActionID string `yaml:"action_id"` - ActionType string `yaml:"action_type"` - IsDetected bool `yaml:"is_detected"` - Signed *fleetapi.Signed `yaml:"signed,omitempty"` + ActionID string `json:"action_id"` + ActionType string `json:"action_type"` + IsDetected bool `json:"is_detected"` + Signed *fleetapi.Signed `json:"signed,omitempty"` } // add a guards between the serializer structs and the original struct. -var _ actionUnenrollSerializer = actionUnenrollSerializer(fleetapi.ActionUnenroll{}) +var _ = actionUnenrollSerializer(fleetapi.ActionUnenroll{}) diff --git a/internal/pkg/agent/storage/store/action_store_test.go b/internal/pkg/agent/storage/store/action_store_test.go index 5a5d56b8056..691e4302c16 100644 --- a/internal/pkg/agent/storage/store/action_store_test.go +++ b/internal/pkg/agent/storage/store/action_store_test.go @@ -58,9 +58,8 @@ func TestActionStore(t *testing.T) { ActionPolicyChange := &fleetapi.ActionPolicyChange{ ActionID: "abc123", ActionType: "POLICY_CHANGE", - Policy: map[string]interface{}{ - "hello": "world", - }, + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{"hello": "world"}}, } s := storage.NewDiskStore(file) diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index 1955d479e67..e11837d7f42 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -7,13 +7,12 @@ package store import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" "sync" - "gopkg.in/yaml.v2" - "github.com/elastic/elastic-agent/internal/pkg/agent/storage" "github.com/elastic/elastic-agent/internal/pkg/conv" "github.com/elastic/elastic-agent/internal/pkg/fleetapi" @@ -43,25 +42,32 @@ type StateStore struct { log *logger.Logger store storeLoad dirty bool - state stateT + state state mx sync.RWMutex } -type stateT struct { +type state struct { action action ackToken string - queue []action + // TODO: the queue is for scheduled actions. Set its type accordingly. + queue []action } // actionSerializer is a combined yml serializer for the ActionPolicyChange and ActionUnenroll -// it is used to read the yaml file and assign the action to stateT.action as we must provide the +// it is used to read the yaml file and assign the action to state.action as we must provide the // underlying struct that provides the action interface. +// TODO: get rid of this type type actionSerializer struct { - ID string `yaml:"action_id"` - Type string `yaml:"action_type"` - Policy map[string]interface{} `yaml:"policy,omitempty"` - IsDetected *bool `yaml:"is_detected,omitempty"` + ID string `json:"action_id"` + Type string `json:"action_type"` + Data actionDataSerializer `json:"data,omitempty"` + IsDetected *bool `json:"is_detected,omitempty"` + Signed *fleetapi.Signed `json:"signed,omitempty"` +} + +type actionDataSerializer struct { + Policy map[string]interface{} `json:"policy" yaml:"policy,omitempty"` } // stateSerializer is used to serialize the state to yaml. @@ -69,9 +75,9 @@ type actionSerializer struct { // queue serialization is handled through yaml struct tags or the actions unmarshaller defined in fleetapi // TODO clean up action serialization (have it be part of the fleetapi?) type stateSerializer struct { - Action *actionSerializer `yaml:"action,omitempty"` - AckToken string `yaml:"ack_token,omitempty"` - Queue fleetapi.Actions `yaml:"action_queue,omitempty"` + Action *actionSerializer `json:"action,omitempty"` + AckToken string `json:"ack_token,omitempty"` + Queue fleetapi.Actions `json:"action_queue,omitempty"` } // NewStateStoreWithMigration creates a new state store and migrates the old one. @@ -102,10 +108,10 @@ func NewStateStore(log *logger.Logger, store storeLoad) (*StateStore, error) { } defer reader.Close() - var sr stateSerializer + var serializer stateSerializer - dec := yaml.NewDecoder(reader) - err = dec.Decode(&sr) + dec := json.NewDecoder(reader) + err = dec.Decode(&serializer) if errors.Is(err, io.EOF) { return &StateStore{ log: log, @@ -117,23 +123,29 @@ func NewStateStore(log *logger.Logger, store storeLoad) (*StateStore, error) { return nil, err } - state := stateT{ - ackToken: sr.AckToken, - queue: sr.Queue, + st := state{ + ackToken: serializer.AckToken, + queue: serializer.Queue, } - if sr.Action != nil { - if sr.Action.IsDetected != nil { - state.action = &fleetapi.ActionUnenroll{ - ActionID: sr.Action.ID, - ActionType: sr.Action.Type, - IsDetected: *sr.Action.IsDetected, + if serializer.Action != nil { + // TODO: use ActionType instead + if serializer.Action.IsDetected != nil { + st.action = &fleetapi.ActionUnenroll{ + ActionID: serializer.Action.ID, + ActionType: serializer.Action.Type, + IsDetected: *serializer.Action.IsDetected, + Signed: serializer.Action.Signed, } } else { - state.action = &fleetapi.ActionPolicyChange{ - ActionID: sr.Action.ID, - ActionType: sr.Action.Type, - Policy: conv.YAMLMapToJSONMap(sr.Action.Policy), // Fix Policy, in order to make it consistent with the policy received from the fleet gateway as nested map[string]interface{} + st.action = &fleetapi.ActionPolicyChange{ + ActionID: serializer.Action.ID, + ActionType: serializer.Action.Type, + Data: fleetapi.ActionPolicyChangeData{ + // Fix Policy, in order to make it consistent with the policy + // received from the fleet gateway as nested map[string]interface{} + Policy: conv.YAMLMapToJSONMap(serializer.Action.Data.Policy), + }, } } } @@ -141,7 +153,7 @@ func NewStateStore(log *logger.Logger, store storeLoad) (*StateStore, error) { return &StateStore{ log: log, store: store, - state: state, + state: st, }, nil } @@ -216,6 +228,11 @@ func migrateStateStore( // Add is only taking care of ActionPolicyChange for now and will only keep the last one it receive, // any other type of action will be silently ignored. +// TODO: fix docs: state: +// - the valid actions, +// - it silently discard invalid actions +// - perhaps rename it as it does not add to the queue, but sets the current +// action func (s *StateStore) Add(a action) { s.mx.Lock() defer s.mx.Unlock() @@ -269,16 +286,28 @@ func (s *StateStore) Save() error { } if s.state.action != nil { - if apc, ok := s.state.action.(*fleetapi.ActionPolicyChange); ok { - serialize.Action = &actionSerializer{apc.ActionID, apc.ActionType, apc.Policy, nil} - } else if aun, ok := s.state.action.(*fleetapi.ActionUnenroll); ok { - serialize.Action = &actionSerializer{aun.ActionID, aun.ActionType, nil, &aun.IsDetected} - } else { - return fmt.Errorf("incompatible type, expected ActionPolicyChange and received %T", s.state.action) + switch a := s.state.action.(type) { + case *fleetapi.ActionPolicyChange: + serialize.Action = &actionSerializer{ + ID: a.ActionID, + Type: a.ActionType, + Data: actionDataSerializer{ + Policy: a.Data.Policy, + }} + case *fleetapi.ActionUnenroll: + serialize.Action = &actionSerializer{ + ID: a.ActionID, + Type: a.ActionType, + IsDetected: &a.IsDetected, + Signed: a.Signed, + } + default: + return fmt.Errorf("incompatible type, expected ActionPolicyChange "+ + "or ActionUnenroll but received %T", s.state.action) } } - reader, err := yamlToReader(&serialize) + reader, err := jsonToReader(&serialize) if err != nil { return err } @@ -342,8 +371,8 @@ func (a *StateStoreActionAcker) Commit(ctx context.Context) error { return a.acker.Commit(ctx) } -func yamlToReader(in interface{}) (io.Reader, error) { - data, err := yaml.Marshal(in) +func jsonToReader(in interface{}) (io.Reader, error) { + data, err := json.Marshal(in) if err != nil { return nil, fmt.Errorf("could not marshal to YAML: %w", err) } diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index a31cb9006db..0b5a07c5de3 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -9,6 +9,7 @@ import ( "io" "os" "path/filepath" + "reflect" "runtime" "sync" "testing" @@ -73,9 +74,10 @@ func runTestStateStore(t *testing.T, ackToken string) { ActionPolicyChange := &fleetapi.ActionPolicyChange{ ActionID: "abc123", ActionType: "POLICY_CHANGE", - Policy: map[string]interface{}{ - "hello": "world", - }, + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + }}, } storePath := filepath.Join(t.TempDir(), "state.yml") @@ -111,9 +113,10 @@ func runTestStateStore(t *testing.T, ackToken string) { ActionID: "test", ActionType: fleetapi.ActionTypeUpgrade, ActionStartTime: ts.Format(time.RFC3339), - Version: "1.2.3", - SourceURI: "https://example.com", - }} + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + }}} storePath := filepath.Join(t.TempDir(), "state.yml") s := storage.NewDiskStore(storePath) @@ -146,15 +149,17 @@ func runTestStateStore(t *testing.T, ackToken string) { ActionID: "test", ActionType: fleetapi.ActionTypeUpgrade, ActionStartTime: ts.Format(time.RFC3339), - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, &fleetapi.ActionPolicyChange{ + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }}, &fleetapi.ActionPolicyChange{ ActionID: "abc123", ActionType: "POLICY_CHANGE", - Policy: map[string]interface{}{ - "hello": "world", - }, + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + }}, }} storePath := filepath.Join(t.TempDir(), "state.yml") @@ -269,6 +274,11 @@ func runTestStateStore(t *testing.T, ackToken string) { }) t.Run("migrate", func(t *testing.T) { + // TODO: DO NOT MERGE TO MAIN WITHOUT REMOVING THIS SKIP + t.Skip("this test is broken because the migration haven't been" + + " implemented yet. It'll implemented on another PR as part of" + + "https://github.com/elastic/elastic-agent/issues/3912") + if runtime.GOOS == "darwin" { // the original migrate never actually run, so with this at least // there is coverage for linux and windows. @@ -279,10 +289,12 @@ func runTestStateStore(t *testing.T, ackToken string) { want := &fleetapi.ActionPolicyChange{ ActionID: "abc123", ActionType: "POLICY_CHANGE", - Policy: map[string]interface{}{ - "hello": "world", - "phi": 1.618, - "answer": 42, + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42, + }, }, } @@ -337,6 +349,148 @@ func runTestStateStore(t *testing.T, ackToken string) { "queue should be empty, old action store did not have a queue") }) + t.Run("state store is correctly loaded from disk", func(t *testing.T) { + t.Run("ActionPolicyChange", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.yaml") + want := &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + }, + }, + } + + s := storage.NewDiskStore(storePath) + stateStore, err := NewStateStore(log, s) + require.NoError(t, err, "could not create disk store") + + stateStore.SetAckToken(ackToken) + stateStore.Add(want) + err = stateStore.Save() + require.NoError(t, err, "failed saving state store") + + // to load from disk a new store needs to be created + s = storage.NewDiskStore(storePath) + stateStore, err = NewStateStore(log, s) + require.NoError(t, err, "could not create disk store") + + actions := stateStore.Actions() + require.Len(t, actions, 1, + "should have loaded exactly 1 action") + got, ok := actions[0].(*fleetapi.ActionPolicyChange) + require.True(t, ok, "could not cast action to fleetapi.ActionPolicyChange") + assert.Equal(t, want, got) + + emptyFields := hasEmptyFields(got) + if len(emptyFields) > 0 { + t.Errorf("the following fields of %T are serialized and are empty: %s."+ + " All serialised fields must have a value. Perhaps the action was"+ + " updated but this test was not. Ensure the test covers all"+ + "JSON serialized fields for this action.", + got, emptyFields) + } + }) + + t.Run("ActionUnenroll", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.yaml") + want := &fleetapi.ActionUnenroll{ + ActionID: "abc123", + ActionType: fleetapi.ActionTypeUnenroll, + IsDetected: true, + Signed: &fleetapi.Signed{ + Data: "some data", + Signature: "a signature", + }, + } + + s := storage.NewDiskStore(storePath) + stateStore, err := NewStateStore(log, s) + require.NoError(t, err, "could not create disk store") + + stateStore.SetAckToken(ackToken) + stateStore.Add(want) + err = stateStore.Save() + require.NoError(t, err, "failed saving state store") + + // to load from disk a new store needs to be created + s = storage.NewDiskStore(storePath) + stateStore, err = NewStateStore(log, s) + require.NoError(t, err, "could not create disk store") + + actions := stateStore.Actions() + require.Len(t, actions, 1, + "should have loaded exactly 1 action") + got, ok := actions[0].(*fleetapi.ActionUnenroll) + require.True(t, ok, "could not cast action to fleetapi.ActionUnenroll") + assert.Equal(t, want, got) + + emptyFields := hasEmptyFields(got) + if len(emptyFields) > 0 { + t.Errorf("the following fields of %T are serialized and are empty: %s."+ + " All serialised fields must have a value. Perhaps the action was"+ + " updated but this test was not. Ensure the test covers all"+ + "JSON serialized fields for this action.", + got, emptyFields) + } + }) + + t.Run("action queue", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.yaml") + now := time.Now().UTC().Round(time.Second) + want := &fleetapi.ActionUpgrade{ + ActionID: "test", + ActionType: fleetapi.ActionTypeUpgrade, + ActionStartTime: now.Format(time.RFC3339), + ActionExpiration: now.Add(time.Hour).Format(time.RFC3339), + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: &fleetapi.Signed{ + Data: "some data", + Signature: "a signature", + }, + } + + t.Logf("state store: %q", storePath) + s := storage.NewDiskStore(storePath) + stateStore, err := NewStateStore(log, s) + require.NoError(t, err, "could not create disk store") + + stateStore.SetAckToken(ackToken) + stateStore.SetQueue([]action{want}) + err = stateStore.Save() + require.NoError(t, err, "failed saving state store") + + // to load from disk a new store needs to be created + s = storage.NewDiskStore(storePath) + stateStore, err = NewStateStore(log, s) + require.NoError(t, err, "could not create disk store") + + queue := stateStore.Queue() + require.Len(t, queue, 1, "action queue should have only 1 action") + got := queue[0] + assert.Equal(t, want, got, + "deserialized action is different from what was saved to disk") + _, ok := got.(*fleetapi.ActionUpgrade) + require.True(t, ok, "could not cast action in the queue to upgradeAction") + + emptyFields := hasEmptyFields(got) + if len(emptyFields) > 0 { + t.Errorf("the following fields of %T are serialized and are empty: %s."+ + " All serialised fields must have a value. Perhaps the action was"+ + " updated but this test was not. Ensure the test covers all"+ + "JSON serialized fields for this action.", + got, emptyFields) + } + }) + }) + } type testAcker struct { @@ -372,3 +526,40 @@ func (t *testAcker) Items() []string { defer t.ackedLock.Unlock() return t.acked } + +// hasEmptyFields will check if action has any empty fields. It returns a string +// slice with any empty field, the field value is the zero value for its type. +// If the json tag of the field is "-", the field is ignored. +// If no field is empty, it returns nil. +func hasEmptyFields(action fleetapi.Action) []string { + var actionValue reflect.Value + actionValue = reflect.ValueOf(action) + // dereference if it's a pointer + if actionValue.Kind() == reflect.Pointer { + actionValue = actionValue.Elem() + } + + var failures []string + for i := 0; i < actionValue.NumField(); i++ { + fieldValue := actionValue.Field(i) + actionType := actionValue.Type() + structField := actionType.Field(i) + + fieldName := structField.Name + tag := structField.Tag.Get("json") + + // If the field isn't serialised, ignore it. + if tag == "-" { + continue + } + + got := fieldValue.Interface() + zeroValue := reflect.Zero(fieldValue.Type()).Interface() + + if reflect.DeepEqual(got, zeroValue) { + failures = append(failures, fieldName) + } + } + + return failures +} diff --git a/internal/pkg/config/operations/inspector.go b/internal/pkg/config/operations/inspector.go index 55e78069292..ef4ab0ab32b 100644 --- a/internal/pkg/config/operations/inspector.go +++ b/internal/pkg/config/operations/inspector.go @@ -124,7 +124,7 @@ func loadFleetConfig(ctx context.Context, l *logger.Logger) (map[string]interfac continue } - return cfgChange.Policy, nil + return cfgChange.Data.Policy, nil } return nil, nil } diff --git a/internal/pkg/fleetapi/ack_cmd_test.go b/internal/pkg/fleetapi/ack_cmd_test.go index 00b85d2ac80..27a26823e5f 100644 --- a/internal/pkg/fleetapi/ack_cmd_test.go +++ b/internal/pkg/fleetapi/ack_cmd_test.go @@ -51,9 +51,12 @@ func TestAck(t *testing.T) { action := &ActionPolicyChange{ ActionID: "my-id", ActionType: "POLICY_CHANGE", - Policy: map[string]interface{}{ + + Data: struct { + Policy map[string]interface{} `json:"policy" yaml:"policy,omitempty"` + }{Policy: map[string]interface{}{ "id": "config_id", - }, + }}, } cmd := NewAckCmd(&agentinfo{}, client) diff --git a/internal/pkg/fleetapi/acker/fleet/fleet_acker_test.go b/internal/pkg/fleetapi/acker/fleet/fleet_acker_test.go index fcde240fe34..8dce010ce61 100644 --- a/internal/pkg/fleetapi/acker/fleet/fleet_acker_test.go +++ b/internal/pkg/fleetapi/acker/fleet/fleet_acker_test.go @@ -125,14 +125,18 @@ func TestAcker_Ack(t *testing.T) { &fleetapi.ActionUpgrade{ ActionID: "upgrade-retry", ActionType: fleetapi.ActionTypeUpgrade, - Retry: 1, - Err: errors.New("upgrade failed"), + Data: fleetapi.ActionUpgradeData{ + Retry: 1, + }, + Err: errors.New("upgrade failed"), }, &fleetapi.ActionUpgrade{ ActionID: "upgrade-failed", ActionType: fleetapi.ActionTypeUpgrade, - Retry: -1, - Err: errors.New("upgrade failed"), + Data: fleetapi.ActionUpgradeData{ + Retry: -1, + }, + Err: errors.New("upgrade failed"), }, }, }, @@ -165,7 +169,8 @@ func TestAcker_Ack(t *testing.T) { } err := json.Unmarshal(req.Events[i].Payload, &pl) require.NoError(t, err) - assert.Equal(t, a.Retry, pl.Attempt, "action ID %s failed", a.ActionID) + assert.Equal(t, a.Data.Retry, pl.Attempt, + "action ID %s failed", a.ActionID) // Check retry flag if pl.Attempt > 0 { assert.True(t, pl.Retry) diff --git a/internal/pkg/fleetapi/action.go b/internal/pkg/fleetapi/action.go index df70677d716..a7d4fdfb82f 100644 --- a/internal/pkg/fleetapi/action.go +++ b/internal/pkg/fleetapi/action.go @@ -11,7 +11,6 @@ import ( "time" "github.com/mitchellh/mapstructure" - "gopkg.in/yaml.v2" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" ) @@ -65,45 +64,27 @@ type ScheduledAction interface { type RetryableAction interface { ScheduledAction // RetryAttempt returns the retry-attempt number of the action - // the retry_attempt number is meant to be an interal counter for the elastic-agent and not communicated to fleet-server or ES. + // the retry_attempt number is meant to be an internal counter for the elastic-agent and not communicated to fleet-server or ES. // If RetryAttempt returns > 1, and GetError is not nil the acker should signal that the action is being retried. // If RetryAttempt returns < 1, and GetError is not nil the acker should signal that the action has failed. RetryAttempt() int // SetRetryAttempt sets the retry-attempt number of the action - // the retry_attempt number is meant to be an interal counter for the elastic-agent and not communicated to fleet-server or ES. + // the retry_attempt number is meant to be an internal counter for the elastic-agent and not communicated to fleet-server or ES. SetRetryAttempt(int) // SetStartTime sets the start_time of the action to the specified value. // this is used by the action-retry mechanism. SetStartTime(t time.Time) // GetError returns the error that is associated with the retry. // If it is a retryable action fleet-server should mark it as such. - // Otherwise fleet-server should mark the action as failed. + // Otherwise, fleet-server should mark the action as failed. GetError() error // SetError sets the retryable action error SetError(error) } type Signed struct { - Data string `yaml:"data" json:"data" mapstructure:"data"` - Signature string `yaml:"signature" json:"signature" mapstructure:"signature"` -} - -// FleetAction represents an action from fleet-server. -// should copy the action definition in fleet-server/model/schema.json -type FleetAction struct { - ActionID string `yaml:"action_id" json:"id"` // NOTE schema defines this as action_id, but fleet-server remaps it to id in the json response to agent check-in. - ActionType string `yaml:"type,omitempty" json:"type,omitempty"` - InputType string `yaml:"input_type,omitempty" json:"input_type,omitempty"` - ActionExpiration string `yaml:"expiration,omitempty" json:"expiration,omitempty"` - ActionStartTime string `yaml:"start_time,omitempty" json:"start_time,omitempty"` - Timeout int64 `yaml:"timeout,omitempty" json:"timeout,omitempty"` - Data json.RawMessage `yaml:"data,omitempty" json:"data,omitempty"` - Retry int `json:"retry_attempt,omitempty" yaml:"retry_attempt,omitempty"` // used internally for serialization by elastic-agent. - //Agents []string // disabled, fleet-server uses this to generate each agent's actions - //Timestamp string // disabled, agent does not care when the document was created - //UserID string // disabled, agent does not care - //MinimumExecutionDuration int64 // disabled, used by fleet-server for scheduling - Signed *Signed `yaml:"signed,omitempty" json:"signed,omitempty"` + Data string `json:"data" yaml:"data" mapstructure:"data"` + Signature string `json:"signature" yaml:"signature" mapstructure:"signature"` } func newAckEvent(id, aType string) AckEvent { @@ -121,9 +102,10 @@ func newAckEvent(id, aType string) AckEvent { // NOTE: We only keep the original type and the action id, the payload of the event is dropped, we // do this to make sure we do not leak any unwanted information. type ActionUnknown struct { - originalType string - ActionID string - ActionType string + ActionID string `json:"id" yaml:"id" mapstructure:"id"` + ActionType string `json:"type,omitempty" yaml:"type,omitempty" mapstructure:"type"` + // OriginalType is the original type of the action as returned by the API. + OriginalType string `json:"original_type,omitempty" yaml:"original_type,omitempty" mapstructure:"original_type"` } // Type returns the type of the Action. @@ -143,30 +125,30 @@ func (a *ActionUnknown) String() string { s.WriteString(", type: ") s.WriteString(a.ActionType) s.WriteString(" (original type: ") - s.WriteString(a.OriginalType()) + s.WriteString(a.OriginalType) s.WriteString(")") return s.String() } -// OriginalType returns the original type of the action as returned by the API. -func (a *ActionUnknown) OriginalType() string { - return a.originalType -} - func (a *ActionUnknown) AckEvent() AckEvent { return AckEvent{ EventType: "ACTION_RESULT", // TODO Discuss EventType/SubType needed - by default only ACTION_RESULT was used - what is (or was) the intended purpose of these attributes? Are they documented? Can we change them to better support acking an error or a retry? SubType: "ACKNOWLEDGED", ActionID: a.ActionID, Message: fmt.Sprintf("Action %q of type %q acknowledged.", a.ActionID, a.ActionType), - Error: fmt.Sprintf("Action %q of type %q is unknown to the elastic-agent", a.ActionID, a.originalType), + Error: fmt.Sprintf("Action %q of type %q is unknown to the elastic-agent", a.ActionID, a.OriginalType), } } -// ActionPolicyReassign is a request to apply a new +// ActionPolicyReassign is a request to apply a new policy type ActionPolicyReassign struct { - ActionID string `yaml:"action_id"` - ActionType string `yaml:"type"` + ActionID string `json:"id" yaml:"id"` + ActionType string `json:"type" yaml:"type"` + Data ActionPolicyReassignData `json:"data,omitempty"` +} + +type ActionPolicyReassignData struct { + PolicyID string `json:"policy_id"` } func (a *ActionPolicyReassign) String() string { @@ -194,9 +176,13 @@ func (a *ActionPolicyReassign) AckEvent() AckEvent { // ActionPolicyChange is a request to apply a new type ActionPolicyChange struct { - ActionID string `yaml:"action_id"` - ActionType string `yaml:"type"` - Policy map[string]interface{} `json:"policy" yaml:"policy,omitempty"` + ActionID string `json:"id" yaml:"id"` + ActionType string `json:"type" yaml:"type"` + Data ActionPolicyChangeData `json:"data,omitempty" yaml:"data,omitempty"` +} + +type ActionPolicyChangeData struct { + Policy map[string]interface{} `json:"policy" yaml:"policy,omitempty"` } func (a *ActionPolicyChange) String() string { @@ -224,15 +210,21 @@ func (a *ActionPolicyChange) AckEvent() AckEvent { // ActionUpgrade is a request for agent to upgrade. type ActionUpgrade struct { - ActionID string `yaml:"action_id" mapstructure:"id"` - ActionType string `yaml:"type" mapstructure:"type"` - ActionStartTime string `json:"start_time" yaml:"start_time,omitempty" mapstructure:"-"` // TODO change to time.Time in unmarshal - ActionExpiration string `json:"expiration" yaml:"expiration,omitempty" mapstructure:"-"` - Version string `json:"version" yaml:"version,omitempty" mapstructure:"-"` - SourceURI string `json:"source_uri,omitempty" yaml:"source_uri,omitempty" mapstructure:"-"` - Retry int `json:"retry_attempt,omitempty" yaml:"retry_attempt,omitempty" mapstructure:"-"` - Signed *Signed `json:"signed,omitempty" yaml:"signed,omitempty" mapstructure:"signed,omitempty"` - Err error `json:"-" yaml:"-" mapstructure:"-"` + ActionID string `json:"id" yaml:"id" mapstructure:"id"` + ActionType string `json:"type" yaml:"type" mapstructure:"type"` + ActionStartTime string `json:"start_time" yaml:"start_time,omitempty" mapstructure:"-"` // TODO change to time.Time in unmarshal + ActionExpiration string `json:"expiration" yaml:"expiration,omitempty" mapstructure:"-"` + // does anyone know why those aren't mapped to mapstructure? + Data ActionUpgradeData `json:"data,omitempty" mapstructure:"-"` + Signed *Signed `json:"signed,omitempty" yaml:"signed,omitempty" mapstructure:"signed,omitempty"` + Err error `json:"-" yaml:"-" mapstructure:"-"` +} + +type ActionUpgradeData struct { + Version string `json:"version" yaml:"version,omitempty" mapstructure:"-"` + SourceURI string `json:"source_uri,omitempty" yaml:"source_uri,omitempty" mapstructure:"-"` + // TODO: update fleet open api schema + Retry int `json:"retry_attempt,omitempty" yaml:"retry_attempt,omitempty" mapstructure:"-"` } func (a *ActionUpgrade) String() string { @@ -254,8 +246,8 @@ func (a *ActionUpgrade) AckEvent() AckEvent { Attempt int `json:"retry_attempt,omitempty"` } payload.Retry = true - payload.Attempt = a.Retry - if a.Retry < 1 { // retry is set to -1 if it will not re attempt + payload.Attempt = a.Data.Retry + if a.Data.Retry < 1 { // retry is set to -1 if it will not re attempt payload.Retry = false } p, _ := json.Marshal(payload) @@ -300,12 +292,12 @@ func (a *ActionUpgrade) Expiration() (time.Time, error) { // RetryAttempt will return the retry_attempt of the action func (a *ActionUpgrade) RetryAttempt() int { - return a.Retry + return a.Data.Retry } // SetRetryAttempt sets the retry_attempt of the action func (a *ActionUpgrade) SetRetryAttempt(n int) { - a.Retry = n + a.Data.Retry = n } // GetError returns the error associated with the attempt to run the action. @@ -332,8 +324,8 @@ func (a *ActionUpgrade) MarshalMap() (map[string]interface{}, error) { // ActionUnenroll is a request for agent to unhook from fleet. type ActionUnenroll struct { - ActionID string `yaml:"action_id" mapstructure:"id"` - ActionType string `yaml:"type" mapstructure:"type"` + ActionID string `json:"id" yaml:"id" mapstructure:"id"` + ActionType string `json:"type" yaml:"type" mapstructure:"type"` IsDetected bool `json:"is_detected,omitempty" yaml:"is_detected,omitempty" mapstructure:"-"` Signed *Signed `json:"signed,omitempty" mapstructure:"signed,omitempty"` } @@ -370,9 +362,15 @@ func (a *ActionUnenroll) MarshalMap() (map[string]interface{}, error) { // ActionSettings is a request to change agent settings. type ActionSettings struct { - ActionID string `yaml:"action_id"` - ActionType string `yaml:"type"` - LogLevel string `json:"log_level" yaml:"log_level,omitempty"` + ActionID string `json:"id" yaml:"id"` + ActionType string `json:"type" yaml:"type"` + Data ActionSettingsData `json:"data,omitempty"` +} + +type ActionSettingsData struct { + // LogLevel can only be one of "debug", "info", "warning", "error" + // TODO: add validation + LogLevel string `json:"log_level" yaml:"log_level,omitempty"` } // ID returns the ID of the Action. @@ -392,7 +390,7 @@ func (a *ActionSettings) String() string { s.WriteString(", type: ") s.WriteString(a.ActionType) s.WriteString(", log_level: ") - s.WriteString(a.LogLevel) + s.WriteString(a.Data.LogLevel) return s.String() } @@ -402,9 +400,13 @@ func (a *ActionSettings) AckEvent() AckEvent { // ActionCancel is a request to cancel an action. type ActionCancel struct { - ActionID string `yaml:"action_id"` - ActionType string `yaml:"type"` - TargetID string `json:"target_id" yaml:"target_id,omitempty"` + ActionID string `json:"id" yaml:"id"` + ActionType string `json:"type" yaml:"type"` + Data ActionCancelData `json:"data,omitempty"` +} + +type ActionCancelData struct { + TargetID string `json:"target_id" yaml:"target_id,omitempty"` } // ID returns the ID of the Action. @@ -424,7 +426,7 @@ func (a *ActionCancel) String() string { s.WriteString(", type: ") s.WriteString(a.ActionType) s.WriteString(", target_id: ") - s.WriteString(a.TargetID) + s.WriteString(a.Data.TargetID) return s.String() } @@ -434,7 +436,7 @@ func (a *ActionCancel) AckEvent() AckEvent { // ActionDiagnostics is a request to gather and upload a diagnostics bundle. type ActionDiagnostics struct { - ActionID string `json:"action_id"` + ActionID string `json:"id"` ActionType string `json:"type"` UploadID string `json:"-"` Err error `json:"-"` @@ -538,99 +540,54 @@ type Actions []Action // UnmarshalJSON takes every raw representation of an action and try to decode them. func (a *Actions) UnmarshalJSON(data []byte) error { - var responses []FleetAction - if err := json.Unmarshal(data, &responses); err != nil { + var typeUnmarshaler []struct { + ActionType string `json:"type,omitempty" yaml:"type,omitempty"` + } + + if err := json.Unmarshal(data, &typeUnmarshaler); err != nil { + return errors.New(err, + "fail to decode actions to read their types", + errors.TypeConfig) + } + + rawActions := make([]json.RawMessage, len(typeUnmarshaler)) + if err := json.Unmarshal(data, &rawActions); err != nil { return errors.New(err, "fail to decode actions", errors.TypeConfig) } - actions := make([]Action, 0, len(responses)) - for _, response := range responses { + actions := make([]Action, 0, len(typeUnmarshaler)) + for i, response := range typeUnmarshaler { var action Action + + // keep the case statements alphabetically sorted switch response.ActionType { - case ActionTypePolicyChange: - action = &ActionPolicyChange{ - ActionID: response.ActionID, - ActionType: response.ActionType, - } - if err := json.Unmarshal(response.Data, action); err != nil { - return errors.New(err, - "fail to decode POLICY_CHANGE action", - errors.TypeConfig) - } - case ActionTypePolicyReassign: - action = &ActionPolicyReassign{ - ActionID: response.ActionID, - ActionType: response.ActionType, - } + case ActionTypeCancel: + action = &ActionCancel{} + case ActionTypeDiagnostics: + action = &ActionDiagnostics{} case ActionTypeInputAction: // Only INPUT_ACTION type actions could possibly be signed https://github.com/elastic/elastic-agent/pull/2348 - action = &ActionApp{ - ActionID: response.ActionID, - ActionType: response.ActionType, - InputType: response.InputType, - Timeout: response.Timeout, - Data: response.Data, - Signed: response.Signed, - } + action = &ActionApp{} + case ActionTypePolicyChange: + action = &ActionPolicyChange{} + case ActionTypePolicyReassign: + action = &ActionPolicyReassign{} + case ActionTypeSettings: + action = &ActionSettings{} case ActionTypeUnenroll: - action = &ActionUnenroll{ - ActionID: response.ActionID, - ActionType: response.ActionType, - Signed: response.Signed, - } + action = &ActionUnenroll{} case ActionTypeUpgrade: - action = &ActionUpgrade{ - ActionID: response.ActionID, - ActionType: response.ActionType, - ActionStartTime: response.ActionStartTime, - ActionExpiration: response.ActionExpiration, - Signed: response.Signed, - } - - if err := json.Unmarshal(response.Data, action); err != nil { - return errors.New(err, - "fail to decode UPGRADE_ACTION action", - errors.TypeConfig) - } - case ActionTypeSettings: - action = &ActionSettings{ - ActionID: response.ActionID, - ActionType: response.ActionType, - } - - if err := json.Unmarshal(response.Data, action); err != nil { - return errors.New(err, - "fail to decode SETTINGS_ACTION action", - errors.TypeConfig) - } - case ActionTypeCancel: - action = &ActionCancel{ - ActionID: response.ActionID, - ActionType: response.ActionType, - } - if err := json.Unmarshal(response.Data, action); err != nil { - return errors.New(err, - "fail to decode CANCEL_ACTION action", - errors.TypeConfig) - } - case ActionTypeDiagnostics: - action = &ActionDiagnostics{ - ActionID: response.ActionID, - ActionType: response.ActionType, - } - if err := json.Unmarshal(response.Data, action); err != nil { - return errors.New(err, - "fail to decode REQUEST_DIAGNOSTICS_ACTION action", - errors.TypeConfig) - } + action = &ActionUpgrade{} default: - action = &ActionUnknown{ - ActionID: response.ActionID, - ActionType: ActionTypeUnknown, - originalType: response.ActionType, - } + action = &ActionUnknown{OriginalType: response.ActionType} + } + + if err := json.Unmarshal(rawActions[i], action); err != nil { + return errors.New(err, + fmt.Sprintf("fail to decode %s action", action.Type()), + errors.TypeConfig) } actions = append(actions, action) } @@ -639,101 +596,13 @@ func (a *Actions) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalYAML attempts to decode yaml actions. -func (a *Actions) UnmarshalYAML(unmarshal func(interface{}) error) error { - var nodes []FleetAction - if err := unmarshal(&nodes); err != nil { - return errors.New(err, - "fail to decode action", - errors.TypeConfig) - } - actions := make([]Action, 0, len(nodes)) - for i := range nodes { - var action Action - n := nodes[i] - switch n.ActionType { - case ActionTypePolicyChange: - action = &ActionPolicyChange{ - ActionID: n.ActionID, - ActionType: n.ActionType, - } - if err := yaml.Unmarshal(n.Data, action); err != nil { - return errors.New(err, - "fail to decode POLICY_CHANGE action", - errors.TypeConfig) - } - case ActionTypePolicyReassign: - action = &ActionPolicyReassign{ - ActionID: n.ActionID, - ActionType: n.ActionType, - } - case ActionTypeInputAction: - action = &ActionApp{ - ActionID: n.ActionID, - ActionType: n.ActionType, - InputType: n.InputType, - Timeout: n.Timeout, - Data: n.Data, - Signed: n.Signed, - } - case ActionTypeUnenroll: - action = &ActionUnenroll{ - ActionID: n.ActionID, - ActionType: n.ActionType, - Signed: n.Signed, - } - case ActionTypeUpgrade: - action = &ActionUpgrade{ - ActionID: n.ActionID, - ActionType: n.ActionType, - ActionStartTime: n.ActionStartTime, - ActionExpiration: n.ActionExpiration, - Retry: n.Retry, - } - if err := yaml.Unmarshal(n.Data, &action); err != nil { - return errors.New(err, - "fail to decode UPGRADE_ACTION action", - errors.TypeConfig) - } - case ActionTypeSettings: - action = &ActionSettings{ - ActionID: n.ActionID, - ActionType: n.ActionType, - } - if err := yaml.Unmarshal(n.Data, action); err != nil { - return errors.New(err, - "fail to decode SETTINGS_ACTION action", - errors.TypeConfig) - } - case ActionTypeCancel: - action = &ActionCancel{ - ActionID: n.ActionID, - ActionType: n.ActionType, - } - if err := yaml.Unmarshal(n.Data, action); err != nil { - return errors.New(err, - "fail to decode CANCEL_ACTION action", - errors.TypeConfig) - } - case ActionTypeDiagnostics: - action = &ActionDiagnostics{ - ActionID: n.ActionID, - ActionType: n.ActionType, - } - if err := yaml.Unmarshal(n.Data, action); err != nil { - return errors.New(err, - "fail to decode REQUEST_DIAGNOSTICS_ACTION action", - errors.TypeConfig) - } - default: - action = &ActionUnknown{ - ActionID: n.ActionID, - ActionType: ActionTypeUnknown, - originalType: n.ActionType, - } - } - actions = append(actions, action) - } - *a = actions - return nil +// UnmarshalYAML prevents to decode actions from . +func (a *Actions) UnmarshalYAML(_ func(interface{}) error) error { + // TODO(AndersonQ): we need this to migrate the store from YAML to JSON + return errors.New("Actions cannot be Unmarshalled from YAML") +} + +// MarshalYAML attempts to decode yaml actions. +func (a *Actions) MarshalYAML() (interface{}, error) { + return nil, errors.New("Actions cannot be Marshaled to YAML") } diff --git a/internal/pkg/fleetapi/action_test.go b/internal/pkg/fleetapi/action_test.go index ac83f31852b..00d132edf92 100644 --- a/internal/pkg/fleetapi/action_test.go +++ b/internal/pkg/fleetapi/action_test.go @@ -96,9 +96,9 @@ func TestActionsUnmarshalJSON(t *testing.T) { assert.Equal(t, ActionTypeUpgrade, action.ActionType) assert.Empty(t, action.ActionStartTime) assert.Empty(t, action.ActionExpiration) - assert.Equal(t, "1.2.3", action.Version) - assert.Equal(t, "http://example.com", action.SourceURI) - assert.Equal(t, 0, action.Retry) + assert.Equal(t, "1.2.3", action.Data.Version) + assert.Equal(t, "http://example.com", action.Data.SourceURI) + assert.Equal(t, 0, action.Data.Retry) }) t.Run("ActionUpgrade with start time", func(t *testing.T) { p := []byte(`[{"id":"testid","type":"UPGRADE","start_time":"2022-01-02T12:00:00Z","expiration":"2022-01-02T13:00:00Z","data":{"version":"1.2.3","source_uri":"http://example.com"}}]`) @@ -111,9 +111,9 @@ func TestActionsUnmarshalJSON(t *testing.T) { assert.Equal(t, ActionTypeUpgrade, action.ActionType) assert.Equal(t, "2022-01-02T12:00:00Z", action.ActionStartTime) assert.Equal(t, "2022-01-02T13:00:00Z", action.ActionExpiration) - assert.Equal(t, "1.2.3", action.Version) - assert.Equal(t, "http://example.com", action.SourceURI) - assert.Equal(t, 0, action.Retry) + assert.Equal(t, "1.2.3", action.Data.Version) + assert.Equal(t, "http://example.com", action.Data.SourceURI) + assert.Equal(t, 0, action.Data.Retry) }) t.Run("ActionPolicyChange no start time", func(t *testing.T) { p := []byte(`[{"id":"testid","type":"POLICY_CHANGE","data":{"policy":{"key":"value"}}}]`) @@ -124,7 +124,7 @@ func TestActionsUnmarshalJSON(t *testing.T) { require.True(t, ok, "unable to cast action to specific type") assert.Equal(t, "testid", action.ActionID) assert.Equal(t, ActionTypePolicyChange, action.ActionType) - assert.NotNil(t, action.Policy) + assert.NotNil(t, action.Data.Policy) }) t.Run("ActionPolicyChange with start time", func(t *testing.T) { p := []byte(`[{"id":"testid","type":"POLICY_CHANGE","start_time":"2022-01-02T12:00:00Z","expiration":"2022-01-02T13:00:00Z","data":{"policy":{"key":"value"}}}]`) @@ -135,7 +135,7 @@ func TestActionsUnmarshalJSON(t *testing.T) { require.True(t, ok, "unable to cast action to specific type") assert.Equal(t, "testid", action.ActionID) assert.Equal(t, ActionTypePolicyChange, action.ActionType) - assert.NotNil(t, action.Policy) + assert.NotNil(t, action.Data.Policy) }) t.Run("ActionUpgrade with retry_attempt", func(t *testing.T) { p := []byte(`[{"id":"testid","type":"UPGRADE","data":{"version":"1.2.3","source_uri":"http://example.com","retry_attempt":1}}]`) @@ -148,9 +148,9 @@ func TestActionsUnmarshalJSON(t *testing.T) { assert.Equal(t, ActionTypeUpgrade, action.ActionType) assert.Empty(t, action.ActionStartTime) assert.Empty(t, action.ActionExpiration) - assert.Equal(t, "1.2.3", action.Version) - assert.Equal(t, "http://example.com", action.SourceURI) - assert.Equal(t, 1, action.Retry) + assert.Equal(t, "1.2.3", action.Data.Version) + assert.Equal(t, "http://example.com", action.Data.SourceURI) + assert.Equal(t, 1, action.Data.Retry) }) } diff --git a/internal/pkg/fleetapi/checkin_cmd_test.go b/internal/pkg/fleetapi/checkin_cmd_test.go index 46a0b4db4d1..568da8601cc 100644 --- a/internal/pkg/fleetapi/checkin_cmd_test.go +++ b/internal/pkg/fleetapi/checkin_cmd_test.go @@ -175,7 +175,7 @@ func TestCheckin(t *testing.T) { // UnknownAction require.Equal(t, "id2", r.Actions[1].ID()) require.Equal(t, "UNKNOWN", r.Actions[1].Type()) - require.Equal(t, "WHAT_TO_DO_WITH_IT", r.Actions[1].(*ActionUnknown).OriginalType()) + require.Equal(t, "WHAT_TO_DO_WITH_IT", r.Actions[1].(*ActionUnknown).OriginalType) }, )) From a7b7bcae164972641a618b7152bf9285d28620b3 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 7 Mar 2024 10:59:03 +0100 Subject: [PATCH 03/39] refactor state store (#4253) It modifies the state store API to match the current needs. --- .../handlers/handler_action_unenroll.go | 6 +- .../gateway/fleet/fleet_gateway.go | 2 - .../pkg/agent/application/managed_mode.go | 15 +- internal/pkg/agent/cmd/run.go | 4 +- .../pkg/agent/storage/store/action_store.go | 14 +- .../pkg/agent/storage/store/state_store.go | 368 +++++++++--------- .../agent/storage/store/state_store_test.go | 148 ++++--- internal/pkg/config/operations/inspector.go | 12 +- internal/pkg/fleetapi/action.go | 64 +-- internal/pkg/queue/actionqueue.go | 20 +- internal/pkg/queue/actionqueue_test.go | 10 +- 11 files changed, 325 insertions(+), 338 deletions(-) diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_unenroll.go b/internal/pkg/agent/application/actions/handlers/handler_action_unenroll.go index 0fb9acb8da7..4b4c9c4dfdb 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_unenroll.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_unenroll.go @@ -22,11 +22,11 @@ const ( ) type stateStore interface { - Add(fleetapi.Action) + SetAction(fleetapi.Action) AckToken() string SetAckToken(ackToken string) Save() error - Actions() []fleetapi.Action + Action() fleetapi.Action } // Unenroll results in running agent entering idle state, non managed non standalone. @@ -94,7 +94,7 @@ func (h *Unenroll) Handle(ctx context.Context, a fleetapi.Action, acker acker.Ac if h.stateStore != nil { // backup action for future start to avoid starting fleet gateway loop - h.stateStore.Add(a) + h.stateStore.SetAction(a) if err := h.stateStore.Save(); err != nil { h.log.Warnf("Failed to update state store: %v", err) } diff --git a/internal/pkg/agent/application/gateway/fleet/fleet_gateway.go b/internal/pkg/agent/application/gateway/fleet/fleet_gateway.go index 109ece58be9..2421f0e7792 100644 --- a/internal/pkg/agent/application/gateway/fleet/fleet_gateway.go +++ b/internal/pkg/agent/application/gateway/fleet/fleet_gateway.go @@ -58,11 +58,9 @@ type agentInfo interface { } type stateStore interface { - Add(fleetapi.Action) AckToken() string SetAckToken(ackToken string) Save() error - Actions() []fleetapi.Action } type FleetGateway struct { diff --git a/internal/pkg/agent/application/managed_mode.go b/internal/pkg/agent/application/managed_mode.go index 7f8d22a16ca..f940641c0d1 100644 --- a/internal/pkg/agent/application/managed_mode.go +++ b/internal/pkg/agent/application/managed_mode.go @@ -157,14 +157,14 @@ func (m *managedConfigManager) Run(ctx context.Context) error { close(retrierRun) }() - actions := m.stateStore.Actions() + action := m.stateStore.Action() stateRestored := false - if len(actions) > 0 && !m.wasUnenrolled() { + if action != nil && !m.wasUnenrolled() { // TODO(ph) We will need an improvement on fleet, if there is an error while dispatching a // persisted action on disk we should be able to ask Fleet to get the latest configuration. // But at the moment this is not possible because the policy change was acked. m.log.Info("restoring current policy from disk") - m.dispatcher.Dispatch(ctx, m.coord.SetUpgradeDetails, actionAcker, actions...) + m.dispatcher.Dispatch(ctx, m.coord.SetUpgradeDetails, actionAcker, action) stateRestored = true } @@ -268,13 +268,8 @@ func (m *managedConfigManager) Watch() <-chan coordinator.ConfigChange { } func (m *managedConfigManager) wasUnenrolled() bool { - actions := m.stateStore.Actions() - for _, a := range actions { - if a.Type() == "UNENROLL" { - return true - } - } - return false + return m.stateStore.Action() != nil && + m.stateStore.Action().Type() == fleetapi.ActionTypeUnenroll } func (m *managedConfigManager) initFleetServer(ctx context.Context, cfg *configuration.FleetServerConfig) error { diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index ae99ba108a9..00ebe186d6e 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -200,7 +200,9 @@ func runElasticAgent(ctx context.Context, cancel context.CancelFunc, override cf } // the encrypted state does not exist but the unencrypted file does - err = migration.MigrateToEncryptedConfig(ctx, l, paths.AgentStateStoreYmlFile(), paths.AgentStateStoreFile()) + err = migration.MigrateToEncryptedConfig(ctx, l, + paths.AgentStateStoreYmlFile(), + paths.AgentStateStoreFile()) if err != nil { return errors.New(err, "error migrating agent state") } diff --git a/internal/pkg/agent/storage/store/action_store.go b/internal/pkg/agent/storage/store/action_store.go index 9f40dd678e3..dd7f839d846 100644 --- a/internal/pkg/agent/storage/store/action_store.go +++ b/internal/pkg/agent/storage/store/action_store.go @@ -23,13 +23,13 @@ import ( // Deprecated. type actionStore struct { log *logger.Logger - store storeLoad + store saveLoader dirty bool - action action + action fleetapi.Action } // newActionStore creates a new action store. -func newActionStore(log *logger.Logger, store storeLoad) (*actionStore, error) { +func newActionStore(log *logger.Logger, store saveLoader) (*actionStore, 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() @@ -64,7 +64,7 @@ func newActionStore(log *logger.Logger, store storeLoad) (*actionStore, error) { // add is only taking care of ActionPolicyChange for now and will only keep the last one it receive, // any other type of action will be silently ignored. -func (s *actionStore) add(a action) { +func (s *actionStore) add(a fleetapi.Action) { switch v := a.(type) { case *fleetapi.ActionPolicyChange, *fleetapi.ActionUnenroll: // Only persist the action if the action is different. @@ -117,12 +117,12 @@ func (s *actionStore) save() error { // actions returns a slice of action to execute in order, currently only a action policy change is // persisted. -func (s *actionStore) actions() []action { +func (s *actionStore) actions() fleetapi.Actions { if s.action == nil { - return []action{} + return fleetapi.Actions{} } - return []action{s.action} + return fleetapi.Actions{s.action} } // actionPolicyChangeSerializer is a struct that adds a YAML serialization, I don't think serialization diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index e11837d7f42..e3feb23103d 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -14,23 +14,24 @@ import ( "sync" "github.com/elastic/elastic-agent/internal/pkg/agent/storage" - "github.com/elastic/elastic-agent/internal/pkg/conv" "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" ) -type store interface { +// 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 storeLoad interface { - store +type saveLoader interface { + saver Load() (io.ReadCloser, error) } -type action = fleetapi.Action - // 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: @@ -40,7 +41,7 @@ type action = fleetapi.Action // Fleet. The store is not thread safe. type StateStore struct { log *logger.Logger - store storeLoad + store saveLoader dirty bool state state @@ -48,43 +49,75 @@ type StateStore struct { } type state struct { - action action - ackToken string - // TODO: the queue is for scheduled actions. Set its type accordingly. - queue []action + Version string `json:"version"` + ActionSerializer actionSerializer `json:"action,omitempty"` + AckToken string `json:"ack_token,omitempty"` + Queue actionQueue `json:"action_queue,omitempty"` } -// actionSerializer is a combined yml serializer for the ActionPolicyChange and ActionUnenroll -// it is used to read the yaml file and assign the action to state.action as we must provide the -// underlying struct that provides the action interface. -// TODO: get rid of this type +// actionSerializer is JSON Marshaler/Unmarshaler for fleetapi.Action. type actionSerializer struct { - ID string `json:"action_id"` - Type string `json:"action_type"` - Data actionDataSerializer `json:"data,omitempty"` - IsDetected *bool `json:"is_detected,omitempty"` - Signed *fleetapi.Signed `json:"signed,omitempty"` + json.Marshaler + json.Unmarshaler + + Action fleetapi.Action } -type actionDataSerializer struct { - Policy map[string]interface{} `json:"policy" yaml:"policy,omitempty"` +// 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 + +func (as *actionSerializer) MarshalJSON() ([]byte, error) { + return json.Marshal(as.Action) } -// stateSerializer is used to serialize the state to yaml. -// action serialization is handled through the actionSerializer struct -// queue serialization is handled through yaml struct tags or the actions unmarshaller defined in fleetapi -// TODO clean up action serialization (have it be part of the fleetapi?) -type stateSerializer struct { - Action *actionSerializer `json:"action,omitempty"` - AckToken string `json:"ack_token,omitempty"` - Queue fleetapi.Actions `json:"action_queue,omitempty"` +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 } // NewStateStoreWithMigration creates a new state store and migrates the old one. func NewStateStoreWithMigration(ctx context.Context, log *logger.Logger, actionStorePath, stateStorePath string) (*StateStore, error) { stateDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath) - err := migrateStateStore(log, actionStorePath, stateDiskStore) + err := migrateActionStoreToStateStore(log, actionStorePath, stateDiskStore) if err != nil { return nil, err } @@ -98,7 +131,7 @@ func NewStateStoreActionAcker(acker acker.Acker, store *StateStore) *StateStoreA } // NewStateStore creates a new state store. -func NewStateStore(log *logger.Logger, store storeLoad) (*StateStore, error) { +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() @@ -108,46 +141,24 @@ func NewStateStore(log *logger.Logger, store storeLoad) (*StateStore, error) { } defer reader.Close() - var serializer stateSerializer - + st := state{} dec := json.NewDecoder(reader) - err = dec.Decode(&serializer) + err = dec.Decode(&st) if errors.Is(err, io.EOF) { return &StateStore{ log: log, store: store, + state: state{Version: Version}, }, nil } - if err != nil { - return nil, err + return nil, fmt.Errorf("could not JSON unmarshal state store: %w", err) } - st := state{ - ackToken: serializer.AckToken, - queue: serializer.Queue, - } - - if serializer.Action != nil { - // TODO: use ActionType instead - if serializer.Action.IsDetected != nil { - st.action = &fleetapi.ActionUnenroll{ - ActionID: serializer.Action.ID, - ActionType: serializer.Action.Type, - IsDetected: *serializer.Action.IsDetected, - Signed: serializer.Action.Signed, - } - } else { - st.action = &fleetapi.ActionPolicyChange{ - ActionID: serializer.Action.ID, - ActionType: serializer.Action.Type, - Data: fleetapi.ActionPolicyChangeData{ - // Fix Policy, in order to make it consistent with the policy - // received from the fleet gateway as nested map[string]interface{} - Policy: conv.YAMLMapToJSONMap(serializer.Action.Data.Policy), - }, - } - } + if st.Version != Version { + return nil, fmt.Errorf( + "invalid state store version, got %q isntead of %s", + st.Version, Version) } return &StateStore{ @@ -157,94 +168,21 @@ func NewStateStore(log *logger.Logger, store storeLoad) (*StateStore, error) { }, nil } -func migrateStateStore( - log *logger.Logger, - actionStorePath string, - stateDiskStore storage.Storage) (err error) { - - log = log.Named("state_migration") - actionDiskStore := storage.NewDiskStore(actionStorePath) - - stateStoreExits, err := stateDiskStore.Exists() - if err != nil { - log.Errorf("failed to check if state store exists: %v", err) - return err - } - - // do not migrate if the state store already exists - if stateStoreExits { - log.Debugf("state store already exists") - return nil - } - - actionStoreExits, err := actionDiskStore.Exists() - if err != nil { - log.Errorf("failed to check if action store %s exists: %v", actionStorePath, err) - return err - } - - // delete the actions store file upon successful migration - defer func() { - if err == nil && actionStoreExits { - err = actionDiskStore.Delete() - if err != nil { - log.Errorf("failed to delete action store %s exists: %v", actionStorePath, err) - } - } - }() - - // nothing to migrate if the action store doesn't exists - if !actionStoreExits { - log.Debugf("action store %s doesn't exists, nothing to migrate", actionStorePath) - return nil - } - - actionStore, err := newActionStore(log, actionDiskStore) - if err != nil { - log.Errorf("failed to create action store %s: %v", actionStorePath, err) - return err - } - - // no actions stored nothing to migrate - if len(actionStore.actions()) == 0 { - log.Debugf("no actions stored in the action store %s, nothing to migrate", actionStorePath) - return nil - } - - stateStore, err := NewStateStore(log, stateDiskStore) - if err != nil { - return err - } - - // set actions from the action store to the state store - stateStore.Add(actionStore.actions()[0]) - - err = stateStore.Save() - if err != nil { - log.Debugf("failed to save agent state store, err: %v", err) - } - return err -} - -// Add is only taking care of ActionPolicyChange for now and will only keep the last one it receive, -// any other type of action will be silently ignored. -// TODO: fix docs: state: -// - the valid actions, -// - it silently discard invalid actions -// - perhaps rename it as it does not add to the queue, but sets the current -// action -func (s *StateStore) Add(a action) { +// 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.action != nil && s.state.action.ID() == v.ID() { + if s.state.ActionSerializer.Action != nil && + s.state.ActionSerializer.Action.ID() == v.ID() { return } s.dirty = true - s.state.action = a + s.state.ActionSerializer.Action = a } } @@ -253,20 +191,21 @@ func (s *StateStore) SetAckToken(ackToken string) { s.mx.Lock() defer s.mx.Unlock() - if s.state.ackToken == ackToken { + if s.state.AckToken == ackToken { return } s.dirty = true - s.state.ackToken = ackToken + s.state.AckToken = ackToken } // SetQueue sets the action_queue to agent state -func (s *StateStore) SetQueue(q []action) { +// TODO: receive only scheduled actions. It might break something. Needs to +// investigate it better. +func (s *StateStore) SetQueue(q []fleetapi.ScheduledAction) { s.mx.Lock() defer s.mx.Unlock() - s.state.queue = q + s.state.Queue = q s.dirty = true - } // Save saves the actions into a state store. @@ -280,34 +219,18 @@ func (s *StateStore) Save() error { } var reader io.Reader - serialize := stateSerializer{ - AckToken: s.state.ackToken, - Queue: s.state.queue, - } - if s.state.action != nil { - switch a := s.state.action.(type) { - case *fleetapi.ActionPolicyChange: - serialize.Action = &actionSerializer{ - ID: a.ActionID, - Type: a.ActionType, - Data: actionDataSerializer{ - Policy: a.Data.Policy, - }} - case *fleetapi.ActionUnenroll: - serialize.Action = &actionSerializer{ - ID: a.ActionID, - Type: a.ActionType, - IsDetected: &a.IsDetected, - Signed: a.Signed, - } - default: - return fmt.Errorf("incompatible type, expected ActionPolicyChange "+ - "or ActionUnenroll but received %T", s.state.action) - } + 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(&serialize) + reader, err := jsonToReader(&s.state) if err != nil { return err } @@ -320,49 +243,49 @@ func (s *StateStore) Save() error { } // Queue returns a copy of the queue -func (s *StateStore) Queue() []action { +func (s *StateStore) Queue() []fleetapi.ScheduledAction { s.mx.RLock() defer s.mx.RUnlock() - q := make([]action, len(s.state.queue)) - copy(q, s.state.queue) + q := make([]fleetapi.ScheduledAction, len(s.state.Queue)) + copy(q, s.state.Queue) return q } -// Actions returns a slice of action to execute in order, currently only a action policy change is -// persisted. -func (s *StateStore) Actions() []action { +// 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.action == nil { - return []action{} + if s.state.ActionSerializer.Action == nil { + return nil } - return []action{s.state.action} + 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 + return s.state.AckToken } -// StateStoreActionAcker wraps an existing acker and will send any acked event to the action store, -// its up to the action store to decide if we need to persist the event for future replay or just -// discard the event. +// 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 action using underlying acker. -// After action is acked it is stored to backing store. +// 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.Add(action) + a.store.SetAction(action) return a.store.Save() } @@ -371,10 +294,79 @@ func (a *StateStoreActionAcker) Commit(ctx context.Context) error { return a.acker.Commit(ctx) } +func migrateActionStoreToStateStore( + log *logger.Logger, + actionStorePath string, + stateDiskStore storage.Storage) (err error) { + + log = log.Named("state_migration") + actionDiskStore := storage.NewDiskStore(actionStorePath) + + stateStoreExits, err := stateDiskStore.Exists() + if err != nil { + log.Errorf("failed to check if state store exists: %v", err) + return err + } + + // do not migrate if the state store already exists + if stateStoreExits { + log.Debugf("state store already exists") + return nil + } + + actionStoreExits, err := actionDiskStore.Exists() + if err != nil { + log.Errorf("failed to check if action store %s exists: %v", actionStorePath, err) + return err + } + + // delete the actions store file upon successful migration + defer func() { + if err == nil && actionStoreExits { + err = actionDiskStore.Delete() + if err != nil { + log.Errorf("failed to delete action store %s exists: %v", actionStorePath, err) + } + } + }() + + // nothing to migrate if the action store doesn't exists + if !actionStoreExits { + log.Debugf("action store %s doesn't exists, nothing to migrate", actionStorePath) + return nil + } + + actionStore, err := newActionStore(log, actionDiskStore) + if err != nil { + log.Errorf("failed to create action store %s: %v", actionStorePath, err) + return err + } + + // no actions stored nothing to migrate + if len(actionStore.actions()) == 0 { + log.Debugf("no actions stored in the action store %s, nothing to migrate", actionStorePath) + return nil + } + + stateStore, err := NewStateStore(log, stateDiskStore) + if err != nil { + return err + } + + // set actions from the action store to the state store + stateStore.SetAction(actionStore.actions()[0]) + + err = stateStore.Save() + if err != nil { + log.Debugf("failed to save agent state store, err: %v", err) + } + return err +} + func jsonToReader(in interface{}) (io.Reader, error) { data, err := json.Marshal(in) if err != nil { - return nil, fmt.Errorf("could not marshal to YAML: %w", err) + return nil, fmt.Errorf("could not marshal to JSON: %w", err) } return bytes.NewReader(data), nil } diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 0b5a07c5de3..2ffd0746af9 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -46,7 +46,7 @@ func runTestStateStore(t *testing.T, ackToken string) { s := storage.NewDiskStore(storePath) store, err := NewStateStore(log, s) require.NoError(t, err) - require.Empty(t, store.Actions()) + require.Empty(t, store.Action()) require.Empty(t, store.Queue()) }) @@ -60,12 +60,12 @@ func runTestStateStore(t *testing.T, ackToken string) { store, err := NewStateStore(log, s) require.NoError(t, err) - require.Equal(t, 0, len(store.Actions())) - store.Add(actionPolicyChange) + require.Nil(t, store.Action()) + store.SetAction(actionPolicyChange) store.SetAckToken(ackToken) err = store.Save() require.NoError(t, err) - require.Empty(t, store.Actions()) + require.Empty(t, store.Action()) require.Empty(t, store.Queue()) require.Equal(t, ackToken, store.AckToken()) }) @@ -85,13 +85,13 @@ func runTestStateStore(t *testing.T, ackToken string) { store, err := NewStateStore(log, s) require.NoError(t, err) - require.Empty(t, store.Actions()) + require.Empty(t, store.Action()) require.Empty(t, store.Queue()) - store.Add(ActionPolicyChange) + store.SetAction(ActionPolicyChange) store.SetAckToken(ackToken) err = store.Save() require.NoError(t, err) - require.Len(t, store.Actions(), 1) + require.NotNil(t, store.Action(), "store should have an action stored") require.Empty(t, store.Queue()) require.Equal(t, ackToken, store.AckToken()) @@ -99,17 +99,17 @@ func runTestStateStore(t *testing.T, ackToken string) { store1, err := NewStateStore(log, s) require.NoError(t, err) - actions := store1.Actions() - require.Len(t, actions, 1) + action := store1.Action() + require.NotNil(t, action, "store should have an action stored") require.Empty(t, store1.Queue()) - require.Equal(t, ActionPolicyChange, actions[0]) + require.Equal(t, ActionPolicyChange, action) require.Equal(t, ackToken, store.AckToken()) }) t.Run("can save a queue with one upgrade action", func(t *testing.T) { ts := time.Now().UTC().Round(time.Second) - queue := []action{&fleetapi.ActionUpgrade{ + queue := []fleetapi.ScheduledAction{&fleetapi.ActionUpgrade{ ActionID: "test", ActionType: fleetapi.ActionTypeUpgrade, ActionStartTime: ts.Format(time.RFC3339), @@ -123,29 +123,29 @@ func runTestStateStore(t *testing.T, ackToken string) { store, err := NewStateStore(log, s) require.NoError(t, err) - require.Empty(t, store.Actions()) + require.Empty(t, store.Action()) store.SetQueue(queue) err = store.Save() require.NoError(t, err) - require.Empty(t, store.Actions()) + require.Empty(t, store.Action()) require.Len(t, store.Queue(), 1) s = storage.NewDiskStore(storePath) - store1, err := NewStateStore(log, s) - require.NoError(t, err) - require.Empty(t, store1.Actions()) - require.Len(t, store1.Queue(), 1) - require.Equal(t, "test", store1.Queue()[0].ID()) - scheduledAction, ok := store1.Queue()[0].(fleetapi.ScheduledAction) - require.True(t, ok, "expected to be able to cast Action as ScheduledAction") - start, err := scheduledAction.StartTime() + store, err = NewStateStore(log, s) require.NoError(t, err) - require.Equal(t, ts, start) + + assert.Nil(t, store.Action()) + assert.Len(t, store.Queue(), 1) + assert.Equal(t, "test", store.Queue()[0].ID()) + + start, err := store.Queue()[0].StartTime() + assert.NoError(t, err) + assert.Equal(t, ts, start) }) t.Run("can save a queue with two actions", func(t *testing.T) { ts := time.Now().UTC().Round(time.Second) - queue := []action{&fleetapi.ActionUpgrade{ + queue := []fleetapi.ScheduledAction{&fleetapi.ActionUpgrade{ ActionID: "test", ActionType: fleetapi.ActionTypeUpgrade, ActionStartTime: ts.Format(time.RFC3339), @@ -153,50 +153,49 @@ func runTestStateStore(t *testing.T, ackToken string) { Version: "1.2.3", SourceURI: "https://example.com", Retry: 1, - }}, &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ - "hello": "world", - }}, - }} + }}, + // only the latest upgrade action is kept, however it's not the store + // which handled that. Besides upgrade actions are the only + // ScheduledAction right now, so it'll use 2 of them for this test. + &fleetapi.ActionUpgrade{ + ActionID: "test2", + ActionType: fleetapi.ActionTypeUpgrade, + ActionStartTime: ts.Format(time.RFC3339), + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.4", + SourceURI: "https://example.com", + Retry: 1, + }}} storePath := filepath.Join(t.TempDir(), "state.yml") s := storage.NewDiskStore(storePath) store, err := NewStateStore(log, s) require.NoError(t, err) - require.Empty(t, store.Actions()) + require.Empty(t, store.Action()) store.SetQueue(queue) err = store.Save() require.NoError(t, err) - require.Empty(t, store.Actions()) + require.Empty(t, store.Action()) require.Len(t, store.Queue(), 2) + // Load state store from disk s = storage.NewDiskStore(storePath) - store1, err := NewStateStore(log, s) - require.NoError(t, err) - require.Empty(t, store1.Actions()) - require.Len(t, store1.Queue(), 2) + store, err = NewStateStore(log, s) + require.NoError(t, err, "could not load store from disk") - require.Equal(t, "test", store1.Queue()[0].ID()) - scheduledAction, ok := store1.Queue()[0].(fleetapi.ScheduledAction) - require.True(t, ok, "expected to be able to cast Action as ScheduledAction") - start, err := scheduledAction.StartTime() - require.NoError(t, err) - require.Equal(t, ts, start) - retryableAction, ok := store1.Queue()[0].(fleetapi.RetryableAction) - require.True(t, ok, "expected to be able to cast Action as RetryableAction") - require.Equal(t, 1, retryableAction.RetryAttempt()) - - require.Equal(t, "abc123", store1.Queue()[1].ID()) - _, ok = store1.Queue()[1].(fleetapi.ScheduledAction) - require.False(t, ok, "expected cast to ScheduledAction to fail") + got := store.Queue() + for i, want := range queue { + upgradeAction, ok := got[i].(*fleetapi.ActionUpgrade) + assert.True(t, ok, + "expected to be able to cast Action as ActionUpgrade") + + assert.Equal(t, want, upgradeAction, "saved action is different from expected") + } }) t.Run("can save to disk unenroll action type", func(t *testing.T) { - action := &fleetapi.ActionUnenroll{ + want := &fleetapi.ActionUnenroll{ ActionID: "abc123", ActionType: "UNENROLL", } @@ -206,13 +205,13 @@ func runTestStateStore(t *testing.T, ackToken string) { store, err := NewStateStore(log, s) require.NoError(t, err) - require.Empty(t, store.Actions()) + require.Empty(t, store.Action()) require.Empty(t, store.Queue()) - store.Add(action) + store.SetAction(want) store.SetAckToken(ackToken) err = store.Save() require.NoError(t, err) - require.Len(t, store.Actions(), 1) + require.NotNil(t, store.Action(), "store should have an action stored") require.Empty(t, store.Queue()) require.Equal(t, ackToken, store.AckToken()) @@ -220,10 +219,10 @@ func runTestStateStore(t *testing.T, ackToken string) { store1, err := NewStateStore(log, s) require.NoError(t, err) - actions := store1.Actions() - require.Len(t, actions, 1) + got := store1.Action() + require.NotNil(t, got, "store should have an action stored") require.Empty(t, store1.Queue()) - require.Equal(t, action, actions[0]) + require.Equal(t, want, got) require.Equal(t, ackToken, store.AckToken()) }) @@ -239,10 +238,10 @@ func runTestStateStore(t *testing.T, ackToken string) { store.SetAckToken(ackToken) acker := NewStateStoreActionAcker(&testAcker{}, store) - require.Empty(t, store.Actions()) + require.Empty(t, store.Action()) require.NoError(t, acker.Ack(context.Background(), ActionPolicyChange)) - require.Len(t, store.Actions(), 1) + require.NotNil(t, store.Action(), "store should have an action stored") require.Empty(t, store.Queue()) require.Equal(t, ackToken, store.AckToken()) }) @@ -260,7 +259,7 @@ func runTestStateStore(t *testing.T, ackToken string) { newStateStorePath := filepath.Join(tempDir, "state_store.yml") newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath) - err := migrateStateStore(log, oldActionStorePath, newStateStore) + err := migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) require.NoError(t, err, "migration action store -> state store failed") // to load from disk a new store needs to be created, it loads the file @@ -268,7 +267,7 @@ func runTestStateStore(t *testing.T, ackToken string) { stateStore, err := NewStateStore(log, storage.NewDiskStore(newStateStorePath)) require.NoError(t, err) stateStore.SetAckToken(ackToken) - require.Empty(t, stateStore.Actions()) + require.Empty(t, stateStore.Action()) require.Equal(t, ackToken, stateStore.AckToken()) require.Empty(t, stateStore.Queue()) }) @@ -329,7 +328,7 @@ func runTestStateStore(t *testing.T, ackToken string) { newStateStorePath := filepath.Join(tempDir, "state_store.yaml") newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath, storage.WithVaultPath(vaultPath)) - err = migrateStateStore(log, oldActionStorePath, newStateStore) + err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) require.NoError(t, err, "migration action store -> state store failed") // to load from disk a new store needs to be created, it loads the file @@ -339,9 +338,8 @@ func runTestStateStore(t *testing.T, ackToken string) { stateStore, err := NewStateStore(log, newStateStore) require.NoError(t, err, "could not create state store") - actions := stateStore.Actions() - require.Len(t, actions, 1, "state store should load exactly 1 action") - got := actions[0] + got := stateStore.Action() + require.NotNil(t, got, "should have loaded an action") assert.Equalf(t, want, got, "loaded action differs from action on the old action store") @@ -369,7 +367,7 @@ func runTestStateStore(t *testing.T, ackToken string) { require.NoError(t, err, "could not create disk store") stateStore.SetAckToken(ackToken) - stateStore.Add(want) + stateStore.SetAction(want) err = stateStore.Save() require.NoError(t, err, "failed saving state store") @@ -378,10 +376,9 @@ func runTestStateStore(t *testing.T, ackToken string) { stateStore, err = NewStateStore(log, s) require.NoError(t, err, "could not create disk store") - actions := stateStore.Actions() - require.Len(t, actions, 1, - "should have loaded exactly 1 action") - got, ok := actions[0].(*fleetapi.ActionPolicyChange) + action := stateStore.Action() + require.NotNil(t, action, "should have loaded an action") + got, ok := action.(*fleetapi.ActionPolicyChange) require.True(t, ok, "could not cast action to fleetapi.ActionPolicyChange") assert.Equal(t, want, got) @@ -412,7 +409,7 @@ func runTestStateStore(t *testing.T, ackToken string) { require.NoError(t, err, "could not create disk store") stateStore.SetAckToken(ackToken) - stateStore.Add(want) + stateStore.SetAction(want) err = stateStore.Save() require.NoError(t, err, "failed saving state store") @@ -421,10 +418,9 @@ func runTestStateStore(t *testing.T, ackToken string) { stateStore, err = NewStateStore(log, s) require.NoError(t, err, "could not create disk store") - actions := stateStore.Actions() - require.Len(t, actions, 1, - "should have loaded exactly 1 action") - got, ok := actions[0].(*fleetapi.ActionUnenroll) + action := stateStore.Action() + require.NotNil(t, action, "should have loaded an action") + got, ok := action.(*fleetapi.ActionUnenroll) require.True(t, ok, "could not cast action to fleetapi.ActionUnenroll") assert.Equal(t, want, got) @@ -463,7 +459,7 @@ func runTestStateStore(t *testing.T, ackToken string) { require.NoError(t, err, "could not create disk store") stateStore.SetAckToken(ackToken) - stateStore.SetQueue([]action{want}) + stateStore.SetQueue([]fleetapi.ScheduledAction{want}) err = stateStore.Save() require.NoError(t, err, "failed saving state store") diff --git a/internal/pkg/config/operations/inspector.go b/internal/pkg/config/operations/inspector.go index ef4ab0ab32b..f43b4fd9488 100644 --- a/internal/pkg/config/operations/inspector.go +++ b/internal/pkg/config/operations/inspector.go @@ -113,18 +113,16 @@ func loadConfig(ctx context.Context, configPath string) (*config.Config, error) } func loadFleetConfig(ctx context.Context, l *logger.Logger) (map[string]interface{}, error) { - stateStore, err := store.NewStateStoreWithMigration(ctx, l, paths.AgentActionStoreFile(), paths.AgentStateStoreFile()) + stateStore, err := store.NewStateStoreWithMigration( + ctx, l, paths.AgentActionStoreFile(), paths.AgentStateStoreFile()) if err != nil { return nil, err } - for _, c := range stateStore.Actions() { - cfgChange, ok := c.(*fleetapi.ActionPolicyChange) - if !ok { - continue - } - + cfgChange, ok := stateStore.Action().(*fleetapi.ActionPolicyChange) + if ok { return cfgChange.Data.Policy, nil } + return nil, nil } diff --git a/internal/pkg/fleetapi/action.go b/internal/pkg/fleetapi/action.go index a7d4fdfb82f..1065dca7913 100644 --- a/internal/pkg/fleetapi/action.go +++ b/internal/pkg/fleetapi/action.go @@ -50,6 +50,10 @@ type Action interface { AckEvent() AckEvent } +// Actions is a slice of Actions to executes and allow to unmarshal +// heterogeneous action types. +type Actions []Action + // ScheduledAction is an Action that may be executed at a later date // Only ActionUpgrade implements this at the moment type ScheduledAction interface { @@ -87,6 +91,38 @@ type Signed struct { Signature string `json:"signature" yaml:"signature" mapstructure:"signature"` } +// NewAction returns a new, zero-value, action of the type defined by 'actionType' +// or an ActionUnknown with the 'OriginalType' field set to 'actionType' if the +// type is not valid. +func NewAction(actionType string) Action { + var action Action + + // keep the case statements alphabetically sorted + switch actionType { + case ActionTypeCancel: + action = &ActionCancel{} + case ActionTypeDiagnostics: + action = &ActionDiagnostics{} + case ActionTypeInputAction: + // Only INPUT_ACTION type actions could possibly be signed https://github.com/elastic/elastic-agent/pull/2348 + action = &ActionApp{} + case ActionTypePolicyChange: + action = &ActionPolicyChange{} + case ActionTypePolicyReassign: + action = &ActionPolicyReassign{} + case ActionTypeSettings: + action = &ActionSettings{} + case ActionTypeUnenroll: + action = &ActionUnenroll{} + case ActionTypeUpgrade: + action = &ActionUpgrade{} + default: + action = &ActionUnknown{OriginalType: actionType} + } + + return action +} + func newAckEvent(id, aType string) AckEvent { return AckEvent{ EventType: "ACTION_RESULT", @@ -535,9 +571,6 @@ func (a *ActionApp) MarshalMap() (map[string]interface{}, error) { return res, err } -// Actions is a list of Actions to executes and allow to unmarshal heterogenous action type. -type Actions []Action - // UnmarshalJSON takes every raw representation of an action and try to decode them. func (a *Actions) UnmarshalJSON(data []byte) error { var typeUnmarshaler []struct { @@ -559,30 +592,7 @@ func (a *Actions) UnmarshalJSON(data []byte) error { actions := make([]Action, 0, len(typeUnmarshaler)) for i, response := range typeUnmarshaler { - var action Action - - // keep the case statements alphabetically sorted - switch response.ActionType { - case ActionTypeCancel: - action = &ActionCancel{} - case ActionTypeDiagnostics: - action = &ActionDiagnostics{} - case ActionTypeInputAction: - // Only INPUT_ACTION type actions could possibly be signed https://github.com/elastic/elastic-agent/pull/2348 - action = &ActionApp{} - case ActionTypePolicyChange: - action = &ActionPolicyChange{} - case ActionTypePolicyReassign: - action = &ActionPolicyReassign{} - case ActionTypeSettings: - action = &ActionSettings{} - case ActionTypeUnenroll: - action = &ActionUnenroll{} - case ActionTypeUpgrade: - action = &ActionUpgrade{} - default: - action = &ActionUnknown{OriginalType: response.ActionType} - } + action := NewAction(response.ActionType) if err := json.Unmarshal(rawActions[i], action); err != nil { return errors.New(err, diff --git a/internal/pkg/queue/actionqueue.go b/internal/pkg/queue/actionqueue.go index b0cdc127dff..da2c84aa8c9 100644 --- a/internal/pkg/queue/actionqueue.go +++ b/internal/pkg/queue/actionqueue.go @@ -11,9 +11,9 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/fleetapi" ) -// saver is an the minimal interface needed for state storage. +// saver is the minimal interface needed for state storage. type saver interface { - SetQueue(a []fleetapi.Action) + SetQueue(a []fleetapi.ScheduledAction) Save() error } @@ -74,13 +74,9 @@ func (q *queue) Pop() interface{} { // newQueue creates a new priority queue using container/heap. // Will return an error if StartTime fails for any action. -func newQueue(actions []fleetapi.Action) (*queue, error) { +func newQueue(actions []fleetapi.ScheduledAction) (*queue, error) { q := make(queue, len(actions)) - for i, a := range actions { - action, ok := a.(fleetapi.ScheduledAction) - if !ok { - continue - } + for i, action := range actions { ts, err := action.StartTime() if err != nil { return nil, err @@ -95,8 +91,8 @@ func newQueue(actions []fleetapi.Action) (*queue, error) { return &q, nil } -// NewActionQueue creates a new queue with the passed actions using the persistor for state storage. -func NewActionQueue(actions []fleetapi.Action, s saver) (*ActionQueue, error) { +// NewActionQueue creates a new queue with the passed actions using the saver for state storage. +func NewActionQueue(actions []fleetapi.ScheduledAction, s saver) (*ActionQueue, error) { q, err := newQueue(actions) if err != nil { return nil, err @@ -149,8 +145,8 @@ func (q *ActionQueue) Cancel(actionID string) int { } // Actions returns all actions in the queue, item 0 is garunteed to be the min, the rest may not be in sorted order. -func (q *ActionQueue) Actions() []fleetapi.Action { - actions := make([]fleetapi.Action, q.q.Len()) +func (q *ActionQueue) Actions() []fleetapi.ScheduledAction { + actions := make([]fleetapi.ScheduledAction, q.q.Len()) for i, item := range *q.q { actions[i] = item.action } diff --git a/internal/pkg/queue/actionqueue_test.go b/internal/pkg/queue/actionqueue_test.go index d5a7a5c41d4..df6b5a3d1c4 100644 --- a/internal/pkg/queue/actionqueue_test.go +++ b/internal/pkg/queue/actionqueue_test.go @@ -56,7 +56,7 @@ type mockSaver struct { mock.Mock } -func (m *mockSaver) SetQueue(a []fleetapi.Action) { +func (m *mockSaver) SetQueue(a []fleetapi.ScheduledAction) { m.Called(a) } @@ -85,14 +85,14 @@ func TestNewQueue(t *testing.T) { }) t.Run("empty actions slice", func(t *testing.T) { - q, err := newQueue([]fleetapi.Action{}) + q, err := newQueue([]fleetapi.ScheduledAction{}) require.NoError(t, err) assert.NotNil(t, q) assert.Empty(t, q) }) t.Run("ordered actions list", func(t *testing.T) { - q, err := newQueue([]fleetapi.Action{a1, a2, a3}) + q, err := newQueue([]fleetapi.ScheduledAction{a1, a2, a3}) assert.NotNil(t, q) require.NoError(t, err) assert.Len(t, *q, 3) @@ -107,7 +107,7 @@ func TestNewQueue(t *testing.T) { }) t.Run("unordered actions list", func(t *testing.T) { - q, err := newQueue([]fleetapi.Action{a3, a2, a1}) + q, err := newQueue([]fleetapi.ScheduledAction{a3, a2, a1}) require.NoError(t, err) assert.NotNil(t, q) assert.Len(t, *q, 3) @@ -124,7 +124,7 @@ func TestNewQueue(t *testing.T) { t.Run("start time error", func(t *testing.T) { a := &mockAction{} a.On("StartTime").Return(time.Time{}, errors.New("oh no")) - q, err := newQueue([]fleetapi.Action{a}) + q, err := newQueue([]fleetapi.ScheduledAction{a}) assert.EqualError(t, err, "oh no") assert.Nil(t, q) }) From 9f7ccc249c716a656c435450387d7d347950d72e Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Wed, 20 Mar 2024 07:09:01 +0100 Subject: [PATCH 04/39] add migrations for action and state stores (#4305) --- internal/pkg/agent/application/paths/files.go | 13 +- ...crypted_disk_storage_windows_linux_test.go | 6 + .../pkg/agent/storage/encrypted_disk_store.go | 9 +- internal/pkg/agent/storage/storage.go | 4 +- .../pkg/agent/storage/store/action_store.go | 156 ---- .../agent/storage/store/action_store_test.go | 84 -- .../store/internal/migrations/migrations.go | 95 +++ .../pkg/agent/storage/store/migrations.go | 197 +++++ .../pkg/agent/storage/store/state_store.go | 179 ++--- .../agent/storage/store/state_store_test.go | 744 ++++++++++++++---- .../testdata/8.0.0-action_policy_change.yml | 23 + .../store/testdata/8.0.0-action_unenroll.yml | 20 + internal/pkg/queue/actionqueue.go | 4 +- 13 files changed, 1039 insertions(+), 495 deletions(-) delete mode 100644 internal/pkg/agent/storage/store/action_store.go delete mode 100644 internal/pkg/agent/storage/store/action_store_test.go create mode 100644 internal/pkg/agent/storage/store/internal/migrations/migrations.go create mode 100644 internal/pkg/agent/storage/store/migrations.go create mode 100644 internal/pkg/agent/storage/store/testdata/8.0.0-action_policy_change.yml create mode 100644 internal/pkg/agent/storage/store/testdata/8.0.0-action_unenroll.yml diff --git a/internal/pkg/agent/application/paths/files.go b/internal/pkg/agent/application/paths/files.go index e6a1bf2eda1..298c9ad58a0 100644 --- a/internal/pkg/agent/application/paths/files.go +++ b/internal/pkg/agent/application/paths/files.go @@ -23,13 +23,20 @@ const defaultAgentFleetFile = "fleet.enc" // defaultAgentEnrollFile is a name of file used to enroll agent on first-start const defaultAgentEnrollFile = "enroll.yml" -// defaultAgentActionStoreFile is the file that will contain the action that can be replayed after restart. +// defaultAgentActionStoreFile is the file that will contain the action that can +// be replayed after restart. +// It's deprecated and kept for migration purposes. +// Deprecated. const defaultAgentActionStoreFile = "action_store.yml" -// defaultAgentStateStoreYmlFile is the file that will contain the action that can be replayed after restart. +// defaultAgentStateStoreYmlFile is the file that will contain the action that +// can be replayed after restart. +// It's deprecated and kept for migration purposes. +// Deprecated. const defaultAgentStateStoreYmlFile = "state.yml" -// defaultAgentStateStoreFile is the file that will contain the action that can be replayed after restart encrypted. +// defaultAgentStateStoreFile is the file that will contain the encrypted state +// store. const defaultAgentStateStoreFile = "state.enc" // defaultInputDPath return the location of the inputs.d. diff --git a/internal/pkg/agent/storage/encrypted_disk_storage_windows_linux_test.go b/internal/pkg/agent/storage/encrypted_disk_storage_windows_linux_test.go index 83d0106d308..878a8cd011c 100644 --- a/internal/pkg/agent/storage/encrypted_disk_storage_windows_linux_test.go +++ b/internal/pkg/agent/storage/encrypted_disk_storage_windows_linux_test.go @@ -109,4 +109,10 @@ func TestEncryptedDiskStorageWindowsLinuxLoad(t *testing.T) { if diff != "" { t.Error(diff) } + + // Save something else + err = s.Save(bytes.NewBuffer([]byte("new data"))) + if err != nil { + t.Fatal(err) + } } diff --git a/internal/pkg/agent/storage/encrypted_disk_store.go b/internal/pkg/agent/storage/encrypted_disk_store.go index 280d1c8b3ba..7af2869bf7a 100644 --- a/internal/pkg/agent/storage/encrypted_disk_store.go +++ b/internal/pkg/agent/storage/encrypted_disk_store.go @@ -88,8 +88,13 @@ func (d *EncryptedDiskStore) ensureKey(ctx context.Context) error { return nil } -// Save will write the encrypted storage to disk. -// Specifically it will write to a .tmp file then rotate the file to the target name to ensure that an error does not corrupt the previously written file. +// Save will read 'in' and write its contents encrypted to disk. +// If EncryptedDiskStore.Load() was called, the io.ReadCloser it returns MUST be +// closed before Save() can be called. It is so because Save() writes to a .tmp +// file then rotate the file to the target name to ensure that an error does not +// corrupt the previously written file. +// Specially on windows systems, if the original files is still open because of +// Load(), Save() would fail. func (d *EncryptedDiskStore) Save(in io.Reader) error { // Ensure has agent key err := d.ensureKey(d.ctx) diff --git a/internal/pkg/agent/storage/storage.go b/internal/pkg/agent/storage/storage.go index 952f82c5883..bf7d47fe293 100644 --- a/internal/pkg/agent/storage/storage.go +++ b/internal/pkg/agent/storage/storage.go @@ -14,7 +14,9 @@ const perms os.FileMode = 0600 // Store saves the io.Reader. type Store interface { - // Save the io.Reader. + // Save the io.Reader. Depending on the underlying implementation, if + // Storage.Load() was called, the io.ReadCloser MUST be closed before Save() + // can be called. Save(io.Reader) error } diff --git a/internal/pkg/agent/storage/store/action_store.go b/internal/pkg/agent/storage/store/action_store.go deleted file mode 100644 index dd7f839d846..00000000000 --- a/internal/pkg/agent/storage/store/action_store.go +++ /dev/null @@ -1,156 +0,0 @@ -// 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 ( - "errors" - "fmt" - "io" - - "gopkg.in/yaml.v2" - - "github.com/elastic/elastic-agent/internal/pkg/fleetapi" - "github.com/elastic/elastic-agent/pkg/core/logger" -) - -// actionStore 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 threadsafe. -// The actionStore is deprecated, please use and extend the stateStore instead. The actionStore will be eventually removed. -// Deprecated. -type actionStore struct { - log *logger.Logger - store saveLoader - dirty bool - action fleetapi.Action -} - -// newActionStore creates a new action store. -func newActionStore(log *logger.Logger, store saveLoader) (*actionStore, 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 action store, returning empty contents: %v", err.Error()) - return &actionStore{log: log, store: store}, nil - } - defer reader.Close() - - var action actionPolicyChangeSerializer - - dec := yaml.NewDecoder(reader) - err = dec.Decode(&action) - if errors.Is(err, io.EOF) { - return &actionStore{ - log: log, - store: store, - }, nil - } - if err != nil { - return nil, err - } - - apc := fleetapi.ActionPolicyChange(action) - - return &actionStore{ - log: log, - store: store, - action: &apc, - }, nil -} - -// add is only taking care of ActionPolicyChange for now and will only keep the last one it receive, -// any other type of action will be silently ignored. -func (s *actionStore) add(a fleetapi.Action) { - switch v := a.(type) { - case *fleetapi.ActionPolicyChange, *fleetapi.ActionUnenroll: - // Only persist the action if the action is different. - if s.action != nil && s.action.ID() == v.ID() { - return - } - s.dirty = true - s.action = a - } -} - -// save saves actions to backing store. -func (s *actionStore) save() error { - defer func() { s.dirty = false }() - if !s.dirty { - return nil - } - - var reader io.Reader - if apc, ok := s.action.(*fleetapi.ActionPolicyChange); ok { - serialize := actionPolicyChangeSerializer(*apc) - - r, err := jsonToReader(&serialize) - if err != nil { - return err - } - - reader = r - } else if aun, ok := s.action.(*fleetapi.ActionUnenroll); ok { - serialize := actionUnenrollSerializer(*aun) - - r, err := jsonToReader(&serialize) - if err != nil { - return err - } - - reader = r - } - - if reader == nil { - return fmt.Errorf("incompatible type, expected ActionPolicyChange and received %T", s.action) - } - - if err := s.store.Save(reader); err != nil { - return err - } - s.log.Debugf("save on disk action policy change: %+v", s.action) - return nil -} - -// actions returns a slice of action to execute in order, currently only a action policy change is -// persisted. -func (s *actionStore) actions() fleetapi.Actions { - if s.action == nil { - return fleetapi.Actions{} - } - - return fleetapi.Actions{s.action} -} - -// actionPolicyChangeSerializer is a struct that adds a YAML serialization, I don't think serialization -// is a concern of the fleetapi package. I went this route so I don't have to do much refactoring. -// -// There are four ways to achieve the same results: -// 1. We create a second struct that map the existing field. -// 2. We add the serialization in the fleetapi. -// 3. We move the actual action type outside the actual fleetapi package. -// 4. We have two sets of type. -// -// This could be done in a refactoring. -type actionPolicyChangeSerializer struct { - ActionID string `json:"id" yaml:"id"` - ActionType string `json:"type" yaml:"type"` - Data fleetapi.ActionPolicyChangeData `json:"data,omitempty" yaml:"data,omitempty"` -} - -// add a guards between the serializer structs and the original struct. -var _ = actionPolicyChangeSerializer(fleetapi.ActionPolicyChange{}) - -// actionUnenrollSerializer is a struct that adds a YAML serialization, -type actionUnenrollSerializer struct { - ActionID string `json:"action_id"` - ActionType string `json:"action_type"` - IsDetected bool `json:"is_detected"` - Signed *fleetapi.Signed `json:"signed,omitempty"` -} - -// add a guards between the serializer structs and the original struct. -var _ = actionUnenrollSerializer(fleetapi.ActionUnenroll{}) diff --git a/internal/pkg/agent/storage/store/action_store_test.go b/internal/pkg/agent/storage/store/action_store_test.go deleted file mode 100644 index 691e4302c16..00000000000 --- a/internal/pkg/agent/storage/store/action_store_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// 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 ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/elastic/elastic-agent/internal/pkg/agent/storage" - "github.com/elastic/elastic-agent/internal/pkg/fleetapi" - "github.com/elastic/elastic-agent/pkg/core/logger" -) - -func TestActionStore(t *testing.T) { - log, _ := logger.New("action_store", false) - withFile := func(fn func(t *testing.T, file string)) func(*testing.T) { - return func(t *testing.T) { - dir, err := os.MkdirTemp("", "action-store") - require.NoError(t, err) - defer os.RemoveAll(dir) - file := filepath.Join(dir, "config.yml") - fn(t, file) - } - } - - t.Run("action returns empty when no action is saved on disk", - withFile(func(t *testing.T, file string) { - s := storage.NewDiskStore(file) - store, err := newActionStore(log, s) - require.NoError(t, err) - require.Equal(t, 0, len(store.actions())) - })) - - t.Run("will discard silently unknown action", - withFile(func(t *testing.T, file string) { - actionPolicyChange := &fleetapi.ActionUnknown{ - ActionID: "abc123", - } - - s := storage.NewDiskStore(file) - store, err := newActionStore(log, s) - require.NoError(t, err) - - require.Equal(t, 0, len(store.actions())) - store.add(actionPolicyChange) - err = store.save() - require.NoError(t, err) - require.Equal(t, 0, len(store.actions())) - })) - - t.Run("can save to disk known action type", - withFile(func(t *testing.T, file string) { - ActionPolicyChange := &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{"hello": "world"}}, - } - - s := storage.NewDiskStore(file) - store, err := newActionStore(log, s) - require.NoError(t, err) - - require.Equal(t, 0, len(store.actions())) - store.add(ActionPolicyChange) - err = store.save() - require.NoError(t, err) - require.Equal(t, 1, len(store.actions())) - - s = storage.NewDiskStore(file) - store1, err := newActionStore(log, s) - require.NoError(t, err) - - actions := store1.actions() - require.Equal(t, 1, len(actions)) - - require.Equal(t, ActionPolicyChange, actions[0]) - })) -} diff --git a/internal/pkg/agent/storage/store/internal/migrations/migrations.go b/internal/pkg/agent/storage/store/internal/migrations/migrations.go new file mode 100644 index 00000000000..d711b791964 --- /dev/null +++ b/internal/pkg/agent/storage/store/internal/migrations/migrations.go @@ -0,0 +1,95 @@ +// 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 migrations + +import ( + "bytes" + "errors" + "fmt" + "io" + "time" + + "gopkg.in/yaml.v2" +) + +type StateStore struct { + Action Action `yaml:"action"` + AckToken string `yaml:"ack_token"` + ActionQueue []ActionQueue `yaml:"action_queue"` +} + +// Action is a struct to read an Action Policy Change from its old +// YAML format. +type Action struct { + ActionID string `yaml:"action_id"` + Type string `yaml:"action_type"` + StartTime time.Time `yaml:"start_time,omitempty"` + SourceURI string `yaml:"source_uri,omitempty"` + RetryAttempt int `yaml:"retry_attempt,omitempty"` + Policy map[string]interface{} `yaml:"policy,omitempty"` + IsDetected bool `yaml:"is_detected,omitempty"` +} + +// ActionQueue is a struct to read the action queue from its old YAML format. +type ActionQueue struct { + ActionID string `yaml:"action_id"` + Type string `yaml:"type"` + StartTime time.Time `yaml:"start_time,omitempty"` + ExpirationTime time.Time `yaml:"expiration,omitempty"` + Version string `yaml:"version,omitempty"` + SourceURI string `yaml:"source_uri,omitempty"` + RetryAttempt int `yaml:"retry_attempt,omitempty"` + Policy map[string]interface{} `yaml:"policy,omitempty"` + IsDetected bool `yaml:"is_detected,omitempty"` +} + +type loader interface { + Load() (io.ReadCloser, error) +} + +// LoadActionStore loads an action store from loader. +func LoadActionStore(loader loader) (*Action, error) { + return LoadStore[Action](loader) +} + +// LoadYAMLStateStore loads the old YAML state store from loader. +func LoadYAMLStateStore(loader loader) (*StateStore, error) { + return LoadStore[StateStore](loader) +} + +// LoadStore loads a YAML file. +func LoadStore[Store any](loader loader) (store *Store, err error) { + // Store is a generic type, this might be needed. + store = new(Store) + + reader, err := loader.Load() + if err != nil { + return nil, fmt.Errorf("failed to load action store: %w", err) + } + defer func() { + err2 := reader.Close() + if err != nil { + err = errors.Join(err, + fmt.Errorf("migration storeLoad failed to close reader: %w", err2)) + } + }() + + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + buff := bytes.NewReader(data) + + dec := yaml.NewDecoder(buff) + err = dec.Decode(&store) + if errors.Is(err, io.EOF) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("could not YAML unmarshal action from action store: %w", err) + } + + return store, nil +} diff --git a/internal/pkg/agent/storage/store/migrations.go b/internal/pkg/agent/storage/store/migrations.go new file mode 100644 index 00000000000..49149f9e156 --- /dev/null +++ b/internal/pkg/agent/storage/store/migrations.go @@ -0,0 +1,197 @@ +// 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 ( + "errors" + "fmt" + "time" + + "github.com/elastic/elastic-agent/internal/pkg/agent/storage" + "github.com/elastic/elastic-agent/internal/pkg/agent/storage/store/internal/migrations" + "github.com/elastic/elastic-agent/internal/pkg/fleetapi" + "github.com/elastic/elastic-agent/pkg/core/logger" +) + +var ErrInvalidYAML = errors.New("could not parse YAML") + +func migrateActionStoreToStateStore( + log *logger.Logger, + actionStorePath string, + stateDiskStore storage.Storage) (err error) { + + log = log.Named("state_migration") + actionDiskStore := storage.NewDiskStore(actionStorePath) + + stateStoreExists, err := stateDiskStore.Exists() + if err != nil { + log.Errorf("failed to check if state store exists: %v", err) + return err + } + + // do not migrate if the state store already exists + if stateStoreExists { + log.Debugf("state store already exists") + return nil + } + + actionStoreExists, err := actionDiskStore.Exists() + if err != nil { + log.Errorf("failed to check if action store %s exists: %v", actionStorePath, err) + return err + } + + // delete the actions store file upon successful migration + defer func() { + if err == nil && actionStoreExists { + err = actionDiskStore.Delete() + if err != nil { + log.Errorf("failed to delete action store %s exists: %v", actionStorePath, err) + } + } + }() + + // nothing to migrate if the action store doesn't exist + if !actionStoreExists { + log.Debugf("action store %s doesn't exists, nothing to migrate", actionStorePath) + return nil + } + + action, err := migrations.LoadActionStore(actionDiskStore) + if err != nil { + log.Errorf("failed to create action store %s: %v", actionStorePath, err) + return err + } + + // no actions stored nothing to migrate + if action == nil { + log.Debugf("no action stored in the action store %s, nothing to migrate", actionStorePath) + return nil + } + + if action.Type != fleetapi.ActionTypePolicyChange { + log.Warnf("unexpected action type when migrating from action store. "+ + "Found %s, but only %s is suported. Ignoring action and proceeding.", + action.Type, fleetapi.ActionTypePolicyChange) + // If it isn't ignored, the agent will be stuck here and require manual + // intervention to fix the store. + return nil + } + + stateStore, err := NewStateStore(log, stateDiskStore) + if err != nil { + return err + } + + // set actions from the action store to the state store + stateStore.SetAction(&fleetapi.ActionPolicyChange{ + ActionID: action.ActionID, + ActionType: action.Type, + Data: fleetapi.ActionPolicyChangeData{Policy: action.Policy}, + }) + + err = stateStore.Save() + if err != nil { + log.Debugf("failed to save agent state store, err: %v", err) + } + return err +} + +// migrateYAMLStateStoreToStateStoreV1 migrates the YAML store to the new JSON +// state store. If the contents of store is already a JSON, it returns the +// parsed JSON. +func migrateYAMLStateStoreToStateStoreV1(log *logger.Logger, store storage.Storage) error { + exists, err := store.Exists() + if err != nil { + return fmt.Errorf("migration YMAL to Store v1 failed: "+ + "could not load store from disk: %w", err) + } + + // nothing to migrate, return empty store. + if !exists { + return nil + } + + // JSON is a subset of YAML, so first check if it's already a JSON store. + reader, err := store.Load() + if err != nil { + return fmt.Errorf("could not read store content: %w", err) + } + + st, err := readState(reader) + // close it as soon as possible and before the next store save + _ = reader.Close() + if err == nil { + // it's a valid JSON, therefore nothing to migrate + return nil + } + + // Try to read the store as YAML + yamlStore, err := migrations.LoadYAMLStateStore(store) + if err != nil { + // it isn't a YAML store + return errors.Join(ErrInvalidYAML, err) + } + + // Store was empty, nothing to migrate + if yamlStore == nil { + return nil + } + + var action fleetapi.Action + switch yamlStore.Action.Type { + case fleetapi.ActionTypePolicyChange: + action = &fleetapi.ActionPolicyChange{ + ActionID: yamlStore.Action.ActionID, + ActionType: yamlStore.Action.Type, + Data: fleetapi.ActionPolicyChangeData{ + Policy: yamlStore.Action.Policy}, + } + case fleetapi.ActionTypeUnenroll: + action = &fleetapi.ActionUnenroll{ + ActionID: yamlStore.Action.ActionID, + ActionType: yamlStore.Action.Type, + IsDetected: yamlStore.Action.IsDetected, + } + default: + log.Warnf("loaded a unsupported %s action from the deprecated YAML state store, ignoring it", + yamlStore.Action.Type) + } + + var queue actionQueue + for _, a := range yamlStore.ActionQueue { + queue = append(queue, + &fleetapi.ActionUpgrade{ + ActionID: a.ActionID, + ActionType: a.Type, + ActionStartTime: a.StartTime.Format(time.RFC3339), + ActionExpiration: a.ExpirationTime.Format(time.RFC3339), + Data: fleetapi.ActionUpgradeData{ + Version: a.Version, + SourceURI: a.SourceURI, + Retry: a.RetryAttempt, + }, + }) + } + + st = state{ + Version: "1", + ActionSerializer: actionSerializer{Action: action}, + AckToken: yamlStore.AckToken, + Queue: queue, + } + + jsonReader, err := jsonToReader(&st) + if err != nil { + return fmt.Errorf("store state migrated from YAML failed: %w", err) + } + + err = store.Save(jsonReader) + if err != nil { + return fmt.Errorf("failed to save store state migrated from YAML: %w", err) + } + + return nil +} diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index e3feb23103d..a2c206d3127 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -8,7 +8,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "io" "sync" @@ -70,59 +69,34 @@ type actionSerializer struct { // therefore the need for this type to do so. type actionQueue []fleetapi.ScheduledAction -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 - } +// NewStateStoreWithMigration creates a new state store and migrates the old ones. +func NewStateStoreWithMigration( + ctx context.Context, + log *logger.Logger, + actionStorePath, + stateStorePath string) (*StateStore, error) { + stateDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath) - return nil + return newStateStoreWithMigration(log, actionStorePath, stateDiskStore) } -func (aq *actionQueue) UnmarshalJSON(data []byte) error { - actions := fleetapi.Actions{} - err := json.Unmarshal(data, &actions) +func newStateStoreWithMigration( + log *logger.Logger, + actionStorePath string, + stateStore storage.Storage) (*StateStore, error) { + err := migrateActionStoreToStateStore(log, actionStorePath, stateStore) if err != nil { - return fmt.Errorf("actionQueue failed to unmarshal: %w", err) + return nil, fmt.Errorf("failed migrating action store to YAML state store: %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 -} - -// NewStateStoreWithMigration creates a new state store and migrates the old one. -func NewStateStoreWithMigration(ctx context.Context, log *logger.Logger, actionStorePath, stateStorePath string) (*StateStore, error) { - - stateDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath) - err := migrateActionStoreToStateStore(log, actionStorePath, stateDiskStore) + err = migrateYAMLStateStoreToStateStoreV1(log, stateStore) if err != nil { - return nil, err + return nil, fmt.Errorf("failed dmigrating YAML store JSON store: %w", + err) } - return NewStateStore(log, storage.NewEncryptedDiskStore(ctx, stateStorePath)) + return NewStateStore(log, stateStore) } // NewStateStoreActionAcker creates a new state store backed action acker. @@ -141,18 +115,9 @@ func NewStateStore(log *logger.Logger, store saveLoader) (*StateStore, error) { } defer reader.Close() - st := state{} - dec := json.NewDecoder(reader) - err = dec.Decode(&st) - if errors.Is(err, io.EOF) { - return &StateStore{ - log: log, - store: store, - state: state{Version: Version}, - }, nil - } + st, err := readState(reader) if err != nil { - return nil, fmt.Errorf("could not JSON unmarshal state store: %w", err) + return nil, fmt.Errorf("could not parse store content: %w", err) } if st.Version != Version { @@ -168,6 +133,30 @@ func NewStateStore(log *logger.Logger, store saveLoader) (*StateStore, error) { }, 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) { @@ -294,73 +283,47 @@ func (a *StateStoreActionAcker) Commit(ctx context.Context) error { return a.acker.Commit(ctx) } -func migrateActionStoreToStateStore( - log *logger.Logger, - actionStorePath string, - stateDiskStore storage.Storage) (err error) { - - log = log.Named("state_migration") - actionDiskStore := storage.NewDiskStore(actionStorePath) - - stateStoreExits, err := stateDiskStore.Exists() - if err != nil { - log.Errorf("failed to check if state store exists: %v", err) - return err - } +func (as *actionSerializer) MarshalJSON() ([]byte, error) { + return json.Marshal(as.Action) +} - // do not migrate if the state store already exists - if stateStoreExits { - log.Debugf("state store already exists") - return nil +func (as *actionSerializer) UnmarshalJSON(data []byte) error { + var typeUnmarshaler struct { + Type string `json:"type,omitempty" yaml:"type,omitempty"` } - - actionStoreExits, err := actionDiskStore.Exists() + err := json.Unmarshal(data, &typeUnmarshaler) if err != nil { - log.Errorf("failed to check if action store %s exists: %v", actionStorePath, err) return err } - // delete the actions store file upon successful migration - defer func() { - if err == nil && actionStoreExits { - err = actionDiskStore.Delete() - if err != nil { - log.Errorf("failed to delete action store %s exists: %v", actionStorePath, err) - } - } - }() - - // nothing to migrate if the action store doesn't exists - if !actionStoreExits { - log.Debugf("action store %s doesn't exists, nothing to migrate", actionStorePath) - return nil - } - - actionStore, err := newActionStore(log, actionDiskStore) + as.Action = fleetapi.NewAction(typeUnmarshaler.Type) + err = json.Unmarshal(data, &as.Action) if err != nil { - log.Errorf("failed to create action store %s: %v", actionStorePath, err) return err } - // no actions stored nothing to migrate - if len(actionStore.actions()) == 0 { - log.Debugf("no actions stored in the action store %s, nothing to migrate", actionStorePath) - return nil - } + return nil +} - stateStore, err := NewStateStore(log, stateDiskStore) +func (aq *actionQueue) UnmarshalJSON(data []byte) error { + actions := fleetapi.Actions{} + err := json.Unmarshal(data, &actions) if err != nil { - return err + return fmt.Errorf("actionQueue failed to unmarshal: %w", err) } - // set actions from the action store to the state store - stateStore.SetAction(actionStore.actions()[0]) - - err = stateStore.Save() - if err != nil { - log.Debugf("failed to save agent state store, err: %v", 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) } - return err + + *aq = scheduledActions + return nil } func jsonToReader(in interface{}) (io.Reader, error) { diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 2ffd0746af9..829ca612e40 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -5,8 +5,8 @@ package store import ( + "bytes" "context" - "io" "os" "path/filepath" "reflect" @@ -25,6 +25,13 @@ import ( "github.com/elastic/elastic-agent/pkg/core/logger" ) +type wrongAction struct{} + +func (wrongAction) ID() string { return "" } +func (wrongAction) Type() string { return "" } +func (wrongAction) String() string { return "" } +func (wrongAction) AckEvent() fleetapi.AckEvent { return fleetapi.AckEvent{} } + func TestStateStore(t *testing.T) { t.Run("ack token", func(t *testing.T) { runTestStateStore(t, "czlV93YBwdkt5lYhBY7S") @@ -33,14 +40,542 @@ func TestStateStore(t *testing.T) { t.Run("no ack token", func(t *testing.T) { runTestStateStore(t, "") }) + + t.Run("migrate", func(t *testing.T) { + if runtime.GOOS == "darwin" { + // the original test never actually run, so with this at least + // there is coverage for linux and windows. + t.Skipf("needs https://github.com/elastic/elastic-agent/issues/3866" + + "to be merged so this test can work on darwin") + } + + t.Run("action store file does not exists", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + newStateStorePath := filepath.Join(tempDir, "state_store.yml") + + newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath) + err := migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "migration action store -> state store failed") + + // to load from disk a new store needs to be created, it loads the + // file to memory during the store creation. + stateStore, err := NewStateStore(log, newStateStore) + require.NoError(t, err, "could not load state store") + + assert.Nil(t, stateStore.Action()) + assert.Empty(t, stateStore.Queue()) + }) + + t.Run("action store is empty", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + newStateStorePath := filepath.Join(tempDir, "state_store.yml") + + err := os.WriteFile(oldActionStorePath, []byte(""), 0600) + require.NoError(t, err, "could not create empty action store file") + + newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath) + err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "migration action store -> state store failed") + + // to load from disk a new store needs to be created, it loads the + // file to memory during the store creation. + stateStore, err := NewStateStore(log, newStateStore) + require.NoError(t, err, "could not load state store") + + assert.Nil(t, stateStore.Action()) + assert.Empty(t, stateStore.Queue()) + }) + + t.Run("action store to YAML state store", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, // YAML unmarshaller unmarshals int as float + }, + }, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + goldenActionStore, err := os.ReadFile( + filepath.Join("testdata", "7.17.18-action_store.yml")) + require.NoError(t, err, "could not read action store golden file") + + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + err = os.WriteFile(oldActionStorePath, goldenActionStore, 0666) + require.NoError(t, err, "could not copy action store golden file") + + newStateStorePath := filepath.Join(tempDir, "state_store.yaml") + newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath, + storage.WithVaultPath(vaultPath)) + + err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "migration action store -> state store failed") + + // to load from disk a new store needs to be created, it loads the file + // to memory during the store creation. + newStateStore = storage.NewEncryptedDiskStore(ctx, newStateStorePath, + storage.WithVaultPath(vaultPath)) + stateStore, err := NewStateStore(log, newStateStore) + require.NoError(t, err, "could not create state store") + + got := stateStore.Action() + require.NotNil(t, got, "should have loaded an action") + + assert.Equalf(t, want, got, + "loaded action differs from action on the old action store") + assert.Empty(t, stateStore.Queue(), + "queue should be empty, old action store did not have a queue") + }) + + t.Run("YAML state store containing an ActionPolicyChange to JSON state store", + func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + }, + }, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + yamlStorePlain, err := os.ReadFile( + filepath.Join("testdata", "8.0.0-action_policy_change.yml")) + require.NoError(t, err, "could not read action store golden file") + + encDiskStorePath := filepath.Join(tempDir, "store.enc") + encDiskStore := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, + storage.WithVaultPath(vaultPath)) + err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) + require.NoError(t, err, + "failed saving copy of golden files on an EncryptedDiskStore") + + err = migrateYAMLStateStoreToStateStoreV1(log, encDiskStore) + require.NoError(t, err, "YAML state store -> JSON state store failed") + + // Load migrated store from disk + stateStore, err := NewStateStore(log, encDiskStore) + require.NoError(t, err, "could not load store from disk") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("YAML state store containing an ActionUnenroll to JSON state store", + func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionUnenroll{ + ActionID: "abc123", + ActionType: "UNENROLL", + IsDetected: true, + Signed: nil, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + yamlStorePlain, err := os.ReadFile( + filepath.Join("testdata", "8.0.0-action_unenroll.yml")) + require.NoError(t, err, "could not read action store golden file") + + encDiskStorePath := filepath.Join(tempDir, "store.enc") + encDiskStore := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, + storage.WithVaultPath(vaultPath)) + err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) + require.NoError(t, err, + "failed saving copy of golden files on an EncryptedDiskStore") + + err = migrateYAMLStateStoreToStateStoreV1(log, encDiskStore) + require.NoError(t, err, "YAML state store -> JSON state store failed") + + // Load migrated store from disk + stateStore, err := NewStateStore(log, encDiskStore) + require.NoError(t, err, "could not load store from disk") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("YAML state store when JSON state store exists", func(t *testing.T) { + log, _ := logger.NewTesting("") + + ctx := context.Background() + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + }, + }, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + stateStorePath := filepath.Join(tempDir, "store.enc") + endDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath, + storage.WithVaultPath(vaultPath)) + + // Create and save a JSON state store + stateStore, err := NewStateStore(log, endDiskStore) + require.NoError(t, err, "could not create state store") + stateStore.SetAckToken(want.AckToken) + stateStore.SetAction(want.ActionSerializer.Action) + stateStore.SetQueue(want.Queue) + err = stateStore.Save() + require.NoError(t, err, "state store save filed") + + // Try to migrate an existing JSON store + err = migrateYAMLStateStoreToStateStoreV1(log, endDiskStore) + require.NoError(t, err, "YAML state store -> JSON state store failed") + + // Load migrated store from disk + stateStore, err = NewStateStore(log, endDiskStore) + require.NoError(t, err, "could not load store from disk") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("NewStateStoreWithMigration", func(t *testing.T) { + t.Run("action store exists", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, // YAML unmarshaller unmarshals int as float + }, + }, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + goldenActionStore, err := os.ReadFile( + filepath.Join("testdata", "7.17.18-action_store.yml")) + require.NoError(t, err, "could not read action store golden file") + + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + err = os.WriteFile(oldActionStorePath, goldenActionStore, 0666) + require.NoError(t, err, "could not copy action store golden file") + + newStateStorePath := filepath.Join(tempDir, "state_store.yaml") + newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath, + storage.WithVaultPath(vaultPath)) + + stateStore, err := newStateStoreWithMigration(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "newStateStoreWithMigration failed") + + got := stateStore.Action() + assert.Equalf(t, want, got, + "loaded action differs from action on the old action store") + assert.Empty(t, stateStore.Queue(), + "queue should be empty, old action store did not have a queue") + }) + + t.Run("YAML state store to JSON state store", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + }, + }, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + yamlStorePlain, err := os.ReadFile( + filepath.Join("testdata", "8.0.0-action_policy_change.yml")) + require.NoError(t, err, "could not read action store golden file") + + yamlStoreEncPath := filepath.Join(tempDir, "yaml_store.enc") + yamlStoreEnc := storage.NewEncryptedDiskStore(ctx, yamlStoreEncPath, + storage.WithVaultPath(vaultPath)) + err = yamlStoreEnc.Save(bytes.NewBuffer(yamlStorePlain)) + require.NoError(t, err, + "failed saving copy of golden files on an EncryptedDiskStore") + + stateStore, err := newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), yamlStoreEnc) + require.NoError(t, err, "newStateStoreWithMigration failed") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("up to date store, no migration needed", func(t *testing.T) { + log, _ := logger.NewTesting("") + + ctx := context.Background() + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + }, + }, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + stateStorePath := filepath.Join(tempDir, "store.enc") + endDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath, + storage.WithVaultPath(vaultPath)) + + // Create and save a JSON state store + stateStore, err := NewStateStore(log, endDiskStore) + require.NoError(t, err, "could not create state store") + stateStore.SetAckToken(want.AckToken) + stateStore.SetAction(want.ActionSerializer.Action) + stateStore.SetQueue(want.Queue) + err = stateStore.Save() + require.NoError(t, err, "state store save filed") + + stateStore, err = newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), endDiskStore) + require.NoError(t, err, "newStateStoreWithMigration failed") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("no store exists", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + stateStorePath := filepath.Join(tempDir, "store.enc") + endDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath, + storage.WithVaultPath(vaultPath)) + + got, err := newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), endDiskStore) + require.NoError(t, err, "newStateStoreWithMigration failed") + + assert.Nil(t, got.Action(), + "no action should have been loaded") + assert.Empty(t, got.Queue(), "action queue should be empty") + assert.Empty(t, got.AckToken(), + "no AckToken should have been loaded") + }) + }) + }) +} + +func createAgentVaultAndSecret(t *testing.T, ctx context.Context, tempDir string) string { + vaultPath := filepath.Join(tempDir, "vault") + + err := os.MkdirAll(vaultPath, 0o750) + require.NoError(t, err, + "could not create directory for the agent's vault") + + _, err = vault.New(ctx, vaultPath) + require.NoError(t, err, "could not create agent's vault") + + err = secret.CreateAgentSecret( + context.Background(), secret.WithVaultPath(vaultPath)) + require.NoError(t, err, "could not create agent secret") + + return vaultPath } func runTestStateStore(t *testing.T, ackToken string) { log, _ := logger.New("state_store", false) - ctx, cn := context.WithCancel(context.Background()) - defer cn() - t.Run("action returns empty when no action is saved on disk", func(t *testing.T) { storePath := filepath.Join(t.TempDir(), "state.yml") s := storage.NewDiskStore(storePath) @@ -70,7 +605,7 @@ func runTestStateStore(t *testing.T, ackToken string) { require.Equal(t, ackToken, store.AckToken()) }) - t.Run("can save to disk known action type", func(t *testing.T) { + t.Run("can save to disk ActionPolicyChange", func(t *testing.T) { ActionPolicyChange := &fleetapi.ActionPolicyChange{ ActionID: "abc123", ActionType: "POLICY_CHANGE", @@ -107,6 +642,71 @@ func runTestStateStore(t *testing.T, ackToken string) { require.Equal(t, ackToken, store.AckToken()) }) + t.Run("can save to disk ActionUnenroll", func(t *testing.T) { + want := &fleetapi.ActionUnenroll{ + ActionID: "abc123", + ActionType: "UNENROLL", + } + + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + + require.Empty(t, store.Action()) + require.Empty(t, store.Queue()) + store.SetAction(want) + store.SetAckToken(ackToken) + err = store.Save() + require.NoError(t, err) + require.NotNil(t, store.Action(), "store should have an action stored") + require.Empty(t, store.Queue()) + require.Equal(t, ackToken, store.AckToken()) + + s = storage.NewDiskStore(storePath) + store1, err := NewStateStore(log, s) + require.NoError(t, err) + + got := store1.Action() + require.NotNil(t, got, "store should have an action stored") + require.Empty(t, store1.Queue()) + require.Equal(t, want, got) + require.Equal(t, ackToken, store.AckToken()) + }) + + t.Run("errors when saving invalid action type", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + + store.state.ActionSerializer.Action = wrongAction{} + store.dirty = true + err = store.Save() + require.ErrorContains(t, err, "incompatible type, expected") + }) + + t.Run("do not set action if it has the same ID", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.yml") + s := storage.NewDiskStore(storePath) + store, err := NewStateStore(log, s) + require.NoError(t, err) + + want := &fleetapi.ActionUnenroll{ + ActionID: "abc123", + ActionType: "UNENROLL", + } + store.state.ActionSerializer.Action = want + + store.SetAction(&fleetapi.ActionUnenroll{ + ActionID: "abc123", + ActionType: "UNENROLL", + IsDetected: true, + }) + + assert.Equal(t, want, store.state.ActionSerializer.Action) + }) + t.Run("can save a queue with one upgrade action", func(t *testing.T) { ts := time.Now().UTC().Round(time.Second) queue := []fleetapi.ScheduledAction{&fleetapi.ActionUpgrade{ @@ -194,38 +794,6 @@ func runTestStateStore(t *testing.T, ackToken string) { } }) - t.Run("can save to disk unenroll action type", func(t *testing.T) { - want := &fleetapi.ActionUnenroll{ - ActionID: "abc123", - ActionType: "UNENROLL", - } - - storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) - store, err := NewStateStore(log, s) - require.NoError(t, err) - - require.Empty(t, store.Action()) - require.Empty(t, store.Queue()) - store.SetAction(want) - store.SetAckToken(ackToken) - err = store.Save() - require.NoError(t, err) - require.NotNil(t, store.Action(), "store should have an action stored") - require.Empty(t, store.Queue()) - require.Equal(t, ackToken, store.AckToken()) - - s = storage.NewDiskStore(storePath) - store1, err := NewStateStore(log, s) - require.NoError(t, err) - - got := store1.Action() - require.NotNil(t, got, "store should have an action stored") - require.Empty(t, store1.Queue()) - require.Equal(t, want, got) - require.Equal(t, ackToken, store.AckToken()) - }) - t.Run("when we ACK we save to disk", func(t *testing.T) { ActionPolicyChange := &fleetapi.ActionPolicyChange{ ActionID: "abc123", @@ -246,107 +814,6 @@ func runTestStateStore(t *testing.T, ackToken string) { require.Equal(t, ackToken, store.AckToken()) }) - t.Run("migrate actions file does not exists", func(t *testing.T) { - if runtime.GOOS == "darwin" { - // the original test never actually run, so with this at least - // there is coverage for linux and windows. - t.Skipf("needs https://github.com/elastic/elastic-agent/issues/3866" + - "to be merged so this test can work on darwin") - } - - tempDir := t.TempDir() - oldActionStorePath := filepath.Join(tempDir, "action_store.yml") - newStateStorePath := filepath.Join(tempDir, "state_store.yml") - - newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath) - err := migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) - require.NoError(t, err, "migration action store -> state store failed") - - // to load from disk a new store needs to be created, it loads the file - // to memory during the store creation. - stateStore, err := NewStateStore(log, storage.NewDiskStore(newStateStorePath)) - require.NoError(t, err) - stateStore.SetAckToken(ackToken) - require.Empty(t, stateStore.Action()) - require.Equal(t, ackToken, stateStore.AckToken()) - require.Empty(t, stateStore.Queue()) - }) - - t.Run("migrate", func(t *testing.T) { - // TODO: DO NOT MERGE TO MAIN WITHOUT REMOVING THIS SKIP - t.Skip("this test is broken because the migration haven't been" + - " implemented yet. It'll implemented on another PR as part of" + - "https://github.com/elastic/elastic-agent/issues/3912") - - if runtime.GOOS == "darwin" { - // the original migrate never actually run, so with this at least - // there is coverage for linux and windows. - t.Skipf("needs https://github.com/elastic/elastic-agent/issues/3866" + - "to be merged so this test can work on darwin") - } - - want := &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ - "hello": "world", - "phi": 1.618, - "answer": 42, - }, - }, - } - - tempDir := t.TempDir() - vaultPath := filepath.Join(tempDir, "vault") - err := os.MkdirAll(vaultPath, 0o750) - require.NoError(t, err, - "could not create directory for the agent's vault") - _, err = vault.New(ctx, vaultPath) - require.NoError(t, err, "could not create agent's vault") - err = secret.CreateAgentSecret( - context.Background(), secret.WithVaultPath(vaultPath)) - require.NoError(t, err, "could not create agent secret") - - // Copy the golden file as the migration deletes the old store. - goldenActionStoreFile, err := os.Open( - filepath.Join("testdata", "7.17.18-action_store.yml")) - require.NoError(t, err, "could not open action store golden file") - defer goldenActionStoreFile.Close() - - oldActionStorePath := filepath.Join(tempDir, "action_store.yml") - storeFile, err := os.Create(oldActionStorePath) - require.NoError(t, err, "could not create action store file") - - _, err = io.Copy(storeFile, goldenActionStoreFile) - require.NoError(t, err, "could not copy action store golden file") - err = storeFile.Close() - // It needs to be closed now otherwise on windows the store will fail to - // open the file. - require.NoError(t, err, "could not close store file") - - newStateStorePath := filepath.Join(tempDir, "state_store.yaml") - newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath, - storage.WithVaultPath(vaultPath)) - err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) - require.NoError(t, err, "migration action store -> state store failed") - - // to load from disk a new store needs to be created, it loads the file - // to memory during the store creation. - newStateStore = storage.NewEncryptedDiskStore(ctx, newStateStorePath, - storage.WithVaultPath(vaultPath)) - stateStore, err := NewStateStore(log, newStateStore) - require.NoError(t, err, "could not create state store") - - got := stateStore.Action() - require.NotNil(t, got, "should have loaded an action") - - assert.Equalf(t, want, got, - "loaded action differs from action on the old action store") - assert.Empty(t, stateStore.Queue(), - "queue should be empty, old action store did not have a queue") - }) - t.Run("state store is correctly loaded from disk", func(t *testing.T) { t.Run("ActionPolicyChange", func(t *testing.T) { storePath := filepath.Join(t.TempDir(), "state.yaml") @@ -486,7 +953,6 @@ func runTestStateStore(t *testing.T, ackToken string) { } }) }) - } type testAcker struct { diff --git a/internal/pkg/agent/storage/store/testdata/8.0.0-action_policy_change.yml b/internal/pkg/agent/storage/store/testdata/8.0.0-action_policy_change.yml new file mode 100644 index 00000000000..dc4525ae47f --- /dev/null +++ b/internal/pkg/agent/storage/store/testdata/8.0.0-action_policy_change.yml @@ -0,0 +1,23 @@ +action: + action_id: abc123 + action_type: POLICY_CHANGE + policy: + answer: 42 + hello: world + phi: 1.618 +ack_token: czlV93YBwdkt5lYhBY7S +action_queue: + - action_id: action1 + type: UPGRADE + start_time: "2024-02-19T17:48:40Z" + expiration: "2025-02-19T17:48:40Z" + version: 1.2.3 + source_uri: https://example.com + retry_attempt: 1 + - action_id: action2 + type: UPGRADE + start_time: "2024-02-19T17:48:40Z" + expiration: "2025-02-19T17:48:40Z" + version: 1.2.3 + source_uri: https://example.com + retry_attempt: 1 diff --git a/internal/pkg/agent/storage/store/testdata/8.0.0-action_unenroll.yml b/internal/pkg/agent/storage/store/testdata/8.0.0-action_unenroll.yml new file mode 100644 index 00000000000..1bc6f20a055 --- /dev/null +++ b/internal/pkg/agent/storage/store/testdata/8.0.0-action_unenroll.yml @@ -0,0 +1,20 @@ +action: + action_id: abc123 + action_type: UNENROLL + is_detected: true +ack_token: czlV93YBwdkt5lYhBY7S +action_queue: + - action_id: action1 + type: UPGRADE + start_time: "2024-02-19T17:48:40Z" + expiration: "2025-02-19T17:48:40Z" + version: 1.2.3 + source_uri: https://example.com + retry_attempt: 1 + - action_id: action2 + type: UPGRADE + start_time: "2024-02-19T17:48:40Z" + expiration: "2025-02-19T17:48:40Z" + version: 1.2.3 + source_uri: https://example.com + retry_attempt: 1 diff --git a/internal/pkg/queue/actionqueue.go b/internal/pkg/queue/actionqueue.go index da2c84aa8c9..897448221e6 100644 --- a/internal/pkg/queue/actionqueue.go +++ b/internal/pkg/queue/actionqueue.go @@ -55,7 +55,7 @@ func (q queue) Swap(i, j int) { // When using the queue, the Add method should be used instead. func (q *queue) Push(x interface{}) { n := len(*q) - e := x.(*item) //nolint:errcheck // should be an *item + e := x.(*item) e.index = n *q = append(*q, e) } @@ -123,7 +123,7 @@ func (q *ActionQueue) DequeueActions() []fleetapi.ScheduledAction { if (*q.q)[0].priority > ts { break } - item := heap.Pop(q.q).(*item) //nolint:errcheck // should be an *item + item := heap.Pop(q.q).(*item) actions = append(actions, item.action) } return actions From 1c3dfe01bd1401e8e73cc7747d1a40d4484a4b9c Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Wed, 27 Mar 2024 16:45:30 +0100 Subject: [PATCH 05/39] fix stores conflicts --- .../pkg/agent/storage/store/migrations.go | 6 +- .../pkg/agent/storage/store/state_store.go | 7 +- .../agent/storage/store/state_store_test.go | 122 +++++++++++++----- 3 files changed, 99 insertions(+), 36 deletions(-) diff --git a/internal/pkg/agent/storage/store/migrations.go b/internal/pkg/agent/storage/store/migrations.go index 49149f9e156..252361ebf90 100644 --- a/internal/pkg/agent/storage/store/migrations.go +++ b/internal/pkg/agent/storage/store/migrations.go @@ -23,7 +23,11 @@ func migrateActionStoreToStateStore( stateDiskStore storage.Storage) (err error) { log = log.Named("state_migration") - actionDiskStore := storage.NewDiskStore(actionStorePath) + actionDiskStore, err := storage.NewDiskStore(actionStorePath) + if err != nil { + return fmt.Errorf( + "could not create disk store when migratins action store: %w", err) + } stateStoreExists, err := stateDiskStore.Exists() if err != nil { diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index a2c206d3127..5c962fe7b3d 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -75,7 +75,12 @@ func NewStateStoreWithMigration( log *logger.Logger, actionStorePath, stateStorePath string) (*StateStore, error) { - stateDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath) + stateDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath) + if err != nil { + return nil, fmt.Errorf( + "could not create EncryptedDiskStore when creating StateStoreWithMigration: %w", + err) + } return newStateStoreWithMigration(log, actionStorePath, stateDiskStore) } diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 829ca612e40..921902e03f0 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -57,8 +57,10 @@ func TestStateStore(t *testing.T) { oldActionStorePath := filepath.Join(tempDir, "action_store.yml") newStateStorePath := filepath.Join(tempDir, "state_store.yml") - newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath) - err := migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) + newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) require.NoError(t, err, "migration action store -> state store failed") // to load from disk a new store needs to be created, it loads the @@ -81,7 +83,9 @@ func TestStateStore(t *testing.T) { err := os.WriteFile(oldActionStorePath, []byte(""), 0600) require.NoError(t, err, "could not create empty action store file") - newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath) + newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath) + require.NoError(t, err, "failed creating EncryptedDiskStore") + err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) require.NoError(t, err, "migration action store -> state store failed") @@ -122,16 +126,19 @@ func TestStateStore(t *testing.T) { require.NoError(t, err, "could not copy action store golden file") newStateStorePath := filepath.Join(tempDir, "state_store.yaml") - newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath, + newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) require.NoError(t, err, "migration action store -> state store failed") // to load from disk a new store needs to be created, it loads the file // to memory during the store creation. - newStateStore = storage.NewEncryptedDiskStore(ctx, newStateStorePath, + newStateStore, err = storage.NewEncryptedDiskStore(ctx, newStateStorePath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + stateStore, err := NewStateStore(log, newStateStore) require.NoError(t, err, "could not create state store") @@ -199,8 +206,10 @@ func TestStateStore(t *testing.T) { require.NoError(t, err, "could not read action store golden file") encDiskStorePath := filepath.Join(tempDir, "store.enc") - encDiskStore := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, + encDiskStore, err := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) require.NoError(t, err, "failed saving copy of golden files on an EncryptedDiskStore") @@ -265,8 +274,10 @@ func TestStateStore(t *testing.T) { require.NoError(t, err, "could not read action store golden file") encDiskStorePath := filepath.Join(tempDir, "store.enc") - encDiskStore := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, + encDiskStore, err := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) require.NoError(t, err, "failed saving copy of golden files on an EncryptedDiskStore") @@ -332,8 +343,9 @@ func TestStateStore(t *testing.T) { vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) stateStorePath := filepath.Join(tempDir, "store.enc") - endDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath, + endDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") // Create and save a JSON state store stateStore, err := NewStateStore(log, endDiskStore) @@ -384,8 +396,9 @@ func TestStateStore(t *testing.T) { require.NoError(t, err, "could not copy action store golden file") newStateStorePath := filepath.Join(tempDir, "state_store.yaml") - newStateStore := storage.NewEncryptedDiskStore(ctx, newStateStorePath, + newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") stateStore, err := newStateStoreWithMigration(log, oldActionStorePath, newStateStore) require.NoError(t, err, "newStateStoreWithMigration failed") @@ -451,8 +464,10 @@ func TestStateStore(t *testing.T) { require.NoError(t, err, "could not read action store golden file") yamlStoreEncPath := filepath.Join(tempDir, "yaml_store.enc") - yamlStoreEnc := storage.NewEncryptedDiskStore(ctx, yamlStoreEncPath, + yamlStoreEnc, err := storage.NewEncryptedDiskStore(ctx, yamlStoreEncPath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + err = yamlStoreEnc.Save(bytes.NewBuffer(yamlStorePlain)) require.NoError(t, err, "failed saving copy of golden files on an EncryptedDiskStore") @@ -514,8 +529,9 @@ func TestStateStore(t *testing.T) { vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) stateStorePath := filepath.Join(tempDir, "store.enc") - endDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath, + endDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") // Create and save a JSON state store stateStore, err := NewStateStore(log, endDiskStore) @@ -540,8 +556,9 @@ func TestStateStore(t *testing.T) { vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) stateStorePath := filepath.Join(tempDir, "store.enc") - endDiskStore := storage.NewEncryptedDiskStore(ctx, stateStorePath, + endDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") got, err := newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), endDiskStore) require.NoError(t, err, "newStateStoreWithMigration failed") @@ -563,11 +580,10 @@ func createAgentVaultAndSecret(t *testing.T, ctx context.Context, tempDir string require.NoError(t, err, "could not create directory for the agent's vault") - _, err = vault.New(ctx, vaultPath) + _, err = vault.New(ctx, vault.WithVaultPath(vaultPath)) require.NoError(t, err, "could not create agent's vault") - err = secret.CreateAgentSecret( - context.Background(), secret.WithVaultPath(vaultPath)) + context.Background(), vault.WithVaultPath(vaultPath)) require.NoError(t, err, "could not create agent secret") return vaultPath @@ -578,7 +594,9 @@ func runTestStateStore(t *testing.T, ackToken string) { t.Run("action returns empty when no action is saved on disk", func(t *testing.T) { storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err := NewStateStore(log, s) require.NoError(t, err) require.Empty(t, store.Action()) @@ -591,7 +609,9 @@ func runTestStateStore(t *testing.T, ackToken string) { } storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err := NewStateStore(log, s) require.NoError(t, err) @@ -616,7 +636,9 @@ func runTestStateStore(t *testing.T, ackToken string) { } storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err := NewStateStore(log, s) require.NoError(t, err) @@ -630,7 +652,9 @@ func runTestStateStore(t *testing.T, ackToken string) { require.Empty(t, store.Queue()) require.Equal(t, ackToken, store.AckToken()) - s = storage.NewDiskStore(storePath) + s, err = storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store1, err := NewStateStore(log, s) require.NoError(t, err) @@ -649,7 +673,9 @@ func runTestStateStore(t *testing.T, ackToken string) { } storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err := NewStateStore(log, s) require.NoError(t, err) @@ -663,7 +689,9 @@ func runTestStateStore(t *testing.T, ackToken string) { require.Empty(t, store.Queue()) require.Equal(t, ackToken, store.AckToken()) - s = storage.NewDiskStore(storePath) + s, err = storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store1, err := NewStateStore(log, s) require.NoError(t, err) @@ -676,7 +704,9 @@ func runTestStateStore(t *testing.T, ackToken string) { t.Run("errors when saving invalid action type", func(t *testing.T) { storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err := NewStateStore(log, s) require.NoError(t, err) @@ -688,7 +718,9 @@ func runTestStateStore(t *testing.T, ackToken string) { t.Run("do not set action if it has the same ID", func(t *testing.T) { storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err := NewStateStore(log, s) require.NoError(t, err) @@ -719,7 +751,9 @@ func runTestStateStore(t *testing.T, ackToken string) { }}} storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err := NewStateStore(log, s) require.NoError(t, err) @@ -730,7 +764,9 @@ func runTestStateStore(t *testing.T, ackToken string) { require.Empty(t, store.Action()) require.Len(t, store.Queue(), 1) - s = storage.NewDiskStore(storePath) + s, err = storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err = NewStateStore(log, s) require.NoError(t, err) @@ -768,7 +804,9 @@ func runTestStateStore(t *testing.T, ackToken string) { }}} storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err := NewStateStore(log, s) require.NoError(t, err) @@ -780,7 +818,9 @@ func runTestStateStore(t *testing.T, ackToken string) { require.Len(t, store.Queue(), 2) // Load state store from disk - s = storage.NewDiskStore(storePath) + s, err = storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err = NewStateStore(log, s) require.NoError(t, err, "could not load store from disk") @@ -800,7 +840,9 @@ func runTestStateStore(t *testing.T, ackToken string) { } storePath := filepath.Join(t.TempDir(), "state.yml") - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + store, err := NewStateStore(log, s) require.NoError(t, err) store.SetAckToken(ackToken) @@ -829,7 +871,9 @@ func runTestStateStore(t *testing.T, ackToken string) { }, } - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + stateStore, err := NewStateStore(log, s) require.NoError(t, err, "could not create disk store") @@ -839,7 +883,9 @@ func runTestStateStore(t *testing.T, ackToken string) { require.NoError(t, err, "failed saving state store") // to load from disk a new store needs to be created - s = storage.NewDiskStore(storePath) + s, err = storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + stateStore, err = NewStateStore(log, s) require.NoError(t, err, "could not create disk store") @@ -871,7 +917,9 @@ func runTestStateStore(t *testing.T, ackToken string) { }, } - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + stateStore, err := NewStateStore(log, s) require.NoError(t, err, "could not create disk store") @@ -881,7 +929,9 @@ func runTestStateStore(t *testing.T, ackToken string) { require.NoError(t, err, "failed saving state store") // to load from disk a new store needs to be created - s = storage.NewDiskStore(storePath) + s, err = storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + stateStore, err = NewStateStore(log, s) require.NoError(t, err, "could not create disk store") @@ -921,7 +971,9 @@ func runTestStateStore(t *testing.T, ackToken string) { } t.Logf("state store: %q", storePath) - s := storage.NewDiskStore(storePath) + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + stateStore, err := NewStateStore(log, s) require.NoError(t, err, "could not create disk store") @@ -931,7 +983,9 @@ func runTestStateStore(t *testing.T, ackToken string) { require.NoError(t, err, "failed saving state store") // to load from disk a new store needs to be created - s = storage.NewDiskStore(storePath) + s, err = storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + stateStore, err = NewStateStore(log, s) require.NoError(t, err, "could not create disk store") From f61bbdffd6a232955d49b79456929d7259c34b30 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 2 Apr 2024 14:05:34 +0200 Subject: [PATCH 06/39] adjust actions in protection package and diagnostics action --- internal/pkg/agent/protection/action.go | 2 +- internal/pkg/agent/protection/action_test.go | 4 +-- internal/pkg/fleetapi/action.go | 30 +++++++++++--------- internal/pkg/fleetapi/action_test.go | 6 ++-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/internal/pkg/agent/protection/action.go b/internal/pkg/agent/protection/action.go index 2548f90d4e9..aca222b10cb 100644 --- a/internal/pkg/agent/protection/action.go +++ b/internal/pkg/agent/protection/action.go @@ -21,7 +21,7 @@ var ( ) type fleetActionWithAgents struct { - ActionID string `json:"action_id"` // Note the action_id here, since the signed action uses action_id for id + ActionID string `json:"id"` ActionType string `json:"type,omitempty"` InputType string `json:"input_type,omitempty"` Timestamp string `json:"@timestamp"` diff --git a/internal/pkg/agent/protection/action_test.go b/internal/pkg/agent/protection/action_test.go index 48774585cb2..d14c16410dd 100644 --- a/internal/pkg/agent/protection/action_test.go +++ b/internal/pkg/agent/protection/action_test.go @@ -46,9 +46,7 @@ func signAction(action map[string]interface{}, emptyData bool, pk *ecdsa.Private "data": base64.StdEncoding.EncodeToString(payload), "signature": base64.StdEncoding.EncodeToString(sig), } - // Remap the action_id to id same way the fleet server does for checkins - action["id"] = action["action_id"] - delete(action, "action_id") + return action, nil } diff --git a/internal/pkg/fleetapi/action.go b/internal/pkg/fleetapi/action.go index d4d67662e1a..f002a810fcf 100644 --- a/internal/pkg/fleetapi/action.go +++ b/internal/pkg/fleetapi/action.go @@ -156,7 +156,7 @@ func (a *ActionUnknown) ID() string { func (a *ActionUnknown) String() string { var s strings.Builder - s.WriteString("action_id: ") + s.WriteString("id: ") s.WriteString(a.ActionID) s.WriteString(", type: ") s.WriteString(a.ActionType) @@ -189,7 +189,7 @@ type ActionPolicyReassignData struct { func (a *ActionPolicyReassign) String() string { var s strings.Builder - s.WriteString("action_id: ") + s.WriteString("id: ") s.WriteString(a.ActionID) s.WriteString(", type: ") s.WriteString(a.ActionType) @@ -223,7 +223,7 @@ type ActionPolicyChangeData struct { func (a *ActionPolicyChange) String() string { var s strings.Builder - s.WriteString("action_id: ") + s.WriteString("id: ") s.WriteString(a.ActionID) s.WriteString(", type: ") s.WriteString(a.ActionType) @@ -265,7 +265,7 @@ type ActionUpgradeData struct { func (a *ActionUpgrade) String() string { var s strings.Builder - s.WriteString("action_id: ") + s.WriteString("id: ") s.WriteString(a.ActionID) s.WriteString(", type: ") s.WriteString(a.ActionType) @@ -368,7 +368,7 @@ type ActionUnenroll struct { func (a *ActionUnenroll) String() string { var s strings.Builder - s.WriteString("action_id: ") + s.WriteString("id: ") s.WriteString(a.ActionID) s.WriteString(", type: ") s.WriteString(a.ActionType) @@ -421,7 +421,7 @@ func (a *ActionSettings) Type() string { func (a *ActionSettings) String() string { var s strings.Builder - s.WriteString("action_id: ") + s.WriteString("id: ") s.WriteString(a.ActionID) s.WriteString(", type: ") s.WriteString(a.ActionType) @@ -457,7 +457,7 @@ func (a *ActionCancel) Type() string { func (a *ActionCancel) String() string { var s strings.Builder - s.WriteString("action_id: ") + s.WriteString("id: ") s.WriteString(a.ActionID) s.WriteString(", type: ") s.WriteString(a.ActionType) @@ -472,11 +472,13 @@ func (a *ActionCancel) AckEvent() AckEvent { // ActionDiagnostics is a request to gather and upload a diagnostics bundle. type ActionDiagnostics struct { - ActionID string `json:"action_id"` - ActionType string `json:"type"` - AdditionalMetrics []string `json:"additional_metrics"` - UploadID string `json:"-"` - Err error `json:"-"` + ActionID string `json:"id"` + ActionType string `json:"type"` + Data struct { + AdditionalMetrics []string `json:"additional_metrics"` + } `json:"data"` + UploadID string `json:"-"` + Err error `json:"-"` } // ID returns the ID of the action. @@ -491,7 +493,7 @@ func (a *ActionDiagnostics) Type() string { func (a *ActionDiagnostics) String() string { var s strings.Builder - s.WriteString("action_id: ") + s.WriteString("id: ") s.WriteString(a.ActionID) s.WriteString(", type: ") s.WriteString(a.ActionType) @@ -531,7 +533,7 @@ type ActionApp struct { func (a *ActionApp) String() string { var s strings.Builder - s.WriteString("action_id: ") + s.WriteString("id: ") s.WriteString(a.ActionID) s.WriteString(", type: ") s.WriteString(a.ActionType) diff --git a/internal/pkg/fleetapi/action_test.go b/internal/pkg/fleetapi/action_test.go index 1e8ddd65737..4df6b75063c 100644 --- a/internal/pkg/fleetapi/action_test.go +++ b/internal/pkg/fleetapi/action_test.go @@ -161,7 +161,7 @@ func TestActionsUnmarshalJSON(t *testing.T) { require.True(t, ok, "unable to cast action to specific type") assert.Equal(t, "testid", action.ActionID) assert.Equal(t, ActionTypeDiagnostics, action.ActionType) - assert.Empty(t, action.AdditionalMetrics) + assert.Empty(t, action.Data.AdditionalMetrics) }) t.Run("ActionDiagnostics with additional CPU metrics", func(t *testing.T) { p := []byte(`[{"id":"testid","type":"REQUEST_DIAGNOSTICS","data":{"additional_metrics":["CPU"]}}]`) @@ -172,8 +172,8 @@ func TestActionsUnmarshalJSON(t *testing.T) { require.True(t, ok, "unable to cast action to specific type") assert.Equal(t, "testid", action.ActionID) assert.Equal(t, ActionTypeDiagnostics, action.ActionType) - require.Len(t, action.AdditionalMetrics, 1) - assert.Equal(t, "CPU", action.AdditionalMetrics[0]) + require.Len(t, action.Data.AdditionalMetrics, 1) + assert.Equal(t, "CPU", action.Data.AdditionalMetrics[0]) }) } From 9589a5d551869b6cf7bd4bb31ccf8bcfc65acbab Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 2 Apr 2024 14:10:36 +0200 Subject: [PATCH 07/39] fix action diagnostics handler --- .../actions/handlers/handler_action_diagnostics.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics.go b/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics.go index e38b49ce66a..2cf72e49d4b 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics.go @@ -196,7 +196,7 @@ func (h *Diagnostics) runHooks(ctx context.Context, action *fleetapi.ActionDiagn // Currently CPU is the only additional metric we can collect. // If this changes we would need to change how we scan AdditionalMetrics. collectCPU := false - for _, metric := range action.AdditionalMetrics { + for _, metric := range action.Data.AdditionalMetrics { if metric == "CPU" { h.log.Debug("Diagnostics will collect CPU profile.") collectCPU = true @@ -290,7 +290,7 @@ func (h *Diagnostics) diagComponents(ctx context.Context, action *fleetapi.Actio h.log.Debugf("Component diagnostics complete. Took: %s", time.Since(startTime)) }() additionalMetrics := []cproto.AdditionalDiagnosticRequest{} - for _, metric := range action.AdditionalMetrics { + for _, metric := range action.Data.AdditionalMetrics { if metric == "CPU" { additionalMetrics = append(additionalMetrics, cproto.AdditionalDiagnosticRequest_CPU) } From 40320053a311a72d561ef7b1bc69eef7252ab590 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 2 Apr 2024 14:22:36 +0200 Subject: [PATCH 08/39] it should not be there --- .../storage/encrypted_disk_storage_windows_linux_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/pkg/agent/storage/encrypted_disk_storage_windows_linux_test.go b/internal/pkg/agent/storage/encrypted_disk_storage_windows_linux_test.go index aa5e80be367..e0ffc915466 100644 --- a/internal/pkg/agent/storage/encrypted_disk_storage_windows_linux_test.go +++ b/internal/pkg/agent/storage/encrypted_disk_storage_windows_linux_test.go @@ -112,10 +112,4 @@ func TestEncryptedDiskStorageWindowsLinuxLoad(t *testing.T) { if diff != "" { t.Error(diff) } - - // Save something else - err = s.Save(bytes.NewBuffer([]byte("new data"))) - if err != nil { - t.Fatal(err) - } } From bd1f715e80078cc051dbdc0d12b2580933c7730f Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 2 Apr 2024 14:27:17 +0200 Subject: [PATCH 09/39] use .json instead of .yml/.yaml --- .../agent/storage/store/state_store_test.go | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 921902e03f0..17778bd2267 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -593,7 +593,7 @@ func runTestStateStore(t *testing.T, ackToken string) { log, _ := logger.New("state_store", false) t.Run("action returns empty when no action is saved on disk", func(t *testing.T) { - storePath := filepath.Join(t.TempDir(), "state.yml") + storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") @@ -608,7 +608,7 @@ func runTestStateStore(t *testing.T, ackToken string) { ActionID: "abc123", } - storePath := filepath.Join(t.TempDir(), "state.yml") + storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") @@ -635,7 +635,7 @@ func runTestStateStore(t *testing.T, ackToken string) { }}, } - storePath := filepath.Join(t.TempDir(), "state.yml") + storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") @@ -672,7 +672,7 @@ func runTestStateStore(t *testing.T, ackToken string) { ActionType: "UNENROLL", } - storePath := filepath.Join(t.TempDir(), "state.yml") + storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") @@ -703,7 +703,7 @@ func runTestStateStore(t *testing.T, ackToken string) { }) t.Run("errors when saving invalid action type", func(t *testing.T) { - storePath := filepath.Join(t.TempDir(), "state.yml") + storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") @@ -717,7 +717,7 @@ func runTestStateStore(t *testing.T, ackToken string) { }) t.Run("do not set action if it has the same ID", func(t *testing.T) { - storePath := filepath.Join(t.TempDir(), "state.yml") + storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") @@ -750,7 +750,7 @@ func runTestStateStore(t *testing.T, ackToken string) { SourceURI: "https://example.com", }}} - storePath := filepath.Join(t.TempDir(), "state.yml") + storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") @@ -803,7 +803,7 @@ func runTestStateStore(t *testing.T, ackToken string) { Retry: 1, }}} - storePath := filepath.Join(t.TempDir(), "state.yml") + storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") @@ -839,7 +839,7 @@ func runTestStateStore(t *testing.T, ackToken string) { ActionID: "abc123", } - storePath := filepath.Join(t.TempDir(), "state.yml") + storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") @@ -858,7 +858,7 @@ func runTestStateStore(t *testing.T, ackToken string) { t.Run("state store is correctly loaded from disk", func(t *testing.T) { t.Run("ActionPolicyChange", func(t *testing.T) { - storePath := filepath.Join(t.TempDir(), "state.yaml") + storePath := filepath.Join(t.TempDir(), "state.json") want := &fleetapi.ActionPolicyChange{ ActionID: "abc123", ActionType: "POLICY_CHANGE", @@ -906,7 +906,7 @@ func runTestStateStore(t *testing.T, ackToken string) { }) t.Run("ActionUnenroll", func(t *testing.T) { - storePath := filepath.Join(t.TempDir(), "state.yaml") + storePath := filepath.Join(t.TempDir(), "state.json") want := &fleetapi.ActionUnenroll{ ActionID: "abc123", ActionType: fleetapi.ActionTypeUnenroll, @@ -952,7 +952,7 @@ func runTestStateStore(t *testing.T, ackToken string) { }) t.Run("action queue", func(t *testing.T) { - storePath := filepath.Join(t.TempDir(), "state.yaml") + storePath := filepath.Join(t.TempDir(), "state.json") now := time.Now().UTC().Round(time.Second) want := &fleetapi.ActionUpgrade{ ActionID: "test", From 27920c3229c4f41cc8381672c3c1bfbf36309ec8 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 2 Apr 2024 14:58:58 +0200 Subject: [PATCH 10/39] fix more tests --- testing/fleetservertest/fleetserver_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/fleetservertest/fleetserver_test.go b/testing/fleetservertest/fleetserver_test.go index a96c1df3606..bd101f39e25 100644 --- a/testing/fleetservertest/fleetserver_test.go +++ b/testing/fleetservertest/fleetserver_test.go @@ -170,7 +170,7 @@ func ExampleNewServer_checkin() { fmt.Println(got.Actions) // Output: - // [action_id: anActionID, type: POLICY_CHANGE] + // [id: anActionID, type: POLICY_CHANGE] } func ExampleNewServer_ack() { @@ -353,7 +353,7 @@ func ExampleNewServer_checkin_fakeComponent() { fmt.Println(resp.Actions) // Output: - // [action_id: anActionID, type: POLICY_CHANGE] + // [id: anActionID, type: POLICY_CHANGE] // Error: status code: 418, fleet-server returned an error: I'm a teapot // [] } @@ -423,7 +423,7 @@ func ExampleNewServer_checkin_withDelay() { resp.Actions) // Output: - // took more than 250ms: true. response: [action_id: anActionID, type: POLICY_CHANGE] + // took more than 250ms: true. response: [id: anActionID, type: POLICY_CHANGE] // took more than 250ms: false. response: [] } @@ -647,7 +647,7 @@ func ExampleNewServer_checkin_and_ackWithAcker() { // Output: // [1st ack] &fleetapi.AckResponse{Action:"acks", Errors:true, Items:[]fleetapi.AckResponseItem{fleetapi.AckResponseItem{Status:404, Message:"action anActionID not found"}}} - // [1st checkin] [action_id: anActionID, type: POLICY_CHANGE] + // [1st checkin] [id: anActionID, type: POLICY_CHANGE] // [2nd ack] &fleetapi.AckResponse{Action:"acks", Errors:false, Items:[]fleetapi.AckResponseItem{fleetapi.AckResponseItem{Status:200, Message:"OK"}}} // [2nd checkin] Error: status code: 418, fleet-server returned an error: I'm a teapot // [3rd ack] &fleetapi.AckResponse{Action:"acks", Errors:true, Items:[]fleetapi.AckResponseItem{fleetapi.AckResponseItem{Status:404, Message:"action not-received-on-checkin not found"}}} From cc4d6b4150842e64ba18eb7dd2422841de52c1da Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 2 Apr 2024 15:23:09 +0200 Subject: [PATCH 11/39] split actionDiagnosticsData into its own type and fix another test --- .../handlers/handler_action_diagnostics_test.go | 2 +- internal/pkg/fleetapi/action.go | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics_test.go b/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics_test.go index 27e476338b3..11977c36b11 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics_test.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics_test.go @@ -394,7 +394,7 @@ func TestDiagnosticHandlerWithCPUProfile(t *testing.T) { mockUploader.EXPECT().UploadDiagnostics(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("upload-id", nil) diagAction := &fleetapi.ActionDiagnostics{ - AdditionalMetrics: []string{"CPU"}, + Data: fleetapi.ActionDiagnosticsData{AdditionalMetrics: []string{"CPU"}}, } handler.collectDiag(context.Background(), diagAction, mockAcker) diff --git a/internal/pkg/fleetapi/action.go b/internal/pkg/fleetapi/action.go index f002a810fcf..c184cae6620 100644 --- a/internal/pkg/fleetapi/action.go +++ b/internal/pkg/fleetapi/action.go @@ -472,13 +472,15 @@ func (a *ActionCancel) AckEvent() AckEvent { // ActionDiagnostics is a request to gather and upload a diagnostics bundle. type ActionDiagnostics struct { - ActionID string `json:"id"` - ActionType string `json:"type"` - Data struct { - AdditionalMetrics []string `json:"additional_metrics"` - } `json:"data"` - UploadID string `json:"-"` - Err error `json:"-"` + ActionID string `json:"id"` + ActionType string `json:"type"` + Data ActionDiagnosticsData `json:"data"` + UploadID string `json:"-"` + Err error `json:"-"` +} + +type ActionDiagnosticsData struct { + AdditionalMetrics []string `json:"additional_metrics"` } // ID returns the ID of the action. From 85324a5447c29f7aace73f0f94b8cbdfe81ac583 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 2 Apr 2024 16:19:29 +0200 Subject: [PATCH 12/39] add changelog --- .../fragments/1712067343-fix-state-store.yaml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 changelog/fragments/1712067343-fix-state-store.yaml diff --git a/changelog/fragments/1712067343-fix-state-store.yaml b/changelog/fragments/1712067343-fix-state-store.yaml new file mode 100644 index 00000000000..58bf1ae0eba --- /dev/null +++ b/changelog/fragments/1712067343-fix-state-store.yaml @@ -0,0 +1,35 @@ +# Kind can be one of: +# - breaking-change: a change to previously-documented behavior +# - deprecation: functionality that is being removed in a later release +# - bug-fix: fixes a problem in a previous version +# - enhancement: extends functionality but does not break or fix existing behavior +# - feature: new functionality +# - known-issue: problems that we are aware of in a given version +# - security: impacts on the security of a product or a user’s deployment. +# - upgrade: important information for someone upgrading from a prior version +# - other: does not fit into any of the other categories +kind: feature + +# Change summary; a 80ish characters long description of the change. +summary: Fix the Elastic Agent state store + +# Long description; in case the summary is not enough to describe the change +# this field accommodate a description without length limits. +# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment. +description: | + This change fixes issues when loading data from the Agent's internal state store. + Which include the error `error parsing version ""` the Agent would present after + when trying to execute a scheduled upgrade after a restart. + +# Affected component; a word indicating the component this changeset affects. +component: + +# PR URL; optional; the PR number that added the changeset. +# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added. +# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number. +# Please provide it if you are adding a fragment for a different PR. +#pr: https://github.com/owner/repo/1234 + +# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of). +# If not present is automatically filled by the tooling with the issue linked to the PR number. +issue: https://github.com/elastic/elastic-agent/issues/3912 From 27a235e334471ecd587d5690d430c778bec5988c Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 2 Apr 2024 16:28:46 +0200 Subject: [PATCH 13/39] fix signed action data schema --- internal/pkg/agent/protection/action.go | 6 +++++- internal/pkg/agent/protection/action_test.go | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/pkg/agent/protection/action.go b/internal/pkg/agent/protection/action.go index aca222b10cb..6ff81dea262 100644 --- a/internal/pkg/agent/protection/action.go +++ b/internal/pkg/agent/protection/action.go @@ -20,8 +20,12 @@ var ( ErrInvalidSignatureValue = errors.New("invalid signature value") ) +// fleetActionWithAgents represents an action signed by Kibana. Its schema +// differs from the fleet-server API. +// Notably, in this struct, the action ID is referred to as `action_id` instead of `id`. +// The fleet-server API schema is documented at: https://raw.githubusercontent.com/elastic/fleet-server/main/model/openapi.yml. type fleetActionWithAgents struct { - ActionID string `json:"id"` + ActionID string `json:"action_id"` ActionType string `json:"type,omitempty"` InputType string `json:"input_type,omitempty"` Timestamp string `json:"@timestamp"` diff --git a/internal/pkg/agent/protection/action_test.go b/internal/pkg/agent/protection/action_test.go index d14c16410dd..29aef50f910 100644 --- a/internal/pkg/agent/protection/action_test.go +++ b/internal/pkg/agent/protection/action_test.go @@ -47,6 +47,9 @@ func signAction(action map[string]interface{}, emptyData bool, pk *ecdsa.Private "signature": base64.StdEncoding.EncodeToString(sig), } + // Remap the action_id to id to match fleet-server API schema. + action["id"] = action["action_id"] + delete(action, "action_id") return action, nil } From 7fd93e981adaea77ab34c666aa8667b88ac39605 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Wed, 3 Apr 2024 07:53:40 +0200 Subject: [PATCH 14/39] making the linter happy --- testing/fleetservertest/fleetserver_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/fleetservertest/fleetserver_test.go b/testing/fleetservertest/fleetserver_test.go index bd101f39e25..c3d65e2062b 100644 --- a/testing/fleetservertest/fleetserver_test.go +++ b/testing/fleetservertest/fleetserver_test.go @@ -339,7 +339,7 @@ func ExampleNewServer_checkin_fakeComponent() { fmt.Println(resp.Actions) // 2nd subsequent call to nextAction() will return an error. - resp, _, err = cmd.Execute(context.Background(), &fleetapi.CheckinRequest{}) + _, _, err = cmd.Execute(context.Background(), &fleetapi.CheckinRequest{}) if err == nil { panic("expected an error, got none") } @@ -622,7 +622,7 @@ func ExampleNewServer_checkin_and_ackWithAcker() { fmt.Printf("[2nd ack] %#v\n", respAck) // 2nd checkin: it will fail. - respCheckin, _, err = cmdCheckin.Execute(context.Background(), &fleetapi.CheckinRequest{}) + _, _, err = cmdCheckin.Execute(context.Background(), &fleetapi.CheckinRequest{}) if err == nil { panic("expected an error, got none") } From 85ac7b37ab8ac9ede40083887dfbff3adc5e498a Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 4 Apr 2024 15:43:02 +0200 Subject: [PATCH 15/39] PR review changes --- .../store/internal/migrations/migrations.go | 6 ++++-- .../pkg/agent/storage/store/migrations.go | 20 +++++++++---------- .../pkg/agent/storage/store/state_store.go | 2 +- internal/pkg/fleetapi/action.go | 1 - 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/internal/pkg/agent/storage/store/internal/migrations/migrations.go b/internal/pkg/agent/storage/store/internal/migrations/migrations.go index d711b791964..f811e45005d 100644 --- a/internal/pkg/agent/storage/store/internal/migrations/migrations.go +++ b/internal/pkg/agent/storage/store/internal/migrations/migrations.go @@ -70,9 +70,11 @@ func LoadStore[Store any](loader loader) (store *Store, err error) { } defer func() { err2 := reader.Close() + if err2 != nil { + err2 = fmt.Errorf("migration storeLoad failed to close reader: %w", err2) + } if err != nil { - err = errors.Join(err, - fmt.Errorf("migration storeLoad failed to close reader: %w", err2)) + err = errors.Join(err, err2) } }() diff --git a/internal/pkg/agent/storage/store/migrations.go b/internal/pkg/agent/storage/store/migrations.go index 252361ebf90..e787d3c1b7c 100644 --- a/internal/pkg/agent/storage/store/migrations.go +++ b/internal/pkg/agent/storage/store/migrations.go @@ -37,7 +37,7 @@ func migrateActionStoreToStateStore( // do not migrate if the state store already exists if stateStoreExists { - log.Debugf("state store already exists") + log.Debugf("not attempting to migrare from action store: state store already exists") return nil } @@ -47,25 +47,25 @@ func migrateActionStoreToStateStore( return err } + // nothing to migrate if the action store doesn't exist + if !actionStoreExists { + log.Debugf("action store %s doesn't exists, nothing to migrate", actionStorePath) + return nil + } // delete the actions store file upon successful migration defer func() { - if err == nil && actionStoreExists { + if err == nil { err = actionDiskStore.Delete() if err != nil { - log.Errorf("failed to delete action store %s exists: %v", actionStorePath, err) + log.Errorf("failed to delete action store %s after migration: %v", actionStorePath, err) } } }() - // nothing to migrate if the action store doesn't exist - if !actionStoreExists { - log.Debugf("action store %s doesn't exists, nothing to migrate", actionStorePath) - return nil - } - action, err := migrations.LoadActionStore(actionDiskStore) if err != nil { - log.Errorf("failed to create action store %s: %v", actionStorePath, err) + log.Errorf("failed to load action store for migration %s: %v", + actionStorePath, err) return err } diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index 5c962fe7b3d..e84c83bf27e 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -97,7 +97,7 @@ func newStateStoreWithMigration( err = migrateYAMLStateStoreToStateStoreV1(log, stateStore) if err != nil { - return nil, fmt.Errorf("failed dmigrating YAML store JSON store: %w", + return nil, fmt.Errorf("failed migrating YAML store JSON store: %w", err) } diff --git a/internal/pkg/fleetapi/action.go b/internal/pkg/fleetapi/action.go index c184cae6620..bcdfefa8f09 100644 --- a/internal/pkg/fleetapi/action.go +++ b/internal/pkg/fleetapi/action.go @@ -104,7 +104,6 @@ func NewAction(actionType string) Action { case ActionTypeDiagnostics: action = &ActionDiagnostics{} case ActionTypeInputAction: - // Only INPUT_ACTION type actions could possibly be signed https://github.com/elastic/elastic-agent/pull/2348 action = &ActionApp{} case ActionTypePolicyChange: action = &ActionPolicyChange{} From 4c8dfe7faa723f3540c217c05d22dcc47e447892 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 4 Apr 2024 15:43:35 +0200 Subject: [PATCH 16/39] move migration tests to its own file and add more tests --- .../agent/storage/store/migrations_test.go | 602 ++++++++++++++++++ .../agent/storage/store/state_store_test.go | 533 ---------------- 2 files changed, 602 insertions(+), 533 deletions(-) create mode 100644 internal/pkg/agent/storage/store/migrations_test.go diff --git a/internal/pkg/agent/storage/store/migrations_test.go b/internal/pkg/agent/storage/store/migrations_test.go new file mode 100644 index 00000000000..1550f5afb4c --- /dev/null +++ b/internal/pkg/agent/storage/store/migrations_test.go @@ -0,0 +1,602 @@ +package store + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/storage" + "github.com/elastic/elastic-agent/internal/pkg/fleetapi" + "github.com/elastic/elastic-agent/pkg/core/logger" +) + +func TestStoreMigrations(t *testing.T) { + t.Run("action store file does not exists", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + newStateStorePath := filepath.Join(tempDir, "state_store.yml") + + newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "migration action store -> state store failed") + + // to load from disk a new store needs to be created, it loads the + // file to memory during the store creation. + stateStore, err := NewStateStore(log, newStateStore) + require.NoError(t, err, "could not load state store") + + assert.Nil(t, stateStore.Action()) + assert.Empty(t, stateStore.Queue()) + }) + + t.Run("action store is empty", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + newStateStorePath := filepath.Join(tempDir, "state_store.yml") + + err := os.WriteFile(oldActionStorePath, []byte(""), 0600) + require.NoError(t, err, "could not create empty action store file") + + newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "migration action store -> state store failed") + + // to load from disk a new store needs to be created, it loads the + // file to memory during the store creation. + stateStore, err := NewStateStore(log, newStateStore) + require.NoError(t, err, "could not load state store") + + assert.Nil(t, stateStore.Action()) + assert.Empty(t, stateStore.Queue()) + }) + + t.Run("action store to YAML state store", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, // YAML unmarshaller unmarshals int as float + }, + }, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + goldenActionStore, err := os.ReadFile( + filepath.Join("testdata", "7.17.18-action_store.yml")) + require.NoError(t, err, "could not read action store golden file") + + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + err = os.WriteFile(oldActionStorePath, goldenActionStore, 0666) + require.NoError(t, err, "could not copy action store golden file") + + newStateStorePath := filepath.Join(tempDir, "state_store.yaml") + newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath, + storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "migration action store -> state store failed") + + // to load from disk a new store needs to be created, it loads the file + // to memory during the store creation. + stateStore, err := NewStateStore(log, newStateStore) + require.NoError(t, err, "could not create state store") + + got := stateStore.Action() + require.NotNil(t, got, "should have loaded an action") + + assert.Equalf(t, want, got, + "loaded action differs from action on the old action store") + assert.Empty(t, stateStore.Queue(), + "queue should be empty, old action store did not have a queue") + }) + + t.Run("YAML state store containing an ActionPolicyChange to JSON state store", + func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + }, + }, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + yamlStorePlain, err := os.ReadFile( + filepath.Join("testdata", "8.0.0-action_policy_change.yml")) + require.NoError(t, err, "could not read action store golden file") + + encDiskStorePath := filepath.Join(tempDir, "store.enc") + encDiskStore, err := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, + storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) + require.NoError(t, err, + "failed saving copy of golden files on an EncryptedDiskStore") + + err = migrateYAMLStateStoreToStateStoreV1(log, encDiskStore) + require.NoError(t, err, "YAML state store -> JSON state store failed") + + // Load migrated store from disk + stateStore, err := NewStateStore(log, encDiskStore) + require.NoError(t, err, "could not load store from disk") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("YAML state store containing an ActionUnenroll to JSON state store", + func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionUnenroll{ + ActionID: "abc123", + ActionType: "UNENROLL", + IsDetected: true, + Signed: nil, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + yamlStorePlain, err := os.ReadFile( + filepath.Join("testdata", "8.0.0-action_unenroll.yml")) + require.NoError(t, err, "could not read action store golden file") + + encDiskStorePath := filepath.Join(tempDir, "store.enc") + encDiskStore, err := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, + storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) + require.NoError(t, err, + "failed saving copy of golden files on an EncryptedDiskStore") + + err = migrateYAMLStateStoreToStateStoreV1(log, encDiskStore) + require.NoError(t, err, "YAML state store -> JSON state store failed") + + // Load migrated store from disk + stateStore, err := NewStateStore(log, encDiskStore) + require.NoError(t, err, "could not load store from disk") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("YAML state store when JSON state store exists", func(t *testing.T) { + log, _ := logger.NewTesting("") + + ctx := context.Background() + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + }, + }, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + stateStorePath := filepath.Join(tempDir, "store.enc") + endDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath, + storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + // Create and save a JSON state store + stateStore, err := NewStateStore(log, endDiskStore) + require.NoError(t, err, "could not create state store") + stateStore.SetAckToken(want.AckToken) + stateStore.SetAction(want.ActionSerializer.Action) + stateStore.SetQueue(want.Queue) + err = stateStore.Save() + require.NoError(t, err, "state store save filed") + + // Try to migrate an existing JSON store + err = migrateYAMLStateStoreToStateStoreV1(log, endDiskStore) + require.NoError(t, err, "YAML state store -> JSON state store failed") + + // Load migrated store from disk + stateStore, err = NewStateStore(log, endDiskStore) + require.NoError(t, err, "could not load store from disk") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("newStateStoreWithMigration", func(t *testing.T) { + t.Run("action store exists", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, // YAML unmarshaller unmarshals int as float + }, + }, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + goldenActionStore, err := os.ReadFile( + filepath.Join("testdata", "7.17.18-action_store.yml")) + require.NoError(t, err, "could not read action store golden file") + + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + err = os.WriteFile(oldActionStorePath, goldenActionStore, 0666) + require.NoError(t, err, "could not copy action store golden file") + + newStateStorePath := filepath.Join(tempDir, "state_store.yaml") + newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath, + storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + stateStore, err := newStateStoreWithMigration(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "newStateStoreWithMigration failed") + + got := stateStore.Action() + assert.Equalf(t, want, got, + "loaded action differs from action on the old action store") + assert.Empty(t, stateStore.Queue(), + "queue should be empty, old action store did not have a queue") + assert.NoFileExists(t, oldActionStorePath, + "old action store should have been deleted upon successful migration") + }) + + t.Run("YAML state store to JSON state store", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + }, + }, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + yamlStorePlain, err := os.ReadFile( + filepath.Join("testdata", "8.0.0-action_policy_change.yml")) + require.NoError(t, err, "could not read action store golden file") + + yamlStoreEncPath := filepath.Join(tempDir, "yaml_store.enc") + yamlStoreEnc, err := storage.NewEncryptedDiskStore(ctx, yamlStoreEncPath, + storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + err = yamlStoreEnc.Save(bytes.NewBuffer(yamlStorePlain)) + require.NoError(t, err, + "failed saving copy of golden files on an EncryptedDiskStore") + + stateStore, err := newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), yamlStoreEnc) + require.NoError(t, err, "newStateStoreWithMigration failed") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("up to date store, no migration needed", func(t *testing.T) { + log, _ := logger.NewTesting("") + + ctx := context.Background() + + want := state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "abc123", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]interface{}{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + }, + }, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + } + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + stateStorePath := filepath.Join(tempDir, "store.enc") + endDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath, + storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + // Create and save a JSON state store + stateStore, err := NewStateStore(log, endDiskStore) + require.NoError(t, err, "could not create state store") + stateStore.SetAckToken(want.AckToken) + stateStore.SetAction(want.ActionSerializer.Action) + stateStore.SetQueue(want.Queue) + err = stateStore.Save() + require.NoError(t, err, "state store save filed") + + stateStore, err = newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), endDiskStore) + require.NoError(t, err, "newStateStoreWithMigration failed") + + assert.Equal(t, want, stateStore.state) + }) + + t.Run("no store exists", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + paths.SetConfig(tempDir) + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + stateStorePath := filepath.Join(tempDir, "store.enc") + endDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath, + storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + got, err := newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), endDiskStore) + require.NoError(t, err, "newStateStoreWithMigration failed") + + assert.Nil(t, got.Action(), + "no action should have been loaded") + assert.Empty(t, got.Queue(), "action queue should be empty") + assert.Empty(t, got.AckToken(), + "no AckToken should have been loaded") + }) + }) + + t.Run("NewStateStoreWithMigration", func(t *testing.T) { + t.Run("return error if action store is invalid", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + newStateStorePath := filepath.Join(tempDir, "state_store.enc") + + err := os.WriteFile(oldActionStorePath, []byte("&"), 0600) + require.NoError(t, err, "could not create empty action store file") + + s, err := NewStateStoreWithMigration(ctx, log, oldActionStorePath, newStateStorePath) + + assert.Error(t, err, "when the action store migration fails, it should return an error") + assert.FileExists(t, oldActionStorePath, "invalid action store should NOT have been deleted") + assert.Nil(t, s, "state store should be nil when an error is returned") + }) + + t.Run("returns error if YAML state store is invalid", func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + paths.SetConfig(tempDir) + createAgentVaultAndSecret(t, ctx, tempDir) + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + newStateStorePath := filepath.Join(tempDir, "state_store.enc") + + err := os.WriteFile(newStateStorePath, []byte("&"), 0600) + require.NoError(t, err, "could not create empty action store file") + + s, err := NewStateStoreWithMigration(ctx, log, oldActionStorePath, newStateStorePath) + assert.ErrorContains(t, err, "failed migrating YAML store JSON store") + assert.Nil(t, s, "state store should be nil when an error ir returned") + }) + + t.Run("returns error if state store V1 (JSON) is invalid", func(t *testing.T) { + // As YAML 1.2 is a superset of JSON, the migration code checks first + // if the content is a valid JSON, if it's, no migration happens. + // If the content is invalid, then it tries to migrate from the YAML store. + // Therefore, the error is regarding invalid YAML and not invalid JSON. + + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + paths.SetConfig(tempDir) + createAgentVaultAndSecret(t, ctx, tempDir) + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + newStateStorePath := filepath.Join(tempDir, "state_store.enc") + + err := os.WriteFile(newStateStorePath, []byte("}"), 0600) + require.NoError(t, err, "could not create empty action store file") + + s, err := NewStateStoreWithMigration(ctx, log, oldActionStorePath, newStateStorePath) + assert.ErrorContains(t, err, "failed migrating YAML store JSON store", + "As YAML 1.2 is a superset of JSON, the migration code checks first if the content is a valid JSON, if it's, no migration happens.\nIf the content is invalid, then it tries to migrate from the YAML store.\nTherefore, the error is regarding invalid YAML and not invalid JSON.") + assert.Nil(t, s, "state store should be nil when an error ir returned") + }) + }) +} diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 17778bd2267..6bec7a6fcd5 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -5,12 +5,10 @@ package store import ( - "bytes" "context" "os" "path/filepath" "reflect" - "runtime" "sync" "testing" "time" @@ -40,537 +38,6 @@ func TestStateStore(t *testing.T) { t.Run("no ack token", func(t *testing.T) { runTestStateStore(t, "") }) - - t.Run("migrate", func(t *testing.T) { - if runtime.GOOS == "darwin" { - // the original test never actually run, so with this at least - // there is coverage for linux and windows. - t.Skipf("needs https://github.com/elastic/elastic-agent/issues/3866" + - "to be merged so this test can work on darwin") - } - - t.Run("action store file does not exists", func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - tempDir := t.TempDir() - oldActionStorePath := filepath.Join(tempDir, "action_store.yml") - newStateStorePath := filepath.Join(tempDir, "state_store.yml") - - newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) - require.NoError(t, err, "migration action store -> state store failed") - - // to load from disk a new store needs to be created, it loads the - // file to memory during the store creation. - stateStore, err := NewStateStore(log, newStateStore) - require.NoError(t, err, "could not load state store") - - assert.Nil(t, stateStore.Action()) - assert.Empty(t, stateStore.Queue()) - }) - - t.Run("action store is empty", func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - tempDir := t.TempDir() - oldActionStorePath := filepath.Join(tempDir, "action_store.yml") - newStateStorePath := filepath.Join(tempDir, "state_store.yml") - - err := os.WriteFile(oldActionStorePath, []byte(""), 0600) - require.NoError(t, err, "could not create empty action store file") - - newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) - require.NoError(t, err, "migration action store -> state store failed") - - // to load from disk a new store needs to be created, it loads the - // file to memory during the store creation. - stateStore, err := NewStateStore(log, newStateStore) - require.NoError(t, err, "could not load state store") - - assert.Nil(t, stateStore.Action()) - assert.Empty(t, stateStore.Queue()) - }) - - t.Run("action store to YAML state store", func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - want := &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ - "hello": "world", - "phi": 1.618, - "answer": 42.0, // YAML unmarshaller unmarshals int as float - }, - }, - } - - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - - goldenActionStore, err := os.ReadFile( - filepath.Join("testdata", "7.17.18-action_store.yml")) - require.NoError(t, err, "could not read action store golden file") - - oldActionStorePath := filepath.Join(tempDir, "action_store.yml") - err = os.WriteFile(oldActionStorePath, goldenActionStore, 0666) - require.NoError(t, err, "could not copy action store golden file") - - newStateStorePath := filepath.Join(tempDir, "state_store.yaml") - newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) - require.NoError(t, err, "migration action store -> state store failed") - - // to load from disk a new store needs to be created, it loads the file - // to memory during the store creation. - newStateStore, err = storage.NewEncryptedDiskStore(ctx, newStateStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - stateStore, err := NewStateStore(log, newStateStore) - require.NoError(t, err, "could not create state store") - - got := stateStore.Action() - require.NotNil(t, got, "should have loaded an action") - - assert.Equalf(t, want, got, - "loaded action differs from action on the old action store") - assert.Empty(t, stateStore.Queue(), - "queue should be empty, old action store did not have a queue") - }) - - t.Run("YAML state store containing an ActionPolicyChange to JSON state store", - func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - want := state{ - Version: "1", - ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ - "hello": "world", - "phi": 1.618, - "answer": 42.0, - }, - }, - }}, - AckToken: "czlV93YBwdkt5lYhBY7S", - Queue: actionQueue{&fleetapi.ActionUpgrade{ - ActionID: "action1", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }, - &fleetapi.ActionUpgrade{ - ActionID: "action2", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }}, - } - - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - - yamlStorePlain, err := os.ReadFile( - filepath.Join("testdata", "8.0.0-action_policy_change.yml")) - require.NoError(t, err, "could not read action store golden file") - - encDiskStorePath := filepath.Join(tempDir, "store.enc") - encDiskStore, err := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) - require.NoError(t, err, - "failed saving copy of golden files on an EncryptedDiskStore") - - err = migrateYAMLStateStoreToStateStoreV1(log, encDiskStore) - require.NoError(t, err, "YAML state store -> JSON state store failed") - - // Load migrated store from disk - stateStore, err := NewStateStore(log, encDiskStore) - require.NoError(t, err, "could not load store from disk") - - assert.Equal(t, want, stateStore.state) - }) - - t.Run("YAML state store containing an ActionUnenroll to JSON state store", - func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - want := state{ - Version: "1", - ActionSerializer: actionSerializer{Action: &fleetapi.ActionUnenroll{ - ActionID: "abc123", - ActionType: "UNENROLL", - IsDetected: true, - Signed: nil, - }}, - AckToken: "czlV93YBwdkt5lYhBY7S", - Queue: actionQueue{&fleetapi.ActionUpgrade{ - ActionID: "action1", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }, - &fleetapi.ActionUpgrade{ - ActionID: "action2", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }}, - } - - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - - yamlStorePlain, err := os.ReadFile( - filepath.Join("testdata", "8.0.0-action_unenroll.yml")) - require.NoError(t, err, "could not read action store golden file") - - encDiskStorePath := filepath.Join(tempDir, "store.enc") - encDiskStore, err := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) - require.NoError(t, err, - "failed saving copy of golden files on an EncryptedDiskStore") - - err = migrateYAMLStateStoreToStateStoreV1(log, encDiskStore) - require.NoError(t, err, "YAML state store -> JSON state store failed") - - // Load migrated store from disk - stateStore, err := NewStateStore(log, encDiskStore) - require.NoError(t, err, "could not load store from disk") - - assert.Equal(t, want, stateStore.state) - }) - - t.Run("YAML state store when JSON state store exists", func(t *testing.T) { - log, _ := logger.NewTesting("") - - ctx := context.Background() - - want := state{ - Version: "1", - ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ - "hello": "world", - "phi": 1.618, - "answer": 42.0, - }, - }, - }}, - AckToken: "czlV93YBwdkt5lYhBY7S", - Queue: actionQueue{&fleetapi.ActionUpgrade{ - ActionID: "action1", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }, - &fleetapi.ActionUpgrade{ - ActionID: "action2", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }}, - } - - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - - stateStorePath := filepath.Join(tempDir, "store.enc") - endDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - // Create and save a JSON state store - stateStore, err := NewStateStore(log, endDiskStore) - require.NoError(t, err, "could not create state store") - stateStore.SetAckToken(want.AckToken) - stateStore.SetAction(want.ActionSerializer.Action) - stateStore.SetQueue(want.Queue) - err = stateStore.Save() - require.NoError(t, err, "state store save filed") - - // Try to migrate an existing JSON store - err = migrateYAMLStateStoreToStateStoreV1(log, endDiskStore) - require.NoError(t, err, "YAML state store -> JSON state store failed") - - // Load migrated store from disk - stateStore, err = NewStateStore(log, endDiskStore) - require.NoError(t, err, "could not load store from disk") - - assert.Equal(t, want, stateStore.state) - }) - - t.Run("NewStateStoreWithMigration", func(t *testing.T) { - t.Run("action store exists", func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - want := &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ - "hello": "world", - "phi": 1.618, - "answer": 42.0, // YAML unmarshaller unmarshals int as float - }, - }, - } - - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - - goldenActionStore, err := os.ReadFile( - filepath.Join("testdata", "7.17.18-action_store.yml")) - require.NoError(t, err, "could not read action store golden file") - - oldActionStorePath := filepath.Join(tempDir, "action_store.yml") - err = os.WriteFile(oldActionStorePath, goldenActionStore, 0666) - require.NoError(t, err, "could not copy action store golden file") - - newStateStorePath := filepath.Join(tempDir, "state_store.yaml") - newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - stateStore, err := newStateStoreWithMigration(log, oldActionStorePath, newStateStore) - require.NoError(t, err, "newStateStoreWithMigration failed") - - got := stateStore.Action() - assert.Equalf(t, want, got, - "loaded action differs from action on the old action store") - assert.Empty(t, stateStore.Queue(), - "queue should be empty, old action store did not have a queue") - }) - - t.Run("YAML state store to JSON state store", func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - want := state{ - Version: "1", - ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ - "hello": "world", - "phi": 1.618, - "answer": 42.0, - }, - }, - }}, - AckToken: "czlV93YBwdkt5lYhBY7S", - Queue: actionQueue{&fleetapi.ActionUpgrade{ - ActionID: "action1", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }, - &fleetapi.ActionUpgrade{ - ActionID: "action2", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }}, - } - - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - - yamlStorePlain, err := os.ReadFile( - filepath.Join("testdata", "8.0.0-action_policy_change.yml")) - require.NoError(t, err, "could not read action store golden file") - - yamlStoreEncPath := filepath.Join(tempDir, "yaml_store.enc") - yamlStoreEnc, err := storage.NewEncryptedDiskStore(ctx, yamlStoreEncPath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - err = yamlStoreEnc.Save(bytes.NewBuffer(yamlStorePlain)) - require.NoError(t, err, - "failed saving copy of golden files on an EncryptedDiskStore") - - stateStore, err := newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), yamlStoreEnc) - require.NoError(t, err, "newStateStoreWithMigration failed") - - assert.Equal(t, want, stateStore.state) - }) - - t.Run("up to date store, no migration needed", func(t *testing.T) { - log, _ := logger.NewTesting("") - - ctx := context.Background() - - want := state{ - Version: "1", - ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ - "hello": "world", - "phi": 1.618, - "answer": 42.0, - }, - }, - }}, - AckToken: "czlV93YBwdkt5lYhBY7S", - Queue: actionQueue{&fleetapi.ActionUpgrade{ - ActionID: "action1", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }, - &fleetapi.ActionUpgrade{ - ActionID: "action2", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, - }}, - } - - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - - stateStorePath := filepath.Join(tempDir, "store.enc") - endDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - // Create and save a JSON state store - stateStore, err := NewStateStore(log, endDiskStore) - require.NoError(t, err, "could not create state store") - stateStore.SetAckToken(want.AckToken) - stateStore.SetAction(want.ActionSerializer.Action) - stateStore.SetQueue(want.Queue) - err = stateStore.Save() - require.NoError(t, err, "state store save filed") - - stateStore, err = newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), endDiskStore) - require.NoError(t, err, "newStateStoreWithMigration failed") - - assert.Equal(t, want, stateStore.state) - }) - - t.Run("no store exists", func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - - stateStorePath := filepath.Join(tempDir, "store.enc") - endDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - got, err := newStateStoreWithMigration(log, filepath.Join(tempDir, "non-existing-action-store.yaml"), endDiskStore) - require.NoError(t, err, "newStateStoreWithMigration failed") - - assert.Nil(t, got.Action(), - "no action should have been loaded") - assert.Empty(t, got.Queue(), "action queue should be empty") - assert.Empty(t, got.AckToken(), - "no AckToken should have been loaded") - }) - }) - }) } func createAgentVaultAndSecret(t *testing.T, ctx context.Context, tempDir string) string { From 6cdad7173716266a698e3bf95aea73318322f7c1 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 5 Apr 2024 07:18:03 +0200 Subject: [PATCH 17/39] use unprivileged vault --- internal/pkg/agent/storage/store/state_store_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 6bec7a6fcd5..8692e18ff2a 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -47,7 +47,9 @@ func createAgentVaultAndSecret(t *testing.T, ctx context.Context, tempDir string require.NoError(t, err, "could not create directory for the agent's vault") - _, err = vault.New(ctx, vault.WithVaultPath(vaultPath)) + _, err = vault.New(ctx, + vault.WithVaultPath(vaultPath), + vault.WithUnprivileged(true)) require.NoError(t, err, "could not create agent's vault") err = secret.CreateAgentSecret( context.Background(), vault.WithVaultPath(vaultPath)) From f89a3ab77f1495fcae8c3312be5766e847ec293f Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 5 Apr 2024 07:32:24 +0200 Subject: [PATCH 18/39] another mac fix --- internal/pkg/agent/storage/store/state_store_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 8692e18ff2a..2454e70eae5 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -52,7 +52,9 @@ func createAgentVaultAndSecret(t *testing.T, ctx context.Context, tempDir string vault.WithUnprivileged(true)) require.NoError(t, err, "could not create agent's vault") err = secret.CreateAgentSecret( - context.Background(), vault.WithVaultPath(vaultPath)) + context.Background(), + vault.WithVaultPath(vaultPath), + vault.WithUnprivileged(true)) require.NoError(t, err, "could not create agent secret") return vaultPath From ce05de046c752c0c8792f653e9d55852ed380101 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 5 Apr 2024 07:33:40 +0200 Subject: [PATCH 19/39] remove debug log --- internal/pkg/agent/storage/store/state_store_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 2454e70eae5..7c5415c7c06 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -441,7 +441,6 @@ func runTestStateStore(t *testing.T, ackToken string) { }, } - t.Logf("state store: %q", storePath) s, err := storage.NewDiskStore(storePath) require.NoError(t, err, "failed creating DiskStore") From 177693d42a09ba747382cf292d1210baf8c200f6 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 5 Apr 2024 14:15:28 +0200 Subject: [PATCH 20/39] fix typo and comment --- internal/pkg/agent/storage/store/state_store.go | 2 -- internal/pkg/queue/actionqueue.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index e84c83bf27e..304bc22a282 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -193,8 +193,6 @@ func (s *StateStore) SetAckToken(ackToken string) { } // SetQueue sets the action_queue to agent state -// TODO: receive only scheduled actions. It might break something. Needs to -// investigate it better. func (s *StateStore) SetQueue(q []fleetapi.ScheduledAction) { s.mx.Lock() defer s.mx.Unlock() diff --git a/internal/pkg/queue/actionqueue.go b/internal/pkg/queue/actionqueue.go index 897448221e6..8d94c52bb39 100644 --- a/internal/pkg/queue/actionqueue.go +++ b/internal/pkg/queue/actionqueue.go @@ -144,7 +144,7 @@ func (q *ActionQueue) Cancel(actionID string) int { return len(items) } -// Actions returns all actions in the queue, item 0 is garunteed to be the min, the rest may not be in sorted order. +// Actions returns all actions in the queue, item 0 is guaranteed to be the min, the rest may not be in sorted order. func (q *ActionQueue) Actions() []fleetapi.ScheduledAction { actions := make([]fleetapi.ScheduledAction, q.q.Len()) for i, item := range *q.q { From 4f05e9755c4968b6c9f4de48f81103810358a00f Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 5 Apr 2024 15:55:46 +0200 Subject: [PATCH 21/39] add notice --- internal/pkg/agent/storage/store/migrations_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/pkg/agent/storage/store/migrations_test.go b/internal/pkg/agent/storage/store/migrations_test.go index 1550f5afb4c..8e02f577da5 100644 --- a/internal/pkg/agent/storage/store/migrations_test.go +++ b/internal/pkg/agent/storage/store/migrations_test.go @@ -1,3 +1,7 @@ +// 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 ( From c215ade80c72edbbb8300d682c21f7d5f670a2a1 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Wed, 10 Apr 2024 08:24:18 +0200 Subject: [PATCH 22/39] fix and add test for JSON nested map marshalling --- .../pkg/agent/application/managed_mode.go | 2 +- .../pkg/agent/storage/store/migrations.go | 7 +- .../agent/storage/store/migrations_test.go | 92 +++++++++++++++++-- .../store/testdata/7.17.18-action_store.yml | 7 ++ .../testdata/8.0.0-action_policy_change.yml | 11 ++- 5 files changed, 104 insertions(+), 15 deletions(-) diff --git a/internal/pkg/agent/application/managed_mode.go b/internal/pkg/agent/application/managed_mode.go index f940641c0d1..7251f2592a0 100644 --- a/internal/pkg/agent/application/managed_mode.go +++ b/internal/pkg/agent/application/managed_mode.go @@ -78,7 +78,7 @@ func newManagedConfigManager( // Create the state store that will persist the last good policy change on disk. stateStore, err := store.NewStateStoreWithMigration(ctx, log, paths.AgentActionStoreFile(), paths.AgentStateStoreFile()) if err != nil { - return nil, errors.New(err, fmt.Sprintf("fail to read action store '%s'", paths.AgentActionStoreFile())) + return nil, errors.New(err, fmt.Sprintf("fail to read state store '%s'", paths.AgentStateStoreFile())) } actionQueue, err := queue.NewActionQueue(stateStore.Queue(), stateStore) diff --git a/internal/pkg/agent/storage/store/migrations.go b/internal/pkg/agent/storage/store/migrations.go index e787d3c1b7c..6ea798b2e91 100644 --- a/internal/pkg/agent/storage/store/migrations.go +++ b/internal/pkg/agent/storage/store/migrations.go @@ -11,6 +11,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/storage" "github.com/elastic/elastic-agent/internal/pkg/agent/storage/store/internal/migrations" + "github.com/elastic/elastic-agent/internal/pkg/conv" "github.com/elastic/elastic-agent/internal/pkg/fleetapi" "github.com/elastic/elastic-agent/pkg/core/logger" ) @@ -93,7 +94,9 @@ func migrateActionStoreToStateStore( stateStore.SetAction(&fleetapi.ActionPolicyChange{ ActionID: action.ActionID, ActionType: action.Type, - Data: fleetapi.ActionPolicyChangeData{Policy: action.Policy}, + Data: fleetapi.ActionPolicyChangeData{ + Policy: conv.YAMLMapToJSONMap(action.Policy), + }, }) err = stateStore.Save() @@ -151,7 +154,7 @@ func migrateYAMLStateStoreToStateStoreV1(log *logger.Logger, store storage.Stora ActionID: yamlStore.Action.ActionID, ActionType: yamlStore.Action.Type, Data: fleetapi.ActionPolicyChangeData{ - Policy: yamlStore.Action.Policy}, + Policy: conv.YAMLMapToJSONMap(yamlStore.Action.Policy)}, } case fleetapi.ActionTypeUnenroll: action = &fleetapi.ActionUnenroll{ diff --git a/internal/pkg/agent/storage/store/migrations_test.go b/internal/pkg/agent/storage/store/migrations_test.go index 8e02f577da5..6fa537289d9 100644 --- a/internal/pkg/agent/storage/store/migrations_test.go +++ b/internal/pkg/agent/storage/store/migrations_test.go @@ -78,10 +78,22 @@ func TestStoreMigrations(t *testing.T) { ActionID: "abc123", ActionType: "POLICY_CHANGE", Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ + Policy: map[string]any{ "hello": "world", "phi": 1.618, - "answer": 42.0, // YAML unmarshaller unmarshals int as float + "answer": 42.0, + "a_map": []any{ + map[string]any{ + "nested_map1": map[string]any{ + "nested_map1_key1": "value1", + "nested_map1_key2": "value2", + }}, + map[string]any{ + "nested_map2": map[string]any{ + "nested_map2_key1": "value1", + "nested_map2_key2": "value2", + }}, + }, }, }, } @@ -127,13 +139,25 @@ func TestStoreMigrations(t *testing.T) { want := state{ Version: "1", ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ - ActionID: "abc123", + ActionID: "policy:POLICY-ID:1:1", ActionType: "POLICY_CHANGE", Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ + Policy: map[string]any{ "hello": "world", "phi": 1.618, "answer": 42.0, + "a_map": []any{ + map[string]any{ + "nested_map1": map[string]any{ + "nested_map1_key1": "value1", + "nested_map1_key2": "value2", + }}, + map[string]any{ + "nested_map2": map[string]any{ + "nested_map2_key1": "value1", + "nested_map2_key2": "value2", + }}, + }, }, }, }}, @@ -271,10 +295,22 @@ func TestStoreMigrations(t *testing.T) { ActionID: "abc123", ActionType: "POLICY_CHANGE", Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ + Policy: map[string]any{ "hello": "world", "phi": 1.618, "answer": 42.0, + "a_map": []any{ + map[string]any{ + "nested_map1": map[string]any{ + "nested_map1_key1": "value1", + "nested_map1_key2": "value2", + }}, + map[string]any{ + "nested_map2": map[string]any{ + "nested_map2_key1": "value1", + "nested_map2_key2": "value2", + }}, + }, }, }, }}, @@ -344,10 +380,22 @@ func TestStoreMigrations(t *testing.T) { ActionID: "abc123", ActionType: "POLICY_CHANGE", Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ + Policy: map[string]any{ "hello": "world", "phi": 1.618, - "answer": 42.0, // YAML unmarshaller unmarshals int as float + "answer": 42.0, + "a_map": []any{ + map[string]any{ + "nested_map1": map[string]any{ + "nested_map1_key1": "value1", + "nested_map1_key2": "value2", + }}, + map[string]any{ + "nested_map2": map[string]any{ + "nested_map2_key1": "value1", + "nested_map2_key2": "value2", + }}, + }, }, }, } @@ -387,13 +435,25 @@ func TestStoreMigrations(t *testing.T) { want := state{ Version: "1", ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ - ActionID: "abc123", + ActionID: "policy:POLICY-ID:1:1", ActionType: "POLICY_CHANGE", Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ + Policy: map[string]any{ "hello": "world", "phi": 1.618, "answer": 42.0, + "a_map": []any{ + map[string]any{ + "nested_map1": map[string]any{ + "nested_map1_key1": "value1", + "nested_map1_key2": "value2", + }}, + map[string]any{ + "nested_map2": map[string]any{ + "nested_map2_key1": "value1", + "nested_map2_key2": "value2", + }}, + }, }, }, }}, @@ -459,10 +519,22 @@ func TestStoreMigrations(t *testing.T) { ActionID: "abc123", ActionType: "POLICY_CHANGE", Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]interface{}{ + Policy: map[string]any{ "hello": "world", "phi": 1.618, "answer": 42.0, + "a_map": []any{ + map[string]any{ + "nested_map1": map[string]any{ + "nested_map1_key1": "value1", + "nested_map1_key2": "value2", + }}, + map[string]any{ + "nested_map2": map[string]any{ + "nested_map2_key1": "value1", + "nested_map2_key2": "value2", + }}, + }, }, }, }}, diff --git a/internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml b/internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml index 8f559ec80d9..4af89837171 100644 --- a/internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml +++ b/internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml @@ -4,3 +4,10 @@ policy: answer: 42 hello: world phi: 1.618 + a_map: + - nested_map1: + nested_map1_key1: value1 + nested_map1_key2: value2 + - nested_map2: + nested_map2_key1: value1 + nested_map2_key2: value2 diff --git a/internal/pkg/agent/storage/store/testdata/8.0.0-action_policy_change.yml b/internal/pkg/agent/storage/store/testdata/8.0.0-action_policy_change.yml index dc4525ae47f..b87bf2cfa58 100644 --- a/internal/pkg/agent/storage/store/testdata/8.0.0-action_policy_change.yml +++ b/internal/pkg/agent/storage/store/testdata/8.0.0-action_policy_change.yml @@ -1,11 +1,18 @@ +ack_token: czlV93YBwdkt5lYhBY7S action: - action_id: abc123 + action_id: policy:POLICY-ID:1:1 action_type: POLICY_CHANGE policy: answer: 42 hello: world phi: 1.618 -ack_token: czlV93YBwdkt5lYhBY7S + a_map: + - nested_map1: + nested_map1_key1: value1 + nested_map1_key2: value2 + - nested_map2: + nested_map2_key1: value1 + nested_map2_key2: value2 action_queue: - action_id: action1 type: UPGRADE From 469c793ea4abe312ae59cc7146403846cd3354cf Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 11 Jun 2024 10:48:59 +0200 Subject: [PATCH 23/39] fix after merging main --- .../application/actions/handlers/handler_action_settings.go | 2 +- internal/pkg/agent/storage/store/state_store.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_settings.go b/internal/pkg/agent/application/actions/handlers/handler_action_settings.go index 4c4c4c6a827..d2f396c5a60 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_settings.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_settings.go @@ -47,7 +47,7 @@ func (h *Settings) Handle(ctx context.Context, a fleetapi.Action, acker acker.Ac return fmt.Errorf("invalid type, expected ActionSettings and received %T", a) } - logLevel := action.LogLevel + logLevel := action.Data.LogLevel return h.handleLogLevel(ctx, logLevel, acker, action) } diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index 304bc22a282..fd1d8c9445a 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -74,8 +74,10 @@ func NewStateStoreWithMigration( ctx context.Context, log *logger.Logger, actionStorePath, - stateStorePath string) (*StateStore, error) { - stateDiskStore, err := storage.NewEncryptedDiskStore(ctx, stateStorePath) + 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", From 03116c0f16439353fd7c0535eaa48ffcc6da66a2 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 11 Jun 2024 12:46:20 +0200 Subject: [PATCH 24/39] fix after merge --- .../actions/handlers/handler_action_diagnostics.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics.go b/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics.go index 7c12d7fc64a..76f469eb236 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_diagnostics.go @@ -148,10 +148,10 @@ func (h *Diagnostics) collectDiag(ctx context.Context, action *fleetapi.ActionDi cDiag := h.diagComponents(ctx, action) var r io.Reader - // attempt to create the a temporary diagnostics file on disk in order to avoid loading a - // potentially large file in memory. + // attempt to create a temporary diagnostics file on disk in order to avoid + // loading a potentially large file in memory. // if on-disk creation fails an in-memory buffer is used. - f, s, err := h.diagFile(aDiag, uDiag, cDiag, action.ExcludeEventsLog) + f, s, err := h.diagFile(aDiag, uDiag, cDiag, action.Data.ExcludeEventsLog) if err != nil { var b bytes.Buffer h.log.Warnw("Diagnostics action unable to use temporary file, using buffer instead.", "error.message", err) @@ -161,7 +161,7 @@ func (h *Diagnostics) collectDiag(ctx context.Context, action *fleetapi.ActionDi h.log.Warn(str) } }() - err := diagnostics.ZipArchive(&wBuf, &b, h.topPath, aDiag, uDiag, cDiag, action.ExcludeEventsLog) + err := diagnostics.ZipArchive(&wBuf, &b, h.topPath, aDiag, uDiag, cDiag, action.Data.ExcludeEventsLog) if err != nil { h.log.Errorw( "diagnostics action handler failed generate zip archive", From ec9874f78594cc6cdf55feba7dc21b2467fd4242 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 11 Jun 2024 16:50:50 +0200 Subject: [PATCH 25/39] fix after merge --- .../actions/handlers/handler_action_settings_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_settings_test.go b/internal/pkg/agent/application/actions/handlers/handler_action_settings_test.go index 0d8fee3b61b..0d8d6e1444e 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_settings_test.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_settings_test.go @@ -133,7 +133,7 @@ func TestSettings_handleLogLevel(t *testing.T) { action: &fleetapi.ActionSettings{ ActionID: "someactionid", ActionType: fleetapi.ActionTypeSettings, - LogLevel: "debug", + Data: fleetapi.ActionSettingsData{LogLevel: "debug"}, }, }, setupMocks: func(t *testing.T, agent *mockinfo.Agent, setter *mockhandlers.LogLevelSetter, acker *mockfleetacker.Acker) { @@ -154,7 +154,8 @@ func TestSettings_handleLogLevel(t *testing.T) { action: &fleetapi.ActionSettings{ ActionID: "someactionid", ActionType: fleetapi.ActionTypeSettings, - LogLevel: clearLogLevelValue, + Data: fleetapi.ActionSettingsData{ + LogLevel: clearLogLevelValue}, }, }, setupMocks: func(t *testing.T, agent *mockinfo.Agent, setter *mockhandlers.LogLevelSetter, acker *mockfleetacker.Acker) { @@ -175,7 +176,8 @@ func TestSettings_handleLogLevel(t *testing.T) { action: &fleetapi.ActionSettings{ ActionID: "someactionid", ActionType: fleetapi.ActionTypeSettings, - LogLevel: clearLogLevelValue, + Data: fleetapi.ActionSettingsData{ + LogLevel: clearLogLevelValue}, }, }, setupMocks: func(t *testing.T, agent *mockinfo.Agent, setter *mockhandlers.LogLevelSetter, acker *mockfleetacker.Acker) { From 97093aab45150fd3e6a10342c369b3cfd63e5271 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 11 Jun 2024 19:21:31 +0200 Subject: [PATCH 26/39] adjust changelog --- changelog/fragments/1712067343-fix-state-store.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/fragments/1712067343-fix-state-store.yaml b/changelog/fragments/1712067343-fix-state-store.yaml index 58bf1ae0eba..e56527defb0 100644 --- a/changelog/fragments/1712067343-fix-state-store.yaml +++ b/changelog/fragments/1712067343-fix-state-store.yaml @@ -8,7 +8,7 @@ # - security: impacts on the security of a product or a user’s deployment. # - upgrade: important information for someone upgrading from a prior version # - other: does not fit into any of the other categories -kind: feature +kind: bug-fix # Change summary; a 80ish characters long description of the change. summary: Fix the Elastic Agent state store @@ -19,7 +19,7 @@ summary: Fix the Elastic Agent state store description: | This change fixes issues when loading data from the Agent's internal state store. Which include the error `error parsing version ""` the Agent would present after - when trying to execute a scheduled upgrade after a restart. + trying to execute a scheduled upgrade after a restart. # Affected component; a word indicating the component this changeset affects. component: From cad7c9e93f7e73e1c37b5b10232e8374a83c3afc Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Tue, 11 Jun 2024 19:21:44 +0200 Subject: [PATCH 27/39] remove unnecessary empty line --- internal/pkg/fleetapi/ack_cmd_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/pkg/fleetapi/ack_cmd_test.go b/internal/pkg/fleetapi/ack_cmd_test.go index 27a26823e5f..0f80e10d4ee 100644 --- a/internal/pkg/fleetapi/ack_cmd_test.go +++ b/internal/pkg/fleetapi/ack_cmd_test.go @@ -51,7 +51,6 @@ func TestAck(t *testing.T) { action := &ActionPolicyChange{ ActionID: "my-id", ActionType: "POLICY_CHANGE", - Data: struct { Policy map[string]interface{} `json:"policy" yaml:"policy,omitempty"` }{Policy: map[string]interface{}{ From 1c3fc9a76c7c0e93d360025440b55a21e6acd462 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 13 Jun 2024 10:12:20 +0200 Subject: [PATCH 28/39] fix after merge --- testing/fleetservertest/fleetserver_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/fleetservertest/fleetserver_test.go b/testing/fleetservertest/fleetserver_test.go index e98e1945f73..cc5868bb314 100644 --- a/testing/fleetservertest/fleetserver_test.go +++ b/testing/fleetservertest/fleetserver_test.go @@ -209,7 +209,7 @@ func ExampleNewServer_checkin_fleetConnectionParams() { fmt.Println(got.Actions) if len(got.Actions) > 0 { - policy := got.Actions[0].(*fleetapi.ActionPolicyChange).Policy + policy := got.Actions[0].(*fleetapi.ActionPolicyChange).Data.Policy b := new(strings.Builder) encoder := json.NewEncoder(b) encoder.SetIndent("", " ") From c84451a98660560a68a134072581a1120548ab14 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Mon, 17 Jun 2024 14:57:20 +0200 Subject: [PATCH 29/39] fix another merge issue --- testing/fleetservertest/fleetserver_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/fleetservertest/fleetserver_test.go b/testing/fleetservertest/fleetserver_test.go index cc5868bb314..f13de667515 100644 --- a/testing/fleetservertest/fleetserver_test.go +++ b/testing/fleetservertest/fleetserver_test.go @@ -221,7 +221,7 @@ func ExampleNewServer_checkin_fleetConnectionParams() { } // Output: - // [action_id: anActionID, type: POLICY_CHANGE] + // [id: anActionID, type: POLICY_CHANGE] // { // "agent": { // "download": { From a305fd4e9a0771aad3655aaaed426495b8acde3b Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 20 Jun 2024 19:24:11 +0200 Subject: [PATCH 30/39] improve docs/comments --- internal/pkg/agent/storage/store/state_store.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index fd1d8c9445a..6471a0728b1 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -31,13 +31,12 @@ type saveLoader interface { 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. +// StateStore stores the agent state: +// - the last fleet action (not all actions are stored, refer to Save for details) +// - a queue of scheduled actions +// - the ack token +// +// See each method documentation for details. type StateStore struct { log *logger.Logger store saveLoader @@ -141,7 +140,7 @@ func NewStateStore(log *logger.Logger, store saveLoader) (*StateStore, error) { } // readState parsed the content from reader as JSON to state. -// It's mostly to abstract the parsing of the date so different functions can +// It's mostly to abstract the parsing of the data so different functions can // reuse this. func readState(reader io.ReadCloser) (state, error) { st := state{} @@ -171,6 +170,8 @@ func (s *StateStore) SetAction(a fleetapi.Action) { defer s.mx.Unlock() switch v := a.(type) { + // If any new action type is added, don't forget to update the method's + // description. case *fleetapi.ActionPolicyChange, *fleetapi.ActionUnenroll: // Only persist the action if the action is different. if s.state.ActionSerializer.Action != nil && From e949aa193534495492ad1a2a32fb50ad4e2c1d93 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 20 Jun 2024 19:24:49 +0200 Subject: [PATCH 31/39] fix store dirty state being cleaned on failed save and add tests --- .../pkg/agent/storage/store/state_store.go | 13 +++++--- .../agent/storage/store/state_store_test.go | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index 6471a0728b1..3a5c7ef9b73 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -203,12 +203,17 @@ func (s *StateStore) SetQueue(q []fleetapi.ScheduledAction) { s.dirty = true } -// Save saves the actions into a state store. -func (s *StateStore) Save() error { +// Save saves the actions into the state store. If the action type is not +// supported or if any error happens, it returns a non-nil error. +func (s *StateStore) Save() (err error) { s.mx.Lock() defer s.mx.Unlock() - defer func() { s.dirty = false }() + defer func() { + if err == nil { + s.dirty = false + } + }() if !s.dirty { return nil } @@ -225,7 +230,7 @@ func (s *StateStore) Save() error { "ActionUnenroll or nil, but received %T", a) } - reader, err := jsonToReader(&s.state) + reader, err = jsonToReader(&s.state) if err != nil { return err } diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 7c5415c7c06..96f88ef460d 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -63,6 +63,39 @@ func createAgentVaultAndSecret(t *testing.T, ctx context.Context, tempDir string func runTestStateStore(t *testing.T, ackToken string) { log, _ := logger.New("state_store", false) + t.Run("store is not dirty on successful save", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.json") + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + + store, err := NewStateStore(log, s) + require.NoError(t, err) + + store.dirty = true + err = store.Save() + require.NoError(t, err, "unexpected error when saving") + + assert.False(t, store.dirty, + "the store should not be marked as dirty") + }) + + t.Run("store is dirty when save fails", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.json") + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + + store, err := NewStateStore(log, s) + require.NoError(t, err) + + store.dirty = true + store.state.ActionSerializer.Action = fleetapi.NewAction(fleetapi.ActionTypeUnknown) + err = store.Save() + require.Error(t, err, "expected and error when saving sore with invalid state") + + assert.True(t, store.dirty, + "the store should be kept dirty when save fails") + }) + t.Run("action returns empty when no action is saved on disk", func(t *testing.T) { storePath := filepath.Join(t.TempDir(), "state.json") s, err := storage.NewDiskStore(storePath) From 057fc26720ade0e559773922b0cfc813fc666b12 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 20 Jun 2024 19:25:01 +0200 Subject: [PATCH 32/39] fix docs and remove TODO --- internal/pkg/fleetapi/action.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/pkg/fleetapi/action.go b/internal/pkg/fleetapi/action.go index 5d02060c90a..99470262ea3 100644 --- a/internal/pkg/fleetapi/action.go +++ b/internal/pkg/fleetapi/action.go @@ -611,13 +611,12 @@ func (a *Actions) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalYAML prevents to decode actions from . +// UnmarshalYAML prevents to unmarshal actions from YAML. func (a *Actions) UnmarshalYAML(_ func(interface{}) error) error { - // TODO(AndersonQ): we need this to migrate the store from YAML to JSON return errors.New("Actions cannot be Unmarshalled from YAML") } -// MarshalYAML attempts to decode yaml actions. +// MarshalYAML prevents to marshal actions from YAML. func (a *Actions) MarshalYAML() (interface{}, error) { - return nil, errors.New("Actions cannot be Marshaled to YAML") + return nil, errors.New("Actions cannot be Marshaled into YAML") } From 100375f6e06fa241c0cc4b833ac147acfd64ed4e Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 20 Jun 2024 19:36:53 +0200 Subject: [PATCH 33/39] add tests to load store --- .../agent/storage/store/state_store_test.go | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index 96f88ef460d..d0092dcd73c 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -360,7 +360,83 @@ func runTestStateStore(t *testing.T, ackToken string) { require.Equal(t, ackToken, store.AckToken()) }) - t.Run("state store is correctly loaded from disk", func(t *testing.T) { + t.Run("state store is loaded from disk", func(t *testing.T) { + t.Run("no store", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.json") + + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + + stateStore, err := NewStateStore(log, s) + require.NoError(t, err, "could not create disk store") + + assert.Empty(t, stateStore.Queue()) + assert.Empty(t, stateStore.Action()) + assert.Empty(t, stateStore.AckToken()) + }) + + t.Run("empty store file", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.json") + f, err := os.Create(storePath) + require.NoError(t, err, "could not create store file") + err = f.Close() + require.NoError(t, err, "could not close store file") + + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + + stateStore, err := NewStateStore(log, s) + require.NoError(t, err, "could not create disk store") + + assert.Empty(t, stateStore.Queue()) + assert.Empty(t, stateStore.Action()) + assert.Empty(t, stateStore.AckToken()) + }) + + t.Run("fails for invalid store content", func(t *testing.T) { + t.Run("wrong store version", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.json") + require.NoError(t, + os.WriteFile(storePath, []byte(`{"version":"0"}`), 0600), + "could not create store file") + + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + + _, err = NewStateStore(log, s) + require.Errorf(t, err, + "state store creation should have failed with invalid store version") + }) + + t.Run("empty store version", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.json") + require.NoError(t, + os.WriteFile(storePath, []byte(`{"version":""}`), 0600), + "could not create store file") + + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + + _, err = NewStateStore(log, s) + require.Errorf(t, err, + "state store creation should have failed with invalid store version") + }) + + t.Run("garbage data/invalid JSON", func(t *testing.T) { + storePath := filepath.Join(t.TempDir(), "state.json") + require.NoError(t, + os.WriteFile(storePath, []byte(`}`), 0600), + "could not create store file") + + s, err := storage.NewDiskStore(storePath) + require.NoError(t, err, "failed creating DiskStore") + + _, err = NewStateStore(log, s) + require.Errorf(t, err, + "state store creation should have failed") + }) + }) + t.Run("ActionPolicyChange", func(t *testing.T) { storePath := filepath.Join(t.TempDir(), "state.json") want := &fleetapi.ActionPolicyChange{ From 785372bb292b801968cd5aef7cb216a518522cb3 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Thu, 20 Jun 2024 19:37:03 +0200 Subject: [PATCH 34/39] WIP --- internal/pkg/agent/storage/store/migrations.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/pkg/agent/storage/store/migrations.go b/internal/pkg/agent/storage/store/migrations.go index 6ea798b2e91..27005f55807 100644 --- a/internal/pkg/agent/storage/store/migrations.go +++ b/internal/pkg/agent/storage/store/migrations.go @@ -7,6 +7,7 @@ package store import ( "errors" "fmt" + "slices" "time" "github.com/elastic/elastic-agent/internal/pkg/agent/storage" @@ -76,10 +77,17 @@ func migrateActionStoreToStateStore( return nil } - if action.Type != fleetapi.ActionTypePolicyChange { + supportedActions := []string{ + fleetapi.ActionTypePolicyChange, + // Unenroll action is supported for completeness as an unenrolled agent + // would not be upgraded. + fleetapi.ActionTypeUnenroll, + } + panic("TODO: double check why it does not need to load unenroll actions. I think 7.17 did not save them") + if !slices.Contains(supportedActions, action.Type) { log.Warnf("unexpected action type when migrating from action store. "+ - "Found %s, but only %s is suported. Ignoring action and proceeding.", - action.Type, fleetapi.ActionTypePolicyChange) + "Found %s, but only %v are suported. Ignoring action and proceeding.", + action.Type, supportedActions) // If it isn't ignored, the agent will be stuck here and require manual // intervention to fix the store. return nil From a1ae94152576ed7d0f4be54f34e1d07fdd5a48e8 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 21 Jun 2024 11:30:57 +0200 Subject: [PATCH 35/39] migration: add tests for empty and unknown actions --- .../pkg/agent/storage/store/migrations.go | 15 +- .../agent/storage/store/migrations_test.go | 346 ++++++++++++------ .../pkg/agent/storage/store/state_store.go | 2 +- .../testdata/7.17.18-action_store_empty.yml | 0 ...=> 7.17.18-action_store_policy_change.yml} | 0 .../7.18.18-action_store_unenroll.yml | 3 + .../testdata/7.18.18-action_store_unknown.yml | 2 + .../store/testdata/8.0.0-action_unknown.yml | 20 + .../storage/store/testdata/8.0.0-empty.yml | 0 9 files changed, 271 insertions(+), 117 deletions(-) create mode 100644 internal/pkg/agent/storage/store/testdata/7.17.18-action_store_empty.yml rename internal/pkg/agent/storage/store/testdata/{7.17.18-action_store.yml => 7.17.18-action_store_policy_change.yml} (100%) create mode 100644 internal/pkg/agent/storage/store/testdata/7.18.18-action_store_unenroll.yml create mode 100644 internal/pkg/agent/storage/store/testdata/7.18.18-action_store_unknown.yml create mode 100644 internal/pkg/agent/storage/store/testdata/8.0.0-action_unknown.yml create mode 100644 internal/pkg/agent/storage/store/testdata/8.0.0-empty.yml diff --git a/internal/pkg/agent/storage/store/migrations.go b/internal/pkg/agent/storage/store/migrations.go index 27005f55807..8f33f743a29 100644 --- a/internal/pkg/agent/storage/store/migrations.go +++ b/internal/pkg/agent/storage/store/migrations.go @@ -83,7 +83,6 @@ func migrateActionStoreToStateStore( // would not be upgraded. fleetapi.ActionTypeUnenroll, } - panic("TODO: double check why it does not need to load unenroll actions. I think 7.17 did not save them") if !slices.Contains(supportedActions, action.Type) { log.Warnf("unexpected action type when migrating from action store. "+ "Found %s, but only %v are suported. Ignoring action and proceeding.", @@ -149,8 +148,9 @@ func migrateYAMLStateStoreToStateStoreV1(log *logger.Logger, store storage.Stora // it isn't a YAML store return errors.Join(ErrInvalidYAML, err) } - - // Store was empty, nothing to migrate + // nil here would mean an empty store. However and empty file is a valid + // JSON store as well. Thus, it should never reach this point. Nevertheless, + // better to ensue it does not proceed if the store is nil. if yamlStore == nil { return nil } @@ -164,6 +164,8 @@ func migrateYAMLStateStoreToStateStoreV1(log *logger.Logger, store storage.Stora Data: fleetapi.ActionPolicyChangeData{ Policy: conv.YAMLMapToJSONMap(yamlStore.Action.Policy)}, } + // Unenroll action is supported for completeness as an unenrolled agent + // would not be upgraded. case fleetapi.ActionTypeUnenroll: action = &fleetapi.ActionUnenroll{ ActionID: yamlStore.Action.ActionID, @@ -177,6 +179,13 @@ func migrateYAMLStateStoreToStateStoreV1(log *logger.Logger, store storage.Stora var queue actionQueue for _, a := range yamlStore.ActionQueue { + if a.Type != fleetapi.ActionTypeUpgrade { + log.Warnf( + "loaded a unsupported %s action from the deprecated YAML state store action queue, ignoring it", + yamlStore.Action.Type) + continue + } + queue = append(queue, &fleetapi.ActionUpgrade{ ActionID: a.ActionID, diff --git a/internal/pkg/agent/storage/store/migrations_test.go b/internal/pkg/agent/storage/store/migrations_test.go index 6fa537289d9..bb55322dc1b 100644 --- a/internal/pkg/agent/storage/store/migrations_test.go +++ b/internal/pkg/agent/storage/store/migrations_test.go @@ -71,75 +71,16 @@ func TestStoreMigrations(t *testing.T) { }) t.Run("action store to YAML state store", func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - want := &fleetapi.ActionPolicyChange{ - ActionID: "abc123", - ActionType: "POLICY_CHANGE", - Data: fleetapi.ActionPolicyChangeData{ - Policy: map[string]any{ - "hello": "world", - "phi": 1.618, - "answer": 42.0, - "a_map": []any{ - map[string]any{ - "nested_map1": map[string]any{ - "nested_map1_key1": "value1", - "nested_map1_key2": "value2", - }}, - map[string]any{ - "nested_map2": map[string]any{ - "nested_map2_key1": "value1", - "nested_map2_key2": "value2", - }}, - }, - }, - }, - } - - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - - goldenActionStore, err := os.ReadFile( - filepath.Join("testdata", "7.17.18-action_store.yml")) - require.NoError(t, err, "could not read action store golden file") - - oldActionStorePath := filepath.Join(tempDir, "action_store.yml") - err = os.WriteFile(oldActionStorePath, goldenActionStore, 0666) - require.NoError(t, err, "could not copy action store golden file") - - newStateStorePath := filepath.Join(tempDir, "state_store.yaml") - newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") - - err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) - require.NoError(t, err, "migration action store -> state store failed") - - // to load from disk a new store needs to be created, it loads the file - // to memory during the store creation. - stateStore, err := NewStateStore(log, newStateStore) - require.NoError(t, err, "could not create state store") - - got := stateStore.Action() - require.NotNil(t, got, "should have loaded an action") - - assert.Equalf(t, want, got, - "loaded action differs from action on the old action store") - assert.Empty(t, stateStore.Queue(), - "queue should be empty, old action store did not have a queue") - }) - - t.Run("YAML state store containing an ActionPolicyChange to JSON state store", - func(t *testing.T) { - ctx := context.Background() - log, _ := logger.NewTesting("") - - want := state{ - Version: "1", - ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ - ActionID: "policy:POLICY-ID:1:1", + tcs := []struct { + name string + storePath string + want fleetapi.Action + }{ + { + name: "policy change", + storePath: "7.17.18-action_store_policy_change.yml", + want: &fleetapi.ActionPolicyChange{ + ActionID: "abc123", ActionType: "POLICY_CHANGE", Data: fleetapi.ActionPolicyChangeData{ Policy: map[string]any{ @@ -160,23 +101,105 @@ func TestStoreMigrations(t *testing.T) { }, }, }, - }}, - AckToken: "czlV93YBwdkt5lYhBY7S", - Queue: actionQueue{&fleetapi.ActionUpgrade{ - ActionID: "action1", - ActionType: "UPGRADE", - ActionStartTime: "2024-02-19T17:48:40Z", - ActionExpiration: "2025-02-19T17:48:40Z", - Data: fleetapi.ActionUpgradeData{ - Version: "1.2.3", - SourceURI: "https://example.com", - Retry: 1, - }, - Signed: nil, - Err: nil, }, - &fleetapi.ActionUpgrade{ - ActionID: "action2", + }, + { + name: "unenroll", + storePath: "7.18.18-action_store_unenroll.yml", + want: &fleetapi.ActionUnenroll{ + ActionID: "f450373c-ea62-475c-98c5-26fa174d759f", + ActionType: "UNENROLL", + IsDetected: false, + }, + }, + { + name: "unsupported", + storePath: "7.18.18-action_store_unknown.yml", + want: nil, + }, + { + name: "empty store", + storePath: "7.17.18-action_store_empty.yml", + want: nil, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") + + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + + goldenActionStore, err := os.ReadFile( + filepath.Join("testdata", tc.storePath)) + require.NoError(t, err, "could not read action store golden file") + + oldActionStorePath := filepath.Join(tempDir, "action_store.yml") + err = os.WriteFile(oldActionStorePath, goldenActionStore, 0666) + require.NoError(t, err, "could not copy action store golden file") + + newStateStorePath := filepath.Join(tempDir, "state_store.yaml") + newStateStore, err := storage.NewEncryptedDiskStore(ctx, newStateStorePath, + storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") + + err = migrateActionStoreToStateStore(log, oldActionStorePath, newStateStore) + require.NoError(t, err, "migration action store -> state store failed") + + // to load from disk a new store needs to be created, it loads the file + // to memory during the store creation. + stateStore, err := NewStateStore(log, newStateStore) + require.NoError(t, err, "could not create state store") + + got := stateStore.Action() + + assert.Equalf(t, tc.want, got, + "loaded action differs from action on the old action store") + assert.Empty(t, stateStore.Queue(), + "queue should be empty, old action store did not have a queue") + }) + } + }) + + t.Run("YAML state store to JSON state store", func(t *testing.T) { + tests := []struct { + name string + yamlStore string + wantState state + }{ + { + name: "ActionPolicyChange", + yamlStore: "8.0.0-action_policy_change.yml", + wantState: state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "policy:POLICY-ID:1:1", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]any{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + "a_map": []any{ + map[string]any{ + "nested_map1": map[string]any{ + "nested_map1_key1": "value1", + "nested_map1_key2": "value2", + }}, + map[string]any{ + "nested_map2": map[string]any{ + "nested_map2_key1": "value1", + "nested_map2_key2": "value2", + }}, + }, + }, + }, + }}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", ActionType: "UPGRADE", ActionStartTime: "2024-02-19T17:48:40Z", ActionExpiration: "2025-02-19T17:48:40Z", @@ -187,47 +210,144 @@ func TestStoreMigrations(t *testing.T) { }, Signed: nil, Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + }, + }, + { + name: "ActionUnenroll", + yamlStore: "8.0.0-action_unenroll.yml", + wantState: state{ + Version: "1", + ActionSerializer: actionSerializer{Action: &fleetapi.ActionUnenroll{ + ActionID: "abc123", + ActionType: "UNENROLL", + IsDetected: true, + Signed: nil, }}, - } + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: actionQueue{&fleetapi.ActionUpgrade{ + ActionID: "action1", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }, + &fleetapi.ActionUpgrade{ + ActionID: "action2", + ActionType: "UPGRADE", + ActionStartTime: "2024-02-19T17:48:40Z", + ActionExpiration: "2025-02-19T17:48:40Z", + Data: fleetapi.ActionUpgradeData{ + Version: "1.2.3", + SourceURI: "https://example.com", + Retry: 1, + }, + Signed: nil, + Err: nil, + }}, + }, + }, + { + name: "unknown", + yamlStore: "8.0.0-action_unknown.yml", + wantState: state{ + Version: "1", + ActionSerializer: actionSerializer{Action: nil}, + AckToken: "czlV93YBwdkt5lYhBY7S", + Queue: nil, + }, + }, + { + name: "empty store", + yamlStore: "8.0.0-empty.yml", + wantState: state{ + Version: "1", + ActionSerializer: actionSerializer{Action: nil}, + AckToken: "", + Queue: nil, + }, + }, + } - tempDir := t.TempDir() - vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + log, _ := logger.NewTesting("") - yamlStorePlain, err := os.ReadFile( - filepath.Join("testdata", "8.0.0-action_policy_change.yml")) - require.NoError(t, err, "could not read action store golden file") + tempDir := t.TempDir() + vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) - encDiskStorePath := filepath.Join(tempDir, "store.enc") - encDiskStore, err := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, - storage.WithVaultPath(vaultPath)) - require.NoError(t, err, "failed creating EncryptedDiskStore") + yamlStorePlain, err := os.ReadFile( + filepath.Join("testdata", tt.yamlStore)) + require.NoError(t, err, "could not read action store golden file") - err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) - require.NoError(t, err, - "failed saving copy of golden files on an EncryptedDiskStore") + encDiskStorePath := filepath.Join(tempDir, "store.enc") + encDiskStore, err := storage.NewEncryptedDiskStore(ctx, encDiskStorePath, storage.WithVaultPath(vaultPath)) + require.NoError(t, err, "failed creating EncryptedDiskStore") - err = migrateYAMLStateStoreToStateStoreV1(log, encDiskStore) - require.NoError(t, err, "YAML state store -> JSON state store failed") + err = encDiskStore.Save(bytes.NewBuffer(yamlStorePlain)) + require.NoError(t, err, "failed saving copy of golden files on an EncryptedDiskStore") - // Load migrated store from disk - stateStore, err := NewStateStore(log, encDiskStore) - require.NoError(t, err, "could not load store from disk") + err = migrateYAMLStateStoreToStateStoreV1(log, encDiskStore) + require.NoError(t, err, "YAML state store -> JSON state store failed") - assert.Equal(t, want, stateStore.state) - }) + // Load migrated store from disk + stateStore, err := NewStateStore(log, encDiskStore) + require.NoError(t, err, "could not load store from disk") + + assert.Equal(t, tt.wantState, stateStore.state) + }) + } + }) - t.Run("YAML state store containing an ActionUnenroll to JSON state store", + t.Run("YAML state store containing an ActionPolicyChange to JSON state store", func(t *testing.T) { ctx := context.Background() log, _ := logger.NewTesting("") want := state{ Version: "1", - ActionSerializer: actionSerializer{Action: &fleetapi.ActionUnenroll{ - ActionID: "abc123", - ActionType: "UNENROLL", - IsDetected: true, - Signed: nil, + ActionSerializer: actionSerializer{Action: &fleetapi.ActionPolicyChange{ + ActionID: "policy:POLICY-ID:1:1", + ActionType: "POLICY_CHANGE", + Data: fleetapi.ActionPolicyChangeData{ + Policy: map[string]any{ + "hello": "world", + "phi": 1.618, + "answer": 42.0, + "a_map": []any{ + map[string]any{ + "nested_map1": map[string]any{ + "nested_map1_key1": "value1", + "nested_map1_key2": "value2", + }}, + map[string]any{ + "nested_map2": map[string]any{ + "nested_map2_key1": "value1", + "nested_map2_key2": "value2", + }}, + }, + }, + }, }}, AckToken: "czlV93YBwdkt5lYhBY7S", Queue: actionQueue{&fleetapi.ActionUpgrade{ @@ -262,7 +382,7 @@ func TestStoreMigrations(t *testing.T) { vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) yamlStorePlain, err := os.ReadFile( - filepath.Join("testdata", "8.0.0-action_unenroll.yml")) + filepath.Join("testdata", "8.0.0-action_policy_change.yml")) require.NoError(t, err, "could not read action store golden file") encDiskStorePath := filepath.Join(tempDir, "store.enc") @@ -404,7 +524,7 @@ func TestStoreMigrations(t *testing.T) { vaultPath := createAgentVaultAndSecret(t, ctx, tempDir) goldenActionStore, err := os.ReadFile( - filepath.Join("testdata", "7.17.18-action_store.yml")) + filepath.Join("testdata", "7.17.18-action_store_policy_change.yml")) require.NoError(t, err, "could not read action store golden file") oldActionStorePath := filepath.Join(tempDir, "action_store.yml") diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index 3a5c7ef9b73..784374a5d02 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -327,7 +327,7 @@ func (aq *actionQueue) UnmarshalJSON(data []byte) error { for _, a := range actions { sa, ok := a.(fleetapi.ScheduledAction) if !ok { - return fmt.Errorf("actionQueue: action %s isn't a ScheduledAction,"+ + return fmt.Errorf("actionQueue: action %s isn't a ScheduledAction, "+ "cannot unmarshal it to actionQueue", a.Type()) } scheduledActions = append(scheduledActions, sa) diff --git a/internal/pkg/agent/storage/store/testdata/7.17.18-action_store_empty.yml b/internal/pkg/agent/storage/store/testdata/7.17.18-action_store_empty.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml b/internal/pkg/agent/storage/store/testdata/7.17.18-action_store_policy_change.yml similarity index 100% rename from internal/pkg/agent/storage/store/testdata/7.17.18-action_store.yml rename to internal/pkg/agent/storage/store/testdata/7.17.18-action_store_policy_change.yml diff --git a/internal/pkg/agent/storage/store/testdata/7.18.18-action_store_unenroll.yml b/internal/pkg/agent/storage/store/testdata/7.18.18-action_store_unenroll.yml new file mode 100644 index 00000000000..3080ec5fd89 --- /dev/null +++ b/internal/pkg/agent/storage/store/testdata/7.18.18-action_store_unenroll.yml @@ -0,0 +1,3 @@ +action_id: f450373c-ea62-475c-98c5-26fa174d759f +action_type: UNENROLL +is_detected: false diff --git a/internal/pkg/agent/storage/store/testdata/7.18.18-action_store_unknown.yml b/internal/pkg/agent/storage/store/testdata/7.18.18-action_store_unknown.yml new file mode 100644 index 00000000000..620631839fa --- /dev/null +++ b/internal/pkg/agent/storage/store/testdata/7.18.18-action_store_unknown.yml @@ -0,0 +1,2 @@ +action_id: u450373c-ea62-475c-98c5-26fa174d759u +action_type: UNKNOWN diff --git a/internal/pkg/agent/storage/store/testdata/8.0.0-action_unknown.yml b/internal/pkg/agent/storage/store/testdata/8.0.0-action_unknown.yml new file mode 100644 index 00000000000..0e75f998a47 --- /dev/null +++ b/internal/pkg/agent/storage/store/testdata/8.0.0-action_unknown.yml @@ -0,0 +1,20 @@ +action: + action_id: abc123 + action_type: UNKNOWN + is_detected: true +ack_token: czlV93YBwdkt5lYhBY7S +action_queue: + - action_id: action1 + type: UNKNOWN + start_time: "2024-02-19T17:48:40Z" + expiration: "2025-02-19T17:48:40Z" + version: 1.2.3 + source_uri: https://example.com + retry_attempt: 1 + - action_id: action2 + type: UNKNOWN + start_time: "2024-02-19T17:48:40Z" + expiration: "2025-02-19T17:48:40Z" + version: 1.2.3 + source_uri: https://example.com + retry_attempt: 1 diff --git a/internal/pkg/agent/storage/store/testdata/8.0.0-empty.yml b/internal/pkg/agent/storage/store/testdata/8.0.0-empty.yml new file mode 100644 index 00000000000..e69de29bb2d From eba70e6a4035b86dd0ae40851e23c9cc7150fda0 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 21 Jun 2024 12:45:51 +0200 Subject: [PATCH 36/39] always return the reader.Close error --- .../storage/store/internal/migrations/migrations.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/pkg/agent/storage/store/internal/migrations/migrations.go b/internal/pkg/agent/storage/store/internal/migrations/migrations.go index f811e45005d..0ae7d7a6889 100644 --- a/internal/pkg/agent/storage/store/internal/migrations/migrations.go +++ b/internal/pkg/agent/storage/store/internal/migrations/migrations.go @@ -69,13 +69,12 @@ func LoadStore[Store any](loader loader) (store *Store, err error) { return nil, fmt.Errorf("failed to load action store: %w", err) } defer func() { - err2 := reader.Close() - if err2 != nil { - err2 = fmt.Errorf("migration storeLoad failed to close reader: %w", err2) - } - if err != nil { - err = errors.Join(err, err2) + errClose := reader.Close() + if errClose != nil { + errClose = fmt.Errorf( + "migration storeLoad failed to close reader: %w", errClose) } + err = errors.Join(err, errClose) }() data, err := io.ReadAll(reader) From 987c9f6971b3edd14e00d025f72049901fe3644b Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 21 Jun 2024 15:05:48 +0200 Subject: [PATCH 37/39] do not log errors --- internal/pkg/agent/storage/store/migrations.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/pkg/agent/storage/store/migrations.go b/internal/pkg/agent/storage/store/migrations.go index 8f33f743a29..30f6b818a80 100644 --- a/internal/pkg/agent/storage/store/migrations.go +++ b/internal/pkg/agent/storage/store/migrations.go @@ -33,20 +33,18 @@ func migrateActionStoreToStateStore( stateStoreExists, err := stateDiskStore.Exists() if err != nil { - log.Errorf("failed to check if state store exists: %v", err) - return err + return fmt.Errorf("failed to check if state store exists: %w", err) } // do not migrate if the state store already exists if stateStoreExists { - log.Debugf("not attempting to migrare from action store: state store already exists") + log.Debugf("not attempting to migrate from action store: state store already exists") return nil } actionStoreExists, err := actionDiskStore.Exists() if err != nil { - log.Errorf("failed to check if action store %s exists: %v", actionStorePath, err) - return err + return fmt.Errorf("failed to check if action store %s exists: %w", actionStorePath, err) } // nothing to migrate if the action store doesn't exist @@ -66,9 +64,8 @@ func migrateActionStoreToStateStore( action, err := migrations.LoadActionStore(actionDiskStore) if err != nil { - log.Errorf("failed to load action store for migration %s: %v", + return fmt.Errorf("failed to load action store for migration %s: %w", actionStorePath, err) - return err } // no actions stored nothing to migrate From e61cf6d7e0cde8b673801cf56db0b83e417cd4f8 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 21 Jun 2024 17:33:04 +0200 Subject: [PATCH 38/39] add debug log when dropping actions on SetAction --- internal/pkg/agent/storage/store/state_store.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/pkg/agent/storage/store/state_store.go b/internal/pkg/agent/storage/store/state_store.go index 784374a5d02..f3fdb7f8bda 100644 --- a/internal/pkg/agent/storage/store/state_store.go +++ b/internal/pkg/agent/storage/store/state_store.go @@ -128,8 +128,8 @@ func NewStateStore(log *logger.Logger, store saveLoader) (*StateStore, error) { if st.Version != Version { return nil, fmt.Errorf( - "invalid state store version, got %q isntead of %s", - st.Version, Version) + "invalid state store version, current version is %q loaded store verion is %q", + Version, st.Version) } return &StateStore{ @@ -180,6 +180,10 @@ func (s *StateStore) SetAction(a fleetapi.Action) { } s.dirty = true s.state.ActionSerializer.Action = a + default: + s.log.Debugw("trying to set invalid action type on the state store, ignoring the action", + "action.type", a.Type(), + "action.id", a.ID()) } } From 57dec1eb67e9b2982e6672685387485d79bd4151 Mon Sep 17 00:00:00 2001 From: Anderson Queiroz Date: Fri, 21 Jun 2024 17:33:20 +0200 Subject: [PATCH 39/39] fix typos/better logs --- internal/pkg/agent/storage/store/migrations.go | 2 +- internal/pkg/agent/storage/store/state_store_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/pkg/agent/storage/store/migrations.go b/internal/pkg/agent/storage/store/migrations.go index 30f6b818a80..213ffeddd7a 100644 --- a/internal/pkg/agent/storage/store/migrations.go +++ b/internal/pkg/agent/storage/store/migrations.go @@ -178,7 +178,7 @@ func migrateYAMLStateStoreToStateStoreV1(log *logger.Logger, store storage.Stora for _, a := range yamlStore.ActionQueue { if a.Type != fleetapi.ActionTypeUpgrade { log.Warnf( - "loaded a unsupported %s action from the deprecated YAML state store action queue, ignoring it", + "loaded a unsupported %s action from the deprecated YAML state store action queue, it will be dropped", yamlStore.Action.Type) continue } diff --git a/internal/pkg/agent/storage/store/state_store_test.go b/internal/pkg/agent/storage/store/state_store_test.go index d0092dcd73c..0c1b18e661c 100644 --- a/internal/pkg/agent/storage/store/state_store_test.go +++ b/internal/pkg/agent/storage/store/state_store_test.go @@ -90,7 +90,7 @@ func runTestStateStore(t *testing.T, ackToken string) { store.dirty = true store.state.ActionSerializer.Action = fleetapi.NewAction(fleetapi.ActionTypeUnknown) err = store.Save() - require.Error(t, err, "expected and error when saving sore with invalid state") + require.Error(t, err, "expected an error when saving store with invalid state") assert.True(t, store.dirty, "the store should be kept dirty when save fails")