mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-04 15:16:58 +00:00
Initial commit
Signed-off-by: Andy Goldstein <andy.goldstein@gmail.com>
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
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 server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
"github.com/golang/glog"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/backup"
|
||||
"github.com/heptio/ark/pkg/client"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
arkaws "github.com/heptio/ark/pkg/cloudprovider/aws"
|
||||
"github.com/heptio/ark/pkg/cloudprovider/azure"
|
||||
"github.com/heptio/ark/pkg/cloudprovider/gcp"
|
||||
"github.com/heptio/ark/pkg/cmd"
|
||||
"github.com/heptio/ark/pkg/controller"
|
||||
arkdiscovery "github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/generated/clientset"
|
||||
arkv1client "github.com/heptio/ark/pkg/generated/clientset/typed/ark/v1"
|
||||
informers "github.com/heptio/ark/pkg/generated/informers/externalversions"
|
||||
"github.com/heptio/ark/pkg/restore"
|
||||
"github.com/heptio/ark/pkg/restore/restorers"
|
||||
"github.com/heptio/ark/pkg/util/kube"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var kubeconfig string
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Run the ark server",
|
||||
Long: "Run the ark server",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
s, err := newServer(kubeconfig)
|
||||
cmd.CheckError(err)
|
||||
|
||||
cmd.CheckError(s.run())
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&kubeconfig, "kubeconfig", "", "Path to the kubeconfig file to use to talk to the Kubernetes apiserver. If unset, try the environment variable KUBECONFIG, as well as in-cluster configuration")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
type server struct {
|
||||
kubeClient kubernetes.Interface
|
||||
apiExtensionsClient apiextensionsclient.Interface
|
||||
arkClient clientset.Interface
|
||||
backupService cloudprovider.BackupService
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
discoveryClient discovery.DiscoveryInterface
|
||||
clientPool dynamic.ClientPool
|
||||
sharedInformerFactory informers.SharedInformerFactory
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func newServer(kubeconfig string) (*server, error) {
|
||||
clientConfig, err := client.Config(kubeconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubeClient, err := kubernetes.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiExtensionsClient, err := apiextensionsclient.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arkClient, err := clientset.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
|
||||
s := &server{
|
||||
kubeClient: kubeClient,
|
||||
apiExtensionsClient: apiExtensionsClient,
|
||||
arkClient: arkClient,
|
||||
discoveryClient: apiExtensionsClient.Discovery(),
|
||||
clientPool: dynamic.NewDynamicClientPool(clientConfig),
|
||||
sharedInformerFactory: informers.NewSharedInformerFactory(arkClient, 0),
|
||||
ctx: ctx,
|
||||
cancelFunc: cancelFunc,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *server) run() error {
|
||||
if err := s.ensureArkNamespace(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := s.loadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applyConfigDefaults(config)
|
||||
|
||||
s.watchConfig(config)
|
||||
|
||||
if err := s.initBackupService(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.initSnapshotService(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.runControllers(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) ensureArkNamespace() error {
|
||||
glog.Infof("Ensuring %s namespace exists for backups", api.DefaultNamespace)
|
||||
defaultNamespace := v1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: api.DefaultNamespace,
|
||||
},
|
||||
}
|
||||
|
||||
if created, err := kube.EnsureNamespaceExists(&defaultNamespace, s.kubeClient.CoreV1().Namespaces()); created {
|
||||
glog.Infof("Namespace created")
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
glog.Infof("Namespace already exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) loadConfig() (*api.Config, error) {
|
||||
glog.Infof("Retrieving Ark configuration")
|
||||
var (
|
||||
config *api.Config
|
||||
err error
|
||||
)
|
||||
for {
|
||||
config, err = s.arkClient.ArkV1().Configs(api.DefaultNamespace).Get("default", metav1.GetOptions{})
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if !apierrors.IsNotFound(err) {
|
||||
glog.Errorf("error retrieving configuration: %v", err)
|
||||
}
|
||||
glog.Infof("Will attempt to retrieve configuration again in 5 seconds")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
glog.Infof("Successfully retrieved Ark configuration")
|
||||
return config, nil
|
||||
}
|
||||
|
||||
const (
|
||||
defaultGCSyncPeriod = 60 * time.Minute
|
||||
defaultBackupSyncPeriod = 60 * time.Minute
|
||||
defaultScheduleSyncPeriod = time.Minute
|
||||
)
|
||||
|
||||
var defaultResourcePriorities = []string{
|
||||
"namespaces",
|
||||
"persistentvolumes",
|
||||
"persistentvolumeclaims",
|
||||
"secrets",
|
||||
"configmaps",
|
||||
}
|
||||
|
||||
func applyConfigDefaults(c *api.Config) {
|
||||
if c.GCSyncPeriod.Duration == 0 {
|
||||
c.GCSyncPeriod.Duration = defaultGCSyncPeriod
|
||||
}
|
||||
|
||||
if c.BackupSyncPeriod.Duration == 0 {
|
||||
c.BackupSyncPeriod.Duration = defaultBackupSyncPeriod
|
||||
}
|
||||
|
||||
if c.ScheduleSyncPeriod.Duration == 0 {
|
||||
c.ScheduleSyncPeriod.Duration = defaultScheduleSyncPeriod
|
||||
}
|
||||
|
||||
if len(c.ResourcePriorities) == 0 {
|
||||
c.ResourcePriorities = defaultResourcePriorities
|
||||
glog.Infof("Using default resource priorities: %v", c.ResourcePriorities)
|
||||
} else {
|
||||
glog.Infof("Using resource priorities from config: %v", c.ResourcePriorities)
|
||||
}
|
||||
}
|
||||
|
||||
// watchConfig adds an update event handler to the Config shared informer, invoking s.cancelFunc
|
||||
// when it sees a change.
|
||||
func (s *server) watchConfig(config *api.Config) {
|
||||
s.sharedInformerFactory.Ark().V1().Configs().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||
updated := newObj.(*api.Config)
|
||||
|
||||
if updated.Name != config.Name {
|
||||
glog.V(5).Infof("config watch channel received other config %q", updated.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(config, updated) {
|
||||
glog.Infof("Detected a config change. Gracefully shutting down")
|
||||
s.cancelFunc()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) initBackupService(config *api.Config) error {
|
||||
glog.Infof("Configuring cloud provider for backup service")
|
||||
cloud, err := initCloud(config.BackupStorageProvider.CloudProviderConfig, "backupStorageProvider")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.backupService = cloudprovider.NewBackupService(cloud.ObjectStorage())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) initSnapshotService(config *api.Config) error {
|
||||
glog.Infof("Configuring cloud provider for snapshot service")
|
||||
cloud, err := initCloud(config.PersistentVolumeProvider, "persistentVolumeProvider")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.snapshotService = cloudprovider.NewSnapshotService(cloud.BlockStorage())
|
||||
return nil
|
||||
}
|
||||
|
||||
func initCloud(config api.CloudProviderConfig, field string) (cloudprovider.StorageAdapter, error) {
|
||||
var (
|
||||
cloud cloudprovider.StorageAdapter
|
||||
err error
|
||||
)
|
||||
|
||||
if config.AWS != nil {
|
||||
cloud, err = getAWSCloudProvider(config)
|
||||
}
|
||||
|
||||
if config.GCP != nil {
|
||||
if cloud != nil {
|
||||
return nil, fmt.Errorf("you may only specify one of aws, gcp, or azure for %s", field)
|
||||
}
|
||||
cloud, err = getGCPCloudProvider(config)
|
||||
}
|
||||
|
||||
if config.Azure != nil {
|
||||
if cloud != nil {
|
||||
return nil, fmt.Errorf("you may only specify one of aws, gcp, or azure for %s", field)
|
||||
}
|
||||
cloud, err = getAzureCloudProvider(config)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cloud == nil {
|
||||
return nil, fmt.Errorf("you must specify one of aws, gcp, or azure for %s", field)
|
||||
}
|
||||
|
||||
return cloud, err
|
||||
}
|
||||
|
||||
func getAWSCloudProvider(cloudConfig api.CloudProviderConfig) (cloudprovider.StorageAdapter, error) {
|
||||
if cloudConfig.AWS == nil {
|
||||
return nil, errors.New("missing aws configuration in config file")
|
||||
}
|
||||
if cloudConfig.AWS.Region == "" {
|
||||
return nil, errors.New("missing region in aws configuration in config file")
|
||||
}
|
||||
if cloudConfig.AWS.AvailabilityZone == "" {
|
||||
return nil, errors.New("missing availabilityZone in aws configuration in config file")
|
||||
}
|
||||
|
||||
awsConfig := aws.NewConfig().
|
||||
WithRegion(cloudConfig.AWS.Region).
|
||||
WithS3ForcePathStyle(cloudConfig.AWS.S3ForcePathStyle)
|
||||
|
||||
if cloudConfig.AWS.S3Url != "" {
|
||||
awsConfig = awsConfig.WithEndpointResolver(
|
||||
endpoints.ResolverFunc(func(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
|
||||
if service == endpoints.S3ServiceID {
|
||||
return endpoints.ResolvedEndpoint{
|
||||
URL: cloudConfig.AWS.S3Url,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return endpoints.DefaultResolver().EndpointFor(service, region, optFns...)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return arkaws.NewStorageAdapter(awsConfig, cloudConfig.AWS.AvailabilityZone)
|
||||
}
|
||||
|
||||
func getGCPCloudProvider(cloudConfig api.CloudProviderConfig) (cloudprovider.StorageAdapter, error) {
|
||||
if cloudConfig.GCP == nil {
|
||||
return nil, errors.New("missing gcp configuration in config file")
|
||||
}
|
||||
if cloudConfig.GCP.Project == "" {
|
||||
return nil, errors.New("missing project in gcp configuration in config file")
|
||||
}
|
||||
if cloudConfig.GCP.Zone == "" {
|
||||
return nil, errors.New("missing zone in gcp configuration in config file")
|
||||
}
|
||||
return gcp.NewStorageAdapter(cloudConfig.GCP.Project, cloudConfig.GCP.Zone)
|
||||
}
|
||||
|
||||
func getAzureCloudProvider(cloudConfig api.CloudProviderConfig) (cloudprovider.StorageAdapter, error) {
|
||||
if cloudConfig.Azure == nil {
|
||||
return nil, errors.New("missing azure configuration in config file")
|
||||
}
|
||||
if cloudConfig.Azure.Location == "" {
|
||||
return nil, errors.New("missing location in azure configuration in config file")
|
||||
}
|
||||
return azure.NewStorageAdapter(cloudConfig.Azure.Location, cloudConfig.Azure.APITimeout.Duration)
|
||||
}
|
||||
|
||||
func durationMin(a, b time.Duration) time.Duration {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (s *server) runControllers(config *api.Config) error {
|
||||
glog.Infof("Starting controllers")
|
||||
|
||||
ctx := s.ctx
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cloudBackupCacheResyncPeriod := durationMin(config.GCSyncPeriod.Duration, config.BackupSyncPeriod.Duration)
|
||||
glog.Infof("Caching cloud backups every %s", cloudBackupCacheResyncPeriod)
|
||||
s.backupService = cloudprovider.NewBackupServiceWithCachedBackupGetter(
|
||||
ctx,
|
||||
s.backupService,
|
||||
cloudBackupCacheResyncPeriod,
|
||||
)
|
||||
|
||||
backupSyncController := controller.NewBackupSyncController(
|
||||
s.arkClient.ArkV1(),
|
||||
s.backupService,
|
||||
config.BackupStorageProvider.Bucket,
|
||||
config.BackupSyncPeriod.Duration,
|
||||
)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
backupSyncController.Run(ctx, 1)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
discoveryHelper, err := arkdiscovery.NewHelper(s.discoveryClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go wait.Until(
|
||||
func() {
|
||||
if err := discoveryHelper.Refresh(); err != nil {
|
||||
glog.Errorf("error refreshing discovery: %v", err)
|
||||
}
|
||||
},
|
||||
5*time.Minute,
|
||||
ctx.Done(),
|
||||
)
|
||||
|
||||
if config.RestoreOnlyMode {
|
||||
glog.Infof("Restore only mode - not starting the backup, schedule or GC controllers")
|
||||
} else {
|
||||
backupper, err := newBackupper(discoveryHelper, s.clientPool, s.backupService, s.snapshotService)
|
||||
cmd.CheckError(err)
|
||||
backupController := controller.NewBackupController(
|
||||
s.sharedInformerFactory.Ark().V1().Backups(),
|
||||
s.arkClient.ArkV1(),
|
||||
backupper,
|
||||
s.backupService,
|
||||
config.BackupStorageProvider.Bucket,
|
||||
)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
backupController.Run(ctx, 1)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
scheduleController := controller.NewScheduleController(
|
||||
s.arkClient.ArkV1(),
|
||||
s.arkClient.ArkV1(),
|
||||
s.sharedInformerFactory.Ark().V1().Schedules(),
|
||||
config.ScheduleSyncPeriod.Duration,
|
||||
)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
scheduleController.Run(ctx, 1)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
gcController := controller.NewGCController(
|
||||
s.backupService,
|
||||
s.snapshotService,
|
||||
config.BackupStorageProvider.Bucket,
|
||||
config.GCSyncPeriod.Duration,
|
||||
s.sharedInformerFactory.Ark().V1().Backups(),
|
||||
s.arkClient.ArkV1(),
|
||||
)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
gcController.Run(ctx, 1)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
restorer, err := newRestorer(
|
||||
discoveryHelper,
|
||||
s.clientPool,
|
||||
s.backupService,
|
||||
s.snapshotService,
|
||||
config.ResourcePriorities,
|
||||
s.arkClient.ArkV1(),
|
||||
s.kubeClient,
|
||||
)
|
||||
cmd.CheckError(err)
|
||||
|
||||
restoreController := controller.NewRestoreController(
|
||||
s.sharedInformerFactory.Ark().V1().Restores(),
|
||||
s.arkClient.ArkV1(),
|
||||
s.arkClient.ArkV1(),
|
||||
restorer,
|
||||
s.backupService,
|
||||
config.BackupStorageProvider.Bucket,
|
||||
s.sharedInformerFactory.Ark().V1().Backups(),
|
||||
)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
restoreController.Run(ctx, 1)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// SHARED INFORMERS HAVE TO BE STARTED AFTER ALL CONTROLLERS
|
||||
go s.sharedInformerFactory.Start(ctx.Done())
|
||||
|
||||
glog.Infof("Server started successfully")
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
glog.Info("Waiting for all controllers to shut down gracefully")
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newBackupper(
|
||||
discoveryHelper arkdiscovery.Helper,
|
||||
clientPool dynamic.ClientPool,
|
||||
backupService cloudprovider.BackupService,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) (backup.Backupper, error) {
|
||||
actions := map[string]backup.Action{}
|
||||
|
||||
if snapshotService != nil {
|
||||
actions["persistentvolumes"] = backup.NewVolumeSnapshotAction(snapshotService)
|
||||
}
|
||||
|
||||
return backup.NewKubernetesBackupper(
|
||||
discoveryHelper,
|
||||
client.NewDynamicFactory(clientPool),
|
||||
actions,
|
||||
)
|
||||
}
|
||||
|
||||
func newRestorer(
|
||||
discoveryHelper arkdiscovery.Helper,
|
||||
clientPool dynamic.ClientPool,
|
||||
backupService cloudprovider.BackupService,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resourcePriorities []string,
|
||||
backupClient arkv1client.BackupsGetter,
|
||||
kubeClient kubernetes.Interface,
|
||||
) (restore.Restorer, error) {
|
||||
restorers := map[string]restorers.ResourceRestorer{
|
||||
"persistentvolumes": restorers.NewPersistentVolumeRestorer(snapshotService),
|
||||
"persistentvolumeclaims": restorers.NewPersistentVolumeClaimRestorer(),
|
||||
"services": restorers.NewServiceRestorer(),
|
||||
"namespaces": restorers.NewNamespaceRestorer(),
|
||||
"pods": restorers.NewPodRestorer(),
|
||||
"jobs": restorers.NewJobRestorer(),
|
||||
}
|
||||
|
||||
return restore.NewKubernetesRestorer(
|
||||
discoveryHelper,
|
||||
client.NewDynamicFactory(clientPool),
|
||||
restorers,
|
||||
backupService,
|
||||
resourcePriorities,
|
||||
backupClient,
|
||||
kubeClient.CoreV1().Namespaces(),
|
||||
)
|
||||
}
|
||||
@@ -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 server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
)
|
||||
|
||||
func TestApplyConfigDefaults(t *testing.T) {
|
||||
c := &v1.Config{}
|
||||
|
||||
// test defaulting
|
||||
applyConfigDefaults(c)
|
||||
assert.Equal(t, defaultGCSyncPeriod, c.GCSyncPeriod.Duration)
|
||||
assert.Equal(t, defaultBackupSyncPeriod, c.BackupSyncPeriod.Duration)
|
||||
assert.Equal(t, defaultScheduleSyncPeriod, c.ScheduleSyncPeriod.Duration)
|
||||
assert.Equal(t, defaultResourcePriorities, c.ResourcePriorities)
|
||||
|
||||
// make sure defaulting doesn't overwrite real values
|
||||
c.GCSyncPeriod.Duration = 5 * time.Minute
|
||||
c.BackupSyncPeriod.Duration = 4 * time.Minute
|
||||
c.ScheduleSyncPeriod.Duration = 3 * time.Minute
|
||||
c.ResourcePriorities = []string{"a", "b"}
|
||||
|
||||
applyConfigDefaults(c)
|
||||
|
||||
assert.Equal(t, 5*time.Minute, c.GCSyncPeriod.Duration)
|
||||
assert.Equal(t, 4*time.Minute, c.BackupSyncPeriod.Duration)
|
||||
assert.Equal(t, 3*time.Minute, c.ScheduleSyncPeriod.Duration)
|
||||
assert.Equal(t, []string{"a", "b"}, c.ResourcePriorities)
|
||||
}
|
||||
Reference in New Issue
Block a user