diff --git a/CHANGELOG.md b/CHANGELOG.md index 10e843638..2c15e9b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## Features * Removed BETA labels for StateVersion Upload method, ConfigurationVersion `provisional` field, and `save-plan` runs by @brandonc [#800](https://github.com/hashicorp/go-tfe/pull/800) +* Allow soft deleting, restoring, and permanently deleting StateVersion and ConfigurationVersion backing data by @mwudka [#801](https://github.com/hashicorp/go-tfe/pull/801) # v.1.38.0 diff --git a/configuration_version.go b/configuration_version.go index d2e1f5c74..b83926ae7 100644 --- a/configuration_version.go +++ b/configuration_version.go @@ -55,6 +55,18 @@ type ConfigurationVersions interface { // Download a configuration version. Only configuration versions in the uploaded state may be downloaded. Download(ctx context.Context, cvID string) ([]byte, error) + + // SoftDeleteBackingData soft deletes the configuration version's backing data + // **Note: This functionality is only available in Terraform Enterprise.** + SoftDeleteBackingData(ctx context.Context, svID string) error + + // RestoreBackingData restores a soft deleted configuration version's backing data + // **Note: This functionality is only available in Terraform Enterprise.** + RestoreBackingData(ctx context.Context, svID string) error + + // PermanentlyDeleteBackingData permanently deletes a soft deleted configuration version's backing data + // **Note: This functionality is only available in Terraform Enterprise.** + PermanentlyDeleteBackingData(ctx context.Context, svID string) error } // configurationVersions implements ConfigurationVersions. @@ -356,3 +368,29 @@ func (s *configurationVersions) Download(ctx context.Context, cvID string) ([]by return buf.Bytes(), nil } + +func (s *configurationVersions) SoftDeleteBackingData(ctx context.Context, cvID string) error { + return s.manageBackingData(ctx, cvID, "soft_delete_backing_data") +} + +func (s *configurationVersions) RestoreBackingData(ctx context.Context, cvID string) error { + return s.manageBackingData(ctx, cvID, "restore_backing_data") +} + +func (s *configurationVersions) PermanentlyDeleteBackingData(ctx context.Context, cvID string) error { + return s.manageBackingData(ctx, cvID, "permanently_delete_backing_data") +} + +func (s *configurationVersions) manageBackingData(ctx context.Context, cvID, action string) error { + if !validStringID(&cvID) { + return ErrInvalidConfigVersionID + } + + u := fmt.Sprintf("configuration-versions/%s/actions/%s", cvID, action) + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} diff --git a/configuration_version_integration_test.go b/configuration_version_integration_test.go index f52dfefb2..ea517c640 100644 --- a/configuration_version_integration_test.go +++ b/configuration_version_integration_test.go @@ -472,3 +472,49 @@ func TestConfigurationVersions_Unmarshal(t *testing.T) { assert.Equal(t, cv.Provisional, true) assert.Equal(t, cv.Speculative, true) } + +func TestConfigurationVersions_ManageBackingData(t *testing.T) { + skipUnlessEnterprise(t) + + client := testClient(t) + ctx := context.Background() + + workspace, workspaceCleanup := createWorkspace(t, client, nil) + t.Cleanup(workspaceCleanup) + + nonCurrentCv, uploadedCvCleanup := createUploadedConfigurationVersion(t, client, workspace) + defer uploadedCvCleanup() + + _, uploadedCvCleanup = createUploadedConfigurationVersion(t, client, workspace) + defer uploadedCvCleanup() + + t.Run("soft delete backing data", func(t *testing.T) { + err := client.ConfigurationVersions.SoftDeleteBackingData(ctx, nonCurrentCv.ID) + require.NoError(t, err) + + _, err = client.ConfigurationVersions.Download(ctx, nonCurrentCv.ID) + assert.Equal(t, ErrResourceNotFound, err) + }) + + t.Run("restore backing data", func(t *testing.T) { + err := client.ConfigurationVersions.RestoreBackingData(ctx, nonCurrentCv.ID) + require.NoError(t, err) + + _, err = client.ConfigurationVersions.Download(ctx, nonCurrentCv.ID) + require.NoError(t, err) + }) + + t.Run("permanently delete backing data", func(t *testing.T) { + err := client.ConfigurationVersions.SoftDeleteBackingData(ctx, nonCurrentCv.ID) + require.NoError(t, err) + + err = client.ConfigurationVersions.PermanentlyDeleteBackingData(ctx, nonCurrentCv.ID) + require.NoError(t, err) + + err = client.ConfigurationVersions.RestoreBackingData(ctx, nonCurrentCv.ID) + require.ErrorContainsf(t, err, "transition not allowed", "Restore backing data should fail") + + _, err = client.ConfigurationVersions.Download(ctx, nonCurrentCv.ID) + assert.Equal(t, ErrResourceNotFound, err) + }) +} diff --git a/examples/backing_data/main.go b/examples/backing_data/main.go new file mode 100644 index 000000000..cfe1db88f --- /dev/null +++ b/examples/backing_data/main.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "context" + "flag" + "fmt" + tfe "github.com/hashicorp/go-tfe" + "log" + "strings" +) + +func main() { + action := flag.String("action", "", "Action (soft-delete|restore|permanently-delete") + externalId := flag.String("external-id", "", "External ID of StateVersion or ConfigurationVersion") + + flag.Parse() + + if action == nil || *action == "" { + log.Fatal("No Action provided") + } + + if externalId == nil || *externalId == "" { + log.Fatal("No external ID provided") + } + + ctx := context.Background() + client, err := tfe.NewClient(&tfe.Config{ + RetryServerErrors: true, + }) + if err != nil { + log.Fatal(err) + } + + err = performAction(ctx, client, *action, *externalId) + if err != nil { + log.Fatalf("Error performing action: %v", err) + } +} + +func performAction(ctx context.Context, client *tfe.Client, action string, id string) error { + externalIdParts := strings.Split(id, "-") + switch externalIdParts[0] { + case "cv": + switch action { + case "soft-delete": + return client.ConfigurationVersions.SoftDeleteBackingData(ctx, id) + case "restore": + return client.ConfigurationVersions.RestoreBackingData(ctx, id) + case "permanently-delete": + return client.ConfigurationVersions.PermanentlyDeleteBackingData(ctx, id) + default: + return fmt.Errorf("unsupported action: %s", action) + } + case "sv": + switch action { + case "soft-delete": + return client.StateVersions.SoftDeleteBackingData(ctx, id) + case "restore": + return client.StateVersions.RestoreBackingData(ctx, id) + case "permanently-delete": + return client.StateVersions.PermanentlyDeleteBackingData(ctx, id) + default: + return fmt.Errorf("unsupported action: %s", action) + } + default: + return fmt.Errorf("unsupported external ID: %s", id) + } + return nil +} diff --git a/mocks/configuration_version_mocks.go b/mocks/configuration_version_mocks.go index f6b6640bd..1b296958b 100644 --- a/mocks/configuration_version_mocks.go +++ b/mocks/configuration_version_mocks.go @@ -110,6 +110,20 @@ func (mr *MockConfigurationVersionsMockRecorder) List(ctx, workspaceID, options return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockConfigurationVersions)(nil).List), ctx, workspaceID, options) } +// PermanentlyDeleteBackingData mocks base method. +func (m *MockConfigurationVersions) PermanentlyDeleteBackingData(ctx context.Context, svID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PermanentlyDeleteBackingData", ctx, svID) + ret0, _ := ret[0].(error) + return ret0 +} + +// PermanentlyDeleteBackingData indicates an expected call of PermanentlyDeleteBackingData. +func (mr *MockConfigurationVersionsMockRecorder) PermanentlyDeleteBackingData(ctx, svID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermanentlyDeleteBackingData", reflect.TypeOf((*MockConfigurationVersions)(nil).PermanentlyDeleteBackingData), ctx, svID) +} + // Read mocks base method. func (m *MockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) { m.ctrl.T.Helper() @@ -140,6 +154,34 @@ func (mr *MockConfigurationVersionsMockRecorder) ReadWithOptions(ctx, cvID, opti return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockConfigurationVersions)(nil).ReadWithOptions), ctx, cvID, options) } +// RestoreBackingData mocks base method. +func (m *MockConfigurationVersions) RestoreBackingData(ctx context.Context, svID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestoreBackingData", ctx, svID) + ret0, _ := ret[0].(error) + return ret0 +} + +// RestoreBackingData indicates an expected call of RestoreBackingData. +func (mr *MockConfigurationVersionsMockRecorder) RestoreBackingData(ctx, svID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreBackingData", reflect.TypeOf((*MockConfigurationVersions)(nil).RestoreBackingData), ctx, svID) +} + +// SoftDeleteBackingData mocks base method. +func (m *MockConfigurationVersions) SoftDeleteBackingData(ctx context.Context, svID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SoftDeleteBackingData", ctx, svID) + ret0, _ := ret[0].(error) + return ret0 +} + +// SoftDeleteBackingData indicates an expected call of SoftDeleteBackingData. +func (mr *MockConfigurationVersionsMockRecorder) SoftDeleteBackingData(ctx, svID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SoftDeleteBackingData", reflect.TypeOf((*MockConfigurationVersions)(nil).SoftDeleteBackingData), ctx, svID) +} + // Upload mocks base method. func (m *MockConfigurationVersions) Upload(ctx context.Context, url, path string) error { m.ctrl.T.Helper() diff --git a/mocks/state_version_mocks.go b/mocks/state_version_mocks.go index ce9b04c75..c764fb61e 100644 --- a/mocks/state_version_mocks.go +++ b/mocks/state_version_mocks.go @@ -95,6 +95,20 @@ func (mr *MockStateVersionsMockRecorder) ListOutputs(ctx, svID, options interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOutputs", reflect.TypeOf((*MockStateVersions)(nil).ListOutputs), ctx, svID, options) } +// PermanentlyDeleteBackingData mocks base method. +func (m *MockStateVersions) PermanentlyDeleteBackingData(ctx context.Context, svID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PermanentlyDeleteBackingData", ctx, svID) + ret0, _ := ret[0].(error) + return ret0 +} + +// PermanentlyDeleteBackingData indicates an expected call of PermanentlyDeleteBackingData. +func (mr *MockStateVersionsMockRecorder) PermanentlyDeleteBackingData(ctx, svID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermanentlyDeleteBackingData", reflect.TypeOf((*MockStateVersions)(nil).PermanentlyDeleteBackingData), ctx, svID) +} + // Read mocks base method. func (m *MockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) { m.ctrl.T.Helper() @@ -155,6 +169,34 @@ func (mr *MockStateVersionsMockRecorder) ReadWithOptions(ctx, svID, options inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockStateVersions)(nil).ReadWithOptions), ctx, svID, options) } +// RestoreBackingData mocks base method. +func (m *MockStateVersions) RestoreBackingData(ctx context.Context, svID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestoreBackingData", ctx, svID) + ret0, _ := ret[0].(error) + return ret0 +} + +// RestoreBackingData indicates an expected call of RestoreBackingData. +func (mr *MockStateVersionsMockRecorder) RestoreBackingData(ctx, svID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreBackingData", reflect.TypeOf((*MockStateVersions)(nil).RestoreBackingData), ctx, svID) +} + +// SoftDeleteBackingData mocks base method. +func (m *MockStateVersions) SoftDeleteBackingData(ctx context.Context, svID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SoftDeleteBackingData", ctx, svID) + ret0, _ := ret[0].(error) + return ret0 +} + +// SoftDeleteBackingData indicates an expected call of SoftDeleteBackingData. +func (mr *MockStateVersionsMockRecorder) SoftDeleteBackingData(ctx, svID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SoftDeleteBackingData", reflect.TypeOf((*MockStateVersions)(nil).SoftDeleteBackingData), ctx, svID) +} + // Upload mocks base method. func (m *MockStateVersions) Upload(ctx context.Context, workspaceID string, options tfe.StateVersionUploadOptions) (*tfe.StateVersion, error) { m.ctrl.T.Helper() diff --git a/state_version.go b/state_version.go index d376b4212..3397250e8 100644 --- a/state_version.go +++ b/state_version.go @@ -62,6 +62,18 @@ type StateVersions interface { // process outputs asynchronously. When consuming outputs or other async StateVersion fields, be sure to // wait for ResourcesProcessed to become `true` before assuming they are empty. ListOutputs(ctx context.Context, svID string, options *StateVersionOutputsListOptions) (*StateVersionOutputsList, error) + + // SoftDeleteBackingData soft deletes the state version's backing data + // **Note: This functionality is only available in Terraform Enterprise.** + SoftDeleteBackingData(ctx context.Context, svID string) error + + // RestoreBackingData restores a soft deleted state version's backing data + // **Note: This functionality is only available in Terraform Enterprise.** + RestoreBackingData(ctx context.Context, svID string) error + + // PermanentlyDeleteBackingData permanently deletes a soft deleted state version's backing data + // **Note: This functionality is only available in Terraform Enterprise.** + PermanentlyDeleteBackingData(ctx context.Context, svID string) error } // stateVersions implements StateVersions. @@ -398,6 +410,32 @@ func (s *stateVersions) ListOutputs(ctx context.Context, svID string, options *S return sv, nil } +func (s *stateVersions) SoftDeleteBackingData(ctx context.Context, svID string) error { + return s.manageBackingData(ctx, svID, "soft_delete_backing_data") +} + +func (s *stateVersions) RestoreBackingData(ctx context.Context, svID string) error { + return s.manageBackingData(ctx, svID, "restore_backing_data") +} + +func (s *stateVersions) PermanentlyDeleteBackingData(ctx context.Context, svID string) error { + return s.manageBackingData(ctx, svID, "permanently_delete_backing_data") +} + +func (s *stateVersions) manageBackingData(ctx context.Context, svID, action string) error { + if !validStringID(&svID) { + return ErrInvalidStateVerID + } + + u := fmt.Sprintf("state-versions/%s/actions/%s", svID, action) + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} + // check that StateVersionListOptions fields had valid values func (o *StateVersionListOptions) valid() error { if o == nil { diff --git a/state_version_integration_test.go b/state_version_integration_test.go index 406c3f4be..fd0c85bdf 100644 --- a/state_version_integration_test.go +++ b/state_version_integration_test.go @@ -634,3 +634,49 @@ func TestStateVersionOutputs(t *testing.T) { assert.Error(t, err) }) } + +func TestStateVersions_ManageBackingData(t *testing.T) { + skipUnlessEnterprise(t) + + client := testClient(t) + ctx := context.Background() + + workspace, workspaceCleanup := createWorkspace(t, client, nil) + t.Cleanup(workspaceCleanup) + + nonCurrentStateVersion, svTestCleanup := createStateVersion(t, client, 0, workspace) + t.Cleanup(svTestCleanup) + + _, svTestCleanup = createStateVersion(t, client, 0, workspace) + t.Cleanup(svTestCleanup) + + t.Run("soft delete backing data", func(t *testing.T) { + err := client.StateVersions.SoftDeleteBackingData(ctx, nonCurrentStateVersion.ID) + require.NoError(t, err) + + _, err = client.StateVersions.Download(ctx, nonCurrentStateVersion.DownloadURL) + assert.Equal(t, ErrResourceNotFound, err) + }) + + t.Run("restore backing data", func(t *testing.T) { + err := client.StateVersions.RestoreBackingData(ctx, nonCurrentStateVersion.ID) + require.NoError(t, err) + + _, err = client.StateVersions.Download(ctx, nonCurrentStateVersion.DownloadURL) + require.NoError(t, err) + }) + + t.Run("permanently delete backing data", func(t *testing.T) { + err := client.StateVersions.SoftDeleteBackingData(ctx, nonCurrentStateVersion.ID) + require.NoError(t, err) + + err = client.StateVersions.PermanentlyDeleteBackingData(ctx, nonCurrentStateVersion.ID) + require.NoError(t, err) + + err = client.StateVersions.RestoreBackingData(ctx, nonCurrentStateVersion.ID) + require.ErrorContainsf(t, err, "transition not allowed", "Restore backing data should fail") + + _, err = client.StateVersions.Download(ctx, nonCurrentStateVersion.DownloadURL) + assert.Equal(t, ErrResourceNotFound, err) + }) +}