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/logger"
"github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/bucket/lifecycle"
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
"github.com/minio/minio/pkg/bucket/object/tagging"
"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
// useful with gateway implementations.
switch e := err.(type) {
case lifecycle.Error:
apiErr = APIError{
Code: "InvalidRequest",
Description: e.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case tagging.Error:
apiErr = APIError{
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))
if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r))
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}

View File

@ -129,7 +129,7 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
// Calculate the common prefix of all lifecycle rules
var prefixes []string
for _, rule := range l.Rules {
prefixes = append(prefixes, rule.Filter.Prefix)
prefixes = append(prefixes, rule.Prefix())
}
commonPrefix := lcp(prefixes)
@ -143,7 +143,7 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
var objects []string
for _, obj := range res.Objects {
// 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 {
case lifecycle.DeleteAction:
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))
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return

View File

@ -18,25 +18,47 @@ package lifecycle
import (
"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.
type And struct {
XMLName xml.Name `xml:"And"`
Prefix string `xml:"Prefix,omitempty"`
Tags []Tag `xml:"Tag,omitempty"`
XMLName xml.Name `xml:"And"`
Prefix string `xml:"Prefix,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
// tag in object lifecycle configuration
func (a And) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return errAndUnsupported
// isEmpty returns true if Tags field is null
func (a And) isEmpty() bool {
return len(a.Tags) == 0 && a.Prefix == ""
}
// MarshalXML is extended to leave out <And></And> tags
func (a And) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
// Validate - validates the And field
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
}
// 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 (
"encoding/xml"
"errors"
"time"
)
var (
errLifecycleInvalidDate = errors.New("Date must be provided in ISO 8601 format")
errLifecycleInvalidDays = errors.New("Days must be positive integer when used with Expiration")
errLifecycleInvalidExpiration = errors.New("At least one of Days or Date should be present inside Expiration")
errLifecycleDateNotMidnight = errors.New(" 'Date' must be at midnight GMT")
errLifecycleInvalidDate = Errorf("Date must be provided in ISO 8601 format")
errLifecycleInvalidDays = Errorf("Days must be positive integer when used with Expiration")
errLifecycleInvalidExpiration = Errorf("At least one of Days or Date should be present inside Expiration")
errLifecycleDateNotMidnight = Errorf("'Date' must be at midnight GMT")
)
// 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
func (e Expiration) IsDaysNull() bool {
return e.Days == ExpirationDays(0)
}
// IsDateNull returns true if date field is null

View File

@ -16,17 +16,52 @@
package lifecycle
import "encoding/xml"
import (
"encoding/xml"
"github.com/minio/minio/pkg/bucket/object/tagging"
)
// Filter - a filter for a lifecycle configuration Rule.
type Filter struct {
XMLName xml.Name `xml:"Filter"`
And And `xml:"And,omitempty"`
Prefix string `xml:"Prefix"`
Tag Tag `xml:"Tag,omitempty"`
XMLName xml.Name `xml:"Filter"`
Prefix string `xml:"Prefix,omitempty"`
And And `xml:"And,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
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
}
// 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
inputXML: ` <Filter>
<And>
<Prefix></Prefix>
</And>
</Filter>`,
expectedErr: errAndUnsupported,
<And>
<Prefix>key-prefix</Prefix>
</And>
</Filter>`,
expectedErr: nil,
},
{ // Filter with Tag tags
inputXML: ` <Filter>
<Tag></Tag>
</Filter>`,
expectedErr: errTagUnsupported,
<Tag>
<Key>key1</Key>
<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 {
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
var filter 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 {
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
}

View File

@ -18,16 +18,15 @@ package lifecycle
import (
"encoding/xml"
"errors"
"io"
"strings"
"time"
)
var (
errLifecycleTooManyRules = errors.New("Lifecycle configuration allows a maximum of 1000 rules")
errLifecycleNoRule = errors.New("Lifecycle configuration should have at least one rule")
errLifecycleOverlappingPrefix = errors.New("Lifecycle configuration has rules with overlapping prefix")
errLifecycleTooManyRules = Errorf("Lifecycle configuration allows a maximum of 1000 rules")
errLifecycleNoRule = Errorf("Lifecycle configuration should have at least one rule")
errLifecycleOverlappingPrefix = Errorf("Lifecycle configuration has rules with overlapping prefix")
)
// 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
otherRules := lc.Rules[i+1:]
for _, otherRule := range otherRules {
if strings.HasPrefix(lc.Rules[i].Filter.Prefix, otherRule.Filter.Prefix) ||
strings.HasPrefix(otherRule.Filter.Prefix, lc.Rules[i].Filter.Prefix) {
if strings.HasPrefix(lc.Rules[i].Prefix(), otherRule.Prefix()) ||
strings.HasPrefix(otherRule.Prefix(), lc.Rules[i].Prefix()) {
return errLifecycleOverlappingPrefix
}
}
@ -99,13 +98,20 @@ func (lc Lifecycle) Validate() error {
// FilterRuleActions returns the expiration and transition from the object name
// 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 {
if strings.ToLower(rule.Status) != "enabled" {
continue
}
if strings.HasPrefix(objName, rule.Filter.Prefix) {
return rule.Expiration, Transition{}
tags := rule.Tags()
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{}
@ -113,9 +119,9 @@ func (lc Lifecycle) FilterRuleActions(objName string) (Expiration, Transition) {
// ComputeAction returns the action to perform by evaluating all lifecycle rules
// 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
exp, _ := lc.FilterRuleActions(objName)
exp, _ := lc.FilterRuleActions(objName, objTags)
if !exp.IsDateNull() {
if time.Now().After(exp.Date.Time) {
action = DeleteAction

View File

@ -52,7 +52,9 @@ func TestParseLifecycleConfig(t *testing.T) {
Status: "Enabled",
Expiration: Expiration{Days: ExpirationDays(3)},
Filter: Filter{
Prefix: "/a/b/c",
And: And{
Prefix: "/a/b/c",
},
},
}
overlappingRules := []Rule{rule1, rule2}
@ -67,26 +69,26 @@ func TestParseLifecycleConfig(t *testing.T) {
}{
{ // Valid lifecycle config
inputConfig: `<LifecycleConfiguration>
<Rule>
<Filter>
<Prefix>prefix</Prefix>
</Filter>
<Status>Enabled</Status>
<Expiration><Days>3</Days></Expiration>
</Rule>
<Rule>
<Filter>
<Prefix>another-prefix</Prefix>
</Filter>
<Status>Enabled</Status>
<Expiration><Days>3</Days></Expiration>
</Rule>
</LifecycleConfiguration>`,
<Rule>
<Filter>
<Prefix>prefix</Prefix>
</Filter>
<Status>Enabled</Status>
<Expiration><Days>3</Days></Expiration>
</Rule>
<Rule>
<Filter>
<Prefix>another-prefix</Prefix>
</Filter>
<Status>Enabled</Status>
<Expiration><Days>3</Days></Expiration>
</Rule>
</LifecycleConfiguration>`,
expectedErr: nil,
},
{ // lifecycle config with no rules
inputConfig: `<LifecycleConfiguration>
</LifecycleConfiguration>`,
</LifecycleConfiguration>`,
expectedErr: errLifecycleNoRule,
},
{ // 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 {
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
}
})
}
}
@ -163,6 +163,7 @@ func TestComputeActions(t *testing.T) {
testCases := []struct {
inputConfig string
objectName string
objectTags string
objectModTime time.Time
expectedAction Action
}{
@ -213,6 +214,46 @@ func TestComputeActions(t *testing.T) {
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></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 {
@ -221,7 +262,7 @@ func TestComputeActions(t *testing.T) {
if err != nil {
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)
}
})

View File

@ -18,7 +18,6 @@ package lifecycle
import (
"encoding/xml"
"errors"
)
// NoncurrentVersionExpiration - an action for lifecycle configuration rule.
@ -34,8 +33,8 @@ type NoncurrentVersionTransition struct {
}
var (
errNoncurrentVersionExpirationUnsupported = errors.New("Specifying <NoncurrentVersionExpiration></NoncurrentVersionExpiration> is not supported")
errNoncurrentVersionTransitionUnsupported = errors.New("Specifying <NoncurrentVersionTransition></NoncurrentVersionTransition> is not supported")
errNoncurrentVersionExpirationUnsupported = Errorf("Specifying <NoncurrentVersionExpiration></NoncurrentVersionExpiration> is not supported")
errNoncurrentVersionTransitionUnsupported = Errorf("Specifying <NoncurrentVersionTransition></NoncurrentVersionTransition> is not supported")
)
// UnmarshalXML is extended to indicate lack of support for

View File

@ -17,8 +17,8 @@
package lifecycle
import (
"bytes"
"encoding/xml"
"errors"
)
// Rule - a rule for lifecycle configuration.
@ -26,7 +26,7 @@ type Rule struct {
XMLName xml.Name `xml:"Rule"`
ID string `xml:"ID,omitempty"`
Status string `xml:"Status"`
Filter Filter `xml:"Filter"`
Filter Filter `xml:"Filter,omitempty"`
Expiration Expiration `xml:"Expiration,omitempty"`
Transition Transition `xml:"Transition,omitempty"`
// FIXME: add a type to catch unsupported AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty"`
@ -35,13 +35,13 @@ type Rule struct {
}
var (
errInvalidRuleID = errors.New("ID must be less than 255 characters")
errEmptyRuleStatus = errors.New("Status should not be empty")
errInvalidRuleStatus = errors.New("Status must be set to either Enabled or Disabled")
errMissingExpirationAction = errors.New("No expiration action found")
errInvalidRuleID = Errorf("ID must be less than 255 characters")
errEmptyRuleStatus = Errorf("Status should not be empty")
errInvalidRuleStatus = Errorf("Status must be set to either Enabled or Disabled")
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 {
// cannot be longer than 255 characters
if len(string(r.ID)) > 255 {
@ -50,7 +50,7 @@ func (r Rule) validateID() error {
return nil
}
// isStatusValid - checks if status is valid or not.
// validateStatus - checks if status is valid or not.
func (r Rule) validateStatus() error {
// Status can't be empty
if len(r.Status) == 0 {
@ -71,6 +71,43 @@ func (r Rule) validateAction() error {
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
func (r Rule) Validate() error {
if err := r.validateID(); err != nil {
@ -82,5 +119,8 @@ func (r Rule) Validate() error {
if err := r.validateAction(); err != nil {
return err
}
if err := r.validateFilter(); err != nil {
return err
}
return nil
}

View File

@ -18,7 +18,6 @@ package lifecycle
import (
"encoding/xml"
"errors"
)
// Tag - a tag for a lifecycle configuration Rule filter.
@ -28,7 +27,7 @@ type Tag struct {
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
// xml tag in object lifecycle configuration

View File

@ -18,7 +18,6 @@ package lifecycle
import (
"encoding/xml"
"errors"
)
// Transition - transition actions for a rule in lifecycle configuration.
@ -29,7 +28,7 @@ type Transition struct {
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
// xml tag in object lifecycle configuration

View File

@ -18,14 +18,15 @@ package tagging
import (
"encoding/xml"
"strings"
"unicode/utf8"
)
// Tag - single tag
type Tag struct {
XMLName xml.Name `xml:"Tag"`
Key string `xml:"Key"`
Value string `xml:"Value"`
Key string `xml:"Key,omitempty"`
Value string `xml:"Value,omitempty"`
}
// Validate - validates the tag element
@ -49,6 +50,10 @@ func (t Tag) validateKey() error {
if len(t.Key) == 0 {
return ErrInvalidTagKey
}
// Tag key shouldn't have "&"
if strings.Contains(t.Key, "&") {
return ErrInvalidTagKey
}
return nil
}
@ -58,5 +63,20 @@ func (t Tag) validateValue() error {
if utf8.RuneCountInString(t.Value) > maxTagValueLength {
return ErrInvalidTagValue
}
// Tag value shouldn't have "&"
if strings.Contains(t.Value, "&") {
return ErrInvalidTagValue
}
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 {
return ErrTooManyTags
}
if t.TagSet.ContainsDuplicateTag() {
return ErrInvalidTag
}
// Validate all the rules in the tagging config
for _, ts := range t.TagSet.Tags {
if t.TagSet.ContainsDuplicate(ts.Key) {
return ErrInvalidTag
}
if err := ts.Validate(); err != nil {
return err
}
@ -71,8 +71,7 @@ func (t Tagging) String() string {
if buf.Len() > 0 {
buf.WriteString("&")
}
buf.WriteString(tag.Key + "=")
buf.WriteString(tag.Value)
buf.WriteString(tag.String())
}
return buf.String()
}

View File

@ -26,16 +26,16 @@ type TagSet struct {
Tags []Tag `xml:"Tag"`
}
// ContainsDuplicate - returns true if duplicate keys are present in TagSet
func (t TagSet) ContainsDuplicate(key string) bool {
var found bool
for _, tag := range t.Tags {
if tag.Key == key {
if found {
return true
}
found = true
// ContainsDuplicateTag - returns true if duplicate keys are present in TagSet
func (t TagSet) ContainsDuplicateTag() bool {
x := make(map[string]struct{}, len(t.Tags))
for _, t := range t.Tags {
if _, has := x[t.Key]; has {
return true
}
x[t.Key] = struct{}{}
}
return false
}