From bfc971ae20b3bf68cb5f0b414ea60f484a342726 Mon Sep 17 00:00:00 2001 From: Andrew Lavery Date: Fri, 3 May 2024 20:29:07 +0800 Subject: [PATCH] Split boxes for snapshot schedule and retention (#4592) * html update mk1 * format * split routes * format, again * fix route naming * show message in right location * add blank line * explicitly callout that retention policies are for both manual and scheduled snapshots * f * fix nbsp * update schedule box whitespace, retention button name * fix whitespace in retention box * scheduled, not automatic --- pkg/handlers/handlers.go | 12 +- pkg/handlers/handlers_test.go | 29 ++- pkg/handlers/interface.go | 6 +- pkg/handlers/mock/mock.go | 48 +++-- pkg/handlers/snapshots.go | 173 ++++++++++++----- .../components/snapshots/SnapshotSchedule.jsx | 174 +++++++++++++++--- 6 files changed, 346 insertions(+), 96 deletions(-) diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index a0118b414e..8e0a75eacc 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -225,8 +225,10 @@ func RegisterSessionAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOT HandlerFunc(middleware.EnforceAccess(policy.AppBackupRead, handler.ListBackups)) r.Name("GetSnapshotConfig").Path("/api/v1/app/{appSlug}/snapshot/config").Methods("GET"). HandlerFunc(middleware.EnforceAccess(policy.AppSnapshotsettingsRead, handler.GetSnapshotConfig)) - r.Name("SaveSnapshotConfig").Path("/api/v1/app/{appSlug}/snapshot/config").Methods("PUT"). - HandlerFunc(middleware.EnforceAccess(policy.AppSnapshotsettingsWrite, handler.SaveSnapshotConfig)) + r.Name("SaveSnapshotSchedule").Path("/api/v1/app/{appSlug}/snapshot/schedule").Methods("PUT"). + HandlerFunc(middleware.EnforceAccess(policy.AppSnapshotsettingsWrite, handler.SaveSnapshotSchedule)) + r.Name("SaveSnapshotRetention").Path("/api/v1/app/{appSlug}/snapshot/retention").Methods("PUT"). + HandlerFunc(middleware.EnforceAccess(policy.AppSnapshotsettingsWrite, handler.SaveSnapshotRetention)) // Global snapshot routes r.Name("ListInstanceBackups").Path("/api/v1/snapshots").Methods("GET"). @@ -235,8 +237,10 @@ func RegisterSessionAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOT HandlerFunc(middleware.EnforceAccess(policy.BackupWrite, handler.CreateInstanceBackup)) r.Name("GetInstanceSnapshotConfig").Path("/api/v1/snapshot/config").Methods("GET"). HandlerFunc(middleware.EnforceAccess(policy.SnapshotsettingsRead, handler.GetInstanceSnapshotConfig)) - r.Name("SaveInstanceSnapshotConfig").Path("/api/v1/snapshot/config").Methods("PUT"). - HandlerFunc(middleware.EnforceAccess(policy.SnapshotsettingsWrite, handler.SaveInstanceSnapshotConfig)) + r.Name("SaveInstanceSnapshotSchedule").Path("/api/v1/snapshot/schedule").Methods("PUT"). + HandlerFunc(middleware.EnforceAccess(policy.SnapshotsettingsWrite, handler.SaveInstanceSnapshotSchedule)) + r.Name("SaveInstanceSnapshotRetention").Path("/api/v1/snapshot/retention").Methods("PUT"). + HandlerFunc(middleware.EnforceAccess(policy.SnapshotsettingsWrite, handler.SaveInstanceSnapshotRetention)) r.Name("GetGlobalSnapshotSettings").Path("/api/v1/snapshots/settings").Methods("GET"). HandlerFunc(middleware.EnforceAccess(policy.SnapshotsettingsRead, handler.GetGlobalSnapshotSettings)) r.Name("UpdateGlobalSnapshotSettings").Path("/api/v1/snapshots/settings").Methods("PUT"). diff --git a/pkg/handlers/handlers_test.go b/pkg/handlers/handlers_test.go index 18804302e8..6af55e1e26 100644 --- a/pkg/handlers/handlers_test.go +++ b/pkg/handlers/handlers_test.go @@ -976,13 +976,24 @@ var HandlerPolicyTests = map[string][]HandlerPolicyTest{ ExpectStatus: http.StatusOK, }, }, - "SaveSnapshotConfig": { + "SaveSnapshotSchedule": { { Vars: map[string]string{"appSlug": "my-app"}, Roles: []rbactypes.Role{rbac.ClusterAdminRole}, SessionRoles: []string{rbac.ClusterAdminRoleID}, Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { - handlerRecorder.SaveSnapshotConfig(gomock.Any(), gomock.Any()) + handlerRecorder.SaveSnapshotSchedule(gomock.Any(), gomock.Any()) + }, + ExpectStatus: http.StatusOK, + }, + }, + "SaveSnapshotRetention": { + { + Vars: map[string]string{"appSlug": "my-app"}, + Roles: []rbactypes.Role{rbac.ClusterAdminRole}, + SessionRoles: []string{rbac.ClusterAdminRoleID}, + Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { + handlerRecorder.SaveSnapshotRetention(gomock.Any(), gomock.Any()) }, ExpectStatus: http.StatusOK, }, @@ -1018,12 +1029,22 @@ var HandlerPolicyTests = map[string][]HandlerPolicyTest{ ExpectStatus: http.StatusOK, }, }, - "SaveInstanceSnapshotConfig": { + "SaveInstanceSnapshotSchedule": { + { + Roles: []rbactypes.Role{rbac.ClusterAdminRole}, + SessionRoles: []string{rbac.ClusterAdminRoleID}, + Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { + handlerRecorder.SaveInstanceSnapshotSchedule(gomock.Any(), gomock.Any()) + }, + ExpectStatus: http.StatusOK, + }, + }, + "SaveInstanceSnapshotRetention": { { Roles: []rbactypes.Role{rbac.ClusterAdminRole}, SessionRoles: []string{rbac.ClusterAdminRoleID}, Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { - handlerRecorder.SaveInstanceSnapshotConfig(gomock.Any(), gomock.Any()) + handlerRecorder.SaveInstanceSnapshotRetention(gomock.Any(), gomock.Any()) }, ExpectStatus: http.StatusOK, }, diff --git a/pkg/handlers/interface.go b/pkg/handlers/interface.go index eef23d8939..6e10c76a5a 100644 --- a/pkg/handlers/interface.go +++ b/pkg/handlers/interface.go @@ -111,13 +111,15 @@ type KOTSHandler interface { GetRestoreDetails(w http.ResponseWriter, r *http.Request) ListBackups(w http.ResponseWriter, r *http.Request) GetSnapshotConfig(w http.ResponseWriter, r *http.Request) - SaveSnapshotConfig(w http.ResponseWriter, r *http.Request) + SaveSnapshotSchedule(w http.ResponseWriter, r *http.Request) + SaveSnapshotRetention(w http.ResponseWriter, r *http.Request) // Global snapshot routes ListInstanceBackups(w http.ResponseWriter, r *http.Request) CreateInstanceBackup(w http.ResponseWriter, r *http.Request) GetInstanceSnapshotConfig(w http.ResponseWriter, r *http.Request) - SaveInstanceSnapshotConfig(w http.ResponseWriter, r *http.Request) + SaveInstanceSnapshotSchedule(w http.ResponseWriter, r *http.Request) + SaveInstanceSnapshotRetention(w http.ResponseWriter, r *http.Request) GetGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) GetFileSystemSnapshotProviderInstructions(w http.ResponseWriter, r *http.Request) diff --git a/pkg/handlers/mock/mock.go b/pkg/handlers/mock/mock.go index 3ffdb10508..0eb87eb88c 100644 --- a/pkg/handlers/mock/mock.go +++ b/pkg/handlers/mock/mock.go @@ -1258,28 +1258,52 @@ func (mr *MockKOTSHandlerMockRecorder) ResumeInstallOnline(w, r interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeInstallOnline", reflect.TypeOf((*MockKOTSHandler)(nil).ResumeInstallOnline), w, r) } -// SaveInstanceSnapshotConfig mocks base method. -func (m *MockKOTSHandler) SaveInstanceSnapshotConfig(w http.ResponseWriter, r *http.Request) { +// SaveInstanceSnapshotRetention mocks base method. +func (m *MockKOTSHandler) SaveInstanceSnapshotRetention(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SaveInstanceSnapshotConfig", w, r) + m.ctrl.Call(m, "SaveInstanceSnapshotRetention", w, r) } -// SaveInstanceSnapshotConfig indicates an expected call of SaveInstanceSnapshotConfig. -func (mr *MockKOTSHandlerMockRecorder) SaveInstanceSnapshotConfig(w, r interface{}) *gomock.Call { +// SaveInstanceSnapshotRetention indicates an expected call of SaveInstanceSnapshotRetention. +func (mr *MockKOTSHandlerMockRecorder) SaveInstanceSnapshotRetention(w, r interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveInstanceSnapshotConfig", reflect.TypeOf((*MockKOTSHandler)(nil).SaveInstanceSnapshotConfig), w, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveInstanceSnapshotRetention", reflect.TypeOf((*MockKOTSHandler)(nil).SaveInstanceSnapshotRetention), w, r) } -// SaveSnapshotConfig mocks base method. -func (m *MockKOTSHandler) SaveSnapshotConfig(w http.ResponseWriter, r *http.Request) { +// SaveInstanceSnapshotSchedule mocks base method. +func (m *MockKOTSHandler) SaveInstanceSnapshotSchedule(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SaveSnapshotConfig", w, r) + m.ctrl.Call(m, "SaveInstanceSnapshotSchedule", w, r) } -// SaveSnapshotConfig indicates an expected call of SaveSnapshotConfig. -func (mr *MockKOTSHandlerMockRecorder) SaveSnapshotConfig(w, r interface{}) *gomock.Call { +// SaveInstanceSnapshotSchedule indicates an expected call of SaveInstanceSnapshotSchedule. +func (mr *MockKOTSHandlerMockRecorder) SaveInstanceSnapshotSchedule(w, r interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSnapshotConfig", reflect.TypeOf((*MockKOTSHandler)(nil).SaveSnapshotConfig), w, r) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveInstanceSnapshotSchedule", reflect.TypeOf((*MockKOTSHandler)(nil).SaveInstanceSnapshotSchedule), w, r) +} + +// SaveSnapshotRetention mocks base method. +func (m *MockKOTSHandler) SaveSnapshotRetention(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SaveSnapshotRetention", w, r) +} + +// SaveSnapshotRetention indicates an expected call of SaveSnapshotRetention. +func (mr *MockKOTSHandlerMockRecorder) SaveSnapshotRetention(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSnapshotRetention", reflect.TypeOf((*MockKOTSHandler)(nil).SaveSnapshotRetention), w, r) +} + +// SaveSnapshotSchedule mocks base method. +func (m *MockKOTSHandler) SaveSnapshotSchedule(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SaveSnapshotSchedule", w, r) +} + +// SaveSnapshotSchedule indicates an expected call of SaveSnapshotSchedule. +func (mr *MockKOTSHandlerMockRecorder) SaveSnapshotSchedule(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSnapshotSchedule", reflect.TypeOf((*MockKOTSHandler)(nil).SaveSnapshotSchedule), w, r) } // SetAppConfigValues mocks base method. diff --git a/pkg/handlers/snapshots.go b/pkg/handlers/snapshots.go index 0acf8e23a2..86063ef51d 100644 --- a/pkg/handlers/snapshots.go +++ b/pkg/handlers/snapshots.go @@ -552,12 +552,10 @@ func (h *Handler) GetVeleroStatus(w http.ResponseWriter, r *http.Request) { JSON(w, http.StatusOK, getVeleroStatusResponse) } -type SaveSnapshotConfigRequest struct { - AppID string `json:"appId"` - InputValue string `json:"inputValue"` - InputTimeUnit string `json:"inputTimeUnit"` - Schedule string `json:"schedule"` - AutoEnabled bool `json:"autoEnabled"` +type SaveSnapshotScheduleRequest struct { + AppID string `json:"appId"` + Schedule string `json:"schedule"` + AutoEnabled bool `json:"autoEnabled"` } type SaveSnapshotConfigResponse struct { @@ -565,7 +563,7 @@ type SaveSnapshotConfigResponse struct { Error string `json:"error,omitempty"` } -func (h *Handler) SaveSnapshotConfig(w http.ResponseWriter, r *http.Request) { +func (h *Handler) SaveSnapshotSchedule(w http.ResponseWriter, r *http.Request) { responseBody := SaveSnapshotConfigResponse{} // check minimal rbac @@ -573,7 +571,7 @@ func (h *Handler) SaveSnapshotConfig(w http.ResponseWriter, r *http.Request) { return } - requestBody := SaveSnapshotConfigRequest{} + requestBody := SaveSnapshotScheduleRequest{} if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { logger.Error(err) responseBody.Error = "failed to decode request body" @@ -589,24 +587,6 @@ func (h *Handler) SaveSnapshotConfig(w http.ResponseWriter, r *http.Request) { return } - retention, err := snapshot.FormatTTL(requestBody.InputValue, requestBody.InputTimeUnit) - if err != nil { - logger.Error(err) - responseBody.Error = fmt.Sprintf("Invalid snapshot retention: %s %s", requestBody.InputValue, requestBody.InputTimeUnit) - JSON(w, http.StatusBadRequest, responseBody) - return - } - - if app.SnapshotTTL != retention { - app.SnapshotTTL = retention - if err := store.GetStore().SetSnapshotTTL(app.ID, retention); err != nil { - logger.Error(err) - responseBody.Error = "Failed to set snapshot retention" - JSON(w, http.StatusInternalServerError, responseBody) - return - } - } - if !requestBody.AutoEnabled { if err := store.GetStore().SetSnapshotSchedule(app.ID, ""); err != nil { logger.Error(err) @@ -660,6 +640,58 @@ func (h *Handler) SaveSnapshotConfig(w http.ResponseWriter, r *http.Request) { JSON(w, http.StatusOK, responseBody) } +type SaveSnapshotRetentionRequest struct { + AppID string `json:"appId"` + InputValue string `json:"inputValue"` + InputTimeUnit string `json:"inputTimeUnit"` +} + +func (h *Handler) SaveSnapshotRetention(w http.ResponseWriter, r *http.Request) { + responseBody := SaveSnapshotConfigResponse{} + + // check minimal rbac + if err := requiresKotsadmVeleroAccess(w, r); err != nil { + return + } + + requestBody := SaveSnapshotRetentionRequest{} + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + logger.Error(err) + responseBody.Error = "failed to decode request body" + JSON(w, http.StatusBadRequest, responseBody) + return + } + + app, err := store.GetStore().GetApp(requestBody.AppID) + if err != nil { + logger.Error(err) + responseBody.Error = "Failed to get app" + JSON(w, http.StatusInternalServerError, responseBody) + return + } + + retention, err := snapshot.FormatTTL(requestBody.InputValue, requestBody.InputTimeUnit) + if err != nil { + logger.Error(err) + responseBody.Error = fmt.Sprintf("Invalid snapshot retention: %s %s", requestBody.InputValue, requestBody.InputTimeUnit) + JSON(w, http.StatusBadRequest, responseBody) + return + } + + if app.SnapshotTTL != retention { + app.SnapshotTTL = retention + if err := store.GetStore().SetSnapshotTTL(app.ID, retention); err != nil { + logger.Error(err) + responseBody.Error = "Failed to set snapshot retention" + JSON(w, http.StatusInternalServerError, responseBody) + return + } + } + + responseBody.Success = true + JSON(w, http.StatusOK, responseBody) +} + type InstanceSnapshotConfig struct { AutoEnabled bool `json:"autoEnabled"` AutoSchedule *snapshottypes.SnapshotSchedule `json:"autoSchedule"` @@ -713,11 +745,9 @@ func (h *Handler) GetInstanceSnapshotConfig(w http.ResponseWriter, r *http.Reque JSON(w, http.StatusOK, getInstanceSnapshotConfigResponse) } -type SaveInstanceSnapshotConfigRequest struct { - InputValue string `json:"inputValue"` - InputTimeUnit string `json:"inputTimeUnit"` - Schedule string `json:"schedule"` - AutoEnabled bool `json:"autoEnabled"` +type SaveInstanceSnapshotScheduleRequest struct { + Schedule string `json:"schedule"` + AutoEnabled bool `json:"autoEnabled"` } type SaveInstanceSnapshotConfigResponse struct { @@ -725,7 +755,7 @@ type SaveInstanceSnapshotConfigResponse struct { Error string `json:"error,omitempty"` } -func (h *Handler) SaveInstanceSnapshotConfig(w http.ResponseWriter, r *http.Request) { +func (h *Handler) SaveInstanceSnapshotSchedule(w http.ResponseWriter, r *http.Request) { responseBody := SaveInstanceSnapshotConfigResponse{} // check minimal rbac @@ -733,7 +763,7 @@ func (h *Handler) SaveInstanceSnapshotConfig(w http.ResponseWriter, r *http.Requ return } - requestBody := SaveInstanceSnapshotConfigRequest{} + requestBody := SaveInstanceSnapshotScheduleRequest{} if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { logger.Error(err) responseBody.Error = "failed to decode request body" @@ -757,24 +787,6 @@ func (h *Handler) SaveInstanceSnapshotConfig(w http.ResponseWriter, r *http.Requ } c := clusters[0] - retention, err := snapshot.FormatTTL(requestBody.InputValue, requestBody.InputTimeUnit) - if err != nil { - logger.Error(err) - responseBody.Error = fmt.Sprintf("Invalid instance snapshot retention: %s %s", requestBody.InputValue, requestBody.InputTimeUnit) - JSON(w, http.StatusBadRequest, responseBody) - return - } - - if c.SnapshotTTL != retention { - c.SnapshotTTL = retention - if err := store.GetStore().SetInstanceSnapshotTTL(c.ClusterID, retention); err != nil { - logger.Error(err) - responseBody.Error = "Failed to set instance snapshot retention" - JSON(w, http.StatusInternalServerError, responseBody) - return - } - } - if !requestBody.AutoEnabled { if err := store.GetStore().SetInstanceSnapshotSchedule(c.ClusterID, ""); err != nil { logger.Error(err) @@ -828,6 +840,65 @@ func (h *Handler) SaveInstanceSnapshotConfig(w http.ResponseWriter, r *http.Requ JSON(w, http.StatusOK, responseBody) } +type SaveInstanceSnapshotRetentionRequest struct { + InputValue string `json:"inputValue"` + InputTimeUnit string `json:"inputTimeUnit"` +} + +func (h *Handler) SaveInstanceSnapshotRetention(w http.ResponseWriter, r *http.Request) { + responseBody := SaveInstanceSnapshotConfigResponse{} + + // check minimal rbac + if err := requiresKotsadmVeleroAccess(w, r); err != nil { + return + } + + requestBody := SaveInstanceSnapshotRetentionRequest{} + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + logger.Error(err) + responseBody.Error = "failed to decode request body" + JSON(w, http.StatusBadRequest, responseBody) + return + } + + clusters, err := store.GetStore().ListClusters() + if err != nil { + logger.Error(err) + responseBody.Error = "Failed to list clusters" + JSON(w, http.StatusInternalServerError, responseBody) + return + } + if len(clusters) == 0 { + err := errors.New("No clusters found") + logger.Error(err) + responseBody.Error = err.Error() + JSON(w, http.StatusInternalServerError, responseBody) + return + } + c := clusters[0] + + retention, err := snapshot.FormatTTL(requestBody.InputValue, requestBody.InputTimeUnit) + if err != nil { + logger.Error(err) + responseBody.Error = fmt.Sprintf("Invalid instance snapshot retention: %s %s", requestBody.InputValue, requestBody.InputTimeUnit) + JSON(w, http.StatusBadRequest, responseBody) + return + } + + if c.SnapshotTTL != retention { + c.SnapshotTTL = retention + if err := store.GetStore().SetInstanceSnapshotTTL(c.ClusterID, retention); err != nil { + logger.Error(err) + responseBody.Error = "Failed to set instance snapshot retention" + JSON(w, http.StatusInternalServerError, responseBody) + return + } + } + + responseBody.Success = true + JSON(w, http.StatusOK, responseBody) +} + func requiresKotsadmVeleroAccess(w http.ResponseWriter, r *http.Request) error { kotsadmNamespace := util.PodNamespace requiresVeleroAccess, err := kotssnapshot.CheckKotsadmVeleroAccess(r.Context(), kotsadmNamespace) diff --git a/web/src/components/snapshots/SnapshotSchedule.jsx b/web/src/components/snapshots/SnapshotSchedule.jsx index 1aba09bfad..eab466f78f 100644 --- a/web/src/components/snapshots/SnapshotSchedule.jsx +++ b/web/src/components/snapshots/SnapshotSchedule.jsx @@ -2,10 +2,10 @@ import { Component } from "react"; import Select from "react-select"; import { withRouter } from "@src/utilities/react-router-utilities"; import { - Utilities, getCronFrequency, getCronInterval, getReadableCronDescriptor, + Utilities, } from "../../utilities/utilities"; import ErrorModal from "../modals/ErrorModal"; import Loader from "../shared/Loader"; @@ -289,20 +289,16 @@ class SnapshotSchedule extends Component { if (isAppConfig) { body = { appId: this.state.selectedApp.id, - inputValue: this.state.retentionInput, - inputTimeUnit: this.state.selectedRetentionUnit?.value, schedule: this.state.frequency, autoEnabled: this.state.autoEnabled, }; - url = `${process.env.API_ENDPOINT}/app/${this.state.selectedApp.slug}/snapshot/config`; + url = `${process.env.API_ENDPOINT}/app/${this.state.selectedApp.slug}/snapshot/schedule`; } else { body = { - inputValue: this.state.retentionInput, - inputTimeUnit: this.state.selectedRetentionUnit?.value, schedule: this.state.frequency, autoEnabled: this.state.autoEnabled, }; - url = `${process.env.API_ENDPOINT}/snapshot/config`; + url = `${process.env.API_ENDPOINT}/snapshot/schedule`; } fetch(url, { headers: { @@ -336,7 +332,7 @@ class SnapshotSchedule extends Component { } this.setState({ updateScheduleErrMsg: - data.error || "Failed to save snapshot config", + data.error || "Failed to save snapshot schedule", messageType: "error", updatingSchedule: false, }); @@ -366,6 +362,88 @@ class SnapshotSchedule extends Component { }); }; + saveRetentionConfig = () => { + const isAppConfig = this.checkIsAppConfig(); + + this.setState({ updatingRetention: true }); + let body; + let url; + if (isAppConfig) { + body = { + appId: this.state.selectedApp.id, + inputValue: this.state.retentionInput, + inputTimeUnit: this.state.selectedRetentionUnit?.value, + }; + url = `${process.env.API_ENDPOINT}/app/${this.state.selectedApp.slug}/snapshot/retention`; + } else { + body = { + inputValue: this.state.retentionInput, + inputTimeUnit: this.state.selectedRetentionUnit?.value, + }; + url = `${process.env.API_ENDPOINT}/snapshot/retention`; + } + fetch(url, { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + method: "PUT", + body: JSON.stringify(body), + }) + .then(async (res) => { + if (!res.ok && res.status === 409) { + const result = await res.json(); + if (result.kotsadmRequiresVeleroAccess) { + this.setState({ + updatingRetention: false, + }); + this.props.openConfigureSnapshotsMinimalRBACModal( + result.kotsadmRequiresVeleroAccess, + result.kotsadmNamespace + ); + return; + } + } + + const data = await res.json(); + if (!res.ok || !data.success) { + if (res.status === 401) { + Utilities.logoutUser(); + return; + } + this.setState({ + updateRetentionErrMsg: + data.error || "Failed to save snapshot retention", + messageType: "error", + updatingRetention: false, + }); + return; + } + + this.setState({ + updatingRetention: false, + updateRetentionConfirm: true, + updateRetentionErrMsg: " ", + }); + + if (this.confirmTimeout) { + clearTimeout(this.confirmTimeout); + } + this.confirmTimeout = setTimeout(() => { + this.setState({ updateRetentionConfirm: false }); + }, 5000); + }) + .catch((err) => { + console.log(err); + this.setState({ + updateRetentionErrMsg: err ? err.message : "Failed to connect to API", + messageType: "error", + updatingRetention: false, + }); + }); + }; + toggleErrorModal = () => { this.setState({ displayErrorModal: !this.state.displayErrorModal }); }; @@ -402,9 +480,12 @@ class SnapshotSchedule extends Component { const { hasValidCron, updatingSchedule, + updatingRetention, updateConfirm, + updateRetentionConfirm, loadingConfig, updateScheduleErrMsg, + updateRetentionErrMsg, } = this.state; const selectedRetentionUnit = RETENTION_UNITS.find((ru) => { return ru.value === this.state.selectedRetentionUnit?.value; @@ -455,11 +536,11 @@ class SnapshotSchedule extends Component { )}
-

Automatic {featureName}s

+

Scheduled {featureName}s

- Configure a schedule and retention policy for {featureName}s of - the admin console and all application data. + Configure a schedule for {featureName}s of the admin console and + all application data.

{!isEmbeddedCluster && ( @@ -527,7 +608,7 @@ class SnapshotSchedule extends Component { >

- Enable automatic scheduled {featureName}s + Enable scheduled {featureName}s

@@ -535,7 +616,7 @@ class SnapshotSchedule extends Component {
{this.state.autoEnabled && ( -
+

@@ -587,10 +668,55 @@ class SnapshotSchedule extends Component { )}

)} +
+ + {updateConfirm && ( +
+ + + Schedule updated + +
+ )} + {updateScheduleErrMsg && ( +
+ + {updateScheduleErrMsg} + +
+ )} +
+
+
+
+
 
+ {/*start of retention box*/} +
+
+

Retention policy

+
+

+ Configure the retention policy for {featureName}s of the admin + console and all application data. This applies to both manual + and scheduled {featureName}s. +

+
+
-

- Retention policy -

Choose how long to retain {featureName}s before they are automatically deleted. @@ -627,12 +753,14 @@ class SnapshotSchedule extends Component {

- {updateConfirm && ( + {updateRetentionConfirm && (
- Schedule updated + Retention updated
)} - {updateScheduleErrMsg && ( + {updateRetentionErrMsg && (
- {updateScheduleErrMsg} + {updateRetentionErrMsg}
)}