make PVProvider optional in server config; disallow snap/restore PVs when not provided

Signed-off-by: Steve Kriss <steve@heptio.com>
pull/43/head
Steve Kriss 2017-08-09 15:52:27 -07:00
parent 3ca085eb58
commit ebc06fd632
18 changed files with 225 additions and 83 deletions

View File

@ -25,17 +25,12 @@ metadata:
name: default
persistentVolumeProvider:
aws:
region: minio
availabilityZone: minio
s3ForcePathStyle: true
s3Url: http://minio:9000
region: us-west-2
availabilityZone: us-west-2a
backupStorageProvider:
bucket: ark
aws:
region: minio
availabilityZone: minio
s3ForcePathStyle: true
s3Url: http://minio:9000
region: us-west-2
backupSyncPeriod: 60m
gcSyncPeriod: 60m
scheduleSyncPeriod: 1m
@ -50,7 +45,7 @@ The configurable parameters are as follows:
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `persistentVolumeProvider` | CloudProviderConfig<br><br>(Supported key values are `aws`, `gcp`, and `azure`, but only one can be present. See the corresponding [AWS][0], [GCP][1], and [Azure][2]-specific configs.) | Required Field | The specification for whichever cloud provider the cluster is using for persistent volumes (to be snapshotted).<br><br> *NOTE*: For Azure, your Kubernetes cluster needs to be version 1.7.2+ in order to support PV snapshotting of its managed disks. |
| `persistentVolumeProvider` | CloudProviderConfig<br><br>(Supported key values are `aws`, `gcp`, and `azure`, but only one can be present. See the corresponding [AWS][0], [GCP][1], and [Azure][2]-specific configs.) | None (Optional) | The specification for whichever cloud provider the cluster is using for persistent volumes (to be snapshotted), if any.<br><br>If not specified, Backups and Restores requesting PV snapshots & restores, respectively, are considered invalid. <br><br> *NOTE*: For Azure, your Kubernetes cluster needs to be version 1.7.2+ in order to support PV snapshotting of its managed disks. |
| `backupStorageProvider`/(inline) | CloudProviderConfig<br><br>(Supported key values are `aws`, `gcp`, and `azure`, but only one can be present. See the corresponding [AWS][0], [GCP][1], and [Azure][2]-specific configs.) | Required Field | The specification for whichever cloud provider will be used to actually store the backups. |
| `backupStorageProvider/bucket` | String | Required Field | The storage bucket where backups are to be uploaded. |
| `backupSyncPeriod` | metav1.Duration | 60m0s | How frequently Ark queries the object storage to make sure that the appropriate Backup resources have been created for existing backup files. |
@ -63,22 +58,44 @@ The configurable parameters are as follows:
**(Or other S3-compatible storage)**
#### backupStorageProvider
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `region` | string | Required Field | *Example*: "us-east-1"<br><br>See [AWS documentation][3] for the full list. |
| `disableSSL` | bool | `false` | Set this to `true` if you are using Minio (or another local, S3-compatible storage service) and your deployment is not secured. |
| `s3ForcePathStyle` | bool | `false` | Set this to `true` if you are using a local storage service like Minio. |
| `s3Url` | string | Required field for non-AWS-hosted storage| *Example*: http://minio:9000<br><br>You can specify the AWS S3 URL here for explicitness, but Ark can already generate it from `region`, `availabilityZone`, and `bucket`. This field is primarily for local storage services like Minio.|
| `kmsKeyID` | string | Empty | *Example*: "502b409c-4da1-419f-a16e-eif453b3i49f"<br><br>Specify an [AWS KMS key][12] id to enable encryption of the backups stored in S3. Only works with AWS S3 and may require explicitly granting key usage rights.|
#### persistentVolumeProvider (AWS Only)
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `region` | string | Required Field | *Example*: "us-east-1"<br><br>See [AWS documentation][3] for the full list. |
| `availabilityZone` | string | Required Field | *Example*: "us-east-1a"<br><br>See [AWS documentation][4] for details. |
| `disableSSL` | bool | `false` | Set this to `true` if you are using Minio (or another local, S3-compatible storage service) and your deployment is not secured. |
| `s3ForcePathStyle` | bool | `false` | Set this to `true` if you are using a local storage service like Minio. |
| `s3Url` | string | Required field for non-AWS-hosted storage| *Example*: http://minio:9000<br><br>You can specify the AWS S3 URL here for explicitness, but Ark can already generate it from `region`, `availabilityZone`, and `bucket`. This field is primarily for local sotrage services like Minio.|
| `kmsKeyID` | string | Empty | *Example*: "502b409c-4da1-419f-a16e-eif453b3i49f"<br><br>Specify an [AWS KMS key][12] id to enable encryption of the backups stored in S3. Only works with AWS S3 and may require explicitly granting key usage rights.|
### GCP
#### backupStorageProvider
No parameters required; specify an empty object per [example file][13].
#### persistentVolumeProvider
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `project` | string | Required Field | *Example*: "project-example-3jsn23"<br><br> See the [Project ID documentation][5] for details. |
| `zone` | string | Required Field | *Example*: "us-central1-a"<br><br>See [GCP documentation][6] for the full list. |
### Azure
#### backupStorageProvider
No parameters required; specify an empty object per [example file][14].
#### persistentVolumeProvider
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `location` | string | Required Field | *Example*: "Canada East"<br><br>See [the list of available locations][7] (note that this particular page refers to them as "Regions"). |
@ -97,4 +114,6 @@ The configurable parameters are as follows:
[10]: #overview
[11]: #example
[12]: http://docs.aws.amazon.com/kms/latest/developerguide/overview.html
[13]: ../examples/gcp/00-ark-config.yaml
[14]: ../examples/azure/10-ark-config.yaml

