868 lines
22 KiB
Go
868 lines
22 KiB
Go
/*
|
|
Copyright 2019 the Velero contributors.
|
|
|
|
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 backup
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
discoveryfake "k8s.io/client-go/discovery/fake"
|
|
dynamicfake "k8s.io/client-go/dynamic/fake"
|
|
kubefake "k8s.io/client-go/kubernetes/fake"
|
|
|
|
velerov1 "github.com/heptio/velero/pkg/apis/velero/v1"
|
|
"github.com/heptio/velero/pkg/client"
|
|
"github.com/heptio/velero/pkg/discovery"
|
|
"github.com/heptio/velero/pkg/generated/clientset/versioned/fake"
|
|
"github.com/heptio/velero/pkg/test"
|
|
)
|
|
|
|
func TestBackupResourceFiltering(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
backup *velerov1.Backup
|
|
apiResources []*apiResource
|
|
want []string
|
|
}{
|
|
{
|
|
name: "no filters backs up everything",
|
|
backup: defaultBackup().Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/pods/namespaces/zoo/raz.json",
|
|
"resources/deployments.apps/namespaces/foo/bar.json",
|
|
"resources/deployments.apps/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
{
|
|
name: "included resources filter only backs up resources of those types",
|
|
backup: defaultBackup().
|
|
IncludedResources("pods").
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/pods/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
{
|
|
name: "excluded resources filter only backs up resources not of those types",
|
|
backup: defaultBackup().
|
|
ExcludedResources("deployments").
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/pods/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
{
|
|
name: "included namespaces filter only backs up resources in those namespaces",
|
|
backup: defaultBackup().
|
|
IncludedNamespaces("foo").
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/deployments.apps/namespaces/foo/bar.json",
|
|
},
|
|
},
|
|
{
|
|
name: "excluded namespaces filter only backs up resources not in those namespaces",
|
|
backup: defaultBackup().
|
|
ExcludedNamespaces("zoo").
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/deployments.apps/namespaces/foo/bar.json",
|
|
},
|
|
},
|
|
{
|
|
name: "IncludeClusterResources=false only backs up namespaced resources",
|
|
backup: defaultBackup().
|
|
IncludeClusterResources(false).
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
pvs(
|
|
newPV("bar"),
|
|
newPV("baz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/pods/namespaces/zoo/raz.json",
|
|
"resources/deployments.apps/namespaces/foo/bar.json",
|
|
"resources/deployments.apps/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
{
|
|
name: "label selector only backs up matching resources",
|
|
backup: defaultBackup().
|
|
LabelSelector(&metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}}).
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
withLabel(newPod("foo", "bar"), "a", "b"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
withLabel(newDeployment("zoo", "raz"), "a", "b"),
|
|
),
|
|
pvs(
|
|
withLabel(newPV("bar"), "a", "b"),
|
|
withLabel(newPV("baz"), "a", "c"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/deployments.apps/namespaces/zoo/raz.json",
|
|
"resources/persistentvolumes/cluster/bar.json",
|
|
},
|
|
},
|
|
{
|
|
name: "should include cluster-scoped resources if backing up subset of namespaces and IncludeClusterResources=true",
|
|
backup: defaultBackup().
|
|
IncludedNamespaces("ns-1", "ns-2").
|
|
IncludeClusterResources(true).
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("ns-1", "pod-1"),
|
|
newPod("ns-2", "pod-1"),
|
|
newPod("ns-3", "pod-1"),
|
|
),
|
|
pvs(
|
|
newPV("pv-1"),
|
|
newPV("pv-2"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/ns-1/pod-1.json",
|
|
"resources/pods/namespaces/ns-2/pod-1.json",
|
|
"resources/persistentvolumes/cluster/pv-1.json",
|
|
"resources/persistentvolumes/cluster/pv-2.json",
|
|
},
|
|
},
|
|
{
|
|
name: "should not include cluster-scoped resource if backing up subset of namespaces and IncludeClusterResources=false",
|
|
backup: defaultBackup().
|
|
IncludedNamespaces("ns-1", "ns-2").
|
|
IncludeClusterResources(false).
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("ns-1", "pod-1"),
|
|
newPod("ns-2", "pod-1"),
|
|
newPod("ns-3", "pod-1"),
|
|
),
|
|
pvs(
|
|
newPV("pv-1"),
|
|
newPV("pv-2"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/ns-1/pod-1.json",
|
|
"resources/pods/namespaces/ns-2/pod-1.json",
|
|
},
|
|
},
|
|
{
|
|
name: "should not include cluster-scoped resource if backing up subset of namespaces and IncludeClusterResources=nil",
|
|
backup: defaultBackup().
|
|
IncludedNamespaces("ns-1", "ns-2").
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("ns-1", "pod-1"),
|
|
newPod("ns-2", "pod-1"),
|
|
newPod("ns-3", "pod-1"),
|
|
),
|
|
pvs(
|
|
newPV("pv-1"),
|
|
newPV("pv-2"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/ns-1/pod-1.json",
|
|
"resources/pods/namespaces/ns-2/pod-1.json",
|
|
},
|
|
},
|
|
{
|
|
name: "should include cluster-scoped resources if backing up all namespaces and IncludeClusterResources=true",
|
|
backup: defaultBackup().
|
|
IncludeClusterResources(true).
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("ns-1", "pod-1"),
|
|
newPod("ns-2", "pod-1"),
|
|
newPod("ns-3", "pod-1"),
|
|
),
|
|
pvs(
|
|
newPV("pv-1"),
|
|
newPV("pv-2"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/ns-1/pod-1.json",
|
|
"resources/pods/namespaces/ns-2/pod-1.json",
|
|
"resources/pods/namespaces/ns-3/pod-1.json",
|
|
"resources/persistentvolumes/cluster/pv-1.json",
|
|
"resources/persistentvolumes/cluster/pv-2.json",
|
|
},
|
|
},
|
|
{
|
|
name: "should not include cluster-scoped resources if backing up all namespaces and IncludeClusterResources=false",
|
|
backup: defaultBackup().
|
|
IncludeClusterResources(false).
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("ns-1", "pod-1"),
|
|
newPod("ns-2", "pod-1"),
|
|
newPod("ns-3", "pod-1"),
|
|
),
|
|
pvs(
|
|
newPV("pv-1"),
|
|
newPV("pv-2"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/ns-1/pod-1.json",
|
|
"resources/pods/namespaces/ns-2/pod-1.json",
|
|
"resources/pods/namespaces/ns-3/pod-1.json",
|
|
},
|
|
},
|
|
{
|
|
name: "should include cluster-scoped resources if backing up all namespaces and IncludeClusterResources=nil",
|
|
backup: defaultBackup().
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("ns-1", "pod-1"),
|
|
newPod("ns-2", "pod-1"),
|
|
newPod("ns-3", "pod-1"),
|
|
),
|
|
pvs(
|
|
newPV("pv-1"),
|
|
newPV("pv-2"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/ns-1/pod-1.json",
|
|
"resources/pods/namespaces/ns-2/pod-1.json",
|
|
"resources/pods/namespaces/ns-3/pod-1.json",
|
|
"resources/persistentvolumes/cluster/pv-1.json",
|
|
"resources/persistentvolumes/cluster/pv-2.json",
|
|
},
|
|
},
|
|
{
|
|
name: "when a wildcard and a specific resource are included, the wildcard takes precedence",
|
|
backup: defaultBackup().
|
|
IncludedResources("*", "pods").
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/pods/namespaces/zoo/raz.json",
|
|
"resources/deployments.apps/namespaces/foo/bar.json",
|
|
"resources/deployments.apps/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
{
|
|
name: "wildcard excludes are ignored",
|
|
backup: defaultBackup().
|
|
ExcludedResources("*").
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/pods/namespaces/zoo/raz.json",
|
|
"resources/deployments.apps/namespaces/foo/bar.json",
|
|
"resources/deployments.apps/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
{
|
|
name: "unresolvable included resources are ignored",
|
|
backup: defaultBackup().
|
|
IncludedResources("pods", "unresolvable").
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/pods/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
{
|
|
name: "unresolvable excluded resources are ignored",
|
|
backup: defaultBackup().
|
|
ExcludedResources("deployments", "unresolvable").
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/pods/namespaces/foo/bar.json",
|
|
"resources/pods/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var (
|
|
h = newHarness(t)
|
|
req = &Request{Backup: tc.backup}
|
|
backupFile = bytes.NewBuffer([]byte{})
|
|
)
|
|
|
|
for _, resource := range tc.apiResources {
|
|
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
|
|
}
|
|
|
|
h.backupper.Backup(h.log, req, backupFile, nil, nil)
|
|
|
|
assertTarballContents(t, backupFile, append(tc.want, "metadata/version")...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBackupResourceCohabitation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
backup *velerov1.Backup
|
|
apiResources []*apiResource
|
|
want []string
|
|
}{
|
|
{
|
|
name: "when deployments exist only in extensions, they're backed up",
|
|
backup: defaultBackup().Backup(),
|
|
apiResources: []*apiResource{
|
|
extensionsDeployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/deployments.extensions/namespaces/foo/bar.json",
|
|
"resources/deployments.extensions/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
{
|
|
name: "when deployments exist in both apps and extensions, only apps/deployments are backed up",
|
|
backup: defaultBackup().Backup(),
|
|
apiResources: []*apiResource{
|
|
extensionsDeployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
deployments(
|
|
newDeployment("foo", "bar"),
|
|
newDeployment("zoo", "raz"),
|
|
),
|
|
},
|
|
want: []string{
|
|
"resources/deployments.apps/namespaces/foo/bar.json",
|
|
"resources/deployments.apps/namespaces/zoo/raz.json",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var (
|
|
h = newHarness(t)
|
|
req = &Request{Backup: tc.backup}
|
|
backupFile = bytes.NewBuffer([]byte{})
|
|
)
|
|
|
|
for _, resource := range tc.apiResources {
|
|
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
|
|
}
|
|
|
|
h.backupper.Backup(h.log, req, backupFile, nil, nil)
|
|
|
|
assertTarballContents(t, backupFile, append(tc.want, "metadata/version")...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) {
|
|
h := newHarness(t)
|
|
|
|
// run and verify backup 1
|
|
backup1 := &Request{
|
|
Backup: defaultBackup().Backup(),
|
|
}
|
|
backup1File := bytes.NewBuffer([]byte{})
|
|
|
|
h.addItems(t, "apps", "v1", "deployments", "deploys", true, newDeployment("ns-1", "deploy-1"))
|
|
h.addItems(t, "extensions", "v1", "deployments", "deploys", true, newDeployment("ns-1", "deploy-1"))
|
|
|
|
h.backupper.Backup(h.log, backup1, backup1File, nil, nil)
|
|
|
|
assertTarballContents(t, backup1File, "metadata/version", "resources/deployments.apps/namespaces/ns-1/deploy-1.json")
|
|
|
|
// run and verify backup 2
|
|
backup2 := &Request{
|
|
Backup: defaultBackup().Backup(),
|
|
}
|
|
backup2File := bytes.NewBuffer([]byte{})
|
|
|
|
h.backupper.Backup(h.log, backup2, backup2File, nil, nil)
|
|
|
|
assertTarballContents(t, backup2File, "metadata/version", "resources/deployments.apps/namespaces/ns-1/deploy-1.json")
|
|
}
|
|
|
|
func TestBackupResourceOrdering(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
backup *velerov1.Backup
|
|
apiResources []*apiResource
|
|
}{
|
|
{
|
|
name: "core API group: pods come before pvcs, pvcs come before pvs, pvs come before anything else",
|
|
backup: defaultBackup().
|
|
SnapshotVolumes(false).
|
|
Backup(),
|
|
apiResources: []*apiResource{
|
|
pods(
|
|
newPod("foo", "bar"),
|
|
newPod("zoo", "raz"),
|
|
),
|
|
pvcs(
|
|
newPVC("foo", "bar"),
|
|
newPVC("zoo", "raz"),
|
|
),
|
|
pvs(
|
|
newPV("bar"),
|
|
newPV("baz"),
|
|
),
|
|
secrets(
|
|
newSecret("foo", "bar"),
|
|
newSecret("zoo", "raz"),
|
|
),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var (
|
|
h = newHarness(t)
|
|
req = &Request{Backup: tc.backup}
|
|
backupFile = bytes.NewBuffer([]byte{})
|
|
)
|
|
|
|
for _, resource := range tc.apiResources {
|
|
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
|
|
}
|
|
|
|
h.backupper.Backup(h.log, req, backupFile, nil, nil)
|
|
|
|
assertTarballOrdering(t, backupFile, "pods", "persistentvolumeclaims", "persistentvolumes")
|
|
})
|
|
}
|
|
}
|
|
|
|
type apiResource struct {
|
|
group string
|
|
version string
|
|
name string
|
|
shortName string
|
|
namespaced bool
|
|
items []metav1.Object
|
|
}
|
|
|
|
func pods(items ...metav1.Object) *apiResource {
|
|
return &apiResource{
|
|
group: "",
|
|
version: "v1",
|
|
name: "pods",
|
|
shortName: "po",
|
|
namespaced: true,
|
|
items: items,
|
|
}
|
|
}
|
|
|
|
func pvcs(items ...metav1.Object) *apiResource {
|
|
return &apiResource{
|
|
group: "",
|
|
version: "v1",
|
|
name: "persistentvolumeclaims",
|
|
shortName: "pvc",
|
|
namespaced: true,
|
|
items: items,
|
|
}
|
|
}
|
|
|
|
func secrets(items ...metav1.Object) *apiResource {
|
|
return &apiResource{
|
|
group: "",
|
|
version: "v1",
|
|
name: "secrets",
|
|
shortName: "secrets",
|
|
namespaced: true,
|
|
items: items,
|
|
}
|
|
}
|
|
|
|
func deployments(items ...metav1.Object) *apiResource {
|
|
return &apiResource{
|
|
group: "apps",
|
|
version: "v1",
|
|
name: "deployments",
|
|
shortName: "deploy",
|
|
namespaced: true,
|
|
items: items,
|
|
}
|
|
}
|
|
|
|
func extensionsDeployments(items ...metav1.Object) *apiResource {
|
|
return &apiResource{
|
|
group: "extensions",
|
|
version: "v1",
|
|
name: "deployments",
|
|
shortName: "deploy",
|
|
namespaced: true,
|
|
items: items,
|
|
}
|
|
}
|
|
|
|
func pvs(items ...metav1.Object) *apiResource {
|
|
return &apiResource{
|
|
group: "",
|
|
version: "v1",
|
|
name: "persistentvolumes",
|
|
shortName: "pv",
|
|
namespaced: false,
|
|
items: items,
|
|
}
|
|
}
|
|
|
|
type harness struct {
|
|
veleroClient *fake.Clientset
|
|
kubeClient *kubefake.Clientset
|
|
dynamicClient *dynamicfake.FakeDynamicClient
|
|
discoveryClient *test.DiscoveryClient
|
|
backupper *kubernetesBackupper
|
|
log logrus.FieldLogger
|
|
}
|
|
|
|
func (h *harness) addItems(t *testing.T, group, version, resource, shortName string, namespaced bool, items ...metav1.Object) {
|
|
t.Helper()
|
|
|
|
h.discoveryClient.WithResource(group, version, resource, namespaced, shortName)
|
|
require.NoError(t, h.backupper.discoveryHelper.Refresh())
|
|
|
|
gvr := schema.GroupVersionResource{
|
|
Group: group,
|
|
Version: version,
|
|
Resource: resource,
|
|
}
|
|
|
|
for _, item := range items {
|
|
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(item)
|
|
require.NoError(t, err)
|
|
|
|
unstructuredObj := &unstructured.Unstructured{Object: obj}
|
|
|
|
if namespaced {
|
|
_, err = h.dynamicClient.Resource(gvr).Namespace(item.GetNamespace()).Create(unstructuredObj, metav1.CreateOptions{})
|
|
} else {
|
|
_, err = h.dynamicClient.Resource(gvr).Create(unstructuredObj, metav1.CreateOptions{})
|
|
}
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func newHarness(t *testing.T) *harness {
|
|
t.Helper()
|
|
|
|
// API server fakes
|
|
var (
|
|
veleroClient = fake.NewSimpleClientset()
|
|
kubeClient = kubefake.NewSimpleClientset()
|
|
dynamicClient = dynamicfake.NewSimpleDynamicClient(runtime.NewScheme())
|
|
discoveryClient = &test.DiscoveryClient{FakeDiscovery: kubeClient.Discovery().(*discoveryfake.FakeDiscovery)}
|
|
)
|
|
|
|
log := logrus.StandardLogger()
|
|
|
|
discoveryHelper, err := discovery.NewHelper(discoveryClient, log)
|
|
require.NoError(t, err)
|
|
|
|
return &harness{
|
|
veleroClient: veleroClient,
|
|
kubeClient: kubeClient,
|
|
dynamicClient: dynamicClient,
|
|
discoveryClient: discoveryClient,
|
|
backupper: &kubernetesBackupper{
|
|
dynamicFactory: client.NewDynamicFactory(dynamicClient),
|
|
discoveryHelper: discoveryHelper,
|
|
groupBackupperFactory: new(defaultGroupBackupperFactory),
|
|
|
|
// unsupported
|
|
podCommandExecutor: nil,
|
|
resticBackupperFactory: nil,
|
|
resticTimeout: 0,
|
|
},
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
func withLabel(obj metav1.Object, key, val string) metav1.Object {
|
|
labels := obj.GetLabels()
|
|
if labels == nil {
|
|
labels = make(map[string]string)
|
|
}
|
|
labels[key] = val
|
|
obj.SetLabels(labels)
|
|
|
|
return obj
|
|
}
|
|
|
|
func newPod(ns, name string) *corev1.Pod {
|
|
return &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: ns,
|
|
Name: name,
|
|
},
|
|
}
|
|
}
|
|
|
|
func newPVC(ns, name string) *corev1.PersistentVolumeClaim {
|
|
return &corev1.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: ns,
|
|
Name: name,
|
|
},
|
|
}
|
|
}
|
|
|
|
func newSecret(ns, name string) *corev1.Secret {
|
|
return &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: ns,
|
|
Name: name,
|
|
},
|
|
}
|
|
}
|
|
|
|
func newDeployment(ns, name string) *appsv1.Deployment {
|
|
return &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: ns,
|
|
Name: name,
|
|
},
|
|
}
|
|
}
|
|
|
|
func newPV(name string) *corev1.PersistentVolume {
|
|
return &corev1.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
}
|
|
}
|
|
|
|
func defaultBackup() *Builder {
|
|
return NewNamedBuilder(velerov1.DefaultNamespace, "backup-1")
|
|
}
|
|
|
|
func assertTarballContents(t *testing.T, backupFile io.Reader, items ...string) {
|
|
t.Helper()
|
|
|
|
gzr, err := gzip.NewReader(backupFile)
|
|
require.NoError(t, err)
|
|
|
|
r := tar.NewReader(gzr)
|
|
|
|
var files []string
|
|
for {
|
|
hdr, err := r.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
files = append(files, hdr.Name)
|
|
}
|
|
|
|
sort.Strings(files)
|
|
sort.Strings(items)
|
|
assert.Equal(t, items, files)
|
|
}
|
|
|
|
// assertTarballOrdering ensures that resources were written to the tarball in the expected
|
|
// order. Any resources *not* in orderedResources are required to come *after* all resources
|
|
// in orderedResources, in any order.
|
|
func assertTarballOrdering(t *testing.T, backupFile io.Reader, orderedResources ...string) {
|
|
t.Helper()
|
|
|
|
gzr, err := gzip.NewReader(backupFile)
|
|
require.NoError(t, err)
|
|
|
|
r := tar.NewReader(gzr)
|
|
|
|
// lastSeen tracks the index in 'orderedResources' of the last resource type
|
|
// we saw in the tarball. Once we've seen a resource in 'orderedResources',
|
|
// we should never see another instance of a prior resource.
|
|
lastSeen := 0
|
|
|
|
for {
|
|
hdr, err := r.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
// ignore files like metadata/version
|
|
if !strings.HasPrefix(hdr.Name, "resources/") {
|
|
continue
|
|
}
|
|
|
|
// get the resource name
|
|
parts := strings.Split(hdr.Name, "/")
|
|
require.True(t, len(parts) >= 2)
|
|
resourceName := parts[1]
|
|
|
|
// Find the index in 'orderedResources' of the resource type for
|
|
// the current tar item, if it exists. This index ('current') *must*
|
|
// be greater than or equal to 'lastSeen', which was the last resource
|
|
// we saw, since otherwise the current resource would be out of order. By
|
|
// initializing current to len(ordered), we're saying that if the resource
|
|
// is not explicitly in orederedResources, then it must come *after*
|
|
// all orderedResources.
|
|
current := len(orderedResources)
|
|
for i, item := range orderedResources {
|
|
if item == resourceName {
|
|
current = i
|
|
break
|
|
}
|
|
}
|
|
|
|
// the index of the current resource must be the same as or greater than the index of
|
|
// the last resource we saw for the backed-up order to be correct.
|
|
assert.True(t, current >= lastSeen, "%s was backed up out of order", resourceName)
|
|
lastSeen = current
|
|
}
|
|
}
|