Add support for server side bucket replication (#9882)

This commit is contained in:
poornas
2020-07-21 17:49:56 -07:00
committed by GitHub
parent ca4c15bc63
commit c43da3005a
44 changed files with 2387 additions and 322 deletions

View File

@@ -149,28 +149,49 @@ const (
// PutObjectVersionTaggingAction - PutObjectVersionTagging Rest API action.
PutObjectVersionTaggingAction = "s3:PutObjectVersionTagging"
// GetReplicationConfigurationAction - GetReplicationConfiguration REST API action
GetReplicationConfigurationAction = "s3:GetReplicationConfiguration"
// PutReplicationConfigurationAction - PutReplicationConfiguration REST API action
PutReplicationConfigurationAction = "s3:PutReplicationConfiguration"
// ReplicateObjectAction - ReplicateObject REST API action
ReplicateObjectAction = "s3:ReplicateObject"
// ReplicateDeleteAction - ReplicateDelete REST API action
ReplicateDeleteAction = "s3:ReplicateDelete"
// ReplicateTagsAction - ReplicateTags REST API action
ReplicateTagsAction = "s3:ReplicateTags"
// GetObjectVersionForReplicationAction - GetObjectVersionForReplication REST API action
GetObjectVersionForReplicationAction = "s3:GetObjectVersionForReplication"
)
// List of all supported object actions.
var supportedObjectActions = map[Action]struct{}{
AbortMultipartUploadAction: {},
DeleteObjectAction: {},
GetObjectAction: {},
ListMultipartUploadPartsAction: {},
PutObjectAction: {},
BypassGovernanceRetentionAction: {},
PutObjectRetentionAction: {},
GetObjectRetentionAction: {},
PutObjectLegalHoldAction: {},
GetObjectLegalHoldAction: {},
GetObjectTaggingAction: {},
PutObjectTaggingAction: {},
DeleteObjectTaggingAction: {},
GetObjectVersionAction: {},
GetObjectVersionTaggingAction: {},
DeleteObjectVersionAction: {},
DeleteObjectVersionTaggingAction: {},
PutObjectVersionTaggingAction: {},
AbortMultipartUploadAction: {},
DeleteObjectAction: {},
GetObjectAction: {},
ListMultipartUploadPartsAction: {},
PutObjectAction: {},
BypassGovernanceRetentionAction: {},
PutObjectRetentionAction: {},
GetObjectRetentionAction: {},
PutObjectLegalHoldAction: {},
GetObjectLegalHoldAction: {},
GetObjectTaggingAction: {},
PutObjectTaggingAction: {},
DeleteObjectTaggingAction: {},
GetObjectVersionAction: {},
GetObjectVersionTaggingAction: {},
DeleteObjectVersionAction: {},
DeleteObjectVersionTaggingAction: {},
PutObjectVersionTaggingAction: {},
ReplicateObjectAction: {},
ReplicateDeleteAction: {},
ReplicateTagsAction: {},
GetObjectVersionForReplicationAction: {},
}
// isObjectAction - returns whether action is object type or not.
@@ -224,6 +245,12 @@ var supportedActions = map[Action]struct{}{
GetBucketEncryptionAction: {},
PutBucketVersioningAction: {},
GetBucketVersioningAction: {},
GetReplicationConfigurationAction: {},
PutReplicationConfigurationAction: {},
ReplicateObjectAction: {},
ReplicateDeleteAction: {},
ReplicateTagsAction: {},
GetObjectVersionForReplicationAction: {},
}
// IsValid - checks if action is valid or not.
@@ -366,4 +393,10 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
append([]condition.Key{
condition.S3VersionID,
}, condition.CommonKeys...)...),
GetReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
ReplicateObjectAction: condition.NewKeySet(condition.CommonKeys...),
ReplicateDeleteAction: condition.NewKeySet(condition.CommonKeys...),
ReplicateTagsAction: condition.NewKeySet(condition.CommonKeys...),
GetObjectVersionForReplicationAction: condition.NewKeySet(condition.CommonKeys...),
}

View File

@@ -0,0 +1,62 @@
/*
* 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 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
}

View File

@@ -0,0 +1,118 @@
/*
* 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 replication
import (
"encoding/xml"
"fmt"
"strings"
"github.com/minio/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 %v", 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 '%v'", s)
}
bucketName := strings.TrimPrefix(s, DestinationARNPrefix)
return Destination{
Bucket: bucketName,
}, nil
}

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

View File

@@ -0,0 +1,120 @@
/*
* 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 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
}

View File

@@ -0,0 +1,206 @@
/*
* 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 replication
import (
"encoding/xml"
"io"
"sort"
"strings"
)
// StatusType of Replication for x-amz-replication-status header
type StatusType string
const (
// Pending - replication is pending.
Pending StatusType = "PENDING"
// Complete - replication completed ok.
Complete StatusType = "COMPLETE"
// 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)
}
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")
errReplicationArnMissing = Errorf("Replication Arn missing")
)
// 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"`
// ReplicationArn is a MinIO only extension and optional for AWS
ReplicationArn string `xml:"ReplicationArn,omitempty" json:"ReplicationArn,omitempty"`
}
// 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
}
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.ReplicationArn == "" {
return errReplicationArnMissing
}
// 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[string(r.Priority)]; ok {
return errReplicationUniquePriority
}
priorityMap[string(r.Priority)] = struct{}{}
}
return nil
}
// 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
}
// 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 {
for _, rule := range c.Rules {
if rule.Status == Disabled {
continue
}
return rule.Destination
}
return Destination{}
}
// Replicate returns true if the object should be replicated.
func (c Config) Replicate(obj ObjectOpts) bool {
for _, rule := range c.FilterActionableRules(obj) {
if obj.DeleteMarker {
// Indicates whether MinIO will remove a delete marker. By default, delete markers
// are not replicated.
return false
}
if obj.SSEC {
return false
}
if obj.VersionID != "" && !obj.IsLatest {
return false
}
if rule.Status == Disabled {
continue
}
return true
}
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.
if recursive && !strings.HasPrefix(rule.Filter.Prefix, prefix) {
continue
}
}
return true
}
return false
}

View File

@@ -0,0 +1,156 @@
/*
* 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 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 {
return errInvalidDeleteMarkerReplicationStatus
}
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"`
Destination Destination `xml:"Destination" json:"Destination"`
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 is currently not supported")
errDestinationSourceIdentical = Errorf("Destination bucket cannot be the same as the source bucket.")
)
// 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 {
if err := r.Filter.Validate(); err != nil {
return err
}
return nil
}
// 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 r.Priority <= 0 {
return errPriorityMissing
}
if r.Destination.Bucket == bucket && sameTarget {
return errDestinationSourceIdentical
}
return nil
}

View File

@@ -0,0 +1,56 @@
/*
* 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 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
}