minio/pkg/madmin/remote-target-commands.go
Poorna Krishnamoorthy 7824e19d20
Allow synchronous replication if enabled. (#11165)
Synchronous replication can be enabled by setting the --sync
flag while adding a remote replication target.

This PR also adds proxying on GET/HEAD to another node in a
active-active replication setup in the event of a 404 on the current node.
2021-01-11 22:36:51 -08:00

309 lines
8.1 KiB
Go

/*
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package madmin
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/minio/minio/pkg/auth"
)
// ServiceType represents service type
type ServiceType string
const (
// ReplicationService specifies replication service
ReplicationService ServiceType = "replication"
// ILMService specifies ilm service
ILMService ServiceType = "ilm"
)
// IsValid returns true if ARN type represents replication or ilm
func (t ServiceType) IsValid() bool {
return t == ReplicationService || t == ILMService
}
// ARN is a struct to define arn.
type ARN struct {
Type ServiceType
ID string
Region string
Bucket string
}
// Empty returns true if arn struct is empty
func (a ARN) Empty() bool {
return !a.Type.IsValid()
}
func (a ARN) String() string {
return fmt.Sprintf("arn:minio:%s:%s:%s:%s", a.Type, a.Region, a.ID, a.Bucket)
}
// ParseARN return ARN struct from string in arn format.
func ParseARN(s string) (*ARN, error) {
// ARN must be in the format of arn:minio:<Type>:<REGION>:<ID>:<remote-bucket>
if !strings.HasPrefix(s, "arn:minio:") {
return nil, fmt.Errorf("Invalid ARN %s", s)
}
tokens := strings.Split(s, ":")
if len(tokens) != 6 {
return nil, fmt.Errorf("Invalid ARN %s", s)
}
if tokens[4] == "" || tokens[5] == "" {
return nil, fmt.Errorf("Invalid ARN %s", s)
}
return &ARN{
Type: ServiceType(tokens[2]),
Region: tokens[3],
ID: tokens[4],
Bucket: tokens[5],
}, nil
}
// BucketTarget represents the target bucket and site association.
type BucketTarget struct {
SourceBucket string `json:"sourcebucket"`
Endpoint string `json:"endpoint"`
Credentials *auth.Credentials `json:"credentials"`
TargetBucket string `json:"targetbucket"`
Secure bool `json:"secure"`
Path string `json:"path,omitempty"`
API string `json:"api,omitempty"`
Arn string `json:"arn,omitempty"`
Type ServiceType `json:"type"`
Region string `json:"omitempty"`
Label string `json:"label,omitempty"`
BandwidthLimit int64 `json:"bandwidthlimit,omitempty"`
ReplicationSync bool `json:"replicationSync"`
HealthCheckDuration time.Duration `json:"healthCheckDuration,omitempty"`
}
// Clone returns shallow clone of BucketTarget without secret key in credentials
func (t *BucketTarget) Clone() BucketTarget {
return BucketTarget{
SourceBucket: t.SourceBucket,
Endpoint: t.Endpoint,
TargetBucket: t.TargetBucket,
Credentials: &auth.Credentials{AccessKey: t.Credentials.AccessKey},
Secure: t.Secure,
Path: t.Path,
API: t.Path,
Arn: t.Arn,
Type: t.Type,
Region: t.Region,
Label: t.Label,
BandwidthLimit: t.BandwidthLimit,
ReplicationSync: t.ReplicationSync,
HealthCheckDuration: t.HealthCheckDuration,
}
}
// URL returns target url
func (t BucketTarget) URL() *url.URL {
scheme := "http"
if t.Secure {
scheme = "https"
}
return &url.URL{
Scheme: scheme,
Host: t.Endpoint,
}
}
// Empty returns true if struct is empty.
func (t BucketTarget) Empty() bool {
return t.String() == "" || t.Credentials == nil
}
func (t *BucketTarget) String() string {
return fmt.Sprintf("%s %s", t.Endpoint, t.TargetBucket)
}
// BucketTargets represents a slice of bucket targets by type and endpoint
type BucketTargets struct {
Targets []BucketTarget
}
// Empty returns true if struct is empty.
func (t BucketTargets) Empty() bool {
if len(t.Targets) == 0 {
return true
}
empty := true
for _, t := range t.Targets {
if !t.Empty() {
return false
}
}
return empty
}
// ListRemoteTargets - gets target(s) for this bucket
func (adm *AdminClient) ListRemoteTargets(ctx context.Context, bucket, arnType string) (targets []BucketTarget, err error) {
queryValues := url.Values{}
queryValues.Set("bucket", bucket)
queryValues.Set("type", arnType)
reqData := requestData{
relPath: adminAPIPrefix + "/list-remote-targets",
queryValues: queryValues,
}
// Execute GET on /minio/admin/v3/list-remote-targets
resp, err := adm.executeMethod(ctx, http.MethodGet, reqData)
defer closeResponse(resp)
if err != nil {
return targets, err
}
if resp.StatusCode != http.StatusOK {
return targets, httpRespToErrorResponse(resp)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return targets, err
}
if err = json.Unmarshal(b, &targets); err != nil {
return targets, err
}
return targets, nil
}
// SetRemoteTarget sets up a remote target for this bucket
func (adm *AdminClient) SetRemoteTarget(ctx context.Context, bucket string, target *BucketTarget) (string, error) {
data, err := json.Marshal(target)
if err != nil {
return "", err
}
encData, err := EncryptData(adm.getSecretKey(), data)
if err != nil {
return "", err
}
queryValues := url.Values{}
queryValues.Set("bucket", bucket)
reqData := requestData{
relPath: adminAPIPrefix + "/set-remote-target",
queryValues: queryValues,
content: encData,
}
// Execute PUT on /minio/admin/v3/set-remote-target to set a target for this bucket of specific arn type.
resp, err := adm.executeMethod(ctx, http.MethodPut, reqData)
defer closeResponse(resp)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", httpRespToErrorResponse(resp)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var arn string
if err = json.Unmarshal(b, &arn); err != nil {
return "", err
}
return arn, nil
}
// UpdateRemoteTarget updates credentials for a remote bucket target
func (adm *AdminClient) UpdateRemoteTarget(ctx context.Context, target *BucketTarget) (string, error) {
if target == nil {
return "", fmt.Errorf("target cannot be nil")
}
data, err := json.Marshal(target)
if err != nil {
return "", err
}
encData, err := EncryptData(adm.getSecretKey(), data)
if err != nil {
return "", err
}
queryValues := url.Values{}
queryValues.Set("bucket", target.SourceBucket)
queryValues.Set("update", "true")
reqData := requestData{
relPath: adminAPIPrefix + "/set-remote-target",
queryValues: queryValues,
content: encData,
}
// Execute PUT on /minio/admin/v3/set-remote-target to set a target for this bucket of specific arn type.
resp, err := adm.executeMethod(ctx, http.MethodPut, reqData)
defer closeResponse(resp)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", httpRespToErrorResponse(resp)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var arn string
if err = json.Unmarshal(b, &arn); err != nil {
return "", err
}
return arn, nil
}
// RemoveRemoteTarget removes a remote target associated with particular ARN for this bucket
func (adm *AdminClient) RemoveRemoteTarget(ctx context.Context, bucket, arn string) error {
queryValues := url.Values{}
queryValues.Set("bucket", bucket)
queryValues.Set("arn", arn)
reqData := requestData{
relPath: adminAPIPrefix + "/remove-remote-target",
queryValues: queryValues,
}
// Execute PUT on /minio/admin/v3/remove-remote-target to remove a target for this bucket
// with specific ARN
resp, err := adm.executeMethod(ctx, http.MethodDelete, reqData)
defer closeResponse(resp)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return httpRespToErrorResponse(resp)
}
return nil
}