From 768c7c70a3ad7df46066a791a6c28345343c7c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Nieto?= Date: Thu, 2 Apr 2020 20:09:36 -0700 Subject: [PATCH] mcs add bucket event api using public mc S3Client struct (#15) * mcs add bucket event api using public mc S3Client struct * remove log * remove replace repo on go.mod * apply go mod tidy --- go.sum | 3 - models/bucket_event_request.go | 93 ++++++++++++++ models/notification_config.go | 17 ++- restapi/client.go | 92 ++++++++++++-- restapi/config.go | 2 +- restapi/embedded_spec.go | 102 +++++++++++++++ restapi/operations/mcs_api.go | 12 ++ .../user_api/create_bucket_event.go | 90 +++++++++++++ .../create_bucket_event_parameters.go | 120 ++++++++++++++++++ .../user_api/create_bucket_event_responses.go | 113 +++++++++++++++++ .../create_bucket_event_urlbuilder.go | 113 +++++++++++++++++ restapi/user_buckets_events.go | 59 ++++++++- restapi/user_buckets_events_test.go | 67 +++++++++- swagger.yml | 33 +++++ 14 files changed, 890 insertions(+), 26 deletions(-) create mode 100644 models/bucket_event_request.go create mode 100644 restapi/operations/user_api/create_bucket_event.go create mode 100644 restapi/operations/user_api/create_bucket_event_parameters.go create mode 100644 restapi/operations/user_api/create_bucket_event_responses.go create mode 100644 restapi/operations/user_api/create_bucket_event_urlbuilder.go diff --git a/go.sum b/go.sum index dca4a45b6..8a9eda160 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,6 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dvaldivia/mc v0.0.0-20200402232537-1048833f1701 h1:TaXA4hNW3k99MbCKOFnXsaLr9xsx/kh9YXcUNQ/Q8Wk= -github.com/dvaldivia/mc v0.0.0-20200402232537-1048833f1701/go.mod h1:IDy4dA4aFY6zFFNkYgdUztl0jcYuev/Ubg3NadoaMKc= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -385,7 +383,6 @@ github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2 github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= github.com/minio/lsync v1.0.1 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs= github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA= -github.com/minio/mc v0.0.0-20200401220942-e05f02d9f459/go.mod h1:GWohdY5tXSiMnBCofmDRK5yRCihQH2FKNM0eh+UsY5Y= github.com/minio/mc v0.0.0-20200403024131-4d36c1f8b856 h1:4uIc5fw4tVr5glh2Mc8GFuiY04pTGEhmihPxJPUvCoU= github.com/minio/mc v0.0.0-20200403024131-4d36c1f8b856/go.mod h1:IDy4dA4aFY6zFFNkYgdUztl0jcYuev/Ubg3NadoaMKc= github.com/minio/minio v0.0.0-20200327214830-6f992134a25f h1:RoOBi0vhXkZqe2b6RTROOsVJUwMqLMoet9r7eL01euo= diff --git a/models/bucket_event_request.go b/models/bucket_event_request.go new file mode 100644 index 000000000..1b6128b63 --- /dev/null +++ b/models/bucket_event_request.go @@ -0,0 +1,93 @@ +// 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 models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// BucketEventRequest bucket event request +// +// swagger:model bucketEventRequest +type BucketEventRequest struct { + + // configuration + // Required: true + Configuration *NotificationConfig `json:"configuration"` + + // ignore existing + IgnoreExisting bool `json:"ignoreExisting,omitempty"` +} + +// Validate validates this bucket event request +func (m *BucketEventRequest) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateConfiguration(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BucketEventRequest) validateConfiguration(formats strfmt.Registry) error { + + if err := validate.Required("configuration", "body", m.Configuration); err != nil { + return err + } + + if m.Configuration != nil { + if err := m.Configuration.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("configuration") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *BucketEventRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BucketEventRequest) UnmarshalBinary(b []byte) error { + var res BucketEventRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/notification_config.go b/models/notification_config.go index 8d644f57a..87bb0093c 100644 --- a/models/notification_config.go +++ b/models/notification_config.go @@ -28,6 +28,7 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" + "github.com/go-openapi/validate" ) // NotificationConfig notification config @@ -36,7 +37,8 @@ import ( type NotificationConfig struct { // arn - Arn string `json:"arn,omitempty"` + // Required: true + Arn *string `json:"arn"` // filter specific type of event. Defaults to all event (default: '[put,delete,get]') Events []NotificationEventType `json:"events"` @@ -55,6 +57,10 @@ type NotificationConfig struct { func (m *NotificationConfig) Validate(formats strfmt.Registry) error { var res []error + if err := m.validateArn(formats); err != nil { + res = append(res, err) + } + if err := m.validateEvents(formats); err != nil { res = append(res, err) } @@ -65,6 +71,15 @@ func (m *NotificationConfig) Validate(formats strfmt.Registry) error { return nil } +func (m *NotificationConfig) validateArn(formats strfmt.Registry) error { + + if err := validate.Required("arn", "body", m.Arn); err != nil { + return err + } + + return nil +} + func (m *NotificationConfig) validateEvents(formats strfmt.Registry) error { if swag.IsZero(m.Events) { // not required diff --git a/restapi/client.go b/restapi/client.go index 517778f3e..0b063b95f 100644 --- a/restapi/client.go +++ b/restapi/client.go @@ -18,7 +18,10 @@ package restapi import ( "context" + "fmt" + mc "github.com/minio/mc/cmd" + "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v6" ) @@ -50,33 +53,53 @@ type minioClient struct { } // implements minio.ListBucketsWithContext(ctx) -func (mc minioClient) listBucketsWithContext(ctx context.Context) ([]minio.BucketInfo, error) { - return mc.client.ListBucketsWithContext(ctx) +func (c minioClient) listBucketsWithContext(ctx context.Context) ([]minio.BucketInfo, error) { + return c.client.ListBucketsWithContext(ctx) } // implements minio.MakeBucketWithContext(ctx, bucketName, location) -func (mc minioClient) makeBucketWithContext(ctx context.Context, bucketName, location string) error { - return mc.client.MakeBucketWithContext(ctx, bucketName, location) +func (c minioClient) makeBucketWithContext(ctx context.Context, bucketName, location string) error { + return c.client.MakeBucketWithContext(ctx, bucketName, location) } // implements minio.SetBucketPolicyWithContext(ctx, bucketName, policy) -func (mc minioClient) setBucketPolicyWithContext(ctx context.Context, bucketName, policy string) error { - return mc.client.SetBucketPolicyWithContext(ctx, bucketName, policy) +func (c minioClient) setBucketPolicyWithContext(ctx context.Context, bucketName, policy string) error { + return c.client.SetBucketPolicyWithContext(ctx, bucketName, policy) } // implements minio.RemoveBucket(bucketName) -func (mc minioClient) removeBucket(bucketName string) error { - return mc.client.RemoveBucket(bucketName) +func (c minioClient) removeBucket(bucketName string) error { + return c.client.RemoveBucket(bucketName) } // implements minio.GetBucketNotification(bucketName) -func (mc minioClient) getBucketNotification(bucketName string) (bucketNotification minio.BucketNotification, err error) { - return mc.client.GetBucketNotification(bucketName) +func (c minioClient) getBucketNotification(bucketName string) (bucketNotification minio.BucketNotification, err error) { + return c.client.GetBucketNotification(bucketName) } // implements minio.GetBucketPolicy(bucketName) -func (mc minioClient) getBucketPolicy(bucketName string) (string, error) { - return mc.client.GetBucketPolicy(bucketName) +func (c minioClient) getBucketPolicy(bucketName string) (string, error) { + return c.client.GetBucketPolicy(bucketName) +} + +// Define MCS3Client interface with all functions to be implemented +// by mock when testing, it should include all mc/S3Client respective api calls +// that are used within this project. +type MCS3Client interface { + addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error +} + +// Interface implementation +// +// Define the structure of a mc S3Client and define the functions that are actually used +// from mcS3client api. +type mcS3Client struct { + client *mc.S3Client +} + +// implements minio.ListBucketsWithContext(ctx) +func (c mcS3Client) addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error { + return c.client.AddNotificationConfig(arn, events, prefix, suffix, ignoreExisting) } // newMinioClient creates a new MinIO client to talk to the server @@ -84,7 +107,7 @@ func newMinioClient() (*minio.Client, error) { endpoint := getMinIOEndpoint() accessKeyID := getAccessKey() secretAccessKey := getSecretKey() - useSSL := getMinIOEndpointSSL() + useSSL := getMinIOEndpointIsSecure() // Initialize minio client object. minioClient, err := minio.NewV4(endpoint, accessKeyID, secretAccessKey, useSSL) @@ -94,3 +117,46 @@ func newMinioClient() (*minio.Client, error) { return minioClient, nil } + +// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket +func newS3BucketClient(bucketName *string) (*mc.S3Client, error) { + endpoint := getMinIOServer() + accessKeyID := getAccessKey() + secretAccessKey := getSecretKey() + useSSL := getMinIOEndpointIsSecure() + + if bucketName != nil { + endpoint += fmt.Sprintf("/%s", *bucketName) + } + s3Config := newS3Config(endpoint, accessKeyID, secretAccessKey, !useSSL) + client, err := mc.S3New(s3Config) + if err != nil { + return nil, err.Cause + } + s3Client, ok := client.(*mc.S3Client) + if !ok { + return nil, fmt.Errorf("the provided url doesn't point to a S3 server") + } + + return s3Client, nil +} + +// newS3Config simply creates a new Config struct using the passed +// parameters. +func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Config { + // We have a valid alias and hostConfig. We populate the + // credentials from the match found in the config file. + s3Config := new(mc.Config) + + s3Config.AppName = "mcs" // TODO: make this a constant + s3Config.AppVersion = "" // TODO: get this from constant or build + s3Config.AppComments = []string{} + s3Config.Debug = false + s3Config.Insecure = isSecure + + s3Config.HostURL = endpoint + s3Config.AccessKey = accessKey + s3Config.SecretKey = secretKey + s3Config.Signature = "S3v4" + return s3Config +} diff --git a/restapi/config.go b/restapi/config.go index 6c6891704..140cd2592 100644 --- a/restapi/config.go +++ b/restapi/config.go @@ -45,7 +45,7 @@ func getMinIOEndpoint() string { return server } -func getMinIOEndpointSSL() bool { +func getMinIOEndpointIsSecure() bool { server := getMinIOServer() if strings.Contains(server, "://") { parts := strings.Split(server, "://") diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 0bc0a7505..59fb7c46e 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -161,6 +161,40 @@ func init() { } } } + }, + "post": { + "tags": [ + "UserAPI" + ], + "summary": "Create Bucket Event", + "operationId": "CreateBucketEvent", + "parameters": [ + { + "type": "string", + "name": "bucket_name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/bucketEventRequest" + } + } + ], + "responses": { + "201": { + "description": "A successful response." + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } } }, "/api/v1/buckets/{name}": { @@ -987,6 +1021,20 @@ func init() { "CUSTOM" ] }, + "bucketEventRequest": { + "type": "object", + "required": [ + "configuration" + ], + "properties": { + "configuration": { + "$ref": "#/definitions/notificationConfig" + }, + "ignoreExisting": { + "type": "boolean" + } + } + }, "configDescription": { "type": "object", "properties": { @@ -1207,6 +1255,9 @@ func init() { }, "notificationConfig": { "type": "object", + "required": [ + "arn" + ], "properties": { "arn": { "type": "string" @@ -1576,6 +1627,40 @@ func init() { } } } + }, + "post": { + "tags": [ + "UserAPI" + ], + "summary": "Create Bucket Event", + "operationId": "CreateBucketEvent", + "parameters": [ + { + "type": "string", + "name": "bucket_name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/bucketEventRequest" + } + } + ], + "responses": { + "201": { + "description": "A successful response." + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } } }, "/api/v1/buckets/{name}": { @@ -2402,6 +2487,20 @@ func init() { "CUSTOM" ] }, + "bucketEventRequest": { + "type": "object", + "required": [ + "configuration" + ], + "properties": { + "configuration": { + "$ref": "#/definitions/notificationConfig" + }, + "ignoreExisting": { + "type": "boolean" + } + } + }, "configDescription": { "type": "object", "properties": { @@ -2622,6 +2721,9 @@ func init() { }, "notificationConfig": { "type": "object", + "required": [ + "arn" + ], "properties": { "arn": { "type": "string" diff --git a/restapi/operations/mcs_api.go b/restapi/operations/mcs_api.go index c99900533..4bbe49746 100644 --- a/restapi/operations/mcs_api.go +++ b/restapi/operations/mcs_api.go @@ -81,6 +81,9 @@ func NewMcsAPI(spec *loads.Document) *McsAPI { AdminAPIConfigInfoHandler: admin_api.ConfigInfoHandlerFunc(func(params admin_api.ConfigInfoParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation admin_api.ConfigInfo has not yet been implemented") }), + UserAPICreateBucketEventHandler: user_api.CreateBucketEventHandlerFunc(func(params user_api.CreateBucketEventParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation user_api.CreateBucketEvent has not yet been implemented") + }), UserAPIDeleteBucketHandler: user_api.DeleteBucketHandlerFunc(func(params user_api.DeleteBucketParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation user_api.DeleteBucket has not yet been implemented") }), @@ -205,6 +208,8 @@ type McsAPI struct { UserAPIBucketSetPolicyHandler user_api.BucketSetPolicyHandler // AdminAPIConfigInfoHandler sets the operation handler for the config info operation AdminAPIConfigInfoHandler admin_api.ConfigInfoHandler + // UserAPICreateBucketEventHandler sets the operation handler for the create bucket event operation + UserAPICreateBucketEventHandler user_api.CreateBucketEventHandler // UserAPIDeleteBucketHandler sets the operation handler for the delete bucket operation UserAPIDeleteBucketHandler user_api.DeleteBucketHandler // AdminAPIGroupInfoHandler sets the operation handler for the group info operation @@ -338,6 +343,9 @@ func (o *McsAPI) Validate() error { if o.AdminAPIConfigInfoHandler == nil { unregistered = append(unregistered, "admin_api.ConfigInfoHandler") } + if o.UserAPICreateBucketEventHandler == nil { + unregistered = append(unregistered, "user_api.CreateBucketEventHandler") + } if o.UserAPIDeleteBucketHandler == nil { unregistered = append(unregistered, "user_api.DeleteBucketHandler") } @@ -525,6 +533,10 @@ func (o *McsAPI) initHandlerCache() { o.handlers["GET"] = make(map[string]http.Handler) } o.handlers["GET"]["/api/v1/configs/{name}"] = admin_api.NewConfigInfo(o.context, o.AdminAPIConfigInfoHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } + o.handlers["POST"]["/api/v1/buckets/{bucket_name}/events"] = user_api.NewCreateBucketEvent(o.context, o.UserAPICreateBucketEventHandler) if o.handlers["DELETE"] == nil { o.handlers["DELETE"] = make(map[string]http.Handler) } diff --git a/restapi/operations/user_api/create_bucket_event.go b/restapi/operations/user_api/create_bucket_event.go new file mode 100644 index 000000000..0c4c8433b --- /dev/null +++ b/restapi/operations/user_api/create_bucket_event.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/m3/mcs/models" +) + +// CreateBucketEventHandlerFunc turns a function with the right signature into a create bucket event handler +type CreateBucketEventHandlerFunc func(CreateBucketEventParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn CreateBucketEventHandlerFunc) Handle(params CreateBucketEventParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// CreateBucketEventHandler interface for that can handle valid create bucket event params +type CreateBucketEventHandler interface { + Handle(CreateBucketEventParams, *models.Principal) middleware.Responder +} + +// NewCreateBucketEvent creates a new http.Handler for the create bucket event operation +func NewCreateBucketEvent(ctx *middleware.Context, handler CreateBucketEventHandler) *CreateBucketEvent { + return &CreateBucketEvent{Context: ctx, Handler: handler} +} + +/*CreateBucketEvent swagger:route POST /api/v1/buckets/{bucket_name}/events UserAPI createBucketEvent + +Create Bucket Event + +*/ +type CreateBucketEvent struct { + Context *middleware.Context + Handler CreateBucketEventHandler +} + +func (o *CreateBucketEvent) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewCreateBucketEventParams() + + 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/create_bucket_event_parameters.go b/restapi/operations/user_api/create_bucket_event_parameters.go new file mode 100644 index 000000000..b4d793cf2 --- /dev/null +++ b/restapi/operations/user_api/create_bucket_event_parameters.go @@ -0,0 +1,120 @@ +// 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 ( + "io" + "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/minio/m3/mcs/models" +) + +// NewCreateBucketEventParams creates a new CreateBucketEventParams object +// no default values defined in spec. +func NewCreateBucketEventParams() CreateBucketEventParams { + + return CreateBucketEventParams{} +} + +// CreateBucketEventParams contains all the bound params for the create bucket event operation +// typically these are obtained from a http.Request +// +// swagger:parameters CreateBucketEvent +type CreateBucketEventParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + Body *models.BucketEventRequest + /* + Required: true + In: path + */ + BucketName 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 NewCreateBucketEventParams() beforehand. +func (o *CreateBucketEventParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.BucketEventRequest + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("body", "body")) + } else { + res = append(res, errors.NewParseError("body", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Body = &body + } + } + } else { + res = append(res, errors.Required("body", "body")) + } + rBucketName, rhkBucketName, _ := route.Params.GetOK("bucket_name") + if err := o.bindBucketName(rBucketName, rhkBucketName, 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 *CreateBucketEventParams) 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 +} diff --git a/restapi/operations/user_api/create_bucket_event_responses.go b/restapi/operations/user_api/create_bucket_event_responses.go new file mode 100644 index 000000000..c5f6e9c9a --- /dev/null +++ b/restapi/operations/user_api/create_bucket_event_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/m3/mcs/models" +) + +// CreateBucketEventCreatedCode is the HTTP code returned for type CreateBucketEventCreated +const CreateBucketEventCreatedCode int = 201 + +/*CreateBucketEventCreated A successful response. + +swagger:response createBucketEventCreated +*/ +type CreateBucketEventCreated struct { +} + +// NewCreateBucketEventCreated creates CreateBucketEventCreated with default headers values +func NewCreateBucketEventCreated() *CreateBucketEventCreated { + + return &CreateBucketEventCreated{} +} + +// WriteResponse to the client +func (o *CreateBucketEventCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(201) +} + +/*CreateBucketEventDefault Generic error response. + +swagger:response createBucketEventDefault +*/ +type CreateBucketEventDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewCreateBucketEventDefault creates CreateBucketEventDefault with default headers values +func NewCreateBucketEventDefault(code int) *CreateBucketEventDefault { + if code <= 0 { + code = 500 + } + + return &CreateBucketEventDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the create bucket event default response +func (o *CreateBucketEventDefault) WithStatusCode(code int) *CreateBucketEventDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the create bucket event default response +func (o *CreateBucketEventDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the create bucket event default response +func (o *CreateBucketEventDefault) WithPayload(payload *models.Error) *CreateBucketEventDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the create bucket event default response +func (o *CreateBucketEventDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *CreateBucketEventDefault) 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/create_bucket_event_urlbuilder.go b/restapi/operations/user_api/create_bucket_event_urlbuilder.go new file mode 100644 index 000000000..d53baa84a --- /dev/null +++ b/restapi/operations/user_api/create_bucket_event_urlbuilder.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 generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// CreateBucketEventURL generates an URL for the create bucket event operation +type CreateBucketEventURL struct { + BucketName 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 *CreateBucketEventURL) WithBasePath(bp string) *CreateBucketEventURL { + 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 *CreateBucketEventURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *CreateBucketEventURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/api/v1/buckets/{bucket_name}/events" + + bucketName := o.BucketName + if bucketName != "" { + _path = strings.Replace(_path, "{bucket_name}", bucketName, -1) + } else { + return nil, errors.New("bucketName is required on CreateBucketEventURL") + } + + _basePath := o._basePath + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *CreateBucketEventURL) 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 *CreateBucketEventURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *CreateBucketEventURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on CreateBucketEventURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on CreateBucketEventURL") + } + + 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 *CreateBucketEventURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/restapi/user_buckets_events.go b/restapi/user_buckets_events.go index 70504dbb8..2db6b5ea0 100644 --- a/restapi/user_buckets_events.go +++ b/restapi/user_buckets_events.go @@ -36,6 +36,12 @@ func registerBucketEventsHandlers(api *operations.McsAPI) { } return user_api.NewListBucketEventsOK().WithPayload(listBucketEventsResponse) }) + api.UserAPICreateBucketEventHandler = user_api.CreateBucketEventHandlerFunc(func(params user_api.CreateBucketEventParams, principal *models.Principal) middleware.Responder { + if err := getCreateBucketEventsResponse(params.BucketName, params.Body); err != nil { + return user_api.NewCreateBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) + } + return user_api.NewCreateBucketEventCreated() + }) } // listBucketEvents fetches a list of all events set for a bucket and serializes them for a proper output @@ -83,7 +89,7 @@ func listBucketEvents(client MinioClient, bucketName string) ([]*models.Notifica for _, config := range bn.TopicConfigs { prefix, suffix := getFilters(config.NotificationConfig) configs = append(configs, &models.NotificationConfig{ID: config.ID, - Arn: config.Topic, + Arn: swag.String(config.Topic), Events: prettyEventNames(config.Events), Prefix: prefix, Suffix: suffix}) @@ -91,7 +97,7 @@ func listBucketEvents(client MinioClient, bucketName string) ([]*models.Notifica for _, config := range bn.QueueConfigs { prefix, suffix := getFilters(config.NotificationConfig) configs = append(configs, &models.NotificationConfig{ID: config.ID, - Arn: config.Queue, + Arn: swag.String(config.Queue), Events: prettyEventNames(config.Events), Prefix: prefix, Suffix: suffix}) @@ -99,7 +105,7 @@ func listBucketEvents(client MinioClient, bucketName string) ([]*models.Notifica for _, config := range bn.LambdaConfigs { prefix, suffix := getFilters(config.NotificationConfig) configs = append(configs, &models.NotificationConfig{ID: config.ID, - Arn: config.Lambda, + Arn: swag.String(config.Lambda), Events: prettyEventNames(config.Events), Prefix: prefix, Suffix: suffix}) @@ -130,3 +136,50 @@ func getListBucketEventsResponse(params user_api.ListBucketEventsParams) (*model } return listBucketsResponse, nil } + +// createBucketEvent calls mc AddNotificationConfig() to create a bucket nofication +// +// If notificationEvents is empty, by default will set [get, put, delete], else the provided +// ones will be set. +// this function follows same behavior as minio/mc for adding a bucket event +func createBucketEvent(client MCS3Client, arn string, notificationEvents []models.NotificationEventType, prefix, suffix string, ignoreExisting bool) error { + var events []string + if len(notificationEvents) == 0 { + // default event values are [get, put, delete] + events = []string{ + string(models.NotificationEventTypeGet), + string(models.NotificationEventTypePut), + string(models.NotificationEventTypeDelete), + } + } else { + // else use defined events in request + // cast type models.NotificationEventType to string + for _, e := range notificationEvents { + events = append(events, string(e)) + } + } + + perr := client.addNotificationConfig(arn, events, prefix, suffix, ignoreExisting) + if perr != nil { + return perr.Cause + } + return nil +} + +// getCreateBucketEventsResponse calls createBucketEvent to add a bucket event notification +func getCreateBucketEventsResponse(bucketName string, eventReq *models.BucketEventRequest) error { + s3Client, err := newS3BucketClient(swag.String(bucketName)) + if err != nil { + log.Println("error creating MinIO Client:", err) + return err + } + // create a minioClient interface implementation + // defining the client to be used + mcS3Client := mcS3Client{client: s3Client} + err = createBucketEvent(mcS3Client, *eventReq.Configuration.Arn, eventReq.Configuration.Events, eventReq.Configuration.Prefix, eventReq.Configuration.Suffix, eventReq.IgnoreExisting) + if err != nil { + log.Println("error creating bucket event:", err) + return err + } + return nil +} diff --git a/restapi/user_buckets_events_test.go b/restapi/user_buckets_events_test.go index b8d127c73..2555ff3b1 100644 --- a/restapi/user_buckets_events_test.go +++ b/restapi/user_buckets_events_test.go @@ -22,7 +22,9 @@ import ( "errors" + "github.com/go-openapi/swag" "github.com/minio/m3/mcs/models" + "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v6" "github.com/stretchr/testify/assert" ) @@ -35,6 +37,61 @@ func (mc minioClientMock) getBucketNotification(bucketName string) (bucketNotifi return minioGetBucketNotificationMock(bucketName) } +//// Mock mc S3Client functions //// +var mcAddNotificationConfigMock func(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error + +// Define a mock struct of mc S3Client interface implementation +type s3ClientMock struct { +} + +// implements mc.S3Client.AddNotificationConfigMock(ctx) +func (c s3ClientMock) addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error { + return mcAddNotificationConfigMock(arn, events, prefix, suffix, ignoreExisting) +} + +func TestAddBucketNotification(t *testing.T) { + assert := assert.New(t) + // mock minIO client + client := s3ClientMock{} + function := "createBucketEvent()" + // Test-1: createBucketEvent() set an event with empty parameters and events, should set default values with no error + testArn := "arn:minio:sqs::test:postgresql" + testNotificationEvents := []models.NotificationEventType{} + testPrefix := "" + testSuffix := "" + testIgnoreExisting := false + mcAddNotificationConfigMock = func(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error { + return nil + } + if err := createBucketEvent(client, testArn, testNotificationEvents, testPrefix, testSuffix, testIgnoreExisting); err != nil { + t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + } + + // Test-2: createBucketEvent() with different even types in list shouls create event with no errors + testArn = "arn:minio:sqs::test:postgresql" + testNotificationEvents = []models.NotificationEventType{ + models.NotificationEventTypePut, + models.NotificationEventTypeGet, + } + testPrefix = "photos/" + testSuffix = ".jpg" + testIgnoreExisting = true + mcAddNotificationConfigMock = func(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error { + return nil + } + if err := createBucketEvent(client, testArn, testNotificationEvents, testPrefix, testSuffix, testIgnoreExisting); err != nil { + t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) + } + + // Test-3 createBucketEvent() S3Client.AddNotificationConfig returns an error and is handled correctly + mcAddNotificationConfigMock = func(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error { + return probe.NewError(errors.New("error")) + } + if err := createBucketEvent(client, testArn, testNotificationEvents, testPrefix, testSuffix, testIgnoreExisting); assert.Error(err) { + assert.Equal("error", err.Error()) + } +} + func TestListBucketEvents(t *testing.T) { assert := assert.New(t) // mock minIO client @@ -76,7 +133,7 @@ func TestListBucketEvents(t *testing.T) { } expectedOutput := []*models.NotificationConfig{ &models.NotificationConfig{ - Arn: "arn:minio:sqs::test:postgresql", + Arn: swag.String("arn:minio:sqs::test:postgresql"), ID: "", Prefix: "file/", Suffix: ".jpg", @@ -125,7 +182,7 @@ func TestListBucketEvents(t *testing.T) { } expectedOutput = []*models.NotificationConfig{ &models.NotificationConfig{ - Arn: "arn:minio:sqs::test:postgresql", + Arn: swag.String("arn:minio:sqs::test:postgresql"), ID: "", Prefix: "", Suffix: "", @@ -226,7 +283,7 @@ func TestListBucketEvents(t *testing.T) { // order matters in output: topic,queue then lambda are given respectively expectedOutput = []*models.NotificationConfig{ &models.NotificationConfig{ - Arn: "topic", + Arn: swag.String("topic"), ID: "", Prefix: "topic/", Suffix: ".gif", @@ -235,7 +292,7 @@ func TestListBucketEvents(t *testing.T) { }, }, &models.NotificationConfig{ - Arn: "arn:minio:sqs::test:postgresql", + Arn: swag.String("arn:minio:sqs::test:postgresql"), ID: "", Prefix: "", Suffix: "", @@ -244,7 +301,7 @@ func TestListBucketEvents(t *testing.T) { }, }, &models.NotificationConfig{ - Arn: "lambda", + Arn: swag.String("lambda"), ID: "", Prefix: "lambda/", Suffix: ".png", diff --git a/swagger.yml b/swagger.yml index 380efcb1c..2054038de 100644 --- a/swagger.yml +++ b/swagger.yml @@ -159,6 +159,28 @@ paths: $ref: "#/definitions/error" tags: - UserAPI + post: + summary: Create Bucket Event + operationId: CreateBucketEvent + parameters: + - name: bucket_name + in: path + required: true + type: string + - name: body + in: body + required: true + schema: + $ref: '#/definitions/bucketEventRequest' + responses: + 201: + description: A successful response. + default: + description: Generic error response. + schema: + $ref: "#/definitions/error" + tags: + - UserAPI /api/v1/users: get: summary: List Users @@ -839,6 +861,8 @@ definitions: - get notificationConfig: type: object + required: + - arn properties: id: type: string @@ -855,6 +879,15 @@ definitions: suffix: type: string title: "filter event associated to the specified suffix" + bucketEventRequest: + type: object + required: + - configuration + properties: + configuration: + $ref: "#/definitions/notificationConfig" + ignoreExisting: + type: boolean listBucketEventsResponse: type: object properties: