From 86211e9f3bc32cd97187c7ee9beb32be60264fed Mon Sep 17 00:00:00 2001 From: stormgbs Date: Fri, 29 Mar 2019 01:56:29 +0800 Subject: [PATCH] *: add Alibaba Cloud Object Storage Service (OSS) backend for etcd-backup/restore-operators (#2065) --- Gopkg.lock | 10 +- Gopkg.toml | 4 + doc/design/oss_backup.md | 42 ++++++ doc/user/oss_backup.md | 125 +++++++++++++++ pkg/apis/etcd/v1beta2/backup_types.go | 42 ++++++ pkg/apis/etcd/v1beta2/restore_types.go | 37 +++++ .../etcd/v1beta2/zz_generated.deepcopy.go | 42 ++++++ pkg/backup/reader/oss_reader.go | 59 ++++++++ pkg/backup/writer/oss_writer.go | 142 ++++++++++++++++++ pkg/controller/backup-operator/oss_backup.go | 61 ++++++++ pkg/controller/backup-operator/sync.go | 7 + pkg/controller/restore-operator/http.go | 18 +++ .../alibabacloudutil/ossfactory/client.go | 62 ++++++++ 13 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 doc/design/oss_backup.md create mode 100644 doc/user/oss_backup.md create mode 100644 pkg/backup/reader/oss_reader.go create mode 100644 pkg/backup/writer/oss_writer.go create mode 100644 pkg/controller/backup-operator/oss_backup.go create mode 100644 pkg/util/alibabacloudutil/ossfactory/client.go diff --git a/Gopkg.lock b/Gopkg.lock index 3c60b6901..a346563b0 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -55,6 +55,14 @@ pruneopts = "NT" revision = "de5bf2ad457846296e2031421a34e2568e304e35" +[[projects]] + digest = "1:9a6d75376aa0c967c34dda295d1901fea4c2b29db11d21f9ea50ce81e8cca39e" + name = "github.com/aliyun/aliyun-oss-go-sdk" + packages = ["oss"] + pruneopts = "NT" + revision = "2b29687e15f2cc71fb3a50f28be1d6d30c40c86a" + version = "1.9.4" + [[projects]] digest = "1:1e47beabb90c1e4cc35a3b9ee2d43c191930940363d6c02d2c59acd38c07cef6" name = "github.com/aws/aws-sdk-go" @@ -974,6 +982,7 @@ "cloud.google.com/go/storage", "github.com/Azure/azure-sdk-for-go/storage", "github.com/Azure/go-autorest/autorest/azure", + "github.com/aliyun/aliyun-oss-go-sdk/oss", "github.com/aws/aws-sdk-go/aws", "github.com/aws/aws-sdk-go/aws/session", "github.com/aws/aws-sdk-go/service/s3", @@ -995,7 +1004,6 @@ "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1", "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset", "k8s.io/apimachinery/pkg/api/errors", - "k8s.io/apimachinery/pkg/api/meta", "k8s.io/apimachinery/pkg/api/resource", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/fields", diff --git a/Gopkg.toml b/Gopkg.toml index a287df546..2e1682cd5 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -73,3 +73,7 @@ required = [ [[prune.project]] name = "k8s.io/code-generator" non-go = false + +[[constraint]] + name = "github.com/aliyun/aliyun-oss-go-sdk" + version = "=1.9.4" diff --git a/doc/design/oss_backup.md b/doc/design/oss_backup.md new file mode 100644 index 000000000..62bf5a866 --- /dev/null +++ b/doc/design/oss_backup.md @@ -0,0 +1,42 @@ +# Backups using Alibaba Cloud Object Storage Service (OSS) + +## Cluster configured with OSS backup + +To create a backup in OSS, set `backup.storageType` to `"OSS"`, supply the path (in the format `/`) in `oss.path` and provide the Kubernetes secret storing the Alibaba Cloud account credentials to `oss.ossSecret`. The secret must exist prior to backup creation. Etcd backup operator will create the bucket and object if not found. The field `oss.endpoint` is the target OSS service endpoint where the data is backed up. By default, `http://oss-cn-hangzhou.aliyuncs.com` will be used. If you want to back up the data to other regions, please specify another endpoint from [the list of region endpoints](https://www.alibabacloud.com/help/doc-detail/31837.htm). + + +An example backup manifest would look like: + +```yaml +apiVersion: etcd.database.coreos.com/v1beta2 +kind: EtcdBackup +metadata: + name: etcd-cluster-with-oss-backup + namespace: my-namespace +spec: + backupPolicy: + ... + etcdEndpoints: + - "http://example-etcd-cluster-client:2379" + storageType: OSS + oss: + endpoint: http://oss-cn-hangzhou.aliyuncs.com + ossSecret: my-oss-credentials + path: my-etcd-backups-bucket/etcd.backup +``` + +### In Detail: + +- `"ossSecret"` represents the name of the Kubernetes secret object that stores the credentials needed for Alibaba Cloud authorization, namely an authorization token. + + The Kubernetes secret manifest must have the following format: + ```yaml + apiVersion: v1 + kind: Secret + metadata: + name: my-oss-credentials + type: Opaque + data: + accessKeyID: + accessKeySecret: + ``` diff --git a/doc/user/oss_backup.md b/doc/user/oss_backup.md new file mode 100644 index 000000000..06bf76df0 --- /dev/null +++ b/doc/user/oss_backup.md @@ -0,0 +1,125 @@ +# Backups using Alibaba Cloud Object Storage Service (OSS) + +Etcd backup operator backs up the data of an etcd cluster running on Kubernetes to a remote storage such as Alibaba Cloud Object Storage Service (OSS). If it is not deployed yet, please follow the [instructions](walkthrough/backup-operator.md#deploy-etcd-backup-operator) to deploy it, e.g. by running + +```sh +kubectl apply -f example/etcd-backup-operator/deployment.yaml +``` + +## Setup Alibaba Cloud backup account, OSS bucket, and Secret + +1. Login [Alibaba Cloud Console](https://www.alibabacloud.com) (or [Aliyun Console](https://www.aliyun.com/) if you are in China) and create your own [AccessKey](https://www.alibabacloud.com/help/doc-detail/29009.htm) which gives you the AccessKeyID (AKID) and AccessKeySecret (AKS). You can optionally create an Object Storage Service ([OSS](https://www.alibabacloud.com/help/doc-detail/31947.htm)) bucket for backups. +2. Create a secret storing your AKID and AKS in Kubernetes. + + ```yaml +apiVersion: v1 + kind: Secret + metadata: + name: my-oss-credentials + type: Opaque + data: + accessKeyID: + accessKeySecret: + ``` + +3. Create an `EtcdBackup` CR file `etcdbackup.yaml` which uses secret `my-oss-credentials` from the previous step. +```yaml +apiVersion: etcd.database.coreos.com/v1beta2 +kind: EtcdBackup +metadata: + name: etcd-cluster-with-oss-backup +spec: + backupPolicy: + ... + etcdEndpoints: + - "http://example-etcd-cluster-client:2379" + storageType: OSS + oss: + endpoint: http://oss-cn-hangzhou.aliyuncs.com + ossSecret: my-oss-credentials + path: my-etcd-backups-bucket/etcd.backup +``` + +4. Apply yaml file to kubernetes cluster. +```sh +kubectl apply -f etcdbackup.yaml +``` +5. Check the `status` section of the `EtcdBackup` CR. +```console +$ kubectl get EtcdBackup etcd-cluster-with-oss-backup -o yaml +apiVersion: etcd.database.coreos.com/v1beta2 +kind: EtcdBackup +... +spec: + oss: + ossSecret: my-oss-credentials + path: my-etcd-backups-bucket/etcd.backup + endpoint: http://oss-cn-hangzhou.aliyuncs.com + etcdEndpoints: + - http://example-etcd-cluster-client:2379 + storageType: OSS +status: + etcdRevision: 1 + etcdVersion: 3.2.13 + succeeded: true +``` + +6. We should see the backup files from Alibaba Cloud OSS Console. + + +## Restore etcd based on data from OSS. + +Etcd restore operator is in charge of restoring etcd cluster from backup. If it is not deployed, please deploy by following command: + +```sh +kubectl apply -f example/etcd-restore-operator/deployment.yaml +``` + +Now kill all the etcd pods to simulate a cluster failure: + +```sh +kubectl delete pod -l app=etcd,etcd_cluster=example-etcd-cluster --force --grace-period=0 +``` + +1. Create an EtcdRestore CR. +```yaml +apiVersion: "etcd.database.coreos.com/v1beta2" +kind: "EtcdRestore" +metadata: + # The restore CR name must be the same as spec.etcdCluster.name + name: example-etcd-cluster +spec: + etcdCluster: + # The namespace is the same as this EtcdRestore CR + name: example-etcd-cluster + backupStorageType: OSS + oss: + # The format of the path must be: "/" + path: my-etcd-backups-bucket/etcd.backup + ossSecret: my-oss-credentials + endpoint: http://oss-cn-hangzhou.aliyuncs.com +``` + +2. Check the `status` section of the `EtcdRestore` CR. +```sh +$ kubectl get etcdrestore example-etcd-cluster -o yaml +apiVersion: etcd.database.coreos.com/v1beta2 +kind: EtcdRestore +... +spec: + oss: + ossSecret: my-oss-credentials + path: my-etcd-backups-bucket/etcd.backup + endpoint: http://oss-cn-hangzhou.aliyuncs.com + backupStorageType: OSS + etcdCluster: + name: example-etcd-cluster +status: + succeeded: true +``` + +3. Verify the `EtcdCluster` CR and restored pods for the restored cluster. +```sh +$ kubectl get etcdcluster +$ kubectl get pods -l app=etcd,etcd_cluster=example-etcd-cluster +``` diff --git a/pkg/apis/etcd/v1beta2/backup_types.go b/pkg/apis/etcd/v1beta2/backup_types.go index f9c977487..3f6ff2367 100644 --- a/pkg/apis/etcd/v1beta2/backup_types.go +++ b/pkg/apis/etcd/v1beta2/backup_types.go @@ -34,6 +34,11 @@ const ( BackupStorageTypeGCS BackupStorageType = "GCS" GCPAccessToken = "access-token" GCPCredentialsJson = "credentials.json" + + // Alibaba Cloud OSS related consts + BackupStorageTypeOSS BackupStorageType = "OSS" + AlibabaCloudSecretCredentialsAccessKeyID = "accessKeyID" + AlibabaCloudSecretCredentialsAccessKeySecret = "accessKeySecret" ) type BackupStorageType string @@ -90,6 +95,8 @@ type BackupSource struct { ABS *ABSBackupSource `json:"abs,omitempty"` // GCS defines the GCS backup source spec. GCS *GCSBackupSource `json:"gcs,omitempty"` + // OSS defines the OSS backup source spec. + OSS *OSSBackupSource `json:"oss,omitempty"` } // BackupPolicy defines backup policy. @@ -169,3 +176,38 @@ type GCSBackupSource struct { // If omitted, client will use the default application credentials. GCPSecret string `json:"gcpSecret,omitempty"` } + +// OSSBackupSource provides the spec how to store backups on OSS. +type OSSBackupSource struct { + // Path is the full abs path where the backup is saved. + // The format of the path must be: "/" + // e.g: "mybucket/etcd.backup" + Path string `json:"path"` + + // The name of the secret object that stores the credential which will be used + // to access Alibaba Cloud OSS. + // + // The secret must contain the following keys/fields: + // accessKeyID + // accessKeySecret + // + // The format of secret: + // + // apiVersion: v1 + // kind: Secret + // metadata: + // name: + // type: Opaque + // data: + // accessKeyID: + // accessKeySecret: + // + OSSSecret string `json:"ossSecret"` + + // Endpoint is the OSS service endpoint on alibaba cloud, defaults to + // "http://oss-cn-hangzhou.aliyuncs.com". + // + // Details about regions and endpoints, see: + // https://www.alibabacloud.com/help/doc-detail/31837.htm + Endpoint string `json:"endpoint,omitempty"` +} diff --git a/pkg/apis/etcd/v1beta2/restore_types.go b/pkg/apis/etcd/v1beta2/restore_types.go index 997833c19..11ee51d72 100644 --- a/pkg/apis/etcd/v1beta2/restore_types.go +++ b/pkg/apis/etcd/v1beta2/restore_types.go @@ -69,6 +69,9 @@ type RestoreSource struct { // GCS tells where on GCS the backup is saved and how to fetch the backup. GCS *GCSRestoreSource `json:"gcs,omitempty"` + + // OSS tells where on OSS the backup is saved and how to fetch the backup. + OSS *OSSRestoreSource `json:"oss,omitempty"` } type S3RestoreSource struct { @@ -120,6 +123,40 @@ type GCSRestoreSource struct { GCPSecret string `json:"gcpSecret,omitempty"` } +type OSSRestoreSource struct { + // Path is the full abs path where the backup is saved. + // The format of the path must be: "/" + // e.g: "myossbucket/etcd.backup" + Path string `json:"path"` + + // The name of the secret object that stores the credential which will be used + // to access Alibaba Cloud OSS. + // + // The secret must contain the following keys/fields: + // accessKeyID + // accessKeySecret + // + // The format of secret: + // + // apiVersion: v1 + // kind: Secret + // metadata: + // name: + // type: Opaque + // data: + // accessKeyID: + // accessKeySecret: + // + OSSSecret string `json:"ossSecret"` + + // Endpoint is the OSS service endpoint on alibaba cloud, defaults to + // "http://oss-cn-hangzhou.aliyuncs.com". + // + // Details about regions and endpoints, see: + // https://www.alibabacloud.com/help/doc-detail/31837.htm + Endpoint string `json:"endpoint,omitempty"` +} + // RestoreStatus reports the status of this restore operation. type RestoreStatus struct { // Succeeded indicates if the backup has Succeeded. diff --git a/pkg/apis/etcd/v1beta2/zz_generated.deepcopy.go b/pkg/apis/etcd/v1beta2/zz_generated.deepcopy.go index c4ff202e8..c9e2ef230 100644 --- a/pkg/apis/etcd/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/etcd/v1beta2/zz_generated.deepcopy.go @@ -91,6 +91,11 @@ func (in *BackupSource) DeepCopyInto(out *BackupSource) { *out = new(GCSBackupSource) **out = **in } + if in.OSS != nil { + in, out := &in.OSS, &out.OSS + *out = new(OSSBackupSource) + **out = **in + } return } @@ -485,6 +490,38 @@ func (in *MembersStatus) DeepCopy() *MembersStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OSSBackupSource) DeepCopyInto(out *OSSBackupSource) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSSBackupSource. +func (in *OSSBackupSource) DeepCopy() *OSSBackupSource { + if in == nil { + return nil + } + out := new(OSSBackupSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OSSRestoreSource) DeepCopyInto(out *OSSRestoreSource) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSSRestoreSource. +func (in *OSSRestoreSource) DeepCopy() *OSSRestoreSource { + if in == nil { + return nil + } + out := new(OSSRestoreSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PodPolicy) DeepCopyInto(out *PodPolicy) { *out = *in @@ -570,6 +607,11 @@ func (in *RestoreSource) DeepCopyInto(out *RestoreSource) { *out = new(GCSRestoreSource) **out = **in } + if in.OSS != nil { + in, out := &in.OSS, &out.OSS + *out = new(OSSRestoreSource) + **out = **in + } return } diff --git a/pkg/backup/reader/oss_reader.go b/pkg/backup/reader/oss_reader.go new file mode 100644 index 000000000..e11eed0c7 --- /dev/null +++ b/pkg/backup/reader/oss_reader.go @@ -0,0 +1,59 @@ +// Copyright 2019 The etcd-operator Authors +// +// 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 reader + +import ( + "fmt" + "io" + + "github.com/coreos/etcd-operator/pkg/backup/util" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// ensure ossReader satisfies reader interface. +var _ Reader = &ossReader{} + +// ossReader provides Reader imlementation for reading a file from S3 +type ossReader struct { + client *oss.Client +} + +// NewOSSReader return a Reader implementation to read a file from OSS in the form of ossReader +func NewOSSReader(client *oss.Client) Reader { + return &ossReader{client: client} +} + +// Open opens the file on path where path must be in the format "/" +func (ossr *ossReader) Open(path string) (io.ReadCloser, error) { + bk, key, err := util.ParseBucketAndKey(path) + if err != nil { + return nil, fmt.Errorf("failed to parse oss bucket and key: %v", err) + } + + exist, err := ossr.client.IsBucketExist(bk) + if err != nil { + return nil, err + } else if !exist { + return nil, fmt.Errorf("OSS: bucket<%s> not found", bk) + } + + bucket, err := ossr.client.Bucket(bk) + if err != nil { + return nil, err + } + + return bucket.GetObject(key) +} diff --git a/pkg/backup/writer/oss_writer.go b/pkg/backup/writer/oss_writer.go new file mode 100644 index 000000000..e254659cb --- /dev/null +++ b/pkg/backup/writer/oss_writer.go @@ -0,0 +1,142 @@ +// Copyright 2019 The etcd-operator Authors +// +// 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 writer + +import ( + "context" + "fmt" + "io" + "path" + "strconv" + + "github.com/coreos/etcd-operator/pkg/backup/util" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +type ossWriter struct { + oss *oss.Client +} + +// NewOSSWriter creates a oss writer. +func NewOSSWriter(oss *oss.Client) Writer { + return &ossWriter{oss: oss} +} + +// Write writes the backup file to the given oss path, "/". +func (ossw *ossWriter) Write(ctx context.Context, path string, r io.Reader) (int64, error) { + // TODO: support context. + bk, key, err := util.ParseBucketAndKey(path) + if err != nil { + return 0, err + } + + // If bucket doesn't exist, we create it. + exist, err := ossw.oss.IsBucketExist(bk) + if err != nil { + return 0, err + } else if !exist { + if err = ossw.oss.CreateBucket(bk); err != nil { + return 0, fmt.Errorf("failed to create bucket, error: %v", err) + } + } + + bucket, err := ossw.oss.Bucket(bk) + if err != nil { + return 0, err + } + + if err = bucket.PutObject(key, r); err != nil { + return 0, err + } + + rc, err := bucket.GetObject(key) + if err != nil { + return 0, fmt.Errorf("failed to get oss object: %v", err) + } + + var resp *oss.Response + var ok bool + + if resp, ok = rc.(*oss.Response); !ok { + return 0, fmt.Errorf("the response type from GetObject(%s) is not *oss.Response", key) + } + + defer resp.Close() + + clstr := resp.Headers.Get("content-length") + if clstr == "" { + return 0, fmt.Errorf("content-length not found in headers of response in GetObject") + } + + cl, err := strconv.ParseInt(clstr, 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid content-length: %s", clstr) + } + + return cl, nil +} + +func (ossw *ossWriter) List(ctx context.Context, basePath string) ([]string, error) { + // TODO: support context. + bk, key, err := util.ParseBucketAndKey(basePath) + if err != nil { + return nil, err + } + + bucket, err := ossw.oss.Bucket(bk) + if err != nil { + return nil, err + } + + var objKeys []string + + marker := oss.Marker("") + prefix := oss.Prefix(key) + for { + resp, err := bucket.ListObjects(oss.MaxKeys(1000), marker, prefix) + if err != nil { + return nil, fmt.Errorf("failed to list objects: %v", err) + } + + for _, obj := range resp.Objects { + objKeys = append(objKeys, path.Join(bk, obj.Key)) + } + + prefix = oss.Prefix(resp.Prefix) + marker = oss.Marker(resp.NextMarker) + + if !resp.IsTruncated { + break + } + } + + return objKeys, nil +} + +func (ossw *ossWriter) Delete(ctx context.Context, path string) error { + // TODO: support context. + bk, key, err := util.ParseBucketAndKey(path) + if err != nil { + return err + } + + bucket, err := ossw.oss.Bucket(bk) + if err != nil { + return err + } + + return bucket.DeleteObject(key) +} diff --git a/pkg/controller/backup-operator/oss_backup.go b/pkg/controller/backup-operator/oss_backup.go new file mode 100644 index 000000000..ed3ebfaa1 --- /dev/null +++ b/pkg/controller/backup-operator/oss_backup.go @@ -0,0 +1,61 @@ +// Copyright 2019 The etcd-operator Authors +// +// 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 controller + +import ( + "context" + "fmt" + + api "github.com/coreos/etcd-operator/pkg/apis/etcd/v1beta2" + "github.com/coreos/etcd-operator/pkg/backup" + "github.com/coreos/etcd-operator/pkg/backup/writer" + "github.com/coreos/etcd-operator/pkg/util/alibabacloudutil/ossfactory" + + "k8s.io/client-go/kubernetes" +) + +// handleOSS saves etcd cluster's backup to specificed OSS path. +func handleOSS(ctx context.Context, kubecli kubernetes.Interface, s *api.OSSBackupSource, endpoints []string, clientTLSSecret, + namespace string, isPeriodic bool, maxBackup int) (*api.BackupStatus, error) { + if s.Endpoint == "" { + s.Endpoint = "http://oss-cn-hangzhou.aliyuncs.com" + } + // TODO: controls NewClientFromSecret with ctx. This depends on upstream kubernetes to support API calls with ctx. + cli, err := ossfactory.NewClientFromSecret(kubecli, namespace, s.Endpoint, s.OSSSecret) + if err != nil { + return nil, err + } + + tlsConfig, err := generateTLSConfig(kubecli, clientTLSSecret, namespace) + if err != nil { + return nil, err + } + + bm := backup.NewBackupManagerFromWriter(kubecli, writer.NewOSSWriter(cli.OSS), tlsConfig, endpoints, namespace) + + rev, etcdVersion, now, err := bm.SaveSnap(ctx, s.Path, isPeriodic) + if err != nil { + return nil, fmt.Errorf("failed to save snapshot (%v)", err) + } + + if maxBackup > 0 { + err := bm.EnsureMaxBackup(ctx, s.Path, maxBackup) + if err != nil { + return nil, fmt.Errorf("succeeded in saving snapshot but failed to delete old snapshot (%v)", err) + } + } + + return &api.BackupStatus{EtcdVersion: etcdVersion, EtcdRevision: rev, LastSuccessDate: *now}, nil +} diff --git a/pkg/controller/backup-operator/sync.go b/pkg/controller/backup-operator/sync.go index 797027c7f..e5ae56567 100644 --- a/pkg/controller/backup-operator/sync.go +++ b/pkg/controller/backup-operator/sync.go @@ -288,6 +288,13 @@ func (b *Backup) handleBackup(parentContext *context.Context, spec *api.BackupSp return nil, err } return bs, nil + case api.BackupStorageTypeOSS: + bs, err := handleOSS(ctx, b.kubecli, spec.OSS, spec.EtcdEndpoints, spec.ClientTLSSecret, + b.namespace, isPeriodic, backupMaxCount) + if err != nil { + return nil, err + } + return bs, nil default: logrus.Fatalf("unknown StorageType: %v", spec.StorageType) } diff --git a/pkg/controller/restore-operator/http.go b/pkg/controller/restore-operator/http.go index fa436260a..cc20e28fe 100644 --- a/pkg/controller/restore-operator/http.go +++ b/pkg/controller/restore-operator/http.go @@ -24,6 +24,7 @@ import ( api "github.com/coreos/etcd-operator/pkg/apis/etcd/v1beta2" "github.com/coreos/etcd-operator/pkg/backup/backupapi" "github.com/coreos/etcd-operator/pkg/backup/reader" + "github.com/coreos/etcd-operator/pkg/util/alibabacloudutil/ossfactory" "github.com/coreos/etcd-operator/pkg/util/awsutil/s3factory" "github.com/coreos/etcd-operator/pkg/util/azureutil/absfactory" "github.com/coreos/etcd-operator/pkg/util/gcputil/gcsfactory" @@ -138,6 +139,23 @@ func (r *Restore) serveBackup(w http.ResponseWriter, req *http.Request) error { backupReader = reader.NewGCSReader(ctx, gcsCli.GCS) path = gcsRestoreSource.Path + case api.BackupStorageTypeOSS: + restoreSource := cr.Spec.RestoreSource + if restoreSource.OSS == nil { + return errors.New("empty oss restore source") + } + ossRestoreSource := restoreSource.OSS + if len(ossRestoreSource.OSSSecret) == 0 || len(ossRestoreSource.Path) == 0 { + return errors.New("invalid oss restore source field (spec.oss), must specify all required subfields") + } + + ossCli, err := ossfactory.NewClientFromSecret(r.kubecli, r.namespace, ossRestoreSource.Endpoint, ossRestoreSource.OSSSecret) + if err != nil { + return fmt.Errorf("failed to create OSS client: %v", err) + } + + backupReader = reader.NewOSSReader(ossCli.OSS) + path = ossRestoreSource.Path default: return fmt.Errorf("unknown backup storage type (%s) for restore CR (%v)", cr.Spec.BackupStorageType, restoreName) } diff --git a/pkg/util/alibabacloudutil/ossfactory/client.go b/pkg/util/alibabacloudutil/ossfactory/client.go new file mode 100644 index 000000000..4eee6b38d --- /dev/null +++ b/pkg/util/alibabacloudutil/ossfactory/client.go @@ -0,0 +1,62 @@ +// Copyright 2019 The etcd-operator Authors +// +// 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 ossfactory + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" + api "github.com/coreos/etcd-operator/pkg/apis/etcd/v1beta2" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// OSSClient is a wrapper of OSS client that provides cleanup functionality. +type OSSClient struct { + OSS *oss.Client +} + +// NewClientFromSecret returns a OSS client based on given k8s secret containing alibabacloud credentials. +func NewClientFromSecret(kubecli kubernetes.Interface, namespace, endpoint, ossSecret string) (w *OSSClient, err error) { + defer func() { + if err != nil { + err = fmt.Errorf("new OSS client failed: %v", err) + } + }() + + se, err := kubecli.CoreV1().Secrets(namespace).Get(ossSecret, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get k8s secret: %v", err) + } + + accessKeyID, ok := se.Data[api.AlibabaCloudSecretCredentialsAccessKeyID] + if !ok { + return nil, fmt.Errorf("key \"%s\" not found in secret \"%s\" in namespace \"%s\"", + api.AlibabaCloudSecretCredentialsAccessKeyID, ossSecret, namespace) + } + + accessKeySecret, ok := se.Data[api.AlibabaCloudSecretCredentialsAccessKeySecret] + if !ok { + return nil, fmt.Errorf("key \"%s\" not found in secret \"%s\" in namespace \"%s\"", + api.AlibabaCloudSecretCredentialsAccessKeySecret, ossSecret, namespace) + } + + client, err := oss.New(endpoint, string(accessKeyID), string(accessKeySecret)) + if err != nil { + return nil, fmt.Errorf("failed to create OSS client: %v", err) + } + return &OSSClient{OSS: client}, nil +}