From 32a309438691a17c8fe9240cb3c1fec495aeb69f Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Thu, 10 Feb 2022 10:34:56 -0700 Subject: [PATCH] Added lifecycle rule edit capability (#1539) Signed-off-by: Benjamin Perez Co-authored-by: Benjamin Perez --- models/expiration_response.go | 3 + models/lifecycle_rule_type.go | 95 ++++ models/transition_response.go | 6 + models/update_bucket_lifecycle.go | 60 +++ .../BucketDetails/AddLifecycleModal.tsx | 2 +- .../BucketDetails/BucketLifecyclePanel.tsx | 22 +- .../EditLifecycleConfiguration.tsx | 456 +++++++++++++++--- .../src/screens/Console/Buckets/types.tsx | 4 + .../RadioGroupSelector/RadioGroupSelector.tsx | 1 + restapi/embedded_spec.go | 44 ++ restapi/user_buckets_lifecycle.go | 44 +- restapi/user_buckets_lifecycle_test.go | 30 ++ swagger-console.yml | 16 + 13 files changed, 690 insertions(+), 93 deletions(-) create mode 100644 models/lifecycle_rule_type.go diff --git a/models/expiration_response.go b/models/expiration_response.go index 394e40974..c36fd8760 100644 --- a/models/expiration_response.go +++ b/models/expiration_response.go @@ -42,6 +42,9 @@ type ExpirationResponse struct { // delete marker DeleteMarker bool `json:"delete_marker,omitempty"` + + // noncurrent expiration days + NoncurrentExpirationDays int64 `json:"noncurrent_expiration_days,omitempty"` } // Validate validates this expiration response diff --git a/models/lifecycle_rule_type.go b/models/lifecycle_rule_type.go new file mode 100644 index 000000000..d37ce7d19 --- /dev/null +++ b/models/lifecycle_rule_type.go @@ -0,0 +1,95 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// LifecycleRuleType lifecycle rule type +// +// swagger:model lifecycleRuleType +type LifecycleRuleType string + +func NewLifecycleRuleType(value LifecycleRuleType) *LifecycleRuleType { + return &value +} + +// Pointer returns a pointer to a freshly-allocated LifecycleRuleType. +func (m LifecycleRuleType) Pointer() *LifecycleRuleType { + return &m +} + +const ( + + // LifecycleRuleTypeExpiry captures enum value "expiry" + LifecycleRuleTypeExpiry LifecycleRuleType = "expiry" + + // LifecycleRuleTypeTransition captures enum value "transition" + LifecycleRuleTypeTransition LifecycleRuleType = "transition" +) + +// for schema +var lifecycleRuleTypeEnum []interface{} + +func init() { + var res []LifecycleRuleType + if err := json.Unmarshal([]byte(`["expiry","transition"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + lifecycleRuleTypeEnum = append(lifecycleRuleTypeEnum, v) + } +} + +func (m LifecycleRuleType) validateLifecycleRuleTypeEnum(path, location string, value LifecycleRuleType) error { + if err := validate.EnumCase(path, location, value, lifecycleRuleTypeEnum, true); err != nil { + return err + } + return nil +} + +// Validate validates this lifecycle rule type +func (m LifecycleRuleType) Validate(formats strfmt.Registry) error { + var res []error + + // value enum + if err := m.validateLifecycleRuleTypeEnum("", "body", m); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validates this lifecycle rule type based on context it is used +func (m LifecycleRuleType) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/models/transition_response.go b/models/transition_response.go index 19ac6997d..00b73fc02 100644 --- a/models/transition_response.go +++ b/models/transition_response.go @@ -40,6 +40,12 @@ type TransitionResponse struct { // days Days int64 `json:"days,omitempty"` + // noncurrent storage class + NoncurrentStorageClass string `json:"noncurrent_storage_class,omitempty"` + + // noncurrent transition days + NoncurrentTransitionDays int64 `json:"noncurrent_transition_days,omitempty"` + // storage class StorageClass string `json:"storage_class,omitempty"` } diff --git a/models/update_bucket_lifecycle.go b/models/update_bucket_lifecycle.go index aee949e74..564de9aee 100644 --- a/models/update_bucket_lifecycle.go +++ b/models/update_bucket_lifecycle.go @@ -24,9 +24,12 @@ package models import ( "context" + "encoding/json" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" + "github.com/go-openapi/validate" ) // UpdateBucketLifecycle update bucket lifecycle @@ -63,10 +66,67 @@ type UpdateBucketLifecycle struct { // Required in case of transition_date or expiry fields are not set. it defines a transition days for ILM TransitionDays int32 `json:"transition_days,omitempty"` + + // ILM Rule type (Expiry or transition) + // Required: true + // Enum: [expiry transition] + Type *string `json:"type"` } // Validate validates this update bucket lifecycle func (m *UpdateBucketLifecycle) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateType(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +var updateBucketLifecycleTypeTypePropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["expiry","transition"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + updateBucketLifecycleTypeTypePropEnum = append(updateBucketLifecycleTypeTypePropEnum, v) + } +} + +const ( + + // UpdateBucketLifecycleTypeExpiry captures enum value "expiry" + UpdateBucketLifecycleTypeExpiry string = "expiry" + + // UpdateBucketLifecycleTypeTransition captures enum value "transition" + UpdateBucketLifecycleTypeTransition string = "transition" +) + +// prop value enum +func (m *UpdateBucketLifecycle) validateTypeEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, updateBucketLifecycleTypeTypePropEnum, true); err != nil { + return err + } + return nil +} + +func (m *UpdateBucketLifecycle) validateType(formats strfmt.Registry) error { + + if err := validate.Required("type", "body", m.Type); err != nil { + return err + } + + // value enum + if err := m.validateTypeEnum("type", "body", *m.Type); err != nil { + return err + } + return nil } diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/AddLifecycleModal.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/AddLifecycleModal.tsx index eb8315de0..1b3916286 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/AddLifecycleModal.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/AddLifecycleModal.tsx @@ -51,7 +51,7 @@ interface IReplicationModal { setModalErrorSnackMessage: typeof setModalErrorSnackMessage; } -interface ITiersDropDown { +export interface ITiersDropDown { label: string; value: string; } diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx index e4217b155..48b0b350a 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx @@ -71,6 +71,8 @@ const BucketLifecyclePanel = ({ const [lifecycleRecords, setLifecycleRecords] = useState([]); const [addLifecycleOpen, setAddLifecycleOpen] = useState(false); const [editLifecycleOpen, setEditLifecycleOpen] = useState(false); + const [selectedLifecycleRule, setSelectedLifecycleRule] = + useState(null); const bucketName = match.params["bucketName"]; @@ -112,6 +114,7 @@ const BucketLifecyclePanel = ({ const closeEditLCAndRefresh = (refresh: boolean) => { setEditLifecycleOpen(false); + setSelectedLifecycleRule(null); if (refresh) { setLoadingLifecycle(true); } @@ -182,16 +185,25 @@ const BucketLifecyclePanel = ({ }, ]; + const lifecycleActions = [ + { + type: "view", + + onClick(valueToSend: any): any { + setSelectedLifecycleRule(valueToSend); + setEditLifecycleOpen(true); + }, + }, + ]; + return ( - {editLifecycleOpen && ( + {editLifecycleOpen && selectedLifecycleRule && ( )} {addLifecycleOpen && ( @@ -232,7 +244,7 @@ const BucketLifecyclePanel = ({ errorProps={{ disabled: true }} > . -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, Fragment } from "react"; import { connect } from "react-redux"; -import Grid from "@mui/material/Grid"; -import { Button, LinearProgress } from "@mui/material"; +import { Button, LinearProgress, SelectChangeEvent } from "@mui/material"; import { Theme } from "@mui/material/styles"; +import get from "lodash/get"; +import Grid from "@mui/material/Grid"; import createStyles from "@mui/styles/createStyles"; import withStyles from "@mui/styles/withStyles"; -import { modalBasic } from "../../Common/FormComponents/common/styleLibrary"; +import { + createTenantCommon, + formFieldStyles, + modalStyleUtils, + spacingUtils, +} from "../../Common/FormComponents/common/styleLibrary"; import { setModalErrorSnackMessage } from "../../../../actions"; import { LifeCycleItem } from "../types"; import { ErrorResponseHandler } from "../../../../common/types"; +import { LifecycleConfigIcon } from "../../../../icons"; +import { ITiersDropDown } from "./AddLifecycleModal"; +import { + ITierElement, + ITierResponse, +} from "../../Configurations/TiersConfiguration/types"; import api from "../../../../common/api"; import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector"; -import { LifecycleConfigIcon } from "../../../../icons"; +import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper"; +import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector"; const styles = (theme: Theme) => createStyles({ - buttonContainer: { - textAlign: "right", + dateSelector: { + "& div": { + borderBottom: 0, + marginBottom: 0, + + "& div:nth-child(2)": { + border: "1px solid #EAEAEA", + paddingLeft: 5, + + "& div": { + border: 0, + }, + }, + }, }, - ...modalBasic, + ...spacingUtils, + ...modalStyleUtils, + ...formFieldStyles, + ...createTenantCommon, }); interface IAddUserContentProps { @@ -57,15 +85,120 @@ const EditLifecycleConfiguration = ({ open, setModalErrorSnackMessage, }: IAddUserContentProps) => { + const [loadingTiers, setLoadingTiers] = useState(true); const [addLoading, setAddLoading] = useState(false); const [tags, setTags] = useState(""); const [enabled, setEnabled] = useState(false); + const [tiersList, setTiersList] = useState([]); + const [prefix, setPrefix] = useState(""); + const [storageClass, setStorageClass] = useState(""); + const [NCTransitionSC, setNCTransitionSC] = useState(""); + const [expiredObjectDM, setExpiredObjectDM] = useState(false); + const [NCExpirationDays, setNCExpirationDays] = useState("0"); + const [NCTransitionDays, setNCTransitionDays] = useState("0"); + const [ilmType, setIlmType] = useState("expiry"); + const [expiryDays, setExpiryDays] = useState("0"); + const [transitionDays, setTransitionDays] = useState("0"); + const [isFormValid, setIsFormValid] = useState(false); useEffect(() => { + if (loadingTiers) { + api + .invoke("GET", `/api/v1/admin/tiers`) + .then((res: ITierResponse) => { + const tiersList: ITierElement[] | null = get(res, "items", []); + + if (tiersList !== null && tiersList.length >= 1) { + const objList = tiersList.map((tier: ITierElement) => { + const tierType = tier.type; + const value = get(tier, `${tierType}.name`, ""); + + return { label: value, value: value }; + }); + + setTiersList(objList); + if (objList.length > 0) { + setStorageClass(objList[0].value); + } + } + setLoadingTiers(false); + }) + .catch((err: ErrorResponseHandler) => { + setLoadingTiers(false); + }); + } + }, [loadingTiers]); + + useEffect(() => { + let valid = true; + + if (ilmType !== "expiry") { + if (storageClass === "") { + valid = false; + } + } + setIsFormValid(valid); + }, [ilmType, expiryDays, transitionDays, storageClass]); + + useEffect(() => { + console.log("lifecycle::", lifecycle); if (lifecycle.status === "Enabled") { setEnabled(true); } + let transitionMode = false; + + if (lifecycle.transition) { + if (lifecycle.transition.days && lifecycle.transition.days !== 0) { + setTransitionDays(lifecycle.transition.days.toString()); + setIlmType("transition"); + transitionMode = true; + } + + // Fallback to old rules by date + if ( + lifecycle.transition.date && + lifecycle.transition.date !== "0001-01-01T00:00:00Z" + ) { + setIlmType("transition"); + transitionMode = true; + } + } + + if (lifecycle.expiration) { + if (lifecycle.expiration.days && lifecycle.expiration.days !== 0) { + setExpiryDays(lifecycle.expiration.days.toString()); + setIlmType("expiry"); + transitionMode = false; + } + + // Fallback to old rules by date + if ( + lifecycle.expiration.date && + lifecycle.expiration.date !== "0001-01-01T00:00:00Z" + ) { + setIlmType("expiry"); + transitionMode = false; + } + } + + // Transition fields + if (transitionMode) { + setStorageClass(lifecycle.transition?.storage_class || ""); + setNCTransitionDays( + lifecycle.transition?.noncurrent_transition_days?.toString() || "0" + ); + setNCTransitionSC(lifecycle.transition?.noncurrent_storage_class || ""); + } else { + // Expiry fields + setNCExpirationDays( + lifecycle.expiration?.noncurrent_expiration_days?.toString() || "0" + ); + } + + setExpiredObjectDM(!!lifecycle.expiration?.delete_marker); + setPrefix(lifecycle.prefix || ""); + if (lifecycle.tags) { const tgs = lifecycle.tags.reduce( (stringLab: string, currItem: any, index: number) => { @@ -88,14 +221,44 @@ const EditLifecycleConfiguration = ({ } setAddLoading(true); if (selectedBucket !== null && lifecycle !== null) { + let rules = {}; + + if (ilmType === "expiry") { + let expiry = { + expiry_days: parseInt(expiryDays), + }; + + rules = { + ...expiry, + noncurrentversion_expiration_days: parseInt(NCExpirationDays), + }; + } else { + let transition = { + transition_days: parseInt(transitionDays), + }; + + rules = { + ...transition, + noncurrentversion_transition_days: parseInt(NCTransitionDays), + noncurrentversion_transition_storage_class: NCTransitionSC, + storage_class: storageClass, + }; + } + + const lifecycleUpdate = { + type: ilmType, + disable: !enabled, + prefix, + tags, + expired_object_delete_marker: expiredObjectDM, + ...rules, + }; + api .invoke( "PUT", `/api/v1/buckets/${selectedBucket}/lifecycle/${lifecycle.id}`, - { - disable: !enabled, - tags: tags, - } + lifecycleUpdate ) .then((res) => { setAddLoading(false); @@ -117,72 +280,217 @@ const EditLifecycleConfiguration = ({ title={"Edit Lifecycle Configuration"} titleIcon={} > -
- { - setEnabled(e.target.checked); - }} - switchOnly - /> -
+
) => { + saveRecord(e); + }} + > + + + + {}} + disabled + /> + + + { + setEnabled(e.target.checked); + }} + /> + + +
+ + Lifecycle Configuration + - - ) => { - saveRecord(e); - }} - > - - - - {}} - disabled - /> - - - { - setTags(vl); - }} - keyPlaceholder="Tag Key" - valuePlaceholder="Tag Value" - withBorder - /> - + + {}} + disableOptions + /> + + {ilmType === "expiry" ? ( + + + ) => { + setExpiryDays(e.target.value); + }} + label="Expiry Days" + value={expiryDays} + min="0" + /> + + + + ) => { + setNCExpirationDays(e.target.value); + }} + label="Non-current Expiration Days" + value={NCExpirationDays} + min="0" + /> + + + ) : ( + + + ) => { + setTransitionDays(e.target.value); + }} + label="Transition Days" + value={transitionDays} + min="0" + /> + + + ) => { + setNCTransitionDays(e.target.value); + }} + label="Non-current Transition Days" + value={NCTransitionDays} + min="0" + /> + + + ) => { + setNCTransitionSC(e.target.value); + }} + placeholder="Set Non-current Version Transition Storage Class" + label="Non-current Version Transition Storage Class" + value={NCTransitionSC} + /> + + + ) => { + setStorageClass(e.target.value as string); + }} + options={tiersList} + /> + + + )} +
- - + +
+ + File Configuration + + + + ) => { + setPrefix(e.target.value); + }} + label="Prefix" + value={prefix} + /> + + + { + setTags(vl); + }} + keyPlaceholder="Tag Key" + valuePlaceholder="Tag Value" + withBorder + /> + + + ) => { + setExpiredObjectDM(event.target.checked); + }} + label={"Expired Object Delete Marker"} + /> + +
- {addLoading && ( - - - - )}
- - + + + + + {addLoading && ( + + + + )} +
+ ); }; diff --git a/portal-ui/src/screens/Console/Buckets/types.tsx b/portal-ui/src/screens/Console/Buckets/types.tsx index dfe169551..18ff88e2d 100644 --- a/portal-ui/src/screens/Console/Buckets/types.tsx +++ b/portal-ui/src/screens/Console/Buckets/types.tsx @@ -181,12 +181,16 @@ export interface BulkReplicationItem { interface IExpirationLifecycle { days: number; date: string; + delete_marker?: boolean; + noncurrent_expiration_days?: number; } interface ITransitionLifecycle { days: number; date: string; storage_class?: string; + noncurrent_transition_days?: number; + noncurrent_storage_class?: string; } export interface LifeCycleItem { diff --git a/portal-ui/src/screens/Console/Common/FormComponents/RadioGroupSelector/RadioGroupSelector.tsx b/portal-ui/src/screens/Console/Common/FormComponents/RadioGroupSelector/RadioGroupSelector.tsx index 8e9e53835..27e9eaf35 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/RadioGroupSelector/RadioGroupSelector.tsx +++ b/portal-ui/src/screens/Console/Common/FormComponents/RadioGroupSelector/RadioGroupSelector.tsx @@ -146,6 +146,7 @@ export const RadioGroupSelector = ({ [classes.checkedOption]: selectorOption.value === currentSelection, })} + /> ); })} diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 2336034d5..e43e0c3f0 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -4425,6 +4425,10 @@ func init() { }, "delete_marker": { "type": "boolean" + }, + "noncurrent_expiration_days": { + "type": "integer", + "format": "int64" } } }, @@ -5947,6 +5951,13 @@ func init() { "type": "integer", "format": "int64" }, + "noncurrent_storage_class": { + "type": "string" + }, + "noncurrent_transition_days": { + "type": "integer", + "format": "int64" + }, "storage_class": { "type": "string" } @@ -5954,6 +5965,9 @@ func init() { }, "updateBucketLifecycle": { "type": "object", + "required": [ + "type" + ], "properties": { "disable": { "description": "Non required, toggle to disable or enable rule", @@ -6002,6 +6016,14 @@ func init() { "type": "integer", "format": "int32", "default": 0 + }, + "type": { + "description": "ILM Rule type (Expiry or transition)", + "type": "string", + "enum": [ + "expiry", + "transition" + ] } } }, @@ -10694,6 +10716,10 @@ func init() { }, "delete_marker": { "type": "boolean" + }, + "noncurrent_expiration_days": { + "type": "integer", + "format": "int64" } } }, @@ -12216,6 +12242,13 @@ func init() { "type": "integer", "format": "int64" }, + "noncurrent_storage_class": { + "type": "string" + }, + "noncurrent_transition_days": { + "type": "integer", + "format": "int64" + }, "storage_class": { "type": "string" } @@ -12223,6 +12256,9 @@ func init() { }, "updateBucketLifecycle": { "type": "object", + "required": [ + "type" + ], "properties": { "disable": { "description": "Non required, toggle to disable or enable rule", @@ -12271,6 +12307,14 @@ func init() { "type": "integer", "format": "int32", "default": 0 + }, + "type": { + "description": "ILM Rule type (Expiry or transition)", + "type": "string", + "enum": [ + "expiry", + "transition" + ] } } }, diff --git a/restapi/user_buckets_lifecycle.go b/restapi/user_buckets_lifecycle.go index 3879a0424..e43aeb2c7 100644 --- a/restapi/user_buckets_lifecycle.go +++ b/restapi/user_buckets_lifecycle.go @@ -56,7 +56,7 @@ func registerBucketsLifecycleHandlers(api *operations.ConsoleAPI) { api.UserAPIUpdateBucketLifecycleHandler = user_api.UpdateBucketLifecycleHandlerFunc(func(params user_api.UpdateBucketLifecycleParams, session *models.Principal) middleware.Responder { err := getEditBucketLifecycleRule(session, params) if err != nil { - user_api.NewUpdateBucketLifecycleDefault(int(err.Code)).WithPayload(err) + return user_api.NewUpdateBucketLifecycleDefault(int(err.Code)).WithPayload(err) } return user_api.NewUpdateBucketLifecycleOK() @@ -83,13 +83,30 @@ func getBucketLifecycle(ctx context.Context, client MinioClient, bucketName stri }) } + rulePrefix := rule.RuleFilter.And.Prefix + + if rulePrefix == "" { + rulePrefix = rule.RuleFilter.Prefix + } + rules = append(rules, &models.ObjectBucketLifecycle{ - ID: rule.ID, - Status: rule.Status, - Prefix: rule.RuleFilter.And.Prefix, - Expiration: &models.ExpirationResponse{Date: rule.Expiration.Date.Format(time.RFC3339), Days: int64(rule.Expiration.Days), DeleteMarker: rule.Expiration.DeleteMarker.IsEnabled()}, - Transition: &models.TransitionResponse{Date: rule.Transition.Date.Format(time.RFC3339), Days: int64(rule.Transition.Days), StorageClass: rule.Transition.StorageClass}, - Tags: tags, + ID: rule.ID, + Status: rule.Status, + Prefix: rulePrefix, + Expiration: &models.ExpirationResponse{ + Date: rule.Expiration.Date.Format(time.RFC3339), + Days: int64(rule.Expiration.Days), + DeleteMarker: rule.Expiration.DeleteMarker.IsEnabled(), + NoncurrentExpirationDays: int64(rule.NoncurrentVersionExpiration.NoncurrentDays), + }, + Transition: &models.TransitionResponse{ + Date: rule.Transition.Date.Format(time.RFC3339), + Days: int64(rule.Transition.Days), + StorageClass: rule.Transition.StorageClass, + NoncurrentStorageClass: rule.NoncurrentVersionTransition.StorageClass, + NoncurrentTransitionDays: int64(rule.NoncurrentVersionTransition.NoncurrentDays), + }, + Tags: tags, }) } @@ -251,7 +268,7 @@ func getAddBucketLifecycleResponse(session *models.Principal, params user_api.Ad return nil } -// addBucketLifecycle gets lifecycle lists for a bucket from MinIO API and returns their implementations +// editBucketLifecycle gets lifecycle lists for a bucket from MinIO API and updates the selected lifecycle rule func editBucketLifecycle(ctx context.Context, client MinioClient, params user_api.UpdateBucketLifecycleParams) error { // Configuration that is already set. lfcCfg, err := client.getLifecycleRules(ctx, params.BucketName) @@ -268,10 +285,9 @@ func editBucketLifecycle(ctx context.Context, client MinioClient, params user_ap opts := ilm.LifecycleOptions{} // Verify if transition items are set - if params.Body.ExpiryDays == 0 && params.Body.TransitionDays != 0 { - - if params.Body.NoncurrentversionExpirationDays != 0 { - return errors.New("non current version expiration days cannot be set when transition is being configured") + if *params.Body.Type == models.UpdateBucketLifecycleTypeTransition { + if params.Body.TransitionDays == 0 && params.Body.NoncurrentversionTransitionDays == 0 { + return errors.New("you must select transition days or non-current transition days configuration") } opts = ilm.LifecycleOptions{ @@ -285,8 +301,10 @@ func editBucketLifecycle(ctx context.Context, client MinioClient, params user_ap ExpiredObjectDeleteMarker: params.Body.ExpiredObjectDeleteMarker, NoncurrentVersionTransitionDays: int(params.Body.NoncurrentversionTransitionDays), NoncurrentVersionTransitionStorageClass: strings.ToUpper(params.Body.NoncurrentversionTransitionStorageClass), + IsTransitionDaysSet: params.Body.TransitionDays != 0, + IsNoncurrentVersionTransitionDaysSet: params.Body.NoncurrentversionTransitionDays != 0, } - } else if params.Body.TransitionDays == 0 && params.Body.ExpiryDays != 0 { // Verify if expiry configuration is set + } else if *params.Body.Type == models.UpdateBucketLifecycleTypeExpiry { // Verify if expiry configuration is set if params.Body.NoncurrentversionTransitionDays != 0 { return errors.New("non current version Transition Days cannot be set when expiry is being configured") } diff --git a/restapi/user_buckets_lifecycle_test.go b/restapi/user_buckets_lifecycle_test.go index 260b04783..c07f8ea99 100644 --- a/restapi/user_buckets_lifecycle_test.go +++ b/restapi/user_buckets_lifecycle_test.go @@ -226,9 +226,12 @@ func TestUpdateLifecycleRule(t *testing.T) { // Test-2 : editBucketLifecycle() Update lifecycle rule + expiryRule := "expiry" + editMock := user_api.UpdateBucketLifecycleParams{ BucketName: "testBucket", Body: &models.UpdateBucketLifecycle{ + Type: &expiryRule, Disable: false, ExpiredObjectDeleteMarker: false, ExpiryDays: int32(16), @@ -250,6 +253,33 @@ func TestUpdateLifecycleRule(t *testing.T) { assert.Equal(nil, err, fmt.Sprintf("Failed on %s: Error returned", function)) + // Test-2a : editBucketLifecycle() Update lifecycle rule + + transitionRule := "transition" + + editMock = user_api.UpdateBucketLifecycleParams{ + BucketName: "testBucket", + Body: &models.UpdateBucketLifecycle{ + Type: &transitionRule, + Disable: false, + ExpiredObjectDeleteMarker: false, + NoncurrentversionTransitionDays: 5, + Prefix: "pref1", + StorageClass: "TEST", + NoncurrentversionTransitionStorageClass: "TESTNC", + Tags: "", + TransitionDays: int32(16), + }, + } + + minioSetBucketLifecycleMock = func(ctx context.Context, bucketName string, config *lifecycle.Configuration) error { + return nil + } + + err = editBucketLifecycle(ctx, minClient, editMock) + + assert.Equal(nil, err, fmt.Sprintf("Failed on %s: Error returned", function)) + // Test-3 : editBucketLifecycle() returns error minioSetBucketLifecycleMock = func(ctx context.Context, bucketName string, config *lifecycle.Configuration) error { diff --git a/swagger-console.yml b/swagger-console.yml index 6662f030c..0450273c3 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -3817,6 +3817,9 @@ definitions: format: int64 delete_marker: type: boolean + noncurrent_expiration_days: + type: integer + format: int64 transitionResponse: type: object @@ -3828,6 +3831,11 @@ definitions: days: type: integer format: int64 + noncurrent_transition_days: + type: integer + format: int64 + noncurrent_storage_class: + type: string lifecycleTag: type: object @@ -3905,7 +3913,15 @@ definitions: updateBucketLifecycle: type: object + required: + - type properties: + type: + description: ILM Rule type (Expiry or transition) + type: string + enum: + - expiry + - transition prefix: description: Non required field, it matches a prefix to perform ILM operations on it type: string