github.com/minio/console@v1.3.0/api/user_buckets_lifecycle.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package api
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/minio/minio-go/v7"
    28  
    29  	"github.com/rs/xid"
    30  
    31  	"github.com/minio/mc/cmd/ilm"
    32  
    33  	"github.com/minio/minio-go/v7/pkg/lifecycle"
    34  
    35  	"github.com/go-openapi/runtime/middleware"
    36  	"github.com/minio/console/api/operations"
    37  	bucketApi "github.com/minio/console/api/operations/bucket"
    38  	"github.com/minio/console/models"
    39  )
    40  
    41  type MultiLifecycleResult struct {
    42  	BucketName string
    43  	Error      string
    44  }
    45  
    46  func registerBucketsLifecycleHandlers(api *operations.ConsoleAPI) {
    47  	api.BucketGetBucketLifecycleHandler = bucketApi.GetBucketLifecycleHandlerFunc(func(params bucketApi.GetBucketLifecycleParams, session *models.Principal) middleware.Responder {
    48  		listBucketLifecycleResponse, err := getBucketLifecycleResponse(session, params)
    49  		if err != nil {
    50  			return bucketApi.NewGetBucketLifecycleDefault(err.Code).WithPayload(err.APIError)
    51  		}
    52  		return bucketApi.NewGetBucketLifecycleOK().WithPayload(listBucketLifecycleResponse)
    53  	})
    54  	api.BucketAddBucketLifecycleHandler = bucketApi.AddBucketLifecycleHandlerFunc(func(params bucketApi.AddBucketLifecycleParams, session *models.Principal) middleware.Responder {
    55  		err := getAddBucketLifecycleResponse(session, params)
    56  		if err != nil {
    57  			return bucketApi.NewAddBucketLifecycleDefault(err.Code).WithPayload(err.APIError)
    58  		}
    59  		return bucketApi.NewAddBucketLifecycleCreated()
    60  	})
    61  	api.BucketUpdateBucketLifecycleHandler = bucketApi.UpdateBucketLifecycleHandlerFunc(func(params bucketApi.UpdateBucketLifecycleParams, session *models.Principal) middleware.Responder {
    62  		err := getEditBucketLifecycleRule(session, params)
    63  		if err != nil {
    64  			return bucketApi.NewUpdateBucketLifecycleDefault(err.Code).WithPayload(err.APIError)
    65  		}
    66  
    67  		return bucketApi.NewUpdateBucketLifecycleOK()
    68  	})
    69  	api.BucketDeleteBucketLifecycleRuleHandler = bucketApi.DeleteBucketLifecycleRuleHandlerFunc(func(params bucketApi.DeleteBucketLifecycleRuleParams, session *models.Principal) middleware.Responder {
    70  		err := getDeleteBucketLifecycleRule(session, params)
    71  		if err != nil {
    72  			return bucketApi.NewDeleteBucketLifecycleRuleDefault(err.Code).WithPayload(err.APIError)
    73  		}
    74  
    75  		return bucketApi.NewDeleteBucketLifecycleRuleNoContent()
    76  	})
    77  	api.BucketAddMultiBucketLifecycleHandler = bucketApi.AddMultiBucketLifecycleHandlerFunc(func(params bucketApi.AddMultiBucketLifecycleParams, session *models.Principal) middleware.Responder {
    78  		multiBucketResponse, err := getAddMultiBucketLifecycleResponse(session, params)
    79  		if err != nil {
    80  			bucketApi.NewAddMultiBucketLifecycleDefault(err.Code).WithPayload(err.APIError)
    81  		}
    82  
    83  		return bucketApi.NewAddMultiBucketLifecycleOK().WithPayload(multiBucketResponse)
    84  	})
    85  }
    86  
    87  // getBucketLifecycle() gets lifecycle lists for a bucket from MinIO API and returns their implementations
    88  func getBucketLifecycle(ctx context.Context, client MinioClient, bucketName string) (*models.BucketLifecycleResponse, error) {
    89  	lifecycleList, err := client.getLifecycleRules(ctx, bucketName)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	var rules []*models.ObjectBucketLifecycle
    94  
    95  	for _, rule := range lifecycleList.Rules {
    96  
    97  		var tags []*models.LifecycleTag
    98  
    99  		for _, tagData := range rule.RuleFilter.And.Tags {
   100  			tags = append(tags, &models.LifecycleTag{
   101  				Key:   tagData.Key,
   102  				Value: tagData.Value,
   103  			})
   104  		}
   105  
   106  		rulePrefix := rule.RuleFilter.And.Prefix
   107  
   108  		if rulePrefix == "" {
   109  			rulePrefix = rule.RuleFilter.Prefix
   110  		}
   111  
   112  		rules = append(rules, &models.ObjectBucketLifecycle{
   113  			ID:     rule.ID,
   114  			Status: rule.Status,
   115  			Prefix: rulePrefix,
   116  			Expiration: &models.ExpirationResponse{
   117  				Date:                              rule.Expiration.Date.Format(time.RFC3339),
   118  				Days:                              int64(rule.Expiration.Days),
   119  				DeleteMarker:                      rule.Expiration.DeleteMarker.IsEnabled(),
   120  				DeleteAll:                         bool(rule.Expiration.DeleteAll),
   121  				NoncurrentExpirationDays:          int64(rule.NoncurrentVersionExpiration.NoncurrentDays),
   122  				NewerNoncurrentExpirationVersions: int64(rule.NoncurrentVersionExpiration.NewerNoncurrentVersions),
   123  			},
   124  			Transition: &models.TransitionResponse{
   125  				Date:                     rule.Transition.Date.Format(time.RFC3339),
   126  				Days:                     int64(rule.Transition.Days),
   127  				StorageClass:             rule.Transition.StorageClass,
   128  				NoncurrentStorageClass:   rule.NoncurrentVersionTransition.StorageClass,
   129  				NoncurrentTransitionDays: int64(rule.NoncurrentVersionTransition.NoncurrentDays),
   130  			},
   131  			Tags: tags,
   132  		})
   133  	}
   134  
   135  	// serialize output
   136  	lifecycleBucketsResponse := &models.BucketLifecycleResponse{
   137  		Lifecycle: rules,
   138  	}
   139  
   140  	return lifecycleBucketsResponse, nil
   141  }
   142  
   143  // getBucketLifecycleResponse performs getBucketLifecycle() and serializes it to the handler's output
   144  func getBucketLifecycleResponse(session *models.Principal, params bucketApi.GetBucketLifecycleParams) (*models.BucketLifecycleResponse, *CodedAPIError) {
   145  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   146  	defer cancel()
   147  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   148  	if err != nil {
   149  		return nil, ErrorWithContext(ctx, err)
   150  	}
   151  	// create a minioClient interface implementation
   152  	// defining the client to be used
   153  	minioClient := minioClient{client: mClient}
   154  
   155  	bucketEvents, err := getBucketLifecycle(ctx, minioClient, params.BucketName)
   156  	if err != nil {
   157  		return nil, ErrorWithContext(ctx, ErrBucketLifeCycleNotConfigured, err)
   158  	}
   159  	return bucketEvents, nil
   160  }
   161  
   162  // addBucketLifecycle gets lifecycle lists for a bucket from MinIO API and returns their implementations
   163  func addBucketLifecycle(ctx context.Context, client MinioClient, params bucketApi.AddBucketLifecycleParams) error {
   164  	// Configuration that is already set.
   165  	lfcCfg, err := client.getLifecycleRules(ctx, params.BucketName)
   166  	if err != nil {
   167  		if e := err; minio.ToErrorResponse(e).Code == "NoSuchLifecycleConfiguration" {
   168  			lfcCfg = lifecycle.NewConfiguration()
   169  		} else {
   170  			return err
   171  		}
   172  	}
   173  
   174  	id := xid.New().String()
   175  
   176  	opts := ilm.LifecycleOptions{}
   177  
   178  	// Verify if transition rule is requested
   179  	switch params.Body.Type {
   180  	case models.AddBucketLifecycleTypeTransition:
   181  		if params.Body.TransitionDays == 0 && params.Body.NoncurrentversionTransitionDays == 0 {
   182  			return errors.New("you must provide a value for transition days or date")
   183  		}
   184  
   185  		status := !params.Body.Disable
   186  		opts = ilm.LifecycleOptions{
   187  			ID:                        id,
   188  			Prefix:                    &params.Body.Prefix,
   189  			Status:                    &status,
   190  			Tags:                      &params.Body.Tags,
   191  			ExpiredObjectDeleteMarker: &params.Body.ExpiredObjectDeleteMarker,
   192  			ExpiredObjectAllversions:  &params.Body.ExpiredObjectDeleteAll,
   193  		}
   194  
   195  		if params.Body.NoncurrentversionTransitionDays > 0 {
   196  			noncurrentVersionTransitionDays := int(params.Body.NoncurrentversionTransitionDays)
   197  			noncurrentVersionTransitionStorageClass := strings.ToUpper(params.Body.NoncurrentversionTransitionStorageClass)
   198  			opts.NoncurrentVersionTransitionDays = &noncurrentVersionTransitionDays
   199  			opts.NoncurrentVersionTransitionStorageClass = &noncurrentVersionTransitionStorageClass
   200  		} else if params.Body.TransitionDays > 0 {
   201  			tdays := strconv.Itoa(int(params.Body.TransitionDays))
   202  			sclass := strings.ToUpper(params.Body.StorageClass)
   203  			opts.TransitionDays = &tdays
   204  			opts.StorageClass = &sclass
   205  		}
   206  
   207  	case models.AddBucketLifecycleTypeExpiry:
   208  		// Verify if expiry items are set
   209  		if params.Body.NoncurrentversionTransitionDays != 0 {
   210  			return errors.New("non current version Transition Days cannot be set when expiry is being configured")
   211  		}
   212  
   213  		if params.Body.NoncurrentversionTransitionStorageClass != "" {
   214  			return errors.New("non current version Transition Storage Class cannot be set when expiry is being configured")
   215  		}
   216  
   217  		status := !params.Body.Disable
   218  		opts = ilm.LifecycleOptions{
   219  			ID:                        id,
   220  			Prefix:                    &params.Body.Prefix,
   221  			Status:                    &status,
   222  			Tags:                      &params.Body.Tags,
   223  			ExpiredObjectDeleteMarker: &params.Body.ExpiredObjectDeleteMarker,
   224  			ExpiredObjectAllversions:  &params.Body.ExpiredObjectDeleteAll,
   225  		}
   226  
   227  		if params.Body.NewerNoncurrentversionExpirationVersions > 0 {
   228  			versions := int(params.Body.NewerNoncurrentversionExpirationVersions)
   229  			opts.NewerNoncurrentExpirationVersions = &versions
   230  		}
   231  		switch {
   232  		case params.Body.NoncurrentversionExpirationDays > 0:
   233  			days := int(params.Body.NoncurrentversionExpirationDays)
   234  			opts.NoncurrentVersionExpirationDays = &days
   235  		case params.Body.ExpiryDays > 0:
   236  			days := strconv.Itoa(int(params.Body.ExpiryDays))
   237  			opts.ExpiryDays = &days
   238  		}
   239  	default:
   240  		// Non set, we return errors
   241  		return errors.New("no valid lifecycle configuration requested")
   242  	}
   243  
   244  	newRule, merr := opts.ToILMRule()
   245  	if merr != nil {
   246  		return merr.ToGoError()
   247  	}
   248  
   249  	lfcCfg.Rules = append(lfcCfg.Rules, newRule)
   250  
   251  	return client.setBucketLifecycle(ctx, params.BucketName, lfcCfg)
   252  }
   253  
   254  // getAddBucketLifecycleResponse returns the response of adding a bucket lifecycle response
   255  func getAddBucketLifecycleResponse(session *models.Principal, params bucketApi.AddBucketLifecycleParams) *CodedAPIError {
   256  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   257  	defer cancel()
   258  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   259  	if err != nil {
   260  		return ErrorWithContext(ctx, err)
   261  	}
   262  	// create a minioClient interface implementation
   263  	// defining the client to be used
   264  	minioClient := minioClient{client: mClient}
   265  
   266  	err = addBucketLifecycle(ctx, minioClient, params)
   267  	if err != nil {
   268  		return ErrorWithContext(ctx, err)
   269  	}
   270  
   271  	return nil
   272  }
   273  
   274  // editBucketLifecycle gets lifecycle lists for a bucket from MinIO API and updates the selected lifecycle rule
   275  func editBucketLifecycle(ctx context.Context, client MinioClient, params bucketApi.UpdateBucketLifecycleParams) error {
   276  	// Configuration that is already set.
   277  	lfcCfg, err := client.getLifecycleRules(ctx, params.BucketName)
   278  	if err != nil {
   279  		if e := err; minio.ToErrorResponse(e).Code == "NoSuchLifecycleConfiguration" {
   280  			lfcCfg = lifecycle.NewConfiguration()
   281  		} else {
   282  			return err
   283  		}
   284  	}
   285  
   286  	id := params.LifecycleID
   287  
   288  	opts := ilm.LifecycleOptions{}
   289  
   290  	// Verify if transition items are set
   291  	switch *params.Body.Type {
   292  	case models.UpdateBucketLifecycleTypeTransition:
   293  		if params.Body.TransitionDays == 0 && params.Body.NoncurrentversionTransitionDays == 0 {
   294  			return errors.New("you must select transition days or non-current transition days configuration")
   295  		}
   296  
   297  		status := !params.Body.Disable
   298  		opts = ilm.LifecycleOptions{
   299  			ID:                        id,
   300  			Prefix:                    &params.Body.Prefix,
   301  			Status:                    &status,
   302  			Tags:                      &params.Body.Tags,
   303  			ExpiredObjectDeleteMarker: &params.Body.ExpiredObjectDeleteMarker,
   304  			ExpiredObjectAllversions:  &params.Body.ExpiredObjectDeleteAll,
   305  		}
   306  
   307  		if params.Body.NoncurrentversionTransitionDays > 0 {
   308  			noncurrentVersionTransitionDays := int(params.Body.NoncurrentversionTransitionDays)
   309  			noncurrentVersionTransitionStorageClass := strings.ToUpper(params.Body.NoncurrentversionTransitionStorageClass)
   310  			opts.NoncurrentVersionTransitionDays = &noncurrentVersionTransitionDays
   311  			opts.NoncurrentVersionTransitionStorageClass = &noncurrentVersionTransitionStorageClass
   312  
   313  		} else {
   314  			tdays := strconv.Itoa(int(params.Body.TransitionDays))
   315  			sclass := strings.ToUpper(params.Body.StorageClass)
   316  			opts.TransitionDays = &tdays
   317  			opts.StorageClass = &sclass
   318  		}
   319  	case models.UpdateBucketLifecycleTypeExpiry: // Verify if expiry configuration is set
   320  		if params.Body.NoncurrentversionTransitionDays != 0 {
   321  			return errors.New("non current version Transition Days cannot be set when expiry is being configured")
   322  		}
   323  
   324  		if params.Body.NoncurrentversionTransitionStorageClass != "" {
   325  			return errors.New("non current version Transition Storage Class cannot be set when expiry is being configured")
   326  		}
   327  
   328  		status := !params.Body.Disable
   329  		opts = ilm.LifecycleOptions{
   330  			ID:                        id,
   331  			Prefix:                    &params.Body.Prefix,
   332  			Status:                    &status,
   333  			Tags:                      &params.Body.Tags,
   334  			ExpiredObjectDeleteMarker: &params.Body.ExpiredObjectDeleteMarker,
   335  			ExpiredObjectAllversions:  &params.Body.ExpiredObjectDeleteAll,
   336  		}
   337  
   338  		if params.Body.NoncurrentversionExpirationDays > 0 {
   339  			days := int(params.Body.NoncurrentversionExpirationDays)
   340  			opts.NoncurrentVersionExpirationDays = &days
   341  		} else {
   342  			days := strconv.Itoa(int(params.Body.ExpiryDays))
   343  			opts.ExpiryDays = &days
   344  		}
   345  	default:
   346  		// Non set, we return errors
   347  		return errors.New("no valid configuration requested")
   348  	}
   349  
   350  	var rule *lifecycle.Rule
   351  	for i := range lfcCfg.Rules {
   352  		if lfcCfg.Rules[i].ID == opts.ID {
   353  			rule = &lfcCfg.Rules[i]
   354  			break
   355  		}
   356  	}
   357  	if rule == nil {
   358  		return errors.New("unable to find the matching rule to update")
   359  	}
   360  
   361  	err2 := ilm.ApplyRuleFields(rule, opts)
   362  	if err2.ToGoError() != nil {
   363  		return fmt.Errorf("Unable to generate new lifecycle rule: %v", err2.ToGoError())
   364  	}
   365  
   366  	return client.setBucketLifecycle(ctx, params.BucketName, lfcCfg)
   367  }
   368  
   369  // getEditBucketLifecycleRule returns the response of bucket lifecycle tier edit
   370  func getEditBucketLifecycleRule(session *models.Principal, params bucketApi.UpdateBucketLifecycleParams) *CodedAPIError {
   371  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   372  	defer cancel()
   373  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   374  	if err != nil {
   375  		return ErrorWithContext(ctx, err)
   376  	}
   377  	// create a minioClient interface implementation
   378  	// defining the client to be used
   379  	minioClient := minioClient{client: mClient}
   380  
   381  	err = editBucketLifecycle(ctx, minioClient, params)
   382  	if err != nil {
   383  		return ErrorWithContext(ctx, err)
   384  	}
   385  
   386  	return nil
   387  }
   388  
   389  // deleteBucketLifecycle deletes lifecycle rule by passing an empty rule to a selected ID
   390  func deleteBucketLifecycle(ctx context.Context, client MinioClient, params bucketApi.DeleteBucketLifecycleRuleParams) error {
   391  	// Configuration that is already set.
   392  	lfcCfg, err := client.getLifecycleRules(ctx, params.BucketName)
   393  	if err != nil {
   394  		if e := err; minio.ToErrorResponse(e).Code == "NoSuchLifecycleConfiguration" {
   395  			lfcCfg = lifecycle.NewConfiguration()
   396  		} else {
   397  			return err
   398  		}
   399  	}
   400  
   401  	if len(lfcCfg.Rules) == 0 {
   402  		return errors.New("no rules available to delete")
   403  	}
   404  
   405  	var newRules []lifecycle.Rule
   406  
   407  	for _, rule := range lfcCfg.Rules {
   408  		if rule.ID != params.LifecycleID {
   409  			newRules = append(newRules, rule)
   410  		}
   411  	}
   412  
   413  	if len(newRules) == len(lfcCfg.Rules) && len(lfcCfg.Rules) > 0 {
   414  		// rule doesn't exist
   415  		return fmt.Errorf("lifecycle rule for id '%s' doesn't exist", params.LifecycleID)
   416  	}
   417  
   418  	lfcCfg.Rules = newRules
   419  
   420  	return client.setBucketLifecycle(ctx, params.BucketName, lfcCfg)
   421  }
   422  
   423  // getDeleteBucketLifecycleRule returns the response of bucket lifecycle tier delete
   424  func getDeleteBucketLifecycleRule(session *models.Principal, params bucketApi.DeleteBucketLifecycleRuleParams) *CodedAPIError {
   425  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   426  	defer cancel()
   427  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   428  	if err != nil {
   429  		return ErrorWithContext(ctx, err)
   430  	}
   431  	// create a minioClient interface implementation
   432  	// defining the client to be used
   433  	minioClient := minioClient{client: mClient}
   434  
   435  	err = deleteBucketLifecycle(ctx, minioClient, params)
   436  	if err != nil {
   437  		return ErrorWithContext(ctx, err)
   438  	}
   439  
   440  	return nil
   441  }
   442  
   443  // addMultiBucketLifecycle creates multibuckets lifecycle assignments
   444  func addMultiBucketLifecycle(ctx context.Context, client MinioClient, params bucketApi.AddMultiBucketLifecycleParams) []MultiLifecycleResult {
   445  	bucketsRelation := params.Body.Buckets
   446  
   447  	// Parallel Lifecycle rules set
   448  
   449  	parallelLifecycleBucket := func(bucketName string) chan MultiLifecycleResult {
   450  		remoteProc := make(chan MultiLifecycleResult)
   451  
   452  		lifecycleParams := models.AddBucketLifecycle{
   453  			Type:                                    *params.Body.Type,
   454  			StorageClass:                            params.Body.StorageClass,
   455  			TransitionDays:                          params.Body.TransitionDays,
   456  			Prefix:                                  params.Body.Prefix,
   457  			NoncurrentversionTransitionDays:         params.Body.NoncurrentversionTransitionDays,
   458  			NoncurrentversionTransitionStorageClass: params.Body.NoncurrentversionTransitionStorageClass,
   459  			NoncurrentversionExpirationDays:         params.Body.NoncurrentversionExpirationDays,
   460  			Tags:                                    params.Body.Tags,
   461  			ExpiryDays:                              params.Body.ExpiryDays,
   462  			Disable:                                 false,
   463  			ExpiredObjectDeleteMarker:               params.Body.ExpiredObjectDeleteMarker,
   464  			ExpiredObjectDeleteAll:                  params.Body.ExpiredObjectDeleteMarker,
   465  		}
   466  
   467  		go func() {
   468  			defer close(remoteProc)
   469  
   470  			lifecycleParams := bucketApi.AddBucketLifecycleParams{
   471  				BucketName: bucketName,
   472  				Body:       &lifecycleParams,
   473  			}
   474  
   475  			// We add lifecycle rule & expect a response
   476  			err := addBucketLifecycle(ctx, client, lifecycleParams)
   477  
   478  			errorReturn := ""
   479  
   480  			if err != nil {
   481  				errorReturn = err.Error()
   482  			}
   483  
   484  			retParams := MultiLifecycleResult{
   485  				BucketName: bucketName,
   486  				Error:      errorReturn,
   487  			}
   488  
   489  			remoteProc <- retParams
   490  		}()
   491  		return remoteProc
   492  	}
   493  
   494  	var lifecycleManagement []chan MultiLifecycleResult
   495  
   496  	for _, bucketName := range bucketsRelation {
   497  		rBucket := parallelLifecycleBucket(bucketName)
   498  		lifecycleManagement = append(lifecycleManagement, rBucket)
   499  	}
   500  
   501  	var resultsList []MultiLifecycleResult
   502  	for _, result := range lifecycleManagement {
   503  		res := <-result
   504  		resultsList = append(resultsList, res)
   505  	}
   506  
   507  	return resultsList
   508  }
   509  
   510  // getAddMultiBucketLifecycleResponse returns the response of multibucket lifecycle assignment
   511  func getAddMultiBucketLifecycleResponse(session *models.Principal, params bucketApi.AddMultiBucketLifecycleParams) (*models.MultiLifecycleResult, *CodedAPIError) {
   512  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   513  	defer cancel()
   514  	mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest))
   515  	if err != nil {
   516  		return nil, ErrorWithContext(ctx, err)
   517  	}
   518  	// create a minioClient interface implementation
   519  	// defining the client to be used
   520  	minioClient := minioClient{client: mClient}
   521  
   522  	multiCycleResult := addMultiBucketLifecycle(ctx, minioClient, params)
   523  
   524  	var returnList []*models.MulticycleResultItem
   525  
   526  	for _, resultItem := range multiCycleResult {
   527  		multicycleRS := models.MulticycleResultItem{
   528  			BucketName: resultItem.BucketName,
   529  			Error:      resultItem.Error,
   530  		}
   531  
   532  		returnList = append(returnList, &multicycleRS)
   533  	}
   534  
   535  	finalResult := models.MultiLifecycleResult{Results: returnList}
   536  
   537  	return &finalResult, nil
   538  }