Initial commit

Signed-off-by: Andy Goldstein <andy.goldstein@gmail.com>
This commit is contained in:
Andy Goldstein
2017-08-02 13:27:17 -04:00
commit 2fe501f527
2024 changed files with 948288 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type jobRestorer struct{}
var _ ResourceRestorer = &jobRestorer{}
func NewJobRestorer() ResourceRestorer {
return &jobRestorer{}
}
func (r *jobRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (r *jobRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
glog.V(4).Infof("resetting metadata and status")
_, err := resetMetadataAndStatus(obj, true)
if err != nil {
return nil, err
}
glog.V(4).Infof("getting spec.selector.matchLabels")
matchLabels, err := collections.GetMap(obj.UnstructuredContent(), "spec.selector.matchLabels")
if err != nil {
glog.V(4).Infof("unable to get spec.selector.matchLabels: %v", err)
} else {
delete(matchLabels, "controller-uid")
}
templateLabels, err := collections.GetMap(obj.UnstructuredContent(), "spec.template.metadata.labels")
if err != nil {
glog.V(4).Infof("unable to get spec.template.metadata.labels: %v", err)
} else {
delete(templateLabels, "controller-uid")
}
return obj, nil
}
func (r *jobRestorer) Wait() bool {
return false
}
func (r *jobRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
+138
View File
@@ -0,0 +1,138 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
)
func TestJobRestorerPrepare(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no metadata should error",
obj: NewTestUnstructured().Unstructured,
expectedErr: true,
},
{
name: "missing spec.selector and/or spec.template should not error",
obj: NewTestUnstructured().WithName("job-1").
WithSpec().
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpec().
Unstructured,
},
{
name: "missing spec.selector.matchLabels should not error",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("selector", map[string]interface{}{}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("selector", map[string]interface{}{}).
Unstructured,
},
{
name: "spec.selector.matchLabels[controller-uid] is removed",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("selector", map[string]interface{}{
"matchLabels": map[string]interface{}{
"controller-uid": "foo",
"hello": "world",
},
}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("selector", map[string]interface{}{
"matchLabels": map[string]interface{}{
"hello": "world",
},
}).
Unstructured,
},
{
name: "missing spec.template.metadata should not error",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{}).
Unstructured,
},
{
name: "missing spec.template.metadata.labels should not error",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{
"metadata": map[string]interface{}{},
}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{
"metadata": map[string]interface{}{},
}).
Unstructured,
},
{
name: "spec.template.metadata.labels[controller-uid] is removed",
obj: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"controller-uid": "foo",
"hello": "world",
},
},
}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("job-1").
WithSpecField("template", map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"hello": "world",
},
},
}).
Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewJobRestorer()
res, err := restorer.Prepare(test.obj, nil, nil)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
@@ -0,0 +1,78 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type namespaceRestorer struct{}
var _ ResourceRestorer = &namespaceRestorer{}
func NewNamespaceRestorer() ResourceRestorer {
return &namespaceRestorer{}
}
func (nsr *namespaceRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
nsName, err := collections.GetString(obj.UnstructuredContent(), "metadata.name")
if err != nil {
return false
}
for _, restorableNS := range restore.Spec.Namespaces {
if restorableNS == nsName {
return true
}
}
return false
}
func (nsr *namespaceRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
updated, err := resetMetadataAndStatus(obj, true)
if err != nil {
return nil, err
}
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata")
if err != nil {
return nil, err
}
currentName, err := collections.GetString(obj.UnstructuredContent(), "metadata.name")
if err != nil {
return nil, err
}
if newName, mapped := restore.Spec.NamespaceMapping[currentName]; mapped {
metadata["name"] = newName
}
return updated, nil
}
func (nsr *namespaceRestorer) Wait() bool {
return false
}
func (nsr *namespaceRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
@@ -0,0 +1,145 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/stretchr/testify/assert"
)
func TestHandles(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
restore *api.Restore
expect bool
}{
{
name: "restorable NS",
obj: NewTestUnstructured().WithName("ns-1").Unstructured,
restore: newTestRestore().WithRestorableNamespace("ns-1").Restore,
expect: true,
},
{
name: "non-restorable NS",
obj: NewTestUnstructured().WithName("ns-1").Unstructured,
restore: newTestRestore().WithRestorableNamespace("ns-2").Restore,
expect: false,
},
{
name: "namespace obj doesn't have name",
obj: NewTestUnstructured().WithMetadata().Unstructured,
restore: newTestRestore().WithRestorableNamespace("ns-1").Restore,
expect: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewNamespaceRestorer()
assert.Equal(t, test.expect, restorer.Handles(test.obj, test.restore))
})
}
}
func TestPrepare(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
restore *api.Restore
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "standard non-mapped namespace",
obj: NewTestUnstructured().WithStatus().WithName("ns-1").Unstructured,
restore: newTestRestore().Restore,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("ns-1").Unstructured,
},
{
name: "standard mapped namespace",
obj: NewTestUnstructured().WithStatus().WithName("ns-1").Unstructured,
restore: newTestRestore().WithMappedNamespace("ns-1", "ns-2").Restore,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("ns-2").Unstructured,
},
{
name: "object without name results in error",
obj: NewTestUnstructured().WithMetadata().WithStatus().Unstructured,
restore: newTestRestore().Restore,
expectedErr: true,
},
{
name: "annotations are kept",
obj: NewTestUnstructured().WithName("ns-1").WithAnnotations().Unstructured,
restore: newTestRestore().Restore,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("ns-1").WithAnnotations().Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewNamespaceRestorer()
res, err := restorer.Prepare(test.obj, test.restore, nil)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
type testRestore struct {
*api.Restore
}
func newTestRestore() *testRestore {
return &testRestore{
Restore: &api.Restore{
ObjectMeta: metav1.ObjectMeta{
Namespace: api.DefaultNamespace,
},
Spec: api.RestoreSpec{},
},
}
}
func (r *testRestore) WithRestorableNamespace(namespace string) *testRestore {
r.Spec.Namespaces = append(r.Spec.Namespaces, namespace)
return r
}
func (r *testRestore) WithMappedNamespace(from string, to string) *testRestore {
if r.Spec.NamespaceMapping == nil {
r.Spec.NamespaceMapping = make(map[string]string)
}
r.Spec.NamespaceMapping[from] = to
return r
}
func (r *testRestore) WithRestorePVs(restorePVs bool) *testRestore {
r.Spec.RestorePVs = restorePVs
return r
}
+129
View File
@@ -0,0 +1,129 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"regexp"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type podRestorer struct{}
var _ ResourceRestorer = &podRestorer{}
func NewPodRestorer() ResourceRestorer {
return &podRestorer{}
}
func (nsr *podRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
var (
defaultTokenRegex = regexp.MustCompile("default-token-.*")
)
func (nsr *podRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
glog.V(4).Infof("resetting metadata and status")
_, err := resetMetadataAndStatus(obj, true)
if err != nil {
return nil, err
}
glog.V(4).Infof("getting spec")
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
if err != nil {
return nil, err
}
glog.V(4).Infof("deleting spec.NodeName")
delete(spec, "nodeName")
newVolumes := make([]interface{}, 0)
glog.V(4).Infof("iterating over volumes")
err = collections.ForEach(spec, "volumes", func(volume map[string]interface{}) error {
name, err := collections.GetString(volume, "name")
if err != nil {
return err
}
glog.V(4).Infof("checking volume with name %q", name)
if !defaultTokenRegex.MatchString(name) {
glog.V(4).Infof("preserving volume")
newVolumes = append(newVolumes, volume)
} else {
glog.V(4).Infof("excluding volume")
}
return nil
})
if err != nil {
return nil, err
}
glog.V(4).Infof("setting spec.volumes")
spec["volumes"] = newVolumes
glog.V(4).Infof("iterating over containers")
err = collections.ForEach(spec, "containers", func(container map[string]interface{}) error {
var newVolumeMounts []interface{}
err := collections.ForEach(container, "volumeMounts", func(volumeMount map[string]interface{}) error {
name, err := collections.GetString(volumeMount, "name")
if err != nil {
return err
}
glog.V(4).Infof("checking volumeMount with name %q", name)
if !defaultTokenRegex.MatchString(name) {
glog.V(4).Infof("preserving volumeMount")
newVolumeMounts = append(newVolumeMounts, volumeMount)
} else {
glog.V(4).Infof("excluding volumeMount")
}
return nil
})
if err != nil {
return err
}
container["volumeMounts"] = newVolumeMounts
return nil
})
if err != nil {
return nil, err
}
return obj, nil
}
func (nsr *podRestorer) Wait() bool {
return false
}
func (nsr *podRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
+108
View File
@@ -0,0 +1,108 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
)
func TestPodRestorerPrepare(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no spec should error",
obj: NewTestUnstructured().WithName("pod-1").Unstructured,
expectedErr: true,
},
{
name: "nodeName (only) should be deleted from spec",
obj: NewTestUnstructured().WithName("pod-1").WithSpec("nodeName", "foo").
WithSpecField("volumes", []interface{}{}).
WithSpecField("containers", []interface{}{}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pod-1").WithSpec("foo").
WithSpecField("volumes", []interface{}{}).
WithSpecField("containers", []interface{}{}).
Unstructured,
},
{
name: "volumes matching default-token regex should be deleted",
obj: NewTestUnstructured().WithName("pod-1").
WithSpecField("volumes", []interface{}{
map[string]interface{}{"name": "foo"},
map[string]interface{}{"name": "default-token-foo"},
}).WithSpecField("containers", []interface{}{}).Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pod-1").
WithSpecField("volumes", []interface{}{
map[string]interface{}{"name": "foo"},
}).WithSpecField("containers", []interface{}{}).Unstructured,
},
{
name: "container volumeMounts matching default-token regex should be deleted",
obj: NewTestUnstructured().WithName("svc-1").
WithSpecField("volumes", []interface{}{}).
WithSpecField("containers", []interface{}{
map[string]interface{}{
"volumeMounts": []interface{}{
map[string]interface{}{
"name": "foo",
},
map[string]interface{}{
"name": "default-token-foo",
},
},
},
}).
Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("svc-1").
WithSpecField("volumes", []interface{}{}).
WithSpecField("containers", []interface{}{
map[string]interface{}{
"volumeMounts": []interface{}{
map[string]interface{}{
"name": "foo",
},
},
},
}).
Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewPodRestorer()
res, err := restorer.Prepare(test.obj, nil, nil)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
+117
View File
@@ -0,0 +1,117 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"errors"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/collections"
)
type persistentVolumeRestorer struct {
snapshotService cloudprovider.SnapshotService
}
var _ ResourceRestorer = &persistentVolumeRestorer{}
func NewPersistentVolumeRestorer(snapshotService cloudprovider.SnapshotService) ResourceRestorer {
return &persistentVolumeRestorer{
snapshotService: snapshotService,
}
}
func (sr *persistentVolumeRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (sr *persistentVolumeRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
if _, err := resetMetadataAndStatus(obj, false); err != nil {
return nil, err
}
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
if err != nil {
return nil, err
}
delete(spec, "claimRef")
delete(spec, "storageClassName")
if restore.Spec.RestorePVs {
volumeID, err := sr.restoreVolume(obj.UnstructuredContent(), restore, backup)
if err != nil {
return nil, err
}
if err := setVolumeID(spec, volumeID); err != nil {
return nil, err
}
}
return obj, nil
}
func (sr *persistentVolumeRestorer) Wait() bool {
return true
}
func (sr *persistentVolumeRestorer) Ready(obj runtime.Unstructured) bool {
phase, err := collections.GetString(obj.UnstructuredContent(), "status.phase")
return err == nil && phase == "Available"
}
func setVolumeID(spec map[string]interface{}, volumeID string) error {
if pvSource, found := spec["awsElasticBlockStore"]; found {
pvSourceObj := pvSource.(map[string]interface{})
pvSourceObj["volumeID"] = volumeID
return nil
} else if pvSource, found := spec["gcePersistentDisk"]; found {
pvSourceObj := pvSource.(map[string]interface{})
pvSourceObj["pdName"] = volumeID
return nil
} else if pvSource, found := spec["azureDisk"]; found {
pvSourceObj := pvSource.(map[string]interface{})
pvSourceObj["diskName"] = volumeID
return nil
}
return errors.New("persistent volume source is not compatible")
}
func (sr *persistentVolumeRestorer) restoreVolume(item map[string]interface{}, restore *api.Restore, backup *api.Backup) (string, error) {
pvName, err := collections.GetString(item, "metadata.name")
if err != nil {
return "", err
}
if backup.Status.VolumeBackups == nil {
return "", fmt.Errorf("VolumeBackups map not found for persistent volume %s", pvName)
}
backupInfo, found := backup.Status.VolumeBackups[pvName]
if !found {
return "", fmt.Errorf("BackupInfo not found for PersistentVolume %s", pvName)
}
return sr.snapshotService.CreateVolumeFromSnapshot(backupInfo.SnapshotID, backupInfo.Type, backupInfo.Iops)
}
+186
View File
@@ -0,0 +1,186 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
. "github.com/heptio/ark/pkg/util/test"
)
func TestPVRestorerPrepare(t *testing.T) {
iops := 1000
tests := []struct {
name string
obj runtime.Unstructured
restore *api.Restore
backup *api.Backup
volumeMap map[api.VolumeBackupInfo]string
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no name should error",
obj: NewTestUnstructured().WithMetadata().Unstructured,
restore: newTestRestore().Restore,
expectedErr: true,
},
{
name: "no spec should error",
obj: NewTestUnstructured().WithName("pv-1").Unstructured,
restore: newTestRestore().Restore,
expectedErr: true,
},
{
name: "when RestorePVs=false, should not error if there is no PV->BackupInfo map",
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
restore: newTestRestore().WithRestorePVs(false).Restore,
backup: &api.Backup{Status: api.BackupStatus{}},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
},
{
name: "when RestorePVs=true, error if there is no PV->BackupInfo map",
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{}},
expectedErr: true,
expectedRes: nil,
},
{
name: "claimRef and storageClassName (only) should be cleared from spec",
obj: NewTestUnstructured().
WithName("pv-1").
WithSpecField("claimRef", "foo").
WithSpecField("storageClassName", "foo").
WithSpecField("foo", "bar").
Unstructured,
restore: newTestRestore().WithRestorePVs(false).Restore,
expectedErr: false,
expectedRes: NewTestUnstructured().
WithName("pv-1").
WithSpecField("foo", "bar").
Unstructured,
},
{
name: "when RestorePVs=true, AWS volume ID should be set correctly",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", map[string]interface{}{"volumeID": "volume-1"}).Unstructured,
},
{
name: "when RestorePVs=true, GCE pdName should be set correctly",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("gcePersistentDisk", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("gcePersistentDisk", map[string]interface{}{"pdName": "volume-1"}).Unstructured,
},
{
name: "when RestorePVs=true, Azure pdName should be set correctly",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("azureDisk", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("azureDisk", map[string]interface{}{"diskName": "volume-1"}).Unstructured,
},
{
name: "when RestorePVs=true, unsupported PV source should cause error",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("unsupportedPVSource", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: true,
},
{
name: "volume type and IOPS are correctly passed to CreateVolume",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
restore: newTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1", Type: "gp", Iops: &iops}}}},
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1", Type: "gp", Iops: &iops}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", map[string]interface{}{"volumeID": "volume-1"}).Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
snapService := &FakeSnapshotService{RestorableVolumes: test.volumeMap}
restorer := NewPersistentVolumeRestorer(snapService)
res, err := restorer.Prepare(test.obj, test.restore, test.backup)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
func TestPVRestorerReady(t *testing.T) {
tests := []struct {
name string
obj *unstructured.Unstructured
expected bool
}{
{
name: "no status returns not ready",
obj: NewTestUnstructured().Unstructured,
expected: false,
},
{
name: "no status.phase returns not ready",
obj: NewTestUnstructured().WithStatus().Unstructured,
expected: false,
},
{
name: "empty status.phase returns not ready",
obj: NewTestUnstructured().WithStatusField("phase", "").Unstructured,
expected: false,
},
{
name: "non-Available status.phase returns not ready",
obj: NewTestUnstructured().WithStatusField("phase", "foo").Unstructured,
expected: false,
},
{
name: "Available status.phase returns ready",
obj: NewTestUnstructured().WithStatusField("phase", "Available").Unstructured,
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewPersistentVolumeRestorer(nil)
assert.Equal(t, test.expected, restorer.Ready(test.obj))
})
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type persistentVolumeClaimRestorer struct{}
var _ ResourceRestorer = &persistentVolumeClaimRestorer{}
func NewPersistentVolumeClaimRestorer() ResourceRestorer {
return &persistentVolumeClaimRestorer{}
}
func (sr *persistentVolumeClaimRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (sr *persistentVolumeClaimRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
return resetMetadataAndStatus(obj, true)
}
func (sr *persistentVolumeClaimRestorer) Wait() bool {
return true
}
func (sr *persistentVolumeClaimRestorer) Ready(obj runtime.Unstructured) bool {
phase, err := collections.GetString(obj.UnstructuredContent(), "status.phase")
return err == nil && phase == "Bound"
}
@@ -0,0 +1,67 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestPVCRestorerReady(t *testing.T) {
tests := []struct {
name string
obj *unstructured.Unstructured
expected bool
}{
{
name: "no status returns not ready",
obj: NewTestUnstructured().Unstructured,
expected: false,
},
{
name: "no status.phase returns not ready",
obj: NewTestUnstructured().WithStatus().Unstructured,
expected: false,
},
{
name: "empty status.phase returns not ready",
obj: NewTestUnstructured().WithStatusField("phase", "").Unstructured,
expected: false,
},
{
name: "non-Available status.phase returns not ready",
obj: NewTestUnstructured().WithStatusField("phase", "foo").Unstructured,
expected: false,
},
{
name: "Bound status.phase returns ready",
obj: NewTestUnstructured().WithStatusField("phase", "Bound").Unstructured,
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewPersistentVolumeClaimRestorer()
assert.Equal(t, test.expected, restorer.Ready(test.obj))
})
}
}
@@ -0,0 +1,83 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
// ResourceRestorer exposes the operations necessary to prepare Kubernetes resources
// for restore and confirm their readiness following restoration via Ark.
type ResourceRestorer interface {
// Handles returns true if the Restorer should restore this object.
Handles(obj runtime.Unstructured, restore *api.Restore) bool
// Prepare gets an item ready to be restored
Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error)
// Wait returns true if restoration should wait for all of this restorer's resources to be ready before moving on to the next restorer.
Wait() bool
// Ready returns true if the given item is considered ready by the system. Only used if Wait() returns true.
Ready(obj runtime.Unstructured) bool
}
func resetMetadataAndStatus(obj runtime.Unstructured, keepAnnotations bool) (runtime.Unstructured, error) {
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata")
if err != nil {
return nil, err
}
for k := range metadata {
if k != "name" && k != "namespace" && k != "labels" && (!keepAnnotations || k != "annotations") {
delete(metadata, k)
}
}
delete(obj.UnstructuredContent(), "status")
return obj, nil
}
var _ ResourceRestorer = &basicRestorer{}
type basicRestorer struct {
saveAnnotations bool
}
func (br *basicRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (br *basicRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
return resetMetadataAndStatus(obj, br.saveAnnotations)
}
func (br *basicRestorer) Wait() bool {
return false
}
func (br *basicRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
func NewBasicRestorer(saveAnnotations bool) ResourceRestorer {
return &basicRestorer{saveAnnotations: saveAnnotations}
}
@@ -0,0 +1,160 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/stretchr/testify/assert"
)
func TestResetMetadataAndStatus(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
keepAnnotations bool
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no metadata causes error",
obj: NewTestUnstructured(),
keepAnnotations: false,
expectedErr: true,
},
{
name: "don't keep annotations",
obj: NewTestUnstructured().WithMetadata("name", "namespace", "labels", "annotations").Unstructured,
keepAnnotations: false,
expectedErr: false,
expectedRes: NewTestUnstructured().WithMetadata("name", "namespace", "labels").Unstructured,
},
{
name: "keep annotations",
obj: NewTestUnstructured().WithMetadata("name", "namespace", "labels", "annotations").Unstructured,
keepAnnotations: true,
expectedErr: false,
expectedRes: NewTestUnstructured().WithMetadata("name", "namespace", "labels", "annotations").Unstructured,
},
{
name: "don't keep extraneous metadata",
obj: NewTestUnstructured().WithMetadata("foo").Unstructured,
keepAnnotations: false,
expectedErr: false,
expectedRes: NewTestUnstructured().WithMetadata().Unstructured,
},
{
name: "don't keep status",
obj: NewTestUnstructured().WithMetadata().WithStatus().Unstructured,
keepAnnotations: false,
expectedErr: false,
expectedRes: NewTestUnstructured().WithMetadata().Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := resetMetadataAndStatus(test.obj, test.keepAnnotations)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}
type testUnstructured struct {
*unstructured.Unstructured
}
func NewTestUnstructured() *testUnstructured {
obj := &testUnstructured{
Unstructured: &unstructured.Unstructured{
Object: make(map[string]interface{}),
},
}
return obj
}
func (obj *testUnstructured) WithMetadata(fields ...string) *testUnstructured {
return obj.withMap("metadata", fields...)
}
func (obj *testUnstructured) WithSpec(fields ...string) *testUnstructured {
return obj.withMap("spec", fields...)
}
func (obj *testUnstructured) WithStatus(fields ...string) *testUnstructured {
return obj.withMap("status", fields...)
}
func (obj *testUnstructured) WithMetadataField(field string, value interface{}) *testUnstructured {
return obj.withMapEntry("metadata", field, value)
}
func (obj *testUnstructured) WithSpecField(field string, value interface{}) *testUnstructured {
return obj.withMapEntry("spec", field, value)
}
func (obj *testUnstructured) WithStatusField(field string, value interface{}) *testUnstructured {
return obj.withMapEntry("status", field, value)
}
func (obj *testUnstructured) WithAnnotations(fields ...string) *testUnstructured {
annotations := make(map[string]interface{})
for _, field := range fields {
annotations[field] = "foo"
}
obj = obj.WithMetadataField("annotations", annotations)
return obj
}
func (obj *testUnstructured) WithName(name string) *testUnstructured {
return obj.WithMetadataField("name", name)
}
func (obj *testUnstructured) withMap(name string, fields ...string) *testUnstructured {
m := make(map[string]interface{})
obj.Object[name] = m
for _, field := range fields {
m[field] = "foo"
}
return obj
}
func (obj *testUnstructured) withMapEntry(mapName, field string, value interface{}) *testUnstructured {
var m map[string]interface{}
if res, ok := obj.Unstructured.Object[mapName]; !ok {
m = make(map[string]interface{})
obj.Unstructured.Object[mapName] = m
} else {
m = res.(map[string]interface{})
}
m[field] = value
return obj
}
+69
View File
@@ -0,0 +1,69 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
)
type serviceRestorer struct{}
var _ ResourceRestorer = &serviceRestorer{}
func NewServiceRestorer() ResourceRestorer {
return &serviceRestorer{}
}
func (sr *serviceRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bool {
return true
}
func (sr *serviceRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
if _, err := resetMetadataAndStatus(obj, true); err != nil {
return nil, err
}
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
if err != nil {
return nil, err
}
delete(spec, "clusterIP")
ports, err := collections.GetSlice(obj.UnstructuredContent(), "spec.ports")
if err != nil {
return nil, err
}
for _, port := range ports {
p := port.(map[string]interface{})
delete(p, "nodePort")
}
return obj, nil
}
func (sr *serviceRestorer) Wait() bool {
return false
}
func (sr *serviceRestorer) Ready(obj runtime.Unstructured) bool {
return true
}
@@ -0,0 +1,72 @@
/*
Copyright 2017 Heptio 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 restorers
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
)
func TestServiceRestorerPrepare(t *testing.T) {
tests := []struct {
name string
obj runtime.Unstructured
expectedErr bool
expectedRes runtime.Unstructured
}{
{
name: "no spec should error",
obj: NewTestUnstructured().WithName("svc-1").Unstructured,
expectedErr: true,
},
{
name: "clusterIP (only) should be deleted from spec",
obj: NewTestUnstructured().WithName("svc-1").WithSpec("clusterIP", "foo").WithSpecField("ports", []interface{}{}).Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("svc-1").WithSpec("foo").WithSpecField("ports", []interface{}{}).Unstructured,
},
{
name: "nodePort (only) should be deleted from all spec.ports",
obj: NewTestUnstructured().WithName("svc-1").
WithSpecField("ports", []interface{}{
map[string]interface{}{"nodePort": ""},
map[string]interface{}{"nodePort": "", "foo": "bar"},
}).Unstructured,
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("svc-1").
WithSpecField("ports", []interface{}{
map[string]interface{}{},
map[string]interface{}{"foo": "bar"},
}).Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
restorer := NewServiceRestorer()
res, err := restorer.Prepare(test.obj, nil, nil)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
}
})
}
}