2017-08-02 17:27:17 +00:00
|
|
|
/*
|
2019-03-31 23:09:17 +00:00
|
|
|
Copyright 2017, 2019 the Velero contributors.
|
2017-08-02 17:27:17 +00:00
|
|
|
|
|
|
|
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 aws
|
|
|
|
|
|
|
|
import (
|
2019-03-12 19:04:33 +00:00
|
|
|
"fmt"
|
2018-08-30 14:44:03 +00:00
|
|
|
"os"
|
2017-11-29 17:23:21 +00:00
|
|
|
"regexp"
|
2018-08-30 14:44:03 +00:00
|
|
|
"strings"
|
2017-11-29 17:23:21 +00:00
|
|
|
|
2017-08-15 20:20:23 +00:00
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
2018-07-19 20:31:30 +00:00
|
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
2017-08-15 20:20:23 +00:00
|
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
2017-08-02 17:27:17 +00:00
|
|
|
"github.com/aws/aws-sdk-go/service/ec2"
|
2017-09-14 21:27:31 +00:00
|
|
|
"github.com/pkg/errors"
|
2018-05-13 13:28:09 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2019-01-07 19:00:10 +00:00
|
|
|
v1 "k8s.io/api/core/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
2017-11-29 17:23:21 +00:00
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
2017-08-11 19:10:47 +00:00
|
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
2019-03-31 23:09:17 +00:00
|
|
|
|
|
|
|
"github.com/heptio/velero/pkg/cloudprovider"
|
2017-08-02 17:27:17 +00:00
|
|
|
)
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
const regionKey = "region"
|
|
|
|
|
|
|
|
// iopsVolumeTypes is a set of AWS EBS volume types for which IOPS should
|
|
|
|
// be captured during snapshot and provided when creating a new volume
|
|
|
|
// from snapshot.
|
|
|
|
var iopsVolumeTypes = sets.NewString("io1")
|
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
type VolumeSnapshotter struct {
|
2018-05-13 13:28:09 +00:00
|
|
|
log logrus.FieldLogger
|
2017-08-02 17:27:17 +00:00
|
|
|
ec2 *ec2.EC2
|
|
|
|
}
|
|
|
|
|
2019-06-07 18:01:39 +00:00
|
|
|
// takes AWS credential config & a profile to create a new session
|
|
|
|
func getSession(config *aws.Config, profile string) (*session.Session, error) {
|
|
|
|
sessionOptions := session.Options{Config: *config, Profile: profile}
|
|
|
|
sess, err := session.NewSessionWithOptions(sessionOptions)
|
2017-08-15 20:20:23 +00:00
|
|
|
if err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return nil, errors.WithStack(err)
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := sess.Config.Credentials.Get(); err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return nil, errors.WithStack(err)
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return sess, nil
|
|
|
|
}
|
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
func NewVolumeSnapshotter(logger logrus.FieldLogger) *VolumeSnapshotter {
|
|
|
|
return &VolumeSnapshotter{log: logger}
|
2017-11-13 23:31:36 +00:00
|
|
|
}
|
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
func (b *VolumeSnapshotter) Init(config map[string]string) error {
|
2019-06-07 18:01:39 +00:00
|
|
|
if err := cloudprovider.ValidateVolumeSnapshotterConfigKeys(config, regionKey, credentialProfileKey); err != nil {
|
2019-03-31 23:09:17 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
region := config[regionKey]
|
2019-06-07 18:01:39 +00:00
|
|
|
credentialProfile := config[credentialProfileKey]
|
2017-08-15 20:20:23 +00:00
|
|
|
if region == "" {
|
2017-11-13 23:31:36 +00:00
|
|
|
return errors.Errorf("missing %s in aws configuration", regionKey)
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
awsConfig := aws.NewConfig().WithRegion(region)
|
|
|
|
|
2019-06-07 18:01:39 +00:00
|
|
|
sess, err := getSession(awsConfig, credentialProfile)
|
2017-08-15 20:20:23 +00:00
|
|
|
if err != nil {
|
2017-11-13 23:31:36 +00:00
|
|
|
return err
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
b.ec2 = ec2.New(sess)
|
2017-08-15 20:20:23 +00:00
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
return nil
|
|
|
|
}
|
2017-08-11 19:10:47 +00:00
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
func (b *VolumeSnapshotter) CreateVolumeFromSnapshot(snapshotID, volumeType, volumeAZ string, iops *int64) (volumeID string, err error) {
|
2018-02-28 23:24:46 +00:00
|
|
|
// describe the snapshot so we can apply its tags to the volume
|
|
|
|
snapReq := &ec2.DescribeSnapshotsInput{
|
|
|
|
SnapshotIds: []*string{&snapshotID},
|
|
|
|
}
|
|
|
|
|
|
|
|
snapRes, err := b.ec2.DescribeSnapshots(snapReq)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if count := len(snapRes.Snapshots); count != 1 {
|
|
|
|
return "", errors.Errorf("expected 1 snapshot from DescribeSnapshots for %s, got %v", snapshotID, count)
|
|
|
|
}
|
|
|
|
|
2018-08-30 14:44:03 +00:00
|
|
|
// filter tags through getTagsForCluster() function in order to apply
|
|
|
|
// proper ownership tags to restored volumes
|
2017-08-02 17:27:17 +00:00
|
|
|
req := &ec2.CreateVolumeInput{
|
|
|
|
SnapshotId: &snapshotID,
|
2017-09-25 01:45:36 +00:00
|
|
|
AvailabilityZone: &volumeAZ,
|
2017-08-02 17:27:17 +00:00
|
|
|
VolumeType: &volumeType,
|
2019-03-27 00:30:27 +00:00
|
|
|
Encrypted: snapRes.Snapshots[0].Encrypted,
|
2018-02-28 23:24:46 +00:00
|
|
|
TagSpecifications: []*ec2.TagSpecification{
|
|
|
|
{
|
|
|
|
ResourceType: aws.String(ec2.ResourceTypeVolume),
|
2018-08-30 14:44:03 +00:00
|
|
|
Tags: getTagsForCluster(snapRes.Snapshots[0].Tags),
|
2018-02-28 23:24:46 +00:00
|
|
|
},
|
|
|
|
},
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2017-08-11 19:10:47 +00:00
|
|
|
if iopsVolumeTypes.Has(volumeType) && iops != nil {
|
2017-08-14 16:42:33 +00:00
|
|
|
req.Iops = iops
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
res, err := b.ec2.CreateVolume(req)
|
2017-08-02 17:27:17 +00:00
|
|
|
if err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return "", errors.WithStack(err)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return *res.VolumeId, nil
|
|
|
|
}
|
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
func (b *VolumeSnapshotter) GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error) {
|
2018-02-28 23:24:46 +00:00
|
|
|
volumeInfo, err := b.describeVolume(volumeID)
|
2017-08-02 17:27:17 +00:00
|
|
|
if err != nil {
|
2018-02-28 23:24:46 +00:00
|
|
|
return "", nil, err
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
volumeType string
|
2017-08-14 16:42:33 +00:00
|
|
|
iops *int64
|
2017-08-02 17:27:17 +00:00
|
|
|
)
|
|
|
|
|
2018-02-28 23:24:46 +00:00
|
|
|
if volumeInfo.VolumeType != nil {
|
|
|
|
volumeType = *volumeInfo.VolumeType
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2018-02-28 23:24:46 +00:00
|
|
|
if iopsVolumeTypes.Has(volumeType) && volumeInfo.Iops != nil {
|
|
|
|
iops = volumeInfo.Iops
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2017-08-11 19:10:47 +00:00
|
|
|
return volumeType, iops, nil
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
func (b *VolumeSnapshotter) describeVolume(volumeID string) (*ec2.Volume, error) {
|
2017-08-02 17:27:17 +00:00
|
|
|
req := &ec2.DescribeVolumesInput{
|
|
|
|
VolumeIds: []*string{&volumeID},
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
res, err := b.ec2.DescribeVolumes(req)
|
2017-08-02 17:27:17 +00:00
|
|
|
if err != nil {
|
2018-02-28 23:24:46 +00:00
|
|
|
return nil, errors.WithStack(err)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
2018-02-28 23:24:46 +00:00
|
|
|
if count := len(res.Volumes); count != 1 {
|
|
|
|
return nil, errors.Errorf("Expected one volume from DescribeVolumes for volume ID %v, got %v", volumeID, count)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2018-02-28 23:24:46 +00:00
|
|
|
return res.Volumes[0], nil
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
func (b *VolumeSnapshotter) CreateSnapshot(volumeID, volumeAZ string, tags map[string]string) (string, error) {
|
2018-02-28 23:24:46 +00:00
|
|
|
// describe the volume so we can copy its tags to the snapshot
|
|
|
|
volumeInfo, err := b.describeVolume(volumeID)
|
2017-08-02 17:27:17 +00:00
|
|
|
if err != nil {
|
2018-02-28 23:24:46 +00:00
|
|
|
return "", err
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2018-03-13 18:25:26 +00:00
|
|
|
res, err := b.ec2.CreateSnapshot(&ec2.CreateSnapshotInput{
|
|
|
|
VolumeId: &volumeID,
|
|
|
|
TagSpecifications: []*ec2.TagSpecification{
|
|
|
|
{
|
|
|
|
ResourceType: aws.String(ec2.ResourceTypeSnapshot),
|
|
|
|
Tags: getTags(tags, volumeInfo.Tags),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.WithStack(err)
|
|
|
|
}
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2018-03-13 18:25:26 +00:00
|
|
|
return *res.SnapshotId, nil
|
2018-02-28 23:24:46 +00:00
|
|
|
}
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2018-08-30 14:44:03 +00:00
|
|
|
func getTagsForCluster(snapshotTags []*ec2.Tag) []*ec2.Tag {
|
|
|
|
var result []*ec2.Tag
|
|
|
|
|
|
|
|
clusterName, haveAWSClusterNameEnvVar := os.LookupEnv("AWS_CLUSTER_NAME")
|
|
|
|
|
|
|
|
if haveAWSClusterNameEnvVar {
|
|
|
|
result = append(result, ec2Tag("kubernetes.io/cluster/"+clusterName, "owned"))
|
|
|
|
result = append(result, ec2Tag("KubernetesCluster", clusterName))
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tag := range snapshotTags {
|
|
|
|
if haveAWSClusterNameEnvVar && (strings.HasPrefix(*tag.Key, "kubernetes.io/cluster/") || *tag.Key == "KubernetesCluster") {
|
|
|
|
// if the AWS_CLUSTER_NAME variable is found we want current cluster
|
|
|
|
// to overwrite the old ownership on volumes
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
result = append(result, ec2Tag(*tag.Key, *tag.Value))
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2019-01-25 03:33:07 +00:00
|
|
|
func getTags(veleroTags map[string]string, volumeTags []*ec2.Tag) []*ec2.Tag {
|
2018-03-13 18:25:26 +00:00
|
|
|
var result []*ec2.Tag
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2019-01-25 03:33:07 +00:00
|
|
|
// set Velero-assigned tags
|
|
|
|
for k, v := range veleroTags {
|
2018-03-13 18:25:26 +00:00
|
|
|
result = append(result, ec2Tag(k, v))
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2018-02-28 23:24:46 +00:00
|
|
|
// copy tags from volume to snapshot
|
|
|
|
for _, tag := range volumeTags {
|
2019-01-25 03:33:07 +00:00
|
|
|
// we want current Velero-assigned tags to overwrite any older versions
|
2018-02-28 23:24:46 +00:00
|
|
|
// of them that may exist due to prior snapshots/restores
|
2019-01-25 03:33:07 +00:00
|
|
|
if _, found := veleroTags[*tag.Key]; found {
|
2018-02-28 23:24:46 +00:00
|
|
|
continue
|
|
|
|
}
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2018-03-13 18:25:26 +00:00
|
|
|
result = append(result, ec2Tag(*tag.Key, *tag.Value))
|
2018-02-28 23:24:46 +00:00
|
|
|
}
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2018-03-13 18:25:26 +00:00
|
|
|
return result
|
2018-02-28 23:24:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func ec2Tag(key, val string) *ec2.Tag {
|
|
|
|
return &ec2.Tag{Key: &key, Value: &val}
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
func (b *VolumeSnapshotter) DeleteSnapshot(snapshotID string) error {
|
2017-08-02 17:27:17 +00:00
|
|
|
req := &ec2.DeleteSnapshotInput{
|
|
|
|
SnapshotId: &snapshotID,
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
_, err := b.ec2.DeleteSnapshot(req)
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2018-07-19 20:31:30 +00:00
|
|
|
// if it's a NotFound error, we don't need to return an error
|
|
|
|
// since the snapshot is not there.
|
|
|
|
// see https://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html
|
|
|
|
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidSnapshot.NotFound" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
2017-11-29 17:23:21 +00:00
|
|
|
|
|
|
|
var ebsVolumeIDRegex = regexp.MustCompile("vol-.*")
|
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
func (b *VolumeSnapshotter) GetVolumeID(unstructuredPV runtime.Unstructured) (string, error) {
|
2019-01-07 19:00:10 +00:00
|
|
|
pv := new(v1.PersistentVolume)
|
|
|
|
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredPV.UnstructuredContent(), pv); err != nil {
|
|
|
|
return "", errors.WithStack(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if pv.Spec.AWSElasticBlockStore == nil {
|
2017-11-29 17:23:21 +00:00
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
2019-01-07 19:00:10 +00:00
|
|
|
if pv.Spec.AWSElasticBlockStore.VolumeID == "" {
|
2019-01-23 20:41:03 +00:00
|
|
|
return "", errors.New("spec.awsElasticBlockStore.volumeID not found")
|
2017-11-29 17:23:21 +00:00
|
|
|
}
|
|
|
|
|
2019-01-07 19:00:10 +00:00
|
|
|
return ebsVolumeIDRegex.FindString(pv.Spec.AWSElasticBlockStore.VolumeID), nil
|
2017-11-29 17:23:21 +00:00
|
|
|
}
|
|
|
|
|
2019-03-27 18:22:04 +00:00
|
|
|
func (b *VolumeSnapshotter) SetVolumeID(unstructuredPV runtime.Unstructured, volumeID string) (runtime.Unstructured, error) {
|
2019-01-07 19:00:10 +00:00
|
|
|
pv := new(v1.PersistentVolume)
|
|
|
|
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredPV.UnstructuredContent(), pv); err != nil {
|
|
|
|
return nil, errors.WithStack(err)
|
2017-11-29 17:23:21 +00:00
|
|
|
}
|
|
|
|
|
2019-01-07 19:00:10 +00:00
|
|
|
if pv.Spec.AWSElasticBlockStore == nil {
|
2019-01-23 20:41:03 +00:00
|
|
|
return nil, errors.New("spec.awsElasticBlockStore not found")
|
2019-01-07 19:00:10 +00:00
|
|
|
}
|
|
|
|
|
2019-03-12 19:04:33 +00:00
|
|
|
pvFailureDomainZone := pv.Labels["failure-domain.beta.kubernetes.io/zone"]
|
|
|
|
|
|
|
|
if len(pvFailureDomainZone) > 0 {
|
|
|
|
pv.Spec.AWSElasticBlockStore.VolumeID = fmt.Sprintf("aws://%s/%s", pvFailureDomainZone, volumeID)
|
|
|
|
} else {
|
|
|
|
pv.Spec.AWSElasticBlockStore.VolumeID = volumeID
|
|
|
|
}
|
2019-01-07 19:00:10 +00:00
|
|
|
|
|
|
|
res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pv)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.WithStack(err)
|
|
|
|
}
|
2017-11-29 17:23:21 +00:00
|
|
|
|
2019-01-07 19:00:10 +00:00
|
|
|
return &unstructured.Unstructured{Object: res}, nil
|
2017-11-29 17:23:21 +00:00
|
|
|
}
|