From 585780d415803f6d22c12ba1434f3b5ecb2fe2e0 Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Thu, 3 Dec 2020 16:21:44 -0600 Subject: [PATCH] Connected retention policy modal (#437) --- .../Objects/ObjectDetails/AddTagModal.tsx | 2 +- .../Objects/ObjectDetails/DeleteTagModal.tsx | 2 +- .../Objects/ObjectDetails/ObjectDetails.tsx | 118 ++++++------ .../Objects/ObjectDetails/SetRetention.tsx | 171 +++++++++++++++--- .../Objects/ObjectDetails/types.ts | 2 +- .../RadioGroupSelector/RadioGroupSelector.tsx | 2 +- restapi/embedded_spec.go | 76 ++++++++ restapi/operations/console_api.go | 12 ++ .../user_api/delete_object_retention.go | 90 +++++++++ .../delete_object_retention_parameters.go | 155 ++++++++++++++++ .../delete_object_retention_responses.go | 113 ++++++++++++ .../delete_object_retention_urlbuilder.go | 133 ++++++++++++++ restapi/user_objects.go | 34 ++++ restapi/user_objects_test.go | 47 ++++- swagger.yml | 25 +++ 15 files changed, 903 insertions(+), 79 deletions(-) create mode 100644 restapi/operations/user_api/delete_object_retention.go create mode 100644 restapi/operations/user_api/delete_object_retention_parameters.go create mode 100644 restapi/operations/user_api/delete_object_retention_responses.go create mode 100644 restapi/operations/user_api/delete_object_retention_urlbuilder.go diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/AddTagModal.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/AddTagModal.tsx index 09736419d..7aab17928 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/AddTagModal.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/AddTagModal.tsx @@ -10,7 +10,7 @@ interface ITagModal { modalOpen: boolean; currentTags: any; bucketName: string; - versionId: string; + versionId: string | null; onCloseAndUpdate: (refresh: boolean) => void; selectedObject: string; classes: any; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteTagModal.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteTagModal.tsx index ca11a96d6..31b25a5d2 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteTagModal.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteTagModal.tsx @@ -18,7 +18,7 @@ interface IDeleteTagModal { deleteOpen: boolean; currentTags: any; bucketName: string; - versionId: string; + versionId: string | null; selectedTag: [string, string]; onCloseAndUpdate: (refresh: boolean) => void; selectedObject: string; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx index 3eb254818..199ced516 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx @@ -147,7 +147,7 @@ const emptyFile: IFileInfo = { retention_until_date: "", size: "0", tags: {}, - version_id: "", + version_id: null, }; const ObjectDetails = ({ @@ -206,8 +206,11 @@ const ObjectDetails = ({ setRetentionModalOpen(true); }; - const closeRetentionModal = () => { + const closeRetentionModal = (updateInfo: boolean) => { setRetentionModalOpen(false); + if (updateInfo) { + setLoadObjectData(true); + } }; const shareObject = () => { @@ -232,9 +235,12 @@ const ObjectDetails = ({ { type: "download", onClick: downloadObject }, ]; - const filteredRecords = versions.filter((version) => - version.version_id.includes(filterVersion) - ); + const filteredRecords = versions.filter((version) => { + if (version.version_id) { + return version.version_id.includes(filterVersion); + } + return false; + }); const displayParsedDate = (date: string) => { return {date}; @@ -291,6 +297,8 @@ const ObjectDetails = ({ open={retentionModalOpen} closeModalAndRefresh={closeRetentionModal} objectName={objectName} + objectInfo={actualInfo} + bucketName={bucketName} /> )} {deleteOpen && ( @@ -340,52 +348,60 @@ const ObjectDetails = ({
-
-
- Legal Hold: - - {actualInfo.legal_hold_status - ? actualInfo.legal_hold_status.toLowerCase() - : "Off"} - -
-
- { - setLegalholdOpen(true); - }} - > - - -
-
-
-
- Retention: - - {actualInfo.retention_mode - ? actualInfo.retention_mode.toLowerCase() - : "Undefined"} - -
-
- { - openRetentionModal(); - }} - > - - -
-
+ {actualInfo.version_id && actualInfo.version_id !== "null" && ( + +
+
+ + Legal Hold: + + + {actualInfo.legal_hold_status + ? actualInfo.legal_hold_status.toLowerCase() + : "Off"} + +
+
+ { + setLegalholdOpen(true); + }} + > + + +
+
+
+
+ + Retention: + + + {actualInfo.retention_mode + ? actualInfo.retention_mode.toLowerCase() + : "Undefined"} + +
+
+ { + openRetentionModal(); + }} + > + + +
+
+
+ )}
File Actions:
diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/SetRetention.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/SetRetention.tsx index 2ee74ee87..5d2832d2c 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/SetRetention.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/SetRetention.tsx @@ -1,12 +1,31 @@ -import React, { useState, useRef } from "react"; +// This file is part of MinIO Console Server +// Copyright (c) 2020 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 . + +import React, { useState, useRef, useEffect } from "react"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import Grid from "@material-ui/core/Grid"; import Button from "@material-ui/core/Button"; import { modalBasic } from "../../../../Common/FormComponents/common/styleLibrary"; +import { IFileInfo } from "./types"; import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper"; import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; import RadioGroupSelector from "../../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector"; import DateSelector from "../../../../Common/FormComponents/DateSelector/DateSelector"; +import api from "../../../../../../common/api"; +import Typography from "@material-ui/core/Typography"; const styles = (theme: Theme) => createStyles({ @@ -18,14 +37,19 @@ const styles = (theme: Theme) => buttonContainer: { textAlign: "right", }, + errorBlock: { + color: "red", + }, ...modalBasic, }); interface ISetRetentionProps { classes: any; open: boolean; - closeModalAndRefresh: () => void; + closeModalAndRefresh: (updateInfo: boolean) => void; objectName: string; + bucketName: string; + objectInfo: IFileInfo; } interface IRefObject { @@ -37,9 +61,23 @@ const SetRetention = ({ open, closeModalAndRefresh, objectName, + objectInfo, + bucketName, }: ISetRetentionProps) => { - const [statusEnabled, setStatusEnabled] = useState(false); + const [statusEnabled, setStatusEnabled] = useState(true); + const [error, setError] = useState(""); const [type, setType] = useState(""); + const [date, setDate] = useState(""); + const [isDateValid, setIsDateValid] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [alreadyConfigured, setAlreadyConfigured] = useState(false); + + useEffect(() => { + if (objectInfo.retention_mode) { + setType(objectInfo.retention_mode.toLowerCase()); + setAlreadyConfigured(true); + } + }, []); const dateElement = useRef(null); @@ -59,15 +97,90 @@ const SetRetention = ({ } }; + const addRetention = ( + selectedObject: string, + versionId: string | null, + expireDate: string + ) => { + api + .invoke( + "PUT", + `/api/v1/buckets/${bucketName}/objects/retention?prefix=${selectedObject}&version_id=${versionId}`, + { + expires: expireDate, + mode: type, + } + ) + .then((res: any) => { + setIsSaving(false); + closeModalAndRefresh(true); + }) + .catch((error) => { + setError(error); + setIsSaving(false); + }); + }; + + const disableRetention = ( + selectedObject: string, + versionId: string | null + ) => { + api + .invoke( + "DELETE", + `/api/v1/buckets/${bucketName}/objects/retention?prefix=${selectedObject}&version_id=${versionId}` + ) + .then((res: any) => { + setIsSaving(false); + closeModalAndRefresh(true); + }) + .catch((error) => { + setError(error); + setIsSaving(false); + }); + }; + + const saveNewRetentionPolicy = () => { + setIsSaving(true); + const selectedObject = objectInfo.name; + const versionId = objectInfo.version_id; + + const expireDate = + !statusEnabled && type === "governance" ? "" : `${date}T23:59:59Z`; + + if (!statusEnabled && type === "governance") { + disableRetention(selectedObject, versionId); + + return; + } + + addRetention(selectedObject, versionId, expireDate); + }; + + const showSwitcher = + alreadyConfigured && (type === "governance" || type === ""); + return ( { resetForm(); - closeModalAndRefresh(); + closeModalAndRefresh(false); }} > + {error !== "" && ( + +
+ + {error} + +
+ )} {objectName} @@ -78,27 +191,30 @@ const SetRetention = ({ onSubmit(e); }} > - - ) => { - setStatusEnabled(!statusEnabled); - setType(""); - }} - label={"Status"} - indicatorLabels={["Enabled", "Disabled"]} - /> - + {showSwitcher && ( + + ) => { + setStatusEnabled(!statusEnabled); + }} + label={"Status"} + indicatorLabels={["Enabled", "Disabled"]} + /> + + )} { setType(e.target.value); }} @@ -115,7 +231,10 @@ const SetRetention = ({ disableOptions={dateFieldDisabled()} ref={dateElement} borderBottom={true} - onDateChange={() => {}} + onDateChange={(date: string, isValid: boolean) => { + setIsDateValid(isValid); + setDate(date); + }} /> @@ -127,7 +246,17 @@ const SetRetention = ({ > Reset - diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/types.ts b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/types.ts index e4f243f9c..d6ac7b084 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/types.ts +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/types.ts @@ -23,5 +23,5 @@ export interface IFileInfo { retention_until_date?: string; size?: string; tags?: object; - version_id: string; + version_id: string | null; } 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 3c1b7c71b..529150b79 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/RadioGroupSelector/RadioGroupSelector.tsx +++ b/portal-ui/src/screens/Console/Common/FormComponents/RadioGroupSelector/RadioGroupSelector.tsx @@ -146,7 +146,7 @@ export const RadioGroupSelector = ({ return ( } label={selectorOption.label} disabled={disableOptions} diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index bcad60c8f..95c3c2d1f 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -663,6 +663,44 @@ func init() { } } } + }, + "delete": { + "tags": [ + "UserAPI" + ], + "summary": "Delete Object retention from an object", + "operationId": "DeleteObjectRetention", + "parameters": [ + { + "type": "string", + "name": "bucket_name", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "prefix", + "in": "query", + "required": true + }, + { + "type": "string", + "name": "version_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "A successful response." + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } } }, "/buckets/{bucket_name}/objects/share": { @@ -5685,6 +5723,44 @@ func init() { } } } + }, + "delete": { + "tags": [ + "UserAPI" + ], + "summary": "Delete Object retention from an object", + "operationId": "DeleteObjectRetention", + "parameters": [ + { + "type": "string", + "name": "bucket_name", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "prefix", + "in": "query", + "required": true + }, + { + "type": "string", + "name": "version_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "A successful response." + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } } }, "/buckets/{bucket_name}/objects/share": { diff --git a/restapi/operations/console_api.go b/restapi/operations/console_api.go index 89c24331f..31f23a23d 100644 --- a/restapi/operations/console_api.go +++ b/restapi/operations/console_api.go @@ -119,6 +119,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI { UserAPIDeleteObjectHandler: user_api.DeleteObjectHandlerFunc(func(params user_api.DeleteObjectParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation user_api.DeleteObject has not yet been implemented") }), + UserAPIDeleteObjectRetentionHandler: user_api.DeleteObjectRetentionHandlerFunc(func(params user_api.DeleteObjectRetentionParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation user_api.DeleteObjectRetention has not yet been implemented") + }), UserAPIDeleteRemoteBucketHandler: user_api.DeleteRemoteBucketHandlerFunc(func(params user_api.DeleteRemoteBucketParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation user_api.DeleteRemoteBucket has not yet been implemented") }), @@ -399,6 +402,8 @@ type ConsoleAPI struct { UserAPIDeleteBucketEventHandler user_api.DeleteBucketEventHandler // UserAPIDeleteObjectHandler sets the operation handler for the delete object operation UserAPIDeleteObjectHandler user_api.DeleteObjectHandler + // UserAPIDeleteObjectRetentionHandler sets the operation handler for the delete object retention operation + UserAPIDeleteObjectRetentionHandler user_api.DeleteObjectRetentionHandler // UserAPIDeleteRemoteBucketHandler sets the operation handler for the delete remote bucket operation UserAPIDeleteRemoteBucketHandler user_api.DeleteRemoteBucketHandler // UserAPIDeleteServiceAccountHandler sets the operation handler for the delete service account operation @@ -657,6 +662,9 @@ func (o *ConsoleAPI) Validate() error { if o.UserAPIDeleteObjectHandler == nil { unregistered = append(unregistered, "user_api.DeleteObjectHandler") } + if o.UserAPIDeleteObjectRetentionHandler == nil { + unregistered = append(unregistered, "user_api.DeleteObjectRetentionHandler") + } if o.UserAPIDeleteRemoteBucketHandler == nil { unregistered = append(unregistered, "user_api.DeleteRemoteBucketHandler") } @@ -1026,6 +1034,10 @@ func (o *ConsoleAPI) initHandlerCache() { if o.handlers["DELETE"] == nil { o.handlers["DELETE"] = make(map[string]http.Handler) } + o.handlers["DELETE"]["/buckets/{bucket_name}/objects/retention"] = user_api.NewDeleteObjectRetention(o.context, o.UserAPIDeleteObjectRetentionHandler) + if o.handlers["DELETE"] == nil { + o.handlers["DELETE"] = make(map[string]http.Handler) + } o.handlers["DELETE"]["/remote-buckets/{source-bucket-name}/{arn}"] = user_api.NewDeleteRemoteBucket(o.context, o.UserAPIDeleteRemoteBucketHandler) if o.handlers["DELETE"] == nil { o.handlers["DELETE"] = make(map[string]http.Handler) diff --git a/restapi/operations/user_api/delete_object_retention.go b/restapi/operations/user_api/delete_object_retention.go new file mode 100644 index 000000000..e758c3391 --- /dev/null +++ b/restapi/operations/user_api/delete_object_retention.go @@ -0,0 +1,90 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2020 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 user_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" + + "github.com/minio/console/models" +) + +// DeleteObjectRetentionHandlerFunc turns a function with the right signature into a delete object retention handler +type DeleteObjectRetentionHandlerFunc func(DeleteObjectRetentionParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn DeleteObjectRetentionHandlerFunc) Handle(params DeleteObjectRetentionParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// DeleteObjectRetentionHandler interface for that can handle valid delete object retention params +type DeleteObjectRetentionHandler interface { + Handle(DeleteObjectRetentionParams, *models.Principal) middleware.Responder +} + +// NewDeleteObjectRetention creates a new http.Handler for the delete object retention operation +func NewDeleteObjectRetention(ctx *middleware.Context, handler DeleteObjectRetentionHandler) *DeleteObjectRetention { + return &DeleteObjectRetention{Context: ctx, Handler: handler} +} + +/*DeleteObjectRetention swagger:route DELETE /buckets/{bucket_name}/objects/retention UserAPI deleteObjectRetention + +Delete Object retention from an object + +*/ +type DeleteObjectRetention struct { + Context *middleware.Context + Handler DeleteObjectRetentionHandler +} + +func (o *DeleteObjectRetention) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewDeleteObjectRetentionParams() + + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + r = aCtx + } + var principal *models.Principal + if uprinc != nil { + principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise + } + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params, principal) // actually handle the request + + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/restapi/operations/user_api/delete_object_retention_parameters.go b/restapi/operations/user_api/delete_object_retention_parameters.go new file mode 100644 index 000000000..e1c72c05c --- /dev/null +++ b/restapi/operations/user_api/delete_object_retention_parameters.go @@ -0,0 +1,155 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2020 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 user_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// NewDeleteObjectRetentionParams creates a new DeleteObjectRetentionParams object +// no default values defined in spec. +func NewDeleteObjectRetentionParams() DeleteObjectRetentionParams { + + return DeleteObjectRetentionParams{} +} + +// DeleteObjectRetentionParams contains all the bound params for the delete object retention operation +// typically these are obtained from a http.Request +// +// swagger:parameters DeleteObjectRetention +type DeleteObjectRetentionParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: path + */ + BucketName string + /* + Required: true + In: query + */ + Prefix string + /* + Required: true + In: query + */ + VersionID string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewDeleteObjectRetentionParams() beforehand. +func (o *DeleteObjectRetentionParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + qs := runtime.Values(r.URL.Query()) + + rBucketName, rhkBucketName, _ := route.Params.GetOK("bucket_name") + if err := o.bindBucketName(rBucketName, rhkBucketName, route.Formats); err != nil { + res = append(res, err) + } + + qPrefix, qhkPrefix, _ := qs.GetOK("prefix") + if err := o.bindPrefix(qPrefix, qhkPrefix, route.Formats); err != nil { + res = append(res, err) + } + + qVersionID, qhkVersionID, _ := qs.GetOK("version_id") + if err := o.bindVersionID(qVersionID, qhkVersionID, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindBucketName binds and validates parameter BucketName from path. +func (o *DeleteObjectRetentionParams) bindBucketName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + + o.BucketName = raw + + return nil +} + +// bindPrefix binds and validates parameter Prefix from query. +func (o *DeleteObjectRetentionParams) bindPrefix(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("prefix", "query", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // AllowEmptyValue: false + if err := validate.RequiredString("prefix", "query", raw); err != nil { + return err + } + + o.Prefix = raw + + return nil +} + +// bindVersionID binds and validates parameter VersionID from query. +func (o *DeleteObjectRetentionParams) bindVersionID(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("version_id", "query", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // AllowEmptyValue: false + if err := validate.RequiredString("version_id", "query", raw); err != nil { + return err + } + + o.VersionID = raw + + return nil +} diff --git a/restapi/operations/user_api/delete_object_retention_responses.go b/restapi/operations/user_api/delete_object_retention_responses.go new file mode 100644 index 000000000..c51a0021d --- /dev/null +++ b/restapi/operations/user_api/delete_object_retention_responses.go @@ -0,0 +1,113 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2020 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 user_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/minio/console/models" +) + +// DeleteObjectRetentionOKCode is the HTTP code returned for type DeleteObjectRetentionOK +const DeleteObjectRetentionOKCode int = 200 + +/*DeleteObjectRetentionOK A successful response. + +swagger:response deleteObjectRetentionOK +*/ +type DeleteObjectRetentionOK struct { +} + +// NewDeleteObjectRetentionOK creates DeleteObjectRetentionOK with default headers values +func NewDeleteObjectRetentionOK() *DeleteObjectRetentionOK { + + return &DeleteObjectRetentionOK{} +} + +// WriteResponse to the client +func (o *DeleteObjectRetentionOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(200) +} + +/*DeleteObjectRetentionDefault Generic error response. + +swagger:response deleteObjectRetentionDefault +*/ +type DeleteObjectRetentionDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewDeleteObjectRetentionDefault creates DeleteObjectRetentionDefault with default headers values +func NewDeleteObjectRetentionDefault(code int) *DeleteObjectRetentionDefault { + if code <= 0 { + code = 500 + } + + return &DeleteObjectRetentionDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the delete object retention default response +func (o *DeleteObjectRetentionDefault) WithStatusCode(code int) *DeleteObjectRetentionDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the delete object retention default response +func (o *DeleteObjectRetentionDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the delete object retention default response +func (o *DeleteObjectRetentionDefault) WithPayload(payload *models.Error) *DeleteObjectRetentionDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the delete object retention default response +func (o *DeleteObjectRetentionDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DeleteObjectRetentionDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/restapi/operations/user_api/delete_object_retention_urlbuilder.go b/restapi/operations/user_api/delete_object_retention_urlbuilder.go new file mode 100644 index 000000000..fbc442950 --- /dev/null +++ b/restapi/operations/user_api/delete_object_retention_urlbuilder.go @@ -0,0 +1,133 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2020 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 user_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// DeleteObjectRetentionURL generates an URL for the delete object retention operation +type DeleteObjectRetentionURL struct { + BucketName string + + Prefix string + VersionID string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DeleteObjectRetentionURL) WithBasePath(bp string) *DeleteObjectRetentionURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *DeleteObjectRetentionURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *DeleteObjectRetentionURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/buckets/{bucket_name}/objects/retention" + + bucketName := o.BucketName + if bucketName != "" { + _path = strings.Replace(_path, "{bucket_name}", bucketName, -1) + } else { + return nil, errors.New("bucketName is required on DeleteObjectRetentionURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + qs := make(url.Values) + + prefixQ := o.Prefix + if prefixQ != "" { + qs.Set("prefix", prefixQ) + } + + versionIDQ := o.VersionID + if versionIDQ != "" { + qs.Set("version_id", versionIDQ) + } + + _result.RawQuery = qs.Encode() + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *DeleteObjectRetentionURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *DeleteObjectRetentionURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *DeleteObjectRetentionURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on DeleteObjectRetentionURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on DeleteObjectRetentionURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *DeleteObjectRetentionURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/restapi/user_objects.go b/restapi/user_objects.go index 03edf58de..7ad6ae02f 100644 --- a/restapi/user_objects.go +++ b/restapi/user_objects.go @@ -107,6 +107,14 @@ func registerObjectsHandlers(api *operations.ConsoleAPI) { } return user_api.NewPutObjectRetentionOK() }) + // delete object retention + api.UserAPIDeleteObjectRetentionHandler = user_api.DeleteObjectRetentionHandlerFunc(func(params user_api.DeleteObjectRetentionParams, session *models.Principal) middleware.Responder { + if err := deleteObjectRetentionResponse(session, params); err != nil { + return user_api.NewDeleteObjectRetentionDefault(int(err.Code)).WithPayload(err) + } + return user_api.NewDeleteObjectRetentionOK() + }) + // set tags in object api.UserAPIPutObjectTagsHandler = user_api.PutObjectTagsHandlerFunc(func(params user_api.PutObjectTagsParams, session *models.Principal) middleware.Responder { if err := getPutObjectTagsResponse(session, params); err != nil { return user_api.NewPutObjectTagsDefault(int(err.Code)).WithPayload(err) @@ -512,6 +520,32 @@ func setObjectRetention(ctx context.Context, client MinioClient, bucketName, ver return client.putObjectRetention(ctx, bucketName, prefix, opts) } +func deleteObjectRetentionResponse(session *models.Principal, params user_api.DeleteObjectRetentionParams) *models.Error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + mClient, err := newMinioClient(session) + if err != nil { + return prepareError(err) + } + // create a minioClient interface implementation + // defining the client to be used + minioClient := minioClient{client: mClient} + err = deleteObjectRetention(ctx, minioClient, params.BucketName, params.Prefix, params.VersionID) + if err != nil { + return prepareError(err) + } + return nil +} + +func deleteObjectRetention(ctx context.Context, client MinioClient, bucketName, prefix, versionID string) error { + opts := minio.PutObjectRetentionOptions{ + GovernanceBypass: true, + VersionID: versionID, + } + + return client.putObjectRetention(ctx, bucketName, prefix, opts) +} + func getPutObjectTagsResponse(session *models.Principal, params user_api.PutObjectTagsParams) *models.Error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() diff --git a/restapi/user_objects_test.go b/restapi/user_objects_test.go index 1b7a44c47..8d0bf76c0 100644 --- a/restapi/user_objects_test.go +++ b/restapi/user_objects_test.go @@ -40,7 +40,7 @@ var minioGetObjectLegalHoldMock func(ctx context.Context, bucketName, objectName var minioGetObjectRetentionMock func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) var minioPutObjectMock func(ctx context.Context, bucketName, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (info minio.UploadInfo, err error) var minioPutObjectLegalHoldMock func(ctx context.Context, bucketName, objectName string, opts minio.PutObjectLegalHoldOptions) error -var miinoPutObjectRetentionMock func(ctx context.Context, bucketName, objectName string, opts minio.PutObjectRetentionOptions) error +var minioPutObjectRetentionMock func(ctx context.Context, bucketName, objectName string, opts minio.PutObjectRetentionOptions) error var minioGetObjectTaggingMock func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error) var minioPutObjectTaggingMock func(ctx context.Context, bucketName, objectName string, otags *tags.Tags, opts minio.PutObjectTaggingOptions) error @@ -70,7 +70,7 @@ func (ac minioClientMock) putObjectLegalHold(ctx context.Context, bucketName, ob } func (ac minioClientMock) putObjectRetention(ctx context.Context, bucketName, objectName string, opts minio.PutObjectRetentionOptions) error { - return miinoPutObjectRetentionMock(ctx, bucketName, objectName, opts) + return minioPutObjectRetentionMock(ctx, bucketName, objectName, opts) } func (ac minioClientMock) getObjectTagging(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error) { @@ -985,7 +985,7 @@ func Test_putObjectRetention(t *testing.T) { for _, tt := range tests { t.Run(tt.test, func(t *testing.T) { - miinoPutObjectRetentionMock = tt.args.retentionFunc + minioPutObjectRetentionMock = tt.args.retentionFunc err := setObjectRetention(ctx, client, tt.args.bucket, tt.args.prefix, tt.args.versionID, tt.args.opts) if tt.wantError != nil { fmt.Println(t.Name()) @@ -997,3 +997,44 @@ func Test_putObjectRetention(t *testing.T) { }) } } + +func Test_deleteObjectRetention(t *testing.T) { + assert := assert.New(t) + ctx := context.Background() + client := minioClientMock{} + type args struct { + bucket string + prefix string + versionID string + retentionFunc func(ctx context.Context, bucketName, objectName string, opts minio.PutObjectRetentionOptions) error + } + tests := []struct { + test string + args args + wantError error + }{ + { + test: "Delete Object retention governance", + args: args{ + bucket: "buck1", + versionID: "someversion", + prefix: "folder/file.txt", + retentionFunc: func(ctx context.Context, bucketName, objectName string, opts minio.PutObjectRetentionOptions) error { + return nil + }, + }, + wantError: nil, + }} + for _, tt := range tests { + t.Run(tt.test, func(t *testing.T) { + minioPutObjectRetentionMock = tt.args.retentionFunc + err := deleteObjectRetention(ctx, client, tt.args.bucket, tt.args.prefix, tt.args.versionID) + if tt.wantError != nil { + fmt.Println(t.Name()) + assert.Equal(tt.wantError.Error(), err.Error(), fmt.Sprintf("deleteObjectRetention() error: `%s`, wantErr: `%s`", err, tt.wantError)) + } else { + assert.Nil(err, fmt.Sprintf("deleteObjectRetention() error: %v, wantErr: %v", err, tt.wantError)) + } + }) + } +} diff --git a/swagger.yml b/swagger.yml index 16bebfe86..7d6e361a3 100644 --- a/swagger.yml +++ b/swagger.yml @@ -464,6 +464,31 @@ paths: $ref: "#/definitions/error" tags: - UserAPI + delete: + summary: Delete Object retention from an object + operationId: DeleteObjectRetention + parameters: + - name: bucket_name + in: path + required: true + type: string + - name: prefix + in: query + required: true + type: string + - name: version_id + in: query + required: true + type: string + responses: + 200: + description: A successful response. + default: + description: Generic error response. + schema: + $ref: "#/definitions/error" + tags: + - UserAPI /buckets/{bucket_name}/objects/tags: put: