mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-05-12 18:21:34 +00:00
Initial commit
Signed-off-by: Andy Goldstein <andy.goldstein@gmail.com>
This commit is contained in:
372
pkg/controller/schedule_controller_test.go
Normal file
372
pkg/controller/schedule_controller_test.go
Normal file
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
core "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/generated/clientset/fake"
|
||||
informers "github.com/heptio/ark/pkg/generated/informers/externalversions"
|
||||
. "github.com/heptio/ark/pkg/util/test"
|
||||
)
|
||||
|
||||
func TestProcessSchedule(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scheduleKey string
|
||||
schedule *api.Schedule
|
||||
fakeClockTime string
|
||||
expectedErr bool
|
||||
expectedSchedulePhaseUpdate *api.Schedule
|
||||
expectedScheduleLastBackupUpdate *api.Schedule
|
||||
expectedBackupCreate *api.Backup
|
||||
}{
|
||||
{
|
||||
name: "invalid key returns error",
|
||||
scheduleKey: "invalid/key/value",
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing schedule returns early without an error",
|
||||
scheduleKey: "foo/bar",
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "schedule with phase FailedValidation does not get processed",
|
||||
schedule: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseFailedValidation).Schedule,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "schedule with phase New gets validated and failed if invalid",
|
||||
schedule: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseNew).Schedule,
|
||||
expectedErr: false,
|
||||
expectedSchedulePhaseUpdate: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseFailedValidation).
|
||||
WithValidationError("Schedule must be a non-empty valid Cron expression").Schedule,
|
||||
},
|
||||
{
|
||||
name: "schedule with phase <blank> gets validated and failed if invalid",
|
||||
schedule: NewTestSchedule("ns", "name").Schedule,
|
||||
expectedErr: false,
|
||||
expectedSchedulePhaseUpdate: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseFailedValidation).
|
||||
WithValidationError("Schedule must be a non-empty valid Cron expression").Schedule,
|
||||
},
|
||||
{
|
||||
name: "schedule with phase Enabled gets re-validated and failed if invalid",
|
||||
schedule: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseEnabled).Schedule,
|
||||
expectedErr: false,
|
||||
expectedSchedulePhaseUpdate: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseFailedValidation).
|
||||
WithValidationError("Schedule must be a non-empty valid Cron expression").Schedule,
|
||||
},
|
||||
{
|
||||
name: "schedule with phase New gets validated and triggers a backup",
|
||||
schedule: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseNew).WithCronSchedule("@every 5m").Schedule,
|
||||
fakeClockTime: "2017-01-01 12:00:00",
|
||||
expectedErr: false,
|
||||
expectedSchedulePhaseUpdate: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseEnabled).WithCronSchedule("@every 5m").Schedule,
|
||||
expectedBackupCreate: NewTestBackup().WithNamespace("ns").WithName("name-20170101120000").WithLabel("ark-schedule", "name").Backup,
|
||||
expectedScheduleLastBackupUpdate: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseEnabled).
|
||||
WithCronSchedule("@every 5m").WithLastBackupTime("2017-01-01 12:00:00").Schedule,
|
||||
},
|
||||
{
|
||||
name: "schedule with phase Enabled gets re-validated and triggers a backup if valid",
|
||||
schedule: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseEnabled).WithCronSchedule("@every 5m").Schedule,
|
||||
fakeClockTime: "2017-01-01 12:00:00",
|
||||
expectedErr: false,
|
||||
expectedBackupCreate: NewTestBackup().WithNamespace("ns").WithName("name-20170101120000").WithLabel("ark-schedule", "name").Backup,
|
||||
expectedScheduleLastBackupUpdate: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseEnabled).
|
||||
WithCronSchedule("@every 5m").WithLastBackupTime("2017-01-01 12:00:00").Schedule,
|
||||
},
|
||||
{
|
||||
name: "schedule that's already run gets LastBackup updated",
|
||||
schedule: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseEnabled).
|
||||
WithCronSchedule("@every 5m").WithLastBackupTime("2000-01-01 00:00:00").Schedule,
|
||||
fakeClockTime: "2017-01-01 12:00:00",
|
||||
expectedErr: false,
|
||||
expectedBackupCreate: NewTestBackup().WithNamespace("ns").WithName("name-20170101120000").WithLabel("ark-schedule", "name").Backup,
|
||||
expectedScheduleLastBackupUpdate: NewTestSchedule("ns", "name").WithPhase(api.SchedulePhaseEnabled).
|
||||
WithCronSchedule("@every 5m").WithLastBackupTime("2017-01-01 12:00:00").Schedule,
|
||||
},
|
||||
}
|
||||
|
||||
// flag.Set("logtostderr", "true")
|
||||
// flag.Set("v", "4")
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var (
|
||||
client = fake.NewSimpleClientset()
|
||||
sharedInformers = informers.NewSharedInformerFactory(client, 0)
|
||||
)
|
||||
|
||||
c := NewScheduleController(
|
||||
client.ArkV1(),
|
||||
client.ArkV1(),
|
||||
sharedInformers.Ark().V1().Schedules(),
|
||||
time.Duration(0),
|
||||
)
|
||||
|
||||
var (
|
||||
testTime time.Time
|
||||
err error
|
||||
)
|
||||
if test.fakeClockTime != "" {
|
||||
testTime, err = time.Parse("2006-01-02 15:04:05", test.fakeClockTime)
|
||||
require.NoErrorf(t, err, "unable to parse test.fakeClockTime: %v", err)
|
||||
}
|
||||
c.clock = clock.NewFakeClock(testTime)
|
||||
|
||||
if test.schedule != nil {
|
||||
sharedInformers.Ark().V1().Schedules().Informer().GetStore().Add(test.schedule)
|
||||
|
||||
// this is necessary so the Update() call returns the appropriate object
|
||||
client.PrependReactor("update", "schedules", func(action core.Action) (bool, runtime.Object, error) {
|
||||
obj := action.(core.UpdateAction).GetObject()
|
||||
// need to deep copy so we can test the schedule state for each call to update
|
||||
copy, err := scheme.Scheme.DeepCopy(obj)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
ret := copy.(runtime.Object)
|
||||
return true, ret, nil
|
||||
})
|
||||
}
|
||||
|
||||
key := test.scheduleKey
|
||||
if key == "" && test.schedule != nil {
|
||||
key, err = cache.MetaNamespaceKeyFunc(test.schedule)
|
||||
require.NoErrorf(t, err, "error getting key from test.schedule: %v", err)
|
||||
}
|
||||
|
||||
err = c.processSchedule(key)
|
||||
|
||||
assert.Equal(t, test.expectedErr, err != nil, "got error %v", err)
|
||||
|
||||
expectedActions := make([]core.Action, 0)
|
||||
|
||||
if upd := test.expectedSchedulePhaseUpdate; upd != nil {
|
||||
action := core.NewUpdateAction(
|
||||
api.SchemeGroupVersion.WithResource("schedules"),
|
||||
upd.Namespace,
|
||||
upd)
|
||||
expectedActions = append(expectedActions, action)
|
||||
}
|
||||
|
||||
if created := test.expectedBackupCreate; created != nil {
|
||||
action := core.NewCreateAction(
|
||||
api.SchemeGroupVersion.WithResource("backups"),
|
||||
created.Namespace,
|
||||
created)
|
||||
expectedActions = append(expectedActions, action)
|
||||
}
|
||||
|
||||
if upd := test.expectedScheduleLastBackupUpdate; upd != nil {
|
||||
action := core.NewUpdateAction(
|
||||
api.SchemeGroupVersion.WithResource("schedules"),
|
||||
upd.Namespace,
|
||||
upd)
|
||||
expectedActions = append(expectedActions, action)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedActions, client.Actions())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNextRunTime(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedule *api.Schedule
|
||||
lastRanOffset string
|
||||
expectedDue bool
|
||||
expectedNextRunTimeOffset string
|
||||
}{
|
||||
{
|
||||
name: "first run",
|
||||
schedule: &api.Schedule{Spec: api.ScheduleSpec{Schedule: "@every 5m"}},
|
||||
expectedDue: true,
|
||||
expectedNextRunTimeOffset: "5m",
|
||||
},
|
||||
{
|
||||
name: "just ran",
|
||||
schedule: &api.Schedule{Spec: api.ScheduleSpec{Schedule: "@every 5m"}},
|
||||
lastRanOffset: "0s",
|
||||
expectedDue: false,
|
||||
expectedNextRunTimeOffset: "5m",
|
||||
},
|
||||
{
|
||||
name: "almost but not quite time to run",
|
||||
schedule: &api.Schedule{Spec: api.ScheduleSpec{Schedule: "@every 5m"}},
|
||||
lastRanOffset: "4m59s",
|
||||
expectedDue: false,
|
||||
expectedNextRunTimeOffset: "5m",
|
||||
},
|
||||
{
|
||||
name: "time to run again",
|
||||
schedule: &api.Schedule{Spec: api.ScheduleSpec{Schedule: "@every 5m"}},
|
||||
lastRanOffset: "5m",
|
||||
expectedDue: true,
|
||||
expectedNextRunTimeOffset: "5m",
|
||||
},
|
||||
{
|
||||
name: "several runs missed",
|
||||
schedule: &api.Schedule{Spec: api.ScheduleSpec{Schedule: "@every 5m"}},
|
||||
lastRanOffset: "5h",
|
||||
expectedDue: true,
|
||||
expectedNextRunTimeOffset: "5m",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cronSchedule, err := cron.Parse(test.schedule.Spec.Schedule)
|
||||
require.NoErrorf(t, err, "unable to parse test.schedule.Spec.Schedule: %v", err)
|
||||
|
||||
testClock := clock.NewFakeClock(time.Now())
|
||||
|
||||
if test.lastRanOffset != "" {
|
||||
offsetDuration, err := time.ParseDuration(test.lastRanOffset)
|
||||
require.NoErrorf(t, err, "unable to parse test.lastRanOffset: %v", err)
|
||||
|
||||
test.schedule.Status.LastBackup = metav1.Time{Time: testClock.Now().Add(-offsetDuration)}
|
||||
}
|
||||
|
||||
nextRunTimeOffset, err := time.ParseDuration(test.expectedNextRunTimeOffset)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
expectedNextRunTime := test.schedule.Status.LastBackup.Add(nextRunTimeOffset)
|
||||
|
||||
due, nextRunTime := getNextRunTime(test.schedule, cronSchedule, testClock.Now())
|
||||
|
||||
assert.Equal(t, test.expectedDue, due)
|
||||
// ignore diffs of under a second. the cron library does some rounding.
|
||||
assert.WithinDuration(t, expectedNextRunTime, nextRunTime, time.Second)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBackup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedule *api.Schedule
|
||||
testClockTime string
|
||||
expectedBackup *api.Backup
|
||||
}{
|
||||
{
|
||||
name: "ensure name is formatted correctly (AM time)",
|
||||
schedule: &api.Schedule{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Spec: api.ScheduleSpec{
|
||||
Template: api.BackupSpec{},
|
||||
},
|
||||
},
|
||||
testClockTime: "2017-07-25 09:15:00",
|
||||
expectedBackup: &api.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "foo",
|
||||
Name: "bar-20170725091500",
|
||||
},
|
||||
Spec: api.BackupSpec{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure name is formatted correctly (PM time)",
|
||||
schedule: &api.Schedule{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Spec: api.ScheduleSpec{
|
||||
Template: api.BackupSpec{},
|
||||
},
|
||||
},
|
||||
testClockTime: "2017-07-25 14:15:00",
|
||||
expectedBackup: &api.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "foo",
|
||||
Name: "bar-20170725141500",
|
||||
},
|
||||
Spec: api.BackupSpec{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ensure schedule backup template is copied",
|
||||
schedule: &api.Schedule{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Spec: api.ScheduleSpec{
|
||||
Template: api.BackupSpec{
|
||||
IncludedNamespaces: []string{"ns-1", "ns-2"},
|
||||
ExcludedNamespaces: []string{"ns-3"},
|
||||
IncludedResources: []string{"foo", "bar"},
|
||||
ExcludedResources: []string{"baz"},
|
||||
LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
|
||||
SnapshotVolumes: true,
|
||||
TTL: metav1.Duration{Duration: time.Duration(300)},
|
||||
},
|
||||
},
|
||||
},
|
||||
testClockTime: "2017-07-25 09:15:00",
|
||||
expectedBackup: &api.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "foo",
|
||||
Name: "bar-20170725091500",
|
||||
},
|
||||
Spec: api.BackupSpec{
|
||||
IncludedNamespaces: []string{"ns-1", "ns-2"},
|
||||
ExcludedNamespaces: []string{"ns-3"},
|
||||
IncludedResources: []string{"foo", "bar"},
|
||||
ExcludedResources: []string{"baz"},
|
||||
LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
|
||||
SnapshotVolumes: true,
|
||||
TTL: metav1.Duration{Duration: time.Duration(300)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testTime, err := time.Parse("2006-01-02 15:04:05", test.testClockTime)
|
||||
require.NoErrorf(t, err, "unable to parse test.testClockTime: %v", err)
|
||||
|
||||
backup := getBackup(test.schedule, clock.NewFakeClock(testTime).Now())
|
||||
|
||||
assert.Equal(t, test.expectedBackup.Namespace, backup.Namespace)
|
||||
assert.Equal(t, test.expectedBackup.Name, backup.Name)
|
||||
assert.Equal(t, test.expectedBackup.Spec, backup.Spec)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user