mirror of
https://github.com/minio/minio.git
synced 2024-12-24 06:05:55 -05:00
Add support for Object Tagging in LifeCycle configuration (#8880)
Fixes #8870 Co-Authored-By: Krishnan Parthasarathi <krisis@users.noreply.github.com>
This commit is contained in:
parent
45d725c0a3
commit
e5951e30d0
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||||
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||||
"github.com/minio/minio/pkg/bucket/object/tagging"
|
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||||
"github.com/minio/minio/pkg/bucket/policy"
|
"github.com/minio/minio/pkg/bucket/policy"
|
||||||
@ -1795,6 +1796,12 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
// their internal error types. This code is only
|
// their internal error types. This code is only
|
||||||
// useful with gateway implementations.
|
// useful with gateway implementations.
|
||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
|
case lifecycle.Error:
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "InvalidRequest",
|
||||||
|
Description: e.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
case tagging.Error:
|
case tagging.Error:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "InvalidTag",
|
Code: "InvalidTag",
|
||||||
|
@ -67,7 +67,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
|||||||
|
|
||||||
bucketLifecycle, err := lifecycle.ParseLifecycleConfig(io.LimitReader(r.Body, r.ContentLength))
|
bucketLifecycle, err := lifecycle.ParseLifecycleConfig(io.LimitReader(r.Body, r.ContentLength))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
|||||||
// Calculate the common prefix of all lifecycle rules
|
// Calculate the common prefix of all lifecycle rules
|
||||||
var prefixes []string
|
var prefixes []string
|
||||||
for _, rule := range l.Rules {
|
for _, rule := range l.Rules {
|
||||||
prefixes = append(prefixes, rule.Filter.Prefix)
|
prefixes = append(prefixes, rule.Prefix())
|
||||||
}
|
}
|
||||||
commonPrefix := lcp(prefixes)
|
commonPrefix := lcp(prefixes)
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
|||||||
var objects []string
|
var objects []string
|
||||||
for _, obj := range res.Objects {
|
for _, obj := range res.Objects {
|
||||||
// Find the action that need to be executed
|
// Find the action that need to be executed
|
||||||
action := l.ComputeAction(obj.Name, obj.ModTime)
|
action := l.ComputeAction(obj.Name, obj.UserTags, obj.ModTime)
|
||||||
switch action {
|
switch action {
|
||||||
case lifecycle.DeleteAction:
|
case lifecycle.DeleteAction:
|
||||||
objects = append(objects, obj.Name)
|
objects = append(objects, obj.Name)
|
||||||
|
@ -2877,7 +2877,6 @@ func (api objectAPIHandlers) PutObjectTaggingHandler(w http.ResponseWriter, r *h
|
|||||||
}
|
}
|
||||||
|
|
||||||
tagging, err := tagging.ParseTagging(io.LimitReader(r.Body, r.ContentLength))
|
tagging, err := tagging.ParseTagging(io.LimitReader(r.Body, r.ContentLength))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
|
@ -18,25 +18,47 @@ package lifecycle
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
|
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// And - a tag to combine a prefix and multiple tags for lifecycle configuration rule.
|
// And - a tag to combine a prefix and multiple tags for lifecycle configuration rule.
|
||||||
type And struct {
|
type And struct {
|
||||||
XMLName xml.Name `xml:"And"`
|
XMLName xml.Name `xml:"And"`
|
||||||
Prefix string `xml:"Prefix,omitempty"`
|
Prefix string `xml:"Prefix,omitempty"`
|
||||||
Tags []Tag `xml:"Tag,omitempty"`
|
Tags []tagging.Tag `xml:"Tag,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var errAndUnsupported = errors.New("Specifying <And></And> tag is not supported")
|
var errDuplicateTagKey = Errorf("Duplicate Tag Keys are not allowed")
|
||||||
|
|
||||||
// UnmarshalXML is extended to indicate lack of support for And xml
|
// isEmpty returns true if Tags field is null
|
||||||
// tag in object lifecycle configuration
|
func (a And) isEmpty() bool {
|
||||||
func (a And) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
return len(a.Tags) == 0 && a.Prefix == ""
|
||||||
return errAndUnsupported
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalXML is extended to leave out <And></And> tags
|
// Validate - validates the And field
|
||||||
func (a And) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
func (a And) Validate() error {
|
||||||
|
if a.ContainsDuplicateTag() {
|
||||||
|
return errDuplicateTagKey
|
||||||
|
}
|
||||||
|
for _, t := range a.Tags {
|
||||||
|
if err := t.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContainsDuplicateTag - returns true if duplicate keys are present in And
|
||||||
|
func (a And) ContainsDuplicateTag() bool {
|
||||||
|
x := make(map[string]struct{}, len(a.Tags))
|
||||||
|
|
||||||
|
for _, t := range a.Tags {
|
||||||
|
if _, has := x[t.Key]; has {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
x[t.Key] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
44
pkg/bucket/lifecycle/error.go
Normal file
44
pkg/bucket/lifecycle/error.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error is the generic type for any error happening during tag
|
||||||
|
// parsing.
|
||||||
|
type Error struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf - formats according to a format specifier and returns
|
||||||
|
// the string as a value that satisfies error of type tagging.Error
|
||||||
|
func Errorf(format string, a ...interface{}) error {
|
||||||
|
return Error{err: fmt.Errorf(format, a...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap the internal error.
|
||||||
|
func (e Error) Unwrap() error { return e.err }
|
||||||
|
|
||||||
|
// Error 'error' compatible method.
|
||||||
|
func (e Error) Error() string {
|
||||||
|
if e.err == nil {
|
||||||
|
return "lifecycle: cause <nil>"
|
||||||
|
}
|
||||||
|
return e.err.Error()
|
||||||
|
}
|
@ -18,15 +18,14 @@ package lifecycle
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errLifecycleInvalidDate = errors.New("Date must be provided in ISO 8601 format")
|
errLifecycleInvalidDate = Errorf("Date must be provided in ISO 8601 format")
|
||||||
errLifecycleInvalidDays = errors.New("Days must be positive integer when used with Expiration")
|
errLifecycleInvalidDays = Errorf("Days must be positive integer when used with Expiration")
|
||||||
errLifecycleInvalidExpiration = errors.New("At least one of Days or Date should be present inside Expiration")
|
errLifecycleInvalidExpiration = Errorf("At least one of Days or Date should be present inside Expiration")
|
||||||
errLifecycleDateNotMidnight = errors.New(" 'Date' must be at midnight GMT")
|
errLifecycleDateNotMidnight = Errorf("'Date' must be at midnight GMT")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExpirationDays is a type alias to unmarshal Days in Expiration
|
// ExpirationDays is a type alias to unmarshal Days in Expiration
|
||||||
@ -121,7 +120,6 @@ func (e Expiration) Validate() error {
|
|||||||
// IsDaysNull returns true if days field is null
|
// IsDaysNull returns true if days field is null
|
||||||
func (e Expiration) IsDaysNull() bool {
|
func (e Expiration) IsDaysNull() bool {
|
||||||
return e.Days == ExpirationDays(0)
|
return e.Days == ExpirationDays(0)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDateNull returns true if date field is null
|
// IsDateNull returns true if date field is null
|
||||||
|
@ -16,17 +16,52 @@
|
|||||||
|
|
||||||
package lifecycle
|
package lifecycle
|
||||||
|
|
||||||
import "encoding/xml"
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||||
|
)
|
||||||
|
|
||||||
// Filter - a filter for a lifecycle configuration Rule.
|
// Filter - a filter for a lifecycle configuration Rule.
|
||||||
type Filter struct {
|
type Filter struct {
|
||||||
XMLName xml.Name `xml:"Filter"`
|
XMLName xml.Name `xml:"Filter"`
|
||||||
And And `xml:"And,omitempty"`
|
Prefix string `xml:"Prefix,omitempty"`
|
||||||
Prefix string `xml:"Prefix"`
|
And And `xml:"And,omitempty"`
|
||||||
Tag Tag `xml:"Tag,omitempty"`
|
Tag tagging.Tag `xml:"Tag,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidFilter = Errorf("Filter must have exactly one of Prefix, Tag, or And specified")
|
||||||
|
)
|
||||||
|
|
||||||
// Validate - validates the filter element
|
// Validate - validates the filter element
|
||||||
func (f Filter) Validate() error {
|
func (f Filter) Validate() error {
|
||||||
|
// A Filter must have exactly one of Prefix, Tag, or And specified.
|
||||||
|
if !f.And.isEmpty() {
|
||||||
|
if f.Prefix != "" {
|
||||||
|
return errInvalidFilter
|
||||||
|
}
|
||||||
|
if !f.Tag.IsEmpty() {
|
||||||
|
return errInvalidFilter
|
||||||
|
}
|
||||||
|
if err := f.And.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f.Prefix != "" {
|
||||||
|
if !f.Tag.IsEmpty() {
|
||||||
|
return errInvalidFilter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !f.Tag.IsEmpty() {
|
||||||
|
if err := f.Tag.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isEmpty - returns true if Filter tag is empty
|
||||||
|
func (f Filter) isEmpty() bool {
|
||||||
|
return f.And.isEmpty() && f.Prefix == "" && f.Tag == tagging.Tag{}
|
||||||
|
}
|
||||||
|
@ -31,23 +31,91 @@ func TestUnsupportedFilters(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{ // Filter with And tags
|
{ // Filter with And tags
|
||||||
inputXML: ` <Filter>
|
inputXML: ` <Filter>
|
||||||
<And>
|
<And>
|
||||||
<Prefix></Prefix>
|
<Prefix>key-prefix</Prefix>
|
||||||
</And>
|
</And>
|
||||||
</Filter>`,
|
</Filter>`,
|
||||||
expectedErr: errAndUnsupported,
|
expectedErr: nil,
|
||||||
},
|
},
|
||||||
{ // Filter with Tag tags
|
{ // Filter with Tag tags
|
||||||
inputXML: ` <Filter>
|
inputXML: ` <Filter>
|
||||||
<Tag></Tag>
|
<Tag>
|
||||||
</Filter>`,
|
<Key>key1</Key>
|
||||||
expectedErr: errTagUnsupported,
|
<Value>value1</Value>
|
||||||
|
</Tag>
|
||||||
|
</Filter>`,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // Filter with Prefix tag
|
||||||
|
inputXML: ` <Filter>
|
||||||
|
<Prefix>key-prefix</Prefix>
|
||||||
|
</Filter>`,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // Filter without And and multiple Tag tags
|
||||||
|
inputXML: ` <Filter>
|
||||||
|
<Prefix>key-prefix</Prefix>
|
||||||
|
<Tag>
|
||||||
|
<Key>key1</Key>
|
||||||
|
<Value>value1</Value>
|
||||||
|
</Tag>
|
||||||
|
<Tag>
|
||||||
|
<Key>key2</Key>
|
||||||
|
<Value>value2</Value>
|
||||||
|
</Tag>
|
||||||
|
</Filter>`,
|
||||||
|
expectedErr: errInvalidFilter,
|
||||||
|
},
|
||||||
|
{ // Filter with And, Prefix & multiple Tag tags
|
||||||
|
inputXML: ` <Filter>
|
||||||
|
<And>
|
||||||
|
<Prefix>key-prefix</Prefix>
|
||||||
|
<Tag>
|
||||||
|
<Key>key1</Key>
|
||||||
|
<Value>value1</Value>
|
||||||
|
</Tag>
|
||||||
|
<Tag>
|
||||||
|
<Key>key2</Key>
|
||||||
|
<Value>value2</Value>
|
||||||
|
</Tag>
|
||||||
|
</And>
|
||||||
|
</Filter>`,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // Filter with And and multiple Tag tags
|
||||||
|
inputXML: ` <Filter>
|
||||||
|
<And>
|
||||||
|
<Tag>
|
||||||
|
<Key>key1</Key>
|
||||||
|
<Value>value1</Value>
|
||||||
|
</Tag>
|
||||||
|
<Tag>
|
||||||
|
<Key>key2</Key>
|
||||||
|
<Value>value2</Value>
|
||||||
|
</Tag>
|
||||||
|
</And>
|
||||||
|
</Filter>`,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // Filter without And and single Tag tag
|
||||||
|
inputXML: ` <Filter>
|
||||||
|
<Prefix>key-prefix</Prefix>
|
||||||
|
<Tag>
|
||||||
|
<Key>key1</Key>
|
||||||
|
<Value>value1</Value>
|
||||||
|
</Tag>
|
||||||
|
</Filter>`,
|
||||||
|
expectedErr: errInvalidFilter,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, tc := range testCases {
|
for i, tc := range testCases {
|
||||||
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||||
var filter Filter
|
var filter Filter
|
||||||
err := xml.Unmarshal([]byte(tc.inputXML), &filter)
|
err := xml.Unmarshal([]byte(tc.inputXML), &filter)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: Expected no error but got %v", i+1, err)
|
||||||
|
}
|
||||||
|
err = filter.Validate()
|
||||||
if err != tc.expectedErr {
|
if err != tc.expectedErr {
|
||||||
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
||||||
}
|
}
|
||||||
|
@ -18,16 +18,15 @@ package lifecycle
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errLifecycleTooManyRules = errors.New("Lifecycle configuration allows a maximum of 1000 rules")
|
errLifecycleTooManyRules = Errorf("Lifecycle configuration allows a maximum of 1000 rules")
|
||||||
errLifecycleNoRule = errors.New("Lifecycle configuration should have at least one rule")
|
errLifecycleNoRule = Errorf("Lifecycle configuration should have at least one rule")
|
||||||
errLifecycleOverlappingPrefix = errors.New("Lifecycle configuration has rules with overlapping prefix")
|
errLifecycleOverlappingPrefix = Errorf("Lifecycle configuration has rules with overlapping prefix")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Action represents a delete action or other transition
|
// Action represents a delete action or other transition
|
||||||
@ -88,8 +87,8 @@ func (lc Lifecycle) Validate() error {
|
|||||||
// N B Empty prefixes overlap with all prefixes
|
// N B Empty prefixes overlap with all prefixes
|
||||||
otherRules := lc.Rules[i+1:]
|
otherRules := lc.Rules[i+1:]
|
||||||
for _, otherRule := range otherRules {
|
for _, otherRule := range otherRules {
|
||||||
if strings.HasPrefix(lc.Rules[i].Filter.Prefix, otherRule.Filter.Prefix) ||
|
if strings.HasPrefix(lc.Rules[i].Prefix(), otherRule.Prefix()) ||
|
||||||
strings.HasPrefix(otherRule.Filter.Prefix, lc.Rules[i].Filter.Prefix) {
|
strings.HasPrefix(otherRule.Prefix(), lc.Rules[i].Prefix()) {
|
||||||
return errLifecycleOverlappingPrefix
|
return errLifecycleOverlappingPrefix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,13 +98,20 @@ func (lc Lifecycle) Validate() error {
|
|||||||
|
|
||||||
// FilterRuleActions returns the expiration and transition from the object name
|
// FilterRuleActions returns the expiration and transition from the object name
|
||||||
// after evaluating all rules.
|
// after evaluating all rules.
|
||||||
func (lc Lifecycle) FilterRuleActions(objName string) (Expiration, Transition) {
|
func (lc Lifecycle) FilterRuleActions(objName, objTags string) (Expiration, Transition) {
|
||||||
for _, rule := range lc.Rules {
|
for _, rule := range lc.Rules {
|
||||||
if strings.ToLower(rule.Status) != "enabled" {
|
if strings.ToLower(rule.Status) != "enabled" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(objName, rule.Filter.Prefix) {
|
tags := rule.Tags()
|
||||||
return rule.Expiration, Transition{}
|
if strings.HasPrefix(objName, rule.Prefix()) {
|
||||||
|
if tags != "" {
|
||||||
|
if strings.Contains(objTags, tags) {
|
||||||
|
return rule.Expiration, Transition{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return rule.Expiration, Transition{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Expiration{}, Transition{}
|
return Expiration{}, Transition{}
|
||||||
@ -113,9 +119,9 @@ func (lc Lifecycle) FilterRuleActions(objName string) (Expiration, Transition) {
|
|||||||
|
|
||||||
// ComputeAction returns the action to perform by evaluating all lifecycle rules
|
// ComputeAction returns the action to perform by evaluating all lifecycle rules
|
||||||
// against the object name and its modification time.
|
// against the object name and its modification time.
|
||||||
func (lc Lifecycle) ComputeAction(objName string, modTime time.Time) Action {
|
func (lc Lifecycle) ComputeAction(objName, objTags string, modTime time.Time) Action {
|
||||||
var action = NoneAction
|
var action = NoneAction
|
||||||
exp, _ := lc.FilterRuleActions(objName)
|
exp, _ := lc.FilterRuleActions(objName, objTags)
|
||||||
if !exp.IsDateNull() {
|
if !exp.IsDateNull() {
|
||||||
if time.Now().After(exp.Date.Time) {
|
if time.Now().After(exp.Date.Time) {
|
||||||
action = DeleteAction
|
action = DeleteAction
|
||||||
|
@ -52,7 +52,9 @@ func TestParseLifecycleConfig(t *testing.T) {
|
|||||||
Status: "Enabled",
|
Status: "Enabled",
|
||||||
Expiration: Expiration{Days: ExpirationDays(3)},
|
Expiration: Expiration{Days: ExpirationDays(3)},
|
||||||
Filter: Filter{
|
Filter: Filter{
|
||||||
Prefix: "/a/b/c",
|
And: And{
|
||||||
|
Prefix: "/a/b/c",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
overlappingRules := []Rule{rule1, rule2}
|
overlappingRules := []Rule{rule1, rule2}
|
||||||
@ -67,26 +69,26 @@ func TestParseLifecycleConfig(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{ // Valid lifecycle config
|
{ // Valid lifecycle config
|
||||||
inputConfig: `<LifecycleConfiguration>
|
inputConfig: `<LifecycleConfiguration>
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>
|
<Filter>
|
||||||
<Prefix>prefix</Prefix>
|
<Prefix>prefix</Prefix>
|
||||||
</Filter>
|
</Filter>
|
||||||
<Status>Enabled</Status>
|
<Status>Enabled</Status>
|
||||||
<Expiration><Days>3</Days></Expiration>
|
<Expiration><Days>3</Days></Expiration>
|
||||||
</Rule>
|
</Rule>
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>
|
<Filter>
|
||||||
<Prefix>another-prefix</Prefix>
|
<Prefix>another-prefix</Prefix>
|
||||||
</Filter>
|
</Filter>
|
||||||
<Status>Enabled</Status>
|
<Status>Enabled</Status>
|
||||||
<Expiration><Days>3</Days></Expiration>
|
<Expiration><Days>3</Days></Expiration>
|
||||||
</Rule>
|
</Rule>
|
||||||
</LifecycleConfiguration>`,
|
</LifecycleConfiguration>`,
|
||||||
expectedErr: nil,
|
expectedErr: nil,
|
||||||
},
|
},
|
||||||
{ // lifecycle config with no rules
|
{ // lifecycle config with no rules
|
||||||
inputConfig: `<LifecycleConfiguration>
|
inputConfig: `<LifecycleConfiguration>
|
||||||
</LifecycleConfiguration>`,
|
</LifecycleConfiguration>`,
|
||||||
expectedErr: errLifecycleNoRule,
|
expectedErr: errLifecycleNoRule,
|
||||||
},
|
},
|
||||||
{ // lifecycle config with more than 1000 rules
|
{ // lifecycle config with more than 1000 rules
|
||||||
@ -105,9 +107,7 @@ func TestParseLifecycleConfig(t *testing.T) {
|
|||||||
if _, err = ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig))); err != tc.expectedErr {
|
if _, err = ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig))); err != tc.expectedErr {
|
||||||
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +163,7 @@ func TestComputeActions(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
inputConfig string
|
inputConfig string
|
||||||
objectName string
|
objectName string
|
||||||
|
objectTags string
|
||||||
objectModTime time.Time
|
objectModTime time.Time
|
||||||
expectedAction Action
|
expectedAction Action
|
||||||
}{
|
}{
|
||||||
@ -213,6 +214,46 @@ func TestComputeActions(t *testing.T) {
|
|||||||
objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago
|
objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago
|
||||||
expectedAction: DeleteAction,
|
expectedAction: DeleteAction,
|
||||||
},
|
},
|
||||||
|
// Should remove (Tags match)
|
||||||
|
{
|
||||||
|
inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag1</Key><Value>value1</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`,
|
||||||
|
objectName: "foodir/fooobject",
|
||||||
|
objectTags: "tag1=value1&tag2=value2",
|
||||||
|
objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago
|
||||||
|
expectedAction: DeleteAction,
|
||||||
|
},
|
||||||
|
// Should remove (Multiple Rules, Tags match)
|
||||||
|
{
|
||||||
|
inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag1</Key><Value>value1</Value><Key>tag2</Key><Value>value2</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule><Rule><Filter><And><Prefix>abc/</Prefix><Tag><Key>tag2</Key><Value>value</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`,
|
||||||
|
objectName: "foodir/fooobject",
|
||||||
|
objectTags: "tag1=value1&tag2=value2",
|
||||||
|
objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago
|
||||||
|
expectedAction: DeleteAction,
|
||||||
|
},
|
||||||
|
// Should remove (Tags match)
|
||||||
|
{
|
||||||
|
inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag1</Key><Value>value1</Value><Key>tag2</Key><Value>value2</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`,
|
||||||
|
objectName: "foodir/fooobject",
|
||||||
|
objectTags: "tag1=value1&tag2=value2",
|
||||||
|
objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago
|
||||||
|
expectedAction: DeleteAction,
|
||||||
|
},
|
||||||
|
// Should not remove (Tags don't match)
|
||||||
|
{
|
||||||
|
inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag</Key><Value>value1</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`,
|
||||||
|
objectName: "foodir/fooobject",
|
||||||
|
objectTags: "tag1=value1",
|
||||||
|
objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago
|
||||||
|
expectedAction: NoneAction,
|
||||||
|
},
|
||||||
|
// Should not remove (Tags match, but prefix doesn't match)
|
||||||
|
{
|
||||||
|
inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag1</Key><Value>value1</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`,
|
||||||
|
objectName: "foxdir/fooobject",
|
||||||
|
objectTags: "tag1=value1",
|
||||||
|
objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago
|
||||||
|
expectedAction: NoneAction,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tc := range testCases {
|
for i, tc := range testCases {
|
||||||
@ -221,7 +262,7 @@ func TestComputeActions(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%d: Got unexpected error: %v", i+1, err)
|
t.Fatalf("%d: Got unexpected error: %v", i+1, err)
|
||||||
}
|
}
|
||||||
if resultAction := lc.ComputeAction(tc.objectName, tc.objectModTime); resultAction != tc.expectedAction {
|
if resultAction := lc.ComputeAction(tc.objectName, tc.objectTags, tc.objectModTime); resultAction != tc.expectedAction {
|
||||||
t.Fatalf("%d: Expected action: `%v`, got: `%v`", i+1, tc.expectedAction, resultAction)
|
t.Fatalf("%d: Expected action: `%v`, got: `%v`", i+1, tc.expectedAction, resultAction)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -18,7 +18,6 @@ package lifecycle
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NoncurrentVersionExpiration - an action for lifecycle configuration rule.
|
// NoncurrentVersionExpiration - an action for lifecycle configuration rule.
|
||||||
@ -34,8 +33,8 @@ type NoncurrentVersionTransition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errNoncurrentVersionExpirationUnsupported = errors.New("Specifying <NoncurrentVersionExpiration></NoncurrentVersionExpiration> is not supported")
|
errNoncurrentVersionExpirationUnsupported = Errorf("Specifying <NoncurrentVersionExpiration></NoncurrentVersionExpiration> is not supported")
|
||||||
errNoncurrentVersionTransitionUnsupported = errors.New("Specifying <NoncurrentVersionTransition></NoncurrentVersionTransition> is not supported")
|
errNoncurrentVersionTransitionUnsupported = Errorf("Specifying <NoncurrentVersionTransition></NoncurrentVersionTransition> is not supported")
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnmarshalXML is extended to indicate lack of support for
|
// UnmarshalXML is extended to indicate lack of support for
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
package lifecycle
|
package lifecycle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rule - a rule for lifecycle configuration.
|
// Rule - a rule for lifecycle configuration.
|
||||||
@ -26,7 +26,7 @@ type Rule struct {
|
|||||||
XMLName xml.Name `xml:"Rule"`
|
XMLName xml.Name `xml:"Rule"`
|
||||||
ID string `xml:"ID,omitempty"`
|
ID string `xml:"ID,omitempty"`
|
||||||
Status string `xml:"Status"`
|
Status string `xml:"Status"`
|
||||||
Filter Filter `xml:"Filter"`
|
Filter Filter `xml:"Filter,omitempty"`
|
||||||
Expiration Expiration `xml:"Expiration,omitempty"`
|
Expiration Expiration `xml:"Expiration,omitempty"`
|
||||||
Transition Transition `xml:"Transition,omitempty"`
|
Transition Transition `xml:"Transition,omitempty"`
|
||||||
// FIXME: add a type to catch unsupported AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty"`
|
// FIXME: add a type to catch unsupported AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty"`
|
||||||
@ -35,13 +35,13 @@ type Rule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errInvalidRuleID = errors.New("ID must be less than 255 characters")
|
errInvalidRuleID = Errorf("ID must be less than 255 characters")
|
||||||
errEmptyRuleStatus = errors.New("Status should not be empty")
|
errEmptyRuleStatus = Errorf("Status should not be empty")
|
||||||
errInvalidRuleStatus = errors.New("Status must be set to either Enabled or Disabled")
|
errInvalidRuleStatus = Errorf("Status must be set to either Enabled or Disabled")
|
||||||
errMissingExpirationAction = errors.New("No expiration action found")
|
errMissingExpirationAction = Errorf("No expiration action found")
|
||||||
)
|
)
|
||||||
|
|
||||||
// isIDValid - checks if ID is valid or not.
|
// validateID - checks if ID is valid or not.
|
||||||
func (r Rule) validateID() error {
|
func (r Rule) validateID() error {
|
||||||
// cannot be longer than 255 characters
|
// cannot be longer than 255 characters
|
||||||
if len(string(r.ID)) > 255 {
|
if len(string(r.ID)) > 255 {
|
||||||
@ -50,7 +50,7 @@ func (r Rule) validateID() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isStatusValid - checks if status is valid or not.
|
// validateStatus - checks if status is valid or not.
|
||||||
func (r Rule) validateStatus() error {
|
func (r Rule) validateStatus() error {
|
||||||
// Status can't be empty
|
// Status can't be empty
|
||||||
if len(r.Status) == 0 {
|
if len(r.Status) == 0 {
|
||||||
@ -71,6 +71,43 @@ func (r Rule) validateAction() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r Rule) validateFilter() error {
|
||||||
|
return r.Filter.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix - a rule can either have prefix under <filter></filter> or under
|
||||||
|
// <filter><and></and></filter>. This method returns the prefix from the
|
||||||
|
// location where it is available
|
||||||
|
func (r Rule) Prefix() string {
|
||||||
|
if r.Filter.Prefix != "" {
|
||||||
|
return r.Filter.Prefix
|
||||||
|
}
|
||||||
|
if r.Filter.And.Prefix != "" {
|
||||||
|
return r.Filter.And.Prefix
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags - a rule can either have tag under <filter></filter> or under
|
||||||
|
// <filter><and></and></filter>. This method returns all the tags from the
|
||||||
|
// rule in the format tag1=value1&tag2=value2
|
||||||
|
func (r Rule) Tags() string {
|
||||||
|
if !r.Filter.Tag.IsEmpty() {
|
||||||
|
return r.Filter.Tag.String()
|
||||||
|
}
|
||||||
|
if len(r.Filter.And.Tags) != 0 {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, t := range r.Filter.And.Tags {
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
buf.WriteString("&")
|
||||||
|
}
|
||||||
|
buf.WriteString(t.String())
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// Validate - validates the rule element
|
// Validate - validates the rule element
|
||||||
func (r Rule) Validate() error {
|
func (r Rule) Validate() error {
|
||||||
if err := r.validateID(); err != nil {
|
if err := r.validateID(); err != nil {
|
||||||
@ -82,5 +119,8 @@ func (r Rule) Validate() error {
|
|||||||
if err := r.validateAction(); err != nil {
|
if err := r.validateAction(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := r.validateFilter(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ package lifecycle
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tag - a tag for a lifecycle configuration Rule filter.
|
// Tag - a tag for a lifecycle configuration Rule filter.
|
||||||
@ -28,7 +27,7 @@ type Tag struct {
|
|||||||
Value string `xml:"Value,omitempty"`
|
Value string `xml:"Value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var errTagUnsupported = errors.New("Specifying <Tag></Tag> is not supported")
|
var errTagUnsupported = Errorf("Specifying <Tag></Tag> is not supported")
|
||||||
|
|
||||||
// UnmarshalXML is extended to indicate lack of support for Tag
|
// UnmarshalXML is extended to indicate lack of support for Tag
|
||||||
// xml tag in object lifecycle configuration
|
// xml tag in object lifecycle configuration
|
||||||
|
@ -18,7 +18,6 @@ package lifecycle
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Transition - transition actions for a rule in lifecycle configuration.
|
// Transition - transition actions for a rule in lifecycle configuration.
|
||||||
@ -29,7 +28,7 @@ type Transition struct {
|
|||||||
StorageClass string `xml:"StorageClass"`
|
StorageClass string `xml:"StorageClass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var errTransitionUnsupported = errors.New("Specifying <Transition></Transition> tag is not supported")
|
var errTransitionUnsupported = Errorf("Specifying <Transition></Transition> tag is not supported")
|
||||||
|
|
||||||
// UnmarshalXML is extended to indicate lack of support for Transition
|
// UnmarshalXML is extended to indicate lack of support for Transition
|
||||||
// xml tag in object lifecycle configuration
|
// xml tag in object lifecycle configuration
|
||||||
|
@ -18,14 +18,15 @@ package tagging
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tag - single tag
|
// Tag - single tag
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
XMLName xml.Name `xml:"Tag"`
|
XMLName xml.Name `xml:"Tag"`
|
||||||
Key string `xml:"Key"`
|
Key string `xml:"Key,omitempty"`
|
||||||
Value string `xml:"Value"`
|
Value string `xml:"Value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate - validates the tag element
|
// Validate - validates the tag element
|
||||||
@ -49,6 +50,10 @@ func (t Tag) validateKey() error {
|
|||||||
if len(t.Key) == 0 {
|
if len(t.Key) == 0 {
|
||||||
return ErrInvalidTagKey
|
return ErrInvalidTagKey
|
||||||
}
|
}
|
||||||
|
// Tag key shouldn't have "&"
|
||||||
|
if strings.Contains(t.Key, "&") {
|
||||||
|
return ErrInvalidTagKey
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,5 +63,20 @@ func (t Tag) validateValue() error {
|
|||||||
if utf8.RuneCountInString(t.Value) > maxTagValueLength {
|
if utf8.RuneCountInString(t.Value) > maxTagValueLength {
|
||||||
return ErrInvalidTagValue
|
return ErrInvalidTagValue
|
||||||
}
|
}
|
||||||
|
// Tag value shouldn't have "&"
|
||||||
|
if strings.Contains(t.Value, "&") {
|
||||||
|
return ErrInvalidTagValue
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEmpty - checks if tag is empty or not
|
||||||
|
func (t Tag) IsEmpty() bool {
|
||||||
|
return t.Key == "" && t.Value == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - returns a string in format "tag1=value1" for the
|
||||||
|
// current Tag
|
||||||
|
func (t Tag) String() string {
|
||||||
|
return t.Key + "=" + t.Value
|
||||||
|
}
|
||||||
|
@ -51,11 +51,11 @@ func (t Tagging) Validate() error {
|
|||||||
if len(t.TagSet.Tags) > maxTags {
|
if len(t.TagSet.Tags) > maxTags {
|
||||||
return ErrTooManyTags
|
return ErrTooManyTags
|
||||||
}
|
}
|
||||||
|
if t.TagSet.ContainsDuplicateTag() {
|
||||||
|
return ErrInvalidTag
|
||||||
|
}
|
||||||
// Validate all the rules in the tagging config
|
// Validate all the rules in the tagging config
|
||||||
for _, ts := range t.TagSet.Tags {
|
for _, ts := range t.TagSet.Tags {
|
||||||
if t.TagSet.ContainsDuplicate(ts.Key) {
|
|
||||||
return ErrInvalidTag
|
|
||||||
}
|
|
||||||
if err := ts.Validate(); err != nil {
|
if err := ts.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -71,8 +71,7 @@ func (t Tagging) String() string {
|
|||||||
if buf.Len() > 0 {
|
if buf.Len() > 0 {
|
||||||
buf.WriteString("&")
|
buf.WriteString("&")
|
||||||
}
|
}
|
||||||
buf.WriteString(tag.Key + "=")
|
buf.WriteString(tag.String())
|
||||||
buf.WriteString(tag.Value)
|
|
||||||
}
|
}
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
@ -26,16 +26,16 @@ type TagSet struct {
|
|||||||
Tags []Tag `xml:"Tag"`
|
Tags []Tag `xml:"Tag"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainsDuplicate - returns true if duplicate keys are present in TagSet
|
// ContainsDuplicateTag - returns true if duplicate keys are present in TagSet
|
||||||
func (t TagSet) ContainsDuplicate(key string) bool {
|
func (t TagSet) ContainsDuplicateTag() bool {
|
||||||
var found bool
|
x := make(map[string]struct{}, len(t.Tags))
|
||||||
for _, tag := range t.Tags {
|
|
||||||
if tag.Key == key {
|
for _, t := range t.Tags {
|
||||||
if found {
|
if _, has := x[t.Key]; has {
|
||||||
return true
|
return true
|
||||||
}
|
|
||||||
found = true
|
|
||||||
}
|
}
|
||||||
|
x[t.Key] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user