From e6431a4d7ca55de8bfb8f4749ec6c34f422545c3 Mon Sep 17 00:00:00 2001 From: Vladimir Iliakov Date: Wed, 4 Mar 2026 10:24:14 +0100 Subject: [PATCH] STAC-23601: Delete ES snapshot repository before configuring it --- cmd/elasticsearch/configure.go | 8 ++++ cmd/elasticsearch/configure_test.go | 43 ++++++++++++++++++++- internal/clients/elasticsearch/client.go | 22 +++++++++++ internal/clients/elasticsearch/interface.go | 1 + 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/cmd/elasticsearch/configure.go b/cmd/elasticsearch/configure.go index 0d00199..c56e3c2 100644 --- a/cmd/elasticsearch/configure.go +++ b/cmd/elasticsearch/configure.go @@ -40,6 +40,14 @@ func runConfigure(appCtx *app.Context) error { // Configure snapshot repository repo := appCtx.Config.Elasticsearch.SnapshotRepository + + // Always unregister existing repository to ensure clean state + appCtx.Logger.Infof("Unregistering snapshot repository '%s'...", repo.Name) + if err := appCtx.ESClient.DeleteSnapshotRepository(repo.Name); err != nil { + return fmt.Errorf("failed to unregister snapshot repository: %w", err) + } + appCtx.Logger.Successf("Snapshot repository unregistered successfully") + appCtx.Logger.Infof("Configuring snapshot repository '%s' (bucket: %s)...", repo.Name, repo.Bucket) err = appCtx.ESClient.ConfigureSnapshotRepository( diff --git a/cmd/elasticsearch/configure_test.go b/cmd/elasticsearch/configure_test.go index ddcd4db..b294c0e 100644 --- a/cmd/elasticsearch/configure_test.go +++ b/cmd/elasticsearch/configure_test.go @@ -16,14 +16,24 @@ import ( // mockESClientForConfigure is a mock for testing configure command type mockESClientForConfigure struct { + deleteRepoErr error configureRepoErr error configureSLMErr error + repoDeleted bool repoConfigured bool slmConfigured bool lastRepoConfig map[string]string lastSLMConfig map[string]interface{} } +func (m *mockESClientForConfigure) DeleteSnapshotRepository(_ string) error { + if m.deleteRepoErr != nil { + return m.deleteRepoErr + } + m.repoDeleted = true + return nil +} + func (m *mockESClientForConfigure) ConfigureSnapshotRepository(name, bucket, endpoint, basePath, accessKey, secretKey string) error { if m.configureRepoErr != nil { return m.configureRepoErr @@ -260,32 +270,51 @@ minio: } // TestMockESClientForConfigure demonstrates mock usage for configure +// +//nolint:funlen // Table-driven test func TestMockESClientForConfigure(t *testing.T) { tests := []struct { name string + deleteRepoErr error configureRepoErr error configureSLMErr error + expectDeleteOK bool expectRepoOK bool expectSLMOK bool }{ { name: "successful configuration", + deleteRepoErr: nil, configureRepoErr: nil, configureSLMErr: nil, + expectDeleteOK: true, expectRepoOK: true, expectSLMOK: true, }, + { + name: "repository deletion fails", + deleteRepoErr: fmt.Errorf("repository deletion failed"), + configureRepoErr: nil, + configureSLMErr: nil, + expectDeleteOK: false, + expectRepoOK: false, + expectSLMOK: false, + }, { name: "repository configuration fails", + deleteRepoErr: nil, configureRepoErr: fmt.Errorf("repository creation failed"), configureSLMErr: nil, + expectDeleteOK: true, expectRepoOK: false, expectSLMOK: false, }, { name: "SLM configuration fails", + deleteRepoErr: nil, configureRepoErr: nil, configureSLMErr: fmt.Errorf("SLM policy creation failed"), + expectDeleteOK: true, expectRepoOK: true, expectSLMOK: false, }, @@ -294,12 +323,24 @@ func TestMockESClientForConfigure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &mockESClientForConfigure{ + deleteRepoErr: tt.deleteRepoErr, configureRepoErr: tt.configureRepoErr, configureSLMErr: tt.configureSLMErr, } + // Delete repository + err := mockClient.DeleteSnapshotRepository("backup-repo") + + if tt.expectDeleteOK { + assert.NoError(t, err) + assert.True(t, mockClient.repoDeleted) + } else { + assert.Error(t, err) + return // Don't test configure if delete failed + } + // Configure repository - err := mockClient.ConfigureSnapshotRepository( + err = mockClient.ConfigureSnapshotRepository( "backup-repo", "backup-bucket", "minio:9000", diff --git a/internal/clients/elasticsearch/client.go b/internal/clients/elasticsearch/client.go index ba6e6cc..e0e1876 100644 --- a/internal/clients/elasticsearch/client.go +++ b/internal/clients/elasticsearch/client.go @@ -247,6 +247,28 @@ func (c *Client) RolloverDatastream(datastreamName string) error { return nil } +// DeleteSnapshotRepository deletes a snapshot repository +func (c *Client) DeleteSnapshotRepository(name string) error { + res, err := c.es.Snapshot.DeleteRepository( + []string{name}, + c.es.Snapshot.DeleteRepository.WithContext(context.Background()), + ) + if err != nil { + return fmt.Errorf("failed to delete snapshot repository: %w", err) + } + defer res.Body.Close() + + if res.StatusCode == http.StatusNotFound { + return nil // Repository doesn't exist, which is fine + } + + if res.IsError() { + return fmt.Errorf("elasticsearch returned error: %s", res.String()) + } + + return nil +} + // ConfigureSnapshotRepository configures an S3 snapshot repository func (c *Client) ConfigureSnapshotRepository(name, bucket, endpoint, basePath, accessKey, secretKey string) error { body := map[string]interface{}{ diff --git a/internal/clients/elasticsearch/interface.go b/internal/clients/elasticsearch/interface.go index 0aa7b56..a771097 100644 --- a/internal/clients/elasticsearch/interface.go +++ b/internal/clients/elasticsearch/interface.go @@ -19,6 +19,7 @@ type Interface interface { RolloverDatastream(datastreamName string) error // Repository and SLM operations + DeleteSnapshotRepository(name string) error ConfigureSnapshotRepository(name, bucket, endpoint, basePath, accessKey, secretKey string) error ConfigureSLMPolicy(name, schedule, snapshotName, repository, indices, expireAfter string, minCount, maxCount int) error }