View File

@ -26,7 +26,6 @@ backupStorageProvider:
bucket: <YOUR_BUCKET>
aws:
region: <YOUR_REGION>
availabilityZone: <YOUR_AVAILABILITY_ZONE>
backupSyncPeriod: 30m
gcSyncPeriod: 30m
scheduleSyncPeriod: 1m

View File

@ -24,9 +24,7 @@ persistentVolumeProvider:
apiTimeout: <YOUR_TIMEOUT>
backupStorageProvider:
bucket: <YOUR_BUCKET>
azure:
location: <YOUR_LOCATION>
apiTimeout: <YOUR_TIMEOUT>
azure: {}
backupSyncPeriod: 30m
gcSyncPeriod: 30m
scheduleSyncPeriod: 1m

View File

@ -24,9 +24,7 @@ persistentVolumeProvider:
zone: <YOUR_ZONE>
backupStorageProvider:
bucket: <YOUR_BUCKET>
gcp:
project: <YOUR_PROJECT>
zone: <YOUR_ZONE>
gcp: {}
backupSyncPeriod: 30m
gcSyncPeriod: 30m
scheduleSyncPeriod: 1m

View File

@ -18,17 +18,10 @@ kind: Config
metadata:
namespace: heptio-ark
name: default
persistentVolumeProvider:
aws:
region: minio
availabilityZone: minio
s3ForcePathStyle: true
s3Url: http://minio:9000
backupStorageProvider:
bucket: ark
aws:
region: minio
availabilityZone: minio
s3ForcePathStyle: true
s3Url: http://minio:9000
backupSyncPeriod: 1m

View File

