mirror of
https://github.com/minio/minio.git
synced 2025-11-07 21:02:58 -05:00
rename all remaining packages to internal/ (#12418)
This is to ensure that there are no projects that try to import `minio/minio/pkg` into their own repo. Any such common packages should go to `https://github.com/minio/pkg`
This commit is contained in:
63
internal/bucket/replication/and.go
Normal file
63
internal/bucket/replication/and.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// And - a tag to combine a prefix and multiple tags for replication configuration rule.
|
||||
type And struct {
|
||||
XMLName xml.Name `xml:"And" json:"And"`
|
||||
Prefix string `xml:"Prefix,omitempty" json:"Prefix,omitempty"`
|
||||
Tags []Tag `xml:"Tag,omitempty" json:"Tag,omitempty"`
|
||||
}
|
||||
|
||||
var errDuplicateTagKey = Errorf("Duplicate Tag Keys are not allowed")
|
||||
|
||||
// isEmpty returns true if Tags field is null
|
||||
func (a And) isEmpty() bool {
|
||||
return len(a.Tags) == 0 && a.Prefix == ""
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
119
internal/bucket/replication/destination.go
Normal file
119
internal/bucket/replication/destination.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
// DestinationARNPrefix - destination ARN prefix as per AWS S3 specification.
|
||||
const DestinationARNPrefix = "arn:aws:s3:::"
|
||||
|
||||
// Destination - destination in ReplicationConfiguration.
|
||||
type Destination struct {
|
||||
XMLName xml.Name `xml:"Destination" json:"Destination"`
|
||||
Bucket string `xml:"Bucket" json:"Bucket"`
|
||||
StorageClass string `xml:"StorageClass" json:"StorageClass"`
|
||||
//EncryptionConfiguration TODO: not needed for MinIO
|
||||
}
|
||||
|
||||
func (d Destination) isValidStorageClass() bool {
|
||||
if d.StorageClass == "" {
|
||||
return true
|
||||
}
|
||||
return d.StorageClass == "STANDARD" || d.StorageClass == "REDUCED_REDUNDANCY"
|
||||
}
|
||||
|
||||
// IsValid - checks whether Destination is valid or not.
|
||||
func (d Destination) IsValid() bool {
|
||||
return d.Bucket != "" || !d.isValidStorageClass()
|
||||
}
|
||||
|
||||
func (d Destination) String() string {
|
||||
return DestinationARNPrefix + d.Bucket
|
||||
}
|
||||
|
||||
// MarshalXML - encodes to XML data.
|
||||
func (d Destination) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if err := e.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.EncodeElement(d.String(), xml.StartElement{Name: xml.Name{Local: "Bucket"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if d.StorageClass != "" {
|
||||
if err := e.EncodeElement(d.StorageClass, xml.StartElement{Name: xml.Name{Local: "StorageClass"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
// UnmarshalXML - decodes XML data.
|
||||
func (d *Destination) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) (err error) {
|
||||
// Make subtype to avoid recursive UnmarshalXML().
|
||||
type destination Destination
|
||||
dest := destination{}
|
||||
|
||||
if err := dec.DecodeElement(&dest, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
parsedDest, err := parseDestination(dest.Bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dest.StorageClass != "" {
|
||||
switch dest.StorageClass {
|
||||
case "STANDARD", "REDUCED_REDUNDANCY":
|
||||
default:
|
||||
return fmt.Errorf("unknown storage class %s", dest.StorageClass)
|
||||
}
|
||||
}
|
||||
parsedDest.StorageClass = dest.StorageClass
|
||||
*d = parsedDest
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate - validates Resource is for given bucket or not.
|
||||
func (d Destination) Validate(bucketName string) error {
|
||||
if !d.IsValid() {
|
||||
return Errorf("invalid destination")
|
||||
}
|
||||
|
||||
if !wildcard.Match(d.Bucket, bucketName) {
|
||||
return Errorf("bucket name does not match")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseDestination - parses string to Destination.
|
||||
func parseDestination(s string) (Destination, error) {
|
||||
if !strings.HasPrefix(s, DestinationARNPrefix) {
|
||||
return Destination{}, Errorf("invalid destination '%s'", s)
|
||||
}
|
||||
|
||||
bucketName := strings.TrimPrefix(s, DestinationARNPrefix)
|
||||
|
||||
return Destination{
|
||||
Bucket: bucketName,
|
||||
}, nil
|
||||
}
|
||||
45
internal/bucket/replication/error.go
Normal file
45
internal/bucket/replication/error.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
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 "replication: cause <nil>"
|
||||
}
|
||||
return e.err.Error()
|
||||
}
|
||||
121
internal/bucket/replication/filter.go
Normal file
121
internal/bucket/replication/filter.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidFilter = Errorf("Filter must have exactly one of Prefix, Tag, or And specified")
|
||||
)
|
||||
|
||||
// Filter - a filter for a replication configuration Rule.
|
||||
type Filter struct {
|
||||
XMLName xml.Name `xml:"Filter" json:"Filter"`
|
||||
Prefix string
|
||||
And And
|
||||
Tag Tag
|
||||
// Caching tags, only once
|
||||
cachedTags map[string]struct{}
|
||||
}
|
||||
|
||||
// IsEmpty returns true if filter is not set
|
||||
func (f Filter) IsEmpty() bool {
|
||||
return f.And.isEmpty() && f.Tag.IsEmpty() && f.Prefix == ""
|
||||
}
|
||||
|
||||
// MarshalXML - produces the xml representation of the Filter struct
|
||||
// only one of Prefix, And and Tag should be present in the output.
|
||||
func (f Filter) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if err := e.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case !f.And.isEmpty():
|
||||
if err := e.EncodeElement(f.And, xml.StartElement{Name: xml.Name{Local: "And"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
case !f.Tag.IsEmpty():
|
||||
if err := e.EncodeElement(f.Tag, xml.StartElement{Name: xml.Name{Local: "Tag"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// Always print Prefix field when both And & Tag are empty
|
||||
if err := e.EncodeElement(f.Prefix, xml.StartElement{Name: xml.Name{Local: "Prefix"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// TestTags tests if the object tags satisfy the Filter tags requirement,
|
||||
// it returns true if there is no tags in the underlying Filter.
|
||||
func (f *Filter) TestTags(ttags []string) bool {
|
||||
if f.cachedTags == nil {
|
||||
tags := make(map[string]struct{})
|
||||
for _, t := range append(f.And.Tags, f.Tag) {
|
||||
if !t.IsEmpty() {
|
||||
tags[t.String()] = struct{}{}
|
||||
}
|
||||
}
|
||||
f.cachedTags = tags
|
||||
}
|
||||
for ct := range f.cachedTags {
|
||||
foundTag := false
|
||||
for _, t := range ttags {
|
||||
if ct == t {
|
||||
foundTag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundTag {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
234
internal/bucket/replication/replication.go
Normal file
234
internal/bucket/replication/replication.go
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StatusType of Replication for x-amz-replication-status header
|
||||
type StatusType string
|
||||
|
||||
const (
|
||||
// Pending - replication is pending.
|
||||
Pending StatusType = "PENDING"
|
||||
|
||||
// Completed - replication completed ok.
|
||||
Completed StatusType = "COMPLETED"
|
||||
|
||||
// Failed - replication failed.
|
||||
Failed StatusType = "FAILED"
|
||||
|
||||
// Replica - this is a replica.
|
||||
Replica StatusType = "REPLICA"
|
||||
)
|
||||
|
||||
// String returns string representation of status
|
||||
func (s StatusType) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// Empty returns true if this status is not set
|
||||
func (s StatusType) Empty() bool {
|
||||
return string(s) == ""
|
||||
}
|
||||
|
||||
var (
|
||||
errReplicationTooManyRules = Errorf("Replication configuration allows a maximum of 1000 rules")
|
||||
errReplicationNoRule = Errorf("Replication configuration should have at least one rule")
|
||||
errReplicationUniquePriority = Errorf("Replication configuration has duplicate priority")
|
||||
errReplicationDestinationMismatch = Errorf("The destination bucket must be same for all rules")
|
||||
errRoleArnMissing = Errorf("Missing required parameter `Role` in ReplicationConfiguration")
|
||||
errInvalidSourceSelectionCriteria = Errorf("Invalid ReplicaModification status")
|
||||
)
|
||||
|
||||
// Config - replication configuration specified in
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/dev/replication-add-config.html
|
||||
type Config struct {
|
||||
XMLName xml.Name `xml:"ReplicationConfiguration" json:"-"`
|
||||
Rules []Rule `xml:"Rule" json:"Rules"`
|
||||
// RoleArn is being reused for MinIO replication ARN
|
||||
RoleArn string `xml:"Role" json:"Role"`
|
||||
}
|
||||
|
||||
// Maximum 2MiB size per replication config.
|
||||
const maxReplicationConfigSize = 2 << 20
|
||||
|
||||
// ParseConfig parses ReplicationConfiguration from xml
|
||||
func ParseConfig(reader io.Reader) (*Config, error) {
|
||||
config := Config{}
|
||||
if err := xml.NewDecoder(io.LimitReader(reader, maxReplicationConfigSize)).Decode(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// By default, set replica modification to enabled if unset.
|
||||
for i := range config.Rules {
|
||||
if len(config.Rules[i].SourceSelectionCriteria.ReplicaModifications.Status) == 0 {
|
||||
config.Rules[i].SourceSelectionCriteria = SourceSelectionCriteria{
|
||||
ReplicaModifications: ReplicaModifications{
|
||||
Status: Enabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Validate - validates the replication configuration
|
||||
func (c Config) Validate(bucket string, sameTarget bool) error {
|
||||
// replication config can't have more than 1000 rules
|
||||
if len(c.Rules) > 1000 {
|
||||
return errReplicationTooManyRules
|
||||
}
|
||||
// replication config should have at least one rule
|
||||
if len(c.Rules) == 0 {
|
||||
return errReplicationNoRule
|
||||
}
|
||||
if c.RoleArn == "" {
|
||||
return errRoleArnMissing
|
||||
}
|
||||
// Validate all the rules in the replication config
|
||||
targetMap := make(map[string]struct{})
|
||||
priorityMap := make(map[string]struct{})
|
||||
for _, r := range c.Rules {
|
||||
if len(targetMap) == 0 {
|
||||
targetMap[r.Destination.Bucket] = struct{}{}
|
||||
}
|
||||
if _, ok := targetMap[r.Destination.Bucket]; !ok {
|
||||
return errReplicationDestinationMismatch
|
||||
}
|
||||
if err := r.Validate(bucket, sameTarget); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := priorityMap[strconv.Itoa(r.Priority)]; ok {
|
||||
return errReplicationUniquePriority
|
||||
}
|
||||
priorityMap[strconv.Itoa(r.Priority)] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type - replication type enum
|
||||
type Type int
|
||||
|
||||
// Types of replication
|
||||
const (
|
||||
ObjectReplicationType Type = 1 + iota
|
||||
DeleteReplicationType
|
||||
MetadataReplicationType
|
||||
HealReplicationType
|
||||
)
|
||||
|
||||
// ObjectOpts provides information to deduce whether replication
|
||||
// can be triggered on the resultant object.
|
||||
type ObjectOpts struct {
|
||||
Name string
|
||||
UserTags string
|
||||
VersionID string
|
||||
IsLatest bool
|
||||
DeleteMarker bool
|
||||
SSEC bool
|
||||
OpType Type
|
||||
Replica bool
|
||||
}
|
||||
|
||||
// FilterActionableRules returns the rules actions that need to be executed
|
||||
// after evaluating prefix/tag filtering
|
||||
func (c Config) FilterActionableRules(obj ObjectOpts) []Rule {
|
||||
if obj.Name == "" {
|
||||
return nil
|
||||
}
|
||||
var rules []Rule
|
||||
for _, rule := range c.Rules {
|
||||
if rule.Status == Disabled {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(obj.Name, rule.Prefix()) {
|
||||
continue
|
||||
}
|
||||
if rule.Filter.TestTags(strings.Split(obj.UserTags, "&")) {
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
}
|
||||
sort.Slice(rules[:], func(i, j int) bool {
|
||||
return rules[i].Priority > rules[j].Priority
|
||||
})
|
||||
return rules
|
||||
}
|
||||
|
||||
// GetDestination returns destination bucket and storage class.
|
||||
func (c Config) GetDestination() Destination {
|
||||
if len(c.Rules) > 0 {
|
||||
return c.Rules[0].Destination
|
||||
}
|
||||
return Destination{}
|
||||
}
|
||||
|
||||
// Replicate returns true if the object should be replicated.
|
||||
func (c Config) Replicate(obj ObjectOpts) bool {
|
||||
if obj.SSEC {
|
||||
return false
|
||||
}
|
||||
for _, rule := range c.FilterActionableRules(obj) {
|
||||
if rule.Status == Disabled {
|
||||
continue
|
||||
}
|
||||
if obj.OpType == DeleteReplicationType {
|
||||
switch {
|
||||
case obj.VersionID != "":
|
||||
// // check MinIO extension for versioned deletes
|
||||
return rule.DeleteReplication.Status == Enabled
|
||||
default:
|
||||
return rule.DeleteMarkerReplication.Status == Enabled
|
||||
}
|
||||
} // regular object/metadata replication
|
||||
return rule.MetadataReplicate(obj)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasActiveRules - returns whether replication policy has active rules
|
||||
// Optionally a prefix can be supplied.
|
||||
// If recursive is specified the function will also return true if any level below the
|
||||
// prefix has active rules. If no prefix is specified recursive is effectively true.
|
||||
func (c Config) HasActiveRules(prefix string, recursive bool) bool {
|
||||
if len(c.Rules) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, rule := range c.Rules {
|
||||
if rule.Status == Disabled {
|
||||
continue
|
||||
}
|
||||
if len(prefix) > 0 && len(rule.Filter.Prefix) > 0 {
|
||||
// incoming prefix must be in rule prefix
|
||||
if !recursive && !strings.HasPrefix(prefix, rule.Filter.Prefix) {
|
||||
continue
|
||||
}
|
||||
// If recursive, we can skip this rule if it doesn't match the tested prefix or level below prefix
|
||||
// does not match
|
||||
if recursive && !strings.HasPrefix(rule.Prefix(), prefix) && !strings.HasPrefix(prefix, rule.Prefix()) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
335
internal/bucket/replication/replication_test.go
Normal file
335
internal/bucket/replication/replication_test.go
Normal file
@@ -0,0 +1,335 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseAndValidateReplicationConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputConfig string
|
||||
expectedParsingErr error
|
||||
expectedValidationErr error
|
||||
destBucket string
|
||||
sameTarget bool
|
||||
}{
|
||||
{ //1 Invalid delete marker status in replication config
|
||||
inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>string</Status></DeleteMarkerReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: errInvalidDeleteMarkerReplicationStatus,
|
||||
},
|
||||
//2 Invalid delete replication status in replication config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: errDeleteReplicationMissing,
|
||||
},
|
||||
//3 valid replication config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: nil,
|
||||
},
|
||||
//4 missing role in config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
// destination bucket in config different from bucket specified
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: errRoleArnMissing,
|
||||
},
|
||||
//5 replication destination in different rules not identical
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule><Rule><Status>Enabled</Status><Priority>3</Priority><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket2</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: errReplicationDestinationMismatch,
|
||||
},
|
||||
//6 missing rule status in replication config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: errEmptyRuleStatus,
|
||||
},
|
||||
//7 invalid rule status in replication config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enssabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: errInvalidRuleStatus,
|
||||
},
|
||||
//8 invalid rule id exceeds length allowed in replication config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><ID>vsUVERgOc8zZYagLSzSa5lE8qeI6nh1lyLNS4R9W052yfecrhhepGboswSWMMNO8CPcXM4GM3nKyQ72EadlMzzZBFoYWKn7ju5GoE5w9c57a0piHR1vexpdd9FrMquiruvAJ0MTGVupm0EegMVxoIOdjx7VgZhGrmi2XDvpVEFT7WmYMA9fSK297XkTHWyECaNHBySJ1Qp4vwX8tPNauKpfHx4kzUpnKe1PZbptGMWbY5qTcwlNuMhVSmgFffShq</ID><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: errInvalidRuleID,
|
||||
},
|
||||
//9 invalid priority status in replication config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: errReplicationUniquePriority,
|
||||
},
|
||||
//10 no rule in replication config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: nil,
|
||||
expectedValidationErr: errReplicationNoRule,
|
||||
},
|
||||
//11 no destination in replication config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: Errorf("invalid destination '%v'", ""),
|
||||
expectedValidationErr: nil,
|
||||
},
|
||||
//12 destination not matching ARN in replication config
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>destinationbucket2</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
destBucket: "destinationbucket",
|
||||
sameTarget: false,
|
||||
expectedParsingErr: fmt.Errorf("invalid destination '%v'", "destinationbucket2"),
|
||||
expectedValidationErr: nil,
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||
cfg, err := ParseConfig(bytes.NewReader([]byte(tc.inputConfig)))
|
||||
if err != nil && tc.expectedParsingErr != nil && err.Error() != tc.expectedParsingErr.Error() {
|
||||
t.Fatalf("%d: Expected '%v' during parsing but got '%v'", i+1, tc.expectedParsingErr, err)
|
||||
}
|
||||
if err == nil && tc.expectedParsingErr != nil {
|
||||
t.Fatalf("%d: Expected '%v' during parsing but got '%v'", i+1, tc.expectedParsingErr, err)
|
||||
}
|
||||
if tc.expectedParsingErr != nil {
|
||||
// We already expect a parsing error,
|
||||
// no need to continue this test.
|
||||
return
|
||||
}
|
||||
err = cfg.Validate(tc.destBucket, tc.sameTarget)
|
||||
if err != tc.expectedValidationErr {
|
||||
t.Fatalf("%d: Expected %v during parsing but got %v", i+1, tc.expectedValidationErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestReplicate(t *testing.T) {
|
||||
cfgs := []Config{
|
||||
{ // Config0 - Replication config has no filters, all replication enabled
|
||||
Rules: []Rule{
|
||||
{
|
||||
Status: Enabled,
|
||||
Priority: 3,
|
||||
DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled},
|
||||
DeleteReplication: DeleteReplication{Status: Enabled},
|
||||
Filter: Filter{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // Config1 - Replication config has no filters, delete,delete-marker replication disabled
|
||||
Rules: []Rule{
|
||||
{
|
||||
Status: Enabled,
|
||||
Priority: 3,
|
||||
DeleteMarkerReplication: DeleteMarkerReplication{Status: Disabled},
|
||||
DeleteReplication: DeleteReplication{Status: Disabled},
|
||||
Filter: Filter{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // Config2 - Replication config has filters and more than 1 matching rule, delete,delete-marker replication disabled
|
||||
Rules: []Rule{
|
||||
{
|
||||
Status: Enabled,
|
||||
Priority: 2,
|
||||
DeleteMarkerReplication: DeleteMarkerReplication{Status: Disabled},
|
||||
DeleteReplication: DeleteReplication{Status: Enabled},
|
||||
Filter: Filter{Prefix: "xy", And: And{}, Tag: Tag{Key: "k1", Value: "v1"}},
|
||||
},
|
||||
{
|
||||
Status: Enabled,
|
||||
Priority: 1,
|
||||
DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled},
|
||||
DeleteReplication: DeleteReplication{Status: Disabled},
|
||||
Filter: Filter{Prefix: "xyz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // Config3 - Replication config has filters and no overlapping rules
|
||||
Rules: []Rule{
|
||||
{
|
||||
Status: Enabled,
|
||||
Priority: 2,
|
||||
DeleteMarkerReplication: DeleteMarkerReplication{Status: Disabled},
|
||||
DeleteReplication: DeleteReplication{Status: Enabled},
|
||||
Filter: Filter{Prefix: "xy", And: And{}, Tag: Tag{Key: "k1", Value: "v1"}},
|
||||
},
|
||||
{
|
||||
Status: Enabled,
|
||||
Priority: 1,
|
||||
DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled},
|
||||
DeleteReplication: DeleteReplication{Status: Disabled},
|
||||
Filter: Filter{Prefix: "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // Config4 - Replication config has filters and SourceSelectionCriteria Disabled
|
||||
Rules: []Rule{
|
||||
{
|
||||
Status: Enabled,
|
||||
Priority: 2,
|
||||
DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled},
|
||||
DeleteReplication: DeleteReplication{Status: Enabled},
|
||||
SourceSelectionCriteria: SourceSelectionCriteria{ReplicaModifications: ReplicaModifications{Status: Disabled}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
testCases := []struct {
|
||||
opts ObjectOpts
|
||||
c Config
|
||||
expectedResult bool
|
||||
}{
|
||||
// using config 1 - no filters, all replication enabled
|
||||
{ObjectOpts{}, cfgs[0], false}, //1. invalid ObjectOpts missing object name
|
||||
{ObjectOpts{Name: "c1test"}, cfgs[0], true}, //2. valid ObjectOpts passing empty Filter
|
||||
{ObjectOpts{Name: "c1test", VersionID: "vid"}, cfgs[0], true}, //3. valid ObjectOpts passing empty Filter
|
||||
|
||||
{ObjectOpts{Name: "c1test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[0], true}, //4. DeleteMarker version replication valid case - matches DeleteMarkerReplication status
|
||||
{ObjectOpts{Name: "c1test", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[0], true}, //5. permanent delete of version, matches DeleteReplication status - valid case
|
||||
{ObjectOpts{Name: "c1test", VersionID: "vid", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[0], true}, //6. permanent delete of version, matches DeleteReplication status
|
||||
{ObjectOpts{Name: "c1test", VersionID: "vid", DeleteMarker: true, SSEC: true, OpType: DeleteReplicationType}, cfgs[0], false}, //7. permanent delete of version, disqualified by SSE-C
|
||||
{ObjectOpts{Name: "c1test", DeleteMarker: true, SSEC: true, OpType: DeleteReplicationType}, cfgs[0], false}, //8. setting DeleteMarker on SSE-C encrypted object, disqualified by SSE-C
|
||||
{ObjectOpts{Name: "c1test", SSEC: true}, cfgs[0], false}, //9. replication of SSE-C encrypted object, disqualified
|
||||
|
||||
// using config 2 - no filters, only replication of object, metadata enabled
|
||||
{ObjectOpts{Name: "c2test"}, cfgs[1], true}, //10. valid ObjectOpts passing empty Filter
|
||||
{ObjectOpts{Name: "c2test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[1], false}, //11. DeleteMarker version replication not allowed due to DeleteMarkerReplication status
|
||||
{ObjectOpts{Name: "c2test", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[1], false}, //12. permanent delete of version, disallowed by DeleteReplication status
|
||||
{ObjectOpts{Name: "c2test", VersionID: "vid", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[1], false}, //13. permanent delete of DeleteMarker version, disallowed by DeleteReplication status
|
||||
{ObjectOpts{Name: "c2test", VersionID: "vid", DeleteMarker: true, SSEC: true, OpType: DeleteReplicationType}, cfgs[1], false}, //14. permanent delete of version, disqualified by SSE-C & DeleteReplication status
|
||||
{ObjectOpts{Name: "c2test", DeleteMarker: true, SSEC: true, OpType: DeleteReplicationType}, cfgs[1], false}, //15. setting DeleteMarker on SSE-C encrypted object, disqualified by SSE-C & DeleteMarkerReplication status
|
||||
{ObjectOpts{Name: "c2test", SSEC: true}, cfgs[1], false}, //16. replication of SSE-C encrypted object, disqualified by default
|
||||
// using config 2 - has more than one rule with overlapping prefixes
|
||||
{ObjectOpts{Name: "xy/c3test", UserTags: "k1=v1"}, cfgs[2], true}, //17. matches rule 1 for replication of content/metadata
|
||||
{ObjectOpts{Name: "xyz/c3test", UserTags: "k1=v1"}, cfgs[2], true}, //18. matches rule 1 for replication of content/metadata
|
||||
{ObjectOpts{Name: "xyz/c3test", UserTags: "k1=v1", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[2], false}, //19. matches rule 1 - DeleteMarker replication disallowed by rule
|
||||
{ObjectOpts{Name: "xyz/c3test", UserTags: "k1=v1", DeleteMarker: true, VersionID: "vid", OpType: DeleteReplicationType}, cfgs[2], true}, //20. matches rule 1 - DeleteReplication allowed by rule for permanent delete of DeleteMarker
|
||||
{ObjectOpts{Name: "xyz/c3test", UserTags: "k1=v1", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[2], true}, //21. matches rule 1 - DeleteReplication allowed by rule for permanent delete of version
|
||||
{ObjectOpts{Name: "xyz/c3test"}, cfgs[2], true}, //22. matches rule 2 for replication of content/metadata
|
||||
{ObjectOpts{Name: "xy/c3test", UserTags: "k1=v2"}, cfgs[2], false}, //23. does not match rule1 because tag value does not pass filter
|
||||
{ObjectOpts{Name: "xyz/c3test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[2], true}, //24. matches rule 2 - DeleteMarker replication allowed by rule
|
||||
{ObjectOpts{Name: "xyz/c3test", DeleteMarker: true, VersionID: "vid", OpType: DeleteReplicationType}, cfgs[2], false}, //25. matches rule 2 - DeleteReplication disallowed by rule for permanent delete of DeleteMarker
|
||||
{ObjectOpts{Name: "xyz/c3test", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[2], false}, //26. matches rule 1 - DeleteReplication disallowed by rule for permanent delete of version
|
||||
{ObjectOpts{Name: "abc/c3test"}, cfgs[2], false}, //27. matches no rule because object prefix does not match
|
||||
|
||||
// using config 3 - has no overlapping rules
|
||||
{ObjectOpts{Name: "xy/c4test", UserTags: "k1=v1"}, cfgs[3], true}, //28. matches rule 1 for replication of content/metadata
|
||||
{ObjectOpts{Name: "xa/c4test", UserTags: "k1=v1"}, cfgs[3], false}, //29. no rule match object prefix not in rules
|
||||
{ObjectOpts{Name: "xyz/c4test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[3], false}, //30. rule 1 not matched because of tags filter
|
||||
{ObjectOpts{Name: "xyz/c4test", UserTags: "k1=v1", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[3], false}, //31. matches rule 1 - DeleteMarker replication disallowed by rule
|
||||
{ObjectOpts{Name: "xyz/c4test", UserTags: "k1=v1", DeleteMarker: true, VersionID: "vid", OpType: DeleteReplicationType}, cfgs[3], true}, //32. matches rule 1 - DeleteReplication allowed by rule for permanent delete of DeleteMarker
|
||||
{ObjectOpts{Name: "xyz/c4test", UserTags: "k1=v1", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[3], true}, //33. matches rule 1 - DeleteReplication allowed by rule for permanent delete of version
|
||||
{ObjectOpts{Name: "abc/c4test"}, cfgs[3], true}, //34. matches rule 2 for replication of content/metadata
|
||||
{ObjectOpts{Name: "abc/c4test", UserTags: "k1=v2"}, cfgs[3], true}, //35. matches rule 2 for replication of content/metadata
|
||||
{ObjectOpts{Name: "abc/c4test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[3], true}, //36. matches rule 2 - DeleteMarker replication allowed by rule
|
||||
{ObjectOpts{Name: "abc/c4test", DeleteMarker: true, VersionID: "vid", OpType: DeleteReplicationType}, cfgs[3], false}, //37. matches rule 2 - DeleteReplication disallowed by rule for permanent delete of DeleteMarker
|
||||
{ObjectOpts{Name: "abc/c4test", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[3], false}, //38. matches rule 2 - DeleteReplication disallowed by rule for permanent delete of version
|
||||
// using config 4 - with replica modification sync disabled.
|
||||
{ObjectOpts{Name: "xy/c5test", UserTags: "k1=v1", Replica: true}, cfgs[4], false}, //39. replica syncing disabled, this object is a replica
|
||||
{ObjectOpts{Name: "xa/c5test", UserTags: "k1=v1", Replica: false}, cfgs[4], true}, //40. replica syncing disabled, this object is NOT a replica
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.c.Replicate(testCase.opts)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasActiveRules(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputConfig string
|
||||
prefix string
|
||||
expectedNonRec bool
|
||||
expectedRec bool
|
||||
}{
|
||||
|
||||
// case 1 - only one rule which is in Disabled status
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Disabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
prefix: "miss/prefix",
|
||||
expectedNonRec: false,
|
||||
expectedRec: false,
|
||||
},
|
||||
// case 2 - only one rule which matches prefix filter
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Filter><Prefix>key/prefix</Prefix></Filter><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
prefix: "key/prefix1",
|
||||
expectedNonRec: true,
|
||||
expectedRec: true,
|
||||
},
|
||||
// case 3 - empty prefix
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
prefix: "key-prefix",
|
||||
expectedNonRec: true,
|
||||
expectedRec: true,
|
||||
},
|
||||
// case 4 - has Filter based on prefix
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Filter><Prefix>testdir/dir1/</Prefix></Filter><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
prefix: "testdir/",
|
||||
expectedNonRec: false,
|
||||
expectedRec: true,
|
||||
},
|
||||
//case 5 - has filter with prefix and tags, here we are not matching on tags
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Filter>
|
||||
<And><Prefix>key-prefix</Prefix><Tag><Key>key1</Key><Value>value1</Value></Tag><Tag><Key>key2</Key><Value>value2</Value></Tag></And></Filter><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
|
||||
prefix: "testdir/",
|
||||
expectedNonRec: true,
|
||||
expectedRec: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("Test_%d", i+1), func(t *testing.T) {
|
||||
cfg, err := ParseConfig(bytes.NewReader([]byte(tc.inputConfig)))
|
||||
if err != nil {
|
||||
t.Fatalf("Got unexpected error: %v", err)
|
||||
}
|
||||
if got := cfg.HasActiveRules(tc.prefix, false); got != tc.expectedNonRec {
|
||||
t.Fatalf("Expected result with recursive set to false: `%v`, got: `%v`", tc.expectedNonRec, got)
|
||||
}
|
||||
if got := cfg.HasActiveRules(tc.prefix, true); got != tc.expectedRec {
|
||||
t.Fatalf("Expected result with recursive set to true: `%v`, got: `%v`", tc.expectedRec, got)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
213
internal/bucket/replication/rule.go
Normal file
213
internal/bucket/replication/rule.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Status represents Enabled/Disabled status
|
||||
type Status string
|
||||
|
||||
// Supported status types
|
||||
const (
|
||||
Enabled Status = "Enabled"
|
||||
Disabled Status = "Disabled"
|
||||
)
|
||||
|
||||
// DeleteMarkerReplication - whether delete markers are replicated - https://docs.aws.amazon.com/AmazonS3/latest/dev/replication-add-config.html
|
||||
type DeleteMarkerReplication struct {
|
||||
Status Status `xml:"Status"` // should be set to "Disabled" by default
|
||||
}
|
||||
|
||||
// IsEmpty returns true if DeleteMarkerReplication is not set
|
||||
func (d DeleteMarkerReplication) IsEmpty() bool {
|
||||
return len(d.Status) == 0
|
||||
}
|
||||
|
||||
// Validate validates whether the status is disabled.
|
||||
func (d DeleteMarkerReplication) Validate() error {
|
||||
if d.IsEmpty() {
|
||||
return errDeleteMarkerReplicationMissing
|
||||
}
|
||||
if d.Status != Disabled && d.Status != Enabled {
|
||||
return errInvalidDeleteMarkerReplicationStatus
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteReplication - whether versioned deletes are replicated - this is a MinIO only
|
||||
// extension.
|
||||
type DeleteReplication struct {
|
||||
Status Status `xml:"Status"` // should be set to "Disabled" by default
|
||||
}
|
||||
|
||||
// IsEmpty returns true if DeleteReplication is not set
|
||||
func (d DeleteReplication) IsEmpty() bool {
|
||||
return len(d.Status) == 0
|
||||
}
|
||||
|
||||
// Validate validates whether the status is disabled.
|
||||
func (d DeleteReplication) Validate() error {
|
||||
if d.IsEmpty() {
|
||||
return errDeleteReplicationMissing
|
||||
}
|
||||
if d.Status != Disabled && d.Status != Enabled {
|
||||
return errInvalidDeleteReplicationStatus
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML - decodes XML data.
|
||||
func (d *DeleteReplication) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) (err error) {
|
||||
// Make subtype to avoid recursive UnmarshalXML().
|
||||
type deleteReplication DeleteReplication
|
||||
drep := deleteReplication{}
|
||||
|
||||
if err := dec.DecodeElement(&drep, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(drep.Status) == 0 {
|
||||
drep.Status = Disabled
|
||||
}
|
||||
d.Status = drep.Status
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rule - a rule for replication configuration.
|
||||
type Rule struct {
|
||||
XMLName xml.Name `xml:"Rule" json:"Rule"`
|
||||
ID string `xml:"ID,omitempty" json:"ID,omitempty"`
|
||||
Status Status `xml:"Status" json:"Status"`
|
||||
Priority int `xml:"Priority" json:"Priority"`
|
||||
DeleteMarkerReplication DeleteMarkerReplication `xml:"DeleteMarkerReplication" json:"DeleteMarkerReplication"`
|
||||
// MinIO extension to replicate versioned deletes
|
||||
DeleteReplication DeleteReplication `xml:"DeleteReplication" json:"DeleteReplication"`
|
||||
Destination Destination `xml:"Destination" json:"Destination"`
|
||||
SourceSelectionCriteria SourceSelectionCriteria `xml:"SourceSelectionCriteria" json:"SourceSelectionCriteria"`
|
||||
Filter Filter `xml:"Filter" json:"Filter"`
|
||||
}
|
||||
|
||||
var (
|
||||
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")
|
||||
errDeleteMarkerReplicationMissing = Errorf("DeleteMarkerReplication must be specified")
|
||||
errPriorityMissing = Errorf("Priority must be specified")
|
||||
errInvalidDeleteMarkerReplicationStatus = Errorf("Delete marker replication status is invalid")
|
||||
errDestinationSourceIdentical = Errorf("Destination bucket cannot be the same as the source bucket.")
|
||||
errDeleteReplicationMissing = Errorf("Delete replication must be specified")
|
||||
errInvalidDeleteReplicationStatus = Errorf("Delete replication is either enable|disable")
|
||||
)
|
||||
|
||||
// validateID - checks if ID is valid or not.
|
||||
func (r Rule) validateID() error {
|
||||
// cannot be longer than 255 characters
|
||||
if len(r.ID) > 255 {
|
||||
return errInvalidRuleID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateStatus - checks if status is valid or not.
|
||||
func (r Rule) validateStatus() error {
|
||||
// Status can't be empty
|
||||
if len(r.Status) == 0 {
|
||||
return errEmptyRuleStatus
|
||||
}
|
||||
|
||||
// Status must be one of Enabled or Disabled
|
||||
if r.Status != Enabled && r.Status != Disabled {
|
||||
return errInvalidRuleStatus
|
||||
}
|
||||
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
|
||||
}
|
||||
return r.Filter.And.Prefix
|
||||
}
|
||||
|
||||
// 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(bucket string, sameTarget bool) error {
|
||||
if err := r.validateID(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.validateStatus(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.validateFilter(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.DeleteMarkerReplication.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.DeleteReplication.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.SourceSelectionCriteria.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Priority < 0 {
|
||||
return errPriorityMissing
|
||||
}
|
||||
if r.Destination.Bucket == bucket && sameTarget {
|
||||
return errDestinationSourceIdentical
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MetadataReplicate returns true if object is not a replica or in the case of replicas,
|
||||
// replica modification sync is enabled.
|
||||
func (r Rule) MetadataReplicate(obj ObjectOpts) bool {
|
||||
if !obj.Replica {
|
||||
return true
|
||||
}
|
||||
return obj.Replica && r.SourceSelectionCriteria.ReplicaModifications.Status == Enabled
|
||||
}
|
||||
68
internal/bucket/replication/rule_test.go
Normal file
68
internal/bucket/replication/rule_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMetadataReplicate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputConfig string
|
||||
opts ObjectOpts
|
||||
expectedResult bool
|
||||
}{
|
||||
// case 1 - rule with replica modification enabled; not a replica
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination><SourceSelectionCriteria><ReplicaModifications><Status>Enabled</Status></ReplicaModifications></SourceSelectionCriteria></Rule></ReplicationConfiguration>`,
|
||||
opts: ObjectOpts{Name: "c1test", DeleteMarker: false, OpType: ObjectReplicationType, Replica: false}, //1. Replica mod sync enabled; not a replica
|
||||
expectedResult: true,
|
||||
},
|
||||
// case 2 - rule with replica modification disabled; a replica
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination><SourceSelectionCriteria><ReplicaModifications><Status>Disabled</Status></ReplicaModifications></SourceSelectionCriteria></Rule></ReplicationConfiguration>`,
|
||||
opts: ObjectOpts{Name: "c2test", DeleteMarker: false, OpType: ObjectReplicationType, Replica: true}, //1. Replica mod sync enabled; a replica
|
||||
expectedResult: false,
|
||||
},
|
||||
// case 3 - rule with replica modification disabled; not a replica
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination><SourceSelectionCriteria><ReplicaModifications><Status>Disabled</Status></ReplicaModifications></SourceSelectionCriteria></Rule></ReplicationConfiguration>`,
|
||||
opts: ObjectOpts{Name: "c2test", DeleteMarker: false, OpType: ObjectReplicationType, Replica: false}, //1. Replica mod sync disabled; not a replica
|
||||
expectedResult: true,
|
||||
},
|
||||
|
||||
// case 4 - rule with replica modification enabled; a replica
|
||||
{inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination><SourceSelectionCriteria><ReplicaModifications><Status>Enabled</Status></ReplicaModifications></SourceSelectionCriteria></Rule></ReplicationConfiguration>`,
|
||||
opts: ObjectOpts{Name: "c2test", DeleteMarker: false, OpType: MetadataReplicationType, Replica: true}, //1. Replica mod sync enabled; a replica
|
||||
expectedResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("Test_%d", i+1), func(t *testing.T) {
|
||||
cfg, err := ParseConfig(bytes.NewReader([]byte(tc.inputConfig)))
|
||||
if err != nil {
|
||||
t.Fatalf("Got unexpected error: %v", err)
|
||||
}
|
||||
if got := cfg.Rules[0].MetadataReplicate(tc.opts); got != tc.expectedResult {
|
||||
t.Fatalf("Expected result with recursive set to false: `%v`, got: `%v`", tc.expectedResult, got)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
76
internal/bucket/replication/sourceselectioncriteria.go
Normal file
76
internal/bucket/replication/sourceselectioncriteria.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ReplicaModifications specifies if replica modification sync is enabled
|
||||
type ReplicaModifications struct {
|
||||
Status Status `xml:"Status" json:"Status"`
|
||||
}
|
||||
|
||||
// SourceSelectionCriteria - specifies additional source selection criteria in ReplicationConfiguration.
|
||||
type SourceSelectionCriteria struct {
|
||||
ReplicaModifications ReplicaModifications `xml:"ReplicaModifications" json:"ReplicaModifications"`
|
||||
}
|
||||
|
||||
// IsValid - checks whether SourceSelectionCriteria is valid or not.
|
||||
func (s SourceSelectionCriteria) IsValid() bool {
|
||||
return s.ReplicaModifications.Status == Enabled || s.ReplicaModifications.Status == Disabled
|
||||
}
|
||||
|
||||
// Validate source selection criteria
|
||||
func (s SourceSelectionCriteria) Validate() error {
|
||||
if (s == SourceSelectionCriteria{}) {
|
||||
return nil
|
||||
}
|
||||
if !s.IsValid() {
|
||||
return errInvalidSourceSelectionCriteria
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML - decodes XML data.
|
||||
func (s *SourceSelectionCriteria) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) (err error) {
|
||||
// Make subtype to avoid recursive UnmarshalXML().
|
||||
type sourceSelectionCriteria SourceSelectionCriteria
|
||||
ssc := sourceSelectionCriteria{}
|
||||
if err := dec.DecodeElement(&ssc, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ssc.ReplicaModifications.Status) == 0 {
|
||||
ssc.ReplicaModifications.Status = Enabled
|
||||
}
|
||||
*s = SourceSelectionCriteria(ssc)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalXML - encodes to XML data.
|
||||
func (s SourceSelectionCriteria) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if err := e.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.IsValid() {
|
||||
if err := e.EncodeElement(s.ReplicaModifications, xml.StartElement{Name: xml.Name{Local: "ReplicaModifications"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
57
internal/bucket/replication/tag.go
Normal file
57
internal/bucket/replication/tag.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Tag - a tag for a replication configuration Rule filter.
|
||||
type Tag struct {
|
||||
XMLName xml.Name `xml:"Tag" json:"Tag"`
|
||||
Key string `xml:"Key,omitempty" json:"Key,omitempty"`
|
||||
Value string `xml:"Value,omitempty" json:"Value,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
errInvalidTagKey = Errorf("The TagKey you have provided is invalid")
|
||||
errInvalidTagValue = Errorf("The TagValue you have provided is invalid")
|
||||
)
|
||||
|
||||
func (tag Tag) String() string {
|
||||
return tag.Key + "=" + tag.Value
|
||||
}
|
||||
|
||||
// IsEmpty returns whether this tag is empty or not.
|
||||
func (tag Tag) IsEmpty() bool {
|
||||
return tag.Key == ""
|
||||
}
|
||||
|
||||
// Validate checks this tag.
|
||||
func (tag Tag) Validate() error {
|
||||
if len(tag.Key) == 0 || utf8.RuneCountInString(tag.Key) > 128 {
|
||||
return errInvalidTagKey
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(tag.Value) > 256 {
|
||||
return errInvalidTagValue
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user