mirror of
https://github.com/minio/minio.git
synced 2024-12-26 15:15:55 -05:00
f1302c40fe
Services are unfrozen before `initBackgroundReplication` is finished. This means that the globalReplicationStats write is racy. Switch to an atomic pointer. Provide the `ReplicationPool` with the stats, so it doesn't have to be grabbed from the atomic pointer on every use. All other loads and checks are nil, and calls return empty values when stats still haven't been initialized.
6305 lines
185 KiB
Go
6305 lines
185 KiB
Go
// Copyright (c) 2015-2022 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 cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/url"
|
|
"reflect"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/minio/madmin-go/v3"
|
|
"github.com/minio/minio-go/v7"
|
|
minioClient "github.com/minio/minio-go/v7"
|
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
"github.com/minio/minio-go/v7/pkg/replication"
|
|
"github.com/minio/minio-go/v7/pkg/set"
|
|
"github.com/minio/minio/internal/auth"
|
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
|
sreplication "github.com/minio/minio/internal/bucket/replication"
|
|
"github.com/minio/minio/internal/logger"
|
|
xldap "github.com/minio/pkg/v3/ldap"
|
|
"github.com/minio/pkg/v3/policy"
|
|
"github.com/puzpuzpuz/xsync/v3"
|
|
)
|
|
|
|
const (
|
|
srStatePrefix = minioConfigPrefix + "/site-replication"
|
|
srStateFile = "state.json"
|
|
)
|
|
|
|
const (
|
|
srStateFormatVersion1 = 1
|
|
)
|
|
|
|
var (
|
|
errSRCannotJoin = SRError{
|
|
Cause: errors.New("this site is already configured for site-replication"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
errSRDuplicateSites = SRError{
|
|
Cause: errors.New("duplicate sites provided for site-replication"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
errSRSelfNotFound = SRError{
|
|
Cause: errors.New("none of the given sites correspond to the current one"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
errSRPeerNotFound = SRError{
|
|
Cause: errors.New("peer not found"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
errSRRequestorNotFound = SRError{
|
|
Cause: errors.New("requesting site not found in site replication config"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
errSRNotEnabled = SRError{
|
|
Cause: errors.New("site replication is not enabled"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
errSRResyncStarted = SRError{
|
|
Cause: errors.New("site replication resync is already in progress"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
errSRResyncCanceled = SRError{
|
|
Cause: errors.New("site replication resync is already canceled"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
errSRNoResync = SRError{
|
|
Cause: errors.New("no resync in progress"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
errSRResyncToSelf = SRError{
|
|
Cause: errors.New("invalid peer specified - cannot resync to self"),
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
)
|
|
|
|
func errSRInvalidRequest(err error) SRError {
|
|
return SRError{
|
|
Cause: err,
|
|
Code: ErrSiteReplicationInvalidRequest,
|
|
}
|
|
}
|
|
|
|
func errSRPeerResp(err error) SRError {
|
|
return SRError{
|
|
Cause: err,
|
|
Code: ErrSiteReplicationPeerResp,
|
|
}
|
|
}
|
|
|
|
func errSRBackendIssue(err error) SRError {
|
|
return SRError{
|
|
Cause: err,
|
|
Code: ErrSiteReplicationBackendIssue,
|
|
}
|
|
}
|
|
|
|
func errSRServiceAccount(err error) SRError {
|
|
return SRError{
|
|
Cause: err,
|
|
Code: ErrSiteReplicationServiceAccountError,
|
|
}
|
|
}
|
|
|
|
func errSRBucketConfigError(err error) SRError {
|
|
return SRError{
|
|
Cause: err,
|
|
Code: ErrSiteReplicationBucketConfigError,
|
|
}
|
|
}
|
|
|
|
func errSRBucketMetaError(err error) SRError {
|
|
return SRError{
|
|
Cause: err,
|
|
Code: ErrSiteReplicationBucketMetaError,
|
|
}
|
|
}
|
|
|
|
func errSRIAMError(err error) SRError {
|
|
return SRError{
|
|
Cause: err,
|
|
Code: ErrSiteReplicationIAMError,
|
|
}
|
|
}
|
|
|
|
func errSRConfigMissingError(err error) SRError {
|
|
return SRError{
|
|
Cause: err,
|
|
Code: ErrSiteReplicationConfigMissing,
|
|
}
|
|
}
|
|
|
|
func errSRIAMConfigMismatch(peer1, peer2 string, s1, s2 madmin.IDPSettings) SRError {
|
|
return SRError{
|
|
Cause: fmt.Errorf("IAM/IDP settings mismatch between %s and %s: %#v vs %#v", peer1, peer2, s1, s2),
|
|
Code: ErrSiteReplicationIAMConfigMismatch,
|
|
}
|
|
}
|
|
|
|
var errSRObjectLayerNotReady = SRError{
|
|
Cause: fmt.Errorf("object layer not ready"),
|
|
Code: ErrServerNotInitialized,
|
|
}
|
|
|
|
func getSRStateFilePath() string {
|
|
return srStatePrefix + SlashSeparator + srStateFile
|
|
}
|
|
|
|
// SRError - wrapped error for site replication.
|
|
type SRError struct {
|
|
Cause error
|
|
Code APIErrorCode
|
|
}
|
|
|
|
func (c SRError) Error() string {
|
|
if c.Cause != nil {
|
|
return c.Cause.Error()
|
|
}
|
|
return "<nil>"
|
|
}
|
|
|
|
func (c SRError) Unwrap() error {
|
|
return c.Cause
|
|
}
|
|
|
|
func wrapSRErr(err error) SRError {
|
|
return SRError{Cause: err, Code: ErrInternalError}
|
|
}
|
|
|
|
// SiteReplicationSys - manages cluster-level replication.
|
|
type SiteReplicationSys struct {
|
|
sync.RWMutex
|
|
|
|
enabled bool
|
|
|
|
// In-memory and persisted multi-site replication state.
|
|
state srState
|
|
|
|
iamMetaCache srIAMCache
|
|
}
|
|
|
|
type srState srStateV1
|
|
|
|
// srStateV1 represents version 1 of the site replication state persistence
|
|
// format.
|
|
type srStateV1 struct {
|
|
Name string `json:"name"`
|
|
|
|
// Peers maps peers by their deploymentID
|
|
Peers map[string]madmin.PeerInfo `json:"peers"`
|
|
ServiceAccountAccessKey string `json:"serviceAccountAccessKey"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
// srStateData represents the format of the current `srStateFile`.
|
|
type srStateData struct {
|
|
Version int `json:"version"`
|
|
|
|
SRState srStateV1 `json:"srState"`
|
|
}
|
|
|
|
// Init - initialize the site replication manager.
|
|
func (c *SiteReplicationSys) Init(ctx context.Context, objAPI ObjectLayer) error {
|
|
go c.startHealRoutine(ctx, objAPI)
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
for {
|
|
err := c.loadFromDisk(ctx, objAPI)
|
|
if err == errConfigNotFound {
|
|
return nil
|
|
}
|
|
if err == nil {
|
|
break
|
|
}
|
|
replLogOnceIf(context.Background(), fmt.Errorf("unable to initialize site replication subsystem: (%w)", err), "site-relication-init")
|
|
|
|
duration := time.Duration(r.Float64() * float64(time.Minute))
|
|
if duration < time.Second {
|
|
// Make sure to sleep at least a second to avoid high CPU ticks.
|
|
duration = time.Second
|
|
}
|
|
time.Sleep(duration)
|
|
}
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if c.enabled {
|
|
logger.Info("Cluster replication initialized")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) loadFromDisk(ctx context.Context, objAPI ObjectLayer) error {
|
|
buf, err := readConfig(ctx, objAPI, getSRStateFilePath())
|
|
if err != nil {
|
|
if errors.Is(err, errConfigNotFound) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
c.state = srState{}
|
|
c.enabled = false
|
|
}
|
|
return err
|
|
}
|
|
|
|
// attempt to read just the version key in the state file to ensure we
|
|
// are reading a compatible version.
|
|
var ver struct {
|
|
Version int `json:"version"`
|
|
}
|
|
err = json.Unmarshal(buf, &ver)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ver.Version != srStateFormatVersion1 {
|
|
return fmt.Errorf("Unexpected ClusterRepl state version: %d", ver.Version)
|
|
}
|
|
|
|
var sdata srStateData
|
|
err = json.Unmarshal(buf, &sdata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
c.state = srState(sdata.SRState)
|
|
c.enabled = len(c.state.Peers) != 0
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) saveToDisk(ctx context.Context, state srState) error {
|
|
sdata := srStateData{
|
|
Version: srStateFormatVersion1,
|
|
SRState: srStateV1(state),
|
|
}
|
|
buf, err := json.Marshal(sdata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
objAPI := newObjectLayerFn()
|
|
if objAPI == nil {
|
|
return errServerNotInitialized
|
|
}
|
|
|
|
if err = saveConfig(ctx, objAPI, getSRStateFilePath(), buf); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, err := range globalNotificationSys.ReloadSiteReplicationConfig(ctx) {
|
|
replLogIf(ctx, err)
|
|
}
|
|
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
c.state = state
|
|
c.enabled = len(c.state.Peers) != 0
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) removeFromDisk(ctx context.Context) error {
|
|
objAPI := newObjectLayerFn()
|
|
if objAPI == nil {
|
|
return errServerNotInitialized
|
|
}
|
|
|
|
if err := deleteConfig(ctx, objAPI, getSRStateFilePath()); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, err := range globalNotificationSys.ReloadSiteReplicationConfig(ctx) {
|
|
replLogIf(ctx, err)
|
|
}
|
|
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
c.state = srState{}
|
|
c.enabled = false
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
// Access key of service account used for perform cluster-replication
|
|
// operations.
|
|
siteReplicatorSvcAcc = "site-replicator-0"
|
|
)
|
|
|
|
// PeerSiteInfo is a wrapper struct around madmin.PeerSite with extra info on site status
|
|
type PeerSiteInfo struct {
|
|
madmin.PeerSite
|
|
self bool
|
|
DeploymentID string
|
|
Replicated bool // true if already participating in site replication
|
|
Empty bool // true if cluster has no buckets
|
|
}
|
|
|
|
// getSiteStatuses gathers more info on the sites being added
|
|
func (c *SiteReplicationSys) getSiteStatuses(ctx context.Context, sites ...madmin.PeerSite) (psi []PeerSiteInfo, err error) {
|
|
psi = make([]PeerSiteInfo, 0, len(sites))
|
|
for _, v := range sites {
|
|
admClient, err := getAdminClient(v.Endpoint, v.AccessKey, v.SecretKey)
|
|
if err != nil {
|
|
return psi, errSRPeerResp(fmt.Errorf("unable to create admin client for %s: %w", v.Name, err))
|
|
}
|
|
|
|
info, err := admClient.ServerInfo(ctx)
|
|
if err != nil {
|
|
return psi, errSRPeerResp(fmt.Errorf("unable to fetch server info for %s: %w", v.Name, err))
|
|
}
|
|
|
|
s3Client, err := getS3Client(v)
|
|
if err != nil {
|
|
return psi, errSRPeerResp(fmt.Errorf("unable to create s3 client for %s: %w", v.Name, err))
|
|
}
|
|
|
|
buckets, err := s3Client.ListBuckets(ctx)
|
|
if err != nil {
|
|
return psi, errSRPeerResp(fmt.Errorf("unable to list buckets for %s: %v", v.Name, err))
|
|
}
|
|
|
|
psi = append(psi, PeerSiteInfo{
|
|
PeerSite: v,
|
|
DeploymentID: info.DeploymentID,
|
|
Empty: len(buckets) == 0,
|
|
self: info.DeploymentID == globalDeploymentID(),
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
// AddPeerClusters - add cluster sites for replication configuration.
|
|
func (c *SiteReplicationSys) AddPeerClusters(ctx context.Context, psites []madmin.PeerSite, opts madmin.SRAddOptions) (madmin.ReplicateAddStatus, error) {
|
|
sites, serr := c.getSiteStatuses(ctx, psites...)
|
|
if serr != nil {
|
|
return madmin.ReplicateAddStatus{}, serr
|
|
}
|
|
var (
|
|
currSites madmin.SiteReplicationInfo
|
|
currDeploymentIDsSet = set.NewStringSet()
|
|
err error
|
|
)
|
|
currSites, err = c.GetClusterInfo(ctx)
|
|
if err != nil {
|
|
return madmin.ReplicateAddStatus{}, errSRBackendIssue(err)
|
|
}
|
|
for _, v := range currSites.Sites {
|
|
currDeploymentIDsSet.Add(v.DeploymentID)
|
|
}
|
|
deploymentIDsSet := set.NewStringSet()
|
|
localHasBuckets := false
|
|
nonLocalPeerWithBuckets := ""
|
|
selfIdx := -1
|
|
for i, v := range sites {
|
|
// deploymentIDs must be unique
|
|
if deploymentIDsSet.Contains(v.DeploymentID) {
|
|
return madmin.ReplicateAddStatus{}, errSRDuplicateSites
|
|
}
|
|
deploymentIDsSet.Add(v.DeploymentID)
|
|
|
|
if v.self {
|
|
selfIdx = i
|
|
localHasBuckets = !v.Empty
|
|
continue
|
|
}
|
|
if !v.Empty && !currDeploymentIDsSet.Contains(v.DeploymentID) {
|
|
nonLocalPeerWithBuckets = v.Name
|
|
}
|
|
}
|
|
if selfIdx == -1 {
|
|
return madmin.ReplicateAddStatus{}, errSRBackendIssue(fmt.Errorf("global deployment ID %s mismatch, expected one of %s", globalDeploymentID(), deploymentIDsSet))
|
|
}
|
|
if !currDeploymentIDsSet.IsEmpty() {
|
|
// If current cluster is already SR enabled and no new site being added ,fail.
|
|
if currDeploymentIDsSet.Equals(deploymentIDsSet) {
|
|
return madmin.ReplicateAddStatus{}, errSRCannotJoin
|
|
}
|
|
if len(currDeploymentIDsSet.Intersection(deploymentIDsSet)) != len(currDeploymentIDsSet) {
|
|
diffSlc := getMissingSiteNames(currDeploymentIDsSet, deploymentIDsSet, currSites.Sites)
|
|
return madmin.ReplicateAddStatus{}, errSRInvalidRequest(fmt.Errorf("all existing replicated sites must be specified - missing %s", strings.Join(diffSlc, " ")))
|
|
}
|
|
}
|
|
|
|
// validate that all clusters are using the same IDP settings.
|
|
err = c.validateIDPSettings(ctx, sites)
|
|
if err != nil {
|
|
return madmin.ReplicateAddStatus{}, err
|
|
}
|
|
|
|
// For this `add` API, either all clusters must be empty or the local
|
|
// cluster must be the only one having some buckets.
|
|
if localHasBuckets && nonLocalPeerWithBuckets != "" {
|
|
return madmin.ReplicateAddStatus{}, errSRInvalidRequest(errors.New("only one cluster may have data when configuring site replication"))
|
|
}
|
|
|
|
if !localHasBuckets && nonLocalPeerWithBuckets != "" {
|
|
return madmin.ReplicateAddStatus{}, errSRInvalidRequest(fmt.Errorf("please send your request to the cluster containing data/buckets: %s", nonLocalPeerWithBuckets))
|
|
}
|
|
|
|
// FIXME: Ideally, we also need to check if there are any global IAM
|
|
// policies and any (LDAP user created) service accounts on the other
|
|
// peer clusters, and if so, reject the cluster replicate add request.
|
|
// This is not yet implemented.
|
|
|
|
// VALIDATIONS COMPLETE.
|
|
|
|
// Create a common service account for all clusters, with root
|
|
// permissions.
|
|
|
|
// Create a local service account.
|
|
|
|
// Generate a secret key for the service account if not created already.
|
|
var secretKey string
|
|
var svcCred auth.Credentials
|
|
sa, _, err := globalIAMSys.getServiceAccount(ctx, siteReplicatorSvcAcc)
|
|
switch {
|
|
case err == errNoSuchServiceAccount:
|
|
_, secretKey, err = auth.GenerateCredentials()
|
|
if err != nil {
|
|
return madmin.ReplicateAddStatus{}, errSRServiceAccount(fmt.Errorf("unable to create local service account: %w", err))
|
|
}
|
|
svcCred, _, err = globalIAMSys.NewServiceAccount(ctx, sites[selfIdx].AccessKey, nil, newServiceAccountOpts{
|
|
accessKey: siteReplicatorSvcAcc,
|
|
secretKey: secretKey,
|
|
allowSiteReplicatorAccount: true,
|
|
})
|
|
if err != nil {
|
|
return madmin.ReplicateAddStatus{}, errSRServiceAccount(fmt.Errorf("unable to create local service account: %w", err))
|
|
}
|
|
case err == nil:
|
|
svcCred = sa.Credentials
|
|
secretKey = svcCred.SecretKey
|
|
default:
|
|
return madmin.ReplicateAddStatus{}, errSRBackendIssue(err)
|
|
}
|
|
|
|
currTime := time.Now()
|
|
joinReq := madmin.SRPeerJoinReq{
|
|
SvcAcctAccessKey: svcCred.AccessKey,
|
|
SvcAcctSecretKey: secretKey,
|
|
Peers: make(map[string]madmin.PeerInfo),
|
|
UpdatedAt: currTime,
|
|
}
|
|
// check if few peers exist already and ILM expiry replcation is set to true
|
|
replicateILMExpirySet := false
|
|
if c.state.Peers != nil {
|
|
for _, pi := range c.state.Peers {
|
|
if pi.ReplicateILMExpiry {
|
|
replicateILMExpirySet = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
for _, v := range sites {
|
|
var peerReplicateILMExpiry bool
|
|
// if peers already exist and for one of them ReplicateILMExpiry
|
|
// set true, that means earlier replication of ILM expiry was set
|
|
// for the site replication. All new sites added to the setup should
|
|
// get this enabled as well
|
|
if replicateILMExpirySet {
|
|
peerReplicateILMExpiry = replicateILMExpirySet
|
|
} else {
|
|
peerReplicateILMExpiry = opts.ReplicateILMExpiry
|
|
}
|
|
joinReq.Peers[v.DeploymentID] = madmin.PeerInfo{
|
|
Endpoint: v.Endpoint,
|
|
Name: v.Name,
|
|
DeploymentID: v.DeploymentID,
|
|
ReplicateILMExpiry: peerReplicateILMExpiry,
|
|
}
|
|
}
|
|
|
|
addedCount := 0
|
|
var (
|
|
peerAddErr error
|
|
admClient *madmin.AdminClient
|
|
)
|
|
|
|
for _, v := range sites {
|
|
if v.self {
|
|
continue
|
|
}
|
|
switch {
|
|
case currDeploymentIDsSet.Contains(v.DeploymentID):
|
|
admClient, err = c.getAdminClient(ctx, v.DeploymentID)
|
|
default:
|
|
admClient, err = getAdminClient(v.Endpoint, v.AccessKey, v.SecretKey)
|
|
}
|
|
if err != nil {
|
|
peerAddErr = errSRPeerResp(fmt.Errorf("unable to create admin client for %s: %w", v.Name, err))
|
|
break
|
|
}
|
|
joinReq.SvcAcctParent = v.AccessKey
|
|
err = admClient.SRPeerJoin(ctx, joinReq)
|
|
if err != nil {
|
|
peerAddErr = errSRPeerResp(fmt.Errorf("unable to link with peer %s: %w", v.Name, err))
|
|
break
|
|
}
|
|
addedCount++
|
|
}
|
|
|
|
if peerAddErr != nil {
|
|
if addedCount == 0 {
|
|
return madmin.ReplicateAddStatus{}, peerAddErr
|
|
}
|
|
// In this case, it means at least one cluster was added
|
|
// successfully, we need to send a response to the client with
|
|
// some details - FIXME: the disks on this cluster would need to
|
|
// be cleaned to recover.
|
|
partial := madmin.ReplicateAddStatus{
|
|
Status: madmin.ReplicateAddStatusPartial,
|
|
ErrDetail: peerAddErr.Error(),
|
|
}
|
|
|
|
return partial, nil
|
|
}
|
|
|
|
// Other than handling existing buckets, we can now save the cluster
|
|
// replication configuration state.
|
|
state := srState{
|
|
Name: sites[selfIdx].Name,
|
|
Peers: joinReq.Peers,
|
|
ServiceAccountAccessKey: svcCred.AccessKey,
|
|
UpdatedAt: currTime,
|
|
}
|
|
|
|
if err = c.saveToDisk(ctx, state); err != nil {
|
|
return madmin.ReplicateAddStatus{
|
|
Status: madmin.ReplicateAddStatusPartial,
|
|
ErrDetail: fmt.Sprintf("unable to save cluster-replication state on local: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
if !globalSiteReplicatorCred.IsValid() {
|
|
globalSiteReplicatorCred.Set(svcCred.SecretKey)
|
|
}
|
|
result := madmin.ReplicateAddStatus{
|
|
Success: true,
|
|
Status: madmin.ReplicateAddStatusSuccess,
|
|
}
|
|
|
|
if err := c.syncToAllPeers(ctx, opts); err != nil {
|
|
result.InitialSyncErrorMessage = err.Error()
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// PeerJoinReq - internal API handler to respond to a peer cluster's request to join.
|
|
func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJoinReq) error {
|
|
var ourName string
|
|
for d, p := range arg.Peers {
|
|
if d == globalDeploymentID() {
|
|
ourName = p.Name
|
|
break
|
|
}
|
|
}
|
|
if ourName == "" {
|
|
return errSRSelfNotFound
|
|
}
|
|
|
|
sa, _, err := globalIAMSys.GetServiceAccount(ctx, arg.SvcAcctAccessKey)
|
|
if err == errNoSuchServiceAccount {
|
|
sa, _, err = globalIAMSys.NewServiceAccount(ctx, arg.SvcAcctParent, nil, newServiceAccountOpts{
|
|
accessKey: arg.SvcAcctAccessKey,
|
|
secretKey: arg.SvcAcctSecretKey,
|
|
allowSiteReplicatorAccount: arg.SvcAcctAccessKey == siteReplicatorSvcAcc,
|
|
})
|
|
}
|
|
if err != nil {
|
|
return errSRServiceAccount(fmt.Errorf("unable to create service account on %s: %v", ourName, err))
|
|
}
|
|
|
|
peers := make(map[string]madmin.PeerInfo, len(arg.Peers))
|
|
for dID, pi := range arg.Peers {
|
|
if c.state.Peers != nil {
|
|
if existingPeer, ok := c.state.Peers[dID]; ok {
|
|
// retain existing ReplicateILMExpiry of peer if its already set
|
|
// and incoming arg has it false. it could be default false
|
|
if !pi.ReplicateILMExpiry && existingPeer.ReplicateILMExpiry {
|
|
pi.ReplicateILMExpiry = existingPeer.ReplicateILMExpiry
|
|
}
|
|
}
|
|
}
|
|
peers[dID] = pi
|
|
}
|
|
state := srState{
|
|
Name: ourName,
|
|
Peers: peers,
|
|
ServiceAccountAccessKey: arg.SvcAcctAccessKey,
|
|
UpdatedAt: arg.UpdatedAt,
|
|
}
|
|
if err = c.saveToDisk(ctx, state); err != nil {
|
|
return errSRBackendIssue(fmt.Errorf("unable to save cluster-replication state to drive on %s: %v", ourName, err))
|
|
}
|
|
if !globalSiteReplicatorCred.IsValid() {
|
|
globalSiteReplicatorCred.Set(sa.SecretKey)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetIDPSettings returns info about the configured identity provider. It is
|
|
// used to validate that all peers have the same IDP.
|
|
func (c *SiteReplicationSys) GetIDPSettings(ctx context.Context) madmin.IDPSettings {
|
|
s := madmin.IDPSettings{}
|
|
s.LDAP = madmin.LDAPSettings{
|
|
IsLDAPEnabled: globalIAMSys.LDAPConfig.Enabled(),
|
|
LDAPUserDNSearchBase: globalIAMSys.LDAPConfig.LDAP.UserDNSearchBaseDistName,
|
|
LDAPUserDNSearchFilter: globalIAMSys.LDAPConfig.LDAP.UserDNSearchFilter,
|
|
LDAPGroupSearchBase: globalIAMSys.LDAPConfig.LDAP.GroupSearchBaseDistName,
|
|
LDAPGroupSearchFilter: globalIAMSys.LDAPConfig.LDAP.GroupSearchFilter,
|
|
}
|
|
s.OpenID = globalIAMSys.OpenIDConfig.GetSettings()
|
|
if s.OpenID.Enabled {
|
|
s.OpenID.Region = globalSite.Region()
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (c *SiteReplicationSys) validateIDPSettings(ctx context.Context, peers []PeerSiteInfo) error {
|
|
s := make([]madmin.IDPSettings, 0, len(peers))
|
|
for _, v := range peers {
|
|
if v.self {
|
|
s = append(s, c.GetIDPSettings(ctx))
|
|
continue
|
|
}
|
|
|
|
admClient, err := getAdminClient(v.Endpoint, v.AccessKey, v.SecretKey)
|
|
if err != nil {
|
|
return errSRPeerResp(fmt.Errorf("unable to create admin client for %s: %w", v.Name, err))
|
|
}
|
|
|
|
is, err := admClient.SRPeerGetIDPSettings(ctx)
|
|
if err != nil {
|
|
return errSRPeerResp(fmt.Errorf("unable to fetch IDP settings from %s: %v", v.Name, err))
|
|
}
|
|
s = append(s, is)
|
|
}
|
|
|
|
for i := 1; i < len(s); i++ {
|
|
if !reflect.DeepEqual(s[i], s[0]) {
|
|
return errSRIAMConfigMismatch(peers[0].Name, peers[i].Name, s[0], s[i])
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Netperf for site-replication net perf
|
|
func (c *SiteReplicationSys) Netperf(ctx context.Context, duration time.Duration) (results madmin.SiteNetPerfResult, err error) {
|
|
infos, err := globalSiteReplicationSys.GetClusterInfo(ctx)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
var wg sync.WaitGroup
|
|
var resultsMu sync.RWMutex
|
|
for _, info := range infos.Sites {
|
|
info := info
|
|
// will call siteNetperf, means call others's adminAPISiteReplicationDevNull
|
|
if globalDeploymentID() == info.DeploymentID {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
result := madmin.SiteNetPerfNodeResult{}
|
|
cli, err := globalSiteReplicationSys.getAdminClient(ctx, info.DeploymentID)
|
|
if err != nil {
|
|
result.Error = err.Error()
|
|
} else {
|
|
result = siteNetperf(ctx, duration)
|
|
result.Endpoint = cli.GetEndpointURL().String()
|
|
}
|
|
resultsMu.Lock()
|
|
results.NodeResults = append(results.NodeResults, result)
|
|
resultsMu.Unlock()
|
|
return
|
|
}()
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
ctx, cancel := context.WithTimeout(ctx, duration+10*time.Second)
|
|
defer cancel()
|
|
result := perfNetRequest(
|
|
ctx,
|
|
info.DeploymentID,
|
|
adminPathPrefix+adminAPIVersionPrefix+adminAPISiteReplicationNetPerf,
|
|
nil,
|
|
)
|
|
resultsMu.Lock()
|
|
results.NodeResults = append(results.NodeResults, result)
|
|
resultsMu.Unlock()
|
|
return
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
return
|
|
}
|
|
|
|
// GetClusterInfo - returns site replication information.
|
|
func (c *SiteReplicationSys) GetClusterInfo(ctx context.Context) (info madmin.SiteReplicationInfo, err error) {
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return info, nil
|
|
}
|
|
|
|
info.Enabled = true
|
|
info.Name = c.state.Name
|
|
info.Sites = make([]madmin.PeerInfo, 0, len(c.state.Peers))
|
|
for _, peer := range c.state.Peers {
|
|
info.Sites = append(info.Sites, peer)
|
|
}
|
|
sort.Slice(info.Sites, func(i, j int) bool {
|
|
return info.Sites[i].Name < info.Sites[j].Name
|
|
})
|
|
|
|
info.ServiceAccountAccessKey = c.state.ServiceAccountAccessKey
|
|
return info, nil
|
|
}
|
|
|
|
const (
|
|
makeBucketWithVersion = "MakeBucketWithVersioning"
|
|
configureReplication = "ConfigureReplication"
|
|
deleteBucket = "DeleteBucket"
|
|
replicateIAMItem = "SRPeerReplicateIAMItem"
|
|
replicateBucketMetadata = "SRPeerReplicateBucketMeta"
|
|
siteReplicationEdit = "SiteReplicationEdit"
|
|
)
|
|
|
|
// MakeBucketHook - called during a regular make bucket call when cluster
|
|
// replication is enabled. It is responsible for the creation of the same bucket
|
|
// on remote clusters, and creating replication rules on local and peer
|
|
// clusters.
|
|
func (c *SiteReplicationSys) MakeBucketHook(ctx context.Context, bucket string, opts MakeBucketOptions) error {
|
|
// At this point, the local bucket is created.
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
|
|
optsMap := make(map[string]string)
|
|
if opts.LockEnabled {
|
|
optsMap["lockEnabled"] = "true"
|
|
optsMap["versioningEnabled"] = "true"
|
|
}
|
|
if opts.VersioningEnabled {
|
|
optsMap["versioningEnabled"] = "true"
|
|
}
|
|
if opts.ForceCreate {
|
|
optsMap["forceCreate"] = "true"
|
|
}
|
|
createdAt, _ := globalBucketMetadataSys.CreatedAt(bucket)
|
|
optsMap["createdAt"] = createdAt.UTC().Format(time.RFC3339Nano)
|
|
opts.CreatedAt = createdAt
|
|
|
|
// Create bucket and enable versioning on all peers.
|
|
makeBucketConcErr := c.concDo(
|
|
func() error {
|
|
return c.annotateErr(makeBucketWithVersion, c.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts))
|
|
},
|
|
func(deploymentID string, p madmin.PeerInfo) error {
|
|
admClient, err := c.getAdminClient(ctx, deploymentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.annotatePeerErr(p.Name, makeBucketWithVersion, admClient.SRPeerBucketOps(ctx, bucket, madmin.MakeWithVersioningBktOp, optsMap))
|
|
},
|
|
makeBucketWithVersion,
|
|
)
|
|
|
|
// Create bucket remotes and add replication rules for the bucket on self and peers.
|
|
makeRemotesConcErr := c.concDo(
|
|
func() error {
|
|
return c.annotateErr(configureReplication, c.PeerBucketConfigureReplHandler(ctx, bucket))
|
|
},
|
|
func(deploymentID string, p madmin.PeerInfo) error {
|
|
admClient, err := c.getAdminClient(ctx, deploymentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.annotatePeerErr(p.Name, configureReplication, admClient.SRPeerBucketOps(ctx, bucket, madmin.ConfigureReplBktOp, nil))
|
|
},
|
|
configureReplication,
|
|
)
|
|
|
|
if err := errors.Unwrap(makeBucketConcErr); err != nil {
|
|
return err
|
|
}
|
|
return errors.Unwrap(makeRemotesConcErr)
|
|
}
|
|
|
|
// DeleteBucketHook - called during a regular delete bucket call when cluster
|
|
// replication is enabled. It is responsible for the deletion of the same bucket
|
|
// on remote clusters.
|
|
func (c *SiteReplicationSys) DeleteBucketHook(ctx context.Context, bucket string, forceDelete bool) error {
|
|
// At this point, the local bucket is deleted.
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
|
|
op := madmin.DeleteBucketBktOp
|
|
if forceDelete {
|
|
op = madmin.ForceDeleteBucketBktOp
|
|
}
|
|
|
|
// Send bucket delete to other clusters.
|
|
cerr := c.concDo(nil, func(deploymentID string, p madmin.PeerInfo) error {
|
|
admClient, err := c.getAdminClient(ctx, deploymentID)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
return c.annotatePeerErr(p.Name, deleteBucket, admClient.SRPeerBucketOps(ctx, bucket, op, nil))
|
|
},
|
|
deleteBucket,
|
|
)
|
|
return errors.Unwrap(cerr)
|
|
}
|
|
|
|
// PeerBucketMakeWithVersioningHandler - creates bucket and enables versioning.
|
|
func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Context, bucket string, opts MakeBucketOptions) error {
|
|
objAPI := newObjectLayerFn()
|
|
if objAPI == nil {
|
|
return errServerNotInitialized
|
|
}
|
|
|
|
err := objAPI.MakeBucket(ctx, bucket, opts)
|
|
if err != nil {
|
|
// Check if this is a bucket exists error.
|
|
_, ok1 := err.(BucketExists)
|
|
_, ok2 := err.(BucketAlreadyExists)
|
|
if !ok1 && !ok2 {
|
|
return wrapSRErr(c.annotateErr(makeBucketWithVersion, err))
|
|
}
|
|
} else {
|
|
// Load updated bucket metadata into memory as new
|
|
// bucket was created.
|
|
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
|
}
|
|
|
|
meta, err := globalBucketMetadataSys.Get(bucket)
|
|
if err != nil {
|
|
return wrapSRErr(c.annotateErr(makeBucketWithVersion, err))
|
|
}
|
|
|
|
meta.SetCreatedAt(opts.CreatedAt)
|
|
|
|
meta.VersioningConfigXML = enabledBucketVersioningConfig
|
|
if opts.LockEnabled {
|
|
meta.ObjectLockConfigXML = enabledBucketObjectLockConfig
|
|
}
|
|
|
|
if err := meta.Save(context.Background(), objAPI); err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
globalBucketMetadataSys.Set(bucket, meta)
|
|
|
|
// Load updated bucket metadata into memory as new metadata updated.
|
|
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
|
return nil
|
|
}
|
|
|
|
// PeerBucketConfigureReplHandler - configures replication remote and
|
|
// replication rules to all other peers for the local bucket.
|
|
func (c *SiteReplicationSys) PeerBucketConfigureReplHandler(ctx context.Context, bucket string) error {
|
|
creds, err := c.getPeerCreds()
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
// The following function, creates a bucket remote and sets up a bucket
|
|
// replication rule for the given peer.
|
|
configurePeerFn := func(d string, peer madmin.PeerInfo) error {
|
|
// Create bucket replication rule to this peer.
|
|
|
|
// To add the bucket replication rule, we fetch the current
|
|
// server configuration, and convert it to minio-go's
|
|
// replication configuration type (by converting to xml and
|
|
// parsing it back), use minio-go's add rule function, and
|
|
// finally convert it back to the server type (again via xml).
|
|
// This is needed as there is no add-rule function in the server
|
|
// yet.
|
|
|
|
// Though we do not check if the rule already exists, this is
|
|
// not a problem as we are always using the same replication
|
|
// rule ID - if the rule already exists, it is just replaced.
|
|
replicationConfigS, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
|
if err != nil {
|
|
_, ok := err.(BucketReplicationConfigNotFound)
|
|
if !ok {
|
|
return err
|
|
}
|
|
}
|
|
var replicationConfig replication.Config
|
|
if replicationConfigS != nil {
|
|
replCfgSBytes, err := xml.Marshal(replicationConfigS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = xml.Unmarshal(replCfgSBytes, &replicationConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var (
|
|
ruleID = fmt.Sprintf("site-repl-%s", d)
|
|
hasRule bool
|
|
)
|
|
var ruleARN string
|
|
for _, r := range replicationConfig.Rules {
|
|
if r.ID == ruleID {
|
|
hasRule = true
|
|
ruleARN = r.Destination.Bucket
|
|
}
|
|
}
|
|
|
|
ep, _ := url.Parse(peer.Endpoint)
|
|
var targets []madmin.BucketTarget
|
|
if targetsPtr, _ := globalBucketTargetSys.ListBucketTargets(ctx, bucket); targetsPtr != nil {
|
|
targets = targetsPtr.Targets
|
|
}
|
|
targetARN := ""
|
|
var updateTgt, updateBW bool
|
|
var targetToUpdate madmin.BucketTarget
|
|
for _, target := range targets {
|
|
if target.Arn == ruleARN {
|
|
targetARN = ruleARN
|
|
updateBW = peer.DefaultBandwidth.Limit != 0 && target.BandwidthLimit == 0
|
|
if (target.URL().String() != peer.Endpoint) || updateBW {
|
|
updateTgt = true
|
|
targetToUpdate = target
|
|
}
|
|
break
|
|
}
|
|
}
|
|
// replication config had a stale target ARN - update the endpoint
|
|
if updateTgt {
|
|
targetToUpdate.Endpoint = ep.Host
|
|
targetToUpdate.Secure = ep.Scheme == "https"
|
|
targetToUpdate.Credentials = &madmin.Credentials{
|
|
AccessKey: creds.AccessKey,
|
|
SecretKey: creds.SecretKey,
|
|
}
|
|
if !peer.SyncState.Empty() {
|
|
targetToUpdate.ReplicationSync = (peer.SyncState == madmin.SyncEnabled)
|
|
}
|
|
if updateBW {
|
|
targetToUpdate.BandwidthLimit = int64(peer.DefaultBandwidth.Limit)
|
|
}
|
|
err := globalBucketTargetSys.SetTarget(ctx, bucket, &targetToUpdate, true)
|
|
if err != nil {
|
|
return c.annotatePeerErr(peer.Name, "Bucket target update error", err)
|
|
}
|
|
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
tgtBytes, err := json.Marshal(&targets)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
}
|
|
// no replication rule for this peer or target ARN missing in bucket targets
|
|
if targetARN == "" {
|
|
bucketTarget := madmin.BucketTarget{
|
|
SourceBucket: bucket,
|
|
Endpoint: ep.Host,
|
|
Credentials: &madmin.Credentials{
|
|
AccessKey: creds.AccessKey,
|
|
SecretKey: creds.SecretKey,
|
|
},
|
|
TargetBucket: bucket,
|
|
Secure: ep.Scheme == "https",
|
|
API: "s3v4",
|
|
Type: madmin.ReplicationService,
|
|
Region: "",
|
|
ReplicationSync: peer.SyncState == madmin.SyncEnabled,
|
|
DeploymentID: d,
|
|
BandwidthLimit: int64(peer.DefaultBandwidth.Limit),
|
|
}
|
|
var exists bool // true if ARN already exists
|
|
bucketTarget.Arn, exists = globalBucketTargetSys.getRemoteARN(bucket, &bucketTarget, peer.DeploymentID)
|
|
if !exists { // persist newly generated ARN to targets and metadata on disk
|
|
err := globalBucketTargetSys.SetTarget(ctx, bucket, &bucketTarget, false)
|
|
if err != nil {
|
|
return c.annotatePeerErr(peer.Name, "Bucket target creation error", err)
|
|
}
|
|
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tgtBytes, err := json.Marshal(&targets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
targetARN = bucketTarget.Arn
|
|
}
|
|
opts := replication.Options{
|
|
// Set the ID so we can identify the rule as being
|
|
// created for site-replication and include the
|
|
// destination cluster's deployment ID.
|
|
ID: ruleID,
|
|
|
|
// Use a helper to generate unique priority numbers.
|
|
Priority: fmt.Sprintf("%d", getPriorityHelper(replicationConfig)),
|
|
|
|
Op: replication.AddOption,
|
|
RuleStatus: "enable",
|
|
DestBucket: targetARN,
|
|
// Replicate everything!
|
|
ReplicateDeletes: "enable",
|
|
ReplicateDeleteMarkers: "enable",
|
|
ReplicaSync: "enable",
|
|
ExistingObjectReplicate: "enable",
|
|
}
|
|
|
|
switch {
|
|
case hasRule:
|
|
if ruleARN != opts.DestBucket {
|
|
// remove stale replication rule and replace rule with correct target ARN
|
|
if len(replicationConfig.Rules) > 1 {
|
|
err = replicationConfig.RemoveRule(opts)
|
|
} else {
|
|
replicationConfig = replication.Config{}
|
|
}
|
|
if err == nil {
|
|
err = replicationConfig.AddRule(opts)
|
|
}
|
|
} else {
|
|
err = replicationConfig.EditRule(opts)
|
|
}
|
|
default:
|
|
err = replicationConfig.AddRule(opts)
|
|
}
|
|
if err != nil {
|
|
return c.annotatePeerErr(peer.Name, "Error adding bucket replication rule", err)
|
|
}
|
|
|
|
// Now convert the configuration back to server's type so we can
|
|
// do some validation.
|
|
newReplCfgBytes, err := xml.Marshal(replicationConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newReplicationConfig, err := sreplication.ParseConfig(bytes.NewReader(newReplCfgBytes))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, newReplicationConfig, &validateReplicationDestinationOptions{CheckRemoteBucket: true})
|
|
if apiErr != noError {
|
|
return fmt.Errorf("bucket replication config validation error: %#v", apiErr)
|
|
}
|
|
err = newReplicationConfig.Validate(bucket, sameTarget)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Config looks good, so we save it.
|
|
replCfgData, err := xml.Marshal(newReplicationConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, replCfgData)
|
|
return c.annotatePeerErr(peer.Name, "Error updating replication configuration", err)
|
|
}
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
errMap := make(map[string]error, len(c.state.Peers))
|
|
for d, peer := range c.state.Peers {
|
|
if d == globalDeploymentID() {
|
|
continue
|
|
}
|
|
errMap[d] = configurePeerFn(d, peer)
|
|
}
|
|
return c.toErrorFromErrMap(errMap, configureReplication)
|
|
}
|
|
|
|
// PeerBucketDeleteHandler - deletes bucket on local in response to a delete
|
|
// bucket request from a peer.
|
|
func (c *SiteReplicationSys) PeerBucketDeleteHandler(ctx context.Context, bucket string, opts DeleteBucketOptions) error {
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return errSRNotEnabled
|
|
}
|
|
|
|
objAPI := newObjectLayerFn()
|
|
if objAPI == nil {
|
|
return errServerNotInitialized
|
|
}
|
|
|
|
if globalDNSConfig != nil {
|
|
if err := globalDNSConfig.Delete(bucket); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err := objAPI.DeleteBucket(ctx, bucket, opts)
|
|
if err != nil {
|
|
if globalDNSConfig != nil {
|
|
if err2 := globalDNSConfig.Put(bucket); err2 != nil {
|
|
replLogIf(ctx, fmt.Errorf("Unable to restore bucket DNS entry %w, please fix it manually", err2))
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
globalNotificationSys.DeleteBucketMetadata(ctx, bucket)
|
|
|
|
return nil
|
|
}
|
|
|
|
// IAMChangeHook - called when IAM items need to be replicated to peer clusters.
|
|
// This includes named policy creation, policy mapping changes and service
|
|
// account changes.
|
|
//
|
|
// All policies are replicated.
|
|
//
|
|
// Policy mappings are only replicated when they are for LDAP users or groups
|
|
// (as an external IDP is always assumed when SR is used). In the case of
|
|
// OpenID, such mappings are provided from the IDP directly and so are not
|
|
// applicable here.
|
|
//
|
|
// Service accounts are replicated as long as they are not meant for the root
|
|
// user.
|
|
//
|
|
// STS accounts are replicated, but only if the session token is verifiable
|
|
// using the local cluster's root credential.
|
|
func (c *SiteReplicationSys) IAMChangeHook(ctx context.Context, item madmin.SRIAMItem) error {
|
|
// The IAM item has already been applied to the local cluster at this
|
|
// point, and only needs to be updated on all remote peer clusters.
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
|
|
cerr := c.concDo(nil, func(d string, p madmin.PeerInfo) error {
|
|
admClient, err := c.getAdminClient(ctx, d)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
return c.annotatePeerErr(p.Name, replicateIAMItem, admClient.SRPeerReplicateIAMItem(ctx, item))
|
|
},
|
|
replicateIAMItem,
|
|
)
|
|
return errors.Unwrap(cerr)
|
|
}
|
|
|
|
// PeerAddPolicyHandler - copies IAM policy to local. A nil policy argument,
|
|
// causes the named policy to be deleted.
|
|
func (c *SiteReplicationSys) PeerAddPolicyHandler(ctx context.Context, policyName string, p *policy.Policy, updatedAt time.Time) error {
|
|
var err error
|
|
// skip overwrite of local update if peer sent stale info
|
|
if !updatedAt.IsZero() {
|
|
if p, err := globalIAMSys.store.GetPolicyDoc(policyName); err == nil && p.UpdateDate.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
if p == nil {
|
|
err = globalIAMSys.DeletePolicy(ctx, policyName, true)
|
|
} else {
|
|
_, err = globalIAMSys.SetPolicy(ctx, policyName, *p)
|
|
}
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PeerIAMUserChangeHandler - copies IAM user to local.
|
|
func (c *SiteReplicationSys) PeerIAMUserChangeHandler(ctx context.Context, change *madmin.SRIAMUser, updatedAt time.Time) error {
|
|
if change == nil {
|
|
return errSRInvalidRequest(errInvalidArgument)
|
|
}
|
|
// skip overwrite of local update if peer sent stale info
|
|
if !updatedAt.IsZero() {
|
|
if ui, err := globalIAMSys.GetUserInfo(ctx, change.AccessKey); err == nil && ui.UpdatedAt.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var err error
|
|
if change.IsDeleteReq {
|
|
err = globalIAMSys.DeleteUser(ctx, change.AccessKey, true)
|
|
} else {
|
|
if change.UserReq == nil {
|
|
return errSRInvalidRequest(errInvalidArgument)
|
|
}
|
|
userReq := *change.UserReq
|
|
if userReq.Status != "" && userReq.SecretKey == "" {
|
|
// Status is set without secretKey updates means we are
|
|
// only changing the account status.
|
|
_, err = globalIAMSys.SetUserStatus(ctx, change.AccessKey, userReq.Status)
|
|
} else {
|
|
// We don't allow internal user creation with LDAP enabled for now
|
|
// (both sites must have LDAP disabled).
|
|
if globalIAMSys.LDAPConfig.Enabled() {
|
|
err = errIAMActionNotAllowed
|
|
} else {
|
|
_, err = globalIAMSys.CreateUser(ctx, change.AccessKey, userReq)
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PeerGroupInfoChangeHandler - copies group changes to local.
|
|
func (c *SiteReplicationSys) PeerGroupInfoChangeHandler(ctx context.Context, change *madmin.SRGroupInfo, updatedAt time.Time) error {
|
|
if change == nil {
|
|
return errSRInvalidRequest(errInvalidArgument)
|
|
}
|
|
updReq := change.UpdateReq
|
|
var err error
|
|
|
|
// skip overwrite of local update if peer sent stale info
|
|
if !updatedAt.IsZero() {
|
|
if gd, err := globalIAMSys.GetGroupDescription(updReq.Group); err == nil && gd.UpdatedAt.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if updReq.IsRemove {
|
|
_, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
|
|
} else {
|
|
if updReq.Status != "" && len(updReq.Members) == 0 {
|
|
_, err = globalIAMSys.SetGroupStatus(ctx, updReq.Group, updReq.Status == madmin.GroupEnabled)
|
|
} else {
|
|
if globalIAMSys.LDAPConfig.Enabled() {
|
|
// We don't allow internal group manipulation in this API when
|
|
// LDAP is enabled for now (both sites must have LDAP disabled).
|
|
err = errIAMActionNotAllowed
|
|
} else {
|
|
_, err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members)
|
|
}
|
|
if err == nil && updReq.Status != "" {
|
|
_, err = globalIAMSys.SetGroupStatus(ctx, updReq.Group, updReq.Status == madmin.GroupEnabled)
|
|
}
|
|
}
|
|
}
|
|
if err != nil && !errors.Is(err, errNoSuchGroup) {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PeerSvcAccChangeHandler - copies service-account change to local.
|
|
func (c *SiteReplicationSys) PeerSvcAccChangeHandler(ctx context.Context, change *madmin.SRSvcAccChange, updatedAt time.Time) error {
|
|
if change == nil {
|
|
return errSRInvalidRequest(errInvalidArgument)
|
|
}
|
|
switch {
|
|
case change.Create != nil:
|
|
var sp *policy.Policy
|
|
var err error
|
|
if len(change.Create.SessionPolicy) > 0 {
|
|
sp, err = policy.ParseConfig(bytes.NewReader(change.Create.SessionPolicy))
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
}
|
|
// skip overwrite of local update if peer sent stale info
|
|
if !updatedAt.IsZero() && change.Create.AccessKey != "" {
|
|
if sa, _, err := globalIAMSys.getServiceAccount(ctx, change.Create.AccessKey); err == nil && sa.UpdatedAt.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
opts := newServiceAccountOpts{
|
|
accessKey: change.Create.AccessKey,
|
|
secretKey: change.Create.SecretKey,
|
|
sessionPolicy: sp,
|
|
claims: change.Create.Claims,
|
|
name: change.Create.Name,
|
|
description: change.Create.Description,
|
|
expiration: change.Create.Expiration,
|
|
}
|
|
_, _, err = globalIAMSys.NewServiceAccount(ctx, change.Create.Parent, change.Create.Groups, opts)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
case change.Update != nil:
|
|
var sp *policy.Policy
|
|
var err error
|
|
if len(change.Update.SessionPolicy) > 0 {
|
|
sp, err = policy.ParseConfig(bytes.NewReader(change.Update.SessionPolicy))
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
}
|
|
// skip overwrite of local update if peer sent stale info
|
|
if !updatedAt.IsZero() {
|
|
if sa, _, err := globalIAMSys.getServiceAccount(ctx, change.Update.AccessKey); err == nil && sa.UpdatedAt.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
opts := updateServiceAccountOpts{
|
|
secretKey: change.Update.SecretKey,
|
|
status: change.Update.Status,
|
|
name: change.Update.Name,
|
|
description: change.Update.Description,
|
|
sessionPolicy: sp,
|
|
expiration: change.Update.Expiration,
|
|
}
|
|
|
|
_, err = globalIAMSys.UpdateServiceAccount(ctx, change.Update.AccessKey, opts)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
case change.Delete != nil:
|
|
// skip overwrite of local update if peer sent stale info
|
|
if !updatedAt.IsZero() {
|
|
if sa, _, err := globalIAMSys.getServiceAccount(ctx, change.Delete.AccessKey); err == nil && sa.UpdatedAt.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
if err := globalIAMSys.DeleteServiceAccount(ctx, change.Delete.AccessKey, true); err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PeerPolicyMappingHandler - copies policy mapping to local.
|
|
func (c *SiteReplicationSys) PeerPolicyMappingHandler(ctx context.Context, mapping *madmin.SRPolicyMapping, updatedAt time.Time) error {
|
|
if mapping == nil {
|
|
return errSRInvalidRequest(errInvalidArgument)
|
|
}
|
|
// skip overwrite of local update if peer sent stale info
|
|
if !updatedAt.IsZero() {
|
|
mp, ok := globalIAMSys.store.GetMappedPolicy(mapping.Policy, mapping.IsGroup)
|
|
if ok && mp.UpdatedAt.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// When LDAP is enabled, we verify that the user or group exists in LDAP and
|
|
// use the normalized form of the entityName (which will be an LDAP DN).
|
|
userType := IAMUserType(mapping.UserType)
|
|
isGroup := mapping.IsGroup
|
|
entityName := mapping.UserOrGroup
|
|
if globalIAMSys.GetUsersSysType() == LDAPUsersSysType && userType == stsUser {
|
|
|
|
// Validate that the user or group exists in LDAP and use the normalized
|
|
// form of the entityName (which will be an LDAP DN).
|
|
var err error
|
|
if isGroup {
|
|
var foundGroupDN *xldap.DNSearchResult
|
|
var underBaseDN bool
|
|
if foundGroupDN, underBaseDN, err = globalIAMSys.LDAPConfig.GetValidatedGroupDN(nil, entityName); err != nil {
|
|
iamLogIf(ctx, err)
|
|
} else if foundGroupDN == nil || !underBaseDN {
|
|
err = errNoSuchGroup
|
|
}
|
|
entityName = foundGroupDN.NormDN
|
|
} else {
|
|
var foundUserDN *xldap.DNSearchResult
|
|
if foundUserDN, err = globalIAMSys.LDAPConfig.GetValidatedDNForUsername(entityName); err != nil {
|
|
iamLogIf(ctx, err)
|
|
} else if foundUserDN == nil {
|
|
err = errNoSuchUser
|
|
}
|
|
entityName = foundUserDN.NormDN
|
|
}
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
}
|
|
|
|
_, err := globalIAMSys.PolicyDBSet(ctx, entityName, mapping.Policy, userType, isGroup)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PeerSTSAccHandler - replicates STS credential locally.
|
|
func (c *SiteReplicationSys) PeerSTSAccHandler(ctx context.Context, stsCred *madmin.SRSTSCredential, updatedAt time.Time) error {
|
|
if stsCred == nil {
|
|
return errSRInvalidRequest(errInvalidArgument)
|
|
}
|
|
// skip overwrite of local update if peer sent stale info
|
|
if !updatedAt.IsZero() {
|
|
if u, _, err := globalIAMSys.getTempAccount(ctx, stsCred.AccessKey); err == nil {
|
|
if u.UpdatedAt.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
secretKey, err := getTokenSigningKey()
|
|
if err != nil {
|
|
return errSRInvalidRequest(err)
|
|
}
|
|
|
|
// Verify the session token of the stsCred
|
|
claims, err := auth.ExtractClaims(stsCred.SessionToken, secretKey)
|
|
if err != nil {
|
|
return fmt.Errorf("STS credential could not be verified: %w", err)
|
|
}
|
|
|
|
mapClaims := claims.Map()
|
|
expiry, err := auth.ExpToInt64(mapClaims["exp"])
|
|
if err != nil {
|
|
return fmt.Errorf("Expiry claim was not found: %v: %w", mapClaims, err)
|
|
}
|
|
|
|
cred := auth.Credentials{
|
|
AccessKey: stsCred.AccessKey,
|
|
SecretKey: stsCred.SecretKey,
|
|
Expiration: time.Unix(expiry, 0).UTC(),
|
|
SessionToken: stsCred.SessionToken,
|
|
ParentUser: stsCred.ParentUser,
|
|
Status: auth.AccountOn,
|
|
}
|
|
|
|
// Extract the username and lookup DN and groups in LDAP.
|
|
ldapUser, isLDAPSTS := claims.Lookup(ldapUserN)
|
|
if isLDAPSTS {
|
|
// Need to lookup the groups from LDAP.
|
|
_, ldapGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(ldapUser)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to query LDAP server for %s: %w", ldapUser, err)
|
|
}
|
|
|
|
cred.Groups = ldapGroups
|
|
}
|
|
|
|
// Set these credentials to IAM.
|
|
if _, err := globalIAMSys.SetTempUser(ctx, cred.AccessKey, cred, stsCred.ParentPolicyMapping); err != nil {
|
|
return fmt.Errorf("unable to save STS credential and/or parent policy mapping: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// BucketMetaHook - called when bucket meta changes happen and need to be
|
|
// replicated to peer clusters.
|
|
func (c *SiteReplicationSys) BucketMetaHook(ctx context.Context, item madmin.SRBucketMeta) error {
|
|
// The change has already been applied to the local cluster at this
|
|
// point, and only needs to be updated on all remote peer clusters.
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
|
|
cerr := c.concDo(nil, func(d string, p madmin.PeerInfo) error {
|
|
admClient, err := c.getAdminClient(ctx, d)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
return c.annotatePeerErr(p.Name, replicateBucketMetadata, admClient.SRPeerReplicateBucketMeta(ctx, item))
|
|
},
|
|
replicateBucketMetadata,
|
|
)
|
|
return errors.Unwrap(cerr)
|
|
}
|
|
|
|
// PeerBucketVersioningHandler - updates versioning config to local cluster.
|
|
func (c *SiteReplicationSys) PeerBucketVersioningHandler(ctx context.Context, bucket string, versioning *string, updatedAt time.Time) error {
|
|
if versioning != nil {
|
|
// skip overwrite if local update is newer than peer update.
|
|
if !updatedAt.IsZero() {
|
|
if _, updateTm, err := globalBucketMetadataSys.GetVersioningConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
configData, err := base64.StdEncoding.DecodeString(*versioning)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, configData)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PeerBucketMetadataUpdateHandler - merges the bucket metadata, save and ping other nodes
|
|
func (c *SiteReplicationSys) PeerBucketMetadataUpdateHandler(ctx context.Context, item madmin.SRBucketMeta) error {
|
|
objectAPI := newObjectLayerFn()
|
|
if objectAPI == nil {
|
|
return errSRObjectLayerNotReady
|
|
}
|
|
|
|
if item.Bucket == "" || item.UpdatedAt.IsZero() {
|
|
return wrapSRErr(errInvalidArgument)
|
|
}
|
|
|
|
meta, err := readBucketMetadata(ctx, objectAPI, item.Bucket)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
if meta.Created.After(item.UpdatedAt) {
|
|
return nil
|
|
}
|
|
|
|
if item.Policy != nil {
|
|
meta.PolicyConfigJSON = item.Policy
|
|
meta.PolicyConfigUpdatedAt = item.UpdatedAt
|
|
}
|
|
|
|
if item.Versioning != nil {
|
|
configData, err := base64.StdEncoding.DecodeString(*item.Versioning)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
meta.VersioningConfigXML = configData
|
|
meta.VersioningConfigUpdatedAt = item.UpdatedAt
|
|
}
|
|
|
|
if item.Tags != nil {
|
|
configData, err := base64.StdEncoding.DecodeString(*item.Tags)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
meta.TaggingConfigXML = configData
|
|
meta.TaggingConfigUpdatedAt = item.UpdatedAt
|
|
}
|
|
|
|
if item.ObjectLockConfig != nil {
|
|
configData, err := base64.StdEncoding.DecodeString(*item.ObjectLockConfig)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
meta.ObjectLockConfigXML = configData
|
|
meta.ObjectLockConfigUpdatedAt = item.UpdatedAt
|
|
}
|
|
|
|
if item.SSEConfig != nil {
|
|
configData, err := base64.StdEncoding.DecodeString(*item.SSEConfig)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
meta.EncryptionConfigXML = configData
|
|
meta.EncryptionConfigUpdatedAt = item.UpdatedAt
|
|
}
|
|
|
|
if item.Quota != nil {
|
|
meta.QuotaConfigJSON = item.Quota
|
|
meta.QuotaConfigUpdatedAt = item.UpdatedAt
|
|
}
|
|
|
|
return globalBucketMetadataSys.save(ctx, meta)
|
|
}
|
|
|
|
// PeerBucketPolicyHandler - copies/deletes policy to local cluster.
|
|
func (c *SiteReplicationSys) PeerBucketPolicyHandler(ctx context.Context, bucket string, policy *policy.BucketPolicy, updatedAt time.Time) error {
|
|
// skip overwrite if local update is newer than peer update.
|
|
if !updatedAt.IsZero() {
|
|
if _, updateTm, err := globalBucketMetadataSys.GetPolicyConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if policy != nil {
|
|
configData, err := json.Marshal(policy)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, configData)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Delete the bucket policy
|
|
_, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketPolicyConfig)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PeerBucketTaggingHandler - copies/deletes tags to local cluster.
|
|
func (c *SiteReplicationSys) PeerBucketTaggingHandler(ctx context.Context, bucket string, tags *string, updatedAt time.Time) error {
|
|
// skip overwrite if local update is newer than peer update.
|
|
if !updatedAt.IsZero() {
|
|
if _, updateTm, err := globalBucketMetadataSys.GetTaggingConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if tags != nil {
|
|
configData, err := base64.StdEncoding.DecodeString(*tags)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, configData)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Delete the tags
|
|
_, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketTaggingConfig)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PeerBucketObjectLockConfigHandler - sets object lock on local bucket.
|
|
func (c *SiteReplicationSys) PeerBucketObjectLockConfigHandler(ctx context.Context, bucket string, objectLockData *string, updatedAt time.Time) error {
|
|
if objectLockData != nil {
|
|
// skip overwrite if local update is newer than peer update.
|
|
if !updatedAt.IsZero() {
|
|
if _, updateTm, err := globalBucketMetadataSys.GetObjectLockConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
configData, err := base64.StdEncoding.DecodeString(*objectLockData)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
_, err = globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, configData)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PeerBucketSSEConfigHandler - copies/deletes SSE config to local cluster.
|
|
func (c *SiteReplicationSys) PeerBucketSSEConfigHandler(ctx context.Context, bucket string, sseConfig *string, updatedAt time.Time) error {
|
|
// skip overwrite if local update is newer than peer update.
|
|
if !updatedAt.IsZero() {
|
|
if _, updateTm, err := globalBucketMetadataSys.GetSSEConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if sseConfig != nil {
|
|
configData, err := base64.StdEncoding.DecodeString(*sseConfig)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, configData)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Delete sse config
|
|
_, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketSSEConfig)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PeerBucketQuotaConfigHandler - copies/deletes policy to local cluster.
|
|
func (c *SiteReplicationSys) PeerBucketQuotaConfigHandler(ctx context.Context, bucket string, quota *madmin.BucketQuota, updatedAt time.Time) error {
|
|
// skip overwrite if local update is newer than peer update.
|
|
if !updatedAt.IsZero() {
|
|
if _, updateTm, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket); err == nil && updateTm.After(updatedAt) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if quota != nil {
|
|
quotaData, err := json.Marshal(quota)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, quotaData); err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete the bucket policy
|
|
_, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketQuotaConfigFile)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PeerBucketLCConfigHandler - copies/deletes lifecycle config to local cluster
|
|
func (c *SiteReplicationSys) PeerBucketLCConfigHandler(ctx context.Context, bucket string, expLCConfig *string, updatedAt time.Time) error {
|
|
// skip overwrite if local update is newer than peer update.
|
|
if !updatedAt.IsZero() {
|
|
if cfg, _, err := globalBucketMetadataSys.GetLifecycleConfig(bucket); err == nil && (cfg.ExpiryUpdatedAt != nil && cfg.ExpiryUpdatedAt.After(updatedAt)) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if expLCConfig != nil {
|
|
configData, err := mergeWithCurrentLCConfig(ctx, bucket, expLCConfig, updatedAt)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, configData)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Delete ILM config
|
|
_, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketLifecycleConfig)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getAdminClient - NOTE: ensure to take at least a read lock on SiteReplicationSys
|
|
// before calling this.
|
|
func (c *SiteReplicationSys) getAdminClient(ctx context.Context, deploymentID string) (*madmin.AdminClient, error) {
|
|
creds, err := c.getPeerCreds()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
peer, ok := c.state.Peers[deploymentID]
|
|
if !ok {
|
|
return nil, errSRPeerNotFound
|
|
}
|
|
|
|
return getAdminClient(peer.Endpoint, creds.AccessKey, creds.SecretKey)
|
|
}
|
|
|
|
// getAdminClientWithEndpoint - NOTE: ensure to take at least a read lock on SiteReplicationSys
|
|
// before calling this.
|
|
func (c *SiteReplicationSys) getAdminClientWithEndpoint(ctx context.Context, deploymentID, endpoint string) (*madmin.AdminClient, error) {
|
|
creds, err := c.getPeerCreds()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, ok := c.state.Peers[deploymentID]; !ok {
|
|
return nil, errSRPeerNotFound
|
|
}
|
|
return getAdminClient(endpoint, creds.AccessKey, creds.SecretKey)
|
|
}
|
|
|
|
func (c *SiteReplicationSys) getPeerCreds() (*auth.Credentials, error) {
|
|
u, ok := globalIAMSys.store.GetUser(c.state.ServiceAccountAccessKey)
|
|
if !ok {
|
|
return nil, errors.New("site replication service account not found")
|
|
}
|
|
return &u.Credentials, nil
|
|
}
|
|
|
|
// listBuckets returns a consistent common view of latest unique buckets across
|
|
// sites, this is used for replication.
|
|
func (c *SiteReplicationSys) listBuckets(ctx context.Context) ([]BucketInfo, error) {
|
|
// If local has buckets, enable versioning on them, create them on peers
|
|
// and setup replication rules.
|
|
objAPI := newObjectLayerFn()
|
|
if objAPI == nil {
|
|
return nil, errSRObjectLayerNotReady
|
|
}
|
|
return objAPI.ListBuckets(ctx, BucketOptions{Deleted: true})
|
|
}
|
|
|
|
// syncToAllPeers is used for syncing local data to all remote peers, it is
|
|
// called once during initial "AddPeerClusters" request.
|
|
func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context, addOpts madmin.SRAddOptions) error {
|
|
objAPI := newObjectLayerFn()
|
|
if objAPI == nil {
|
|
return errSRObjectLayerNotReady
|
|
}
|
|
|
|
buckets, err := objAPI.ListBuckets(ctx, BucketOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, bucketInfo := range buckets {
|
|
bucket := bucketInfo.Name
|
|
|
|
meta, err := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket)
|
|
if err != nil && !errors.Is(err, errConfigNotFound) {
|
|
return errSRBackendIssue(err)
|
|
}
|
|
|
|
opts := MakeBucketOptions{
|
|
LockEnabled: meta.ObjectLocking(),
|
|
CreatedAt: bucketInfo.Created.UTC(),
|
|
}
|
|
|
|
// Now call the MakeBucketHook on existing bucket - this will
|
|
// create buckets and replication rules on peer clusters.
|
|
if err = c.MakeBucketHook(ctx, bucket, opts); err != nil {
|
|
return errSRBucketConfigError(err)
|
|
}
|
|
|
|
// Replicate bucket policy if present.
|
|
policyJSON, tm := meta.PolicyConfigJSON, meta.PolicyConfigUpdatedAt
|
|
if len(policyJSON) > 0 {
|
|
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypePolicy,
|
|
Bucket: bucket,
|
|
Policy: policyJSON,
|
|
UpdatedAt: tm,
|
|
})
|
|
if err != nil {
|
|
return errSRBucketMetaError(err)
|
|
}
|
|
}
|
|
|
|
// Replicate bucket tags if present.
|
|
tagCfg, tm := meta.TaggingConfigXML, meta.TaggingConfigUpdatedAt
|
|
if len(tagCfg) > 0 {
|
|
tagCfgStr := base64.StdEncoding.EncodeToString(tagCfg)
|
|
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypeTags,
|
|
Bucket: bucket,
|
|
Tags: &tagCfgStr,
|
|
UpdatedAt: tm,
|
|
})
|
|
if err != nil {
|
|
return errSRBucketMetaError(err)
|
|
}
|
|
}
|
|
|
|
// Replicate object-lock config if present.
|
|
objLockCfgData, tm := meta.ObjectLockConfigXML, meta.ObjectLockConfigUpdatedAt
|
|
if len(objLockCfgData) > 0 {
|
|
objLockStr := base64.StdEncoding.EncodeToString(objLockCfgData)
|
|
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
|
Bucket: bucket,
|
|
Tags: &objLockStr,
|
|
UpdatedAt: tm,
|
|
})
|
|
if err != nil {
|
|
return errSRBucketMetaError(err)
|
|
}
|
|
}
|
|
|
|
// Replicate existing bucket bucket encryption settings
|
|
sseConfigData, tm := meta.EncryptionConfigXML, meta.EncryptionConfigUpdatedAt
|
|
if len(sseConfigData) > 0 {
|
|
sseConfigStr := base64.StdEncoding.EncodeToString(sseConfigData)
|
|
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypeSSEConfig,
|
|
Bucket: bucket,
|
|
SSEConfig: &sseConfigStr,
|
|
UpdatedAt: tm,
|
|
})
|
|
if err != nil {
|
|
return errSRBucketMetaError(err)
|
|
}
|
|
}
|
|
|
|
// Replicate existing bucket quotas settings
|
|
quotaConfigJSON, tm := meta.QuotaConfigJSON, meta.QuotaConfigUpdatedAt
|
|
if len(quotaConfigJSON) > 0 {
|
|
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
|
Bucket: bucket,
|
|
Quota: quotaConfigJSON,
|
|
UpdatedAt: tm,
|
|
})
|
|
if err != nil {
|
|
return errSRBucketMetaError(err)
|
|
}
|
|
}
|
|
|
|
// Replicate ILM expiry rules if needed
|
|
if addOpts.ReplicateILMExpiry && (meta.lifecycleConfig != nil && meta.lifecycleConfig.HasExpiry()) {
|
|
var expLclCfg lifecycle.Lifecycle
|
|
expLclCfg.XMLName = meta.lifecycleConfig.XMLName
|
|
for _, rule := range meta.lifecycleConfig.Rules {
|
|
if !rule.Expiration.IsNull() || !rule.NoncurrentVersionExpiration.IsNull() {
|
|
// copy the non transition details of the rule
|
|
expLclCfg.Rules = append(expLclCfg.Rules, rule.CloneNonTransition())
|
|
}
|
|
}
|
|
currtime := time.Now()
|
|
expLclCfg.ExpiryUpdatedAt = &currtime
|
|
ilmConfigData, err := xml.Marshal(expLclCfg)
|
|
if err != nil {
|
|
return errSRBucketMetaError(err)
|
|
}
|
|
if len(ilmConfigData) > 0 {
|
|
configStr := base64.StdEncoding.EncodeToString(ilmConfigData)
|
|
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaLCConfig,
|
|
Bucket: bucket,
|
|
ExpiryLCConfig: &configStr,
|
|
UpdatedAt: time.Now(),
|
|
})
|
|
if err != nil {
|
|
return errSRBucketMetaError(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Order matters from now on how the information is
|
|
// synced to remote sites.
|
|
|
|
// Policies should be synced first.
|
|
{
|
|
// Replicate IAM policies on local to all peers.
|
|
allPolicyDocs, err := globalIAMSys.ListPolicyDocs(ctx, "")
|
|
if err != nil {
|
|
return errSRBackendIssue(err)
|
|
}
|
|
|
|
for pname, pdoc := range allPolicyDocs {
|
|
policyJSON, err := json.Marshal(pdoc.Policy)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
err = c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicy,
|
|
Name: pname,
|
|
Policy: policyJSON,
|
|
UpdatedAt: pdoc.UpdateDate,
|
|
})
|
|
if err != nil {
|
|
return errSRIAMError(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Next should be userAccounts those are local users, OIDC and LDAP will not
|
|
// may not have any local users.
|
|
{
|
|
userAccounts := make(map[string]UserIdentity)
|
|
err := globalIAMSys.store.loadUsers(ctx, regUser, userAccounts)
|
|
if err != nil {
|
|
return errSRBackendIssue(err)
|
|
}
|
|
|
|
for _, acc := range userAccounts {
|
|
if err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemIAMUser,
|
|
IAMUser: &madmin.SRIAMUser{
|
|
AccessKey: acc.Credentials.AccessKey,
|
|
IsDeleteReq: false,
|
|
UserReq: &madmin.AddOrUpdateUserReq{
|
|
SecretKey: acc.Credentials.SecretKey,
|
|
Status: madmin.AccountStatus(acc.Credentials.Status),
|
|
},
|
|
},
|
|
UpdatedAt: acc.UpdatedAt,
|
|
}); err != nil {
|
|
return errSRIAMError(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Next should be Groups for some of these users, LDAP might have some Group
|
|
// DNs here
|
|
{
|
|
groups := make(map[string]GroupInfo)
|
|
err := globalIAMSys.store.loadGroups(ctx, groups)
|
|
if err != nil {
|
|
return errSRBackendIssue(err)
|
|
}
|
|
|
|
for gname, group := range groups {
|
|
if err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemGroupInfo,
|
|
GroupInfo: &madmin.SRGroupInfo{
|
|
UpdateReq: madmin.GroupAddRemove{
|
|
Group: gname,
|
|
Members: group.Members,
|
|
Status: madmin.GroupStatus(group.Status),
|
|
IsRemove: false,
|
|
},
|
|
},
|
|
UpdatedAt: group.UpdatedAt,
|
|
}); err != nil {
|
|
return errSRIAMError(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Followed by group policy mapping
|
|
{
|
|
// Replicate policy mappings on local to all peers.
|
|
groupPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
errG := globalIAMSys.store.loadMappedPolicies(ctx, unknownIAMUserType, true, groupPolicyMap)
|
|
if errG != nil {
|
|
return errSRBackendIssue(errG)
|
|
}
|
|
|
|
var err error
|
|
groupPolicyMap.Range(func(k string, mp MappedPolicy) bool {
|
|
err = c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicyMapping,
|
|
PolicyMapping: &madmin.SRPolicyMapping{
|
|
UserOrGroup: k,
|
|
UserType: int(unknownIAMUserType),
|
|
IsGroup: true,
|
|
Policy: mp.Policies,
|
|
},
|
|
UpdatedAt: mp.UpdatedAt,
|
|
})
|
|
return err == nil
|
|
})
|
|
if err != nil {
|
|
return errSRIAMError(err)
|
|
}
|
|
}
|
|
|
|
// Service accounts are the static accounts that should be synced with
|
|
// valid claims.
|
|
{
|
|
serviceAccounts := make(map[string]UserIdentity)
|
|
err := globalIAMSys.store.loadUsers(ctx, svcUser, serviceAccounts)
|
|
if err != nil {
|
|
return errSRBackendIssue(err)
|
|
}
|
|
|
|
for user, acc := range serviceAccounts {
|
|
if user == siteReplicatorSvcAcc {
|
|
// skip the site replicate svc account as it is
|
|
// already replicated.
|
|
continue
|
|
}
|
|
|
|
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
|
|
if err != nil {
|
|
return errSRBackendIssue(err)
|
|
}
|
|
|
|
_, policy, err := globalIAMSys.GetServiceAccount(ctx, acc.Credentials.AccessKey)
|
|
if err != nil {
|
|
return errSRBackendIssue(err)
|
|
}
|
|
|
|
var policyJSON []byte
|
|
if policy != nil {
|
|
policyJSON, err = json.Marshal(policy)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
}
|
|
|
|
err = c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSvcAcc,
|
|
SvcAccChange: &madmin.SRSvcAccChange{
|
|
Create: &madmin.SRSvcAccCreate{
|
|
Parent: acc.Credentials.ParentUser,
|
|
AccessKey: user,
|
|
SecretKey: acc.Credentials.SecretKey,
|
|
Groups: acc.Credentials.Groups,
|
|
Claims: claims,
|
|
SessionPolicy: json.RawMessage(policyJSON),
|
|
Status: acc.Credentials.Status,
|
|
Name: acc.Credentials.Name,
|
|
Description: acc.Credentials.Description,
|
|
Expiration: &acc.Credentials.Expiration,
|
|
},
|
|
},
|
|
UpdatedAt: acc.UpdatedAt,
|
|
})
|
|
if err != nil {
|
|
return errSRIAMError(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Followed by policy mapping for the userAccounts we previously synced.
|
|
{
|
|
// Replicate policy mappings on local to all peers.
|
|
userPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
errU := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap)
|
|
if errU != nil {
|
|
return errSRBackendIssue(errU)
|
|
}
|
|
var err error
|
|
userPolicyMap.Range(func(user string, mp MappedPolicy) bool {
|
|
err = c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicyMapping,
|
|
PolicyMapping: &madmin.SRPolicyMapping{
|
|
UserOrGroup: user,
|
|
UserType: int(regUser),
|
|
IsGroup: false,
|
|
Policy: mp.Policies,
|
|
},
|
|
UpdatedAt: mp.UpdatedAt,
|
|
})
|
|
return err == nil
|
|
})
|
|
if err != nil {
|
|
return errSRIAMError(err)
|
|
}
|
|
}
|
|
|
|
// and finally followed by policy mappings for for STS users.
|
|
{
|
|
// Replicate policy mappings on local to all peers.
|
|
stsPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
errU := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, stsPolicyMap)
|
|
if errU != nil {
|
|
return errSRBackendIssue(errU)
|
|
}
|
|
|
|
var err error
|
|
stsPolicyMap.Range(func(user string, mp MappedPolicy) bool {
|
|
err = c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicyMapping,
|
|
PolicyMapping: &madmin.SRPolicyMapping{
|
|
UserOrGroup: user,
|
|
UserType: int(stsUser),
|
|
IsGroup: false,
|
|
Policy: mp.Policies,
|
|
},
|
|
UpdatedAt: mp.UpdatedAt,
|
|
})
|
|
return err == nil
|
|
})
|
|
if err != nil {
|
|
return errSRIAMError(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Concurrency helpers
|
|
|
|
type concErr struct {
|
|
errMap map[string]error
|
|
summaryErr error
|
|
}
|
|
|
|
func (c concErr) Error() string {
|
|
if c.summaryErr != nil {
|
|
return c.summaryErr.Error()
|
|
}
|
|
return "<nil>"
|
|
}
|
|
|
|
func (c concErr) Unwrap() error {
|
|
return c.summaryErr
|
|
}
|
|
|
|
func (c *SiteReplicationSys) toErrorFromErrMap(errMap map[string]error, actionName string) error {
|
|
if len(errMap) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Get ordered list of keys of errMap
|
|
keys := []string{}
|
|
for d := range errMap {
|
|
keys = append(keys, d)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
var success int
|
|
msgs := []string{}
|
|
for _, d := range keys {
|
|
name := c.state.Peers[d].Name
|
|
err := errMap[d]
|
|
if err == nil {
|
|
msgs = append(msgs, fmt.Sprintf("'%s' on site %s (%s): succeeded", actionName, name, d))
|
|
success++
|
|
} else {
|
|
msgs = append(msgs, fmt.Sprintf("'%s' on site %s (%s): failed(%v)", actionName, name, d, err))
|
|
}
|
|
}
|
|
if success == len(keys) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Site replication error(s): \n%s", strings.Join(msgs, "\n"))
|
|
}
|
|
|
|
func (c *SiteReplicationSys) newConcErr(errMap map[string]error, actionName string) error {
|
|
return concErr{
|
|
errMap: errMap,
|
|
summaryErr: c.toErrorFromErrMap(errMap, actionName),
|
|
}
|
|
}
|
|
|
|
// concDo calls actions concurrently. selfActionFn is run for the current
|
|
// cluster and peerActionFn is run for each peer replication cluster.
|
|
func (c *SiteReplicationSys) concDo(selfActionFn func() error, peerActionFn func(deploymentID string, p madmin.PeerInfo) error, actionName string) error {
|
|
depIDs := make([]string, 0, len(c.state.Peers))
|
|
for d := range c.state.Peers {
|
|
depIDs = append(depIDs, d)
|
|
}
|
|
errs := make([]error, len(c.state.Peers))
|
|
var wg sync.WaitGroup
|
|
wg.Add(len(depIDs))
|
|
for i := range depIDs {
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
if depIDs[i] == globalDeploymentID() {
|
|
if selfActionFn != nil {
|
|
errs[i] = selfActionFn()
|
|
}
|
|
} else {
|
|
errs[i] = peerActionFn(depIDs[i], c.state.Peers[depIDs[i]])
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
errMap := make(map[string]error, len(c.state.Peers))
|
|
for i, depID := range depIDs {
|
|
errMap[depID] = errs[i]
|
|
if errs[i] != nil && minio.IsNetworkOrHostDown(errs[i], true) {
|
|
ep := c.state.Peers[depID].Endpoint
|
|
epURL, _ := url.Parse(ep)
|
|
if !globalBucketTargetSys.isOffline(epURL) {
|
|
globalBucketTargetSys.markOffline(epURL)
|
|
}
|
|
}
|
|
}
|
|
return c.newConcErr(errMap, actionName)
|
|
}
|
|
|
|
func (c *SiteReplicationSys) annotateErr(annotation string, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%s: %s: %w", c.state.Name, annotation, err)
|
|
}
|
|
|
|
func (c *SiteReplicationSys) annotatePeerErr(dstPeer string, annotation string, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%s->%s: %s: %w", c.state.Name, dstPeer, annotation, err)
|
|
}
|
|
|
|
// isEnabled returns true if site replication is enabled
|
|
func (c *SiteReplicationSys) isEnabled() bool {
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
return c.enabled
|
|
}
|
|
|
|
var errMissingSRConfig = fmt.Errorf("unable to find site replication configuration")
|
|
|
|
// RemovePeerCluster - removes one or more clusters from site replication configuration.
|
|
func (c *SiteReplicationSys) RemovePeerCluster(ctx context.Context, objectAPI ObjectLayer, rreq madmin.SRRemoveReq) (st madmin.ReplicateRemoveStatus, err error) {
|
|
if !c.isEnabled() {
|
|
return st, errSRNotEnabled
|
|
}
|
|
info, err := c.GetClusterInfo(ctx)
|
|
if err != nil {
|
|
return st, errSRBackendIssue(err)
|
|
}
|
|
peerMap := make(map[string]madmin.PeerInfo)
|
|
var rmvEndpoints []string
|
|
siteNames := rreq.SiteNames
|
|
updatedPeers := make(map[string]madmin.PeerInfo)
|
|
|
|
for _, pi := range info.Sites {
|
|
updatedPeers[pi.DeploymentID] = pi
|
|
peerMap[pi.Name] = pi
|
|
if rreq.RemoveAll {
|
|
siteNames = append(siteNames, pi.Name)
|
|
}
|
|
}
|
|
for _, s := range siteNames {
|
|
pinfo, ok := peerMap[s]
|
|
if !ok {
|
|
return st, errSRConfigMissingError(errMissingSRConfig)
|
|
}
|
|
rmvEndpoints = append(rmvEndpoints, pinfo.Endpoint)
|
|
delete(updatedPeers, pinfo.DeploymentID)
|
|
}
|
|
var wg sync.WaitGroup
|
|
errs := make(map[string]error, len(c.state.Peers))
|
|
|
|
for _, v := range info.Sites {
|
|
wg.Add(1)
|
|
if v.DeploymentID == globalDeploymentID() {
|
|
go func() {
|
|
defer wg.Done()
|
|
err := c.RemoveRemoteTargetsForEndpoint(ctx, objectAPI, rmvEndpoints, false)
|
|
errs[globalDeploymentID()] = err
|
|
}()
|
|
continue
|
|
}
|
|
go func(pi madmin.PeerInfo) {
|
|
defer wg.Done()
|
|
admClient, err := c.getAdminClient(ctx, pi.DeploymentID)
|
|
if err != nil {
|
|
errs[pi.DeploymentID] = errSRPeerResp(fmt.Errorf("unable to create admin client for %s: %w", pi.Name, err))
|
|
return
|
|
}
|
|
// set the requesting site's deploymentID for verification of peer request
|
|
rreq.RequestingDepID = globalDeploymentID()
|
|
if _, err = admClient.SRPeerRemove(ctx, rreq); err != nil {
|
|
if errors.Is(err, errMissingSRConfig) {
|
|
// ignore if peer is already removed.
|
|
return
|
|
}
|
|
errs[pi.DeploymentID] = errSRPeerResp(fmt.Errorf("unable to update peer %s: %w", pi.Name, err))
|
|
return
|
|
}
|
|
}(v)
|
|
}
|
|
wg.Wait()
|
|
|
|
errdID := ""
|
|
selfTgtsDeleted := errs[globalDeploymentID()] == nil // true if all remote targets and replication config cleared successfully on local cluster
|
|
|
|
for dID, err := range errs {
|
|
if err != nil {
|
|
if !rreq.RemoveAll && !selfTgtsDeleted {
|
|
return madmin.ReplicateRemoveStatus{
|
|
ErrDetail: err.Error(),
|
|
Status: madmin.ReplicateRemoveStatusPartial,
|
|
}, errSRPeerResp(fmt.Errorf("unable to update peer %s: %w", c.state.Peers[dID].Name, err))
|
|
}
|
|
errdID = dID
|
|
}
|
|
}
|
|
|
|
// force local config to be cleared even if peers failed since the remote targets are deleted
|
|
// by now from the replication config and user intended to forcibly clear all site replication
|
|
if rreq.RemoveAll {
|
|
if err = c.removeFromDisk(ctx); err != nil {
|
|
return madmin.ReplicateRemoveStatus{
|
|
Status: madmin.ReplicateRemoveStatusPartial,
|
|
ErrDetail: fmt.Sprintf("unable to remove cluster-replication state on local: %v", err),
|
|
}, nil
|
|
}
|
|
if errdID != "" {
|
|
err := errs[errdID]
|
|
return madmin.ReplicateRemoveStatus{
|
|
Status: madmin.ReplicateRemoveStatusPartial,
|
|
ErrDetail: err.Error(),
|
|
}, nil
|
|
}
|
|
return madmin.ReplicateRemoveStatus{
|
|
Status: madmin.ReplicateRemoveStatusSuccess,
|
|
}, nil
|
|
}
|
|
|
|
// Update cluster state
|
|
var state srState
|
|
if len(updatedPeers) > 1 {
|
|
state = srState{
|
|
Name: info.Name,
|
|
Peers: updatedPeers,
|
|
ServiceAccountAccessKey: info.ServiceAccountAccessKey,
|
|
}
|
|
}
|
|
if err = c.saveToDisk(ctx, state); err != nil {
|
|
return madmin.ReplicateRemoveStatus{
|
|
Status: madmin.ReplicateRemoveStatusPartial,
|
|
ErrDetail: fmt.Sprintf("unable to save cluster-replication state on local: %v", err),
|
|
}, err
|
|
}
|
|
|
|
st = madmin.ReplicateRemoveStatus{
|
|
Status: madmin.ReplicateRemoveStatusSuccess,
|
|
}
|
|
if errs[errdID] != nil {
|
|
st.Status = madmin.ReplicateRemoveStatusPartial
|
|
st.ErrDetail = errs[errdID].Error()
|
|
}
|
|
return st, nil
|
|
}
|
|
|
|
// InternalRemoveReq - sends an unlink request to peer cluster to remove one or more sites
|
|
// from the site replication configuration.
|
|
func (c *SiteReplicationSys) InternalRemoveReq(ctx context.Context, objectAPI ObjectLayer, rreq madmin.SRRemoveReq) error {
|
|
if !c.isEnabled() {
|
|
return errSRNotEnabled
|
|
}
|
|
if rreq.RequestingDepID != "" {
|
|
// validate if requesting site is still part of site replication
|
|
var foundRequestor bool
|
|
for _, p := range c.state.Peers {
|
|
if p.DeploymentID == rreq.RequestingDepID {
|
|
foundRequestor = true
|
|
break
|
|
}
|
|
}
|
|
if !foundRequestor {
|
|
return errSRRequestorNotFound
|
|
}
|
|
}
|
|
|
|
ourName := ""
|
|
peerMap := make(map[string]madmin.PeerInfo)
|
|
updatedPeers := make(map[string]madmin.PeerInfo)
|
|
siteNames := rreq.SiteNames
|
|
|
|
for _, p := range c.state.Peers {
|
|
peerMap[p.Name] = p
|
|
if p.DeploymentID == globalDeploymentID() {
|
|
ourName = p.Name
|
|
}
|
|
updatedPeers[p.DeploymentID] = p
|
|
if rreq.RemoveAll {
|
|
siteNames = append(siteNames, p.Name)
|
|
}
|
|
}
|
|
var rmvEndpoints []string
|
|
var unlinkSelf bool
|
|
|
|
for _, s := range siteNames {
|
|
info, ok := peerMap[s]
|
|
if !ok {
|
|
return errMissingSRConfig
|
|
}
|
|
if info.DeploymentID == globalDeploymentID() {
|
|
unlinkSelf = true
|
|
continue
|
|
}
|
|
delete(updatedPeers, info.DeploymentID)
|
|
rmvEndpoints = append(rmvEndpoints, info.Endpoint)
|
|
}
|
|
if err := c.RemoveRemoteTargetsForEndpoint(ctx, objectAPI, rmvEndpoints, unlinkSelf); err != nil {
|
|
return err
|
|
}
|
|
var state srState
|
|
if !unlinkSelf {
|
|
state = srState{
|
|
Name: c.state.Name,
|
|
Peers: updatedPeers,
|
|
ServiceAccountAccessKey: c.state.ServiceAccountAccessKey,
|
|
}
|
|
}
|
|
|
|
if err := c.saveToDisk(ctx, state); err != nil {
|
|
return errSRBackendIssue(fmt.Errorf("unable to save cluster-replication state to drive on %s: %v", ourName, err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveRemoteTargetsForEndpoint removes replication targets corresponding to endpoint
|
|
func (c *SiteReplicationSys) RemoveRemoteTargetsForEndpoint(ctx context.Context, objectAPI ObjectLayer, endpoints []string, unlinkSelf bool) (err error) {
|
|
targets := globalBucketTargetSys.ListTargets(ctx, "", string(madmin.ReplicationService))
|
|
m := make(map[string]madmin.BucketTarget)
|
|
for _, t := range targets {
|
|
for _, endpoint := range endpoints {
|
|
ep, _ := url.Parse(endpoint)
|
|
if t.Endpoint == ep.Host &&
|
|
t.Secure == (ep.Scheme == "https") &&
|
|
t.Type == madmin.ReplicationService {
|
|
m[t.Arn] = t
|
|
}
|
|
}
|
|
// all remote targets from self are to be delinked
|
|
if unlinkSelf {
|
|
m[t.Arn] = t
|
|
}
|
|
}
|
|
buckets, err := objectAPI.ListBuckets(ctx, BucketOptions{})
|
|
if err != nil {
|
|
return errSRBackendIssue(err)
|
|
}
|
|
|
|
for _, b := range buckets {
|
|
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, b.Name)
|
|
if err != nil {
|
|
if errors.Is(err, BucketReplicationConfigNotFound{Bucket: b.Name}) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
var nRules []sreplication.Rule
|
|
for _, r := range config.Rules {
|
|
if _, ok := m[r.Destination.Bucket]; !ok {
|
|
nRules = append(nRules, r)
|
|
}
|
|
}
|
|
if len(nRules) > 0 {
|
|
config.Rules = nRules
|
|
configData, err := xml.Marshal(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = globalBucketMetadataSys.Update(ctx, b.Name, bucketReplicationConfig, configData); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if _, err := globalBucketMetadataSys.Delete(ctx, b.Name, bucketReplicationConfig); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
for arn, t := range m {
|
|
if err := globalBucketTargetSys.RemoveTarget(ctx, t.SourceBucket, arn); err != nil {
|
|
if errors.Is(err, BucketRemoteTargetNotFound{Bucket: t.SourceBucket}) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
targets, terr := globalBucketTargetSys.ListBucketTargets(ctx, t.SourceBucket)
|
|
if terr != nil {
|
|
return err
|
|
}
|
|
tgtBytes, terr := json.Marshal(&targets)
|
|
if terr != nil {
|
|
return err
|
|
}
|
|
if _, err = globalBucketMetadataSys.Update(ctx, t.SourceBucket, bucketTargetsFile, tgtBytes); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Other helpers
|
|
|
|
func getAdminClient(endpoint, accessKey, secretKey string) (*madmin.AdminClient, error) {
|
|
epURL, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if globalBucketTargetSys.isOffline(epURL) {
|
|
return nil, RemoteTargetConnectionErr{Endpoint: epURL.String(), Err: fmt.Errorf("remote target is offline for endpoint %s", epURL.String())}
|
|
}
|
|
client, err := madmin.New(epURL.Host, accessKey, secretKey, epURL.Scheme == "https")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
client.SetCustomTransport(globalRemoteTargetTransport)
|
|
return client, nil
|
|
}
|
|
|
|
func getS3Client(pc madmin.PeerSite) (*minioClient.Client, error) {
|
|
ep, err := url.Parse(pc.Endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if globalBucketTargetSys.isOffline(ep) {
|
|
return nil, RemoteTargetConnectionErr{Endpoint: ep.String(), Err: fmt.Errorf("remote target is offline for endpoint %s", ep.String())}
|
|
}
|
|
|
|
return minioClient.New(ep.Host, &minioClient.Options{
|
|
Creds: credentials.NewStaticV4(pc.AccessKey, pc.SecretKey, ""),
|
|
Secure: ep.Scheme == "https",
|
|
Transport: globalRemoteTargetTransport,
|
|
})
|
|
}
|
|
|
|
func getPriorityHelper(replicationConfig replication.Config) int {
|
|
maxPrio := 0
|
|
for _, rule := range replicationConfig.Rules {
|
|
if rule.Priority > maxPrio {
|
|
maxPrio = rule.Priority
|
|
}
|
|
}
|
|
|
|
// leave some gaps in priority numbers for flexibility
|
|
return maxPrio + 10
|
|
}
|
|
|
|
// returns a slice with site names participating in site replciation but unspecified while adding
|
|
// a new site.
|
|
func getMissingSiteNames(oldDeps, newDeps set.StringSet, currSites []madmin.PeerInfo) []string {
|
|
diff := oldDeps.Difference(newDeps)
|
|
var diffSlc []string
|
|
for _, v := range currSites {
|
|
if diff.Contains(v.DeploymentID) {
|
|
diffSlc = append(diffSlc, v.Name)
|
|
}
|
|
}
|
|
return diffSlc
|
|
}
|
|
|
|
type srBucketMetaInfo struct {
|
|
madmin.SRBucketInfo
|
|
DeploymentID string
|
|
}
|
|
|
|
type srPolicy struct {
|
|
madmin.SRIAMPolicy
|
|
DeploymentID string
|
|
}
|
|
|
|
type srPolicyMapping struct {
|
|
madmin.SRPolicyMapping
|
|
DeploymentID string
|
|
}
|
|
|
|
type srUserInfo struct {
|
|
madmin.UserInfo
|
|
DeploymentID string
|
|
}
|
|
|
|
type srGroupDesc struct {
|
|
madmin.GroupDesc
|
|
DeploymentID string
|
|
}
|
|
|
|
type srILMExpiryRule struct {
|
|
madmin.ILMExpiryRule
|
|
DeploymentID string
|
|
}
|
|
|
|
// SiteReplicationStatus returns the site replication status across clusters participating in site replication.
|
|
func (c *SiteReplicationSys) SiteReplicationStatus(ctx context.Context, objAPI ObjectLayer, opts madmin.SRStatusOptions) (info madmin.SRStatusInfo, err error) {
|
|
sinfo, err := c.siteReplicationStatus(ctx, objAPI, opts)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
info = madmin.SRStatusInfo{
|
|
Enabled: sinfo.Enabled,
|
|
MaxBuckets: sinfo.MaxBuckets,
|
|
MaxUsers: sinfo.MaxUsers,
|
|
MaxGroups: sinfo.MaxGroups,
|
|
MaxPolicies: sinfo.MaxPolicies,
|
|
MaxILMExpiryRules: sinfo.MaxILMExpiryRules,
|
|
Sites: sinfo.Sites,
|
|
StatsSummary: sinfo.StatsSummary,
|
|
Metrics: sinfo.Metrics,
|
|
}
|
|
info.BucketStats = make(map[string]map[string]madmin.SRBucketStatsSummary, len(sinfo.Sites))
|
|
info.PolicyStats = make(map[string]map[string]madmin.SRPolicyStatsSummary)
|
|
info.UserStats = make(map[string]map[string]madmin.SRUserStatsSummary)
|
|
info.GroupStats = make(map[string]map[string]madmin.SRGroupStatsSummary)
|
|
info.ILMExpiryStats = make(map[string]map[string]madmin.SRILMExpiryStatsSummary)
|
|
numSites := len(info.Sites)
|
|
for b, stat := range sinfo.BucketStats {
|
|
for dID, st := range stat {
|
|
if st.TagMismatch ||
|
|
st.VersioningConfigMismatch ||
|
|
st.OLockConfigMismatch ||
|
|
st.SSEConfigMismatch ||
|
|
st.PolicyMismatch ||
|
|
st.ReplicationCfgMismatch ||
|
|
st.QuotaCfgMismatch ||
|
|
opts.Entity == madmin.SRBucketEntity {
|
|
if _, ok := info.BucketStats[b]; !ok {
|
|
info.BucketStats[b] = make(map[string]madmin.SRBucketStatsSummary, numSites)
|
|
}
|
|
info.BucketStats[b][dID] = st.SRBucketStatsSummary
|
|
}
|
|
}
|
|
}
|
|
for u, stat := range sinfo.UserStats {
|
|
for dID, st := range stat {
|
|
if st.PolicyMismatch || st.UserInfoMismatch || opts.Entity == madmin.SRUserEntity {
|
|
if _, ok := info.UserStats[u]; !ok {
|
|
info.UserStats[u] = make(map[string]madmin.SRUserStatsSummary, numSites)
|
|
}
|
|
info.UserStats[u][dID] = st.SRUserStatsSummary
|
|
}
|
|
}
|
|
}
|
|
for g, stat := range sinfo.GroupStats {
|
|
for dID, st := range stat {
|
|
if st.PolicyMismatch || st.GroupDescMismatch || opts.Entity == madmin.SRGroupEntity {
|
|
if _, ok := info.GroupStats[g]; !ok {
|
|
info.GroupStats[g] = make(map[string]madmin.SRGroupStatsSummary, numSites)
|
|
}
|
|
info.GroupStats[g][dID] = st.SRGroupStatsSummary
|
|
}
|
|
}
|
|
}
|
|
for p, stat := range sinfo.PolicyStats {
|
|
for dID, st := range stat {
|
|
if st.PolicyMismatch || opts.Entity == madmin.SRPolicyEntity {
|
|
if _, ok := info.PolicyStats[p]; !ok {
|
|
info.PolicyStats[p] = make(map[string]madmin.SRPolicyStatsSummary, numSites)
|
|
}
|
|
info.PolicyStats[p][dID] = st.SRPolicyStatsSummary
|
|
}
|
|
}
|
|
}
|
|
for p, stat := range sinfo.ILMExpiryRulesStats {
|
|
for dID, st := range stat {
|
|
if st.ILMExpiryRuleMismatch || opts.Entity == madmin.SRILMExpiryRuleEntity {
|
|
if _, ok := info.ILMExpiryStats[p]; !ok {
|
|
info.ILMExpiryStats[p] = make(map[string]madmin.SRILMExpiryStatsSummary, numSites)
|
|
}
|
|
info.ILMExpiryStats[p][dID] = st.SRILMExpiryStatsSummary
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
const (
|
|
replicationStatus = "ReplicationStatus"
|
|
)
|
|
|
|
// siteReplicationStatus returns the site replication status across clusters participating in site replication.
|
|
func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI ObjectLayer, opts madmin.SRStatusOptions) (info srStatusInfo, err error) {
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return info, err
|
|
}
|
|
|
|
sris := make([]madmin.SRInfo, len(c.state.Peers))
|
|
depIdx := make(map[string]int, len(c.state.Peers))
|
|
i := 0
|
|
for d := range c.state.Peers {
|
|
depIdx[d] = i
|
|
i++
|
|
}
|
|
|
|
metaInfoConcErr := c.concDo(
|
|
func() error {
|
|
srInfo, err := c.SiteReplicationMetaInfo(ctx, objAPI, opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sris[depIdx[globalDeploymentID()]] = srInfo
|
|
return nil
|
|
},
|
|
func(deploymentID string, p madmin.PeerInfo) error {
|
|
admClient, err := c.getAdminClient(ctx, deploymentID)
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case RemoteTargetConnectionErr:
|
|
sris[depIdx[deploymentID]] = madmin.SRInfo{}
|
|
return nil
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
srInfo, err := admClient.SRMetaInfo(ctx, opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sris[depIdx[deploymentID]] = srInfo
|
|
return nil
|
|
},
|
|
replicationStatus,
|
|
)
|
|
if err := errors.Unwrap(metaInfoConcErr); err != nil {
|
|
return info, errSRBackendIssue(err)
|
|
}
|
|
|
|
info.Enabled = true
|
|
info.Sites = make(map[string]madmin.PeerInfo, len(c.state.Peers))
|
|
for d, peer := range c.state.Peers {
|
|
info.Sites[d] = peer
|
|
}
|
|
info.UpdatedAt = c.state.UpdatedAt
|
|
|
|
var maxBuckets int
|
|
for _, sri := range sris {
|
|
if len(sri.Buckets) > maxBuckets {
|
|
maxBuckets = len(sri.Buckets)
|
|
}
|
|
}
|
|
// mapping b/w entity and entity config across sites
|
|
bucketStats := make(map[string][]srBucketMetaInfo)
|
|
policyStats := make(map[string][]srPolicy)
|
|
userPolicyStats := make(map[string][]srPolicyMapping)
|
|
groupPolicyStats := make(map[string][]srPolicyMapping)
|
|
userInfoStats := make(map[string][]srUserInfo)
|
|
groupDescStats := make(map[string][]srGroupDesc)
|
|
ilmExpiryRuleStats := make(map[string][]srILMExpiryRule)
|
|
|
|
numSites := len(sris)
|
|
allBuckets := set.NewStringSet() // across sites
|
|
allUsers := set.NewStringSet()
|
|
allUserWPolicies := set.NewStringSet()
|
|
allGroups := set.NewStringSet()
|
|
allGroupWPolicies := set.NewStringSet()
|
|
allILMExpiryRules := set.NewStringSet()
|
|
|
|
allPolicies := set.NewStringSet()
|
|
for _, sri := range sris {
|
|
for b := range sri.Buckets {
|
|
allBuckets.Add(b)
|
|
}
|
|
for u := range sri.UserInfoMap {
|
|
allUsers.Add(u)
|
|
}
|
|
for g := range sri.GroupDescMap {
|
|
allGroups.Add(g)
|
|
}
|
|
for p := range sri.Policies {
|
|
allPolicies.Add(p)
|
|
}
|
|
for u := range sri.UserPolicies {
|
|
allUserWPolicies.Add(u)
|
|
}
|
|
for g := range sri.GroupPolicies {
|
|
allGroupWPolicies.Add(g)
|
|
}
|
|
for r := range sri.ILMExpiryRules {
|
|
allILMExpiryRules.Add(r)
|
|
}
|
|
}
|
|
|
|
for i, sri := range sris {
|
|
for b := range allBuckets {
|
|
if _, ok := bucketStats[b]; !ok {
|
|
bucketStats[b] = make([]srBucketMetaInfo, numSites)
|
|
}
|
|
si, ok := sri.Buckets[b]
|
|
if !ok {
|
|
si = madmin.SRBucketInfo{Bucket: b}
|
|
}
|
|
bucketStats[b][i] = srBucketMetaInfo{SRBucketInfo: si, DeploymentID: sri.DeploymentID}
|
|
}
|
|
|
|
for pname := range allPolicies {
|
|
if _, ok := policyStats[pname]; !ok {
|
|
policyStats[pname] = make([]srPolicy, numSites)
|
|
}
|
|
|
|
// if pname is not present in the map, the zero value
|
|
// will be returned.
|
|
pi := sri.Policies[pname]
|
|
policyStats[pname][i] = srPolicy{SRIAMPolicy: pi, DeploymentID: sri.DeploymentID}
|
|
}
|
|
for user := range allUserWPolicies {
|
|
if _, ok := userPolicyStats[user]; !ok {
|
|
userPolicyStats[user] = make([]srPolicyMapping, numSites)
|
|
}
|
|
up := sri.UserPolicies[user]
|
|
userPolicyStats[user][i] = srPolicyMapping{SRPolicyMapping: up, DeploymentID: sri.DeploymentID}
|
|
}
|
|
for group := range allGroupWPolicies {
|
|
if _, ok := groupPolicyStats[group]; !ok {
|
|
groupPolicyStats[group] = make([]srPolicyMapping, numSites)
|
|
}
|
|
up := sri.GroupPolicies[group]
|
|
groupPolicyStats[group][i] = srPolicyMapping{SRPolicyMapping: up, DeploymentID: sri.DeploymentID}
|
|
}
|
|
for u := range allUsers {
|
|
if _, ok := userInfoStats[u]; !ok {
|
|
userInfoStats[u] = make([]srUserInfo, numSites)
|
|
}
|
|
ui := sri.UserInfoMap[u]
|
|
userInfoStats[u][i] = srUserInfo{UserInfo: ui, DeploymentID: sri.DeploymentID}
|
|
}
|
|
for g := range allGroups {
|
|
if _, ok := groupDescStats[g]; !ok {
|
|
groupDescStats[g] = make([]srGroupDesc, numSites)
|
|
}
|
|
gd := sri.GroupDescMap[g]
|
|
groupDescStats[g][i] = srGroupDesc{GroupDesc: gd, DeploymentID: sri.DeploymentID}
|
|
}
|
|
for r := range allILMExpiryRules {
|
|
if _, ok := ilmExpiryRuleStats[r]; !ok {
|
|
ilmExpiryRuleStats[r] = make([]srILMExpiryRule, numSites)
|
|
}
|
|
rl := sri.ILMExpiryRules[r]
|
|
ilmExpiryRuleStats[r][i] = srILMExpiryRule{ILMExpiryRule: rl, DeploymentID: sri.DeploymentID}
|
|
}
|
|
}
|
|
|
|
info.StatsSummary = make(map[string]madmin.SRSiteSummary, len(c.state.Peers))
|
|
info.BucketStats = make(map[string]map[string]srBucketStatsSummary)
|
|
info.PolicyStats = make(map[string]map[string]srPolicyStatsSummary)
|
|
info.UserStats = make(map[string]map[string]srUserStatsSummary)
|
|
info.GroupStats = make(map[string]map[string]srGroupStatsSummary)
|
|
info.ILMExpiryRulesStats = make(map[string]map[string]srILMExpiryRuleStatsSummary)
|
|
// collect user policy mapping replication status across sites
|
|
if opts.Users || opts.Entity == madmin.SRUserEntity {
|
|
for u, pslc := range userPolicyStats {
|
|
if len(info.UserStats[u]) == 0 {
|
|
info.UserStats[u] = make(map[string]srUserStatsSummary)
|
|
}
|
|
var policyMappings []madmin.SRPolicyMapping
|
|
uPolicyCount := 0
|
|
for _, ps := range pslc {
|
|
policyMappings = append(policyMappings, ps.SRPolicyMapping)
|
|
uPolicyCount++
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
sum.TotalUserPolicyMappingCount++
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
}
|
|
userPolicyMismatch := !isPolicyMappingReplicated(uPolicyCount, numSites, policyMappings)
|
|
for _, ps := range pslc {
|
|
dID := depIdx[ps.DeploymentID]
|
|
_, hasUser := sris[dID].UserPolicies[u]
|
|
info.UserStats[u][ps.DeploymentID] = srUserStatsSummary{
|
|
SRUserStatsSummary: madmin.SRUserStatsSummary{
|
|
PolicyMismatch: userPolicyMismatch,
|
|
HasUser: hasUser,
|
|
HasPolicyMapping: ps.Policy != "",
|
|
},
|
|
userPolicy: ps,
|
|
}
|
|
if !userPolicyMismatch || opts.Entity != madmin.SRUserEntity {
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
if !ps.IsGroup {
|
|
sum.ReplicatedUserPolicyMappings++
|
|
}
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
}
|
|
}
|
|
}
|
|
|
|
// collect user info replication status across sites
|
|
for u, pslc := range userInfoStats {
|
|
var uiSlc []madmin.UserInfo
|
|
userCount := 0
|
|
for _, ps := range pslc {
|
|
uiSlc = append(uiSlc, ps.UserInfo)
|
|
userCount++
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
sum.TotalUsersCount++
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
}
|
|
userInfoMismatch := !isUserInfoReplicated(userCount, numSites, uiSlc)
|
|
for _, ps := range pslc {
|
|
dID := depIdx[ps.DeploymentID]
|
|
_, hasUser := sris[dID].UserInfoMap[u]
|
|
if len(info.UserStats[u]) == 0 {
|
|
info.UserStats[u] = make(map[string]srUserStatsSummary)
|
|
}
|
|
umis, ok := info.UserStats[u][ps.DeploymentID]
|
|
if !ok {
|
|
umis = srUserStatsSummary{
|
|
SRUserStatsSummary: madmin.SRUserStatsSummary{
|
|
HasUser: hasUser,
|
|
},
|
|
}
|
|
}
|
|
umis.UserInfoMismatch = userInfoMismatch
|
|
umis.userInfo = ps
|
|
info.UserStats[u][ps.DeploymentID] = umis
|
|
if !userInfoMismatch || opts.Entity != madmin.SRUserEntity {
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
sum.ReplicatedUsers++
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if opts.Groups || opts.Entity == madmin.SRGroupEntity {
|
|
// collect group policy mapping replication status across sites
|
|
for g, pslc := range groupPolicyStats {
|
|
var policyMappings []madmin.SRPolicyMapping
|
|
gPolicyCount := 0
|
|
for _, ps := range pslc {
|
|
policyMappings = append(policyMappings, ps.SRPolicyMapping)
|
|
gPolicyCount++
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
sum.TotalGroupPolicyMappingCount++
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
}
|
|
groupPolicyMismatch := !isPolicyMappingReplicated(gPolicyCount, numSites, policyMappings)
|
|
if len(info.GroupStats[g]) == 0 {
|
|
info.GroupStats[g] = make(map[string]srGroupStatsSummary)
|
|
}
|
|
for _, ps := range pslc {
|
|
dID := depIdx[ps.DeploymentID]
|
|
_, hasGroup := sris[dID].GroupPolicies[g]
|
|
info.GroupStats[g][ps.DeploymentID] = srGroupStatsSummary{
|
|
SRGroupStatsSummary: madmin.SRGroupStatsSummary{
|
|
PolicyMismatch: groupPolicyMismatch,
|
|
HasGroup: hasGroup,
|
|
HasPolicyMapping: ps.Policy != "",
|
|
DeploymentID: ps.DeploymentID,
|
|
},
|
|
groupPolicy: ps,
|
|
}
|
|
if !groupPolicyMismatch && opts.Entity != madmin.SRGroupEntity {
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
sum.ReplicatedGroupPolicyMappings++
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// collect group desc replication status across sites
|
|
for g, pslc := range groupDescStats {
|
|
var gds []madmin.GroupDesc
|
|
groupCount := 0
|
|
for _, ps := range pslc {
|
|
groupCount++
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
sum.TotalGroupsCount++
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
gds = append(gds, ps.GroupDesc)
|
|
}
|
|
gdMismatch := !isGroupDescReplicated(groupCount, numSites, gds)
|
|
for _, ps := range pslc {
|
|
dID := depIdx[ps.DeploymentID]
|
|
_, hasGroup := sris[dID].GroupDescMap[g]
|
|
if len(info.GroupStats[g]) == 0 {
|
|
info.GroupStats[g] = make(map[string]srGroupStatsSummary)
|
|
}
|
|
gmis, ok := info.GroupStats[g][ps.DeploymentID]
|
|
if !ok {
|
|
gmis = srGroupStatsSummary{
|
|
SRGroupStatsSummary: madmin.SRGroupStatsSummary{
|
|
HasGroup: hasGroup,
|
|
},
|
|
}
|
|
}
|
|
gmis.GroupDescMismatch = gdMismatch
|
|
gmis.groupDesc = ps
|
|
info.GroupStats[g][ps.DeploymentID] = gmis
|
|
if !gdMismatch && opts.Entity != madmin.SRGroupEntity {
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
sum.ReplicatedGroups++
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if opts.Policies || opts.Entity == madmin.SRPolicyEntity {
|
|
// collect IAM policy replication status across sites
|
|
for p, pslc := range policyStats {
|
|
var policies []*policy.Policy
|
|
uPolicyCount := 0
|
|
for _, ps := range pslc {
|
|
plcy, err := policy.ParseConfig(bytes.NewReader([]byte(ps.SRIAMPolicy.Policy)))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
policies = append(policies, plcy)
|
|
uPolicyCount++
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
sum.TotalIAMPoliciesCount++
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
}
|
|
if len(info.PolicyStats[p]) == 0 {
|
|
info.PolicyStats[p] = make(map[string]srPolicyStatsSummary)
|
|
}
|
|
policyMismatch := !isIAMPolicyReplicated(uPolicyCount, numSites, policies)
|
|
for _, ps := range pslc {
|
|
dID := depIdx[ps.DeploymentID]
|
|
_, hasPolicy := sris[dID].Policies[p]
|
|
info.PolicyStats[p][ps.DeploymentID] = srPolicyStatsSummary{
|
|
SRPolicyStatsSummary: madmin.SRPolicyStatsSummary{
|
|
PolicyMismatch: policyMismatch,
|
|
HasPolicy: hasPolicy,
|
|
},
|
|
policy: ps,
|
|
}
|
|
switch {
|
|
case policyMismatch, opts.Entity == madmin.SRPolicyEntity:
|
|
default:
|
|
sum := info.StatsSummary[ps.DeploymentID]
|
|
if !policyMismatch {
|
|
sum.ReplicatedIAMPolicies++
|
|
}
|
|
info.StatsSummary[ps.DeploymentID] = sum
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if opts.Buckets || opts.Entity == madmin.SRBucketEntity {
|
|
// collect bucket metadata replication stats across sites
|
|
for b, slc := range bucketStats {
|
|
tagSet := set.NewStringSet()
|
|
olockConfigSet := set.NewStringSet()
|
|
policies := make([]*policy.BucketPolicy, numSites)
|
|
replCfgs := make([]*sreplication.Config, numSites)
|
|
quotaCfgs := make([]*madmin.BucketQuota, numSites)
|
|
sseCfgSet := set.NewStringSet()
|
|
versionCfgSet := set.NewStringSet()
|
|
var tagCount, olockCfgCount, sseCfgCount, versionCfgCount int
|
|
for i, s := range slc {
|
|
if s.ReplicationConfig != nil {
|
|
cfgBytes, err := base64.StdEncoding.DecodeString(*s.ReplicationConfig)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
cfg, err := sreplication.ParseConfig(bytes.NewReader(cfgBytes))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
replCfgs[i] = cfg
|
|
}
|
|
if s.Versioning != nil {
|
|
configData, err := base64.StdEncoding.DecodeString(*s.Versioning)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
versionCfgCount++
|
|
if !versionCfgSet.Contains(string(configData)) {
|
|
versionCfgSet.Add(string(configData))
|
|
}
|
|
}
|
|
if s.QuotaConfig != nil {
|
|
cfgBytes, err := base64.StdEncoding.DecodeString(*s.QuotaConfig)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
cfg, err := parseBucketQuota(b, cfgBytes)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
quotaCfgs[i] = cfg
|
|
}
|
|
if s.Tags != nil {
|
|
tagBytes, err := base64.StdEncoding.DecodeString(*s.Tags)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
tagCount++
|
|
if !tagSet.Contains(string(tagBytes)) {
|
|
tagSet.Add(string(tagBytes))
|
|
}
|
|
}
|
|
if len(s.Policy) > 0 {
|
|
plcy, err := policy.ParseBucketPolicyConfig(bytes.NewReader(s.Policy), b)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
policies[i] = plcy
|
|
}
|
|
if s.ObjectLockConfig != nil {
|
|
configData, err := base64.StdEncoding.DecodeString(*s.ObjectLockConfig)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
olockCfgCount++
|
|
if !olockConfigSet.Contains(string(configData)) {
|
|
olockConfigSet.Add(string(configData))
|
|
}
|
|
}
|
|
if s.SSEConfig != nil {
|
|
configData, err := base64.StdEncoding.DecodeString(*s.SSEConfig)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sseCfgCount++
|
|
if !sseCfgSet.Contains(string(configData)) {
|
|
sseCfgSet.Add(string(configData))
|
|
}
|
|
}
|
|
ss, ok := info.StatsSummary[s.DeploymentID]
|
|
if !ok {
|
|
ss = madmin.SRSiteSummary{}
|
|
}
|
|
// increment total number of replicated buckets
|
|
if len(slc) == numSites {
|
|
ss.ReplicatedBuckets++
|
|
}
|
|
ss.TotalBucketsCount++
|
|
if tagCount > 0 {
|
|
ss.TotalTagsCount++
|
|
}
|
|
if olockCfgCount > 0 {
|
|
ss.TotalLockConfigCount++
|
|
}
|
|
if sseCfgCount > 0 {
|
|
ss.TotalSSEConfigCount++
|
|
}
|
|
if versionCfgCount > 0 {
|
|
ss.TotalVersioningConfigCount++
|
|
}
|
|
if len(policies) > 0 {
|
|
ss.TotalBucketPoliciesCount++
|
|
}
|
|
info.StatsSummary[s.DeploymentID] = ss
|
|
}
|
|
tagMismatch := !isReplicated(tagCount, numSites, tagSet)
|
|
olockCfgMismatch := !isReplicated(olockCfgCount, numSites, olockConfigSet)
|
|
sseCfgMismatch := !isReplicated(sseCfgCount, numSites, sseCfgSet)
|
|
versionCfgMismatch := !isReplicated(versionCfgCount, numSites, versionCfgSet)
|
|
policyMismatch := !isBktPolicyReplicated(numSites, policies)
|
|
replCfgMismatch := !isBktReplCfgReplicated(numSites, replCfgs)
|
|
quotaCfgMismatch := !isBktQuotaCfgReplicated(numSites, quotaCfgs)
|
|
info.BucketStats[b] = make(map[string]srBucketStatsSummary, numSites)
|
|
for i, s := range slc {
|
|
dIdx := depIdx[s.DeploymentID]
|
|
var hasBucket, isBucketMarkedDeleted bool
|
|
|
|
bi, ok := sris[dIdx].Buckets[s.Bucket]
|
|
if ok {
|
|
isBucketMarkedDeleted = !bi.DeletedAt.IsZero() && (bi.CreatedAt.IsZero() || bi.DeletedAt.After(bi.CreatedAt))
|
|
hasBucket = !bi.CreatedAt.IsZero()
|
|
}
|
|
quotaCfgSet := hasBucket && quotaCfgs[i] != nil && *quotaCfgs[i] != madmin.BucketQuota{}
|
|
ss := madmin.SRBucketStatsSummary{
|
|
DeploymentID: s.DeploymentID,
|
|
HasBucket: hasBucket,
|
|
BucketMarkedDeleted: isBucketMarkedDeleted,
|
|
TagMismatch: tagMismatch,
|
|
OLockConfigMismatch: olockCfgMismatch,
|
|
SSEConfigMismatch: sseCfgMismatch,
|
|
VersioningConfigMismatch: versionCfgMismatch,
|
|
PolicyMismatch: policyMismatch,
|
|
ReplicationCfgMismatch: replCfgMismatch,
|
|
QuotaCfgMismatch: quotaCfgMismatch,
|
|
HasReplicationCfg: s.ReplicationConfig != nil,
|
|
HasTagsSet: s.Tags != nil,
|
|
HasOLockConfigSet: s.ObjectLockConfig != nil,
|
|
HasPolicySet: s.Policy != nil,
|
|
HasQuotaCfgSet: quotaCfgSet,
|
|
HasSSECfgSet: s.SSEConfig != nil,
|
|
}
|
|
var m srBucketMetaInfo
|
|
if len(bucketStats[s.Bucket]) > dIdx {
|
|
m = bucketStats[s.Bucket][dIdx]
|
|
}
|
|
info.BucketStats[b][s.DeploymentID] = srBucketStatsSummary{
|
|
SRBucketStatsSummary: ss,
|
|
meta: m,
|
|
}
|
|
}
|
|
// no mismatch
|
|
for _, s := range slc {
|
|
sum := info.StatsSummary[s.DeploymentID]
|
|
if !olockCfgMismatch && olockCfgCount == numSites {
|
|
sum.ReplicatedLockConfig++
|
|
}
|
|
if !versionCfgMismatch && versionCfgCount == numSites {
|
|
sum.ReplicatedVersioningConfig++
|
|
}
|
|
if !sseCfgMismatch && sseCfgCount == numSites {
|
|
sum.ReplicatedSSEConfig++
|
|
}
|
|
if !policyMismatch && len(policies) == numSites {
|
|
sum.ReplicatedBucketPolicies++
|
|
}
|
|
if !tagMismatch && tagCount == numSites {
|
|
sum.ReplicatedTags++
|
|
}
|
|
info.StatsSummary[s.DeploymentID] = sum
|
|
}
|
|
}
|
|
}
|
|
if opts.ILMExpiryRules || opts.Entity == madmin.SRILMExpiryRuleEntity {
|
|
// collect ILM expiry rules replication status across sites
|
|
for id, ilmExpRules := range ilmExpiryRuleStats {
|
|
var rules []*lifecycle.Rule
|
|
uRuleCount := 0
|
|
for _, rl := range ilmExpRules {
|
|
var rule lifecycle.Rule
|
|
if err := xml.Unmarshal([]byte(rl.ILMExpiryRule.ILMRule), &rule); err != nil {
|
|
continue
|
|
}
|
|
rules = append(rules, &rule)
|
|
uRuleCount++
|
|
sum := info.StatsSummary[rl.DeploymentID]
|
|
sum.TotalILMExpiryRulesCount++
|
|
info.StatsSummary[rl.DeploymentID] = sum
|
|
}
|
|
if len(info.ILMExpiryRulesStats[id]) == 0 {
|
|
info.ILMExpiryRulesStats[id] = make(map[string]srILMExpiryRuleStatsSummary)
|
|
}
|
|
ilmExpRuleMismatch := !isILMExpRuleReplicated(uRuleCount, numSites, rules)
|
|
for _, rl := range ilmExpRules {
|
|
dID := depIdx[rl.DeploymentID]
|
|
_, hasILMExpRule := sris[dID].ILMExpiryRules[id]
|
|
info.ILMExpiryRulesStats[id][rl.DeploymentID] = srILMExpiryRuleStatsSummary{
|
|
SRILMExpiryStatsSummary: madmin.SRILMExpiryStatsSummary{
|
|
ILMExpiryRuleMismatch: ilmExpRuleMismatch,
|
|
HasILMExpiryRules: hasILMExpRule,
|
|
},
|
|
ilmExpiryRule: rl,
|
|
}
|
|
switch {
|
|
case ilmExpRuleMismatch, opts.Entity == madmin.SRILMExpiryRuleEntity:
|
|
default:
|
|
sum := info.StatsSummary[rl.DeploymentID]
|
|
if !ilmExpRuleMismatch {
|
|
sum.ReplicatedILMExpiryRules++
|
|
}
|
|
info.StatsSummary[rl.DeploymentID] = sum
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if opts.PeerState {
|
|
info.PeerStates = make(map[string]madmin.SRStateInfo, numSites)
|
|
for _, sri := range sris {
|
|
info.PeerStates[sri.DeploymentID] = sri.State
|
|
}
|
|
}
|
|
|
|
if opts.Metrics {
|
|
m, err := globalSiteReplicationSys.getSiteMetrics(ctx)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
info.Metrics = m
|
|
}
|
|
|
|
// maximum buckets users etc seen across sites
|
|
info.MaxBuckets = len(bucketStats)
|
|
info.MaxUsers = len(userInfoStats)
|
|
info.MaxGroups = len(groupDescStats)
|
|
info.MaxPolicies = len(policyStats)
|
|
info.MaxILMExpiryRules = len(ilmExpiryRuleStats)
|
|
return
|
|
}
|
|
|
|
// isReplicated returns true if count of replicated matches the number of
|
|
// sites and there is atmost one unique entry in the set.
|
|
func isReplicated(cntReplicated, total int, valSet set.StringSet) bool {
|
|
if cntReplicated > 0 && cntReplicated < total {
|
|
return false
|
|
}
|
|
if len(valSet) > 1 {
|
|
// mismatch - one or more sites has differing tags/policy
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isIAMPolicyReplicated returns true if count of replicated IAM policies matches total
|
|
// number of sites and IAM policies are identical.
|
|
func isIAMPolicyReplicated(cntReplicated, total int, policies []*policy.Policy) bool {
|
|
if cntReplicated > 0 && cntReplicated != total {
|
|
return false
|
|
}
|
|
// check if policies match between sites
|
|
var prev *policy.Policy
|
|
for i, p := range policies {
|
|
if i == 0 {
|
|
prev = p
|
|
continue
|
|
}
|
|
if !prev.Equals(*p) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isPolicyMappingReplicated returns true if count of replicated IAM policy mappings matches total
|
|
// number of sites and IAM policy mappings are identical.
|
|
func isPolicyMappingReplicated(cntReplicated, total int, policies []madmin.SRPolicyMapping) bool {
|
|
if cntReplicated > 0 && cntReplicated != total {
|
|
return false
|
|
}
|
|
// check if policies match between sites
|
|
var prev madmin.SRPolicyMapping
|
|
for i, p := range policies {
|
|
if i == 0 {
|
|
prev = p
|
|
continue
|
|
}
|
|
if prev.IsGroup != p.IsGroup ||
|
|
prev.Policy != p.Policy ||
|
|
prev.UserOrGroup != p.UserOrGroup {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isUserInfoReplicated(cntReplicated, total int, uis []madmin.UserInfo) bool {
|
|
if cntReplicated > 0 && cntReplicated != total {
|
|
return false
|
|
}
|
|
// check if policies match between sites
|
|
var prev madmin.UserInfo
|
|
for i, ui := range uis {
|
|
if i == 0 {
|
|
prev = ui
|
|
continue
|
|
}
|
|
if !isUserInfoEqual(prev, ui) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isGroupDescReplicated(cntReplicated, total int, gds []madmin.GroupDesc) bool {
|
|
if cntReplicated > 0 && cntReplicated != total {
|
|
return false
|
|
}
|
|
// check if policies match between sites
|
|
var prev madmin.GroupDesc
|
|
for i, gd := range gds {
|
|
if i == 0 {
|
|
prev = gd
|
|
continue
|
|
}
|
|
if !isGroupDescEqual(prev, gd) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isBktQuotaCfgReplicated(total int, quotaCfgs []*madmin.BucketQuota) bool {
|
|
numquotaCfgs := 0
|
|
for _, q := range quotaCfgs {
|
|
if q == nil {
|
|
continue
|
|
}
|
|
numquotaCfgs++
|
|
}
|
|
if numquotaCfgs == 0 {
|
|
return true
|
|
}
|
|
if numquotaCfgs > 0 && numquotaCfgs != total {
|
|
return false
|
|
}
|
|
var prev *madmin.BucketQuota
|
|
for i, q := range quotaCfgs {
|
|
if q == nil {
|
|
return false
|
|
}
|
|
if i == 0 {
|
|
prev = q
|
|
continue
|
|
}
|
|
if prev.Quota != q.Quota || prev.Type != q.Type {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isBktPolicyReplicated returns true if count of replicated bucket policies matches total
|
|
// number of sites and bucket policies are identical.
|
|
func isBktPolicyReplicated(total int, policies []*policy.BucketPolicy) bool {
|
|
numPolicies := 0
|
|
for _, p := range policies {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
numPolicies++
|
|
}
|
|
if numPolicies > 0 && numPolicies != total {
|
|
return false
|
|
}
|
|
// check if policies match between sites
|
|
var prev *policy.BucketPolicy
|
|
for i, p := range policies {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
if i == 0 {
|
|
prev = p
|
|
continue
|
|
}
|
|
if !prev.Equals(*p) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isBktReplCfgReplicated returns true if all the sites have same number
|
|
// of replication rules with all replication features enabled.
|
|
func isBktReplCfgReplicated(total int, cfgs []*sreplication.Config) bool {
|
|
cntReplicated := 0
|
|
for _, c := range cfgs {
|
|
if c == nil {
|
|
continue
|
|
}
|
|
cntReplicated++
|
|
}
|
|
|
|
if cntReplicated > 0 && cntReplicated != total {
|
|
return false
|
|
}
|
|
// check if policies match between sites
|
|
var prev *sreplication.Config
|
|
for i, c := range cfgs {
|
|
if c == nil {
|
|
continue
|
|
}
|
|
if i == 0 {
|
|
prev = c
|
|
continue
|
|
}
|
|
if len(prev.Rules) != len(c.Rules) {
|
|
return false
|
|
}
|
|
if len(c.Rules) != total-1 {
|
|
return false
|
|
}
|
|
for _, r := range c.Rules {
|
|
if !strings.HasPrefix(r.ID, "site-repl-") {
|
|
return false
|
|
}
|
|
if r.DeleteMarkerReplication.Status == sreplication.Disabled ||
|
|
r.DeleteReplication.Status == sreplication.Disabled ||
|
|
r.ExistingObjectReplication.Status == sreplication.Disabled ||
|
|
r.SourceSelectionCriteria.ReplicaModifications.Status == sreplication.Disabled {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isILMExpRuleReplicated returns true if count of replicated ILM Expiry rules matches total
|
|
// number of sites and ILM expiry rules are identical.
|
|
func isILMExpRuleReplicated(cntReplicated, total int, rules []*lifecycle.Rule) bool {
|
|
if cntReplicated > 0 && cntReplicated != total {
|
|
return false
|
|
}
|
|
// check if policies match between sites
|
|
var prev *lifecycle.Rule
|
|
for i, r := range rules {
|
|
if i == 0 {
|
|
prev = r
|
|
continue
|
|
}
|
|
// Check equality of rules
|
|
prevRData, err := xml.Marshal(prev)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
rData, err := xml.Marshal(*r)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if !(string(prevRData) == string(rData)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// cache of IAM info fetched in last SiteReplicationMetaInfo call
|
|
type srIAMCache struct {
|
|
sync.RWMutex
|
|
lastUpdate time.Time
|
|
srIAMInfo madmin.SRInfo // caches IAM info
|
|
}
|
|
|
|
func (c *SiteReplicationSys) getSRCachedIAMInfo() (info madmin.SRInfo, ok bool) {
|
|
c.iamMetaCache.RLock()
|
|
defer c.iamMetaCache.RUnlock()
|
|
if c.iamMetaCache.lastUpdate.IsZero() {
|
|
return info, false
|
|
}
|
|
if time.Since(c.iamMetaCache.lastUpdate) < siteHealTimeInterval {
|
|
return c.iamMetaCache.srIAMInfo, true
|
|
}
|
|
return info, false
|
|
}
|
|
|
|
func (c *SiteReplicationSys) srCacheIAMInfo(info madmin.SRInfo) {
|
|
c.iamMetaCache.Lock()
|
|
defer c.iamMetaCache.Unlock()
|
|
c.iamMetaCache.srIAMInfo = info
|
|
c.iamMetaCache.lastUpdate = time.Now()
|
|
}
|
|
|
|
// SiteReplicationMetaInfo returns the metadata info on buckets, policies etc for the replicated site
|
|
func (c *SiteReplicationSys) SiteReplicationMetaInfo(ctx context.Context, objAPI ObjectLayer, opts madmin.SRStatusOptions) (info madmin.SRInfo, err error) {
|
|
if objAPI == nil {
|
|
return info, errSRObjectLayerNotReady
|
|
}
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return info, nil
|
|
}
|
|
info.DeploymentID = globalDeploymentID()
|
|
if opts.Buckets || opts.Entity == madmin.SRBucketEntity {
|
|
var (
|
|
buckets []BucketInfo
|
|
err error
|
|
)
|
|
if opts.Entity == madmin.SRBucketEntity {
|
|
bi, err := objAPI.GetBucketInfo(ctx, opts.EntityValue, BucketOptions{Deleted: opts.ShowDeleted})
|
|
if err != nil {
|
|
if isErrBucketNotFound(err) {
|
|
return info, nil
|
|
}
|
|
return info, errSRBackendIssue(err)
|
|
}
|
|
buckets = append(buckets, bi)
|
|
} else {
|
|
buckets, err = objAPI.ListBuckets(ctx, BucketOptions{Deleted: opts.ShowDeleted})
|
|
if err != nil {
|
|
return info, errSRBackendIssue(err)
|
|
}
|
|
}
|
|
info.Buckets = make(map[string]madmin.SRBucketInfo, len(buckets))
|
|
for _, bucketInfo := range buckets {
|
|
bucket := bucketInfo.Name
|
|
bucketExists := bucketInfo.Deleted.IsZero() || (!bucketInfo.Created.IsZero() && bucketInfo.Created.After(bucketInfo.Deleted))
|
|
bms := madmin.SRBucketInfo{
|
|
Bucket: bucket,
|
|
CreatedAt: bucketInfo.Created.UTC(),
|
|
DeletedAt: bucketInfo.Deleted.UTC(),
|
|
}
|
|
if !bucketExists {
|
|
info.Buckets[bucket] = bms
|
|
continue
|
|
}
|
|
|
|
meta, err := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket)
|
|
if err != nil && !errors.Is(err, errConfigNotFound) {
|
|
return info, errSRBackendIssue(err)
|
|
}
|
|
|
|
bms.Policy = meta.PolicyConfigJSON
|
|
bms.PolicyUpdatedAt = meta.PolicyConfigUpdatedAt
|
|
|
|
if len(meta.TaggingConfigXML) > 0 {
|
|
tagCfgStr := base64.StdEncoding.EncodeToString(meta.TaggingConfigXML)
|
|
bms.Tags = &tagCfgStr
|
|
bms.TagConfigUpdatedAt = meta.TaggingConfigUpdatedAt
|
|
}
|
|
|
|
if len(meta.VersioningConfigXML) > 0 {
|
|
versioningCfgStr := base64.StdEncoding.EncodeToString(meta.VersioningConfigXML)
|
|
bms.Versioning = &versioningCfgStr
|
|
bms.VersioningConfigUpdatedAt = meta.VersioningConfigUpdatedAt
|
|
}
|
|
|
|
if len(meta.ObjectLockConfigXML) > 0 {
|
|
objLockStr := base64.StdEncoding.EncodeToString(meta.ObjectLockConfigXML)
|
|
bms.ObjectLockConfig = &objLockStr
|
|
bms.ObjectLockConfigUpdatedAt = meta.ObjectLockConfigUpdatedAt
|
|
}
|
|
|
|
if len(meta.QuotaConfigJSON) > 0 {
|
|
quotaConfigStr := base64.StdEncoding.EncodeToString(meta.QuotaConfigJSON)
|
|
bms.QuotaConfig = "aConfigStr
|
|
bms.QuotaConfigUpdatedAt = meta.QuotaConfigUpdatedAt
|
|
}
|
|
|
|
if len(meta.EncryptionConfigXML) > 0 {
|
|
sseConfigStr := base64.StdEncoding.EncodeToString(meta.EncryptionConfigXML)
|
|
bms.SSEConfig = &sseConfigStr
|
|
bms.SSEConfigUpdatedAt = meta.EncryptionConfigUpdatedAt
|
|
}
|
|
|
|
if len(meta.ReplicationConfigXML) > 0 {
|
|
rcfgXMLStr := base64.StdEncoding.EncodeToString(meta.ReplicationConfigXML)
|
|
bms.ReplicationConfig = &rcfgXMLStr
|
|
bms.ReplicationConfigUpdatedAt = meta.ReplicationConfigUpdatedAt
|
|
}
|
|
|
|
if meta.lifecycleConfig != nil {
|
|
var expLclCfg lifecycle.Lifecycle
|
|
expLclCfg.XMLName = meta.lifecycleConfig.XMLName
|
|
for _, rule := range meta.lifecycleConfig.Rules {
|
|
if !rule.Expiration.IsNull() || !rule.NoncurrentVersionExpiration.IsNull() {
|
|
// copy the non transition details of the rule
|
|
expLclCfg.Rules = append(expLclCfg.Rules, rule.CloneNonTransition())
|
|
}
|
|
}
|
|
expLclCfg.ExpiryUpdatedAt = meta.lifecycleConfig.ExpiryUpdatedAt
|
|
ilmConfigData, err := xml.Marshal(expLclCfg)
|
|
if err != nil {
|
|
return info, errSRBackendIssue(err)
|
|
}
|
|
|
|
expLclCfgStr := base64.StdEncoding.EncodeToString(ilmConfigData)
|
|
bms.ExpiryLCConfig = &expLclCfgStr
|
|
// if all non expiry rules only, ExpiryUpdatedAt would be nil
|
|
if meta.lifecycleConfig.ExpiryUpdatedAt != nil {
|
|
bms.ExpiryLCConfigUpdatedAt = *(meta.lifecycleConfig.ExpiryUpdatedAt)
|
|
}
|
|
}
|
|
|
|
info.Buckets[bucket] = bms
|
|
}
|
|
}
|
|
|
|
if opts.Users && opts.Groups && opts.Policies && !opts.Buckets {
|
|
// serialize SiteReplicationMetaInfo calls - if data in cache is within
|
|
// healing interval, avoid fetching IAM data again from disk.
|
|
if metaInfo, ok := c.getSRCachedIAMInfo(); ok {
|
|
return metaInfo, nil
|
|
}
|
|
}
|
|
if opts.Policies || opts.Entity == madmin.SRPolicyEntity {
|
|
var allPolicies map[string]PolicyDoc
|
|
if opts.Entity == madmin.SRPolicyEntity {
|
|
if p, err := globalIAMSys.store.GetPolicyDoc(opts.EntityValue); err == nil {
|
|
allPolicies = map[string]PolicyDoc{opts.EntityValue: p}
|
|
}
|
|
} else {
|
|
// Replicate IAM policies on local to all peers.
|
|
allPolicies, err = globalIAMSys.store.listPolicyDocs(ctx, "")
|
|
if err != nil {
|
|
return info, errSRBackendIssue(err)
|
|
}
|
|
}
|
|
info.Policies = make(map[string]madmin.SRIAMPolicy, len(allPolicies))
|
|
for pname, policyDoc := range allPolicies {
|
|
policyJSON, err := json.Marshal(policyDoc.Policy)
|
|
if err != nil {
|
|
return info, wrapSRErr(err)
|
|
}
|
|
info.Policies[pname] = madmin.SRIAMPolicy{Policy: json.RawMessage(policyJSON), UpdatedAt: policyDoc.UpdateDate}
|
|
}
|
|
}
|
|
if opts.ILMExpiryRules || opts.Entity == madmin.SRILMExpiryRuleEntity {
|
|
info.ILMExpiryRules = make(map[string]madmin.ILMExpiryRule)
|
|
buckets, err := objAPI.ListBuckets(ctx, BucketOptions{Deleted: opts.ShowDeleted})
|
|
if err != nil {
|
|
return info, errSRBackendIssue(err)
|
|
}
|
|
|
|
allRules := make(map[string]madmin.ILMExpiryRule)
|
|
for _, bucketInfo := range buckets {
|
|
bucket := bucketInfo.Name
|
|
bucketExists := bucketInfo.Deleted.IsZero() || (!bucketInfo.Created.IsZero() && bucketInfo.Created.After(bucketInfo.Deleted))
|
|
if !bucketExists {
|
|
continue
|
|
}
|
|
|
|
meta, err := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket)
|
|
if err != nil && !errors.Is(err, errConfigNotFound) {
|
|
return info, errSRBackendIssue(err)
|
|
}
|
|
|
|
if meta.lifecycleConfig != nil && meta.lifecycleConfig.HasExpiry() {
|
|
var updatedAt time.Time
|
|
if meta.lifecycleConfig.ExpiryUpdatedAt != nil {
|
|
updatedAt = *meta.lifecycleConfig.ExpiryUpdatedAt
|
|
}
|
|
for _, rule := range meta.lifecycleConfig.Rules {
|
|
if !rule.Expiration.IsNull() || !rule.NoncurrentVersionExpiration.IsNull() {
|
|
// copy the non transition details of the rule
|
|
ruleData, err := xml.Marshal(rule.CloneNonTransition())
|
|
if err != nil {
|
|
return info, errSRBackendIssue(err)
|
|
}
|
|
allRules[rule.ID] = madmin.ILMExpiryRule{ILMRule: string(ruleData), Bucket: bucket, UpdatedAt: updatedAt}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if opts.Entity == madmin.SRILMExpiryRuleEntity {
|
|
if rule, ok := allRules[opts.EntityValue]; ok {
|
|
info.ILMExpiryRules[opts.EntityValue] = rule
|
|
}
|
|
} else {
|
|
for id, rule := range allRules {
|
|
info.ILMExpiryRules[id] = rule
|
|
}
|
|
}
|
|
}
|
|
if opts.PeerState {
|
|
info.State = madmin.SRStateInfo{
|
|
Name: c.state.Name,
|
|
Peers: c.state.Peers,
|
|
UpdatedAt: c.state.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
if opts.Users || opts.Entity == madmin.SRUserEntity {
|
|
// Replicate policy mappings on local to all peers.
|
|
userPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
stsPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
svcPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
if opts.Entity == madmin.SRUserEntity {
|
|
if mp, ok := globalIAMSys.store.GetMappedPolicy(opts.EntityValue, false); ok {
|
|
userPolicyMap.Store(opts.EntityValue, mp)
|
|
}
|
|
} else {
|
|
stsErr := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, stsPolicyMap)
|
|
if stsErr != nil {
|
|
return info, errSRBackendIssue(stsErr)
|
|
}
|
|
usrErr := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap)
|
|
if usrErr != nil {
|
|
return info, errSRBackendIssue(usrErr)
|
|
}
|
|
svcErr := globalIAMSys.store.loadMappedPolicies(ctx, svcUser, false, svcPolicyMap)
|
|
if svcErr != nil {
|
|
return info, errSRBackendIssue(svcErr)
|
|
}
|
|
}
|
|
info.UserPolicies = make(map[string]madmin.SRPolicyMapping, userPolicyMap.Size())
|
|
addPolicy := func(t IAMUserType, mp *xsync.MapOf[string, MappedPolicy]) {
|
|
mp.Range(func(k string, mp MappedPolicy) bool {
|
|
info.UserPolicies[k] = madmin.SRPolicyMapping{
|
|
IsGroup: false,
|
|
UserOrGroup: k,
|
|
UserType: int(t),
|
|
Policy: mp.Policies,
|
|
UpdatedAt: mp.UpdatedAt,
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
addPolicy(regUser, userPolicyMap)
|
|
addPolicy(stsUser, stsPolicyMap)
|
|
addPolicy(svcUser, svcPolicyMap)
|
|
|
|
info.UserInfoMap = make(map[string]madmin.UserInfo)
|
|
if opts.Entity == madmin.SRUserEntity {
|
|
if ui, err := globalIAMSys.GetUserInfo(ctx, opts.EntityValue); err == nil {
|
|
info.UserInfoMap[opts.EntityValue] = ui
|
|
}
|
|
} else {
|
|
userAccounts := make(map[string]UserIdentity)
|
|
uerr := globalIAMSys.store.loadUsers(ctx, regUser, userAccounts)
|
|
if uerr != nil {
|
|
return info, errSRBackendIssue(uerr)
|
|
}
|
|
|
|
serr := globalIAMSys.store.loadUsers(ctx, svcUser, userAccounts)
|
|
if serr != nil {
|
|
return info, errSRBackendIssue(serr)
|
|
}
|
|
|
|
terr := globalIAMSys.store.loadUsers(ctx, stsUser, userAccounts)
|
|
if terr != nil {
|
|
return info, errSRBackendIssue(terr)
|
|
}
|
|
|
|
for k, v := range userAccounts {
|
|
if k == siteReplicatorSvcAcc {
|
|
// skip the site replicate svc account as it is
|
|
// already replicated.
|
|
continue
|
|
}
|
|
|
|
if v.Credentials.ParentUser != "" && v.Credentials.ParentUser == globalActiveCred.AccessKey {
|
|
// skip all root user service accounts.
|
|
continue
|
|
}
|
|
|
|
info.UserInfoMap[k] = madmin.UserInfo{
|
|
Status: madmin.AccountStatus(v.Credentials.Status),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if opts.Groups || opts.Entity == madmin.SRGroupEntity {
|
|
// Replicate policy mappings on local to all peers.
|
|
groupPolicyMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
if opts.Entity == madmin.SRGroupEntity {
|
|
if mp, ok := globalIAMSys.store.GetMappedPolicy(opts.EntityValue, true); ok {
|
|
groupPolicyMap.Store(opts.EntityValue, mp)
|
|
}
|
|
} else {
|
|
stsErr := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, true, groupPolicyMap)
|
|
if stsErr != nil {
|
|
return info, errSRBackendIssue(stsErr)
|
|
}
|
|
userErr := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap)
|
|
if userErr != nil {
|
|
return info, errSRBackendIssue(userErr)
|
|
}
|
|
}
|
|
|
|
info.GroupPolicies = make(map[string]madmin.SRPolicyMapping, groupPolicyMap.Size())
|
|
groupPolicyMap.Range(func(group string, mp MappedPolicy) bool {
|
|
info.GroupPolicies[group] = madmin.SRPolicyMapping{
|
|
IsGroup: true,
|
|
UserOrGroup: group,
|
|
Policy: mp.Policies,
|
|
UpdatedAt: mp.UpdatedAt,
|
|
}
|
|
return true
|
|
})
|
|
info.GroupDescMap = make(map[string]madmin.GroupDesc)
|
|
if opts.Entity == madmin.SRGroupEntity {
|
|
if gd, err := globalIAMSys.GetGroupDescription(opts.EntityValue); err == nil {
|
|
info.GroupDescMap[opts.EntityValue] = gd
|
|
}
|
|
} else {
|
|
// get users/group info on local.
|
|
groups, errG := globalIAMSys.store.listGroups(ctx)
|
|
if errG != nil {
|
|
return info, errSRBackendIssue(errG)
|
|
}
|
|
groupDescMap := make(map[string]madmin.GroupDesc, len(groups))
|
|
for _, g := range groups {
|
|
groupDescMap[g], errG = globalIAMSys.GetGroupDescription(g)
|
|
if errG != nil {
|
|
return info, errSRBackendIssue(errG)
|
|
}
|
|
}
|
|
for group, d := range groupDescMap {
|
|
info.GroupDescMap[group] = d
|
|
}
|
|
}
|
|
}
|
|
// cache SR metadata info for IAM
|
|
if opts.Users && opts.Groups && opts.Policies && !opts.Buckets {
|
|
c.srCacheIAMInfo(info)
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// EditPeerCluster - edits replication configuration and updates peer endpoint.
|
|
func (c *SiteReplicationSys) EditPeerCluster(ctx context.Context, peer madmin.PeerInfo, opts madmin.SREditOptions) (madmin.ReplicateEditStatus, error) {
|
|
sites, err := c.GetClusterInfo(ctx)
|
|
if err != nil {
|
|
return madmin.ReplicateEditStatus{}, errSRBackendIssue(err)
|
|
}
|
|
if !sites.Enabled {
|
|
return madmin.ReplicateEditStatus{}, errSRNotEnabled
|
|
}
|
|
|
|
var (
|
|
found bool
|
|
admClient *madmin.AdminClient
|
|
)
|
|
|
|
if globalDeploymentID() == peer.DeploymentID && !peer.SyncState.Empty() && !peer.DefaultBandwidth.IsSet {
|
|
return madmin.ReplicateEditStatus{}, errSRInvalidRequest(fmt.Errorf("a peer cluster, rather than the local cluster (endpoint=%s, deployment-id=%s) needs to be specified while setting a 'sync' replication mode", peer.Endpoint, peer.DeploymentID))
|
|
}
|
|
|
|
for _, v := range sites.Sites {
|
|
if peer.DeploymentID == v.DeploymentID {
|
|
found = true
|
|
if (!peer.SyncState.Empty() || peer.DefaultBandwidth.IsSet) && peer.Endpoint == "" { // peer.Endpoint may be "" if only sync state/bandwidth is being updated
|
|
break
|
|
}
|
|
if peer.Endpoint == v.Endpoint && peer.SyncState.Empty() && !peer.DefaultBandwidth.IsSet {
|
|
return madmin.ReplicateEditStatus{}, errSRInvalidRequest(fmt.Errorf("Endpoint %s entered for deployment id %s already configured in site replication", v.Endpoint, v.DeploymentID))
|
|
}
|
|
admClient, err = c.getAdminClientWithEndpoint(ctx, v.DeploymentID, peer.Endpoint)
|
|
if err != nil {
|
|
return madmin.ReplicateEditStatus{}, errSRPeerResp(fmt.Errorf("unable to create admin client for %s: %w", v.Name, err))
|
|
}
|
|
// check if endpoint is reachable
|
|
info, err := admClient.ServerInfo(ctx)
|
|
if err != nil {
|
|
return madmin.ReplicateEditStatus{}, errSRInvalidRequest(fmt.Errorf("Endpoint %s not reachable: %w", peer.Endpoint, err))
|
|
}
|
|
if info.DeploymentID != v.DeploymentID {
|
|
return madmin.ReplicateEditStatus{}, errSRInvalidRequest(fmt.Errorf("Endpoint %s does not belong to deployment expected: %s (found %s) ", peer.Endpoint, v.DeploymentID, info.DeploymentID))
|
|
}
|
|
}
|
|
}
|
|
|
|
// if disable/enable ILM expiry replication, deployment id not needed.
|
|
// check for below error only if other options being updated (e.g. endpoint, sync, bandwidth)
|
|
if !opts.DisableILMExpiryReplication && !opts.EnableILMExpiryReplication && !found {
|
|
return madmin.ReplicateEditStatus{}, errSRInvalidRequest(fmt.Errorf("%s not found in existing replicated sites", peer.DeploymentID))
|
|
}
|
|
successMsg := "Cluster replication configuration updated successfully with:"
|
|
var state srState
|
|
c.RLock()
|
|
state = c.state
|
|
c.RUnlock()
|
|
|
|
// in case of --disable-ilm-expiry-replication and --enable-ilm-expiry-replication
|
|
// --deployment-id is not passed
|
|
var (
|
|
prevPeerInfo, pi madmin.PeerInfo
|
|
)
|
|
if peer.DeploymentID != "" {
|
|
pi = c.state.Peers[peer.DeploymentID]
|
|
prevPeerInfo = pi
|
|
if !peer.SyncState.Empty() { // update replication to peer to be sync/async
|
|
pi.SyncState = peer.SyncState
|
|
successMsg = fmt.Sprintf("%s\n- sync state %s for peer %s", successMsg, peer.SyncState, peer.Name)
|
|
}
|
|
if peer.Endpoint != "" { // `admin replicate update` requested an endpoint change
|
|
pi.Endpoint = peer.Endpoint
|
|
successMsg = fmt.Sprintf("%s\n- endpoint %s for peer %s", successMsg, peer.Endpoint, peer.Name)
|
|
}
|
|
|
|
if peer.DefaultBandwidth.IsSet {
|
|
if peer.DeploymentID == globalDeploymentID() {
|
|
return madmin.ReplicateEditStatus{}, errSRInvalidRequest(fmt.Errorf("invalid deployment id specified: expecting a peer deployment-id to be specified for restricting bandwidth from %s, found self %s", peer.Name, globalDeploymentID()))
|
|
}
|
|
pi.DefaultBandwidth = peer.DefaultBandwidth
|
|
pi.DefaultBandwidth.UpdatedAt = UTCNow()
|
|
successMsg = fmt.Sprintf("%s\n- default bandwidth %v for peer %s", successMsg, peer.DefaultBandwidth.Limit, peer.Name)
|
|
}
|
|
state.Peers[peer.DeploymentID] = pi
|
|
}
|
|
|
|
// If ILM expiry replications enabled/disabled, set accordingly
|
|
if opts.DisableILMExpiryReplication {
|
|
for dID, pi := range state.Peers {
|
|
if !pi.ReplicateILMExpiry {
|
|
return madmin.ReplicateEditStatus{
|
|
Status: madmin.ReplicateAddStatusPartial,
|
|
ErrDetail: "ILM expiry already set to false",
|
|
}, nil
|
|
}
|
|
pi.ReplicateILMExpiry = false
|
|
state.Peers[dID] = pi
|
|
}
|
|
successMsg = fmt.Sprintf("%s\n- replicate-ilm-expiry: false", successMsg)
|
|
}
|
|
if opts.EnableILMExpiryReplication {
|
|
for dID, pi := range state.Peers {
|
|
if pi.ReplicateILMExpiry {
|
|
return madmin.ReplicateEditStatus{
|
|
Status: madmin.ReplicateAddStatusPartial,
|
|
ErrDetail: "ILM expiry already set to true",
|
|
}, nil
|
|
}
|
|
pi.ReplicateILMExpiry = true
|
|
state.Peers[dID] = pi
|
|
}
|
|
successMsg = fmt.Sprintf("%s\n- replicate-ilm-expiry: true", successMsg)
|
|
}
|
|
state.UpdatedAt = time.Now()
|
|
|
|
errs := make(map[string]error, len(state.Peers))
|
|
var wg sync.WaitGroup
|
|
|
|
for dID, v := range state.Peers {
|
|
if v.DeploymentID == globalDeploymentID() {
|
|
continue
|
|
}
|
|
// if individual deployment change like mode, endpoint, default bandwidth
|
|
// send it to all sites. Else send the current node details to all sites
|
|
// for ILM expiry flag update
|
|
var p madmin.PeerInfo
|
|
if peer.DeploymentID != "" {
|
|
p = pi
|
|
} else {
|
|
p = v
|
|
}
|
|
wg.Add(1)
|
|
go func(pi madmin.PeerInfo, dID string) {
|
|
defer wg.Done()
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if dID == peer.DeploymentID {
|
|
admClient, err = c.getAdminClientWithEndpoint(ctx, dID, pi.Endpoint)
|
|
}
|
|
if err != nil {
|
|
errs[dID] = errSRPeerResp(fmt.Errorf("unable to create admin client for %s: %w", pi.Name, err))
|
|
return
|
|
}
|
|
if err = admClient.SRPeerEdit(ctx, pi); err != nil {
|
|
errs[dID] = errSRPeerResp(fmt.Errorf("unable to update peer %s: %w", pi.Name, err))
|
|
return
|
|
}
|
|
}(p, dID)
|
|
}
|
|
|
|
wg.Wait()
|
|
for dID, err := range errs {
|
|
replLogOnceIf(ctx, fmt.Errorf("unable to update peer %s: %w", state.Peers[dID].Name, err), "site-relication-edit")
|
|
}
|
|
|
|
// we can now save the cluster replication configuration state.
|
|
if err = c.saveToDisk(ctx, state); err != nil {
|
|
return madmin.ReplicateEditStatus{
|
|
Status: madmin.ReplicateAddStatusPartial,
|
|
ErrDetail: fmt.Sprintf("unable to save cluster-replication state on local: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
if peer.DeploymentID != "" {
|
|
if err = c.updateTargetEndpoints(ctx, prevPeerInfo, pi); err != nil {
|
|
return madmin.ReplicateEditStatus{
|
|
Status: madmin.ReplicateAddStatusPartial,
|
|
ErrDetail: fmt.Sprintf("unable to update peer targets on local: %v", err),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// set partial error message if remote site updates failed for few cases
|
|
if len(errs) > 0 {
|
|
successMsg = fmt.Sprintf("%s\n- partially failed for few remote sites as they could be down/unreachable at the moment", successMsg)
|
|
}
|
|
result := madmin.ReplicateEditStatus{
|
|
Success: true,
|
|
Status: successMsg,
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) updateTargetEndpoints(ctx context.Context, prevInfo, peer madmin.PeerInfo) error {
|
|
objAPI := newObjectLayerFn()
|
|
if objAPI == nil {
|
|
return errSRObjectLayerNotReady
|
|
}
|
|
|
|
buckets, err := objAPI.ListBuckets(ctx, BucketOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, bucketInfo := range buckets {
|
|
bucket := bucketInfo.Name
|
|
ep, _ := url.Parse(peer.Endpoint)
|
|
prevEp, _ := url.Parse(prevInfo.Endpoint)
|
|
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
|
if err != nil {
|
|
continue // site healing will take care of configuring new targets
|
|
}
|
|
for _, target := range targets.Targets {
|
|
if target.SourceBucket == bucket &&
|
|
target.TargetBucket == bucket &&
|
|
target.Endpoint == prevEp.Host &&
|
|
target.Secure == (prevEp.Scheme == "https") &&
|
|
target.Type == madmin.ReplicationService {
|
|
bucketTarget := target
|
|
bucketTarget.Secure = ep.Scheme == "https"
|
|
bucketTarget.Endpoint = ep.Host
|
|
if peer.DefaultBandwidth.IsSet && target.BandwidthLimit == 0 {
|
|
bucketTarget.BandwidthLimit = int64(peer.DefaultBandwidth.Limit)
|
|
}
|
|
if !peer.SyncState.Empty() {
|
|
bucketTarget.ReplicationSync = (peer.SyncState == madmin.SyncEnabled)
|
|
}
|
|
err := globalBucketTargetSys.SetTarget(ctx, bucket, &bucketTarget, true)
|
|
if err != nil {
|
|
replLogIf(ctx, c.annotatePeerErr(peer.Name, "Bucket target creation error", err))
|
|
continue
|
|
}
|
|
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
|
if err != nil {
|
|
replLogIf(ctx, err)
|
|
continue
|
|
}
|
|
tgtBytes, err := json.Marshal(&targets)
|
|
if err != nil {
|
|
bugLogIf(ctx, err)
|
|
continue
|
|
}
|
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
|
replLogIf(ctx, err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PeerEditReq - internal API handler to respond to a peer cluster's request
|
|
// to edit endpoint.
|
|
func (c *SiteReplicationSys) PeerEditReq(ctx context.Context, arg madmin.PeerInfo) error {
|
|
ourName := ""
|
|
|
|
// Set ReplicateILMExpiry for all peers
|
|
currTime := time.Now()
|
|
for i := range c.state.Peers {
|
|
p := c.state.Peers[i]
|
|
if p.ReplicateILMExpiry == arg.ReplicateILMExpiry {
|
|
// its already set due to previous edit req
|
|
break
|
|
}
|
|
p.ReplicateILMExpiry = arg.ReplicateILMExpiry
|
|
c.state.UpdatedAt = currTime
|
|
c.state.Peers[i] = p
|
|
}
|
|
|
|
for i := range c.state.Peers {
|
|
p := c.state.Peers[i]
|
|
if p.DeploymentID == arg.DeploymentID {
|
|
p.Endpoint = arg.Endpoint
|
|
c.state.Peers[arg.DeploymentID] = p
|
|
}
|
|
if p.DeploymentID == globalDeploymentID() {
|
|
ourName = p.Name
|
|
}
|
|
}
|
|
if err := c.saveToDisk(ctx, c.state); err != nil {
|
|
return errSRBackendIssue(fmt.Errorf("unable to save cluster-replication state to drive on %s: %v", ourName, err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PeerStateEditReq - internal API handler to respond to a peer cluster's request
|
|
// to edit state.
|
|
func (c *SiteReplicationSys) PeerStateEditReq(ctx context.Context, arg madmin.SRStateEditReq) error {
|
|
if arg.UpdatedAt.After(c.state.UpdatedAt) {
|
|
state := c.state
|
|
// update only the ReplicateILMExpiry flag for the peers from incoming request
|
|
for _, peer := range arg.Peers {
|
|
currPeer := c.state.Peers[peer.DeploymentID]
|
|
currPeer.ReplicateILMExpiry = peer.ReplicateILMExpiry
|
|
state.Peers[peer.DeploymentID] = currPeer
|
|
}
|
|
state.UpdatedAt = arg.UpdatedAt
|
|
if err := c.saveToDisk(ctx, state); err != nil {
|
|
return errSRBackendIssue(fmt.Errorf("unable to save cluster-replication state to drive on %s: %v", state.Name, err))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const siteHealTimeInterval = 30 * time.Second
|
|
|
|
func (c *SiteReplicationSys) startHealRoutine(ctx context.Context, objAPI ObjectLayer) {
|
|
ctx, cancel := globalLeaderLock.GetLock(ctx)
|
|
defer cancel()
|
|
|
|
healTimer := time.NewTimer(siteHealTimeInterval)
|
|
defer healTimer.Stop()
|
|
|
|
var maxRefreshDurationSecondsForLog float64 = 10 // 10 seconds..
|
|
|
|
for {
|
|
select {
|
|
case <-healTimer.C:
|
|
c.RLock()
|
|
enabled := c.enabled
|
|
c.RUnlock()
|
|
if enabled {
|
|
refreshStart := time.Now()
|
|
c.healIAMSystem(ctx, objAPI) // heal IAM system first
|
|
c.healBuckets(ctx, objAPI) // heal buckets subsequently
|
|
|
|
took := time.Since(refreshStart).Seconds()
|
|
if took > maxRefreshDurationSecondsForLog {
|
|
// Log if we took a lot of time.
|
|
logger.Info("Site replication healing refresh took %.2fs", took)
|
|
}
|
|
|
|
// wait for 200 millisecond, if we are experience lot of I/O
|
|
waitForLowIO(runtime.GOMAXPROCS(0), 200*time.Millisecond, currentHTTPIO)
|
|
}
|
|
healTimer.Reset(siteHealTimeInterval)
|
|
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type srBucketStatsSummary struct {
|
|
madmin.SRBucketStatsSummary
|
|
meta srBucketMetaInfo
|
|
}
|
|
|
|
type srPolicyStatsSummary struct {
|
|
madmin.SRPolicyStatsSummary
|
|
policy srPolicy
|
|
}
|
|
|
|
type srUserStatsSummary struct {
|
|
madmin.SRUserStatsSummary
|
|
userInfo srUserInfo
|
|
userPolicy srPolicyMapping
|
|
}
|
|
|
|
type srGroupStatsSummary struct {
|
|
madmin.SRGroupStatsSummary
|
|
groupDesc srGroupDesc
|
|
groupPolicy srPolicyMapping
|
|
}
|
|
|
|
type srILMExpiryRuleStatsSummary struct {
|
|
madmin.SRILMExpiryStatsSummary
|
|
ilmExpiryRule srILMExpiryRule
|
|
}
|
|
|
|
type srStatusInfo struct {
|
|
// SRStatusInfo returns detailed status on site replication status
|
|
Enabled bool
|
|
MaxBuckets int // maximum buckets seen across sites
|
|
MaxUsers int // maximum users seen across sites
|
|
MaxGroups int // maximum groups seen across sites
|
|
MaxPolicies int // maximum policies across sites
|
|
MaxILMExpiryRules int // maximum ILM expiry rules across sites
|
|
Sites map[string]madmin.PeerInfo // deployment->sitename
|
|
StatsSummary map[string]madmin.SRSiteSummary // map of deployment id -> site stat
|
|
// BucketStats map of bucket to slice of deployment IDs with stats. This is populated only if there are
|
|
// mismatches or if a specific bucket's stats are requested
|
|
BucketStats map[string]map[string]srBucketStatsSummary
|
|
// PolicyStats map of policy to slice of deployment IDs with stats. This is populated only if there are
|
|
// mismatches or if a specific bucket's stats are requested
|
|
PolicyStats map[string]map[string]srPolicyStatsSummary
|
|
// UserStats map of user to slice of deployment IDs with stats. This is populated only if there are
|
|
// mismatches or if a specific bucket's stats are requested
|
|
UserStats map[string]map[string]srUserStatsSummary
|
|
// GroupStats map of group to slice of deployment IDs with stats. This is populated only if there are
|
|
// mismatches or if a specific bucket's stats are requested
|
|
GroupStats map[string]map[string]srGroupStatsSummary
|
|
// ILMExpiryRulesStats map of ILM expiry rules to slice of deployment IDs with stats. This is populated only if there are
|
|
// mismatches or if a specific ILM expiry rule's stats are requested
|
|
ILMExpiryRulesStats map[string]map[string]srILMExpiryRuleStatsSummary
|
|
// PeerStates map of site replication sites to their site replication states
|
|
PeerStates map[string]madmin.SRStateInfo
|
|
Metrics madmin.SRMetricsSummary
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// SRBucketDeleteOp - type of delete op
|
|
type SRBucketDeleteOp string
|
|
|
|
const (
|
|
// MarkDelete creates .minio.sys/buckets/.deleted/<bucket> vol entry to hold onto deleted bucket's state
|
|
// until peers are synced in site replication setup.
|
|
MarkDelete SRBucketDeleteOp = "MarkDelete"
|
|
|
|
// Purge deletes the .minio.sys/buckets/.deleted/<bucket> vol entry
|
|
Purge SRBucketDeleteOp = "Purge"
|
|
// NoOp no action needed
|
|
NoOp SRBucketDeleteOp = "NoOp"
|
|
)
|
|
|
|
// Empty returns true if this Op is not set
|
|
func (s SRBucketDeleteOp) Empty() bool {
|
|
return string(s) == "" || string(s) == string(NoOp)
|
|
}
|
|
|
|
func getSRBucketDeleteOp(isSiteReplicated bool) SRBucketDeleteOp {
|
|
if !isSiteReplicated {
|
|
return NoOp
|
|
}
|
|
return MarkDelete
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healILMExpiryConfig(ctx context.Context, objAPI ObjectLayer, info srStatusInfo) error {
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestPeers map[string]madmin.PeerInfo
|
|
)
|
|
|
|
for dID, ps := range info.PeerStates {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ps.UpdatedAt
|
|
latestID = dID
|
|
latestPeers = ps.Peers
|
|
}
|
|
if ps.UpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ps.UpdatedAt
|
|
latestID = dID
|
|
latestPeers = ps.Peers
|
|
}
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
|
|
for dID, ps := range info.PeerStates {
|
|
// If latest peers ILM expiry flags are equal to current peer, no need to heal
|
|
flagEqual := true
|
|
for id, peer := range latestPeers {
|
|
if !(ps.Peers[id].ReplicateILMExpiry == peer.ReplicateILMExpiry) {
|
|
flagEqual = false
|
|
break
|
|
}
|
|
}
|
|
if flagEqual {
|
|
continue
|
|
}
|
|
|
|
// Dont apply the self state to self
|
|
if dID == globalDeploymentID() {
|
|
continue
|
|
}
|
|
|
|
// Send details to other sites for healing
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
if err = admClient.SRStateEdit(ctx, madmin.SRStateEditReq{Peers: latestPeers, UpdatedAt: lastUpdate}); err != nil {
|
|
replLogIf(ctx, c.annotatePeerErr(ps.Name, siteReplicationEdit,
|
|
fmt.Errorf("Unable to heal site replication state for peer %s from peer %s : %w",
|
|
ps.Name, latestPeerName, err)))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healBuckets(ctx context.Context, objAPI ObjectLayer) error {
|
|
buckets, err := c.listBuckets(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ilmExpiryCfgHealed := false
|
|
opts := validateReplicationDestinationOptions{CheckReady: true}
|
|
for _, bi := range buckets {
|
|
bucket := bi.Name
|
|
info, err := c.siteReplicationStatus(ctx, objAPI, madmin.SRStatusOptions{
|
|
Entity: madmin.SRBucketEntity,
|
|
EntityValue: bucket,
|
|
ShowDeleted: true,
|
|
ILMExpiryRules: true,
|
|
PeerState: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.healBucket(ctx, objAPI, bucket, info)
|
|
|
|
if bi.Deleted.IsZero() || (!bi.Created.IsZero() && bi.Deleted.Before(bi.Created)) {
|
|
c.healVersioningMetadata(ctx, objAPI, bucket, info)
|
|
c.healOLockConfigMetadata(ctx, objAPI, bucket, info)
|
|
c.healSSEMetadata(ctx, objAPI, bucket, info)
|
|
c.healBucketReplicationConfig(ctx, objAPI, bucket, info, &opts)
|
|
c.healBucketPolicies(ctx, objAPI, bucket, info)
|
|
c.healTagMetadata(ctx, objAPI, bucket, info)
|
|
c.healBucketQuotaConfig(ctx, objAPI, bucket, info)
|
|
if !ilmExpiryCfgHealed {
|
|
c.healILMExpiryConfig(ctx, objAPI, info)
|
|
ilmExpiryCfgHealed = true
|
|
}
|
|
if ilmExpiryReplicationEnabled(c.state.Peers) {
|
|
c.healBucketILMExpiry(ctx, objAPI, bucket, info)
|
|
}
|
|
}
|
|
// Notification and ILM are site specific settings.
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healBucketILMExpiry(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
|
bs := info.BucketStats[bucket]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestExpLCConfig *string
|
|
)
|
|
|
|
for dID, ss := range bs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.meta.ExpiryLCConfigUpdatedAt
|
|
latestID = dID
|
|
latestExpLCConfig = ss.meta.ExpiryLCConfig
|
|
}
|
|
// avoid considering just created buckets as latest. Perhaps this site
|
|
// just joined cluster replication and yet to be sync'd
|
|
if ss.meta.CreatedAt.Equal(ss.meta.ExpiryLCConfigUpdatedAt) {
|
|
continue
|
|
}
|
|
if ss.meta.ExpiryLCConfigUpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.meta.ExpiryLCConfigUpdatedAt
|
|
latestID = dID
|
|
latestExpLCConfig = ss.meta.ExpiryLCConfig
|
|
}
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
var err error
|
|
if latestExpLCConfig != nil {
|
|
_, err = base64.StdEncoding.DecodeString(*latestExpLCConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for dID, bStatus := range bs {
|
|
if latestExpLCConfig != nil && bStatus.meta.ExpiryLCConfig != nil && strings.EqualFold(*latestExpLCConfig, *bStatus.meta.ExpiryLCConfig) {
|
|
continue
|
|
}
|
|
|
|
finalConfigData, err := mergeWithCurrentLCConfig(ctx, bucket, latestExpLCConfig, lastUpdate)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
|
|
if dID == globalDeploymentID() {
|
|
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, finalConfigData); err != nil {
|
|
replLogIf(ctx, fmt.Errorf("Unable to heal bucket ILM expiry data from peer site %s : %w", latestPeerName, err))
|
|
}
|
|
continue
|
|
}
|
|
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
if err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaLCConfig,
|
|
Bucket: bucket,
|
|
ExpiryLCConfig: latestExpLCConfig,
|
|
UpdatedAt: lastUpdate,
|
|
}); err != nil {
|
|
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
|
fmt.Errorf("Unable to heal bucket ILM expiry data for peer %s from peer %s : %w",
|
|
peerName, latestPeerName, err)))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healTagMetadata(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
|
bs := info.BucketStats[bucket]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestTaggingConfig *string
|
|
)
|
|
|
|
for dID, ss := range bs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.meta.TagConfigUpdatedAt
|
|
latestID = dID
|
|
latestTaggingConfig = ss.meta.Tags
|
|
}
|
|
// avoid considering just created buckets as latest. Perhaps this site
|
|
// just joined cluster replication and yet to be sync'd
|
|
if ss.meta.CreatedAt.Equal(ss.meta.TagConfigUpdatedAt) {
|
|
continue
|
|
}
|
|
if ss.meta.TagConfigUpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.meta.TagConfigUpdatedAt
|
|
latestID = dID
|
|
latestTaggingConfig = ss.meta.Tags
|
|
}
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
var latestTaggingConfigBytes []byte
|
|
var err error
|
|
if latestTaggingConfig != nil {
|
|
latestTaggingConfigBytes, err = base64.StdEncoding.DecodeString(*latestTaggingConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for dID, bStatus := range bs {
|
|
if !bStatus.TagMismatch {
|
|
continue
|
|
}
|
|
if isBucketMetadataEqual(latestTaggingConfig, bStatus.meta.Tags) {
|
|
continue
|
|
}
|
|
if dID == globalDeploymentID() {
|
|
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, latestTaggingConfigBytes); err != nil {
|
|
replLogIf(ctx, fmt.Errorf("Unable to heal tagging metadata from peer site %s : %w", latestPeerName, err))
|
|
}
|
|
continue
|
|
}
|
|
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypeTags,
|
|
Bucket: bucket,
|
|
Tags: latestTaggingConfig,
|
|
})
|
|
if err != nil {
|
|
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
|
fmt.Errorf("Unable to heal tagging metadata for peer %s from peer %s : %w", peerName, latestPeerName, err)))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healBucketPolicies(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
|
bs := info.BucketStats[bucket]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestIAMPolicy json.RawMessage
|
|
)
|
|
|
|
for dID, ss := range bs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.meta.PolicyUpdatedAt
|
|
latestID = dID
|
|
latestIAMPolicy = ss.meta.Policy
|
|
}
|
|
// avoid considering just created buckets as latest. Perhaps this site
|
|
// just joined cluster replication and yet to be sync'd
|
|
if ss.meta.CreatedAt.Equal(ss.meta.PolicyUpdatedAt) {
|
|
continue
|
|
}
|
|
if ss.meta.PolicyUpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.meta.PolicyUpdatedAt
|
|
latestID = dID
|
|
latestIAMPolicy = ss.meta.Policy
|
|
}
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
for dID, bStatus := range bs {
|
|
if !bStatus.PolicyMismatch {
|
|
continue
|
|
}
|
|
if strings.EqualFold(string(latestIAMPolicy), string(bStatus.meta.Policy)) {
|
|
continue
|
|
}
|
|
if dID == globalDeploymentID() {
|
|
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, latestIAMPolicy); err != nil {
|
|
replLogIf(ctx, fmt.Errorf("Unable to heal bucket policy metadata from peer site %s : %w", latestPeerName, err))
|
|
}
|
|
continue
|
|
}
|
|
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
if err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypePolicy,
|
|
Bucket: bucket,
|
|
Policy: latestIAMPolicy,
|
|
UpdatedAt: lastUpdate,
|
|
}); err != nil {
|
|
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
|
fmt.Errorf("Unable to heal bucket policy metadata for peer %s from peer %s : %w",
|
|
peerName, latestPeerName, err)))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healBucketQuotaConfig(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
|
bs := info.BucketStats[bucket]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestQuotaConfig *string
|
|
latestQuotaConfigBytes []byte
|
|
)
|
|
|
|
for dID, ss := range bs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.meta.QuotaConfigUpdatedAt
|
|
latestID = dID
|
|
latestQuotaConfig = ss.meta.QuotaConfig
|
|
}
|
|
// avoid considering just created buckets as latest. Perhaps this site
|
|
// just joined cluster replication and yet to be sync'd
|
|
if ss.meta.CreatedAt.Equal(ss.meta.QuotaConfigUpdatedAt) {
|
|
continue
|
|
}
|
|
if ss.meta.QuotaConfigUpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.meta.QuotaConfigUpdatedAt
|
|
latestID = dID
|
|
latestQuotaConfig = ss.meta.QuotaConfig
|
|
}
|
|
}
|
|
|
|
var err error
|
|
if latestQuotaConfig != nil {
|
|
latestQuotaConfigBytes, err = base64.StdEncoding.DecodeString(*latestQuotaConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
latestPeerName = info.Sites[latestID].Name
|
|
for dID, bStatus := range bs {
|
|
if !bStatus.QuotaCfgMismatch {
|
|
continue
|
|
}
|
|
if isBucketMetadataEqual(latestQuotaConfig, bStatus.meta.QuotaConfig) {
|
|
continue
|
|
}
|
|
if dID == globalDeploymentID() {
|
|
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, latestQuotaConfigBytes); err != nil {
|
|
replLogIf(ctx, fmt.Errorf("Unable to heal quota metadata from peer site %s : %w", latestPeerName, err))
|
|
}
|
|
continue
|
|
}
|
|
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
|
|
if err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
|
Bucket: bucket,
|
|
Quota: latestQuotaConfigBytes,
|
|
UpdatedAt: lastUpdate,
|
|
}); err != nil {
|
|
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
|
fmt.Errorf("Unable to heal quota config metadata for peer %s from peer %s : %w",
|
|
peerName, latestPeerName, err)))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healVersioningMetadata(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestVersioningConfig *string
|
|
)
|
|
|
|
bs := info.BucketStats[bucket]
|
|
for dID, ss := range bs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.meta.VersioningConfigUpdatedAt
|
|
latestID = dID
|
|
latestVersioningConfig = ss.meta.Versioning
|
|
}
|
|
// avoid considering just created buckets as latest. Perhaps this site
|
|
// just joined cluster replication and yet to be sync'd
|
|
if ss.meta.CreatedAt.Equal(ss.meta.VersioningConfigUpdatedAt) {
|
|
continue
|
|
}
|
|
if ss.meta.VersioningConfigUpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.meta.VersioningConfigUpdatedAt
|
|
latestID = dID
|
|
latestVersioningConfig = ss.meta.Versioning
|
|
}
|
|
}
|
|
|
|
latestPeerName = info.Sites[latestID].Name
|
|
var latestVersioningConfigBytes []byte
|
|
var err error
|
|
if latestVersioningConfig != nil {
|
|
latestVersioningConfigBytes, err = base64.StdEncoding.DecodeString(*latestVersioningConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for dID, bStatus := range bs {
|
|
if !bStatus.VersioningConfigMismatch {
|
|
continue
|
|
}
|
|
if isBucketMetadataEqual(latestVersioningConfig, bStatus.meta.Versioning) {
|
|
continue
|
|
}
|
|
if dID == globalDeploymentID() {
|
|
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, latestVersioningConfigBytes); err != nil {
|
|
replLogIf(ctx, fmt.Errorf("Unable to heal versioning metadata from peer site %s : %w", latestPeerName, err))
|
|
}
|
|
continue
|
|
}
|
|
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypeVersionConfig,
|
|
Bucket: bucket,
|
|
Versioning: latestVersioningConfig,
|
|
UpdatedAt: lastUpdate,
|
|
})
|
|
if err != nil {
|
|
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
|
fmt.Errorf("Unable to heal versioning config metadata for peer %s from peer %s : %w",
|
|
peerName, latestPeerName, err)))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healSSEMetadata(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestSSEConfig *string
|
|
)
|
|
|
|
bs := info.BucketStats[bucket]
|
|
for dID, ss := range bs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.meta.SSEConfigUpdatedAt
|
|
latestID = dID
|
|
latestSSEConfig = ss.meta.SSEConfig
|
|
}
|
|
// avoid considering just created buckets as latest. Perhaps this site
|
|
// just joined cluster replication and yet to be sync'd
|
|
if ss.meta.CreatedAt.Equal(ss.meta.SSEConfigUpdatedAt) {
|
|
continue
|
|
}
|
|
if ss.meta.SSEConfigUpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.meta.SSEConfigUpdatedAt
|
|
latestID = dID
|
|
latestSSEConfig = ss.meta.SSEConfig
|
|
}
|
|
}
|
|
|
|
latestPeerName = info.Sites[latestID].Name
|
|
var latestSSEConfigBytes []byte
|
|
var err error
|
|
if latestSSEConfig != nil {
|
|
latestSSEConfigBytes, err = base64.StdEncoding.DecodeString(*latestSSEConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for dID, bStatus := range bs {
|
|
if !bStatus.SSEConfigMismatch {
|
|
continue
|
|
}
|
|
if isBucketMetadataEqual(latestSSEConfig, bStatus.meta.SSEConfig) {
|
|
continue
|
|
}
|
|
if dID == globalDeploymentID() {
|
|
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, latestSSEConfigBytes); err != nil {
|
|
replLogIf(ctx, fmt.Errorf("Unable to heal sse metadata from peer site %s : %w", latestPeerName, err))
|
|
}
|
|
continue
|
|
}
|
|
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypeSSEConfig,
|
|
Bucket: bucket,
|
|
SSEConfig: latestSSEConfig,
|
|
UpdatedAt: lastUpdate,
|
|
})
|
|
if err != nil {
|
|
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
|
fmt.Errorf("Unable to heal SSE config metadata for peer %s from peer %s : %w",
|
|
peerName, latestPeerName, err)))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healOLockConfigMetadata(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
|
bs := info.BucketStats[bucket]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestObjLockConfig *string
|
|
)
|
|
|
|
for dID, ss := range bs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.meta.ObjectLockConfigUpdatedAt
|
|
latestID = dID
|
|
latestObjLockConfig = ss.meta.ObjectLockConfig
|
|
}
|
|
// avoid considering just created buckets as latest. Perhaps this site
|
|
// just joined cluster replication and yet to be sync'd
|
|
if ss.meta.CreatedAt.Equal(ss.meta.ObjectLockConfigUpdatedAt) {
|
|
continue
|
|
}
|
|
if ss.meta.ObjectLockConfig != nil && ss.meta.ObjectLockConfigUpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.meta.ObjectLockConfigUpdatedAt
|
|
latestID = dID
|
|
latestObjLockConfig = ss.meta.ObjectLockConfig
|
|
}
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
var latestObjLockConfigBytes []byte
|
|
var err error
|
|
if latestObjLockConfig != nil {
|
|
latestObjLockConfigBytes, err = base64.StdEncoding.DecodeString(*latestObjLockConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for dID, bStatus := range bs {
|
|
if !bStatus.OLockConfigMismatch {
|
|
continue
|
|
}
|
|
if isBucketMetadataEqual(latestObjLockConfig, bStatus.meta.ObjectLockConfig) {
|
|
continue
|
|
}
|
|
if dID == globalDeploymentID() {
|
|
if _, err := globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, latestObjLockConfigBytes); err != nil {
|
|
replLogIf(ctx, fmt.Errorf("Unable to heal objectlock config metadata from peer site %s : %w", latestPeerName, err))
|
|
}
|
|
continue
|
|
}
|
|
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return wrapSRErr(err)
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
|
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
|
Bucket: bucket,
|
|
Tags: latestObjLockConfig,
|
|
UpdatedAt: lastUpdate,
|
|
})
|
|
if err != nil {
|
|
replLogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
|
fmt.Errorf("Unable to heal object lock config metadata for peer %s from peer %s : %w",
|
|
peerName, latestPeerName, err)))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) purgeDeletedBucket(ctx context.Context, objAPI ObjectLayer, bucket string) {
|
|
z, ok := objAPI.(*erasureServerPools)
|
|
if !ok {
|
|
return
|
|
}
|
|
z.s3Peer.DeleteBucket(context.Background(), pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, bucket), DeleteBucketOptions{})
|
|
}
|
|
|
|
// healBucket creates/deletes the bucket according to latest state across clusters participating in site replication.
|
|
func (c *SiteReplicationSys) healBucket(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo) error {
|
|
bs := info.BucketStats[bucket]
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
numSites := len(c.state.Peers)
|
|
mostRecent := func(d1, d2 time.Time) time.Time {
|
|
if d1.IsZero() {
|
|
return d2
|
|
}
|
|
if d2.IsZero() {
|
|
return d1
|
|
}
|
|
if d1.After(d2) {
|
|
return d1
|
|
}
|
|
return d2
|
|
}
|
|
|
|
var (
|
|
latestID string
|
|
lastUpdate time.Time
|
|
withB []string
|
|
missingB []string
|
|
deletedCnt int
|
|
)
|
|
for dID, ss := range bs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = mostRecent(ss.meta.CreatedAt, ss.meta.DeletedAt)
|
|
latestID = dID
|
|
}
|
|
recentUpdt := mostRecent(ss.meta.CreatedAt, ss.meta.DeletedAt)
|
|
if recentUpdt.After(lastUpdate) {
|
|
lastUpdate = recentUpdt
|
|
latestID = dID
|
|
}
|
|
if ss.BucketMarkedDeleted {
|
|
deletedCnt++
|
|
}
|
|
if ss.HasBucket {
|
|
withB = append(withB, dID)
|
|
} else {
|
|
missingB = append(missingB, dID)
|
|
}
|
|
}
|
|
|
|
latestPeerName := info.Sites[latestID].Name
|
|
bStatus := info.BucketStats[bucket][latestID].meta
|
|
isMakeBucket := len(missingB) > 0
|
|
deleteOp := NoOp
|
|
if latestID != globalDeploymentID() {
|
|
return nil
|
|
}
|
|
if lastUpdate.Equal(bStatus.DeletedAt) {
|
|
isMakeBucket = false
|
|
switch {
|
|
case len(withB) == numSites && deletedCnt == numSites:
|
|
deleteOp = NoOp
|
|
case len(withB) == 0 && len(missingB) == numSites:
|
|
deleteOp = Purge
|
|
default:
|
|
deleteOp = MarkDelete
|
|
}
|
|
}
|
|
if isMakeBucket {
|
|
var opts MakeBucketOptions
|
|
optsMap := make(map[string]string)
|
|
optsMap["versioningEnabled"] = "true"
|
|
opts.VersioningEnabled = true
|
|
opts.CreatedAt = bStatus.CreatedAt
|
|
optsMap["createdAt"] = bStatus.CreatedAt.UTC().Format(time.RFC3339Nano)
|
|
|
|
if bStatus.ObjectLockConfig != nil {
|
|
config, err := base64.StdEncoding.DecodeString(*bStatus.ObjectLockConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if bytes.Equal([]byte(string(config)), enabledBucketObjectLockConfig) {
|
|
optsMap["lockEnabled"] = "true"
|
|
opts.LockEnabled = true
|
|
}
|
|
}
|
|
for _, dID := range missingB {
|
|
peerName := info.Sites[dID].Name
|
|
if dID == globalDeploymentID() {
|
|
err := c.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
|
|
if err != nil {
|
|
return c.annotateErr(makeBucketWithVersion, fmt.Errorf("error healing bucket for site replication %w from %s -> %s",
|
|
err, latestPeerName, peerName))
|
|
}
|
|
} else {
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return c.annotateErr(configureReplication, fmt.Errorf("unable to use admin client for %s: %w", dID, err))
|
|
}
|
|
if err = admClient.SRPeerBucketOps(ctx, bucket, madmin.MakeWithVersioningBktOp, optsMap); err != nil {
|
|
return c.annotatePeerErr(peerName, makeBucketWithVersion, err)
|
|
}
|
|
if err = admClient.SRPeerBucketOps(ctx, bucket, madmin.ConfigureReplBktOp, nil); err != nil {
|
|
return c.annotatePeerErr(peerName, configureReplication, err)
|
|
}
|
|
}
|
|
}
|
|
if len(missingB) > 0 {
|
|
// configure replication from current cluster to other clusters
|
|
err := c.PeerBucketConfigureReplHandler(ctx, bucket)
|
|
if err != nil {
|
|
return c.annotateErr(configureReplication, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
// all buckets are marked deleted across sites at this point. It should be safe to purge the .minio.sys/buckets/.deleted/<bucket> entry
|
|
// from disk
|
|
if deleteOp == Purge {
|
|
for _, dID := range missingB {
|
|
peerName := info.Sites[dID].Name
|
|
if dID == globalDeploymentID() {
|
|
c.purgeDeletedBucket(ctx, objAPI, bucket)
|
|
} else {
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return c.annotateErr(configureReplication, fmt.Errorf("unable to use admin client for %s: %w", dID, err))
|
|
}
|
|
if err = admClient.SRPeerBucketOps(ctx, bucket, madmin.PurgeDeletedBucketOp, nil); err != nil {
|
|
return c.annotatePeerErr(peerName, deleteBucket, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Mark buckets deleted on remaining peers
|
|
if deleteOp == MarkDelete {
|
|
for _, dID := range withB {
|
|
peerName := info.Sites[dID].Name
|
|
if dID == globalDeploymentID() {
|
|
err := c.PeerBucketDeleteHandler(ctx, bucket, DeleteBucketOptions{
|
|
Force: true,
|
|
})
|
|
if err != nil {
|
|
return c.annotateErr(deleteBucket, fmt.Errorf("error healing bucket for site replication %w from %s -> %s",
|
|
err, latestPeerName, peerName))
|
|
}
|
|
} else {
|
|
admClient, err := c.getAdminClient(ctx, dID)
|
|
if err != nil {
|
|
return c.annotateErr(configureReplication, fmt.Errorf("unable to use admin client for %s: %w", dID, err))
|
|
}
|
|
if err = admClient.SRPeerBucketOps(ctx, bucket, madmin.ForceDeleteBucketBktOp, nil); err != nil {
|
|
return c.annotatePeerErr(peerName, deleteBucket, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healBucketReplicationConfig(ctx context.Context, objAPI ObjectLayer, bucket string, info srStatusInfo, opts *validateReplicationDestinationOptions) error {
|
|
bs := info.BucketStats[bucket]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
|
|
var replMismatch bool
|
|
for _, ss := range bs {
|
|
if ss.ReplicationCfgMismatch {
|
|
replMismatch = true
|
|
break
|
|
}
|
|
}
|
|
rcfg, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
|
if err != nil {
|
|
_, ok := err.(BucketReplicationConfigNotFound)
|
|
if !ok {
|
|
return err
|
|
}
|
|
replMismatch = true
|
|
}
|
|
|
|
var (
|
|
epDeplIDMap = make(map[string]string)
|
|
arnTgtMap = make(map[string]madmin.BucketTarget)
|
|
)
|
|
if targetsPtr, _ := globalBucketTargetSys.ListBucketTargets(ctx, bucket); targetsPtr != nil {
|
|
for _, t := range targetsPtr.Targets {
|
|
arnTgtMap[t.Arn] = t
|
|
}
|
|
}
|
|
for _, p := range c.state.Peers {
|
|
epDeplIDMap[p.Endpoint] = p.DeploymentID
|
|
}
|
|
// fix stale ARN's in replication config and endpoint mismatch between site config and
|
|
// targets associated to this config.
|
|
if rcfg != nil {
|
|
for _, rule := range rcfg.Rules {
|
|
if rule.Status != sreplication.Status(replication.Disabled) {
|
|
tgt, isValidARN := arnTgtMap[rule.Destination.ARN] // detect stale ARN in replication config
|
|
_, epFound := epDeplIDMap[tgt.URL().String()] // detect end point change at site level
|
|
if !isValidARN || !epFound {
|
|
replMismatch = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if rcfg != nil && !replMismatch {
|
|
// validate remote targets on current cluster for this bucket
|
|
_, apiErr := validateReplicationDestination(ctx, bucket, rcfg, opts)
|
|
if apiErr != noError {
|
|
replMismatch = true
|
|
}
|
|
}
|
|
|
|
if replMismatch {
|
|
replLogOnceIf(ctx, c.annotateErr(configureReplication, c.PeerBucketConfigureReplHandler(ctx, bucket)), "heal-bucket-relication-config")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isBucketMetadataEqual(one, two *string) bool {
|
|
switch {
|
|
case one == nil && two == nil:
|
|
return true
|
|
case one == nil || two == nil:
|
|
return false
|
|
default:
|
|
return strings.EqualFold(*one, *two)
|
|
}
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healIAMSystem(ctx context.Context, objAPI ObjectLayer) error {
|
|
info, err := c.siteReplicationStatus(ctx, objAPI, madmin.SRStatusOptions{
|
|
Users: true,
|
|
Policies: true,
|
|
Groups: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for policy := range info.PolicyStats {
|
|
c.healPolicies(ctx, objAPI, policy, info)
|
|
}
|
|
for user := range info.UserStats {
|
|
c.healUsers(ctx, objAPI, user, info)
|
|
}
|
|
for group := range info.GroupStats {
|
|
c.healGroups(ctx, objAPI, group, info)
|
|
}
|
|
for user := range info.UserStats {
|
|
c.healUserPolicies(ctx, objAPI, user, info)
|
|
}
|
|
for group := range info.GroupStats {
|
|
c.healGroupPolicies(ctx, objAPI, group, info)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// heal iam policies present on this site to peers, provided current cluster has the most recent update.
|
|
func (c *SiteReplicationSys) healPolicies(ctx context.Context, objAPI ObjectLayer, policy string, info srStatusInfo) error {
|
|
// create IAM policy on peer cluster if missing
|
|
ps := info.PolicyStats[policy]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestPolicyStat srPolicyStatsSummary
|
|
)
|
|
for dID, ss := range ps {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.policy.UpdatedAt
|
|
latestID = dID
|
|
latestPolicyStat = ss
|
|
}
|
|
if !ss.policy.UpdatedAt.IsZero() && ss.policy.UpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.policy.UpdatedAt
|
|
latestID = dID
|
|
latestPolicyStat = ss
|
|
}
|
|
}
|
|
if latestID != globalDeploymentID() {
|
|
// heal only from the site with latest info.
|
|
return nil
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
// heal policy of peers if peer does not have it.
|
|
for dID, pStatus := range ps {
|
|
if dID == globalDeploymentID() {
|
|
continue
|
|
}
|
|
if !pStatus.PolicyMismatch && pStatus.HasPolicy {
|
|
continue
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicy,
|
|
Name: policy,
|
|
Policy: latestPolicyStat.policy.Policy,
|
|
UpdatedAt: lastUpdate,
|
|
})
|
|
if err != nil {
|
|
replLogOnceIf(
|
|
ctx,
|
|
fmt.Errorf("Unable to heal IAM policy %s from peer site %s -> site %s : %w", policy, latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-policy-%s", policy))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// heal user policy mappings present on this site to peers, provided current cluster has the most recent update.
|
|
func (c *SiteReplicationSys) healUserPolicies(ctx context.Context, objAPI ObjectLayer, user string, info srStatusInfo) error {
|
|
// create user policy mapping on peer cluster if missing
|
|
us := info.UserStats[user]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestUserStat srUserStatsSummary
|
|
)
|
|
for dID, ss := range us {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.userPolicy.UpdatedAt
|
|
latestID = dID
|
|
latestUserStat = ss
|
|
}
|
|
if !ss.userPolicy.UpdatedAt.IsZero() && ss.userPolicy.UpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.userPolicy.UpdatedAt
|
|
latestID = dID
|
|
latestUserStat = ss
|
|
}
|
|
}
|
|
if latestID != globalDeploymentID() {
|
|
// heal only from the site with latest info.
|
|
return nil
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
// heal policy of peers if peer does not have it.
|
|
for dID, pStatus := range us {
|
|
if dID == globalDeploymentID() {
|
|
continue
|
|
}
|
|
if !pStatus.PolicyMismatch && pStatus.HasPolicyMapping {
|
|
continue
|
|
}
|
|
if isPolicyMappingEqual(pStatus.userPolicy, latestUserStat.userPolicy) {
|
|
continue
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicyMapping,
|
|
PolicyMapping: &madmin.SRPolicyMapping{
|
|
UserOrGroup: user,
|
|
IsGroup: false,
|
|
UserType: latestUserStat.userPolicy.UserType,
|
|
Policy: latestUserStat.userPolicy.Policy,
|
|
},
|
|
UpdatedAt: lastUpdate,
|
|
})
|
|
if err != nil {
|
|
replLogOnceIf(ctx, fmt.Errorf("Unable to heal IAM user policy mapping from peer site %s -> site %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-user-policy-%s", user))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// heal group policy mappings present on this site to peers, provided current cluster has the most recent update.
|
|
func (c *SiteReplicationSys) healGroupPolicies(ctx context.Context, objAPI ObjectLayer, group string, info srStatusInfo) error {
|
|
// create group policy mapping on peer cluster if missing
|
|
gs := info.GroupStats[group]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestGroupStat srGroupStatsSummary
|
|
)
|
|
for dID, ss := range gs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.groupPolicy.UpdatedAt
|
|
latestID = dID
|
|
latestGroupStat = ss
|
|
}
|
|
if !ss.groupPolicy.UpdatedAt.IsZero() && ss.groupPolicy.UpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.groupPolicy.UpdatedAt
|
|
latestID = dID
|
|
latestGroupStat = ss
|
|
}
|
|
}
|
|
if latestID != globalDeploymentID() {
|
|
// heal only from the site with latest info.
|
|
return nil
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
// heal policy of peers if peer does not have it.
|
|
for dID, pStatus := range gs {
|
|
if dID == globalDeploymentID() {
|
|
continue
|
|
}
|
|
if !pStatus.PolicyMismatch && pStatus.HasPolicyMapping {
|
|
continue
|
|
}
|
|
if isPolicyMappingEqual(pStatus.groupPolicy, latestGroupStat.groupPolicy) {
|
|
continue
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
|
|
err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemPolicyMapping,
|
|
PolicyMapping: &madmin.SRPolicyMapping{
|
|
UserOrGroup: group,
|
|
IsGroup: true,
|
|
UserType: int(unknownIAMUserType),
|
|
Policy: latestGroupStat.groupPolicy.Policy,
|
|
},
|
|
UpdatedAt: lastUpdate,
|
|
})
|
|
if err != nil {
|
|
replLogOnceIf(ctx,
|
|
fmt.Errorf("Unable to heal IAM group policy mapping for from peer site %s -> site %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-group-policy-%s", group))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// heal all users and their service accounts that are present on this site,
|
|
// provided current cluster has the most recent update.
|
|
func (c *SiteReplicationSys) healUsers(ctx context.Context, objAPI ObjectLayer, user string, info srStatusInfo) error {
|
|
// create user if missing; fix user policy mapping if missing
|
|
us := info.UserStats[user]
|
|
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestUserStat srUserStatsSummary
|
|
)
|
|
for dID, ss := range us {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.userInfo.UserInfo.UpdatedAt
|
|
latestID = dID
|
|
latestUserStat = ss
|
|
}
|
|
if !ss.userInfo.UserInfo.UpdatedAt.IsZero() && ss.userInfo.UserInfo.UpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.userInfo.UserInfo.UpdatedAt
|
|
latestID = dID
|
|
latestUserStat = ss
|
|
}
|
|
}
|
|
if latestID != globalDeploymentID() {
|
|
// heal only from the site with latest info.
|
|
return nil
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
for dID, uStatus := range us {
|
|
if dID == globalDeploymentID() {
|
|
continue
|
|
}
|
|
if !uStatus.UserInfoMismatch {
|
|
continue
|
|
}
|
|
|
|
if isUserInfoEqual(latestUserStat.userInfo.UserInfo, uStatus.userInfo.UserInfo) {
|
|
continue
|
|
}
|
|
|
|
peerName := info.Sites[dID].Name
|
|
|
|
u, ok := globalIAMSys.GetUser(ctx, user)
|
|
if !ok {
|
|
continue
|
|
}
|
|
creds := u.Credentials
|
|
if creds.IsServiceAccount() {
|
|
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, creds.AccessKey)
|
|
if err != nil {
|
|
replLogOnceIf(ctx,
|
|
fmt.Errorf("Unable to heal service account from peer site %s -> %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-user-%s", user))
|
|
continue
|
|
}
|
|
|
|
_, policy, err := globalIAMSys.GetServiceAccount(ctx, creds.AccessKey)
|
|
if err != nil {
|
|
replLogOnceIf(ctx,
|
|
fmt.Errorf("Unable to heal service account from peer site %s -> %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-user-%s", user))
|
|
continue
|
|
}
|
|
|
|
var policyJSON []byte
|
|
if policy != nil {
|
|
policyJSON, err = json.Marshal(policy)
|
|
if err != nil {
|
|
replLogOnceIf(ctx,
|
|
fmt.Errorf("Unable to heal service account from peer site %s -> %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-user-%s", user))
|
|
continue
|
|
}
|
|
}
|
|
|
|
if err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSvcAcc,
|
|
SvcAccChange: &madmin.SRSvcAccChange{
|
|
Create: &madmin.SRSvcAccCreate{
|
|
Parent: creds.ParentUser,
|
|
AccessKey: creds.AccessKey,
|
|
SecretKey: creds.SecretKey,
|
|
Groups: creds.Groups,
|
|
Claims: claims,
|
|
SessionPolicy: json.RawMessage(policyJSON),
|
|
Status: creds.Status,
|
|
Name: creds.Name,
|
|
Description: creds.Description,
|
|
Expiration: &creds.Expiration,
|
|
},
|
|
},
|
|
UpdatedAt: lastUpdate,
|
|
}); err != nil {
|
|
replLogOnceIf(ctx,
|
|
fmt.Errorf("Unable to heal service account from peer site %s -> %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-user-%s", user))
|
|
}
|
|
continue
|
|
}
|
|
if creds.IsTemp() && !creds.IsExpired() {
|
|
var parentPolicy string
|
|
u, err := globalIAMSys.GetUserInfo(ctx, creds.ParentUser)
|
|
if err != nil {
|
|
// Parent may be "virtual" (for ldap, oidc, client tls auth,
|
|
// custom auth plugin), so in such cases we apply no parent
|
|
// policy. The session token will contain info about policy to
|
|
// be applied.
|
|
if !errors.Is(err, errNoSuchUser) {
|
|
replLogOnceIf(ctx,
|
|
fmt.Errorf("Unable to heal temporary credentials from peer site %s -> %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-user-%s", user))
|
|
continue
|
|
}
|
|
} else {
|
|
parentPolicy = u.PolicyName
|
|
}
|
|
// Call hook for site replication.
|
|
if err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSTSAcc,
|
|
STSCredential: &madmin.SRSTSCredential{
|
|
AccessKey: creds.AccessKey,
|
|
SecretKey: creds.SecretKey,
|
|
SessionToken: creds.SessionToken,
|
|
ParentUser: creds.ParentUser,
|
|
ParentPolicyMapping: parentPolicy,
|
|
},
|
|
UpdatedAt: lastUpdate,
|
|
}); err != nil {
|
|
replLogOnceIf(ctx,
|
|
fmt.Errorf("Unable to heal temporary credentials from peer site %s -> %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-user-%s", user))
|
|
}
|
|
continue
|
|
}
|
|
if err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemIAMUser,
|
|
IAMUser: &madmin.SRIAMUser{
|
|
AccessKey: user,
|
|
IsDeleteReq: false,
|
|
UserReq: &madmin.AddOrUpdateUserReq{
|
|
SecretKey: creds.SecretKey,
|
|
Status: latestUserStat.userInfo.Status,
|
|
},
|
|
},
|
|
UpdatedAt: lastUpdate,
|
|
}); err != nil {
|
|
replLogOnceIf(ctx,
|
|
fmt.Errorf("Unable to heal user from peer site %s -> %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-user-%s", user))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *SiteReplicationSys) healGroups(ctx context.Context, objAPI ObjectLayer, group string, info srStatusInfo) error {
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
latestID, latestPeerName string
|
|
lastUpdate time.Time
|
|
latestGroupStat srGroupStatsSummary
|
|
)
|
|
// create group if missing; fix group policy mapping if missing
|
|
gs, ok := info.GroupStats[group]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
for dID, ss := range gs {
|
|
if lastUpdate.IsZero() {
|
|
lastUpdate = ss.groupDesc.UpdatedAt
|
|
latestID = dID
|
|
latestGroupStat = ss
|
|
}
|
|
if !ss.groupDesc.UpdatedAt.IsZero() && ss.groupDesc.UpdatedAt.After(lastUpdate) {
|
|
lastUpdate = ss.groupDesc.UpdatedAt
|
|
latestID = dID
|
|
latestGroupStat = ss
|
|
}
|
|
}
|
|
if latestID != globalDeploymentID() {
|
|
// heal only from the site with latest info.
|
|
return nil
|
|
}
|
|
latestPeerName = info.Sites[latestID].Name
|
|
for dID, gStatus := range gs {
|
|
if dID == globalDeploymentID() {
|
|
continue
|
|
}
|
|
if !gStatus.GroupDescMismatch {
|
|
continue
|
|
}
|
|
|
|
if isGroupDescEqual(latestGroupStat.groupDesc.GroupDesc, gStatus.groupDesc.GroupDesc) {
|
|
continue
|
|
}
|
|
peerName := info.Sites[dID].Name
|
|
if err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemGroupInfo,
|
|
GroupInfo: &madmin.SRGroupInfo{
|
|
UpdateReq: madmin.GroupAddRemove{
|
|
Group: group,
|
|
Status: madmin.GroupStatus(latestGroupStat.groupDesc.Status),
|
|
Members: latestGroupStat.groupDesc.Members,
|
|
IsRemove: false,
|
|
},
|
|
},
|
|
UpdatedAt: lastUpdate,
|
|
}); err != nil {
|
|
replLogOnceIf(ctx,
|
|
fmt.Errorf("Unable to heal group from peer site %s -> site %s : %w", latestPeerName, peerName, err),
|
|
fmt.Sprintf("heal-group-%s", group))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isGroupDescEqual(g1, g2 madmin.GroupDesc) bool {
|
|
if g1.Name != g2.Name ||
|
|
g1.Status != g2.Status ||
|
|
g1.Policy != g2.Policy {
|
|
return false
|
|
}
|
|
if len(g1.Members) != len(g2.Members) {
|
|
return false
|
|
}
|
|
for _, v1 := range g1.Members {
|
|
var found bool
|
|
for _, v2 := range g2.Members {
|
|
if v1 == v2 {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isUserInfoEqual(u1, u2 madmin.UserInfo) bool {
|
|
if u1.PolicyName != u2.PolicyName ||
|
|
u1.Status != u2.Status ||
|
|
u1.SecretKey != u2.SecretKey {
|
|
return false
|
|
}
|
|
for len(u1.MemberOf) != len(u2.MemberOf) {
|
|
return false
|
|
}
|
|
for _, v1 := range u1.MemberOf {
|
|
var found bool
|
|
for _, v2 := range u2.MemberOf {
|
|
if v1 == v2 {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isPolicyMappingEqual(p1, p2 srPolicyMapping) bool {
|
|
return p1.Policy == p2.Policy && p1.IsGroup == p2.IsGroup && p1.UserOrGroup == p2.UserOrGroup
|
|
}
|
|
|
|
type srPeerInfo struct {
|
|
madmin.PeerInfo
|
|
EndpointURL *url.URL
|
|
}
|
|
|
|
// getPeerForUpload returns the site replication peer handling this upload. Defaults to local cluster otherwise
|
|
func (c *SiteReplicationSys) getPeerForUpload(deplID string) (pi srPeerInfo, local bool) {
|
|
ci, _ := c.GetClusterInfo(GlobalContext)
|
|
if !ci.Enabled {
|
|
return pi, true
|
|
}
|
|
for _, site := range ci.Sites {
|
|
if deplID == site.DeploymentID {
|
|
ep, _ := url.Parse(site.Endpoint)
|
|
pi = srPeerInfo{
|
|
PeerInfo: site,
|
|
EndpointURL: ep,
|
|
}
|
|
return pi, site.DeploymentID == globalDeploymentID()
|
|
}
|
|
}
|
|
return pi, true
|
|
}
|
|
|
|
// startResync initiates resync of data to peerSite specified. The overall site resync status
|
|
// is maintained in .minio.sys/buckets/site-replication/resync/<deployment-id.meta>, while collecting
|
|
// individual bucket resync status in .minio.sys/buckets/<bucket-name>/replication/resync.bin
|
|
func (c *SiteReplicationSys) startResync(ctx context.Context, objAPI ObjectLayer, peer madmin.PeerInfo) (res madmin.SRResyncOpStatus, err error) {
|
|
if !c.isEnabled() {
|
|
return res, errSRNotEnabled
|
|
}
|
|
if objAPI == nil {
|
|
return res, errSRObjectLayerNotReady
|
|
}
|
|
|
|
if peer.DeploymentID == globalDeploymentID() {
|
|
return res, errSRResyncToSelf
|
|
}
|
|
if _, ok := c.state.Peers[peer.DeploymentID]; !ok {
|
|
return res, errSRPeerNotFound
|
|
}
|
|
rs, err := globalSiteResyncMetrics.siteStatus(ctx, objAPI, peer.DeploymentID)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
if rs.Status == ResyncStarted {
|
|
return res, errSRResyncStarted
|
|
}
|
|
var buckets []BucketInfo
|
|
buckets, err = objAPI.ListBuckets(ctx, BucketOptions{})
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
rs = newSiteResyncStatus(peer.DeploymentID, buckets)
|
|
defer func() {
|
|
if err != nil {
|
|
rs.Status = ResyncFailed
|
|
saveSiteResyncMetadata(ctx, rs, objAPI)
|
|
globalSiteResyncMetrics.updateState(rs)
|
|
}
|
|
}()
|
|
|
|
if err := globalSiteResyncMetrics.updateState(rs); err != nil {
|
|
return res, err
|
|
}
|
|
|
|
for _, bi := range buckets {
|
|
bucket := bi.Name
|
|
if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
Status: ResyncFailed.String(),
|
|
})
|
|
continue
|
|
}
|
|
// mark remote target for this deployment with the new reset id
|
|
tgtArn := globalBucketTargetSys.getRemoteARNForPeer(bucket, peer)
|
|
if tgtArn == "" {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: fmt.Sprintf("no valid remote target found for this peer %s (%s)", peer.Name, peer.DeploymentID),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
target := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, tgtArn)
|
|
target.ResetBeforeDate = UTCNow()
|
|
target.ResetID = rs.ResyncID
|
|
if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, true); err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
|
if err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
tgtBytes, err := json.Marshal(&targets)
|
|
if err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
if err := globalReplicationPool.Get().resyncer.start(ctx, objAPI, resyncOpts{
|
|
bucket: bucket,
|
|
arn: tgtArn,
|
|
resyncID: rs.ResyncID,
|
|
}); err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
}
|
|
res = madmin.SRResyncOpStatus{
|
|
Status: ResyncStarted.String(),
|
|
OpType: "start",
|
|
ResyncID: rs.ResyncID,
|
|
Buckets: res.Buckets,
|
|
}
|
|
if len(res.Buckets) > 0 {
|
|
res.ErrDetail = "partial failure in starting site resync"
|
|
}
|
|
if len(buckets) != 0 && len(res.Buckets) == len(buckets) {
|
|
return res, fmt.Errorf("all buckets resync failed")
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// cancelResync stops an ongoing site level resync for the peer specified.
|
|
func (c *SiteReplicationSys) cancelResync(ctx context.Context, objAPI ObjectLayer, peer madmin.PeerInfo) (res madmin.SRResyncOpStatus, err error) {
|
|
if !c.isEnabled() {
|
|
return res, errSRNotEnabled
|
|
}
|
|
if objAPI == nil {
|
|
return res, errSRObjectLayerNotReady
|
|
}
|
|
if peer.DeploymentID == globalDeploymentID() {
|
|
return res, errSRResyncToSelf
|
|
}
|
|
if _, ok := c.state.Peers[peer.DeploymentID]; !ok {
|
|
return res, errSRPeerNotFound
|
|
}
|
|
rs, err := globalSiteResyncMetrics.siteStatus(ctx, objAPI, peer.DeploymentID)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
res = madmin.SRResyncOpStatus{
|
|
Status: rs.Status.String(),
|
|
OpType: "cancel",
|
|
ResyncID: rs.ResyncID,
|
|
}
|
|
switch rs.Status {
|
|
case ResyncCanceled:
|
|
return res, errSRResyncCanceled
|
|
case ResyncCompleted, NoResync:
|
|
return res, errSRNoResync
|
|
}
|
|
targets := globalBucketTargetSys.ListTargets(ctx, "", string(madmin.ReplicationService))
|
|
// clear the remote target resetID set while initiating resync to stop replication
|
|
for _, t := range targets {
|
|
if t.ResetID == rs.ResyncID {
|
|
// get tgt with credentials
|
|
tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, t.SourceBucket, t.Arn)
|
|
tgt.ResetID = ""
|
|
bucket := t.SourceBucket
|
|
if err = globalBucketTargetSys.SetTarget(ctx, bucket, &tgt, true); err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
|
if err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
tgtBytes, err := json.Marshal(&targets)
|
|
if err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
|
res.Buckets = append(res.Buckets, madmin.ResyncBucketStatus{
|
|
ErrDetail: err.Error(),
|
|
Bucket: bucket,
|
|
})
|
|
continue
|
|
}
|
|
// update resync state for the bucket
|
|
globalReplicationPool.Get().resyncer.Lock()
|
|
m, ok := globalReplicationPool.Get().resyncer.statusMap[bucket]
|
|
if !ok {
|
|
m = newBucketResyncStatus(bucket)
|
|
}
|
|
if st, ok := m.TargetsMap[t.Arn]; ok {
|
|
st.LastUpdate = UTCNow()
|
|
st.ResyncStatus = ResyncCanceled
|
|
m.TargetsMap[t.Arn] = st
|
|
m.LastUpdate = UTCNow()
|
|
}
|
|
globalReplicationPool.Get().resyncer.statusMap[bucket] = m
|
|
globalReplicationPool.Get().resyncer.Unlock()
|
|
}
|
|
}
|
|
|
|
rs.Status = ResyncCanceled
|
|
rs.LastUpdate = UTCNow()
|
|
if err := saveSiteResyncMetadata(ctx, rs, objAPI); err != nil {
|
|
return res, err
|
|
}
|
|
select {
|
|
case globalReplicationPool.Get().resyncer.resyncCancelCh <- struct{}{}:
|
|
case <-ctx.Done():
|
|
}
|
|
|
|
globalSiteResyncMetrics.updateState(rs)
|
|
|
|
res.Status = rs.Status.String()
|
|
return res, nil
|
|
}
|
|
|
|
const (
|
|
siteResyncMetaFormat = 1
|
|
siteResyncMetaVersionV1 = 1
|
|
siteResyncMetaVersion = siteResyncMetaVersionV1
|
|
siteResyncSaveInterval = 10 * time.Second
|
|
)
|
|
|
|
func newSiteResyncStatus(dID string, buckets []BucketInfo) SiteResyncStatus {
|
|
now := UTCNow()
|
|
s := SiteResyncStatus{
|
|
Version: siteResyncMetaVersion,
|
|
Status: ResyncStarted,
|
|
DeplID: dID,
|
|
TotBuckets: len(buckets),
|
|
BucketStatuses: make(map[string]ResyncStatusType),
|
|
}
|
|
for _, bi := range buckets {
|
|
s.BucketStatuses[bi.Name] = ResyncPending
|
|
}
|
|
s.ResyncID = mustGetUUID()
|
|
s.StartTime = now
|
|
s.LastUpdate = now
|
|
return s
|
|
}
|
|
|
|
// load site resync metadata from disk
|
|
func loadSiteResyncMetadata(ctx context.Context, objAPI ObjectLayer, dID string) (rs SiteResyncStatus, e error) {
|
|
data, err := readConfig(GlobalContext, objAPI, getSRResyncFilePath(dID))
|
|
if err != nil {
|
|
return rs, err
|
|
}
|
|
if len(data) == 0 {
|
|
// Seems to be empty.
|
|
return rs, nil
|
|
}
|
|
if len(data) <= 4 {
|
|
return rs, fmt.Errorf("site resync: no data")
|
|
}
|
|
// Read resync meta header
|
|
switch binary.LittleEndian.Uint16(data[0:2]) {
|
|
case siteResyncMetaFormat:
|
|
default:
|
|
return rs, fmt.Errorf("resyncMeta: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
|
|
}
|
|
switch binary.LittleEndian.Uint16(data[2:4]) {
|
|
case siteResyncMetaVersion:
|
|
default:
|
|
return rs, fmt.Errorf("resyncMeta: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
|
|
}
|
|
// OK, parse data.
|
|
if _, err = rs.UnmarshalMsg(data[4:]); err != nil {
|
|
return rs, err
|
|
}
|
|
|
|
switch rs.Version {
|
|
case siteResyncMetaVersionV1:
|
|
default:
|
|
return rs, fmt.Errorf("unexpected resync meta version: %d", rs.Version)
|
|
}
|
|
return rs, nil
|
|
}
|
|
|
|
// save resync status of peer to resync/depl-id.meta
|
|
func saveSiteResyncMetadata(ctx context.Context, ss SiteResyncStatus, objectAPI ObjectLayer) error {
|
|
if objectAPI == nil {
|
|
return errSRObjectLayerNotReady
|
|
}
|
|
data := make([]byte, 4, ss.Msgsize()+4)
|
|
|
|
// Initialize the resync meta header.
|
|
binary.LittleEndian.PutUint16(data[0:2], siteResyncMetaFormat)
|
|
binary.LittleEndian.PutUint16(data[2:4], siteResyncMetaVersion)
|
|
|
|
buf, err := ss.MarshalMsg(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return saveConfig(ctx, objectAPI, getSRResyncFilePath(ss.DeplID), buf)
|
|
}
|
|
|
|
func getSRResyncFilePath(dID string) string {
|
|
return pathJoin(siteResyncPrefix, dID+".meta")
|
|
}
|
|
|
|
func (c *SiteReplicationSys) getDeplIDForEndpoint(ep string) (dID string, err error) {
|
|
if ep == "" {
|
|
return dID, fmt.Errorf("no deployment id found for endpoint %s", ep)
|
|
}
|
|
c.RLock()
|
|
defer c.RUnlock()
|
|
if !c.enabled {
|
|
return dID, errSRNotEnabled
|
|
}
|
|
for _, peer := range c.state.Peers {
|
|
if ep == peer.Endpoint {
|
|
return peer.DeploymentID, nil
|
|
}
|
|
}
|
|
return dID, fmt.Errorf("no deployment id found for endpoint %s", ep)
|
|
}
|
|
|
|
func (c *SiteReplicationSys) getSiteMetrics(ctx context.Context) (madmin.SRMetricsSummary, error) {
|
|
if !c.isEnabled() {
|
|
return madmin.SRMetricsSummary{}, errSRNotEnabled
|
|
}
|
|
peerSMetricsList := globalNotificationSys.GetClusterSiteMetrics(ctx)
|
|
var sm madmin.SRMetricsSummary
|
|
sm.Metrics = make(map[string]madmin.SRMetric)
|
|
|
|
for _, peer := range peerSMetricsList {
|
|
sm.ActiveWorkers.Avg += peer.ActiveWorkers.Avg
|
|
sm.ActiveWorkers.Curr += peer.ActiveWorkers.Curr
|
|
if peer.ActiveWorkers.Max > sm.ActiveWorkers.Max {
|
|
sm.ActiveWorkers.Max += peer.ActiveWorkers.Max
|
|
}
|
|
sm.Queued.Avg.Bytes += peer.Queued.Avg.Bytes
|
|
sm.Queued.Avg.Count += peer.Queued.Avg.Count
|
|
sm.Queued.Curr.Bytes += peer.Queued.Curr.Bytes
|
|
sm.Queued.Curr.Count += peer.Queued.Curr.Count
|
|
if peer.Queued.Max.Count > sm.Queued.Max.Count {
|
|
sm.Queued.Max.Bytes = peer.Queued.Max.Bytes
|
|
sm.Queued.Max.Count = peer.Queued.Max.Count
|
|
}
|
|
sm.ReplicaCount += peer.ReplicaCount
|
|
sm.ReplicaSize += peer.ReplicaSize
|
|
sm.Proxied.Add(madmin.ReplProxyMetric(peer.Proxied))
|
|
for dID, v := range peer.Metrics {
|
|
v2, ok := sm.Metrics[dID]
|
|
if !ok {
|
|
v2 = madmin.SRMetric{}
|
|
v2.Failed.ErrCounts = make(map[string]int)
|
|
}
|
|
|
|
// use target endpoint metrics from node which has been up the longest
|
|
if v2.LastOnline.After(v.LastOnline) || v2.LastOnline.IsZero() {
|
|
v2.Endpoint = v.Endpoint
|
|
v2.LastOnline = v.LastOnline
|
|
v2.Latency = v.Latency
|
|
v2.Online = v.Online
|
|
v2.TotalDowntime = v.TotalDowntime
|
|
v2.DeploymentID = v.DeploymentID
|
|
}
|
|
v2.ReplicatedCount += v.ReplicatedCount
|
|
v2.ReplicatedSize += v.ReplicatedSize
|
|
v2.Failed = v2.Failed.Add(v.Failed)
|
|
for k, v := range v.Failed.ErrCounts {
|
|
v2.Failed.ErrCounts[k] += v
|
|
}
|
|
if v2.XferStats == nil {
|
|
v2.XferStats = make(map[replication.MetricName]replication.XferStats)
|
|
}
|
|
for rm, x := range v.XferStats {
|
|
x2, ok := v2.XferStats[replication.MetricName(rm)]
|
|
if !ok {
|
|
x2 = replication.XferStats{}
|
|
}
|
|
x2.AvgRate += x.Avg
|
|
x2.CurrRate += x.Curr
|
|
if x.Peak > x2.PeakRate {
|
|
x2.PeakRate = x.Peak
|
|
}
|
|
v2.XferStats[replication.MetricName(rm)] = x2
|
|
}
|
|
sm.Metrics[dID] = v2
|
|
}
|
|
}
|
|
sm.Uptime = UTCNow().Unix() - globalBootTime.Unix()
|
|
return sm, nil
|
|
}
|
|
|
|
// mergeWithCurrentLCConfig - merges the given ilm expiry configuration with existing for the current site and returns
|
|
func mergeWithCurrentLCConfig(ctx context.Context, bucket string, expLCCfg *string, updatedAt time.Time) ([]byte, error) {
|
|
// Get bucket config from current site
|
|
meta, e := globalBucketMetadataSys.GetConfigFromDisk(ctx, bucket)
|
|
if e != nil && !errors.Is(e, errConfigNotFound) {
|
|
return []byte{}, e
|
|
}
|
|
rMap := make(map[string]lifecycle.Rule)
|
|
var xmlName xml.Name
|
|
if len(meta.LifecycleConfigXML) > 0 {
|
|
var lcCfg lifecycle.Lifecycle
|
|
if err := xml.Unmarshal(meta.LifecycleConfigXML, &lcCfg); err != nil {
|
|
return []byte{}, err
|
|
}
|
|
for _, rl := range lcCfg.Rules {
|
|
rMap[rl.ID] = rl
|
|
}
|
|
xmlName = meta.lifecycleConfig.XMLName
|
|
}
|
|
|
|
// get latest expiry rules
|
|
newRMap := make(map[string]lifecycle.Rule)
|
|
if expLCCfg != nil {
|
|
var cfg lifecycle.Lifecycle
|
|
expLcCfgData, err := base64.StdEncoding.DecodeString(*expLCCfg)
|
|
if err != nil {
|
|
return []byte{}, err
|
|
}
|
|
if err := xml.Unmarshal(expLcCfgData, &cfg); err != nil {
|
|
return []byte{}, err
|
|
}
|
|
for _, rl := range cfg.Rules {
|
|
newRMap[rl.ID] = rl
|
|
}
|
|
xmlName = cfg.XMLName
|
|
}
|
|
|
|
// check if current expiry rules are there in new one. if not remove the expiration
|
|
// part of rule as they may have been removed from latest updated one
|
|
for id, rl := range rMap {
|
|
if !rl.Expiration.IsNull() || !rl.NoncurrentVersionExpiration.IsNull() {
|
|
if _, ok := newRMap[id]; !ok {
|
|
// if rule getting removed was pure expiry rule (may be got to this site
|
|
// as part of replication of expiry rules), remove it. Otherwise remove
|
|
// only the expiry part of it
|
|
if rl.Transition.IsNull() && rl.NoncurrentVersionTransition.IsNull() {
|
|
delete(rMap, id)
|
|
} else {
|
|
rl.Expiration = lifecycle.Expiration{}
|
|
rl.NoncurrentVersionExpiration = lifecycle.NoncurrentVersionExpiration{}
|
|
rMap[id] = rl
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// append now
|
|
for id, rl := range newRMap {
|
|
// if rule is already in original list update non tranisition details with latest
|
|
// else simply add to the map. This may happen if ILM expiry replication
|
|
// was disabled for sometime and rules were updated independently in different
|
|
// sites. Latest changes would get applied but merge only the non transition details
|
|
if existingRl, ok := rMap[id]; ok {
|
|
clonedRl := rl.CloneNonTransition()
|
|
clonedRl.Transition = existingRl.Transition
|
|
clonedRl.NoncurrentVersionTransition = existingRl.NoncurrentVersionTransition
|
|
rMap[id] = clonedRl
|
|
} else {
|
|
rMap[id] = rl
|
|
}
|
|
}
|
|
|
|
var rules []lifecycle.Rule
|
|
for _, rule := range rMap {
|
|
rules = append(rules, rule)
|
|
}
|
|
|
|
// no rules, return
|
|
if len(rules) == 0 {
|
|
return []byte{}, nil
|
|
}
|
|
|
|
// get final list for write
|
|
finalLcCfg := lifecycle.Lifecycle{
|
|
XMLName: xmlName,
|
|
Rules: rules,
|
|
ExpiryUpdatedAt: &updatedAt,
|
|
}
|
|
|
|
rcfg, err := globalBucketObjectLockSys.Get(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := finalLcCfg.Validate(rcfg); err != nil {
|
|
return []byte{}, err
|
|
}
|
|
finalConfigData, err := xml.Marshal(finalLcCfg)
|
|
if err != nil {
|
|
return []byte{}, err
|
|
}
|
|
|
|
return finalConfigData, nil
|
|
}
|
|
|
|
func ilmExpiryReplicationEnabled(sites map[string]madmin.PeerInfo) bool {
|
|
flag := true
|
|
for _, pi := range sites {
|
|
flag = flag && pi.ReplicateILMExpiry
|
|
}
|
|
return flag
|
|
}
|
|
|
|
type siteReplicatorCred struct {
|
|
secretKey string
|
|
sync.RWMutex
|
|
}
|
|
|
|
// Get or attempt to load site replicator credentials from disk.
|
|
func (s *siteReplicatorCred) Get(ctx context.Context) (string, error) {
|
|
s.RLock()
|
|
secretKey := s.secretKey
|
|
s.RUnlock()
|
|
|
|
if secretKey != "" {
|
|
return secretKey, nil
|
|
}
|
|
|
|
secretKey, err := globalIAMSys.store.loadSecretKey(ctx, siteReplicatorSvcAcc, svcUser)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
s.Set(secretKey)
|
|
return secretKey, nil
|
|
}
|
|
|
|
func (s *siteReplicatorCred) Set(secretKey string) {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
s.secretKey = secretKey
|
|
}
|
|
|
|
func (s *siteReplicatorCred) IsValid() bool {
|
|
s.RLock()
|
|
defer s.RUnlock()
|
|
return s.secretKey != ""
|
|
}
|