@ -35,8 +35,8 @@ type Config struct {
metav1.ObjectMeta `json:"metadata"`
// PersistentVolumeProvider is the configuration information for the cloud where
// the cluster is running and has PersistentVolumes to snapshot or restore.
PersistentVolumeProvider CloudProviderConfig `json:"persistentVolumeProvider"`
// the cluster is running and has PersistentVolumes to snapshot or restore. Optional.
PersistentVolumeProvider *CloudProviderConfig `json:"persistentVolumeProvider"`
// BackupStorageProvider is the configuration information for the cloud where
// Ark backups are stored in object storage. This may be a different cloud than

View File

@ -17,6 +17,7 @@ limitations under the License.
package backup
import (
"errors"
"fmt"
"regexp"
@ -38,11 +39,15 @@ type volumeSnapshotAction struct {
var _ Action = &volumeSnapshotAction{}
func NewVolumeSnapshotAction(snapshotService cloudprovider.SnapshotService) Action {
func NewVolumeSnapshotAction(snapshotService cloudprovider.SnapshotService) (Action, error) {
if snapshotService == nil {
return nil, errors.New("snapshotService cannot be nil")
}
return &volumeSnapshotAction{
snapshotService: snapshotService,
clock: clock.RealClock{},
}
}, nil
}
// Execute triggers a snapshot for the volume/disk underlying a PersistentVolume if the provided

View File

@ -155,7 +155,10 @@ func TestVolumeSnapshotAction(t *testing.T) {
}
snapshotService := &FakeSnapshotService{SnapshottableVolumes: test.volumeInfo}
action := NewVolumeSnapshotAction(snapshotService).(*volumeSnapshotAction)
vsa, _ := NewVolumeSnapshotAction(snapshotService)
action := vsa.(*volumeSnapshotAction)
fakeClock := clock.NewFakeClock(time.Now())
action.clock = fakeClock

View File

@ -251,8 +251,13 @@ func (s *server) initBackupService(config *api.Config) error {
}
func (s *server) initSnapshotService(config *api.Config) error {
if config.PersistentVolumeProvider == nil {
glog.Infof("PersistentVolumeProvider config not provided, skipping SnapshotService creation")
return nil
}
glog.Infof("Configuring cloud provider for snapshot service")
cloud, err := initCloud(config.PersistentVolumeProvider, "persistentVolumeProvider")
cloud, err := initCloud(*config.PersistentVolumeProvider, "persistentVolumeProvider")
if err != nil {
return err
}
@ -408,6 +413,7 @@ func (s *server) runControllers(config *api.Config) error {
backupper,
s.backupService,
config.BackupStorageProvider.Bucket,
s.snapshotService != nil,
)
wg.Add(1)
go func() {
@ -461,6 +467,7 @@ func (s *server) runControllers(config *api.Config) error {
s.backupService,
config.BackupStorageProvider.Bucket,
s.sharedInformerFactory.Ark().V1().Backups(),
s.snapshotService != nil,
)
wg.Add(1)
go func() {
@ -490,7 +497,12 @@ func newBackupper(
actions := map[string]backup.Action{}
if snapshotService != nil {
actions["persistentvolumes"] = backup.NewVolumeSnapshotAction(snapshotService)
action, err := backup.NewVolumeSnapshotAction(snapshotService)
if err != nil {
return nil, err
}
actions["persistentvolumes"] = action
}
return backup.NewKubernetesBackupper(

View File

@ -52,6 +52,7 @@ type backupController struct {
backupper backup.Backupper
backupService cloudprovider.BackupService
bucket string
allowSnapshots bool
lister listers.BackupLister
listerSynced cache.InformerSynced
@ -68,11 +69,13 @@ func NewBackupController(
backupper backup.Backupper,
backupService cloudprovider.BackupService,
bucket string,
allowSnapshots bool,
) Interface {
c := &backupController{
backupper: backupper,
backupService: backupService,
bucket: bucket,
allowSnapshots: allowSnapshots,
lister: backupInformer.Lister(),
listerSynced: backupInformer.Informer().HasSynced,
@ -297,6 +300,10 @@ func (controller *backupController) getValidationErrors(itm *api.Backup) []strin
validationErrors = append(validationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err))
}
if !controller.allowSnapshots && itm.Spec.SnapshotVolumes {
validationErrors = append(validationErrors, "Server is not configured for PV snapshots")
}
return validationErrors
}

View File

@ -54,6 +54,7 @@ func TestProcessBackup(t *testing.T) {
expectedExcludes []string
backup *TestBackup
expectBackup bool
allowSnapshots bool
}{
{
name: "bad key",
@ -129,6 +130,20 @@ func TestProcessBackup(t *testing.T) {
expectedIncludes: []string{"*"},
expectBackup: true,
},
{
name: "backup with SnapshotVolumes when allowSnapshots=false fails validation",
key: "heptio-ark/backup1",
backup: NewTestBackup().WithName("backup1").WithPhase(v1.BackupPhaseNew).WithSnapshotVolumes(true),
expectBackup: false,
},
{
name: "backup with SnapshotVolumes when allowSnapshots=true gets executed",
key: "heptio-ark/backup1",
backup: NewTestBackup().WithName("backup1").WithPhase(v1.BackupPhaseNew).WithSnapshotVolumes(true),
allowSnapshots: true,
expectedIncludes: []string{"*"},
expectBackup: true,
},
}
// flag.Set("logtostderr", "true")
@ -150,6 +165,7 @@ func TestProcessBackup(t *testing.T) {
backupper,
cloudBackups,
"bucket",
test.allowSnapshots,
).(*backupController)
c.clock = clock.NewFakeClock(time.Now())
@ -180,6 +196,7 @@ func TestProcessBackup(t *testing.T) {
}
backup.Spec.IncludedNamespaces = expectedNSes
backup.Spec.SnapshotVolumes = test.backup.Spec.SnapshotVolumes
backup.Status.Phase = v1.BackupPhaseInProgress
backup.Status.Expiration.Time = expiration
backup.Status.Version = 1
@ -226,6 +243,7 @@ func TestProcessBackup(t *testing.T) {
WithExcludedResources(test.expectedExcludes...).
WithIncludedNamespaces(expectedNSes...).
WithTTL(test.backup.Spec.TTL.Duration).
WithSnapshotVolumes(test.backup.Spec.SnapshotVolumes).
WithExpiration(expiration).
WithVersion(1).
Backup,
@ -241,6 +259,7 @@ func TestProcessBackup(t *testing.T) {
WithExcludedResources(test.expectedExcludes...).
WithIncludedNamespaces(expectedNSes...).
WithTTL(test.backup.Spec.TTL.Duration).
WithSnapshotVolumes(test.backup.Spec.SnapshotVolumes).
WithExpiration(expiration).
WithVersion(1).
Backup,

View File

@ -108,7 +108,19 @@ func (c *gcController) cleanBackups() {
// storage should happen first because otherwise there's a possibility the backup sync
// controller would re-create the API object after deletion.
for _, backup := range backups {
if backup.Status.Expiration.Time.Before(now) {
if !backup.Status.Expiration.Time.Before(now) {
glog.Infof("Backup %s/%s has not expired yet, skipping", backup.Namespace, backup.Name)
continue
}
// if the backup includes snapshots but we don't currently have a PVProvider, we don't
// want to orphan the snapshots so skip garbage-collection entirely.
if c.snapshotService == nil && len(backup.Status.VolumeBackups) > 0 {
glog.Warningf("Cannot garbage-collect backup %s/%s because backup includes snapshots and server is not configured with PersistentVolumeProvider",
backup.Namespace, backup.Name)
continue
}
glog.Infof("Removing backup %s/%s", backup.Namespace, backup.Name)
if err := c.backupService.DeleteBackup(c.bucket, backup.Name); err != nil {
glog.Errorf("error deleting backup %s/%s: %v", backup.Namespace, backup.Name, err)
@ -125,9 +137,7 @@ func (c *gcController) cleanBackups() {
if err := c.client.Backups(backup.Namespace).Delete(backup.Name, &metav1.DeleteOptions{}); err != nil {
glog.Errorf("error deleting backup API object %s/%s: %v", backup.Namespace, backup.Name, err)
}
} else {
glog.Infof("Backup %s/%s has not expired yet, skipping", backup.Namespace, backup.Name)
}
}
// also GC any Backup API objects without files in object storage

View File

@ -31,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/generated/clientset/fake"
informers "github.com/heptio/ark/pkg/generated/informers/externalversions"
. "github.com/heptio/ark/pkg/util/test"
@ -41,6 +42,7 @@ type gcTest struct {
bucket string
backups map[string][]*api.Backup
snapshots sets.String
nilSnapshotService bool
expectedBackupsRemaining map[string]sets.String
expectedSnapshotsRemaining sets.String
@ -149,11 +151,38 @@ func TestGarbageCollect(t *testing.T) {
expectedBackupsRemaining: make(map[string]sets.String),
expectedSnapshotsRemaining: sets.NewString("snapshot-3", "snapshot-4"),
},
gcTest{
name: "no snapshot service only GC's backups without snapshots",
bucket: "bucket-1",
backups: map[string][]*api.Backup{
"bucket-1": []*api.Backup{
NewTestBackup().WithName("backup-1").
WithExpiration(fakeClock.Now().Add(-1*time.Second)).
WithSnapshot("pv-1", "snapshot-1").
WithSnapshot("pv-2", "snapshot-2").
Backup,
NewTestBackup().WithName("backup-2").
WithExpiration(fakeClock.Now().Add(-1 * time.Second)).
Backup,
},
},
snapshots: sets.NewString("snapshot-1", "snapshot-2"),
nilSnapshotService: true,
expectedBackupsRemaining: map[string]sets.String{
"bucket-1": sets.NewString("backup-1"),
},
},
}
for _, test := range tests {
backupService := &fakeBackupService{}
snapshotService := &FakeSnapshotService{}
var (
backupService = &fakeBackupService{}
snapshotService *FakeSnapshotService
)
if !test.nilSnapshotService {
snapshotService = &FakeSnapshotService{SnapshotsTaken: test.snapshots}
}
t.Run(test.name, func(t *testing.T) {
backupService.backupsByBucket = make(map[string][]*api.Backup)
@ -167,16 +196,19 @@ func TestGarbageCollect(t *testing.T) {
backupService.backupsByBucket[bucket] = data
}
snapshotService.SnapshotsTaken = test.snapshots
var (
client = fake.NewSimpleClientset()
sharedInformers = informers.NewSharedInformerFactory(client, 0)
snapSvc cloudprovider.SnapshotService
)
if snapshotService != nil {
snapSvc = snapshotService
}
controller := NewGCController(
backupService,
snapshotService,
snapSvc,
test.bucket,
1*time.Millisecond,
sharedInformers.Ark().V1().Backups(),
@ -202,7 +234,9 @@ func TestGarbageCollect(t *testing.T) {
assert.Equal(t, test.expectedBackupsRemaining[bucket], backupNames)
}
if !test.nilSnapshotService {
assert.Equal(t, test.expectedSnapshotsRemaining, snapshotService.SnapshotsTaken)
}
})
}
}

View File

@ -47,6 +47,7 @@ type restoreController struct {
restorer restore.Restorer
backupService cloudprovider.BackupService
bucket string
allowSnapshotRestores bool
backupLister listers.BackupLister
backupListerSynced cache.InformerSynced
@ -64,6 +65,7 @@ func NewRestoreController(
backupService cloudprovider.BackupService,
bucket string,
backupInformer informers.BackupInformer,
allowSnapshotRestores bool,
) Interface {
c := &restoreController{
restoreClient: restoreClient,
@ -71,6 +73,7 @@ func NewRestoreController(
restorer: restorer,
backupService: backupService,
bucket: bucket,
allowSnapshotRestores: allowSnapshotRestores,
backupLister: backupInformer.Lister(),
backupListerSynced: backupInformer.Informer().HasSynced,
restoreLister: restoreInformer.Lister(),
@ -275,6 +278,10 @@ func (controller *restoreController) getValidationErrors(itm *api.Restore) []str
validationErrors = append(validationErrors, "BackupName must be non-empty and correspond to the name of a backup in object storage.")
}
if !controller.allowSnapshotRestores && itm.Spec.RestorePVs {
validationErrors = append(validationErrors, "Server is not configured for PV snapshot restores")
}
return validationErrors
}

View File

@ -42,6 +42,7 @@ func TestProcessRestore(t *testing.T) {
restore *api.Restore
backup *api.Backup
restorerError error
allowRestoreSnapshots bool
expectedErr bool
expectedRestoreUpdates []*api.Restore
expectedRestorerCall *api.Restore
@ -137,6 +138,28 @@ func TestProcessRestore(t *testing.T) {
},
expectedRestorerCall: NewTestRestore("foo", "bar", api.RestorePhaseInProgress).WithBackup("backup-1").WithRestorableNamespace("*").Restore,
},
{
name: "valid restore with RestorePVs=true gets executed when allowRestoreSnapshots=true",
restore: NewTestRestore("foo", "bar", api.RestorePhaseNew).WithBackup("backup-1").WithRestorableNamespace("ns-1").WithRestorePVs(true).Restore,
backup: NewTestBackup().WithName("backup-1").Backup,
allowRestoreSnapshots: true,
expectedErr: false,
expectedRestoreUpdates: []*api.Restore{
NewTestRestore("foo", "bar", api.RestorePhaseInProgress).WithBackup("backup-1").WithRestorableNamespace("ns-1").WithRestorePVs(true).Restore,
NewTestRestore("foo", "bar", api.RestorePhaseCompleted).WithBackup("backup-1").WithRestorableNamespace("ns-1").WithRestorePVs(true).Restore,
},
expectedRestorerCall: NewTestRestore("foo", "bar", api.RestorePhaseInProgress).WithBackup("backup-1").WithRestorableNamespace("ns-1").WithRestorePVs(true).Restore,
},
{
name: "restore with RestorePVs=true fails validation when allowRestoreSnapshots=false",
restore: NewTestRestore("foo", "bar", api.RestorePhaseNew).WithBackup("backup-1").WithRestorableNamespace("ns-1").WithRestorePVs(true).Restore,
backup: NewTestBackup().WithName("backup-1").Backup,
expectedErr: false,
expectedRestoreUpdates: []*api.Restore{
NewTestRestore("foo", "bar", api.RestorePhaseFailedValidation).WithBackup("backup-1").WithRestorableNamespace("ns-1").WithRestorePVs(true).
WithValidationError("Server is not configured for PV snapshot restores").Restore,
},
},
}
// flag.Set("logtostderr", "true")
@ -160,6 +183,7 @@ func TestProcessRestore(t *testing.T) {
backupSvc,
"bucket",
sharedInformers.Ark().V1().Backups(),
test.allowRestoreSnapshots,
).(*restoreController)
if test.restore != nil {

View File

@ -57,6 +57,10 @@ func (sr *persistentVolumeRestorer) Prepare(obj runtime.Unstructured, restore *a
delete(spec, "storageClassName")
if restore.Spec.RestorePVs {
if sr.snapshotService == nil {
return nil, errors.New("PV restorer is not configured for PV snapshot restores")
}
volumeID, err := sr.restoreVolume(obj.UnstructuredContent(), restore, backup)
if err != nil {
return nil, err

View File

@ -104,3 +104,8 @@ func (b *TestBackup) WithSnapshot(pv string, snapshot string) *TestBackup {
b.Status.VolumeBackups[pv] = &v1.VolumeBackupInfo{SnapshotID: snapshot}
return b
}
func (b *TestBackup) WithSnapshotVolumes(value bool) *TestBackup {
b.Spec.SnapshotVolumes = value
return b
}

View File

@ -60,3 +60,8 @@ func (r *TestRestore) WithErrors(e api.RestoreResult) *TestRestore {
r.Status.Errors = e
return r
}
func (r *TestRestore) WithRestorePVs(value bool) *TestRestore {
r.Spec.RestorePVs = value
return r
}