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:
Nitish Tiwari 2020-02-06 13:20:10 +05:30 committed by GitHub
parent 45d725c0a3
commit e5951e30d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 372 additions and 96 deletions

View file

@ -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",

View file

@ -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
} }

View file

@ -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)

View file

@ -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

View file

@ -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
}

View 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()
}

View file

@ -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

View file

@ -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{}
}

View file

@ -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)
} }

View file

@ -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

View file

@ -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)
} }
}) })

View file

@ -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

View file

@ -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
} }

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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()
} }

View file

@ -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
} }