2023-06-27 12:45:50 -04:00
|
|
|
// Copyright (c) 2015-2023 MinIO, Inc.
|
2021-09-18 16:31:35 -04:00
|
|
|
//
|
|
|
|
// 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 cmd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
2022-03-08 16:58:55 -05:00
|
|
|
"net/http"
|
2022-07-21 14:05:44 -04:00
|
|
|
"net/url"
|
2021-09-18 16:31:35 -04:00
|
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2022-02-10 13:16:52 -05:00
|
|
|
"sync"
|
2021-09-18 16:31:35 -04:00
|
|
|
"time"
|
|
|
|
|
2023-06-19 20:53:08 -04:00
|
|
|
"github.com/minio/madmin-go/v3"
|
2021-09-18 16:31:35 -04:00
|
|
|
"github.com/minio/minio/internal/bucket/replication"
|
2022-03-08 16:58:55 -05:00
|
|
|
xhttp "github.com/minio/minio/internal/http"
|
2021-09-18 16:31:35 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
//go:generate msgp -file=$GOFILE
|
|
|
|
|
|
|
|
// replicatedTargetInfo struct represents replication info on a target
|
|
|
|
type replicatedTargetInfo struct {
|
|
|
|
Arn string
|
|
|
|
Size int64
|
2021-11-17 15:10:57 -05:00
|
|
|
Duration time.Duration
|
2021-09-18 16:31:35 -04:00
|
|
|
ReplicationAction replicationAction // full or metadata only
|
|
|
|
OpType replication.Type // whether incoming replication, existing object, healing etc..
|
|
|
|
ReplicationStatus replication.StatusType
|
|
|
|
PrevReplicationStatus replication.StatusType
|
|
|
|
VersionPurgeStatus VersionPurgeStatusType
|
|
|
|
ResyncTimestamp string
|
|
|
|
ReplicationResynced bool // true only if resync attempted for this target
|
|
|
|
}
|
|
|
|
|
|
|
|
// Empty returns true for a target if arn is empty
|
|
|
|
func (rt replicatedTargetInfo) Empty() bool {
|
|
|
|
return rt.Arn == ""
|
|
|
|
}
|
|
|
|
|
|
|
|
type replicatedInfos struct {
|
|
|
|
ReplicationTimeStamp time.Time
|
|
|
|
Targets []replicatedTargetInfo
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ri replicatedInfos) CompletedSize() (sz int64) {
|
|
|
|
for _, t := range ri.Targets {
|
|
|
|
if t.Empty() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if t.ReplicationStatus == replication.Completed && t.PrevReplicationStatus != replication.Completed {
|
|
|
|
sz += t.Size
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return sz
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReplicationAttempted returns true if replication was attempted on any of the targets for the object version
|
|
|
|
// queued
|
|
|
|
func (ri replicatedInfos) ReplicationResynced() bool {
|
|
|
|
for _, t := range ri.Targets {
|
|
|
|
if t.Empty() || !t.ReplicationResynced {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ri replicatedInfos) ReplicationStatusInternal() string {
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
for _, t := range ri.Targets {
|
|
|
|
if t.Empty() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
fmt.Fprintf(b, "%s=%s;", t.Arn, t.ReplicationStatus.String())
|
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ri replicatedInfos) ReplicationStatus() replication.StatusType {
|
|
|
|
if len(ri.Targets) == 0 {
|
|
|
|
return replication.StatusType("")
|
|
|
|
}
|
|
|
|
completed := 0
|
|
|
|
for _, v := range ri.Targets {
|
|
|
|
switch v.ReplicationStatus {
|
|
|
|
case replication.Failed:
|
|
|
|
return replication.Failed
|
|
|
|
case replication.Completed:
|
|
|
|
completed++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if completed == len(ri.Targets) {
|
|
|
|
return replication.Completed
|
|
|
|
}
|
|
|
|
return replication.Pending
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ri replicatedInfos) VersionPurgeStatus() VersionPurgeStatusType {
|
|
|
|
if len(ri.Targets) == 0 {
|
|
|
|
return VersionPurgeStatusType("")
|
|
|
|
}
|
|
|
|
completed := 0
|
|
|
|
for _, v := range ri.Targets {
|
|
|
|
switch v.VersionPurgeStatus {
|
|
|
|
case Failed:
|
|
|
|
return Failed
|
|
|
|
case Complete:
|
|
|
|
completed++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if completed == len(ri.Targets) {
|
|
|
|
return Complete
|
|
|
|
}
|
|
|
|
return Pending
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ri replicatedInfos) VersionPurgeStatusInternal() string {
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
for _, t := range ri.Targets {
|
|
|
|
if t.Empty() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if t.VersionPurgeStatus.Empty() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
fmt.Fprintf(b, "%s=%s;", t.Arn, t.VersionPurgeStatus)
|
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ri replicatedInfos) Action() replicationAction {
|
|
|
|
for _, t := range ri.Targets {
|
|
|
|
if t.Empty() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// rely on replication action from target that actually performed replication now.
|
|
|
|
if t.PrevReplicationStatus != replication.Completed {
|
|
|
|
return t.ReplicationAction
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return replicateNone
|
|
|
|
}
|
|
|
|
|
|
|
|
var replStatusRegex = regexp.MustCompile(`([^=].*?)=([^,].*?);`)
|
|
|
|
|
|
|
|
// TargetReplicationStatus - returns replication status of a target
|
|
|
|
func (o *ObjectInfo) TargetReplicationStatus(arn string) (status replication.StatusType) {
|
|
|
|
repStatMatches := replStatusRegex.FindAllStringSubmatch(o.ReplicationStatusInternal, -1)
|
|
|
|
for _, repStatMatch := range repStatMatches {
|
|
|
|
if len(repStatMatch) != 3 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if repStatMatch[1] == arn {
|
|
|
|
return replication.StatusType(repStatMatch[2])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
type replicateTargetDecision struct {
|
|
|
|
Replicate bool // Replicate to this target
|
|
|
|
Synchronous bool // Synchronous replication configured.
|
2021-11-16 12:28:29 -05:00
|
|
|
Arn string // ARN of replication target
|
2021-09-18 16:31:35 -04:00
|
|
|
ID string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *replicateTargetDecision) String() string {
|
|
|
|
return fmt.Sprintf("%t;%t;%s;%s", t.Replicate, t.Synchronous, t.Arn, t.ID)
|
|
|
|
}
|
|
|
|
|
|
|
|
func newReplicateTargetDecision(arn string, replicate bool, sync bool) replicateTargetDecision {
|
|
|
|
d := replicateTargetDecision{
|
|
|
|
Replicate: replicate,
|
|
|
|
Synchronous: sync,
|
|
|
|
Arn: arn,
|
|
|
|
}
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReplicateDecision represents replication decision for each target
|
|
|
|
type ReplicateDecision struct {
|
|
|
|
targetsMap map[string]replicateTargetDecision
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReplicateAny returns true if atleast one target qualifies for replication
|
|
|
|
func (d *ReplicateDecision) ReplicateAny() bool {
|
|
|
|
for _, t := range d.targetsMap {
|
|
|
|
if t.Replicate {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Synchronous returns true if atleast one target qualifies for synchronous replication
|
|
|
|
func (d *ReplicateDecision) Synchronous() bool {
|
|
|
|
for _, t := range d.targetsMap {
|
|
|
|
if t.Synchronous {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *ReplicateDecision) String() string {
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
for key, value := range d.targetsMap {
|
|
|
|
fmt.Fprintf(b, "%s=%s,", key, value.String())
|
|
|
|
}
|
|
|
|
return strings.TrimSuffix(b.String(), ",")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set updates ReplicateDecision with target's replication decision
|
|
|
|
func (d *ReplicateDecision) Set(t replicateTargetDecision) {
|
|
|
|
if d.targetsMap == nil {
|
|
|
|
d.targetsMap = make(map[string]replicateTargetDecision)
|
|
|
|
}
|
|
|
|
d.targetsMap[t.Arn] = t
|
|
|
|
}
|
|
|
|
|
|
|
|
// PendingStatus returns a stringified representation of internal replication status with all targets marked as `PENDING`
|
|
|
|
func (d *ReplicateDecision) PendingStatus() string {
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
for _, k := range d.targetsMap {
|
|
|
|
if k.Replicate {
|
|
|
|
fmt.Fprintf(b, "%s=%s;", k.Arn, replication.Pending.String())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResyncDecision is a struct representing a map with target's individual resync decisions
|
|
|
|
type ResyncDecision struct {
|
|
|
|
targets map[string]ResyncTargetDecision
|
|
|
|
}
|
|
|
|
|
|
|
|
// Empty returns true if no targets with resync decision present
|
|
|
|
func (r *ResyncDecision) Empty() bool {
|
|
|
|
return r.targets == nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *ResyncDecision) mustResync() bool {
|
|
|
|
for _, v := range r.targets {
|
|
|
|
if v.Replicate {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *ResyncDecision) mustResyncTarget(tgtArn string) bool {
|
|
|
|
if r.targets == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
v, ok := r.targets[tgtArn]
|
|
|
|
if ok && v.Replicate {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResyncTargetDecision is struct that represents resync decision for this target
|
|
|
|
type ResyncTargetDecision struct {
|
|
|
|
Replicate bool
|
|
|
|
ResetID string
|
|
|
|
ResetBeforeDate time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
var errInvalidReplicateDecisionFormat = fmt.Errorf("ReplicateDecision has invalid format")
|
|
|
|
|
|
|
|
// parse k-v pairs of target ARN to stringified ReplicateTargetDecision delimited by ',' into a
|
|
|
|
// ReplicateDecision struct
|
|
|
|
func parseReplicateDecision(s string) (r ReplicateDecision, err error) {
|
|
|
|
r = ReplicateDecision{
|
|
|
|
targetsMap: make(map[string]replicateTargetDecision),
|
|
|
|
}
|
|
|
|
if len(s) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pairs := strings.Split(s, ",")
|
|
|
|
for _, p := range pairs {
|
|
|
|
slc := strings.Split(p, "=")
|
|
|
|
if len(slc) != 2 {
|
|
|
|
return r, errInvalidReplicateDecisionFormat
|
|
|
|
}
|
|
|
|
tgtStr := strings.TrimPrefix(slc[1], "\"")
|
|
|
|
tgtStr = strings.TrimSuffix(tgtStr, "\"")
|
|
|
|
tgt := strings.Split(tgtStr, ";")
|
|
|
|
if len(tgt) != 4 {
|
|
|
|
return r, errInvalidReplicateDecisionFormat
|
|
|
|
}
|
|
|
|
var replicate, sync bool
|
|
|
|
var err error
|
|
|
|
replicate, err = strconv.ParseBool(tgt[0])
|
|
|
|
if err != nil {
|
|
|
|
return r, err
|
|
|
|
}
|
|
|
|
sync, err = strconv.ParseBool(tgt[1])
|
|
|
|
if err != nil {
|
|
|
|
return r, err
|
|
|
|
}
|
|
|
|
r.targetsMap[slc[0]] = replicateTargetDecision{Replicate: replicate, Synchronous: sync, Arn: tgt[2], ID: tgt[3]}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReplicationState represents internal replication state
|
|
|
|
type ReplicationState struct {
|
|
|
|
ReplicaTimeStamp time.Time // timestamp when last replica update was received
|
|
|
|
ReplicaStatus replication.StatusType // replica statusstringis
|
|
|
|
DeleteMarker bool // represents DeleteMarker replication state
|
|
|
|
ReplicationTimeStamp time.Time // timestamp when last replication activity happened
|
|
|
|
ReplicationStatusInternal string // stringified representation of all replication activity
|
|
|
|
// VersionPurgeStatusInternal is internally in the format "arn1=PENDING;arn2=COMMPLETED;"
|
|
|
|
VersionPurgeStatusInternal string // stringified representation of all version purge statuses
|
|
|
|
ReplicateDecisionStr string // stringified representation of replication decision for each target
|
|
|
|
Targets map[string]replication.StatusType // map of ARN->replication status for ongoing replication activity
|
|
|
|
PurgeTargets map[string]VersionPurgeStatusType // map of ARN->VersionPurgeStatus for all the targets
|
|
|
|
ResetStatusesMap map[string]string // map of ARN-> stringified reset id and timestamp for all the targets
|
|
|
|
}
|
|
|
|
|
|
|
|
// Equal returns true if replication state is identical for version purge statuses and (replica)tion statuses.
|
|
|
|
func (rs *ReplicationState) Equal(o ReplicationState) bool {
|
|
|
|
return rs.ReplicaStatus == o.ReplicaStatus &&
|
|
|
|
rs.ReplicaTimeStamp.Equal(o.ReplicaTimeStamp) &&
|
|
|
|
rs.ReplicationTimeStamp.Equal(o.ReplicationTimeStamp) &&
|
|
|
|
rs.ReplicationStatusInternal == o.ReplicationStatusInternal &&
|
|
|
|
rs.VersionPurgeStatusInternal == o.VersionPurgeStatusInternal
|
|
|
|
}
|
|
|
|
|
|
|
|
// CompositeReplicationStatus returns overall replication status for the object version being replicated.
|
|
|
|
func (rs *ReplicationState) CompositeReplicationStatus() (st replication.StatusType) {
|
|
|
|
switch {
|
|
|
|
case rs.ReplicationStatusInternal != "":
|
|
|
|
switch replication.StatusType(rs.ReplicationStatusInternal) {
|
|
|
|
case replication.Pending, replication.Completed, replication.Failed, replication.Replica: // for backward compatibility
|
|
|
|
return replication.StatusType(rs.ReplicationStatusInternal)
|
|
|
|
default:
|
|
|
|
replStatus := getCompositeReplicationStatus(rs.Targets)
|
|
|
|
// return REPLICA status if replica received timestamp is later than replication timestamp
|
|
|
|
// provided object replication completed for all targets.
|
|
|
|
if !rs.ReplicaTimeStamp.Equal(timeSentinel) && replStatus == replication.Completed && rs.ReplicaTimeStamp.After(rs.ReplicationTimeStamp) {
|
|
|
|
return rs.ReplicaStatus
|
|
|
|
}
|
|
|
|
return replStatus
|
|
|
|
}
|
|
|
|
case !rs.ReplicaStatus.Empty():
|
|
|
|
return rs.ReplicaStatus
|
|
|
|
default:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// CompositeVersionPurgeStatus returns overall replication purge status for the permanent delete being replicated.
|
|
|
|
func (rs *ReplicationState) CompositeVersionPurgeStatus() VersionPurgeStatusType {
|
|
|
|
switch VersionPurgeStatusType(rs.VersionPurgeStatusInternal) {
|
|
|
|
case Pending, Complete, Failed: // for backward compatibility
|
|
|
|
return VersionPurgeStatusType(rs.VersionPurgeStatusInternal)
|
|
|
|
default:
|
|
|
|
return getCompositeVersionPurgeStatus(rs.PurgeTargets)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TargetState returns replicatedInfos struct initialized with the previous state of replication
|
|
|
|
func (rs *ReplicationState) targetState(arn string) (r replicatedTargetInfo) {
|
|
|
|
return replicatedTargetInfo{
|
|
|
|
Arn: arn,
|
|
|
|
PrevReplicationStatus: rs.Targets[arn],
|
|
|
|
VersionPurgeStatus: rs.PurgeTargets[arn],
|
|
|
|
ResyncTimestamp: rs.ResetStatusesMap[arn],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// getReplicationState returns replication state using target replicated info for the targets
|
|
|
|
func getReplicationState(rinfos replicatedInfos, prevState ReplicationState, vID string) ReplicationState {
|
|
|
|
rs := ReplicationState{
|
|
|
|
ReplicateDecisionStr: prevState.ReplicateDecisionStr,
|
|
|
|
ResetStatusesMap: prevState.ResetStatusesMap,
|
|
|
|
ReplicaTimeStamp: prevState.ReplicaTimeStamp,
|
|
|
|
ReplicaStatus: prevState.ReplicaStatus,
|
|
|
|
}
|
|
|
|
var replStatuses, vpurgeStatuses string
|
|
|
|
replStatuses = rinfos.ReplicationStatusInternal()
|
|
|
|
rs.Targets = replicationStatusesMap(replStatuses)
|
|
|
|
rs.ReplicationStatusInternal = replStatuses
|
|
|
|
rs.ReplicationTimeStamp = rinfos.ReplicationTimeStamp
|
|
|
|
|
|
|
|
vpurgeStatuses = rinfos.VersionPurgeStatusInternal()
|
|
|
|
rs.VersionPurgeStatusInternal = vpurgeStatuses
|
|
|
|
rs.PurgeTargets = versionPurgeStatusesMap(vpurgeStatuses)
|
|
|
|
|
|
|
|
for _, rinfo := range rinfos.Targets {
|
|
|
|
if rinfo.ResyncTimestamp != "" {
|
|
|
|
rs.ResetStatusesMap[targetResetHeader(rinfo.Arn)] = rinfo.ResyncTimestamp
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return rs
|
|
|
|
}
|
|
|
|
|
|
|
|
// constructs a replication status map from string representation
|
|
|
|
func replicationStatusesMap(s string) map[string]replication.StatusType {
|
|
|
|
targets := make(map[string]replication.StatusType)
|
|
|
|
repStatMatches := replStatusRegex.FindAllStringSubmatch(s, -1)
|
|
|
|
for _, repStatMatch := range repStatMatches {
|
|
|
|
if len(repStatMatch) != 3 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
status := replication.StatusType(repStatMatch[2])
|
|
|
|
targets[repStatMatch[1]] = status
|
|
|
|
}
|
|
|
|
return targets
|
|
|
|
}
|
|
|
|
|
|
|
|
// constructs a version purge status map from string representation
|
|
|
|
func versionPurgeStatusesMap(s string) map[string]VersionPurgeStatusType {
|
|
|
|
targets := make(map[string]VersionPurgeStatusType)
|
|
|
|
purgeStatusMatches := replStatusRegex.FindAllStringSubmatch(s, -1)
|
|
|
|
for _, purgeStatusMatch := range purgeStatusMatches {
|
|
|
|
if len(purgeStatusMatch) != 3 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
targets[purgeStatusMatch[1]] = VersionPurgeStatusType(purgeStatusMatch[2])
|
|
|
|
}
|
|
|
|
return targets
|
|
|
|
}
|
|
|
|
|
|
|
|
// return the overall replication status for all the targets
|
|
|
|
func getCompositeReplicationStatus(m map[string]replication.StatusType) replication.StatusType {
|
|
|
|
if len(m) == 0 {
|
|
|
|
return replication.StatusType("")
|
|
|
|
}
|
|
|
|
completed := 0
|
|
|
|
for _, v := range m {
|
|
|
|
switch v {
|
|
|
|
case replication.Failed:
|
|
|
|
return replication.Failed
|
|
|
|
case replication.Completed:
|
|
|
|
completed++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if completed == len(m) {
|
|
|
|
return replication.Completed
|
|
|
|
}
|
|
|
|
return replication.Pending
|
|
|
|
}
|
|
|
|
|
|
|
|
// return the overall version purge status for all the targets
|
|
|
|
func getCompositeVersionPurgeStatus(m map[string]VersionPurgeStatusType) VersionPurgeStatusType {
|
|
|
|
if len(m) == 0 {
|
|
|
|
return VersionPurgeStatusType("")
|
|
|
|
}
|
|
|
|
completed := 0
|
|
|
|
for _, v := range m {
|
|
|
|
switch v {
|
|
|
|
case Failed:
|
|
|
|
return Failed
|
|
|
|
case Complete:
|
|
|
|
completed++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if completed == len(m) {
|
|
|
|
return Complete
|
|
|
|
}
|
|
|
|
return Pending
|
|
|
|
}
|
|
|
|
|
|
|
|
// getHealReplicateObjectInfo returns info needed by heal replication in ReplicateObjectInfo
|
|
|
|
func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) ReplicateObjectInfo {
|
|
|
|
oi := objInfo.Clone()
|
|
|
|
if rcfg.Config != nil && rcfg.Config.RoleArn != "" {
|
|
|
|
// For backward compatibility of objects pending/failed replication.
|
|
|
|
// Save replication related statuses in the new internal representation for
|
|
|
|
// compatible behavior.
|
|
|
|
if !oi.ReplicationStatus.Empty() {
|
|
|
|
oi.ReplicationStatusInternal = fmt.Sprintf("%s=%s;", rcfg.Config.RoleArn, oi.ReplicationStatus)
|
|
|
|
}
|
|
|
|
if !oi.VersionPurgeStatus.Empty() {
|
|
|
|
oi.VersionPurgeStatusInternal = fmt.Sprintf("%s=%s;", rcfg.Config.RoleArn, oi.VersionPurgeStatus)
|
|
|
|
}
|
|
|
|
for k, v := range oi.UserDefined {
|
2023-03-06 11:56:10 -05:00
|
|
|
if strings.EqualFold(k, ReservedMetadataPrefixLower+ReplicationReset) {
|
2021-09-18 16:31:35 -04:00
|
|
|
delete(oi.UserDefined, k)
|
|
|
|
oi.UserDefined[targetResetHeader(rcfg.Config.RoleArn)] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var dsc ReplicateDecision
|
|
|
|
var tgtStatuses map[string]replication.StatusType
|
2022-07-21 14:05:44 -04:00
|
|
|
var purgeStatuses map[string]VersionPurgeStatusType
|
|
|
|
|
2021-10-05 18:05:30 -04:00
|
|
|
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
|
2021-09-18 16:31:35 -04:00
|
|
|
dsc = checkReplicateDelete(GlobalContext, oi.Bucket, ObjectToDelete{
|
2022-01-03 04:28:52 -05:00
|
|
|
ObjectV: ObjectV{
|
|
|
|
ObjectName: oi.Name,
|
|
|
|
VersionID: oi.VersionID,
|
|
|
|
},
|
2023-06-27 12:45:50 -04:00
|
|
|
}, oi, ObjectOptions{
|
|
|
|
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
|
|
|
|
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
|
|
|
|
}, nil)
|
2021-09-18 16:31:35 -04:00
|
|
|
} else {
|
|
|
|
dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(ObjectInfo{
|
|
|
|
UserDefined: oi.UserDefined,
|
|
|
|
}, replication.HealReplicationType, ObjectOptions{}))
|
|
|
|
}
|
|
|
|
tgtStatuses = replicationStatusesMap(oi.ReplicationStatusInternal)
|
2022-07-21 14:05:44 -04:00
|
|
|
purgeStatuses = versionPurgeStatusesMap(oi.VersionPurgeStatusInternal)
|
2021-09-18 16:31:35 -04:00
|
|
|
existingObjResync := rcfg.Resync(GlobalContext, oi, &dsc, tgtStatuses)
|
2022-07-21 14:05:44 -04:00
|
|
|
tm, _ := time.Parse(time.RFC3339Nano, oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp])
|
2021-09-18 16:31:35 -04:00
|
|
|
return ReplicateObjectInfo{
|
2022-07-21 14:05:44 -04:00
|
|
|
ObjectInfo: oi,
|
|
|
|
OpType: replication.HealReplicationType,
|
|
|
|
Dsc: dsc,
|
|
|
|
ExistingObjResync: existingObjResync,
|
|
|
|
TargetStatuses: tgtStatuses,
|
|
|
|
TargetPurgeStatuses: purgeStatuses,
|
|
|
|
ReplicationTimestamp: tm,
|
2021-09-18 16:31:35 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// vID here represents the versionID client specified in request - need to distinguish between delete marker and delete marker deletion
|
|
|
|
func (o *ObjectInfo) getReplicationState(dsc string, vID string, heal bool) ReplicationState {
|
|
|
|
rs := ReplicationState{
|
|
|
|
ReplicationStatusInternal: o.ReplicationStatusInternal,
|
|
|
|
VersionPurgeStatusInternal: o.VersionPurgeStatusInternal,
|
|
|
|
ReplicateDecisionStr: dsc,
|
|
|
|
Targets: make(map[string]replication.StatusType),
|
|
|
|
PurgeTargets: make(map[string]VersionPurgeStatusType),
|
|
|
|
ResetStatusesMap: make(map[string]string),
|
|
|
|
}
|
|
|
|
rs.Targets = replicationStatusesMap(o.ReplicationStatusInternal)
|
|
|
|
rs.PurgeTargets = versionPurgeStatusesMap(o.VersionPurgeStatusInternal)
|
|
|
|
for k, v := range o.UserDefined {
|
|
|
|
if strings.HasPrefix(k, ReservedMetadataPrefixLower+ReplicationReset) {
|
|
|
|
arn := strings.TrimPrefix(k, fmt.Sprintf("%s-", ReservedMetadataPrefixLower+ReplicationReset))
|
|
|
|
rs.ResetStatusesMap[arn] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return rs
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReplicationState returns replication state using other internal replication metadata in ObjectToDelete
|
|
|
|
func (o *ObjectToDelete) ReplicationState() ReplicationState {
|
|
|
|
r := ReplicationState{
|
|
|
|
ReplicationStatusInternal: o.DeleteMarkerReplicationStatus,
|
|
|
|
VersionPurgeStatusInternal: o.VersionPurgeStatuses,
|
|
|
|
ReplicateDecisionStr: o.ReplicateDecisionStr,
|
|
|
|
}
|
|
|
|
|
|
|
|
r.Targets = replicationStatusesMap(o.DeleteMarkerReplicationStatus)
|
|
|
|
r.PurgeTargets = versionPurgeStatusesMap(o.VersionPurgeStatuses)
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
// VersionPurgeStatus returns a composite version purge status across targets
|
|
|
|
func (d *DeletedObject) VersionPurgeStatus() VersionPurgeStatusType {
|
|
|
|
return d.ReplicationState.CompositeVersionPurgeStatus()
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteMarkerReplicationStatus return composite replication status of delete marker across targets
|
|
|
|
func (d *DeletedObject) DeleteMarkerReplicationStatus() replication.StatusType {
|
|
|
|
return d.ReplicationState.CompositeReplicationStatus()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResyncTargetsInfo holds a slice of targets with resync info per target
|
|
|
|
type ResyncTargetsInfo struct {
|
|
|
|
Targets []ResyncTarget `json:"target,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResyncTarget is a struct representing the Target reset ID where target is identified by its Arn
|
|
|
|
type ResyncTarget struct {
|
2022-02-10 13:16:52 -05:00
|
|
|
Arn string `json:"arn"`
|
|
|
|
ResetID string `json:"resetid"`
|
|
|
|
StartTime time.Time `json:"startTime"`
|
|
|
|
EndTime time.Time `json:"endTime"`
|
|
|
|
// Status of resync operation
|
|
|
|
ResyncStatus string `json:"resyncStatus,omitempty"`
|
|
|
|
// Completed size in bytes
|
|
|
|
ReplicatedSize int64 `json:"completedReplicationSize"`
|
|
|
|
// Failed size in bytes
|
|
|
|
FailedSize int64 `json:"failedReplicationSize"`
|
|
|
|
// Total number of failed operations
|
|
|
|
FailedCount int64 `json:"failedReplicationCount"`
|
|
|
|
// Total number of failed operations
|
|
|
|
ReplicatedCount int64 `json:"replicationCount"`
|
|
|
|
// Last bucket/object replicated.
|
|
|
|
Bucket string `json:"bucket,omitempty"`
|
|
|
|
Object string `json:"object,omitempty"`
|
2021-09-18 16:31:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// VersionPurgeStatusType represents status of a versioned delete or permanent delete w.r.t bucket replication
|
|
|
|
type VersionPurgeStatusType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Pending - versioned delete replication is pending.
|
|
|
|
Pending VersionPurgeStatusType = "PENDING"
|
|
|
|
|
|
|
|
// Complete - versioned delete replication is now complete, erase version on disk.
|
|
|
|
Complete VersionPurgeStatusType = "COMPLETE"
|
|
|
|
|
|
|
|
// Failed - versioned delete replication failed.
|
|
|
|
Failed VersionPurgeStatusType = "FAILED"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Empty returns true if purge status was not set.
|
|
|
|
func (v VersionPurgeStatusType) Empty() bool {
|
|
|
|
return string(v) == ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pending returns true if the version is pending purge.
|
|
|
|
func (v VersionPurgeStatusType) Pending() bool {
|
|
|
|
return v == Pending || v == Failed
|
|
|
|
}
|
2022-02-10 13:16:52 -05:00
|
|
|
|
2022-11-14 10:16:40 -05:00
|
|
|
type replicationResyncer struct {
|
2022-02-10 13:16:52 -05:00
|
|
|
// map of bucket to their resync status
|
2022-11-14 10:16:40 -05:00
|
|
|
statusMap map[string]BucketReplicationResyncStatus
|
|
|
|
workerSize int
|
|
|
|
resyncCancelCh chan struct{}
|
|
|
|
workerCh chan struct{}
|
2022-02-10 13:16:52 -05:00
|
|
|
sync.RWMutex
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
2022-12-20 03:07:53 -05:00
|
|
|
replicationDir = ".replication"
|
2022-02-10 13:16:52 -05:00
|
|
|
resyncFileName = "resync.bin"
|
|
|
|
resyncMetaFormat = 1
|
|
|
|
resyncMetaVersionV1 = 1
|
|
|
|
resyncMetaVersion = resyncMetaVersionV1
|
|
|
|
)
|
|
|
|
|
2022-11-14 10:16:40 -05:00
|
|
|
type resyncOpts struct {
|
|
|
|
bucket string
|
|
|
|
arn string
|
|
|
|
resyncID string
|
|
|
|
resyncBefore time.Time
|
|
|
|
}
|
|
|
|
|
2022-02-10 13:16:52 -05:00
|
|
|
// ResyncStatusType status of resync operation
|
|
|
|
type ResyncStatusType int
|
|
|
|
|
|
|
|
const (
|
|
|
|
// NoResync - no resync in progress
|
|
|
|
NoResync ResyncStatusType = iota
|
2022-11-14 10:16:40 -05:00
|
|
|
// ResyncPending - resync pending
|
|
|
|
ResyncPending
|
|
|
|
// ResyncCanceled - resync canceled
|
|
|
|
ResyncCanceled
|
2022-02-10 13:16:52 -05:00
|
|
|
// ResyncStarted - resync in progress
|
|
|
|
ResyncStarted
|
|
|
|
// ResyncCompleted - resync finished
|
|
|
|
ResyncCompleted
|
|
|
|
// ResyncFailed - resync failed
|
|
|
|
ResyncFailed
|
|
|
|
)
|
|
|
|
|
2022-11-14 10:16:40 -05:00
|
|
|
func (rt ResyncStatusType) isValid() bool {
|
|
|
|
return rt != NoResync
|
|
|
|
}
|
|
|
|
|
2022-02-10 13:16:52 -05:00
|
|
|
func (rt ResyncStatusType) String() string {
|
|
|
|
switch rt {
|
|
|
|
case ResyncStarted:
|
|
|
|
return "Ongoing"
|
|
|
|
case ResyncCompleted:
|
|
|
|
return "Completed"
|
|
|
|
case ResyncFailed:
|
|
|
|
return "Failed"
|
2022-11-14 10:16:40 -05:00
|
|
|
case ResyncPending:
|
|
|
|
return "Pending"
|
|
|
|
case ResyncCanceled:
|
|
|
|
return "Canceled"
|
2022-02-10 13:16:52 -05:00
|
|
|
default:
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TargetReplicationResyncStatus status of resync of bucket for a specific target
|
|
|
|
type TargetReplicationResyncStatus struct {
|
2022-11-14 10:16:40 -05:00
|
|
|
StartTime time.Time `json:"startTime" msg:"st"`
|
|
|
|
LastUpdate time.Time `json:"lastUpdated" msg:"lst"`
|
2022-02-10 13:16:52 -05:00
|
|
|
// Resync ID assigned to this reset
|
|
|
|
ResyncID string `json:"resyncID" msg:"id"`
|
|
|
|
// ResyncBeforeDate - resync all objects created prior to this date
|
|
|
|
ResyncBeforeDate time.Time `json:"resyncBeforeDate" msg:"rdt"`
|
|
|
|
// Status of resync operation
|
|
|
|
ResyncStatus ResyncStatusType `json:"resyncStatus" msg:"rst"`
|
|
|
|
// Failed size in bytes
|
|
|
|
FailedSize int64 `json:"failedReplicationSize" msg:"fs"`
|
|
|
|
// Total number of failed operations
|
|
|
|
FailedCount int64 `json:"failedReplicationCount" msg:"frc"`
|
|
|
|
// Completed size in bytes
|
|
|
|
ReplicatedSize int64 `json:"completedReplicationSize" msg:"rs"`
|
|
|
|
// Total number of failed operations
|
|
|
|
ReplicatedCount int64 `json:"replicationCount" msg:"rrc"`
|
|
|
|
// Last bucket/object replicated.
|
|
|
|
Bucket string `json:"-" msg:"bkt"`
|
|
|
|
Object string `json:"-" msg:"obj"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// BucketReplicationResyncStatus captures current replication resync status
|
|
|
|
type BucketReplicationResyncStatus struct {
|
|
|
|
Version int `json:"version" msg:"v"`
|
|
|
|
// map of remote arn to their resync status for a bucket
|
|
|
|
TargetsMap map[string]TargetReplicationResyncStatus `json:"resyncMap,omitempty" msg:"brs"`
|
|
|
|
ID int `json:"id" msg:"id"`
|
|
|
|
LastUpdate time.Time `json:"lastUpdate" msg:"lu"`
|
|
|
|
}
|
|
|
|
|
2022-11-14 10:16:40 -05:00
|
|
|
func (rs *BucketReplicationResyncStatus) cloneTgtStats() (m map[string]TargetReplicationResyncStatus) {
|
|
|
|
m = make(map[string]TargetReplicationResyncStatus)
|
|
|
|
for arn, st := range rs.TargetsMap {
|
|
|
|
m[arn] = st
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-02-10 13:16:52 -05:00
|
|
|
func newBucketResyncStatus(bucket string) BucketReplicationResyncStatus {
|
|
|
|
return BucketReplicationResyncStatus{
|
|
|
|
TargetsMap: make(map[string]TargetReplicationResyncStatus),
|
|
|
|
Version: resyncMetaVersion,
|
|
|
|
}
|
|
|
|
}
|
2022-03-08 16:58:55 -05:00
|
|
|
|
|
|
|
var contentRangeRegexp = regexp.MustCompile(`bytes ([0-9]+)-([0-9]+)/([0-9]+|\\*)`)
|
|
|
|
|
|
|
|
// parse size from content-range header
|
|
|
|
func parseSizeFromContentRange(h http.Header) (sz int64, err error) {
|
|
|
|
cr := h.Get(xhttp.ContentRange)
|
|
|
|
if cr == "" {
|
|
|
|
return sz, fmt.Errorf("Content-Range not set")
|
|
|
|
}
|
|
|
|
parts := contentRangeRegexp.FindStringSubmatch(cr)
|
|
|
|
if len(parts) != 4 {
|
|
|
|
return sz, fmt.Errorf("invalid Content-Range header %s", cr)
|
|
|
|
}
|
|
|
|
if parts[3] == "*" {
|
|
|
|
return -1, nil
|
|
|
|
}
|
|
|
|
var usz uint64
|
|
|
|
usz, err = strconv.ParseUint(parts[3], 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return sz, err
|
|
|
|
}
|
|
|
|
return int64(usz), nil
|
|
|
|
}
|
2022-07-21 14:05:44 -04:00
|
|
|
|
|
|
|
func extractReplicateDiffOpts(q url.Values) (opts madmin.ReplDiffOpts) {
|
|
|
|
opts.Verbose = q.Get("verbose") == "true"
|
|
|
|
opts.ARN = q.Get("arn")
|
|
|
|
opts.Prefix = q.Get("prefix")
|
|
|
|
return
|
|
|
|
}
|
2022-08-22 19:53:06 -04:00
|
|
|
|
|
|
|
const (
|
|
|
|
replicationMRFDir = bucketMetaPrefix + SlashSeparator + replicationDir + SlashSeparator + "mrf"
|
|
|
|
mrfMetaFormat = 1
|
|
|
|
mrfMetaVersionV1 = 1
|
|
|
|
mrfMetaVersion = mrfMetaVersionV1
|
|
|
|
)
|
|
|
|
|
|
|
|
// MRFReplicateEntry mrf entry to save to disk
|
|
|
|
type MRFReplicateEntry struct {
|
|
|
|
Bucket string `json:"bucket" msg:"b"`
|
|
|
|
Object string `json:"object" msg:"o"`
|
|
|
|
versionID string `json:"-"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// MRFReplicateEntries has the map of MRF entries to save to disk
|
|
|
|
type MRFReplicateEntries struct {
|
|
|
|
Entries map[string]MRFReplicateEntry `json:"entries" msg:"e"`
|
|
|
|
Version int `json:"version" msg:"v"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// ToMRFEntry returns the relevant info needed by MRF
|
|
|
|
func (ri ReplicateObjectInfo) ToMRFEntry() MRFReplicateEntry {
|
|
|
|
return MRFReplicateEntry{
|
|
|
|
Bucket: ri.Bucket,
|
|
|
|
Object: ri.Name,
|
|
|
|
versionID: ri.VersionID,
|
|
|
|
}
|
|
|
|
}
|
2022-09-12 15:40:02 -04:00
|
|
|
|
2022-12-22 17:25:13 -05:00
|
|
|
func getReplicationStatsPath() string {
|
|
|
|
return bucketMetaPrefix + SlashSeparator + replicationDir + SlashSeparator + "replication.stats"
|
2022-09-12 15:40:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
replStatsMetaFormat = 1
|
|
|
|
replStatsVersionV1 = 1
|
|
|
|
replStatsVersion = replStatsVersionV1
|
|
|
|
replStatsSaveInterval = time.Minute * 5
|
|
|
|
)
|