From a711b1067b14f8059668cb69dcf93fb51391ed5d Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Wed, 27 Nov 2024 14:07:28 +0800 Subject: [PATCH 01/16] fail fs-backup for windows nodes Signed-off-by: Lyndon-Li --- changelogs/unreleased/8518-Lyndon-Li | 1 + pkg/cmd/server/server.go | 19 +++- pkg/exposer/csi_snapshot_test.go | 2 +- pkg/exposer/generic_restore_test.go | 2 +- pkg/nodeagent/node_agent.go | 25 +++-- pkg/nodeagent/node_agent_test.go | 14 +-- pkg/podvolume/backupper.go | 6 ++ pkg/podvolume/backupper_test.go | 46 +++++++++- pkg/podvolume/restorer.go | 8 +- pkg/podvolume/restorer_test.go | 35 +++++-- pkg/util/kube/node.go | 67 ++++++++++++++ pkg/util/kube/node_test.go | 132 +++++++++++++++++++++++++++ 12 files changed, 326 insertions(+), 31 deletions(-) create mode 100644 changelogs/unreleased/8518-Lyndon-Li create mode 100644 pkg/util/kube/node.go create mode 100644 pkg/util/kube/node_test.go diff --git a/changelogs/unreleased/8518-Lyndon-Li b/changelogs/unreleased/8518-Lyndon-Li new file mode 100644 index 000000000..94a8a0158 --- /dev/null +++ b/changelogs/unreleased/8518-Lyndon-Li @@ -0,0 +1 @@ +Make fs-backup work on linux nodes with the new Velero deployment and disable fs-backup if the source/target pod is running in non-linux node (#8424) \ No newline at end of file diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index b5f9576f8..9e35c9dd4 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -82,6 +82,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/restore" "github.com/vmware-tanzu/velero/pkg/uploader" "github.com/vmware-tanzu/velero/pkg/util/filesystem" + "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/logging" ) @@ -454,10 +455,20 @@ func (s *server) veleroResourcesExist() error { func (s *server) checkNodeAgent() { // warn if node agent does not exist - if err := nodeagent.IsRunning(s.ctx, s.kubeClient, s.namespace); err == nodeagent.ErrDaemonSetNotFound { - s.logger.Warn("Velero node agent not found; pod volume backups/restores will not work until it's created") - } else if err != nil { - s.logger.WithError(errors.WithStack(err)).Warn("Error checking for existence of velero node agent") + if kube.WithLinuxNode(s.ctx, s.crClient, s.logger) { + if err := nodeagent.IsRunningOnLinux(s.ctx, s.kubeClient, s.namespace); err == nodeagent.ErrDaemonSetNotFound { + s.logger.Warn("Velero node agent not found for linux nodes; pod volume backups/restores and data mover backups/restores will not work until it's created") + } else if err != nil { + s.logger.WithError(errors.WithStack(err)).Warn("Error checking for existence of velero node agent for linux nodes") + } + } + + if kube.WithWindowsNode(s.ctx, s.crClient, s.logger) { + if err := nodeagent.IsRunningOnWindows(s.ctx, s.kubeClient, s.namespace); err == nodeagent.ErrDaemonSetNotFound { + s.logger.Warn("Velero node agent not found for Windows nodes; pod volume backups/restores and data mover backups/restores will not work until it's created") + } else if err != nil { + s.logger.WithError(errors.WithStack(err)).Warn("Error checking for existence of velero node agent for Windows nodes") + } } } diff --git a/pkg/exposer/csi_snapshot_test.go b/pkg/exposer/csi_snapshot_test.go index 77d792635..9f2865f07 100644 --- a/pkg/exposer/csi_snapshot_test.go +++ b/pkg/exposer/csi_snapshot_test.go @@ -1146,7 +1146,7 @@ func Test_csiSnapshotExposer_DiagnoseExpose(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1.DefaultNamespace, Name: "node-agent-pod-1", - Labels: map[string]string{"name": "node-agent"}, + Labels: map[string]string{"role": "node-agent"}, }, Spec: corev1.PodSpec{ NodeName: "fake-node", diff --git a/pkg/exposer/generic_restore_test.go b/pkg/exposer/generic_restore_test.go index 2eec0ce18..d2d56ece7 100644 --- a/pkg/exposer/generic_restore_test.go +++ b/pkg/exposer/generic_restore_test.go @@ -627,7 +627,7 @@ func Test_ReastoreDiagnoseExpose(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Namespace: velerov1.DefaultNamespace, Name: "node-agent-pod-1", - Labels: map[string]string{"name": "node-agent"}, + Labels: map[string]string{"role": "node-agent"}, }, Spec: corev1.PodSpec{ NodeName: "fake-node", diff --git a/pkg/nodeagent/node_agent.go b/pkg/nodeagent/node_agent.go index a57379f37..8ed6aacdd 100644 --- a/pkg/nodeagent/node_agent.go +++ b/pkg/nodeagent/node_agent.go @@ -33,8 +33,14 @@ import ( ) const ( - // daemonSet is the name of the Velero node agent daemonset. + // daemonSet is the name of the Velero node agent daemonset on linux nodes. daemonSet = "node-agent" + + // daemonsetWindows is the name of the Velero node agent daemonset on Windows nodes. + daemonsetWindows = "node-agent-windows" + + // nodeAgentRole marks pods with node-agent role on all nodes. + nodeAgentRole = "node-agent" ) var ( @@ -89,9 +95,16 @@ type Configs struct { PodResources *kube.PodResources `json:"podResources,omitempty"` } -// IsRunning checks if the node agent daemonset is running properly. If not, return the error found -func IsRunning(ctx context.Context, kubeClient kubernetes.Interface, namespace string) error { - if _, err := kubeClient.AppsV1().DaemonSets(namespace).Get(ctx, daemonSet, metav1.GetOptions{}); apierrors.IsNotFound(err) { +func IsRunningOnLinux(ctx context.Context, kubeClient kubernetes.Interface, namespace string) error { + return isRunning(ctx, kubeClient, namespace, daemonSet) +} + +func IsRunningOnWindows(ctx context.Context, kubeClient kubernetes.Interface, namespace string) error { + return isRunning(ctx, kubeClient, namespace, daemonsetWindows) +} + +func isRunning(ctx context.Context, kubeClient kubernetes.Interface, namespace string, daemonset string) error { + if _, err := kubeClient.AppsV1().DaemonSets(namespace).Get(ctx, daemonset, metav1.GetOptions{}); apierrors.IsNotFound(err) { return ErrDaemonSetNotFound } else if err != nil { return err @@ -116,7 +129,7 @@ func isRunningInNode(ctx context.Context, namespace string, nodeName string, crC } pods := new(v1.PodList) - parsedSelector, err := labels.Parse(fmt.Sprintf("name=%s", daemonSet)) + parsedSelector, err := labels.Parse(fmt.Sprintf("role=%s", nodeAgentRole)) if err != nil { return errors.Wrap(err, "fail to parse selector") } @@ -128,7 +141,7 @@ func isRunningInNode(ctx context.Context, namespace string, nodeName string, crC } if err != nil { - return errors.Wrap(err, "failed to list daemonset pods") + return errors.Wrap(err, "failed to list node-agent pods") } for i := range pods.Items { diff --git a/pkg/nodeagent/node_agent_test.go b/pkg/nodeagent/node_agent_test.go index 1c24427b1..11cb83359 100644 --- a/pkg/nodeagent/node_agent_test.go +++ b/pkg/nodeagent/node_agent_test.go @@ -40,7 +40,7 @@ type reactor struct { } func TestIsRunning(t *testing.T) { - daemonSet := &appsv1.DaemonSet{ + ds := &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-ns", Name: "node-agent", @@ -80,7 +80,7 @@ func TestIsRunning(t *testing.T) { name: "succeed", namespace: "fake-ns", kubeClientObj: []runtime.Object{ - daemonSet, + ds, }, }, } @@ -93,7 +93,7 @@ func TestIsRunning(t *testing.T) { fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc) } - err := IsRunning(context.TODO(), fakeKubeClient, test.namespace) + err := isRunning(context.TODO(), fakeKubeClient, test.namespace, daemonSet) if test.expectErr == "" { assert.NoError(t, err) } else { @@ -108,11 +108,11 @@ func TestIsRunningInNode(t *testing.T) { corev1.AddToScheme(scheme) nonNodeAgentPod := builder.ForPod("fake-ns", "fake-pod").Result() - nodeAgentPodNotRunning := builder.ForPod("fake-ns", "fake-pod").Labels(map[string]string{"name": "node-agent"}).Result() - nodeAgentPodRunning1 := builder.ForPod("fake-ns", "fake-pod-1").Labels(map[string]string{"name": "node-agent"}).Phase(corev1.PodRunning).Result() - nodeAgentPodRunning2 := builder.ForPod("fake-ns", "fake-pod-2").Labels(map[string]string{"name": "node-agent"}).Phase(corev1.PodRunning).Result() + nodeAgentPodNotRunning := builder.ForPod("fake-ns", "fake-pod").Labels(map[string]string{"role": "node-agent"}).Result() + nodeAgentPodRunning1 := builder.ForPod("fake-ns", "fake-pod-1").Labels(map[string]string{"role": "node-agent"}).Phase(corev1.PodRunning).Result() + nodeAgentPodRunning2 := builder.ForPod("fake-ns", "fake-pod-2").Labels(map[string]string{"role": "node-agent"}).Phase(corev1.PodRunning).Result() nodeAgentPodRunning3 := builder.ForPod("fake-ns", "fake-pod-3"). - Labels(map[string]string{"name": "node-agent"}). + Labels(map[string]string{"role": "node-agent"}). Phase(corev1.PodRunning). NodeName("fake-node"). Result() diff --git a/pkg/podvolume/backupper.go b/pkg/podvolume/backupper.go index 0a0c63eff..29452344e 100644 --- a/pkg/podvolume/backupper.go +++ b/pkg/podvolume/backupper.go @@ -206,6 +206,12 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api. return nil, pvcSummary, nil } + if err := kube.IsLinuxNode(b.ctx, pod.Spec.NodeName, b.crClient); err != nil { + err := errors.Wrapf(err, "Pod %s/%s is not running in linux node(%s), skip", pod.Namespace, pod.Name, pod.Spec.NodeName) + skipAllPodVolumes(pod, volumesToBackup, err, pvcSummary, log) + return nil, pvcSummary, []error{err} + } + err := nodeagent.IsRunningInNode(b.ctx, backup.Namespace, pod.Spec.NodeName, b.crClient) if err != nil { skipAllPodVolumes(pod, volumesToBackup, err, pvcSummary, log) diff --git a/pkg/podvolume/backupper_test.go b/pkg/podvolume/backupper_test.go index fe50f9e30..941436830 100644 --- a/pkg/podvolume/backupper_test.go +++ b/pkg/podvolume/backupper_test.go @@ -260,7 +260,7 @@ func createPodObj(running bool, withVolume bool, withVolumeMounted bool, volumeN func createNodeAgentPodObj(running bool) *corev1api.Pod { podObj := builder.ForPod(velerov1api.DefaultNamespace, "fake-node-agent").Result() - podObj.Labels = map[string]string{"name": "node-agent"} + podObj.Labels = map[string]string{"role": "node-agent"} if running { podObj.Status.Phase = corev1api.PodRunning @@ -303,6 +303,14 @@ func createPVBObj(fail bool, withSnapshot bool, index int, uploaderType string) return pvbObj } +func createNodeObj() *corev1api.Node { + return builder.ForNode("fake-node-name").Labels(map[string]string{"kubernetes.io/os": "linux"}).Result() +} + +func createWindowsNodeObj() *corev1api.Node { + return builder.ForNode("fake-node-name").Labels(map[string]string{"kubernetes.io/os": "windows"}).Result() +} + func TestBackupPodVolumes(t *testing.T) { scheme := runtime.NewScheme() velerov1api.AddToScheme(scheme) @@ -358,13 +366,32 @@ func TestBackupPodVolumes(t *testing.T) { uploaderType: "kopia", bsl: "fake-bsl", }, + { + name: "pod is not running on Linux node", + volumes: []string{ + "fake-volume-1", + "fake-volume-2", + }, + kubeClientObj: []runtime.Object{ + createNodeAgentPodObj(true), + createWindowsNodeObj(), + }, + sourcePod: createPodObj(false, false, false, 2), + uploaderType: "kopia", + errs: []string{ + "Pod fake-ns/fake-pod is not running in linux node(fake-node-name), skip", + }, + }, { name: "node-agent pod is not running in node", volumes: []string{ "fake-volume-1", "fake-volume-2", }, - sourcePod: createPodObj(true, false, false, 2), + sourcePod: createPodObj(true, false, false, 2), + kubeClientObj: []runtime.Object{ + createNodeObj(), + }, uploaderType: "kopia", errs: []string{ "daemonset pod not found in running state in node fake-node-name", @@ -379,6 +406,7 @@ func TestBackupPodVolumes(t *testing.T) { sourcePod: createPodObj(true, false, false, 2), kubeClientObj: []runtime.Object{ createNodeAgentPodObj(true), + createNodeObj(), }, uploaderType: "kopia", mockGetRepositoryType: true, @@ -395,6 +423,7 @@ func TestBackupPodVolumes(t *testing.T) { sourcePod: createPodObj(true, false, false, 2), kubeClientObj: []runtime.Object{ createNodeAgentPodObj(true), + createNodeObj(), }, uploaderType: "kopia", errs: []string{ @@ -410,6 +439,7 @@ func TestBackupPodVolumes(t *testing.T) { sourcePod: createPodObj(true, false, false, 2), kubeClientObj: []runtime.Object{ createNodeAgentPodObj(true), + createNodeObj(), }, ctlClientObj: []runtime.Object{ createBackupRepoObj(), @@ -427,6 +457,7 @@ func TestBackupPodVolumes(t *testing.T) { sourcePod: createPodObj(true, true, false, 2), kubeClientObj: []runtime.Object{ createNodeAgentPodObj(true), + createNodeObj(), }, ctlClientObj: []runtime.Object{ createBackupRepoObj(), @@ -448,6 +479,7 @@ func TestBackupPodVolumes(t *testing.T) { sourcePod: createPodObj(true, true, false, 2), kubeClientObj: []runtime.Object{ createNodeAgentPodObj(true), + createNodeObj(), createPVCObj(1), createPVCObj(2), }, @@ -471,6 +503,7 @@ func TestBackupPodVolumes(t *testing.T) { sourcePod: createPodObj(true, true, false, 2), kubeClientObj: []runtime.Object{ createNodeAgentPodObj(true), + createNodeObj(), createPVCObj(1), createPVCObj(2), createPVObj(1, true), @@ -482,6 +515,7 @@ func TestBackupPodVolumes(t *testing.T) { runtimeScheme: scheme, uploaderType: "kopia", bsl: "fake-bsl", + errs: []string{}, }, { name: "volume not mounted by pod should be skipped", @@ -492,6 +526,7 @@ func TestBackupPodVolumes(t *testing.T) { sourcePod: createPodObj(true, true, false, 2), kubeClientObj: []runtime.Object{ createNodeAgentPodObj(true), + createNodeObj(), createPVCObj(1), createPVCObj(2), createPVObj(1, false), @@ -503,6 +538,7 @@ func TestBackupPodVolumes(t *testing.T) { runtimeScheme: scheme, uploaderType: "kopia", bsl: "fake-bsl", + errs: []string{}, }, { name: "return completed pvbs", @@ -512,6 +548,7 @@ func TestBackupPodVolumes(t *testing.T) { sourcePod: createPodObj(true, true, true, 1), kubeClientObj: []runtime.Object{ createNodeAgentPodObj(true), + createNodeObj(), createPVCObj(1), createPVObj(1, false), }, @@ -522,6 +559,7 @@ func TestBackupPodVolumes(t *testing.T) { uploaderType: "kopia", bsl: "fake-bsl", pvbs: 1, + errs: []string{}, }, } // TODO add more verification around PVCBackupSummary returned by "BackupPodVolumes" @@ -568,8 +606,8 @@ func TestBackupPodVolumes(t *testing.T) { pvbs, _, errs := bp.BackupPodVolumes(backupObj, test.sourcePod, test.volumes, nil, velerotest.NewLogger()) - if errs == nil { - assert.Nil(t, test.errs) + if test.errs == nil { + assert.NoError(t, err) } else { for i := 0; i < len(errs); i++ { assert.EqualError(t, errs[i], test.errs[i]) diff --git a/pkg/podvolume/restorer.go b/pkg/podvolume/restorer.go index 4b3e4354d..18e771763 100644 --- a/pkg/podvolume/restorer.go +++ b/pkg/podvolume/restorer.go @@ -122,7 +122,7 @@ func (r *restorer) RestorePodVolumes(data RestoreData, tracker *volume.RestoreVo return nil } - if err := nodeagent.IsRunning(r.ctx, r.kubeClient, data.Restore.Namespace); err != nil { + if err := nodeagent.IsRunningOnLinux(r.ctx, r.kubeClient, data.Restore.Namespace); err != nil { return []error{errors.Wrapf(err, "error to check node agent status")} } @@ -213,6 +213,12 @@ func (r *restorer) RestorePodVolumes(data RestoreData, tracker *volume.RestoreVo } else if err != nil { r.log.WithError(err).Error("Failed to check node-agent pod status, disengage") } else { + if err := kube.IsLinuxNode(checkCtx, nodeName, r.crClient); err != nil { + r.log.WithField("node", nodeName).WithError(err).Error("Restored pod is not running in linux node") + r.nodeAgentCheck <- errors.Wrapf(err, "restored pod %s/%s is not running in linux node(%s)", data.Pod.Namespace, data.Pod.Name, nodeName) + return + } + err = nodeagent.IsRunningInNode(checkCtx, data.Restore.Namespace, nodeName, r.crClient) if err != nil { r.log.WithField("node", nodeName).WithError(err).Error("node-agent pod is not running in node, abort the restore") diff --git a/pkg/podvolume/restorer_test.go b/pkg/podvolume/restorer_test.go index 5d52cf21d..1af4da429 100644 --- a/pkg/podvolume/restorer_test.go +++ b/pkg/podvolume/restorer_test.go @@ -33,7 +33,6 @@ import ( "k8s.io/client-go/kubernetes" kubefake "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/tools/cache" - ctrlfake "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/vmware-tanzu/velero/internal/volume" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -314,6 +313,30 @@ func TestRestorePodVolumes(t *testing.T) { }, }, }, + { + name: "pod is not running on linux nodes", + pvbs: []*velerov1api.PodVolumeBackup{ + createPVBObj(true, true, 1, "kopia"), + }, + kubeClientObj: []runtime.Object{ + createNodeAgentDaemonset(), + createWindowsNodeObj(), + createPVCObj(1), + createPodObj(true, true, true, 1), + }, + ctlClientObj: []runtime.Object{ + createBackupRepoObj(), + }, + restoredPod: createPodObj(true, true, true, 1), + sourceNamespace: "fake-ns", + bsl: "fake-bsl", + runtimeScheme: scheme, + errs: []expectError{ + { + err: "restored pod fake-ns/fake-pod is not running in linux node(fake-node-name): os type windows for node fake-node-name is not linux", + }, + }, + }, { name: "node-agent pod is not running", pvbs: []*velerov1api.PodVolumeBackup{ @@ -321,6 +344,7 @@ func TestRestorePodVolumes(t *testing.T) { }, kubeClientObj: []runtime.Object{ createNodeAgentDaemonset(), + createNodeObj(), createPVCObj(1), createPodObj(true, true, true, 1), }, @@ -344,6 +368,7 @@ func TestRestorePodVolumes(t *testing.T) { }, kubeClientObj: []runtime.Object{ createNodeAgentDaemonset(), + createNodeObj(), createPVCObj(1), createPodObj(true, true, true, 1), createNodeAgentPodObj(true), @@ -368,11 +393,6 @@ func TestRestorePodVolumes(t *testing.T) { ctx = test.ctx } - fakeClientBuilder := ctrlfake.NewClientBuilder() - if test.runtimeScheme != nil { - fakeClientBuilder = fakeClientBuilder.WithScheme(test.runtimeScheme) - } - objClient := append(test.ctlClientObj, test.kubeClientObj...) objClient = append(objClient, test.veleroClientObj...) @@ -438,7 +458,8 @@ func TestRestorePodVolumes(t *testing.T) { for i := 0; i < len(errs); i++ { j := 0 for ; j < len(test.errs); j++ { - if errs[i].Error() == test.errs[j].err { + err := errs[i].Error() + if err == test.errs[j].err { break } } diff --git a/pkg/util/kube/node.go b/pkg/util/kube/node.go new file mode 100644 index 000000000..ec0005b53 --- /dev/null +++ b/pkg/util/kube/node.go @@ -0,0 +1,67 @@ +/* +Copyright 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 kube + +import ( + "context" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + corev1api "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func IsLinuxNode(ctx context.Context, nodeName string, client client.Client) error { + node := &corev1api.Node{} + if err := client.Get(ctx, types.NamespacedName{Name: nodeName}, node); err != nil { + return errors.Wrapf(err, "error getting node %s", nodeName) + } + + if os, found := node.Labels["kubernetes.io/os"]; !found { + return errors.Errorf("no os type label for node %s", nodeName) + } else if os != "linux" { + return errors.Errorf("os type %s for node %s is not linux", os, nodeName) + } else { + return nil + } +} + +func WithLinuxNode(ctx context.Context, client client.Client, log logrus.FieldLogger) bool { + return withOSNode(ctx, client, "linux", log) +} + +func WithWindowsNode(ctx context.Context, client client.Client, log logrus.FieldLogger) bool { + return withOSNode(ctx, client, "windows", log) +} + +func withOSNode(ctx context.Context, client client.Client, osType string, log logrus.FieldLogger) bool { + nodeList := new(corev1api.NodeList) + if err := client.List(ctx, nodeList); err != nil { + log.Warn("Failed to list nodes, cannot decide existence of windows nodes") + return false + } + + for _, node := range nodeList.Items { + if os, found := node.Labels["kubernetes.io/os"]; !found { + log.Warnf("Node %s doesn't have os type label, cannot decide existence of windows nodes") + } else if os == osType { + return true + } + } + + return false +} diff --git a/pkg/util/kube/node_test.go b/pkg/util/kube/node_test.go new file mode 100644 index 000000000..9463938eb --- /dev/null +++ b/pkg/util/kube/node_test.go @@ -0,0 +1,132 @@ +/* +Copyright 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 kube + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/vmware-tanzu/velero/pkg/builder" + + clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" + + velerotest "github.com/vmware-tanzu/velero/pkg/test" +) + +func TestIsLinuxNode(t *testing.T) { + nodeNoOSLabel := builder.ForNode("fake-node").Result() + nodeWindows := builder.ForNode("fake-node").Labels(map[string]string{"kubernetes.io/os": "windows"}).Result() + nodeLinux := builder.ForNode("fake-node").Labels(map[string]string{"kubernetes.io/os": "linux"}).Result() + + scheme := runtime.NewScheme() + corev1.AddToScheme(scheme) + + tests := []struct { + name string + kubeClientObj []runtime.Object + err string + }{ + { + name: "error getting node", + err: "error getting node fake-node: nodes \"fake-node\" not found", + }, + { + name: "no os label", + kubeClientObj: []runtime.Object{ + nodeNoOSLabel, + }, + err: "no os type label for node fake-node", + }, + { + name: "os label does not match", + kubeClientObj: []runtime.Object{ + nodeWindows, + }, + err: "os type windows for node fake-node is not linux", + }, + { + name: "succeed", + kubeClientObj: []runtime.Object{ + nodeLinux, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClientBuilder := clientFake.NewClientBuilder() + fakeClientBuilder = fakeClientBuilder.WithScheme(scheme) + + fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build() + + err := IsLinuxNode(context.TODO(), "fake-node", fakeClient) + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestWithLinuxNode(t *testing.T) { + nodeWindows := builder.ForNode("fake-node-1").Labels(map[string]string{"kubernetes.io/os": "windows"}).Result() + nodeLinux := builder.ForNode("fake-node-2").Labels(map[string]string{"kubernetes.io/os": "linux"}).Result() + + scheme := runtime.NewScheme() + corev1.AddToScheme(scheme) + + tests := []struct { + name string + kubeClientObj []runtime.Object + result bool + }{ + { + name: "error listing node", + }, + { + name: "with node of other type", + kubeClientObj: []runtime.Object{ + nodeWindows, + }, + }, + { + name: "with node of the same type", + kubeClientObj: []runtime.Object{ + nodeWindows, + nodeLinux, + }, + result: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClientBuilder := clientFake.NewClientBuilder() + fakeClientBuilder = fakeClientBuilder.WithScheme(scheme) + + fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build() + + result := withOSNode(context.TODO(), fakeClient, "linux", velerotest.NewLogger()) + assert.Equal(t, test.result, result) + }) + } +} From dfdb1c139d24e366b3ac7477d0fd0576705959ba Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Fri, 13 Dec 2024 16:12:44 +0800 Subject: [PATCH 02/16] backup repo crd changes Signed-off-by: Lyndon-Li --- .../bases/velero.io_backuprepositories.yaml | 26 ++++++++++++++-- config/crd/v1/crds/crds.go | 2 +- pkg/apis/velero/v1/backup_repository_types.go | 22 +++++++++++++- pkg/apis/velero/v1/zz_generated.deepcopy.go | 30 +++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/config/crd/v1/bases/velero.io_backuprepositories.yaml b/config/crd/v1/bases/velero.io_backuprepositories.yaml index 00818bc5e..5ce487be6 100644 --- a/config/crd/v1/bases/velero.io_backuprepositories.yaml +++ b/config/crd/v1/bases/velero.io_backuprepositories.yaml @@ -88,8 +88,8 @@ spec: description: BackupRepositoryStatus is the current status of a BackupRepository. properties: lastMaintenanceTime: - description: LastMaintenanceTime is the last time maintenance was - run. + description: LastMaintenanceTime is the last time repo maintenance + completed. format: date-time nullable: true type: string @@ -104,6 +104,28 @@ spec: - Ready - NotReady type: string + recentMaintenanceStatus: + description: RecentMaintenanceStatus is status of the recent repo + maintenance. + items: + properties: + completeTimestamp: + description: CompleteTimestamp is the completion time of the + repo maintenance. + format: date-time + nullable: true + type: string + message: + description: Message is a message about the current status of + the repo maintenance. + type: string + startTimestamp: + description: StartTimestamp is the start time of the repo maintenance. + format: date-time + nullable: true + type: string + type: object + type: array type: object type: object served: true diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 07ed6845c..7d5871817 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -29,7 +29,7 @@ import ( ) var rawCRDs = [][]byte{ - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVMo\xe36\x10\xbd\xfbW\f\xb6\xd7Jޠ=\x14\xba\xed\xba-\x104\t\x02'ȝ\x92F27\x14ɒC\xa7\xee\xc7\x7f/\x86\x94bY\x92כ\x14\xa8n\"\x87o>\xde\xcc#\xb3,[\t+\x9f\xd0yit\x01\xc2J\xfc\x83P\xf3\x9fϟ\x7f\xf2\xb94\xeb\xfd\xd5\xeaY꺀M\xf0d\xba-z\x13\\\x85?c#\xb5$i\xf4\xaaC\x12\xb5 Q\xac\x00\x84ֆ\x04/{\xfe\x05\xa8\x8c&g\x94B\x97\xb5\xa8\xf3\xe7Pb\x19\xa4\xaa\xd1E\xf0\xc1\xf5\xfec~\xf5c\xfeq\x05\xa0E\x87\x05\x94\xa2z\x0e֡5^\x92q\x12}\xbeG\x85\xce\xe4Ҭ\xbcŊ\xd1[g\x82-ฑN\xf7\x9eSԟ#\xd0v\x00:\xc4-%=\xfd\xb6\xb8}#=E\x13\xab\x82\x13j)\x90\xb8\xed\xa5n\x83\x12nf\xc0\x0e|e,\x16pDZXQa\xbd\x02\xe83\x8d\xb1e \xea:\xd6N\xa8{'5\xa1\xdb\x18\x15\xba\xa1f\x19|\xf1F\xdf\v\xda\x15\x90\x0f\xd5\xcd+\x87\xb1\xb0\x8f\xb2CO\xa2\xb3\xd1v(ا\x16\xfb\x7f:\xb0\xf3Z\x10\xce\xc1\xb8r\xf91\xd6ǃ\xc5\x13\x94c!`\xb4\x97\x10=9\xa9\xdb\xd5\xd1x\x7f\x95JQ\xed\xb0\x13Eok,\xeaO\xf7\xd7O?<\x9c,\x03Xg,:\x92\x03=\xe9\x1b\xb5\xdfh\x15\xa0F_9i)6\xc7\xdf\xd9\xc9\x1e\x00;H\xa7\xa0\xe6>D\x0f\xb4á\xc6X\xf71\x81i\x80v҃C\xebУN\x9d\xc9\xcbB\x83)\xbf`E\xf9\x04\xfa\x01\x1dÀߙ\xa0jn\xdf=:\x02\x87\x95i\xb5\xfc\xf3\x15\xdb\x03\x99\xe8T\tBO\x10Y\xd4B\xc1^\xa8\x80߃\xd0\xf5\x04\xb9\x13\ap\xc8>!\xe8\x11^<\xe0\xa7q\xdc\x1a\x87 uc\n\xd8\x11Y_\xac\u05ed\xa4a(+\xd3uAK:\xac\xe3|\xc92\x90q~]\xe3\x1e\xd5\xda\xcb6\x13\xae\xdaI\u008a\x82õ\xb02\x8b\x89\xe88\x98yW\x7f\xe7\xfa1\xf6'ngD\xa7/N\xd2\x1b\xe8\xe1\xd1\x02\xe9A\xf4P)\xc5#\v\xbcĥ\xdb\xfe\xf2\xf0\bC$\x89\xa9D\xca\xd1tV\x97\x81\x1f\xae\xa6\xd4\r\xbat\xaeq\xa6\x8b\x98\xa8kk\xa4\xa6\xf8S)\x89\x9a\xc0\x87\xb2\x93\xc4m\xf0{@OL\xdd\x14v\x13\x85\vJ\x84`yt\xea\xa9\xc1\xb5\x86\x8d\xe8Pm\x84\xc7\xff\x99+f\xc5gL\xc27\xb15\x96\xe3\xa9q*\xefhc\x90\xd23\xd4N\xe5\xf1\xc1b\xc5\xccrq\xf9\xa8ld\x95f\xaa1\x0e\xc4\xcc\xfe\xb4R\xcb\x12\xc0_\x12\xd1\a2N\xb4xc\x12\xe6\xd4\xe8R\xdb\xf1\xf7y\th\x88\x98e+i\x02.\x1b.\x00\xd2N\xd0H\fHH\xfd\xaa)\x8bI~\x85\x99Ȏ`\xa5\xd0BW\xf8k\xecG]\x1d.$z\xbbp\x84Sڙ\x170\r\xa1\x1e\x83\xf6\xb1.dR\"\xb8\xa0\xdf\x14\xec1Ǎэl灎/\xb2s\xe4^p2\xc9v;\xf1ərs\x1dcɆ\xcecB\x1a\xd9\x06w\x8e\xbcF\xa2\xaag\x12\x02\xa0\x83R\xa2TX\x00\xb9\x80g*2\x9b\x95ӊ\xf0\xfdx\x81\xb8\xed\x891H]\xf3\xb4\xf4\x97\x15;\x19\x9a\x91\xdb\x1fu\r\xee\xf4\x992\xfeP\x87n\xee.\x83gc\xa5XXw\xe8IV\v\x1b\x1f>\xbc\xad\x03\x18\xe6\xbaf9j$\xba\xf7\xcc\xe4v\x821\x8cc\x13\x94\xea\x1dd\x95\xe9\xac Y*\x1c\xee\f\xe6\\\xa63\x87\xa5\xa6\x81\xff4\x86{~o\xe1\xeb\v\xed=i=\x9dB\x8cE&-\xc4\xf8\x92\xb2\x8d\xc2\x1cT\xc4/@ZS\xf7\x91\xf5\xe7b\xeb\xbf!1\x96\a\xe9pr[g\xcb\xfa:\xb1YR\xa6\x89ɴ\x1b&ۓ\xa2~\xd3\xfdC\x82\x82\x7f\xcb\r\x14\x0f\fŮ\x82s\xf1\x86O\xab\xfc\xb0{\xf7\x1d\xa4\x84\xa7\x91\xd4\xf23\xfbB[\xdc\xccO\f\x811\x18\x10/\x8c\xb5\xf9E,\xb1\xbe\xa8ʍq\x9d\xa0\xf4\x8e\xcf\x18\xe8}\"\xb6|\a\xa1\xf7\xa2\xbd\x94\xddm\xb2J\x0f\xb9\xfe\b\x88\xd2\x04:Sz\xdaͣ\x80\vt\\\x88\xd4\ue13f\x14\xe7=\xdb,5\xc4\xe4\xae\xffZ\b\xe7\xd4\xf5\x0e_\x16V\xb7(\xea\xb9Bgpghy\xebl\x86\x8bS1[\xf4\xfc\xe6\xadG<\xfb4\xc8\xe3\x95P\xbe>\xe9\v\xf8\xeb\x9fտ\x01\x00\x00\xff\xff\x12%\xb58\xdc\x0f\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccWM\x8f\xdb6\x13\xbe\xfbW\f\xf2^_\xd9\t\xdaC\xe1[\xe2\xb6@\xd0$X؋\xbd\xd3\xd2\xc8f\x96\"Yr\xe8\xd4\xfd\xf8\xefŐ\x92-K\xb4\xb5\xde\x02Ey\x1393\x9cy\x9e\xf9\xa0\x8a\xa2\x98\t+\x9f\xd0yi\xf4\x12\x84\x95\xf8\x1b\xa1\xe6/?\x7f\xfe\xc1ϥY\x1c\xde͞\xa5\xae\x96\xb0\n\x9eL\xb3Fo\x82+\xf1G\xac\xa5\x96$\x8d\x9e5H\xa2\x12$\x963\x00\xa1\xb5!\xc1۞?\x01J\xa3\xc9\x19\xa5\xd0\x15;\xd4\xf3\xe7\xb0\xc5m\x90\xaaB\x17\x8dwW\x1f\xde\xce\xdf}?\x7f;\x03Т\xc1%lE\xf9\x1c\xacCk\xbc$\xe3$\xfa\xf9\x01\x15:3\x97f\xe6-\x96l}\xe7L\xb0K8\x1f$\xed\xf6\xe6\xe4\xf5\x87hh\xdd\x19:\xc6#%=\xfd\x92=\xfe$=E\x11\xab\x82\x13*\xe7H<\xf6R\xef\x82\x12n$\xc0\x17\xf8\xd2X\\\xc2\x17\xf6Ŋ\x12\xab\x19@\x1bi\xf4\xad\x00QU\x11;\xa1\x1e\x9cԄneTh:\xcc\n\xf8\xea\x8d~\x10\xb4_¼Cw^:\x8c\xc0>\xca\x06=\x89\xc6F\xd9\x0e\xb0\xf7;l\xbf\xe9ȗW\x82pl\x8c\x91\x9b\x9f}}\xbcKP\x94{lIJ\x955\x16\xf5\xfb\x87\x8fO\xdfm.\xb6\x01\xac3\x16\x1dɎ\x9e\xb4z\xe9\xd7\xdb\x05\xa8ЗNZ\x8a\xc9\xf1gqq\x06\xc0\x17$-\xa88\x0f\xd1\x03\xed\xb1\xc3\x18\xab\xd6'05\xd0^zph\x1dz\xd4)3y[h0ۯX\xd2|`z\x83\x8èߛ\xa0*N\xdf\x03:\x02\x87\xa5\xd9i\xf9\xfbɶ\a2\xf1R%\b=AdQ\v\x05\a\xa1\x02\xfe\x1f\x84\xae\x06\x96\x1bq\x04\x87|'\x04ݳ\x17\x15\xfcЏ\xcf\xc6!H]\x9b%쉬_.\x16;I]Q\x96\xa6i\x82\x96t\\\xc4\xfa\x92\xdb@\xc6\xf9E\x85\aT\v/w\x85p\xe5^\x12\x96\x14\x1c.\x84\x95E\fD\xc7\u009c7\xd5\xff\\[\xc6\xfe\xe2\xda\x11\xd1i\xc5J\xba\x83\x1e.-\x90\x1eDk*\x85xf\x81\xb7\x18\xba\xf5O\x9bG\xe8\xea\xf28\x11\xe8\xe7\x8c\n\x87\xb47\xdf\xc0Ԅ\xbao\xb4\xf55\x13\xc9\x16\xc1\x05}\x97\xb3\xe7\x18WF\xd7r7v\xb4?Ȯ\x91;q\xc9 \xda\xf5\xe0N\x8e\x94\x93\xeb\xecK\xd1e\x1e\x13R\xcb]p\xd7ȫ%\xaaj\xd4B\x00tPJl\x15.\x81\\\xc0+\x88\x8cj\xe5\x12\x11\x9e\x8f\x13ĭ/\x84Aꊫ\xa5\x1dV|I\x97\x8c\x9c\xfe\xa8+p\x97ϔ\xfeB\x1d\x9a\xf1u\x05<\x1b+Efߡ'Yf\x0e\u07bc\xb9/\x03\xd8\xccNJ\xdbQ-ѽ\xa6&\xd7\x03\x1b]9\xd6A\xa9\xf6\x82\xa24\x8d\x15$\xb7\n\xbb\x99\xc1\x9cˤs\xcc%\r\xfc\xa32<\xf0{\vO/\xb4ׄ\xf5ti\xa2\xdfd\xd2F\xf4/u\xb6\x9e\x9b]\x17\xf1\x19\x93\xd6T\xadg\xad^L\xfd;\x02\xe3\xf6 \x1d\x0e\xa6u\x91\xef\xaf\x03\x99\\g\x1a\x88\f\xb3ap<\x00\xf5E\xf3\x87\x04\x05\x7f\xcf\x04\x8a\n\x1d\xd8ep.N\xf8\xb4\xcb\x0f\xbbW\xcf %<\xf5Z-?\xb3'\xd2\xe2\xd3X\xa3s\x8c\x8d\x01\xf1\x063\xdf\xc76C;'\xbf\xc2̣\x03\x98\xfeFPz\xce\x17l\xefu\xbd,?\x8a\xd0{\xb1\x9b\n\xf2s\x92J\xef\xb9V\x05\xc4\xd6\x04\xba\xc2\x00\xeds1\xdefe\xc2S\xbb\x17~\xca\xcf\a\x96\xc9\xe5\xc5`\xe4\xdfr\xe1Z\x93\xfd\x82\xdf2\xbbk\x14ոQ\x17\xf0\xc5P\xfe\xe8f\x9f-Q\xf7\x93i\x93)\x8cQ\xcc\xeb\xbc\x16\xa3p\xc1Gk>\xe6b\x86\x9b^v\x8e1\x91\x84Mv\xa4_\xaf\xa4\xb4\xba\xac>\xfd\xaf\xe6\xc5\x06!\xad\x86Z'J\xd3\x01?\xe7b]]ʹ\x0e\xd0˲\x1b\a\x96\xd6t\x81\xa55Qfi\xdd|\xe5\xc0\xad\x92\xcb qo\xe1]\x85\"%\xc0\xcb\xe0\x98\x8c\xc0\x93pt\x1f\xa5\x9b\v\x95\xd3\xef\x03\xef\xf6\xa9\xfcORv\xe3)\xd8\x1d\n\xe7\xc4qz֍6=\xff\xc9V=\xe7|\x1a\xcf\xfd\x9d\xb0=\xfd\xa8/\u13fff\x7f\a\x00\x00\xff\xff\xb4\xb6j\xe3\xb2\x13\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec}_s\xdb8\x92\xf8{>\x05ʿ\x87\xd9ݒ\xecI\xfd\xf6\xe1\xcao\x19'\xb9Q\xedL\xe2\x8a=\xd9g\x88lI\x18\x83\x00\x17\x00ek\xef\xee\xbb_\xa1\x01\xf0\x8f\b\x92\xa0,{\xb2{\xe1Kb\x11l\x00ݍ\xfe\x0f`\xb9\\\xbe\xa1%\xfb\nJ3)\xae\t-\x19<\x19\x10\xf6/}\xf9\xf0\x1f\xfa\x92ɫ\xfd\xdb7\x0fL\xe4\xd7\xe4\xa6\xd2F\x16_@\xcbJe\xf0\x1e6L0äxS\x80\xa195\xf4\xfa\r!T\bi\xa8\xfdY\xdb?\tɤ0Jr\x0ej\xb9\x05q\xf9P\xada]1\x9e\x83B\xe0\xa1\xeb\xfd\x8f\x97o\xffz\xf9\xe3\x1bB\x04-\xe0\x9a\xaci\xf6P\x95\xfar\x0f\x1c\x94\xbcd\xf2\x8d.!\xb3 \xb7JV\xe55i^\xb8O|wn\xa8?\xe1\xd7\xf8\x03g\xda\xfc\xad\xf5\xe3/L\x1b|Q\xf2JQ^\xf7\x84\xbfi&\xb6\x15\xa7*\xfc\xfa\x86\x10\x9d\xc9\x12\xae\xc9'\xdbEI3\xc8\xdf\x10\xe2G\x8d].\xfd\x80\xf7o\x1d\x84l\a\x05uc!D\x96 \xdeݮ\xbe\xfe\xff\xbb\xceτ\xe4\xa03\xc5J\x83s\xff\xefe\xfd;\xf1\xa3$L\x13J\xbe\xe2\x1c\x89\xf2('fG\rQP*\xd0 \x8c&f\a$\xa3\xa5\xa9\x14\x10\xb9!\x7f\xab֠\x04\x18\xd0-x\x19\xaf\xb4\x01E\xb4\xa1\x06\b5\x84\x92R2a\b\x13İ\x02ȟ\xdeݮ\x88\\\xff\x0e\x99ф\x8a\x9cP\xadeƨ\x81\x9c\xec%\xaf\np\xdf\xfe\xf9\xb2\x86Z*Y\x822, \xdd=-Nj\xfd:6W\xfbX\xf4\xb8\xafHnY\nܴ<\x8a!\xf7\x18\xb5\xf33;\xa6\x9b\xe9#\x93ٟ\xa9\xf0ÿ<\x02}\aʂ!z'+\x9e[N܃\xb2\b\xcc\xe4V\xb0\x7fְ51\x12;\xe5Ԁ\xb6\x981\xa0\x04\xe5dOy\x05\v\x8b\x94#\xc8\x05=\x10\x05\xb6OR\x89\x16<\xfc@\x1f\x8f\xe3W\xa9\x800\xb1\x91\xd7dgL\xa9\xaf\xaf\xae\xb6̄\xf5\x95ɢ\xa8\x043\x87+\\*l]\x19\xa9\xf4U\x0e{\xe0W\x9am\x97Te;f \xb3d\xbe\xa2%[\xe2D\x04\xae\xb1\xcb\"\xff\x7f\x81=t\xa7[s\xb0l\xab\x8dbb\xdbz\x81\xebc\x06y\xec\xd2q\xcc\xe8@\xb9)6T\xb0?Y\xd4}\xf9pw\xdffT\xa6=QZ\xfc:D\x1f\x8bM&6\xa0\xdcw\x1b%\v\x84\t\"w\xac\x8a|\xce\x19\bCt\xb5.\x98\xb1l\xf0\x8f\n\xb4]\x03\xf2\x18\xec\r\xca \xb2\x06R\x95\xb9e\xe3\xe3\x06+Anh\x01\xfc\x86jxeZY\xaa\xe8\xa5%B\x12\xb5ڒ\xf5\xb8\xb1Co\xebE\x10\x90\x03\xa4u\x82宄\xac\xb3\xd0\xecWl\xc32\xb7\x9c6R5r\xc7\xc9\xc0.\x86\xe2K\xdf>\x99fw\x82\x96z'\xcd=+@V\xe6\xb8\xc5\x14\xaf!\xf1\xeeVGP\xc2\b\xfdxQfU\x1ar\xbbh\x1f)38曻\x15\xf9\x8a\xc2*|\x8dB\xab\xd2\xc4TJX.\x89\xf4\xf5\x05h~\xb8\x97\xbfi y\x85̝)@<,\xc8\x1a6\x96\x13\x14\xd8\xef\xed+P\xca\xe2F\xe3\x00d\xd5\x136\xf6\xb9߁\xc5-\xad\xb8\xf1\xeb\x84i\xf2\xf6GR0Q\x99\x1e\xab\rR\x1d1E\r-\xe4\x1e\xd4)H|O\r\xfd\xd5~|\x84;\v\x94 T\x8b\xbc\xb5\xc7\xe3\xfa\x80/c\xd4v\xcfjӂ\xc84\xb9\xb8 R\x91\v\xa7\x81/\x16\xee\xeb\x8aq\xb3d\xa2\xdd\xc7#\xe3<\xf42o\xf2\x0e\x87\x8e\xa0\xfa^~ԎyO\xc2\xc5\x00\xac\x16j\x1ew`v\xa0H)k\x8d\xb7a\x1c\x88>h\x03\x85GL\xd0\"~>\x91\x9ep\xedp\xeeAh\x8bW?\x91\xfe\xe4E\xc59]s\xb8&FU0\x80\x9b\xb5\x94\x1c\xa8\x98@\xce\x17Іe\xe7@\x8d\x83\x14A\x8c\xf2/:\x18@\xa5I\x1f\x80\xd0\bh\x8f3\xab\x9d9o!\xb6\x8b\x957\xd1A\x95\n2+\xb6\xaf\xbd:`\xc0Q\x05\tI\xb8\x14[P\xae{k\xaa\x04\x0eS`9.'V\xd2*\xe0V\x9d\x90Me\x85\xf0%\xb1\xcb{\x90\t\x98\xd0\x06h\x84;\x9fA x\xcax\x95C~\xe3,\xaf;k@\xe6\xc1l\xee\x89\xcd\x14B}\x18\x85\xe8\xd53g\x19Z\x81\xde\xe0[\xa2\xe1\x1a\xe3\xd3FK\x1fJp\xb6\xb3\xa5\xa5\x1fv\xa3~G\x05\x82\x06c?\xba\xf8\xcb\xc5\x02I\xdc\xed\xb5ۇ&TA\x8d\x96d\xc1\tEi\x0e\xfd\xd6\xcc@\x11\xc1\xe2\xa8@I\xa4'U\x8a\x1e\x06\xa8Y;\x00g\xa4\xe7\x10\xcc#\x8a\x8a\xd0\xec\x95iz\xdc\xef\xbf3U\xcfCG\x8d\xee.e\xc2\xd2\xcfz\x9e\x1d\xf2i\xe7\xc0Y\xb4\ti\"\xf0\x98p\xf0\xd07\x1b\xa1\xd6\x1f\x84\xac\xb3\xf0\xfc\x10\x93\u05fc\xe5\x99\xf7_\x12S;)\x1f\xa6\xb0\xf3\xb3m\xd3xE$ð\nYÎ\xee\x99T~ꍮ\x85'\xc8*\x13]\xf5Ԑ\x9cm6\xa0,\x9crG5h\xe7'\x0f#d\xd8~'-1\x12}y4\x8f\x86\x90\x96L8\xf3\xa1\xa1[C\xe2XK\x86\xc7\x0e\xd4\xdaר\x8cs\xb6gyE9\xeae*27\x1fZ\x8f+&eF\x88\xdc\x1bs\x943\xdd\xe3\f\x820)K\xa4\x8e\xab$\x05X\xa3\xb7\xb0NA\xbf\xe9\xf0\xcc\xd7\xd4\xda*rh\xf6\x04\x89\xa5*\x0e\xdaw\x95\xa3\x1d\xd9ȌEC\x14\x8cD\x10N\xd7\xc0\x89\x06\x0e\x99\x91*\x8e\x91):\xbb'E\b\x0e 2\"\xf9\xba\xaeF3\x81\x11\x90\x04}\xb8\x1d\xcbv\xceԳL\x84pH.\xc1\x1a|\x86в\xe4\x11u\xd1<\xa3\xc4\xf7\x9d\x8c\xad\xf5\xe6\x99X\xf5\xc7\xf0b\xeb\xbfy\x12df\xf3DQ۬\xaf.fkv\x88;\xb5\xcd\xf3\xef\x89\xd8 \xf9O`ڑ\xd5O0,\x94\xccӃ|k\xb1\xca@_Zs\n-\x9d\x05a&\xfc:\xb5\x12:6W/Z\xd6A·M\x9b\xf9L\x9fH\x9a\x945\xf1B\x84\xa9\xbb\xf8\x17\xa4\v\xaa\x8c;\xaf1\x92i\xf2K\xfb\xab\x05a\x9b\x1a\xe9\xf9\x82l\x187\xa0\x8e\xb0\x7f\x92\xa8\x0f\x949\a2R\xb4\x1e\xc1\xf8\xbd\xc9v\x1f\x9e\xac\t\xa6\x9bTU\"^\x8e?v\x86l\xb0\xf6\xbb\xeay\x02.\xc186SP`|\x1c=\xa6\xf6/hZ\xbd\xfb\xf4>\xee_\xb5\x9f\x04\xce\xebMdbѹ\xe7\xddь\xda\xe3\xf3&|x\x836P\xed\x00\xb9\\ȂP\xf2\x00\ag\xbaPA,}hh\x9cн\x02L\xca \x9f=\xc0\x01\xc1ij,\xfd'\x95\x1b\xdc\xf3\x00\x87\x94fG8\xb4cb\xdag\x8f,\x9e\xec\x0f\x88\b\f\xae\xa7\xb2\x81{\xfcR\x88\xe44\xe2O\xa2,\tO\xc0\xfd\t\xd3Lb\x95v\x1f\xed4%r\xc0\x0f\xda\xd1Ү\x98\x1d+Q\xacb\xc4An\x92\tꞯ\x94\xb3\xbc\xeeȭ\x91\x95X\x90O\xd2\xd8\x7f><1\xed3\x99\xef%\xe8O\xd2\xe0//\x82Q7\xf0\x97ħ\xeb\x01\x17\x9apR\xde\"\xac\x9d\x8bs:\xcdr[\x8d{\xa6\xc9JXwš$\xb1+L\xbb\xba\xee\\GE\xa51\x8d&\xa4X\xba\xb0M\xac'\x8fo\xa9:\xe8~v\xa7\xbe\xc3{\xab,\xdc\x1b\x97\xfc\xe54\x83<\xe4k0+I\rlY\x96\xd8_\x01j\v\xa4\xb4\"<\x8d#\x12\x05\xab\x9f\xcd<\xf6I\xd3\xde\xed\xe7i\xf9P'\xf9\x97V\xe5,=\x04#\x8b\x04\x1cxٝO\xcfgi\xd7lB\xab\xc0\t\x93M\a\x92\x96\xc3MS\x90\xf2\ft\xa0\x16G\x13g\x92\xba4ϱЅ\xf2\xdb\x19\x1ae\x06/\xcc\x15\r\xad\xb1;\x15\\P̵\xfc\x97մ\xb8\x9a\xfe\x87\x94\x94)}I\xdeaM\v\x87\xce;\x1f4k\x81I\xe8\x12kR,\xff\xec)\xb7\xba\xdf\npA\x80;K@nzvт<\xee\xa4vj\xbbN\xe2\\<\xc0\xc1\xa5\f'\xbbl\v\x99\x8b\x95\xb8p6DO`\xd4\x06\x87\x14\xfc@.\xf0\xdd\xc5sL\xa9DNMl\xd6aт\x96i\x1c\x8a5E\xa9\x86\xbauX\x83\x11b?\xacke\xac\x91=6\xdb$\x16-\xa5\x8ed\xf2\a\x862\xc1\xbc\xb7R\x1b\x17/\xeb\xd8\xccр\x9a\fA4B7\xae\x80I\xaaPmb\x85\xf2T\xe8\xb7\xfd\xdc\xef@\x83\xcfW\xf8\xc0\x9c\x03j=\xbb\x8bf};i\x7f\xe1\xf2%\xd8\t\xcd\xd0b\xc1oK%3\xd0\xd1dv\xf3$\xe8\x8bHYF{\xeeȗ:/\xc9\xd5d\x8c\x87@Ón\xf2ZD\xcc\xf4\x17><\xb5\x02\xa2v\xedۿ\xa7xl\xee\xb8\b\xd6\f\x16\x05=\xaeSJ\x1a\xe2\x8d\xfb2\xac\x06\x0f\xc89\x1fj[\xa1$H\xd5\xe55\x03~\v\x86B\xc1\xc4\n; o_\xc0\xb0\xf024Vm\x12{N3eoB'\ru\xea\x1f\xdcR.%\xa6\n\x14t\x88\u05cf\xaa\xa3\x1d*\xa4i\x05$f\x98\x9b\xa5\xcc\x7f\xd0dÔ6\xed!\xe8\x81:\x95(\x98\x99\x8e\x97\xf8\xa0\xd4I~\xd7g\xf7e+ܵ\x93\x8f\xa1>\xcb!&q\xe6\x98_\x02\xc26\x84\x19\x02\"\x93\x95\xc0\x00\x8e]\xc7\u0605C\xae\x93\xb0,u\x91\xa4\xad~\xfb\x80\xa8\x8a4\x04,\x91S\x98\x18\x8d\xf4\xb4\x9b\x7f\xa4\x8c\xbf\x04\xd9\xccP\x19[\xec9mM\x84\x1a\xb7vE^A\x9fXQ\x15\x84\x16\x96F\xa8\xccY\x01]\xa27\x95o\xf6\vT\x13F\xda\x15Sr0\xe0\xab\xd7\x12ǐI\xa1Y\x0e\xb5r\xf5\x8c \x05\xa1dC\x19\xafT\xa2\x04\x9c\x85\xde9\xae\x88\x97\x04\xe7\xf31\xd2:_\"*\x12\xa2\xb9\x89\xb6\xe2\xb84.U\xba\xc57ef)\x98oe\x95\x8aI\xac\v<\xb3\xa1\xe5+)\xa98|\xb7\xb4R\x87\xfa\xdd\xd2\x1a{\xbe[Z\x13\xcfwK뻥\x95\xd2\xf2\xbb\xa5\xf5\xdd\xd2j?\xff',\xad\xa9\x11\xb9\r}\x03/'G\x91\x90\xaa\x1e\x1b\xe2\b|_\\\xe1k\xc0\x9fU\x8b\xb9\x8a\x83\x8aT\xfe\x0f\x94uDŽV\xa3<\xea\xe2L\xbbj\x02ϻ\xfdE\x13\xa6\xe43\xaa\xeeC\xa7竺_\x8dBΠ\xcdw\xe4s\xe9bO\xb3\xed\x8e\xf1\x98JZ1\xe1\xc9%\x84\xdd\x12\xc1\x01\xbbtn\xb6\xfb,[&\xfe\x90\xd2\xc0\xf9\x05\x81)\x11\xb1\x89\xe2\xbf\x13J\xfe\x12k\x8b\x9f\x9d\x9eO)\xea\x9b\xe31\xbfX\x01\xdf\xf9\xcb\xf6\x92\xf03]\xa27\a;/^\x8e\xf7\x8aEx\xafSz\x97Xpw\xbe\xca\xf9\xb4x\xecI\x95cӡ\x83ᢹ\xc9R\xb9\xc9\xd0\xc2\xd4\xc4fOi\xb2\x04nN\xe1\xdb$uҖ٫\x95\xb6\xbdZA\xdb떱\x8dr\xd1\xe8\xcb9\x85j\xf1siȤ\xb2\xe5\xaf\xc5l\xa7\xa2A\xaa\x8e\xf9z\x92\x7f\xf5\xf9\b\x86%|0\xed^\xc9F.*nX\xc91\x91\xbagy4\xd8`vp\xa8\x0f\xd0\xf8]\xe2\xd6S\x7f\x14\xcc\xe7/5\xd7^\x1eY\xfaT\x93G\xe0\x9c\xd0غ\xea\xcd\x9b2\x90SwK\xbd\xac\x8b3\xdfə\xb4*\xd2-\xbf?h\x17\xd4)\xbb\x9f\xd2\n\x00&w;\xbd\x94\xcb3\xe5\xf4$\xdbyi\xbb\x99\xe6%\v_p\xf7\xd2K\xecZJ\xc4T\xca.\xa5yxz\x85]I\xaf\xba\x1b\xe9\xb5v!%\xef>J*qI\xce\x02\xa7\x96\xa8\x9c\xb8\x9df:\xc7;\xbe\x9b(a\x17QB\xf6wz\x92'L/a\x97м\xddA\t4K]\x8a\xaf\xb8\v\xe8\x15w\xff\xbc\xf6\xae\x9f\tΚx=ow\xcf\xc9)\v\xa9rP\xa3i\x9fT.\x1c\xe5\xbf\x14ߦ;\x90\xa3|G8\xf6϶\xea\xd8˨\x1e\xfcI\xa3x\xa6\xecP\xfa\xd2rZ\xcb\xda\xe8\xe4\xa2\x1a\xf3\xa7kL\xfa\x83f]\xbaJCI\x15\x1e^\xbc>\xb8r\x96\xa8j\xfe@\xb3\xdd\x11\xf4\x1d\xd5d#UA\r\xb9\xa8\x13\x80W\x0e\xb8\xfd\xfb⒐\x8f\xb2\xae\x89h\x9fˣYQ\xf2\x83\xf5P\xc8E\xfb\x83\xd38 \xcam\xa1\xb7[\xc9Y\x16\xb1ݢg3\xb9ƽ\xc32\xf0Ĩ\xac]2Pچq\xd3\rͼ\xee\x19\x98\x1bɹ|\x9c\xe9\xfbӒ\xfd'\x1e\xdd\xfd\x8c\xe8л\xdb\x15\xc2\b\xec\x81g\x81\xd7\xc5Y\xf5l\xd6`\xd5r3ϡ\xb5\xbf\xdat v\xeb\x1cۧ\xe3B\xee\x0eB\x0ef\x81\x17\x9d\x99\xb4\xd2\xe5v\xe5\xc61ԋ\xe5\x19*\x0eDbE\x8d\xd91\x95/K\xaa\xcc\xc1\x15j,:c\b\xbat,\xba3\xa8=\xfa\x87;G\xd1\x1b\xcet\xc6\f\xe5\xa1\xec&}\x8fqw\xca8\x86w/N\xee[<\xe38\x86͒%b*\xf2s\xb4\xf2\xeblQ3\xed\x8f&\xfeU\xee\xe1}4z\xd6A\xcf\xddQ\xf3HyV\x80\xe8N\xdd\x1d\xacR]\x03\x9e\xc8\xdb\x7f\xf5\x8cz\xabе?S\xf5\x94@\xd9]\x17Dd~\xe1\x88\xd9\xd0YL>\xe1\t\xf0\ar\xfb\x15}\xb4Z\xb4\xf9%\xea}\xb4\x10*\v\xc9\xe0\b\x1c\xff\xc1O\xe7/M\xd3F*\xba\x85_\xa4;d{\x8a\xec\xdd֝\xc3\u05fd\xd5\x13\xeaGâ\x89\x9d\xc0\xeb\x8f\xfb>\x02\xd6\xd4|\xf7N5\xb6\xa3\x9cyN\xb31\xfc\x14\xba\xdf\xdf\xff\xe2feX\x01\x97\xef+W\xee`e\xa2\x06\x8b\xe20[\aim\xff\xbb\x93\x8fx\xf8o<\x8e\x19.Mh&\xa3\x00\x8bͱ\x04q֔\xaa\x92K\x9a\x83\xba\x91bö\x13\xb3\xfb\xad\xd3\xf8H\xcdf\xf8\xa3\x9f\\\xad\xa3\x02\xfc3\xd7 X\x9b\x87s\xe0\x1f\x19\a톕 \x80o\xfb_\xd5\xf2\xb8*\xd6Ά\xdbؗu\a\x03:\xceM\vC\xd1%(kE\xb9\xa0u\xa5\x03\xaf\x0eO\xbc\xa1\b\x13\x06\xb6\xd0\xf7\x02G$\xf0\xbes\xe8{\xe0\xf3)q\xf45\xfeUˬl\xad4gW\xcaMd\xe0CpZWh<2㏼:\xef\x19\xa5C\xce\xc2\xd0\xe5\x00x\x1a\xfe\xf4\xf5\x00\xee\xd0|\x7f\xa9\x88g\xe4J\xe1\x01\xa3\xfe@}<\x90\xf3\xa4\x1b\x02\xd6u\xa9S]6\xa5\xdf\x19\x03EibZzZ\x90\xfc4\x06\xb0\xb6p\xa4\xa1\xbc\xc5\xcf44\x88٨\xfa \xb2\xb1\x92,\xbf\x8eG\xa89\xc6\xc91\x04\xdc\xf8\x9d\x04gC@\rp\b\x01\xba\xca2\xd0zSq~\xa872|#\xd8\xf8H\x19?\x1f*\x1c\xb4AF\xb0\xd3\x1b\x8549a_(\r\"\x0f+=l\xf2\x99\x87\nO\x05_G\xa8\r-N\xba\xeb\xe0\xa6\x0f\x06o\xbbQy\xab\x1c\x91\xd6c\xa7\xba!\x7fL,7\xe0ܗ\xe8\x9eXh\x90\x13\u0603 V\xaf9\x14\x87\xeb\x9afB\xf1{C\x9dn\b\x9a\"\x04\x11\xa2w\xfa\x10\x1f'\xd0xw\xcc\x0f\xba\x86\x89U\x96x\x15H\x1f\t}\xb3\xd1\xf9\xf9\xd7\xd6n\x86\xa5\x05q\x9a\xbd\x17\x95͙f]\xbd\xf0\xd4\r-nІD\xea4ע\x01g[f\xbd\x04K\xb9-Uk\xba\x85e&9\a\x94\xd6\xfdq\xbd\xe4Z\xf7\xbb\xf6\xbe\x00ՓS\xfb\xd8n\xebsg\x8e\xda.eL]\xa18^|e\x98\x82\xe6\x0e\xbaހ$v<˱qX\x88^\xd0\xd6\x1fi\xbbmXu^,\xfb\b\xa9\xbf\x9fm\xe1=\xea8?\x16\xf4w\xa9\x16\xa4`\xc2\xfeCE\xeeR_\xe1\xe3Y\xe3\xdfI\xf9p\x171b{\x83\xff\xb9n\xd8$\t\x98p\xc3ƭ\x96kY\xf9\xbcum\xd0\xc6\x13\x12x\xa6\xfd\x99\x1d5\x849\xa2\x0fz\xd3\x19\x8c\x85\xfe܁4\xa9\n\\\xcf\x03\xb0\xee\xc2%`\x9c\x1f\x16ǐ\x8f.\x1cl`\xb7\xce\xfc\xf7f@\xb3\x93\x7f\xa0\xa3\x90ˉ\x02\xa9\x8f\x8ch\v\xf4S\xfcE\x8f\xe6!c\xb2\x87㟛\xd6Cxt\xc3l\x99{\x03\x13\xec\x18\x81\xe7uu\xf1\x82\x87\t濵m\xea]\xff-\xc7-\xd4W\rƷ\xe2\xbbƗ\xe4\x13\xf4\x03\xfdn#8\xe4XӀ\xab*\xd2d%n\x95\xdc*\xd0}\xa6[\x92\xbfSf\x98\xd8~\x94\xea\x96W[&>\x0foz\x19k|K\x95a\x96i\xddxb\x03e\x82r\xf6Ϙ|j\xbf\x9c\x06t3\xe8(-I\xc20\x86^\xbc\ak\xab\x0e\xfa\xf7QQXz\xbc\x9ebw\x04\x9aL\xc9\xc6\xda&hl\x8a\xd0\xed%\xf9$\xa3\v\xdc\x17\x04\xb1.LkZ\x816K\xd8l\xa42._\xbb\\\x12\xb6\tA\x04+;0r\xe4\xaek$,\x96h\xadK-\x1a5\x84a_\x85\xda\x14\x0fs/\xe8\xc1\xe5fh\x96U\xd6R\xba҆\xf2\x88\xa1\xf2,\x01\x8e\xd1\x1a\xbb\x88 \xff\xedY\xb9\xacU\x1bP?\xec\x86\xfd8\x94\xe2q\x12\xcez\xe3v\x8a ȣb\xc6X\xdbH\x8e$\xd3=\xaa\x8c\xb5\x918'ڢ\xfa\xa4\xf8\x1bq\xe2p5\\\x94\x926\xe5\xfb\x1aʐ\x98\xf5\xb3\xc6\xcb\t\u05c8\x1bb\xedW\xac\xbf\xf1\xad,\x99\xb3\x1d\x15\xdb\xc1=\xfa;%\xab\xed.p\xf2\x80QL\xf2\n0\\\x89\"E\x87\xbbuM\xa5D+\x99>\xb2\xf1\x99\x04f\xc0\xe1\xd2\xec\x81T\xe5\xc2\xdf]\xeb\xaf&\xbe\xf2\xb7\x80,7J\x16K\xdf/\x16\xd0-|.[1i-\x10\xb3\x8bb\x9d8\xeb\xdb\x1f\xb4\x8f\x9cP\x96 \bվ焳\x92NV7\xdaPe\x9e\x15\x8e\xb8\xeb@\x98\x88D`w\xf1I\xdc\xf9\x94\xbe;,\xea\xc6\xdf\xd1Y\x03^\x10\xcdD\xb8\x1dٕ\a8\xfe\x88f\x8b\x04\xdef(U\xbcbo<\xb4Н\xd0\xebF\x15\xf6\xb5\xae\xfdp\xb2\xd3\xf9\xf5\b\xc6\xd1\xc6_\xbc\xbc\xb2n\x12\x1c\xc5?\xb1X\xe4\x1bK=3;\x95?\xff\xe1\x1bz\xf7INM\x1c#c>\x0e\xba/\xc3\xceJ\xf7\xae\xca[\x0e\xd6\xf4\xd2\x00]\xf7i\x96\x9b\xbc?c\xdc\xe8\x9cA\xa3p\x0f\xf8y\xa2&\xfb3\x86\x8b^,Vt\xde)?R\xbcE\xf8\xa4U\xfbw\xffm$X\xe4\xc1\x9e;\\Ԋ\x16\x85\x81\xbfj\xbc(\xaa\x95z?\xa2\x9c\xce[\xd2\xc2\xf7\xe4\x7f\xf9\xdf\x00\x00\x00\xff\xffU\xf5M\xfa݀\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccYK\x93Z\xbb\x11\xde\xf3+\xba\xee]xc\xc0\xceM\xa5nؤ0\x93T\xb92\xceL\x99\xc9dk!5\xa0\x8b\x8et\xa2\a\x98<\xfe{\xaa\xf5\x80\xc3y\f\x8c\x93r\xae63\xe8\xd1\xeaw\x7f\xad3\x1e\x8fG\xac\x96\xcfh\x9d4z\x06\xac\x96\xf8գ\xa6_n\xb2\xfb\xd9M\xa4\x99\xeeߏvR\x8b\x19,\x82\xf3\xa6\xfa\x8c\xce\x04\xcb\xf1\x0e\xd7RK/\x8d\x1eU\xe8\x99`\x9e\xcdF\x00Lk\xe3\x19M;\xfa\t\xc0\x8d\xf6\xd6(\x85v\xbcA=م\x15\xae\x82T\x02m$^\xae\u07bf\x9b\xbc\xff\xed\xe4\xdd\b@\xb3\ng\xb0b|\x17j\xe7\x8de\x1bT\x86'\x92\x93=*\xb4f\"\xcd\xc8\xd5\xc8醍5\xa1\x9e\xc1y!Qȷ'\xce?Db\xcbD\xec>\x13\x8b\xebJ:\xff\xe7\xe1=\xf7\xd2\xf9\xb8\xafV\xc125\xc4V\xdc\xe2\xb6\xc6\xfa\xbf\x9c\xaf\x1e\xc3ʩ\xb4\"\xf5&(f\a\x8e\x8f\x00\x1c75\xce \x9e\xae\x19G1\x02Ȫ\x89\xd4\xc6\xc0\x84\x88\xcaf\xea\xd1J\xed\xd1.\x8c\n\x95>\xdd%\xd0q+k\x1f\x95\x99d\x81,\f\x14i\xc0y\xe6\x83\x03\x17\xf8\x16\x98\x83\xf9\x9eI\xc5V\n\xa7\x7fլ\xfc\x1f\xe9\x01\xfc\xe2\x8c~d~;\x83I:5\xa9\xb7̕\xd5d\xa3\xc7ƌ?\x92\x00\xce[\xa97},\xdd3矙\x92\"r\xf2$+\x04\xe9\xc0o\x11\x14s\x1eQ\x89:\xea\xe7Tu\xee\xba`\x9bX\x81\xe7\x16\x95\xc4?\xcdd\xee\x1bd\x8b\x7fO\xb8\xc5\x13I\xe7YU_Нop\x88\u0605*\xeep͂\xf2MQ\xc9J\xaa闗b\xd5\xc8'\"\x9d\xba\xb8\xf1\xeeb.ݺ2F!KTҮ\xfd\xfb\xe4\x85|\x8b\x15\x9b\xe5ͦF=\x7f\xfc\xf8\xfc\xd3\xf2b\x1a\xfa\x1c\xa9\x15\x14d8ְ\xcd\x16-\xc2s\x8c\xbfd7\x97E;\xd1\x040\xab_\x90\xfb\xb3\x11kkj\xb4^\x96`I\xa3\x91\x8b\x1a\xb3-\x9e\xfe5\xbeX\x03 1\xd2)\x10\x94\x940\xf9U\x8e\x1f\x14Yr0k\xf0[\xe9\xc0bmѡNi\x8a\xa6\x99\xce\fNZ\xa4\x97h\x89\f\xc5vP\x82r\xd9\x1e\xad\a\x8b\xdcl\xb4\xfclj\xb6\x03o\xb23{t\x1eb\x84j\xa6\xc8Y\x03\xbe\x05\xa6E\x8brŎ`\x91\ue120\x1b\xf4\xe2\x01\xd7\xe6\xe3\x13E\x83\xd4k3\x83\xad\xf7\xb5\x9bM\xa7\x1b\xe9K\x86榪\x82\x96\xfe8\x8d\xc9V\xae\x827\xd6M\x05\xeeQM\x9d܌\x99\xe5[\xe9\x91\xfb`q\xcaj9\x8e\x82\xe8\x94R+\xf1\xa3\xcd9\xdd]\\\xdb\t\xe94bJ}\x85y(\xbd&\x97I\xa4\x92\x88g+\xd0\x14\xa9\xee\xf3\x1f\x97OP8I\x96JF9o\xed\xe8\xa5؇\xb4)\xf5\x1am:\xb7\xb6\xa6\x8a4Q\x8b\xdaH\xed\xe3\x0f\xae$j\x0f.\xac*\xe9\xc9\r\xfe\x1e\xd0y2]\x9b\xec\"V1X!\x84:&\x89\xf6\x86\x8f\x1a\x16\xacB\xb5`\x0e\xbf\xb3\xad\xc8*nLF\xb8\xc9Z\xcd\xda\xdcޜ\xd4\xdbX(5u\xc0\xb4\xbd\xd9`Y#\xbf\x88;\x81NZ\x8a\f\xcf<\xc6\xe8j)(\xa7\x8a\xe1\xa2\\F\x7f\x92\xa0\xc18G\xe7>\x19\x81\xed\x95\x16\xcb\xf3\xd3\xc6\v\x1ek\xb4\x95t\xb1\xbc\xc2\xda\xd8v\xe5a\xa7L\xde\x1c%\xe3\xb5\r\x0e\x80:T]F\xc6\xf0\x19\x99x\xd0\xea8\xb0\xf47+}\xf7\xa2\x01C\xd2H,.\x8f\x9a?\xa2\x95F\\\x11\xfeCk\xfbI\x05[s\x80u\xf4\x7f\xedՑr\x97;j\xde\xcd\xdae\xcc\x1f?\x96\f\x9eb+\af\xd6\xd5\x04\xe69\xa8\xcd\x1aށ\x90\x8e\x80\x84\x8bD\xbb\xca\xd2AE\xa01\x03oë\xc4\xe7F\xaf\xe5\xa6+t\x13\x1b\ry\xcc\x15\xd2-\xcd-\xe2M\x94\xb5\xc8;jk\xf6R\xa0\x1dS|ȵ䙓`S\x05YKT\xa2\x93\x9b\x06\xa3,\x8abQPP3uņ\x8b\xd3ƈ\xa4\x99\xd4Ƀ\xcf\x04b\xae\xb1U.\xcdڣ\x16خ6\x91\x1b\x13\x13\x9aC\x01\a\xe9\xb7)S\xaa\xbe\xb8\x83\x17c\x8f\xc6\x0e\x8f}\xd3-ޟ\xb6H;S\xe1Ep\xc8-\xfa\xe8m\xa8\xc8}ȕ&\x00\x9f\x82\x8b\xb9\xb6\x9d'ʈ\x80\xaf\x9c\xdeᱫh\xb8f\xdc\f\x85\x06X\x8e j\x06?\xfcp]\xa4Nu+\x83\xa0{\x11\xd4\xe2\x1a-\xea\x0e\x9a(\xe3)\xd6(r\x1a\xf20\\\xaf\x91{\xb9Gu\x8c5\x89\x92\xe7[X\x05\x0f\"`\xb4\x1a\xe3\xbb\x03\xb3\xc2\x017Uͼ\\I%\xfd\x11\xa4\x1b\xa0ϔ2\a\x14\xd9\xe2X\xd5\xfe8\x81\x8f\xday\xa69\xba\x13\x0e\"\x8d%W`:\xed\xcaQ\x1c\x01\x1d\xb3}90\x91\xaf\x8c\xf3\xc0ђ;\xaa#\x1c\xacћAa\x1f\xee\x1ef0\x17\x02\x8cߢ%\a\\\aUb\xa6\x01\xf9\xdeFd\xf1\x16\x82\x14\x7f\x18\xa0\xd5SZ\xa9\x9f\xb4\x1a=\xc6\xea*\fwTX9\xd6\xdeM\xcd\x1e\xed^\xe2az0v'\xf5fL\u008es\"\x9b\xc6.q\xfac\xfc\xf3\"\xefw\xd6\xd4\xf0\xa5Ѹβ\xcb|!\xec\xab[\x1d.\b\x83N\xbf\xf1\xa0\x11\x05H\xdf\x04\x01۰\x9apS5\x98\x1e;\xb9q\xd3\x06\xed\xa9t.\xa0\x9b\xfe\xf4\xf3\xef\x7f\xf7M\x8en\xea\x94\no\x88\xcfeLgG\x92\"\x9a\x86|w\x99\xc2\xccX \x8cD\xd1[\xe5\xf0Lţ/\xb3\xf4\xb5\x1e\xcdQ\xbc\xba\xafD\xee\xb0[\x1d_ț\x00_\xc7\r\xedU\xac\x1e\xa7\xdd̛J\xf2Q[\xda\x14\xda/\xa7\xd8ҏI-$'\xfc~\x99\x1aK\x9f*.ڶ\x1e5\xb4\x1b\xb9\xa1\x82Я\xa6$n\x86CW8~h\xee=w\xf7\xa9:e\x88\xe3\xd0\x13\xb4v\xa0\x91 \x10\xb3]=ǚ\xc0\x8d֔\x8c\xbd\x01v\xaato\\\xbbĿ\xb2@\xac\x02\xdfa\x8f\xe2;\xa2|\x88\x1b\x8b\x8e\xd31\xe2%8\x8c\xb5\xf7\x1a\x1bp=\"8[\xa0\xbd\x85\x97Ŝ6\x9eP\x12\x83\xc5\x1cVA\v\x85\x85\xa3\x18\xef{\xb4r}쿋\xc6\xd3\xfd\xb2h5\x02\xcc\xdc\x1a\x16\xdd\xf6ːJ\xf8\fV\xc7\x1eHx\x83\x90\xb5ŵ\xfcz\x83\x90\x8fqcQx\xcd\xfc\x16\xa4vR \xb0\x1e\xf5'\xac> \xe8\t\xfe=\xe4\x9c\xf3\r\xe6y)7$v^\x93\x1e\x8a\x8e\xaf\xc4\xcfc\xdev\xd2B\xf9\x9d\v\xf8e+0\x14ǽ\x12\xedO\xefN\x7fJ\x00\x9b\xf7 \xa5\vf\x9e\xbb'^\x00\xea\xe5\xf5\xab/\x98\t\x16\x1ak\xd1\xd5F\vj\xabo\x83\xe9g\x96\xffw`\xbd߬\xe3\xcb,\xd7Z+V\xb8\xa9S\x8d/}\xaf\xeeU\xd3\xfbg\xb3\x134+\x87v\xdfhW[2~\x97.\xb5\x17U6ZW\xe9\b\xa2\x05\x1d\xc1{DN\x93Ѩ\xe7\xc8\x1d\xd6\x16\xa9\x84\x89\x19\tg\xe3Im\x0et\xbaA.AO\xa3S\xc1\xa7\xfe\x9di\x91_Nh\xa9\x87\xf2A*E \xc0beH[\xd4zXB\xac,\xe2\xc9\xfdo&\xef\xfe\x7fm\xb1b\xceS\x97\x8b\xe23\xeee\xf7\xf9\xf06}\xdfw\xa8\x94\xf4p\n\x1a\xfa\U00065f28Lm\xde\xf6\x05\xd6R\x11.m\xe4\x8e\x1b\xe0A\xcf\xe3\xf7\x87\xe5\xfd\x1b\x17\xf1$j\xef\xe0@\x16t\x91%j\xecL~\xc5\n\xceS\x15\xb9\xee\x00\xcd.C\x1bPFo\b\x80\xa7'-\x82xɟ\x8c\x05\x81\x9e\xaa\x95\xde\x00\xdf2\xbd\xa1\xd8\xe8K\xfa\x91\xe3\xcc~\x93Qr\x9fA\x0f\x91z\xc0=n\xb2\xe8\x93\xec\xeb\xdb^c\xcd\xe1o\r'\xfe\xb3i\xcfO\xda-\xc5\x0f%\xdbb\x8a\xf6b)\xe6\xa4\xe8\xb1?\x7f\x7f8\x8fo\x7f\x04\xe9~\xdc\xf8V\xf5\xfcW\x9fc:\x9fa~\x15ʩ\b\xe9^\x85ϟҮ\xf4(\x9d\x8f\x00[\x99\xe0{\xaa\x7f\xc3\xe1{\x83:~qz\r\x8f\xf1;\xda5\x80B{\x8aEx\xb06>\\\x97\a\u0558*\xfa\xea\xd2\xed)x\xde\xfa\xdc\xd7\\\xeb~\f\xbcA\xae\xde:ݙL\xb5\xb6a\u05ec\xe4\xe6LX\x9d>G\xcc\xe0\x9f\xff\x1e\xfd'\x00\x00\xff\xff\xa1\a\xb8\x04\xa5\x1e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVMs\xdb6\x10\xbd\xebW\xecL\xaf%\x15O{\xe8\xf0\xd689x\xdaf4v&w\bX\x89\x88A\x00\xdd]\xc8u?\xfe{\a\x00)K\x14\xe5$\x97\xf0&`\xb1\xfb\xf0\xde\ue0da\xa6Y\xa9h?!\xb1\r\xbe\x03\x15-\xfe%\xe8\xf3/n\x1f\x7f\xe1ֆ\xf5\xe1f\xf5h\xbd\xe9\xe06\xb1\x84\xe1\x1e9$\xd2\xf8\x0ew\xd6[\xb1\xc1\xaf\x06\x14e\x94\xa8n\x05\xa0\xbc\x0f\xa2\xf22\xe7\x9f\x00:x\xa1\xe0\x1cR\xb3G\xdf>\xa6-n\x93u\x06\xa9$\x9fJ\x1f\u07b47?\xb7oV\x00^\r\u0601A\x87\x82[\xa5\x1fS$\xfc3!\v\xb7\atH\xa1\xb5a\xc5\x11uο\xa7\x90b\a/\x1b\xf5\xfcX\xbb\xe2~WR\xbd-\xa9\xeek\xaa\xb2\xeb,\xcbo\xd7\"~\xb7cTt\x89\x94[\x06T\x02\xd8\xfa}r\x8a\x16CV\x00\xacC\xc4\x0e>dXQi4+\x80\xf1\xda\x05f\x03ʘB\xa4r\x1b\xb2^\x90n\x83K\xc3D`\x03\x06Y\x93\x8dR\x88\xfa\xd8c\xb9\"\x84\x1dH\x8fPˁ\x04\xd8\xe2\x88\xc0\x94s\x00\x9f9\xf8\x8d\x92\xbe\x836\xf3\xd5\xd6\xd0\fd\f\xa8T\xbf\x9d/\xcbs\x06\xccB\xd6\xef\xafA`Q\x92x\x02Q\xea\xda\xe0\x81N\xf8=\aP\xe2\xdb\xd8+>\xaf\xfeP6\xaeU\xae1\x87\x9bʴ\xeeqP\xdd\x18\x1b\"\xfa_7w\x9f~z8[\x86s\xac\v҂eP\x13\xd2L\\e\r\x82G\b\x04C\xa0\x89Un\x8fI#\x85\x88$vj\xad\xfa\x9d\f\xcf\xc9\xea\f¿\xcd\xd9\x1e@F]O\x81\xc9S\x84\\H\x1c\x9b\x02\xcdx\xd1J\xaee \x8c\x84\x8c\xbe\xceU^V\x1e\xc2\xf63jig\xa9\x1f\x90r\x1a\xe0>$g\xf2\xf0\x1d\x90\x04\bu\xd8{\xfb\xf717\xe7{\xe7\xa2NI\xa1$\xb7\x9dW\x0e\x0e\xca%\xfc\x11\x947\xb3̃z\x06\xc2\\\x13\x92?\xc9W\x0e\xf0\x1c\xc7\x1f\x99D\xebw\xa1\x83^$r\xb7^\xef\xadL\x96\xa2\xc30$o\xe5y]\xdc\xc1n\x93\x04\xe2\xb5\xc1\x03\xba5\xdb}\xa3H\xf7VPK\"\\\xabh\x9br\x11_l\xa5\x1d\xcc\x0f4\x9a\x10\x9f\x95\xbd\xe8\x9e\xfa\x15\x17\xf8\x06y\xb2'\xd4\x1e\xa9\xa9\xea\x15_T\xc8K\x99\xba\xfb\xf7\x0f\x1faBR\x95\xaa\xa2\xbc\x84^\xf02\xe9\x93ٴ~\x87T\xcf\xed(\f%'z\x13\x83\xf5R~hg\xd1\vp\xda\x0eVx\xea\xd8,\xdd<\xedm\xb1\xdd\xec\x00)\x1a%h\xe6\x01w\x1enՀ\xeeV1~g\xad\xb2*\xdcd\x11\xbeJ\xad\xd3\xc7d\x1e\\\xe9=٘\x9e\x81+\xd2.\f\xffCD\x9d\xc5\xcd\xfc\xe6\xd3vgu\x1d\xab] x\xea\xad\xee\xa7\xe1\x9f\xd1t4\x8as\xfe\x96\x8d!\x7f/v;߹zy(\"[\xc2Y\xc36p\xe1ݯ\xf3RL\xf5\x1b\x99\xa9\x8e>r\xa3\x13Qi\xbe\xa3ϫ\xa5C_\xcb\x05\x12\x05\xbaX\x9d\x81z_\x82\xca?\x06e=\x83\xf2\xcf\xe3A\x90^\t Date: Wed, 18 Dec 2024 19:54:03 +0800 Subject: [PATCH 03/16] isolate repo maintenane history Signed-off-by: Lyndon-Li --- .../backup_repository_controller.go | 39 +++- .../backup_repository_controller_test.go | 172 ++++++++++++++++++ 2 files changed, 204 insertions(+), 7 deletions(-) diff --git a/pkg/controller/backup_repository_controller.go b/pkg/controller/backup_repository_controller.go index 90608ad72..aa5b91c77 100644 --- a/pkg/controller/backup_repository_controller.go +++ b/pkg/controller/backup_repository_controller.go @@ -46,8 +46,9 @@ import ( ) const ( - repoSyncPeriod = 5 * time.Minute - defaultMaintainFrequency = 7 * 24 * time.Hour + repoSyncPeriod = 5 * time.Minute + defaultMaintainFrequency = 7 * 24 * time.Hour + defaultMaintenanceStatusQueueLength = 3 ) type BackupRepoReconciler struct { @@ -299,9 +300,9 @@ func ensureRepo(repo *velerov1api.BackupRepository, repoManager repomanager.Mana } func (r *BackupRepoReconciler) runMaintenanceIfDue(ctx context.Context, req *velerov1api.BackupRepository, log logrus.FieldLogger) error { - now := r.clock.Now() + startTime := r.clock.Now() - if !dueForMaintenance(req, now) { + if !dueForMaintenance(req, startTime) { log.Debug("not due for maintenance") return nil } @@ -315,16 +316,40 @@ func (r *BackupRepoReconciler) runMaintenanceIfDue(ctx context.Context, req *vel if err := r.repositoryManager.PruneRepo(req); err != nil { log.WithError(err).Warn("error pruning repository") return r.patchBackupRepository(ctx, req, func(rr *velerov1api.BackupRepository) { - rr.Status.Message = err.Error() + updateRepoMaintenanceHistory(rr, startTime, r.clock.Now(), err.Error()) }) } return r.patchBackupRepository(ctx, req, func(rr *velerov1api.BackupRepository) { - rr.Status.Message = "" - rr.Status.LastMaintenanceTime = &metav1.Time{Time: now} + completionTime := r.clock.Now() + rr.Status.LastMaintenanceTime = &metav1.Time{Time: completionTime} + updateRepoMaintenanceHistory(rr, startTime, completionTime, "") }) } +func updateRepoMaintenanceHistory(repo *velerov1api.BackupRepository, startTime time.Time, completionTime time.Time, result string) { + length := defaultMaintenanceStatusQueueLength + if len(repo.Status.RecentMaintenanceStatus) < defaultMaintenanceStatusQueueLength { + length = len(repo.Status.RecentMaintenanceStatus) + 1 + } + + lru := make([]velerov1api.BackupRepositoryMaintenanceStatus, length) + + if len(repo.Status.RecentMaintenanceStatus) >= defaultMaintenanceStatusQueueLength { + copy(lru[:length-1], repo.Status.RecentMaintenanceStatus[len(repo.Status.RecentMaintenanceStatus)-defaultMaintenanceStatusQueueLength+1:]) + } else { + copy(lru[:length-1], repo.Status.RecentMaintenanceStatus[:]) + } + + lru[length-1] = velerov1api.BackupRepositoryMaintenanceStatus{ + StartTimestamp: &metav1.Time{Time: startTime}, + CompleteTimestamp: &metav1.Time{Time: completionTime}, + Message: result, + } + + repo.Status.RecentMaintenanceStatus = lru +} + func dueForMaintenance(req *velerov1api.BackupRepository, now time.Time) bool { return req.Status.LastMaintenanceTime == nil || req.Status.LastMaintenanceTime.Add(req.Spec.MaintenanceFrequency.Duration).Before(now) } diff --git a/pkg/controller/backup_repository_controller_test.go b/pkg/controller/backup_repository_controller_test.go index 89b63e9ee..4f921ca7f 100644 --- a/pkg/controller/backup_repository_controller_test.go +++ b/pkg/controller/backup_repository_controller_test.go @@ -486,3 +486,175 @@ func TestGetBackupRepositoryConfig(t *testing.T) { }) } } + +func TestUpdateRepoMaintenanceHistory(t *testing.T) { + standardTime := time.Now() + + backupRepoWithoutHistory := &velerov1api.BackupRepository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: velerov1api.DefaultNamespace, + Name: "repo", + }, + } + + backupRepoWithHistory := &velerov1api.BackupRepository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: velerov1api.DefaultNamespace, + Name: "repo", + }, + Status: velerov1api.BackupRepositoryStatus{ + RecentMaintenanceStatus: []velerov1api.BackupRepositoryMaintenanceStatus{ + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, + Message: "fake-history-message-1", + }, + }, + }, + } + + backupRepoWithFullHistory := &velerov1api.BackupRepository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: velerov1api.DefaultNamespace, + Name: "repo", + }, + Status: velerov1api.BackupRepositoryStatus{ + RecentMaintenanceStatus: []velerov1api.BackupRepositoryMaintenanceStatus{ + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, + Message: "fake-history-message-2", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 22)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 21)}, + Message: "fake-history-message-3", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 20)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)}, + Message: "fake-history-message-4", + }, + }, + }, + } + + backupRepoWithOverFullHistory := &velerov1api.BackupRepository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: velerov1api.DefaultNamespace, + Name: "repo", + }, + Status: velerov1api.BackupRepositoryStatus{ + RecentMaintenanceStatus: []velerov1api.BackupRepositoryMaintenanceStatus{ + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, + Message: "fake-history-message-5", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 22)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 21)}, + Message: "fake-history-message-6", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 20)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)}, + Message: "fake-history-message-7", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 18)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 17)}, + Message: "fake-history-message-8", + }, + }, + }, + } + + tests := []struct { + name string + backupRepo *velerov1api.BackupRepository + expectedHistory []velerov1api.BackupRepositoryMaintenanceStatus + }{ + { + name: "empty history", + backupRepo: backupRepoWithoutHistory, + expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{ + { + StartTimestamp: &metav1.Time{Time: standardTime}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)}, + Message: "fake-message-0", + }, + }, + }, + { + name: "less than history queue length", + backupRepo: backupRepoWithHistory, + expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{ + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, + Message: "fake-history-message-1", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)}, + Message: "fake-message-0", + }, + }, + }, + { + name: "full history", + backupRepo: backupRepoWithFullHistory, + expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{ + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 22)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 21)}, + Message: "fake-history-message-3", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 20)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)}, + Message: "fake-history-message-4", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)}, + Message: "fake-message-0", + }, + }, + }, + { + name: "over full history", + backupRepo: backupRepoWithOverFullHistory, + expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{ + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 20)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)}, + Message: "fake-history-message-7", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 18)}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 17)}, + Message: "fake-history-message-8", + }, + { + StartTimestamp: &metav1.Time{Time: standardTime}, + CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)}, + Message: "fake-message-0", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + updateRepoMaintenanceHistory(test.backupRepo, standardTime, standardTime.Add(time.Hour), "fake-message-0") + + for at := range test.backupRepo.Status.RecentMaintenanceStatus { + assert.Equal(t, test.expectedHistory[at].StartTimestamp.Time, test.backupRepo.Status.RecentMaintenanceStatus[at].StartTimestamp.Time) + assert.Equal(t, test.expectedHistory[at].CompleteTimestamp.Time, test.backupRepo.Status.RecentMaintenanceStatus[at].CompleteTimestamp.Time) + assert.Equal(t, test.expectedHistory[at].Message, test.backupRepo.Status.RecentMaintenanceStatus[at].Message) + } + }) + } +} From 3b2c50b459faf2d8a29475d7f7472cd674eaddf1 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 19 Dec 2024 15:54:25 +0800 Subject: [PATCH 04/16] add repo maintain result in history Signed-off-by: Lyndon-Li --- changelogs/unreleased/8532-Lyndon-Li | 1 + .../crd/v1/bases/velero.io_backuprepositories.yaml | 6 ++++++ config/crd/v1/crds/crds.go | 2 +- pkg/apis/velero/v1/backup_repository_types.go | 13 +++++++++++++ pkg/controller/backup_repository_controller.go | 9 +++++---- pkg/controller/backup_repository_controller_test.go | 7 ++++++- 6 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/8532-Lyndon-Li diff --git a/changelogs/unreleased/8532-Lyndon-Li b/changelogs/unreleased/8532-Lyndon-Li new file mode 100644 index 000000000..1cedbd27d --- /dev/null +++ b/changelogs/unreleased/8532-Lyndon-Li @@ -0,0 +1 @@ +Fix issue #7810, add maintenance history for backupRepository CRs \ No newline at end of file diff --git a/config/crd/v1/bases/velero.io_backuprepositories.yaml b/config/crd/v1/bases/velero.io_backuprepositories.yaml index 5ce487be6..f9c18e517 100644 --- a/config/crd/v1/bases/velero.io_backuprepositories.yaml +++ b/config/crd/v1/bases/velero.io_backuprepositories.yaml @@ -119,6 +119,12 @@ spec: description: Message is a message about the current status of the repo maintenance. type: string + result: + description: Result is the result of the repo maintenance. + enum: + - Succeeded + - Failed + type: string startTimestamp: description: StartTimestamp is the start time of the repo maintenance. format: date-time diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 7d5871817..5b42ccb87 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -29,7 +29,7 @@ import ( ) var rawCRDs = [][]byte{ - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccWM\x8f\xdb6\x13\xbe\xfbW\f\xf2^_\xd9\t\xdaC\xe1[\xe2\xb6@\xd0$X؋\xbd\xd3\xd2\xc8f\x96\"Yr\xe8\xd4\xfd\xf8\xefŐ\x92-K\xb4\xb5\xde\x02Ey\x1393\x9cy\x9e\xf9\xa0\x8a\xa2\x98\t+\x9f\xd0yi\xf4\x12\x84\x95\xf8\x1b\xa1\xe6/?\x7f\xfe\xc1ϥY\x1c\xde͞\xa5\xae\x96\xb0\n\x9eL\xb3Fo\x82+\xf1G\xac\xa5\x96$\x8d\x9e5H\xa2\x12$\x963\x00\xa1\xb5!\xc1۞?\x01J\xa3\xc9\x19\xa5\xd0\x15;\xd4\xf3\xe7\xb0\xc5m\x90\xaaB\x17\x8dwW\x1f\xde\xce\xdf}?\x7f;\x03Т\xc1%lE\xf9\x1c\xacCk\xbc$\xe3$\xfa\xf9\x01\x15:3\x97f\xe6-\x96l}\xe7L\xb0K8\x1f$\xed\xf6\xe6\xe4\xf5\x87hh\xdd\x19:\xc6#%=\xfd\x92=\xfe$=E\x11\xab\x82\x13*\xe7H<\xf6R\xef\x82\x12n$\xc0\x17\xf8\xd2X\\\xc2\x17\xf6Ŋ\x12\xab\x19@\x1bi\xf4\xad\x00QU\x11;\xa1\x1e\x9cԄneTh:\xcc\n\xf8\xea\x8d~\x10\xb4_¼Cw^:\x8c\xc0>\xca\x06=\x89\xc6F\xd9\x0e\xb0\xf7;l\xbf\xe9ȗW\x82pl\x8c\x91\x9b\x9f}}\xbcKP\x94{lIJ\x955\x16\xf5\xfb\x87\x8fO\xdfm.\xb6\x01\xac3\x16\x1dɎ\x9e\xb4z\xe9\xd7\xdb\x05\xa8ЗNZ\x8a\xc9\xf1gqq\x06\xc0\x17$-\xa88\x0f\xd1\x03\xed\xb1\xc3\x18\xab\xd6'05\xd0^zph\x1dz\xd4)3y[h0ۯX\xd2|`z\x83\x8èߛ\xa0*N\xdf\x03:\x02\x87\xa5\xd9i\xf9\xfbɶ\a2\xf1R%\b=AdQ\v\x05\a\xa1\x02\xfe\x1f\x84\xae\x06\x96\x1bq\x04\x87|'\x04ݳ\x17\x15\xfcЏ\xcf\xc6!H]\x9b%쉬_.\x16;I]Q\x96\xa6i\x82\x96t\\\xc4\xfa\x92\xdb@\xc6\xf9E\x85\aT\v/w\x85p\xe5^\x12\x96\x14\x1c.\x84\x95E\fD\xc7\u009c7\xd5\xff\\[\xc6\xfe\xe2\xda\x11\xd1i\xc5J\xba\x83\x1e.-\x90\x1eDk*\x85xf\x81\xb7\x18\xba\xf5O\x9bG\xe8\xea\xf28\x11\xe8\xe7\x8c\n\x87\xb47\xdf\xc0Ԅ\xbao\xb4\xf55\x13\xc9\x16\xc1\x05}\x97\xb3\xe7\x18WF\xd7r7v\xb4?Ȯ\x91;q\xc9 \xda\xf5\xe0N\x8e\x94\x93\xeb\xecK\xd1e\x1e\x13R\xcb]p\xd7ȫ%\xaaj\xd4B\x00tPJl\x15.\x81\\\xc0+\x88\x8cj\xe5\x12\x11\x9e\x8f\x13ĭ/\x84Aꊫ\xa5\x1dV|I\x97\x8c\x9c\xfe\xa8+p\x97ϔ\xfeB\x1d\x9a\xf1u\x05<\x1b+Efߡ'Yf\x0e\u07bc\xb9/\x03\xd8\xccNJ\xdbQ-ѽ\xa6&\xd7\x03\x1b]9\xd6A\xa9\xf6\x82\xa24\x8d\x15$\xb7\n\xbb\x99\xc1\x9cˤs\xcc%\r\xfc\xa32<\xf0{\vO/\xb4ׄ\xf5ti\xa2\xdfd\xd2F\xf4/u\xb6\x9e\x9b]\x17\xf1\x19\x93\xd6T\xadg\xad^L\xfd;\x02\xe3\xf6 \x1d\x0e\xa6u\x91\xef\xaf\x03\x99\\g\x1a\x88\f\xb3ap<\x00\xf5E\xf3\x87\x04\x05\x7f\xcf\x04\x8a\n\x1d\xd8ep.N\xf8\xb4\xcb\x0f\xbbW\xcf %<\xf5Z-?\xb3'\xd2\xe2\xd3X\xa3s\x8c\x8d\x01\xf1\x063\xdf\xc76C;'\xbf\xc2̣\x03\x98\xfeFPz\xce\x17l\xefu\xbd,?\x8a\xd0{\xb1\x9b\n\xf2s\x92J\xef\xb9V\x05\xc4\xd6\x04\xba\xc2\x00\xeds1\xdefe\xc2S\xbb\x17~\xca\xcf\a\x96\xc9\xe5\xc5`\xe4\xdfr\xe1Z\x93\xfd\x82\xdf2\xbbk\x14ոQ\x17\xf0\xc5P\xfe\xe8f\x9f-Q\xf7\x93i\x93)\x8cQ\xcc\xeb\xbc\x16\xa3p\xc1Gk>\xe6b\x86\x9b^v\x8e1\x91\x84Mv\xa4_\xaf\xa4\xb4\xba\xac>\xfd\xaf\xe6\xc5\x06!\xad\x86Z'J\xd3\x01?\xe7b]]ʹ\x0e\xd0˲\x1b\a\x96\xd6t\x81\xa55Qfi\xdd|\xe5\xc0\xad\x92\xcb qo\xe1]\x85\"%\xc0\xcb\xe0\x98\x8c\xc0\x93pt\x1f\xa5\x9b\v\x95\xd3\xef\x03\xef\xf6\xa9\xfcORv\xe3)\xd8\x1d\n\xe7\xc4qz֍6=\xff\xc9V=\xe7|\x1a\xcf\xfd\x9d\xb0=\xfd\xa8/\u13fff\x7f\a\x00\x00\xff\xff\xb4\xb6j\xe3\xb2\x13\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccXO\xaf\xdb6\f\xbf\xe7S\x10\xdduNZl\x87!\xb76[\x81bm\xf1\x90\x14\xef\xae\xd8t\xa2>Y\xd2$*]\xf6\xe7\xbb\x0f\x94\xecı\x95\xf8\xe5\r\x18\xa6\x9b%\x92\"\xf9\xfb\x91bR\x14\xc5LX\xf9\x88\xceK\xa3\x97 \xac\xc4\xdf\t5\x7f\xf9\xf9\xd3O~.\xcd\xe2\xf0f\xf6$u\xb5\x84U\xf0d\x9a5z\x13\\\x89?c-\xb5$i\xf4\xacA\x12\x95 \xb1\x9c\x01\b\xad\r\t\xde\xf6\xfc\tP\x1aM\xce(\x85\xaeء\x9e?\x85-n\x83T\x15\xbah\xbc\xbb\xfa\xf0z\xfe\xe6\xc7\xf9\xeb\x19\x80\x16\r.a+ʧ`\x1dZ\xe3%\x19'\xd1\xcf\x0f\xa8Й\xb943o\xb1d\xeb;g\x82]\xc2\xf9 i\xb77'\xaf\xdfEC\xeb\xce\xd01\x1e)\xe9\xe9\xd7\xec\xf1G\xe9)\x8aX\x15\x9cP9Gⱗz\x17\x94p#\x01\xbe\xc0\x97\xc6\xe2\x12>\xb3/V\x94X\xcd\x00\xdaH\xa3o\x05\x88\xaa\x8a\xb9\x13\xea\xc1IM\xe8VF\x85\xa6\xcbY\x01_\xbd\xd1\x0f\x82\xf6K\x98wٝ\x97\x0ecb\xbf\xc8\x06=\x89\xc6F\xd9.aow\xd8~ӑ/\xaf\x04\xe1\xd8\x18gn~\xf6\xf5\xcb\xd1⅕s\"\xa0w\x96,zrR\xeffg\xe1Û\x94\x8ar\x8f\x8dX\xb6\xb2Ƣ~\xfb\xf0\xe1\xf1\x87\xcd\xc56\x80uƢ#\xd9\xc1\x93V\x8f~\xbd]\x80\n}餥H\x8e\xbf\x8a\x8b3\x00\xbe iA\xc5\x81\xa9\x81\xf6҃C\xebУN\xcc\xe4m\xa1\xc1l\xbfbI\xf3\x81\xe9\r:6\x03~o\x82\xaa\x98\xbe\at\x04\x0eK\xb3\xd3\xf2\x8f\x93m\x0fd\xe2\xa5J\x10z\x82\x88\xa2\x16\n\x0eB\x05\xfc\x1e\x84\xae\x06\x96\x1bq\x04\x87|'\x04ݳ\x17\x15\xfcЏO\xc6!H]\x9b%쉬_.\x16;I]Q\x96\xa6i\x82\x96t\\\xc4\xfa\x92\xdb@\xc6\xf9E\x85\aT\v/w\x85p\xe5^\x12\x96\x14\x1c.\x84\x95E\fD\xc7\u009c7\xd5w\xae-c\x7fq\xed\b\xe8\xb4b%\xdd\x01\x0f\x97\x16H\x0f\xa25\x95B<\xa3\xc0[\x9c\xba\xf5/\x9b/\xd0y\x92\x90J\xa0\x9cEGy\xe9\xf0\xe1lJ]\xa3Kz\xb53M\xb4\x89\xba\xb2Fj\x8a\x1f\xa5\x92\xa8\t|\xd86\x92\x98\x06\xbf\x05\xf4\xc4\xd0\rͮb\xe3\x82-B\xb0\\:\xd5P\xe0\x83\x86\x95hP\xad\x84\xc7\xff\x18+F\xc5\x17\f³\xd0\xea\xb7\xe3\xa1pJo\xef\xa0k\xa5W\xa0\x1d\xb6ǍŒ\x91\xe5䲪\xace\x99j\xaa6\x0e\xc4H\xfe2S\xf9\x16\xc0+5\xd1\r\x19'v\xf8\xd1$\x9bC\xa1)\xda\xf1z\x973\xd4y\xccm+\xf5\x04\xcc\vf\f\xd2^P\xaf\x19\x90\x90\xfa\xd4S\xb2A\xde@&\xa2#\xb8Sh\xa1K|\x1f\xf9\xa8\xcb\xe3D\xa0\x9f2*\x1c\xd2\xde|\x03S\x13\xea\xbe\xd1\xd6\xd7L$[\x04\x17\xf4]Ξc\\\x19]\xcb\xdd\xd8\xd1\xfeCv\r܉K\x06Ѯ\awr\xa4L\xae\xb3/E\xc7<\x06\xa4\x96\xbb\u0b81WKTը\x85\x00蠔\xd8*\\\x02\xb9\x80W22\xaa\x95ˌ\xf0\xfb8\x01\xdc\xfaB\x18\xa4\xae\xb8Z\xdaNJ/\xe9\xc8\xc8\xf4G]\x81\xbb\x1cS\xfa\vuh\xc6\xd7\x15\xf0d\xac\x14\x99}\x87\x9ed\x999x\xf5\xea>\x06\xb0\x99\x0f\x15\xb7\xa3Z\xa2{IM\xae\a6\xbar\xac\x83R\xed\x05Ei\x1a+Hn\x15vo\x06c.\x93\xce1G\x1a\xf8Wex\xe0y\vO\x13\xdaK\xc2z\xbc4\xd1o2i#\xfa\x97:[\xcfͮ\x8b\xf8\x8cIk\xaaֳV/R\xff\x8e\xc0\xb8=H\x87\x83\u05fa\xc8\xf7ׁL\xae3\rD\x86l\x18\x1c\x0f\x92\xfa\xac\xf7\x87\x04\x05\x7f\xcf\v\x14\x15\xbad\x97\xc1\xb9\xf8§]\x1e\xec^\xfc\x06)\xe1\xa9\xd7jy̞\xa0\xc5DZF\xe7\x18\x1b\x03\xe2\rF\xbe\x9f\xdb\f\xecL~\x85\x99\xa1\x03\x18\xfeFP\x1a\xe7\v\xb6\xf7\xb2^\x96\x7f\x8a\xd0{\xb1\x9b\n\xf2S\x92J\xf3\\\xab\x02bk\x02]A\x80\xf6\xb9\x18o\xa32\xe1\xa9\xdd\v?\xe5\xe7\x03\xcb\xe4x1x\xf2o\xb9p\xad\xc9~\xc6o\x99\xdd5\x8ajܨ\v\xf8l(\x7ft\xb3ϖ\xa8\xfbd\xdad\nc\x14\xf3:\xaf\xc5Y\xb8\xc0\xa35\x1f\xb9\x98\xc1\xa6\xc7\xceqN$a\x93}үWRZ\x1d\xabO\xbfW\xf3b\x83\x90VC\xad\x13\xa4\xe9\x80ǹXWW\x99\xd6%\xf4\xb2\xecƁ\xa55]`iM\x94YZ7\xa7\x1c\xb8Ur\x99L\xdc[xWS\x91\b\xf0\xbctLF\xe0\xd0\aE\xcf\n`\x1dE;\xfc\x92♐\xcf\xf3'_\x91i\x15\xb0\te\x89Xan\xdcM\x12\xef\x85TW\x8f'\x83\xf5$\x1c\xdd\xc7\xdfͅ\xca\xe9\xb7\x12\xef\xf6y\xfb\xbf\xe4獹\xb7;\x14Ή\xe3\xf4\xc3>\xda\xf4\xfc\xb3\xbd\xea9\xe7\xd3,\xd2\xdf\t\xdbӿ\x12K\xf8\xf3\xef\xd9?\x01\x00\x00\xff\xff\xc6⍽\x9f\x14\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec}_s\xdb8\x92\xf8{>\x05ʿ\x87\xd9ݒ\xecI\xfd\xf6\xe1\xcao\x19'\xb9Q\xedL\xe2\x8a=\xd9g\x88lI\x18\x83\x00\x17\x00ek\xef\xee\xbb_\xa1\x01\xf0\x8f\b\x92\xa0,{\xb2{\xe1Kb\x11l\x00ݍ\xfe\x0f`\xb9\\\xbe\xa1%\xfb\nJ3)\xae\t-\x19<\x19\x10\xf6/}\xf9\xf0\x1f\xfa\x92ɫ\xfd\xdb7\x0fL\xe4\xd7\xe4\xa6\xd2F\x16_@\xcbJe\xf0\x1e6L0äxS\x80\xa195\xf4\xfa\r!T\bi\xa8\xfdY\xdb?\tɤ0Jr\x0ej\xb9\x05q\xf9P\xada]1\x9e\x83B\xe0\xa1\xeb\xfd\x8f\x97o\xffz\xf9\xe3\x1bB\x04-\xe0\x9a\xaci\xf6P\x95\xfar\x0f\x1c\x94\xbcd\xf2\x8d.!\xb3 \xb7JV\xe55i^\xb8O|wn\xa8?\xe1\xd7\xf8\x03g\xda\xfc\xad\xf5\xe3/L\x1b|Q\xf2JQ^\xf7\x84\xbfi&\xb6\x15\xa7*\xfc\xfa\x86\x10\x9d\xc9\x12\xae\xc9'\xdbEI3\xc8\xdf\x10\xe2G\x8d].\xfd\x80\xf7o\x1d\x84l\a\x05uc!D\x96 \xdeݮ\xbe\xfe\xff\xbb\xceτ\xe4\xa03\xc5J\x83s\xff\xefe\xfd;\xf1\xa3$L\x13J\xbe\xe2\x1c\x89\xf2('fG\rQP*\xd0 \x8c&f\a$\xa3\xa5\xa9\x14\x10\xb9!\x7f\xab֠\x04\x18\xd0-x\x19\xaf\xb4\x01E\xb4\xa1\x06\b5\x84\x92R2a\b\x13İ\x02ȟ\xdeݮ\x88\\\xff\x0e\x99ф\x8a\x9cP\xadeƨ\x81\x9c\xec%\xaf\np\xdf\xfe\xf9\xb2\x86Z*Y\x822, \xdd=-Nj\xfd:6W\xfbX\xf4\xb8\xafHnY\nܴ<\x8a!\xf7\x18\xb5\xf33;\xa6\x9b\xe9#\x93ٟ\xa9\xf0ÿ<\x02}\aʂ!z'+\x9e[N܃\xb2\b\xcc\xe4V\xb0\x7fְ51\x12;\xe5Ԁ\xb6\x981\xa0\x04\xe5dOy\x05\v\x8b\x94#\xc8\x05=\x10\x05\xb6OR\x89\x16<\xfc@\x1f\x8f\xe3W\xa9\x800\xb1\x91\xd7dgL\xa9\xaf\xaf\xae\xb6̄\xf5\x95ɢ\xa8\x043\x87+\\*l]\x19\xa9\xf4U\x0e{\xe0W\x9am\x97Te;f \xb3d\xbe\xa2%[\xe2D\x04\xae\xb1\xcb\"\xff\x7f\x81=t\xa7[s\xb0l\xab\x8dbb\xdbz\x81\xebc\x06y\xec\xd2q\xcc\xe8@\xb9)6T\xb0?Y\xd4}\xf9pw\xdffT\xa6=QZ\xfc:D\x1f\x8bM&6\xa0\xdcw\x1b%\v\x84\t\"w\xac\x8a|\xce\x19\bCt\xb5.\x98\xb1l\xf0\x8f\n\xb4]\x03\xf2\x18\xec\r\xca \xb2\x06R\x95\xb9e\xe3\xe3\x06+Anh\x01\xfc\x86jxeZY\xaa\xe8\xa5%B\x12\xb5ڒ\xf5\xb8\xb1Co\xebE\x10\x90\x03\xa4u\x82宄\xac\xb3\xd0\xecWl\xc32\xb7\x9c6R5r\xc7\xc9\xc0.\x86\xe2K\xdf>\x99fw\x82\x96z'\xcd=+@V\xe6\xb8\xc5\x14\xaf!\xf1\xeeVGP\xc2\b\xfdxQfU\x1ar\xbbh\x1f)38曻\x15\xf9\x8a\xc2*|\x8dB\xab\xd2\xc4TJX.\x89\xf4\xf5\x05h~\xb8\x97\xbfi y\x85̝)@<,\xc8\x1a6\x96\x13\x14\xd8\xef\xed+P\xca\xe2F\xe3\x00d\xd5\x136\xf6\xb9߁\xc5-\xad\xb8\xf1\xeb\x84i\xf2\xf6GR0Q\x99\x1e\xab\rR\x1d1E\r-\xe4\x1e\xd4)H|O\r\xfd\xd5~|\x84;\v\x94 T\x8b\xbc\xb5\xc7\xe3\xfa\x80/c\xd4v\xcfjӂ\xc84\xb9\xb8 R\x91\v\xa7\x81/\x16\xee\xeb\x8aq\xb3d\xa2\xdd\xc7#\xe3<\xf42o\xf2\x0e\x87\x8e\xa0\xfa^~ԎyO\xc2\xc5\x00\xac\x16j\x1ew`v\xa0H)k\x8d\xb7a\x1c\x88>h\x03\x85GL\xd0\"~>\x91\x9ep\xedp\xeeAh\x8bW?\x91\xfe\xe4E\xc59]s\xb8&FU0\x80\x9b\xb5\x94\x1c\xa8\x98@\xce\x17Іe\xe7@\x8d\x83\x14A\x8c\xf2/:\x18@\xa5I\x1f\x80\xd0\bh\x8f3\xab\x9d9o!\xb6\x8b\x957\xd1A\x95\n2+\xb6\xaf\xbd:`\xc0Q\x05\tI\xb8\x14[P\xae{k\xaa\x04\x0eS`9.'V\xd2*\xe0V\x9d\x90Me\x85\xf0%\xb1\xcb{\x90\t\x98\xd0\x06h\x84;\x9fA x\xcax\x95C~\xe3,\xaf;k@\xe6\xc1l\xee\x89\xcd\x14B}\x18\x85\xe8\xd53g\x19Z\x81\xde\xe0[\xa2\xe1\x1a\xe3\xd3FK\x1fJp\xb6\xb3\xa5\xa5\x1fv\xa3~G\x05\x82\x06c?\xba\xf8\xcb\xc5\x02I\xdc\xed\xb5ۇ&TA\x8d\x96d\xc1\tEi\x0e\xfd\xd6\xcc@\x11\xc1\xe2\xa8@I\xa4'U\x8a\x1e\x06\xa8Y;\x00g\xa4\xe7\x10\xcc#\x8a\x8a\xd0\xec\x95iz\xdc\xef\xbf3U\xcfCG\x8d\xee.e\xc2\xd2\xcfz\x9e\x1d\xf2i\xe7\xc0Y\xb4\ti\"\xf0\x98p\xf0\xd07\x1b\xa1\xd6\x1f\x84\xac\xb3\xf0\xfc\x10\x93\u05fc\xe5\x99\xf7_\x12S;)\x1f\xa6\xb0\xf3\xb3m\xd3xE$ð\nYÎ\xee\x99T~ꍮ\x85'\xc8*\x13]\xf5Ԑ\x9cm6\xa0,\x9crG5h\xe7'\x0f#d\xd8~'-1\x12}y4\x8f\x86\x90\x96L8\xf3\xa1\xa1[C\xe2XK\x86\xc7\x0e\xd4\xdaר\x8cs\xb6gyE9\xeae*27\x1fZ\x8f+&eF\x88\xdc\x1bs\x943\xdd\xe3\f\x820)K\xa4\x8e\xab$\x05X\xa3\xb7\xb0NA\xbf\xe9\xf0\xcc\xd7\xd4\xda*rh\xf6\x04\x89\xa5*\x0e\xdaw\x95\xa3\x1d\xd9ȌEC\x14\x8cD\x10N\xd7\xc0\x89\x06\x0e\x99\x91*\x8e\x91):\xbb'E\b\x0e 2\"\xf9\xba\xaeF3\x81\x11\x90\x04}\xb8\x1d\xcbv\xceԳL\x84pH.\xc1\x1a|\x86в\xe4\x11u\xd1<\xa3\xc4\xf7\x9d\x8c\xad\xf5\xe6\x99X\xf5\xc7\xf0b\xeb\xbfy\x12df\xf3DQ۬\xaf.fkv\x88;\xb5\xcd\xf3\xef\x89\xd8 \xf9O`ڑ\xd5O0,\x94\xccӃ|k\xb1\xca@_Zs\n-\x9d\x05a&\xfc:\xb5\x12:6W/Z\xd6A·M\x9b\xf9L\x9fH\x9a\x945\xf1B\x84\xa9\xbb\xf8\x17\xa4\v\xaa\x8c;\xaf1\x92i\xf2K\xfb\xab\x05a\x9b\x1a\xe9\xf9\x82l\x187\xa0\x8e\xb0\x7f\x92\xa8\x0f\x949\a2R\xb4\x1e\xc1\xf8\xbd\xc9v\x1f\x9e\xac\t\xa6\x9bTU\"^\x8e?v\x86l\xb0\xf6\xbb\xeay\x02.\xc186SP`|\x1c=\xa6\xf6/hZ\xbd\xfb\xf4>\xee_\xb5\x9f\x04\xce\xebMdbѹ\xe7\xddь\xda\xe3\xf3&|x\x836P\xed\x00\xb9\\ȂP\xf2\x00\ag\xbaPA,}hh\x9cн\x02L\xca \x9f=\xc0\x01\xc1ij,\xfd'\x95\x1b\xdc\xf3\x00\x87\x94fG8\xb4cb\xdag\x8f,\x9e\xec\x0f\x88\b\f\xae\xa7\xb2\x81{\xfcR\x88\xe44\xe2O\xa2,\tO\xc0\xfd\t\xd3Lb\x95v\x1f\xed4%r\xc0\x0f\xda\xd1Ү\x98\x1d+Q\xacb\xc4An\x92\tꞯ\x94\xb3\xbc\xeeȭ\x91\x95X\x90O\xd2\xd8\x7f><1\xed3\x99\xef%\xe8O\xd2\xe0//\x82Q7\xf0\x97ħ\xeb\x01\x17\x9apR\xde\"\xac\x9d\x8bs:\xcdr[\x8d{\xa6\xc9JXwš$\xb1+L\xbb\xba\xee\\GE\xa51\x8d&\xa4X\xba\xb0M\xac'\x8fo\xa9:\xe8~v\xa7\xbe\xc3{\xab,\xdc\x1b\x97\xfc\xe54\x83<\xe4k0+I\rlY\x96\xd8_\x01j\v\xa4\xb4\"<\x8d#\x12\x05\xab\x9f\xcd<\xf6I\xd3\xde\xed\xe7i\xf9P'\xf9\x97V\xe5,=\x04#\x8b\x04\x1cxٝO\xcfgi\xd7lB\xab\xc0\t\x93M\a\x92\x96\xc3MS\x90\xf2\ft\xa0\x16G\x13g\x92\xba4ϱЅ\xf2\xdb\x19\x1ae\x06/\xcc\x15\r\xad\xb1;\x15\\P̵\xfc\x97մ\xb8\x9a\xfe\x87\x94\x94)}I\xdeaM\v\x87\xce;\x1f4k\x81I\xe8\x12kR,\xff\xec)\xb7\xba\xdf\npA\x80;K@nzvт<\xee\xa4vj\xbbN\xe2\\<\xc0\xc1\xa5\f'\xbbl\v\x99\x8b\x95\xb8p6DO`\xd4\x06\x87\x14\xfc@.\xf0\xdd\xc5sL\xa9DNMl\xd6aт\x96i\x1c\x8a5E\xa9\x86\xbauX\x83\x11b?\xacke\xac\x91=6\xdb$\x16-\xa5\x8ed\xf2\a\x862\xc1\xbc\xb7R\x1b\x17/\xeb\xd8\xccр\x9a\fA4B7\xae\x80I\xaaPmb\x85\xf2T\xe8\xb7\xfd\xdc\xef@\x83\xcfW\xf8\xc0\x9c\x03j=\xbb\x8bf};i\x7f\xe1\xf2%\xd8\t\xcd\xd0b\xc1oK%3\xd0\xd1dv\xf3$\xe8\x8bHYF{\xeeȗ:/\xc9\xd5d\x8c\x87@Ón\xf2ZD\xcc\xf4\x17><\xb5\x02\xa2v\xedۿ\xa7xl\xee\xb8\b\xd6\f\x16\x05=\xaeSJ\x1a\xe2\x8d\xfb2\xac\x06\x0f\xc89\x1fj[\xa1$H\xd5\xe55\x03~\v\x86B\xc1\xc4\n; o_\xc0\xb0\xf024Vm\x12{N3eoB'\ru\xea\x1f\xdcR.%\xa6\n\x14t\x88\u05cf\xaa\xa3\x1d*\xa4i\x05$f\x98\x9b\xa5\xcc\x7f\xd0dÔ6\xed!\xe8\x81:\x95(\x98\x99\x8e\x97\xf8\xa0\xd4I~\xd7g\xf7e+ܵ\x93\x8f\xa1>\xcb!&q\xe6\x98_\x02\xc26\x84\x19\x02\"\x93\x95\xc0\x00\x8e]\xc7\u0605C\xae\x93\xb0,u\x91\xa4\xad~\xfb\x80\xa8\x8a4\x04,\x91S\x98\x18\x8d\xf4\xb4\x9b\x7f\xa4\x8c\xbf\x04\xd9\xccP\x19[\xec9mM\x84\x1a\xb7vE^A\x9fXQ\x15\x84\x16\x96F\xa8\xccY\x01]\xa27\x95o\xf6\vT\x13F\xda\x15Sr0\xe0\xab\xd7\x12ǐI\xa1Y\x0e\xb5r\xf5\x8c \x05\xa1dC\x19\xafT\xa2\x04\x9c\x85\xde9\xae\x88\x97\x04\xe7\xf31\xd2:_\"*\x12\xa2\xb9\x89\xb6\xe2\xb84.U\xba\xc57ef)\x98oe\x95\x8aI\xac\v<\xb3\xa1\xe5+)\xa98|\xb7\xb4R\x87\xfa\xdd\xd2\x1a{\xbe[Z\x13\xcfwK뻥\x95\xd2\xf2\xbb\xa5\xf5\xdd\xd2j?\xff',\xad\xa9\x11\xb9\r}\x03/'G\x91\x90\xaa\x1e\x1b\xe2\b|_\\\xe1k\xc0\x9fU\x8b\xb9\x8a\x83\x8aT\xfe\x0f\x94uDŽV\xa3<\xea\xe2L\xbbj\x02ϻ\xfdE\x13\xa6\xe43\xaa\xeeC\xa7竺_\x8dBΠ\xcdw\xe4s\xe9bO\xb3\xed\x8e\xf1\x98JZ1\xe1\xc9%\x84\xdd\x12\xc1\x01\xbbtn\xb6\xfb,[&\xfe\x90\xd2\xc0\xf9\x05\x81)\x11\xb1\x89\xe2\xbf\x13J\xfe\x12k\x8b\x9f\x9d\x9eO)\xea\x9b\xe31\xbfX\x01\xdf\xf9\xcb\xf6\x92\xf03]\xa27\a;/^\x8e\xf7\x8aEx\xafSz\x97Xpw\xbe\xca\xf9\xb4x\xecI\x95cӡ\x83ᢹ\xc9R\xb9\xc9\xd0\xc2\xd4\xc4fOi\xb2\x04nN\xe1\xdb$uҖ٫\x95\xb6\xbdZA\xdb떱\x8dr\xd1\xe8\xcb9\x85j\xf1siȤ\xb2\xe5\xaf\xc5l\xa7\xa2A\xaa\x8e\xf9z\x92\x7f\xf5\xf9\b\x86%|0\xed^\xc9F.*nX\xc91\x91\xbagy4\xd8`vp\xa8\x0f\xd0\xf8]\xe2\xd6S\x7f\x14\xcc\xe7/5\xd7^\x1eY\xfaT\x93G\xe0\x9c\xd0غ\xea\xcd\x9b2\x90SwK\xbd\xac\x8b3\xdfə\xb4*\xd2-\xbf?h\x17\xd4)\xbb\x9f\xd2\n\x00&w;\xbd\x94\xcb3\xe5\xf4$\xdbyi\xbb\x99\xe6%\v_p\xf7\xd2K\xecZJ\xc4T\xca.\xa5yxz\x85]I\xaf\xba\x1b\xe9\xb5v!%\xef>J*qI\xce\x02\xa7\x96\xa8\x9c\xb8\x9df:\xc7;\xbe\x9b(a\x17QB\xf6wz\x92'L/a\x97м\xddA\t4K]\x8a\xaf\xb8\v\xe8\x15w\xff\xbc\xf6\xae\x9f\tΚx=ow\xcf\xc9)\v\xa9rP\xa3i\x9fT.\x1c\xe5\xbf\x14ߦ;\x90\xa3|G8\xf6϶\xea\xd8˨\x1e\xfcI\xa3x\xa6\xecP\xfa\xd2rZ\xcb\xda\xe8\xe4\xa2\x1a\xf3\xa7kL\xfa\x83f]\xbaJCI\x15\x1e^\xbc>\xb8r\x96\xa8j\xfe@\xb3\xdd\x11\xf4\x1d\xd5d#UA\r\xb9\xa8\x13\x80W\x0e\xb8\xfd\xfb⒐\x8f\xb2\xae\x89h\x9fˣYQ\xf2\x83\xf5P\xc8E\xfb\x83\xd38 \xcam\xa1\xb7[\xc9Y\x16\xb1ݢg3\xb9ƽ\xc32\xf0Ĩ\xac]2Pچq\xd3\rͼ\xee\x19\x98\x1bɹ|\x9c\xe9\xfbӒ\xfd'\x1e\xdd\xfd\x8c\xe8л\xdb\x15\xc2\b\xec\x81g\x81\xd7\xc5Y\xf5l\xd6`\xd5r3ϡ\xb5\xbf\xdat v\xeb\x1cۧ\xe3B\xee\x0eB\x0ef\x81\x17\x9d\x99\xb4\xd2\xe5v\xe5\xc61ԋ\xe5\x19*\x0eDbE\x8d\xd91\x95/K\xaa\xcc\xc1\x15j,:c\b\xbat,\xba3\xa8=\xfa\x87;G\xd1\x1b\xcet\xc6\f\xe5\xa1\xec&}\x8fqw\xca8\x86w/N\xee[<\xe38\x86͒%b*\xf2s\xb4\xf2\xeblQ3\xed\x8f&\xfeU\xee\xe1}4z\xd6A\xcf\xddQ\xf3HyV\x80\xe8N\xdd\x1d\xacR]\x03\x9e\xc8\xdb\x7f\xf5\x8cz\xabе?S\xf5\x94@\xd9]\x17Dd~\xe1\x88\xd9\xd0YL>\xe1\t\xf0\ar\xfb\x15}\xb4Z\xb4\xf9%\xea}\xb4\x10*\v\xc9\xe0\b\x1c\xff\xc1O\xe7/M\xd3F*\xba\x85_\xa4;d{\x8a\xec\xdd֝\xc3\u05fd\xd5\x13\xeaGâ\x89\x9d\xc0\xeb\x8f\xfb>\x02\xd6\xd4|\xf7N5\xb6\xa3\x9cyN\xb31\xfc\x14\xba\xdf\xdf\xff\xe2feX\x01\x97\xef+W\xee`e\xa2\x06\x8b\xe20[\aim\xff\xbb\x93\x8fx\xf8o<\x8e\x19.Mh&\xa3\x00\x8bͱ\x04q֔\xaa\x92K\x9a\x83\xba\x91bö\x13\xb3\xfb\xad\xd3\xf8H\xcdf\xf8\xa3\x9f\\\xad\xa3\x02\xfc3\xd7 X\x9b\x87s\xe0\x1f\x19\a톕 \x80o\xfb_\xd5\xf2\xb8*\xd6Ά\xdbؗu\a\x03:\xceM\vC\xd1%(kE\xb9\xa0u\xa5\x03\xaf\x0eO\xbc\xa1\b\x13\x06\xb6\xd0\xf7\x02G$\xf0\xbes\xe8{\xe0\xf3)q\xf45\xfeUˬl\xad4gW\xcaMd\xe0CpZWh<2㏼:\xef\x19\xa5C\xce\xc2\xd0\xe5\x00x\x1a\xfe\xf4\xf5\x00\xee\xd0|\x7f\xa9\x88g\xe4J\xe1\x01\xa3\xfe@}<\x90\xf3\xa4\x1b\x02\xd6u\xa9S]6\xa5\xdf\x19\x03EibZzZ\x90\xfc4\x06\xb0\xb6p\xa4\xa1\xbc\xc5\xcf44\x88٨\xfa \xb2\xb1\x92,\xbf\x8eG\xa89\xc6\xc91\x04\xdc\xf8\x9d\x04gC@\rp\b\x01\xba\xca2\xd0zSq~\xa872|#\xd8\xf8H\x19?\x1f*\x1c\xb4AF\xb0\xd3\x1b\x8549a_(\r\"\x0f+=l\xf2\x99\x87\nO\x05_G\xa8\r-N\xba\xeb\xe0\xa6\x0f\x06o\xbbQy\xab\x1c\x91\xd6c\xa7\xba!\x7fL,7\xe0ܗ\xe8\x9eXh\x90\x13\u0603 V\xaf9\x14\x87\xeb\x9afB\xf1{C\x9dn\b\x9a\"\x04\x11\xa2w\xfa\x10\x1f'\xd0xw\xcc\x0f\xba\x86\x89U\x96x\x15H\x1f\t}\xb3\xd1\xf9\xf9\xd7\xd6n\x86\xa5\x05q\x9a\xbd\x17\x95͙f]\xbd\xf0\xd4\r-nІD\xea4ע\x01g[f\xbd\x04K\xb9-Uk\xba\x85e&9\a\x94\xd6\xfdq\xbd\xe4Z\xf7\xbb\xf6\xbe\x00ՓS\xfb\xd8n\xebsg\x8e\xda.eL]\xa18^|e\x98\x82\xe6\x0e\xbaހ$v<˱qX\x88^\xd0\xd6\x1fi\xbbmXu^,\xfb\b\xa9\xbf\x9fm\xe1=\xea8?\x16\xf4w\xa9\x16\xa4`\xc2\xfeCE\xeeR_\xe1\xe3Y\xe3\xdfI\xf9p\x171b{\x83\xff\xb9n\xd8$\t\x98p\xc3ƭ\x96kY\xf9\xbcum\xd0\xc6\x13\x12x\xa6\xfd\x99\x1d5\x849\xa2\x0fz\xd3\x19\x8c\x85\xfe܁4\xa9\n\\\xcf\x03\xb0\xee\xc2%`\x9c\x1f\x16ǐ\x8f.\x1cl`\xb7\xce\xfc\xf7f@\xb3\x93\x7f\xa0\xa3\x90ˉ\x02\xa9\x8f\x8ch\v\xf4S\xfcE\x8f\xe6!c\xb2\x87㟛\xd6Cxt\xc3l\x99{\x03\x13\xec\x18\x81\xe7uu\xf1\x82\x87\t濵m\xea]\xff-\xc7-\xd4W\rƷ\xe2\xbbƗ\xe4\x13\xf4\x03\xfdn#8\xe4XӀ\xab*\xd2d%n\x95\xdc*\xd0}\xa6[\x92\xbfSf\x98\xd8~\x94\xea\x96W[&>\x0foz\x19k|K\x95a\x96i\xddxb\x03e\x82r\xf6Ϙ|j\xbf\x9c\x06t3\xe8(-I\xc20\x86^\xbc\ak\xab\x0e\xfa\xf7QQXz\xbc\x9ebw\x04\x9aL\xc9\xc6\xda&hl\x8a\xd0\xed%\xf9$\xa3\v\xdc\x17\x04\xb1.LkZ\x816K\xd8l\xa42._\xbb\\\x12\xb6\tA\x04+;0r\xe4\xaek$,\x96h\xadK-\x1a5\x84a_\x85\xda\x14\x0fs/\xe8\xc1\xe5fh\x96U\xd6R\xba҆\xf2\x88\xa1\xf2,\x01\x8e\xd1\x1a\xbb\x88 \xff\xedY\xb9\xacU\x1bP?\xec\x86\xfd8\x94\xe2q\x12\xcez\xe3v\x8a ȣb\xc6X\xdbH\x8e$\xd3=\xaa\x8c\xb5\x918'ڢ\xfa\xa4\xf8\x1bq\xe2p5\\\x94\x926\xe5\xfb\x1aʐ\x98\xf5\xb3\xc6\xcb\t\u05c8\x1bb\xedW\xac\xbf\xf1\xad,\x99\xb3\x1d\x15\xdb\xc1=\xfa;%\xab\xed.p\xf2\x80QL\xf2\n0\\\x89\"E\x87\xbbuM\xa5D+\x99>\xb2\xf1\x99\x04f\xc0\xe1\xd2\xec\x81T\xe5\xc2\xdf]\xeb\xaf&\xbe\xf2\xb7\x80,7J\x16K\xdf/\x16\xd0-|.[1i-\x10\xb3\x8bb\x9d8\xeb\xdb\x1f\xb4\x8f\x9cP\x96 \bվ焳\x92NV7\xdaPe\x9e\x15\x8e\xb8\xeb@\x98\x88D`w\xf1I\xdc\xf9\x94\xbe;,\xea\xc6\xdf\xd1Y\x03^\x10\xcdD\xb8\x1dٕ\a8\xfe\x88f\x8b\x04\xdef(U\xbcbo<\xb4Н\xd0\xebF\x15\xf6\xb5\xae\xfdp\xb2\xd3\xf9\xf5\b\xc6\xd1\xc6_\xbc\xbc\xb2n\x12\x1c\xc5?\xb1X\xe4\x1bK=3;\x95?\xff\xe1\x1bz\xf7INM\x1c#c>\x0e\xba/\xc3\xceJ\xf7\xae\xca[\x0e\xd6\xf4\xd2\x00]\xf7i\x96\x9b\xbc?c\xdc\xe8\x9cA\xa3p\x0f\xf8y\xa2&\xfb3\x86\x8b^,Vt\xde)?R\xbcE\xf8\xa4U\xfbw\xffm$X\xe4\xc1\x9e;\\Ԋ\x16\x85\x81\xbfj\xbc(\xaa\x95z?\xa2\x9c\xce[\xd2\xc2\xf7\xe4\x7f\xf9\xdf\x00\x00\x00\xff\xffU\xf5M\xfa݀\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccYK\x93Z\xbb\x11\xde\xf3+\xba\xee]xc\xc0\xceM\xa5nؤ0\x93T\xb92\xceL\x99\xc9dk!5\xa0\x8b\x8et\xa2\a\x98<\xfe{\xaa\xf5\x80\xc3y\f\x8c\x93r\xae63\xe8\xd1\xeaw\x7f\xad3\x1e\x8fG\xac\x96\xcfh\x9d4z\x06\xac\x96\xf8գ\xa6_n\xb2\xfb\xd9M\xa4\x99\xeeߏvR\x8b\x19,\x82\xf3\xa6\xfa\x8c\xce\x04\xcb\xf1\x0e\xd7RK/\x8d\x1eU\xe8\x99`\x9e\xcdF\x00Lk\xe3\x19M;\xfa\t\xc0\x8d\xf6\xd6(\x85v\xbcA=م\x15\xae\x82T\x02m$^\xae\u07bf\x9b\xbc\xff\xed\xe4\xdd\b@\xb3\ng\xb0b|\x17j\xe7\x8de\x1bT\x86'\x92\x93=*\xb4f\"\xcd\xc8\xd5\xc8醍5\xa1\x9e\xc1y!Qȷ'\xce?Db\xcbD\xec>\x13\x8b\xebJ:\xff\xe7\xe1=\xf7\xd2\xf9\xb8\xafV\xc125\xc4V\xdc\xe2\xb6\xc6\xfa\xbf\x9c\xaf\x1e\xc3ʩ\xb4\"\xf5&(f\a\x8e\x8f\x00\x1c75\xce \x9e\xae\x19G1\x02Ȫ\x89\xd4\xc6\xc0\x84\x88\xcaf\xea\xd1J\xed\xd1.\x8c\n\x95>\xdd%\xd0q+k\x1f\x95\x99d\x81,\f\x14i\xc0y\xe6\x83\x03\x17\xf8\x16\x98\x83\xf9\x9eI\xc5V\n\xa7\x7fլ\xfc\x1f\xe9\x01\xfc\xe2\x8c~d~;\x83I:5\xa9\xb7̕\xd5d\xa3\xc7ƌ?\x92\x00\xce[\xa97},\xdd3矙\x92\"r\xf2$+\x04\xe9\xc0o\x11\x14s\x1eQ\x89:\xea\xe7Tu\xee\xba`\x9bX\x81\xe7\x16\x95\xc4?\xcdd\xee\x1bd\x8b\x7fO\xb8\xc5\x13I\xe7YU_Нop\x88\u0605*\xeep͂\xf2MQ\xc9J\xaa闗b\xd5\xc8'\"\x9d\xba\xb8\xf1\xeeb.ݺ2F!KTҮ\xfd\xfb\xe4\x85|\x8b\x15\x9b\xe5ͦF=\x7f\xfc\xf8\xfc\xd3\xf2b\x1a\xfa\x1c\xa9\x15\x14d8ְ\xcd\x16-\xc2s\x8c\xbfd7\x97E;\xd1\x040\xab_\x90\xfb\xb3\x11kkj\xb4^\x96`I\xa3\x91\x8b\x1a\xb3-\x9e\xfe5\xbeX\x03 1\xd2)\x10\x94\x940\xf9U\x8e\x1f\x14Yr0k\xf0[\xe9\xc0bmѡNi\x8a\xa6\x99\xce\fNZ\xa4\x97h\x89\f\xc5vP\x82r\xd9\x1e\xad\a\x8b\xdcl\xb4\xfclj\xb6\x03o\xb23{t\x1eb\x84j\xa6\xc8Y\x03\xbe\x05\xa6E\x8brŎ`\x91\ue120\x1b\xf4\xe2\x01\xd7\xe6\xe3\x13E\x83\xd4k3\x83\xad\xf7\xb5\x9bM\xa7\x1b\xe9K\x86榪\x82\x96\xfe8\x8d\xc9V\xae\x827\xd6M\x05\xeeQM\x9d܌\x99\xe5[\xe9\x91\xfb`q\xcaj9\x8e\x82\xe8\x94R+\xf1\xa3\xcd9\xdd]\\\xdb\t\xe94bJ}\x85y(\xbd&\x97I\xa4\x92\x88g+\xd0\x14\xa9\xee\xf3\x1f\x97OP8I\x96JF9o\xed\xe8\xa5؇\xb4)\xf5\x1am:\xb7\xb6\xa6\x8a4Q\x8b\xdaH\xed\xe3\x0f\xae$j\x0f.\xac*\xe9\xc9\r\xfe\x1e\xd0y2]\x9b\xec\"V1X!\x84:&\x89\xf6\x86\x8f\x1a\x16\xacB\xb5`\x0e\xbf\xb3\xad\xc8*nLF\xb8\xc9Z\xcd\xda\xdcޜ\xd4\xdbX(5u\xc0\xb4\xbd\xd9`Y#\xbf\x88;\x81NZ\x8a\f\xcf<\xc6\xe8j)(\xa7\x8a\xe1\xa2\\F\x7f\x92\xa0\xc18G\xe7>\x19\x81\xed\x95\x16\xcb\xf3\xd3\xc6\v\x1ek\xb4\x95t\xb1\xbc\xc2\xda\xd8v\xe5a\xa7L\xde\x1c%\xe3\xb5\r\x0e\x80:T]F\xc6\xf0\x19\x99x\xd0\xea8\xb0\xf47+}\xf7\xa2\x01C\xd2H,.\x8f\x9a?\xa2\x95F\\\x11\xfeCk\xfbI\x05[s\x80u\xf4\x7f\xedՑr\x97;j\xde\xcd\xdae\xcc\x1f?\x96\f\x9eb+\af\xd6\xd5\x04\xe69\xa8\xcd\x1aށ\x90\x8e\x80\x84\x8bD\xbb\xca\xd2AE\xa01\x03oë\xc4\xe7F\xaf\xe5\xa6+t\x13\x1b\ry\xcc\x15\xd2-\xcd-\xe2M\x94\xb5\xc8;jk\xf6R\xa0\x1dS|ȵ䙓`S\x05YKT\xa2\x93\x9b\x06\xa3,\x8abQPP3uņ\x8b\xd3ƈ\xa4\x99\xd4Ƀ\xcf\x04b\xae\xb1U.\xcdڣ\x16خ6\x91\x1b\x13\x13\x9aC\x01\a\xe9\xb7)S\xaa\xbe\xb8\x83\x17c\x8f\xc6\x0e\x8f}\xd3-ޟ\xb6H;S\xe1Ep\xc8-\xfa\xe8m\xa8\xc8}ȕ&\x00\x9f\x82\x8b\xb9\xb6\x9d'ʈ\x80\xaf\x9c\xdeᱫh\xb8f\xdc\f\x85\x06X\x8e j\x06?\xfcp]\xa4Nu+\x83\xa0{\x11\xd4\xe2\x1a-\xea\x0e\x9a(\xe3)\xd6(r\x1a\xf20\\\xaf\x91{\xb9Gu\x8c5\x89\x92\xe7[X\x05\x0f\"`\xb4\x1a\xe3\xbb\x03\xb3\xc2\x017Uͼ\\I%\xfd\x11\xa4\x1b\xa0ϔ2\a\x14\xd9\xe2X\xd5\xfe8\x81\x8f\xday\xa69\xba\x13\x0e\"\x8d%W`:\xed\xcaQ\x1c\x01\x1d\xb3}90\x91\xaf\x8c\xf3\xc0ђ;\xaa#\x1c\xacћAa\x1f\xee\x1ef0\x17\x02\x8cߢ%\a\\\aUb\xa6\x01\xf9\xdeFd\xf1\x16\x82\x14\x7f\x18\xa0\xd5SZ\xa9\x9f\xb4\x1a=\xc6\xea*\fwTX9\xd6\xdeM\xcd\x1e\xed^\xe2az0v'\xf5fL\u008es\"\x9b\xc6.q\xfac\xfc\xf3\"\xefw\xd6\xd4\xf0\xa5Ѹβ\xcb|!\xec\xab[\x1d.\b\x83N\xbf\xf1\xa0\x11\x05H\xdf\x04\x01۰\x9apS5\x98\x1e;\xb9q\xd3\x06\xed\xa9t.\xa0\x9b\xfe\xf4\xf3\xef\x7f\xf7M\x8en\xea\x94\no\x88\xcfeLgG\x92\"\x9a\x86|w\x99\xc2\xccX \x8cD\xd1[\xe5\xf0Lţ/\xb3\xf4\xb5\x1e\xcdQ\xbc\xba\xafD\xee\xb0[\x1d_ț\x00_\xc7\r\xedU\xac\x1e\xa7\xdd̛J\xf2Q[\xda\x14\xda/\xa7\xd8ҏI-$'\xfc~\x99\x1aK\x9f*.ڶ\x1e5\xb4\x1b\xb9\xa1\x82Я\xa6$n\x86CW8~h\xee=w\xf7\xa9:e\x88\xe3\xd0\x13\xb4v\xa0\x91 \x10\xb3]=ǚ\xc0\x8d֔\x8c\xbd\x01v\xaato\\\xbbĿ\xb2@\xac\x02\xdfa\x8f\xe2;\xa2|\x88\x1b\x8b\x8e\xd31\xe2%8\x8c\xb5\xf7\x1a\x1bp=\"8[\xa0\xbd\x85\x97Ŝ6\x9eP\x12\x83\xc5\x1cVA\v\x85\x85\xa3\x18\xef{\xb4r}쿋\xc6\xd3\xfd\xb2h5\x02\xcc\xdc\x1a\x16\xdd\xf6ːJ\xf8\fV\xc7\x1eHx\x83\x90\xb5ŵ\xfcz\x83\x90\x8fqcQx\xcd\xfc\x16\xa4vR \xb0\x1e\xf5'\xac> \xe8\t\xfe=\xe4\x9c\xf3\r\xe6y)7$v^\x93\x1e\x8a\x8e\xaf\xc4\xcfc\xdev\xd2B\xf9\x9d\v\xf8e+0\x14ǽ\x12\xedO\xefN\x7fJ\x00\x9b\xf7 \xa5\vf\x9e\xbb'^\x00\xea\xe5\xf5\xab/\x98\t\x16\x1ak\xd1\xd5F\vj\xabo\x83\xe9g\x96\xffw`\xbd߬\xe3\xcb,\xd7Z+V\xb8\xa9S\x8d/}\xaf\xeeU\xd3\xfbg\xb3\x134+\x87v\xdfhW[2~\x97.\xb5\x17U6ZW\xe9\b\xa2\x05\x1d\xc1{DN\x93Ѩ\xe7\xc8\x1d\xd6\x16\xa9\x84\x89\x19\tg\xe3Im\x0et\xbaA.AO\xa3S\xc1\xa7\xfe\x9di\x91_Nh\xa9\x87\xf2A*E \xc0beH[\xd4zXB\xac,\xe2\xc9\xfdo&\xef\xfe\x7fm\xb1b\xceS\x97\x8b\xe23\xeee\xf7\xf9\xf06}\xdfw\xa8\x94\xf4p\n\x1a\xfa\U00065f28Lm\xde\xf6\x05\xd6R\x11.m\xe4\x8e\x1b\xe0A\xcf\xe3\xf7\x87\xe5\xfd\x1b\x17\xf1$j\xef\xe0@\x16t\x91%j\xecL~\xc5\n\xceS\x15\xb9\xee\x00\xcd.C\x1bPFo\b\x80\xa7'-\x82xɟ\x8c\x05\x81\x9e\xaa\x95\xde\x00\xdf2\xbd\xa1\xd8\xe8K\xfa\x91\xe3\xcc~\x93Qr\x9fA\x0f\x91z\xc0=n\xb2\xe8\x93\xec\xeb\xdb^c\xcd\xe1o\r'\xfe\xb3i\xcfO\xda-\xc5\x0f%\xdbb\x8a\xf6b)\xe6\xa4\xe8\xb1?\x7f\x7f8\x8fo\x7f\x04\xe9~\xdc\xf8V\xf5\xfcW\x9fc:\x9fa~\x15ʩ\b\xe9^\x85ϟҮ\xf4(\x9d\x8f\x00[\x99\xe0{\xaa\x7f\xc3\xe1{\x83:~qz\r\x8f\xf1;\xda5\x80B{\x8aEx\xb06>\\\x97\a\u0558*\xfa\xea\xd2\xed)x\xde\xfa\xdc\xd7\\\xeb~\f\xbcA\xae\xde:ݙL\xb5\xb6a\u05ec\xe4\xe6LX\x9d>G\xcc\xe0\x9f\xff\x1e\xfd'\x00\x00\xff\xff\xa1\a\xb8\x04\xa5\x1e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVMs\xdb6\x10\xbd\xebW\xecL\xaf%\x15O{\xe8\xf0\xd689x\xdaf4v&w\bX\x89\x88A\x00\xdd]\xc8u?\xfe{\a\x00)K\x14\xe5$\x97\xf0&`\xb1\xfb\xf0\xde\ue0da\xa6Y\xa9h?!\xb1\r\xbe\x03\x15-\xfe%\xe8\xf3/n\x1f\x7f\xe1ֆ\xf5\xe1f\xf5h\xbd\xe9\xe06\xb1\x84\xe1\x1e9$\xd2\xf8\x0ew\xd6[\xb1\xc1\xaf\x06\x14e\x94\xa8n\x05\xa0\xbc\x0f\xa2\xf22\xe7\x9f\x00:x\xa1\xe0\x1cR\xb3G\xdf>\xa6-n\x93u\x06\xa9$\x9fJ\x1f\u07b47?\xb7oV\x00^\r\u0601A\x87\x82[\xa5\x1fS$\xfc3!\v\xb7\atH\xa1\xb5a\xc5\x11uο\xa7\x90b\a/\x1b\xf5\xfcX\xbb\xe2~WR\xbd-\xa9\xeek\xaa\xb2\xeb,\xcbo\xd7\"~\xb7cTt\x89\x94[\x06T\x02\xd8\xfa}r\x8a\x16CV\x00\xacC\xc4\x0e>dXQi4+\x80\xf1\xda\x05f\x03ʘB\xa4r\x1b\xb2^\x90n\x83K\xc3D`\x03\x06Y\x93\x8dR\x88\xfa\xd8c\xb9\"\x84\x1dH\x8fPˁ\x04\xd8\xe2\x88\xc0\x94s\x00\x9f9\xf8\x8d\x92\xbe\x836\xf3\xd5\xd6\xd0\fd\f\xa8T\xbf\x9d/\xcbs\x06\xccB\xd6\xef\xafA`Q\x92x\x02Q\xea\xda\xe0\x81N\xf8=\aP\xe2\xdb\xd8+>\xaf\xfeP6\xaeU\xae1\x87\x9bʴ\xeeqP\xdd\x18\x1b\"\xfa_7w\x9f~z8[\x86s\xac\v҂eP\x13\xd2L\\e\r\x82G\b\x04C\xa0\x89Un\x8fI#\x85\x88$vj\xad\xfa\x9d\f\xcf\xc9\xea\f¿\xcd\xd9\x1e@F]O\x81\xc9S\x84\\H\x1c\x9b\x02\xcdx\xd1J\xaee \x8c\x84\x8c\xbe\xceU^V\x1e\xc2\xf63jig\xa9\x1f\x90r\x1a\xe0>$g\xf2\xf0\x1d\x90\x04\bu\xd8{\xfb\xf717\xe7{\xe7\xa2NI\xa1$\xb7\x9dW\x0e\x0e\xca%\xfc\x11\x947\xb3̃z\x06\xc2\\\x13\x92?\xc9W\x0e\xf0\x1c\xc7\x1f\x99D\xebw\xa1\x83^$r\xb7^\xef\xadL\x96\xa2\xc30$o\xe5y]\xdc\xc1n\x93\x04\xe2\xb5\xc1\x03\xba5\xdb}\xa3H\xf7VPK\"\\\xabh\x9br\x11_l\xa5\x1d\xcc\x0f4\x9a\x10\x9f\x95\xbd\xe8\x9e\xfa\x15\x17\xf8\x06y\xb2'\xd4\x1e\xa9\xa9\xea\x15_T\xc8K\x99\xba\xfb\xf7\x0f\x1faBR\x95\xaa\xa2\xbc\x84^\xf02\xe9\x93ٴ~\x87T\xcf\xed(\f%'z\x13\x83\xf5R~hg\xd1\vp\xda\x0eVx\xea\xd8,\xdd<\xedm\xb1\xdd\xec\x00)\x1a%h\xe6\x01w\x1enՀ\xeeV1~g\xad\xb2*\xdcd\x11\xbeJ\xad\xd3\xc7d\x1e\\\xe9=٘\x9e\x81+\xd2.\f\xffCD\x9d\xc5\xcd\xfc\xe6\xd3vgu\x1d\xab] x\xea\xad\xee\xa7\xe1\x9f\xd1t4\x8as\xfe\x96\x8d!\x7f/v;߹zy(\"[\xc2Y\xc36p\xe1ݯ\xf3RL\xf5\x1b\x99\xa9\x8e>r\xa3\x13Qi\xbe\xa3ϫ\xa5C_\xcb\x05\x12\x05\xbaX\x9d\x81z_\x82\xca?\x06e=\x83\xf2\xcf\xe3A\x90^\t Date: Mon, 23 Dec 2024 15:20:53 +0800 Subject: [PATCH 05/16] backup repo crd changes for repo maintenance history Signed-off-by: Lyndon-Li --- config/crd/v1/bases/velero.io_backuprepositories.yaml | 7 +++---- config/crd/v1/crds/crds.go | 2 +- pkg/apis/velero/v1/backup_repository_types.go | 6 +++--- pkg/apis/velero/v1/zz_generated.deepcopy.go | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/config/crd/v1/bases/velero.io_backuprepositories.yaml b/config/crd/v1/bases/velero.io_backuprepositories.yaml index f9c18e517..a74e3bd7d 100644 --- a/config/crd/v1/bases/velero.io_backuprepositories.yaml +++ b/config/crd/v1/bases/velero.io_backuprepositories.yaml @@ -89,7 +89,7 @@ spec: properties: lastMaintenanceTime: description: LastMaintenanceTime is the last time repo maintenance - completed. + succeeded. format: date-time nullable: true type: string @@ -104,9 +104,8 @@ spec: - Ready - NotReady type: string - recentMaintenanceStatus: - description: RecentMaintenanceStatus is status of the recent repo - maintenance. + recentMaintenance: + description: RecentMaintenance is status of the recent repo maintenance. items: properties: completeTimestamp: diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 5b42ccb87..397042b4a 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -29,7 +29,7 @@ import ( ) var rawCRDs = [][]byte{ - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccXO\xaf\xdb6\f\xbf\xe7S\x10\xdduNZl\x87!\xb76[\x81bm\xf1\x90\x14\xef\xae\xd8t\xa2>Y\xd2$*]\xf6\xe7\xbb\x0f\x94\xecı\x95\xf8\xe5\r\x18\xa6\x9b%\x92\"\xf9\xfb\x91bR\x14\xc5LX\xf9\x88\xceK\xa3\x97 \xac\xc4\xdf\t5\x7f\xf9\xf9\xd3O~.\xcd\xe2\xf0f\xf6$u\xb5\x84U\xf0d\x9a5z\x13\\\x89?c-\xb5$i\xf4\xacA\x12\x95 \xb1\x9c\x01\b\xad\r\t\xde\xf6\xfc\tP\x1aM\xce(\x85\xaeء\x9e?\x85-n\x83T\x15\xbah\xbc\xbb\xfa\xf0z\xfe\xe6\xc7\xf9\xeb\x19\x80\x16\r.a+ʧ`\x1dZ\xe3%\x19'\xd1\xcf\x0f\xa8Й\xb943o\xb1d\xeb;g\x82]\xc2\xf9 i\xb77'\xaf\xdfEC\xeb\xce\xd01\x1e)\xe9\xe9\xd7\xec\xf1G\xe9)\x8aX\x15\x9cP9Gⱗz\x17\x94p#\x01\xbe\xc0\x97\xc6\xe2\x12>\xb3/V\x94X\xcd\x00\xdaH\xa3o\x05\x88\xaa\x8a\xb9\x13\xea\xc1IM\xe8VF\x85\xa6\xcbY\x01_\xbd\xd1\x0f\x82\xf6K\x98wٝ\x97\x0ecb\xbf\xc8\x06=\x89\xc6F\xd9.aow\xd8~ӑ/\xaf\x04\xe1\xd8\x18gn~\xf6\xf5\xcb\xd1⅕s\"\xa0w\x96,zrR\xeffg\xe1Û\x94\x8ar\x8f\x8dX\xb6\xb2Ƣ~\xfb\xf0\xe1\xf1\x87\xcd\xc56\x80uƢ#\xd9\xc1\x93V\x8f~\xbd]\x80\n}餥H\x8e\xbf\x8a\x8b3\x00\xbe iA\xc5\x81\xa9\x81\xf6҃C\xebУN\xcc\xe4m\xa1\xc1l\xbfbI\xf3\x81\xe9\r:6\x03~o\x82\xaa\x98\xbe\at\x04\x0eK\xb3\xd3\xf2\x8f\x93m\x0fd\xe2\xa5J\x10z\x82\x88\xa2\x16\n\x0eB\x05\xfc\x1e\x84\xae\x06\x96\x1bq\x04\x87|'\x04ݳ\x17\x15\xfcЏO\xc6!H]\x9b%쉬_.\x16;I]Q\x96\xa6i\x82\x96t\\\xc4\xfa\x92\xdb@\xc6\xf9E\x85\aT\v/w\x85p\xe5^\x12\x96\x14\x1c.\x84\x95E\fD\xc7\u009c7\xd5w\xae-c\x7fq\xed\b\xe8\xb4b%\xdd\x01\x0f\x97\x16H\x0f\xa25\x95B<\xa3\xc0[\x9c\xba\xf5/\x9b/\xd0y\x92\x90J\xa0\x9cEGy\xe9\xf0\xe1lJ]\xa3Kz\xb53M\xb4\x89\xba\xb2Fj\x8a\x1f\xa5\x92\xa8\t|\xd86\x92\x98\x06\xbf\x05\xf4\xc4\xd0\rͮb\xe3\x82-B\xb0\\:\xd5P\xe0\x83\x86\x95hP\xad\x84\xc7\xff\x18+F\xc5\x17\f³\xd0\xea\xb7\xe3\xa1pJo\xef\xa0k\xa5W\xa0\x1d\xb6ǍŒ\x91\xe5䲪\xace\x99j\xaa6\x0e\xc4H\xfe2S\xf9\x16\xc0+5\xd1\r\x19'v\xf8\xd1$\x9bC\xa1)\xda\xf1z\x973\xd4y\xccm+\xf5\x04\xcc\vf\f\xd2^P\xaf\x19\x90\x90\xfa\xd4S\xb2A\xde@&\xa2#\xb8Sh\xa1K|\x1f\xf9\xa8\xcb\xe3D\xa0\x9f2*\x1c\xd2\xde|\x03S\x13\xea\xbe\xd1\xd6\xd7L$[\x04\x17\xf4]Ξc\\\x19]\xcb\xdd\xd8\xd1\xfeCv\r܉K\x06Ѯ\awr\xa4L\xae\xb3/E\xc7<\x06\xa4\x96\xbb\u0b81WKTը\x85\x00蠔\xd8*\\\x02\xb9\x80W22\xaa\x95ˌ\xf0\xfb8\x01\xdc\xfaB\x18\xa4\xae\xb8Z\xdaNJ/\xe9\xc8\xc8\xf4G]\x81\xbb\x1cS\xfa\vuh\xc6\xd7\x15\xf0d\xac\x14\x99}\x87\x9ed\x999x\xf5\xea>\x06\xb0\x99\x0f\x15\xb7\xa3Z\xa2{IM\xae\a6\xbar\xac\x83R\xed\x05Ei\x1a+Hn\x15vo\x06c.\x93\xce1G\x1a\xf8Wex\xe0y\vO\x13\xdaK\xc2z\xbc4\xd1o2i#\xfa\x97:[\xcfͮ\x8b\xf8\x8cIk\xaaֳV/R\xff\x8e\xc0\xb8=H\x87\x83\u05fa\xc8\xf7ׁL\xae3\rD\x86l\x18\x1c\x0f\x92\xfa\xac\xf7\x87\x04\x05\x7f\xcf\v\x14\x15\xbad\x97\xc1\xb9\xf8§]\x1e\xec^\xfc\x06)\xe1\xa9\xd7jy̞\xa0\xc5DZF\xe7\x18\x1b\x03\xe2\rF\xbe\x9f\xdb\f\xecL~\x85\x99\xa1\x03\x18\xfeFP\x1a\xe7\v\xb6\xf7\xb2^\x96\x7f\x8a\xd0{\xb1\x9b\n\xf2S\x92J\xf3\\\xab\x02bk\x02]A\x80\xf6\xb9\x18o\xa32\xe1\xa9\xdd\v?\xe5\xe7\x03\xcb\xe4x1x\xf2o\xb9p\xad\xc9~\xc6o\x99\xdd5\x8ajܨ\v\xf8l(\x7ft\xb3ϖ\xa8\xfbd\xdad\nc\x14\xf3:\xaf\xc5Y\xb8\xc0\xa35\x1f\xb9\x98\xc1\xa6\xc7\xceqN$a\x93}үWRZ\x1d\xabO\xbfW\xf3b\x83\x90VC\xad\x13\xa4\xe9\x80ǹXWW\x99\xd6%\xf4\xb2\xecƁ\xa55]`iM\x94YZ7\xa7\x1c\xb8Ur\x99L\xdc[xWS\x91\b\xf0\xbctLF\xe0\xd0\aE\xcf\n`\x1dE;\xfc\x92♐\xcf\xf3'_\x91i\x15\xb0\te\x89Xan\xdcM\x12\xef\x85TW\x8f'\x83\xf5$\x1c\xdd\xc7\xdfͅ\xca\xe9\xb7\x12\xef\xf6y\xfb\xbf\xe4獹\xb7;\x14Ή\xe3\xf4\xc3>\xda\xf4\xfc\xb3\xbd\xea9\xe7\xd3,\xd2\xdf\t\xdbӿ\x12K\xf8\xf3\xef\xd9?\x01\x00\x00\xff\xff\xc6⍽\x9f\x14\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccXO\xaf\xdb6\f\xbf\xe7S\x10\xdduNZl\x87!\xb76[\x81bm\xf1\x90\x14\xef\xae\xd8t\xa2>Y\xd2$*]\xf6\xe7\xbb\x0f\x94\xecı\x95\xf8\xe5\r\x18\xa6\x9b%\x92\"\xf9#\x7fTR\x14\xc5LX\xf9\x88\xceK\xa3\x97 \xac\xc4\xdf\t5\x7f\xf9\xf9\xd3O~.\xcd\xe2\xf0f\xf6$u\xb5\x84U\xf0d\x9a5z\x13\\\x89?c-\xb5$i\xf4\xacA\x12\x95 \xb1\x9c\x01\b\xad\r\t\xde\xf6\xfc\tP\x1aM\xce(\x85\xaeء\x9e?\x85-n\x83T\x15\xbah\xbc\xbb\xfa\xf0z\xfe\xe6\xc7\xf9\xeb\x19\x80\x16\r.a+ʧ`\x1dZ\xe3%\x19'\xd1\xcf\x0f\xa8Й\xb943o\xb1d\xeb;g\x82]\xc2\xf9 i\xb77'\xaf\xdfEC\xeb\xce\xd01\x1e)\xe9\xe9\xd7\xec\xf1G\xe9)\x8aX\x15\x9cP9Gⱗz\x17\x94p#\x01\xbe\xc0\x97\xc6\xe2\x12>\xb3/V\x94X\xcd\x00\xdaH\xa3o\x05\x88\xaa\x8a\xb9\x13\xea\xc1IM\xe8VF\x85\xa6\xcbY\x01_\xbd\xd1\x0f\x82\xf6K\x98wٝ\x97\x0ecb\xbf\xc8\x06=\x89\xc6F\xd9.aow\xd8~ӑ/\xaf\x04\xe1\xd8\x18gn~\xf6\xf5\xcb\xd1⅕s\"\xa0w\x96,zrR\xeffg\xe1Û\x94\x8ar\x8f\x8dX\xb6\xb2Ƣ~\xfb\xf0\xe1\xf1\x87\xcd\xc56\x80uƢ#\xd9\xc1\x93V\xaf\xfcz\xbb\x00\x15\xfa\xd2IK\xb18\xfe*.\xce\x00\xf8\x82\xa4\x05\x15\xd7!z\xa0=v9ƪ\xf5\tL\r\xb4\x97\x1e\x1cZ\x87\x1eu\xaaL\xde\x16\x1a\xcc\xf6+\x964\x1f\x98ޠc3\xe0\xf7&\xa8\x8a\xcb\xf7\x80\x8e\xc0aivZ\xfeq\xb2\xed\x81L\xbcT\tBO\x10Q\xd4B\xc1A\xa8\x80߃\xd0\xd5\xc0r#\x8e\xe0\x90\uf120{\xf6\xa2\x82\x1f\xfa\xf1\xc98\x04\xa9k\xb3\x84=\x91\xf5\xcb\xc5b'\xa9k\xca\xd24MВ\x8e\x8b\xd8_r\x1b\xc88\xbf\xa8\xf0\x80j\xe1\xe5\xae\x10\xae\xdcK\u0092\x82Å\xb0\xb2\x88\x81\xe8ؘ\xf3\xa6\xfaεm\xec/\xae\x1d\x01\x9dV\xec\xa4;\xe0\xe1\xd6\x02\xe9A\xb4\xa6R\x88g\x14x\x8bS\xb7\xfee\xf3\x05:O\x12R\t\x94\xb3\xe8(/\x1d>\x9cM\xa9ktI\xafv\xa6\x896QW\xd6HM\xf1\xa3T\x125\x81\x0f\xdbF\x12\x97\xc1o\x01=1tC\xb3\xabH\\\xb0E\b\x96[\xa7\x1a\n|а\x12\r\xaa\x95\xf0\xf8\x1fcŨ\xf8\x82Ax\x16Z}:\x1e\n\xa7\xf4\xf6\x0e:*\xbd\x02\xed\x90\x1e7\x16KF\x96\x93˪\xb2\x96e\xea\xa9\xda8\x10#\xf9\xcbL\xe5)\x80W\"\xd1\r\x19'v\xf8\xd1$\x9bC\xa1\xa9\xb2\xe3\xf5.g\xa8\xf3\x98i+q\x02\xe6\x053\x06i/\xa8G\x06$\xa4>qJ6\xc8\x1b\xc8Dt\x043\x85\x16\xba\xc4\xf7\xb1\x1euy\x9c\b\xf4SF\x85Cڛo`jB\xdd7\xda\xfa\x9a\x89d\x8b\xe0\x82\xbe\xcb\xd9s\x8c+\xa3k\xb9\x1b;\xda\x1fd\xd7\xc0\x9d\xb8d\x10\xedzp'G\xca\xc5u\xf6\xa5\xe8*\x8f\x01\xa9\xe5.\xb8k\xe0\xd5\x12U5\xa2\x10\x00\x1d\x94\x12[\x85K \x17\xf0JFF\xbdr\x99\x11\x9e\x8f\x13\xc0\xad/\x84Aꊻ\xa5\x1dV|IW\x8c\\\xfe\xa8+p\x97ϔ\xfeB\x1d\x9a\xf1u\x05<\x19+Efߡ'Yf\x0e^\xbd\xba\xaf\x02\xd8̇\x8a騖\xe8^ғ끍\xae\x1d\xeb\xa0T{AQ\x9a\xc6\n\x92[\x85\xdd\xcc`\xcce\xd29\xe6\x8a\x06\xfeU\x1b\x1e\xf8\xbd\x85\xa7\x17\xdaK\xc2z\xbc4\xd1'\x99\xb4\x11\xfdK\xcc\xd6s\xb3c\x11\x9f1iM\xd5z\xd6\xea\xc5ҿ#0\xa6\a\xe9p0\xad\x8b<\xbf\x0edr\xcc4\x10\x19V\xc3\xe0x\x90\xd4g\xcd\x1f\x12\x14\xfc=\x13(*t\xc9.\x83sq§]~ؽx\x06)\xe1\xa9G\xb5\xfc̞(\x8b\x8fc\x8d\xce16\x06\xc4\x1b\x8c|?\xb7\x19\xd8}(K\xc4j\xfc\xe8\x00\x86\xbf\x11\x94\x9e\xf3\x05\xdb{\x19\x97\xe5G\x11z/vSA~JR\xe9=ת\x80ؚ@W\x10\xa0}.\xc6ۨLxj\xf7\xc2O\xf9\xf9\xc02\xb9\xba\x18\x8c\xfc[.\\#\xd9\xcf\xf8-\xb3\xbbFQ\x8d\x89\xba\x80φ\xf2G7y\xb6D\xdd/\xa6\xc9\xd12\x90\xe7\xc8/0hM\x8e\xeao\x1c\xb5$l\xb2C\xfbz\xaf\xa4Ť\xad\x90\xf0\xf4\x8b4/6p}5\xd4:\x81\x96\x0e\xf8\xc1\x16;\xe7j-u)\x9b\n,\xad\xe9\x16Jk\xa2\x91Һ\xf9\x8e\x81[M\x95\xc9Ľ\xadu5\x15\t\xee\xe7\xa5c2\x02\x87>(zV\x00\xeb(\xda\xe1\x97\x14\xcf\xe5\xf7<\x7f\xf2=\x97V\x01\x9b\x8e\x1a\xafJ\xbc\x17R]=\x9e\f֓pt_\xfdn.TN\xbf\x86x\xb7_\xb7\xff\xcb\xfa\xbc\xf1\xb2\xed\x0e\x85s\xe28=\xbaG\x9b\x9e\x7f\x98W=\xe7|zm\xf4w\xc2\xf6\xf4\xbf\xc3\x12\xfe\xfc{\xf6O\x00\x00\x00\xff\xff\x1e(\xcb\xee\x81\x14\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec}_s\xdb8\x92\xf8{>\x05ʿ\x87\xd9ݒ\xecI\xfd\xf6\xe1\xcao\x19'\xb9Q\xedL\xe2\x8a=\xd9g\x88lI\x18\x83\x00\x17\x00ek\xef\xee\xbb_\xa1\x01\xf0\x8f\b\x92\xa0,{\xb2{\xe1Kb\x11l\x00ݍ\xfe\x0f`\xb9\\\xbe\xa1%\xfb\nJ3)\xae\t-\x19<\x19\x10\xf6/}\xf9\xf0\x1f\xfa\x92ɫ\xfd\xdb7\x0fL\xe4\xd7\xe4\xa6\xd2F\x16_@\xcbJe\xf0\x1e6L0äxS\x80\xa195\xf4\xfa\r!T\bi\xa8\xfdY\xdb?\tɤ0Jr\x0ej\xb9\x05q\xf9P\xada]1\x9e\x83B\xe0\xa1\xeb\xfd\x8f\x97o\xffz\xf9\xe3\x1bB\x04-\xe0\x9a\xaci\xf6P\x95\xfar\x0f\x1c\x94\xbcd\xf2\x8d.!\xb3 \xb7JV\xe55i^\xb8O|wn\xa8?\xe1\xd7\xf8\x03g\xda\xfc\xad\xf5\xe3/L\x1b|Q\xf2JQ^\xf7\x84\xbfi&\xb6\x15\xa7*\xfc\xfa\x86\x10\x9d\xc9\x12\xae\xc9'\xdbEI3\xc8\xdf\x10\xe2G\x8d].\xfd\x80\xf7o\x1d\x84l\a\x05uc!D\x96 \xdeݮ\xbe\xfe\xff\xbb\xceτ\xe4\xa03\xc5J\x83s\xff\xefe\xfd;\xf1\xa3$L\x13J\xbe\xe2\x1c\x89\xf2('fG\rQP*\xd0 \x8c&f\a$\xa3\xa5\xa9\x14\x10\xb9!\x7f\xab֠\x04\x18\xd0-x\x19\xaf\xb4\x01E\xb4\xa1\x06\b5\x84\x92R2a\b\x13İ\x02ȟ\xdeݮ\x88\\\xff\x0e\x99ф\x8a\x9cP\xadeƨ\x81\x9c\xec%\xaf\np\xdf\xfe\xf9\xb2\x86Z*Y\x822, \xdd=-Nj\xfd:6W\xfbX\xf4\xb8\xafHnY\nܴ<\x8a!\xf7\x18\xb5\xf33;\xa6\x9b\xe9#\x93ٟ\xa9\xf0ÿ<\x02}\aʂ!z'+\x9e[N܃\xb2\b\xcc\xe4V\xb0\x7fְ51\x12;\xe5Ԁ\xb6\x981\xa0\x04\xe5dOy\x05\v\x8b\x94#\xc8\x05=\x10\x05\xb6OR\x89\x16<\xfc@\x1f\x8f\xe3W\xa9\x800\xb1\x91\xd7dgL\xa9\xaf\xaf\xae\xb6̄\xf5\x95ɢ\xa8\x043\x87+\\*l]\x19\xa9\xf4U\x0e{\xe0W\x9am\x97Te;f \xb3d\xbe\xa2%[\xe2D\x04\xae\xb1\xcb\"\xff\x7f\x81=t\xa7[s\xb0l\xab\x8dbb\xdbz\x81\xebc\x06y\xec\xd2q\xcc\xe8@\xb9)6T\xb0?Y\xd4}\xf9pw\xdffT\xa6=QZ\xfc:D\x1f\x8bM&6\xa0\xdcw\x1b%\v\x84\t\"w\xac\x8a|\xce\x19\bCt\xb5.\x98\xb1l\xf0\x8f\n\xb4]\x03\xf2\x18\xec\r\xca \xb2\x06R\x95\xb9e\xe3\xe3\x06+Anh\x01\xfc\x86jxeZY\xaa\xe8\xa5%B\x12\xb5ڒ\xf5\xb8\xb1Co\xebE\x10\x90\x03\xa4u\x82宄\xac\xb3\xd0\xecWl\xc32\xb7\x9c6R5r\xc7\xc9\xc0.\x86\xe2K\xdf>\x99fw\x82\x96z'\xcd=+@V\xe6\xb8\xc5\x14\xaf!\xf1\xeeVGP\xc2\b\xfdxQfU\x1ar\xbbh\x1f)38曻\x15\xf9\x8a\xc2*|\x8dB\xab\xd2\xc4TJX.\x89\xf4\xf5\x05h~\xb8\x97\xbfi y\x85̝)@<,\xc8\x1a6\x96\x13\x14\xd8\xef\xed+P\xca\xe2F\xe3\x00d\xd5\x136\xf6\xb9߁\xc5-\xad\xb8\xf1\xeb\x84i\xf2\xf6GR0Q\x99\x1e\xab\rR\x1d1E\r-\xe4\x1e\xd4)H|O\r\xfd\xd5~|\x84;\v\x94 T\x8b\xbc\xb5\xc7\xe3\xfa\x80/c\xd4v\xcfjӂ\xc84\xb9\xb8 R\x91\v\xa7\x81/\x16\xee\xeb\x8aq\xb3d\xa2\xdd\xc7#\xe3<\xf42o\xf2\x0e\x87\x8e\xa0\xfa^~ԎyO\xc2\xc5\x00\xac\x16j\x1ew`v\xa0H)k\x8d\xb7a\x1c\x88>h\x03\x85GL\xd0\"~>\x91\x9ep\xedp\xeeAh\x8bW?\x91\xfe\xe4E\xc59]s\xb8&FU0\x80\x9b\xb5\x94\x1c\xa8\x98@\xce\x17Іe\xe7@\x8d\x83\x14A\x8c\xf2/:\x18@\xa5I\x1f\x80\xd0\bh\x8f3\xab\x9d9o!\xb6\x8b\x957\xd1A\x95\n2+\xb6\xaf\xbd:`\xc0Q\x05\tI\xb8\x14[P\xae{k\xaa\x04\x0eS`9.'V\xd2*\xe0V\x9d\x90Me\x85\xf0%\xb1\xcb{\x90\t\x98\xd0\x06h\x84;\x9fA x\xcax\x95C~\xe3,\xaf;k@\xe6\xc1l\xee\x89\xcd\x14B}\x18\x85\xe8\xd53g\x19Z\x81\xde\xe0[\xa2\xe1\x1a\xe3\xd3FK\x1fJp\xb6\xb3\xa5\xa5\x1fv\xa3~G\x05\x82\x06c?\xba\xf8\xcb\xc5\x02I\xdc\xed\xb5ۇ&TA\x8d\x96d\xc1\tEi\x0e\xfd\xd6\xcc@\x11\xc1\xe2\xa8@I\xa4'U\x8a\x1e\x06\xa8Y;\x00g\xa4\xe7\x10\xcc#\x8a\x8a\xd0\xec\x95iz\xdc\xef\xbf3U\xcfCG\x8d\xee.e\xc2\xd2\xcfz\x9e\x1d\xf2i\xe7\xc0Y\xb4\ti\"\xf0\x98p\xf0\xd07\x1b\xa1\xd6\x1f\x84\xac\xb3\xf0\xfc\x10\x93\u05fc\xe5\x99\xf7_\x12S;)\x1f\xa6\xb0\xf3\xb3m\xd3xE$ð\nYÎ\xee\x99T~ꍮ\x85'\xc8*\x13]\xf5Ԑ\x9cm6\xa0,\x9crG5h\xe7'\x0f#d\xd8~'-1\x12}y4\x8f\x86\x90\x96L8\xf3\xa1\xa1[C\xe2XK\x86\xc7\x0e\xd4\xdaר\x8cs\xb6gyE9\xeae*27\x1fZ\x8f+&eF\x88\xdc\x1bs\x943\xdd\xe3\f\x820)K\xa4\x8e\xab$\x05X\xa3\xb7\xb0NA\xbf\xe9\xf0\xcc\xd7\xd4\xda*rh\xf6\x04\x89\xa5*\x0e\xdaw\x95\xa3\x1d\xd9ȌEC\x14\x8cD\x10N\xd7\xc0\x89\x06\x0e\x99\x91*\x8e\x91):\xbb'E\b\x0e 2\"\xf9\xba\xaeF3\x81\x11\x90\x04}\xb8\x1d\xcbv\xceԳL\x84pH.\xc1\x1a|\x86в\xe4\x11u\xd1<\xa3\xc4\xf7\x9d\x8c\xad\xf5\xe6\x99X\xf5\xc7\xf0b\xeb\xbfy\x12df\xf3DQ۬\xaf.fkv\x88;\xb5\xcd\xf3\xef\x89\xd8 \xf9O`ڑ\xd5O0,\x94\xccӃ|k\xb1\xca@_Zs\n-\x9d\x05a&\xfc:\xb5\x12:6W/Z\xd6A·M\x9b\xf9L\x9fH\x9a\x945\xf1B\x84\xa9\xbb\xf8\x17\xa4\v\xaa\x8c;\xaf1\x92i\xf2K\xfb\xab\x05a\x9b\x1a\xe9\xf9\x82l\x187\xa0\x8e\xb0\x7f\x92\xa8\x0f\x949\a2R\xb4\x1e\xc1\xf8\xbd\xc9v\x1f\x9e\xac\t\xa6\x9bTU\"^\x8e?v\x86l\xb0\xf6\xbb\xeay\x02.\xc186SP`|\x1c=\xa6\xf6/hZ\xbd\xfb\xf4>\xee_\xb5\x9f\x04\xce\xebMdbѹ\xe7\xddь\xda\xe3\xf3&|x\x836P\xed\x00\xb9\\ȂP\xf2\x00\ag\xbaPA,}hh\x9cн\x02L\xca \x9f=\xc0\x01\xc1ij,\xfd'\x95\x1b\xdc\xf3\x00\x87\x94fG8\xb4cb\xdag\x8f,\x9e\xec\x0f\x88\b\f\xae\xa7\xb2\x81{\xfcR\x88\xe44\xe2O\xa2,\tO\xc0\xfd\t\xd3Lb\x95v\x1f\xed4%r\xc0\x0f\xda\xd1Ү\x98\x1d+Q\xacb\xc4An\x92\tꞯ\x94\xb3\xbc\xeeȭ\x91\x95X\x90O\xd2\xd8\x7f><1\xed3\x99\xef%\xe8O\xd2\xe0//\x82Q7\xf0\x97ħ\xeb\x01\x17\x9apR\xde\"\xac\x9d\x8bs:\xcdr[\x8d{\xa6\xc9JXwš$\xb1+L\xbb\xba\xee\\GE\xa51\x8d&\xa4X\xba\xb0M\xac'\x8fo\xa9:\xe8~v\xa7\xbe\xc3{\xab,\xdc\x1b\x97\xfc\xe54\x83<\xe4k0+I\rlY\x96\xd8_\x01j\v\xa4\xb4\"<\x8d#\x12\x05\xab\x9f\xcd<\xf6I\xd3\xde\xed\xe7i\xf9P'\xf9\x97V\xe5,=\x04#\x8b\x04\x1cxٝO\xcfgi\xd7lB\xab\xc0\t\x93M\a\x92\x96\xc3MS\x90\xf2\ft\xa0\x16G\x13g\x92\xba4ϱЅ\xf2\xdb\x19\x1ae\x06/\xcc\x15\r\xad\xb1;\x15\\P̵\xfc\x97մ\xb8\x9a\xfe\x87\x94\x94)}I\xdeaM\v\x87\xce;\x1f4k\x81I\xe8\x12kR,\xff\xec)\xb7\xba\xdf\npA\x80;K@nzvт<\xee\xa4vj\xbbN\xe2\\<\xc0\xc1\xa5\f'\xbbl\v\x99\x8b\x95\xb8p6DO`\xd4\x06\x87\x14\xfc@.\xf0\xdd\xc5sL\xa9DNMl\xd6aт\x96i\x1c\x8a5E\xa9\x86\xbauX\x83\x11b?\xacke\xac\x91=6\xdb$\x16-\xa5\x8ed\xf2\a\x862\xc1\xbc\xb7R\x1b\x17/\xeb\xd8\xccр\x9a\fA4B7\xae\x80I\xaaPmb\x85\xf2T\xe8\xb7\xfd\xdc\xef@\x83\xcfW\xf8\xc0\x9c\x03j=\xbb\x8bf};i\x7f\xe1\xf2%\xd8\t\xcd\xd0b\xc1oK%3\xd0\xd1dv\xf3$\xe8\x8bHYF{\xeeȗ:/\xc9\xd5d\x8c\x87@Ón\xf2ZD\xcc\xf4\x17><\xb5\x02\xa2v\xedۿ\xa7xl\xee\xb8\b\xd6\f\x16\x05=\xaeSJ\x1a\xe2\x8d\xfb2\xac\x06\x0f\xc89\x1fj[\xa1$H\xd5\xe55\x03~\v\x86B\xc1\xc4\n; o_\xc0\xb0\xf024Vm\x12{N3eoB'\ru\xea\x1f\xdcR.%\xa6\n\x14t\x88\u05cf\xaa\xa3\x1d*\xa4i\x05$f\x98\x9b\xa5\xcc\x7f\xd0dÔ6\xed!\xe8\x81:\x95(\x98\x99\x8e\x97\xf8\xa0\xd4I~\xd7g\xf7e+ܵ\x93\x8f\xa1>\xcb!&q\xe6\x98_\x02\xc26\x84\x19\x02\"\x93\x95\xc0\x00\x8e]\xc7\u0605C\xae\x93\xb0,u\x91\xa4\xad~\xfb\x80\xa8\x8a4\x04,\x91S\x98\x18\x8d\xf4\xb4\x9b\x7f\xa4\x8c\xbf\x04\xd9\xccP\x19[\xec9mM\x84\x1a\xb7vE^A\x9fXQ\x15\x84\x16\x96F\xa8\xccY\x01]\xa27\x95o\xf6\vT\x13F\xda\x15Sr0\xe0\xab\xd7\x12ǐI\xa1Y\x0e\xb5r\xf5\x8c \x05\xa1dC\x19\xafT\xa2\x04\x9c\x85\xde9\xae\x88\x97\x04\xe7\xf31\xd2:_\"*\x12\xa2\xb9\x89\xb6\xe2\xb84.U\xba\xc57ef)\x98oe\x95\x8aI\xac\v<\xb3\xa1\xe5+)\xa98|\xb7\xb4R\x87\xfa\xdd\xd2\x1a{\xbe[Z\x13\xcfwK뻥\x95\xd2\xf2\xbb\xa5\xf5\xdd\xd2j?\xff',\xad\xa9\x11\xb9\r}\x03/'G\x91\x90\xaa\x1e\x1b\xe2\b|_\\\xe1k\xc0\x9fU\x8b\xb9\x8a\x83\x8aT\xfe\x0f\x94uDŽV\xa3<\xea\xe2L\xbbj\x02ϻ\xfdE\x13\xa6\xe43\xaa\xeeC\xa7竺_\x8dBΠ\xcdw\xe4s\xe9bO\xb3\xed\x8e\xf1\x98JZ1\xe1\xc9%\x84\xdd\x12\xc1\x01\xbbtn\xb6\xfb,[&\xfe\x90\xd2\xc0\xf9\x05\x81)\x11\xb1\x89\xe2\xbf\x13J\xfe\x12k\x8b\x9f\x9d\x9eO)\xea\x9b\xe31\xbfX\x01\xdf\xf9\xcb\xf6\x92\xf03]\xa27\a;/^\x8e\xf7\x8aEx\xafSz\x97Xpw\xbe\xca\xf9\xb4x\xecI\x95cӡ\x83ᢹ\xc9R\xb9\xc9\xd0\xc2\xd4\xc4fOi\xb2\x04nN\xe1\xdb$uҖ٫\x95\xb6\xbdZA\xdb떱\x8dr\xd1\xe8\xcb9\x85j\xf1siȤ\xb2\xe5\xaf\xc5l\xa7\xa2A\xaa\x8e\xf9z\x92\x7f\xf5\xf9\b\x86%|0\xed^\xc9F.*nX\xc91\x91\xbagy4\xd8`vp\xa8\x0f\xd0\xf8]\xe2\xd6S\x7f\x14\xcc\xe7/5\xd7^\x1eY\xfaT\x93G\xe0\x9c\xd0غ\xea\xcd\x9b2\x90SwK\xbd\xac\x8b3\xdfə\xb4*\xd2-\xbf?h\x17\xd4)\xbb\x9f\xd2\n\x00&w;\xbd\x94\xcb3\xe5\xf4$\xdbyi\xbb\x99\xe6%\v_p\xf7\xd2K\xecZJ\xc4T\xca.\xa5yxz\x85]I\xaf\xba\x1b\xe9\xb5v!%\xef>J*qI\xce\x02\xa7\x96\xa8\x9c\xb8\x9df:\xc7;\xbe\x9b(a\x17QB\xf6wz\x92'L/a\x97м\xddA\t4K]\x8a\xaf\xb8\v\xe8\x15w\xff\xbc\xf6\xae\x9f\tΚx=ow\xcf\xc9)\v\xa9rP\xa3i\x9fT.\x1c\xe5\xbf\x14ߦ;\x90\xa3|G8\xf6϶\xea\xd8˨\x1e\xfcI\xa3x\xa6\xecP\xfa\xd2rZ\xcb\xda\xe8\xe4\xa2\x1a\xf3\xa7kL\xfa\x83f]\xbaJCI\x15\x1e^\xbc>\xb8r\x96\xa8j\xfe@\xb3\xdd\x11\xf4\x1d\xd5d#UA\r\xb9\xa8\x13\x80W\x0e\xb8\xfd\xfb⒐\x8f\xb2\xae\x89h\x9fˣYQ\xf2\x83\xf5P\xc8E\xfb\x83\xd38 \xcam\xa1\xb7[\xc9Y\x16\xb1ݢg3\xb9ƽ\xc32\xf0Ĩ\xac]2Pچq\xd3\rͼ\xee\x19\x98\x1bɹ|\x9c\xe9\xfbӒ\xfd'\x1e\xdd\xfd\x8c\xe8л\xdb\x15\xc2\b\xec\x81g\x81\xd7\xc5Y\xf5l\xd6`\xd5r3ϡ\xb5\xbf\xdat v\xeb\x1cۧ\xe3B\xee\x0eB\x0ef\x81\x17\x9d\x99\xb4\xd2\xe5v\xe5\xc61ԋ\xe5\x19*\x0eDbE\x8d\xd91\x95/K\xaa\xcc\xc1\x15j,:c\b\xbat,\xba3\xa8=\xfa\x87;G\xd1\x1b\xcet\xc6\f\xe5\xa1\xec&}\x8fqw\xca8\x86w/N\xee[<\xe38\x86͒%b*\xf2s\xb4\xf2\xeblQ3\xed\x8f&\xfeU\xee\xe1}4z\xd6A\xcf\xddQ\xf3HyV\x80\xe8N\xdd\x1d\xacR]\x03\x9e\xc8\xdb\x7f\xf5\x8cz\xabе?S\xf5\x94@\xd9]\x17Dd~\xe1\x88\xd9\xd0YL>\xe1\t\xf0\ar\xfb\x15}\xb4Z\xb4\xf9%\xea}\xb4\x10*\v\xc9\xe0\b\x1c\xff\xc1O\xe7/M\xd3F*\xba\x85_\xa4;d{\x8a\xec\xdd֝\xc3\u05fd\xd5\x13\xeaGâ\x89\x9d\xc0\xeb\x8f\xfb>\x02\xd6\xd4|\xf7N5\xb6\xa3\x9cyN\xb31\xfc\x14\xba\xdf\xdf\xff\xe2feX\x01\x97\xef+W\xee`e\xa2\x06\x8b\xe20[\aim\xff\xbb\x93\x8fx\xf8o<\x8e\x19.Mh&\xa3\x00\x8bͱ\x04q֔\xaa\x92K\x9a\x83\xba\x91bö\x13\xb3\xfb\xad\xd3\xf8H\xcdf\xf8\xa3\x9f\\\xad\xa3\x02\xfc3\xd7 X\x9b\x87s\xe0\x1f\x19\a톕 \x80o\xfb_\xd5\xf2\xb8*\xd6Ά\xdbؗu\a\x03:\xceM\vC\xd1%(kE\xb9\xa0u\xa5\x03\xaf\x0eO\xbc\xa1\b\x13\x06\xb6\xd0\xf7\x02G$\xf0\xbes\xe8{\xe0\xf3)q\xf45\xfeUˬl\xad4gW\xcaMd\xe0CpZWh<2㏼:\xef\x19\xa5C\xce\xc2\xd0\xe5\x00x\x1a\xfe\xf4\xf5\x00\xee\xd0|\x7f\xa9\x88g\xe4J\xe1\x01\xa3\xfe@}<\x90\xf3\xa4\x1b\x02\xd6u\xa9S]6\xa5\xdf\x19\x03EibZzZ\x90\xfc4\x06\xb0\xb6p\xa4\xa1\xbc\xc5\xcf44\x88٨\xfa \xb2\xb1\x92,\xbf\x8eG\xa89\xc6\xc91\x04\xdc\xf8\x9d\x04gC@\rp\b\x01\xba\xca2\xd0zSq~\xa872|#\xd8\xf8H\x19?\x1f*\x1c\xb4AF\xb0\xd3\x1b\x8549a_(\r\"\x0f+=l\xf2\x99\x87\nO\x05_G\xa8\r-N\xba\xeb\xe0\xa6\x0f\x06o\xbbQy\xab\x1c\x91\xd6c\xa7\xba!\x7fL,7\xe0ܗ\xe8\x9eXh\x90\x13\u0603 V\xaf9\x14\x87\xeb\x9afB\xf1{C\x9dn\b\x9a\"\x04\x11\xa2w\xfa\x10\x1f'\xd0xw\xcc\x0f\xba\x86\x89U\x96x\x15H\x1f\t}\xb3\xd1\xf9\xf9\xd7\xd6n\x86\xa5\x05q\x9a\xbd\x17\x95͙f]\xbd\xf0\xd4\r-nІD\xea4ע\x01g[f\xbd\x04K\xb9-Uk\xba\x85e&9\a\x94\xd6\xfdq\xbd\xe4Z\xf7\xbb\xf6\xbe\x00ՓS\xfb\xd8n\xebsg\x8e\xda.eL]\xa18^|e\x98\x82\xe6\x0e\xbaހ$v<˱qX\x88^\xd0\xd6\x1fi\xbbmXu^,\xfb\b\xa9\xbf\x9fm\xe1=\xea8?\x16\xf4w\xa9\x16\xa4`\xc2\xfeCE\xeeR_\xe1\xe3Y\xe3\xdfI\xf9p\x171b{\x83\xff\xb9n\xd8$\t\x98p\xc3ƭ\x96kY\xf9\xbcum\xd0\xc6\x13\x12x\xa6\xfd\x99\x1d5\x849\xa2\x0fz\xd3\x19\x8c\x85\xfe܁4\xa9\n\\\xcf\x03\xb0\xee\xc2%`\x9c\x1f\x16ǐ\x8f.\x1cl`\xb7\xce\xfc\xf7f@\xb3\x93\x7f\xa0\xa3\x90ˉ\x02\xa9\x8f\x8ch\v\xf4S\xfcE\x8f\xe6!c\xb2\x87㟛\xd6Cxt\xc3l\x99{\x03\x13\xec\x18\x81\xe7uu\xf1\x82\x87\t濵m\xea]\xff-\xc7-\xd4W\rƷ\xe2\xbbƗ\xe4\x13\xf4\x03\xfdn#8\xe4XӀ\xab*\xd2d%n\x95\xdc*\xd0}\xa6[\x92\xbfSf\x98\xd8~\x94\xea\x96W[&>\x0foz\x19k|K\x95a\x96i\xddxb\x03e\x82r\xf6Ϙ|j\xbf\x9c\x06t3\xe8(-I\xc20\x86^\xbc\ak\xab\x0e\xfa\xf7QQXz\xbc\x9ebw\x04\x9aL\xc9\xc6\xda&hl\x8a\xd0\xed%\xf9$\xa3\v\xdc\x17\x04\xb1.LkZ\x816K\xd8l\xa42._\xbb\\\x12\xb6\tA\x04+;0r\xe4\xaek$,\x96h\xadK-\x1a5\x84a_\x85\xda\x14\x0fs/\xe8\xc1\xe5fh\x96U\xd6R\xba҆\xf2\x88\xa1\xf2,\x01\x8e\xd1\x1a\xbb\x88 \xff\xedY\xb9\xacU\x1bP?\xec\x86\xfd8\x94\xe2q\x12\xcez\xe3v\x8a ȣb\xc6X\xdbH\x8e$\xd3=\xaa\x8c\xb5\x918'ڢ\xfa\xa4\xf8\x1bq\xe2p5\\\x94\x926\xe5\xfb\x1aʐ\x98\xf5\xb3\xc6\xcb\t\u05c8\x1bb\xedW\xac\xbf\xf1\xad,\x99\xb3\x1d\x15\xdb\xc1=\xfa;%\xab\xed.p\xf2\x80QL\xf2\n0\\\x89\"E\x87\xbbuM\xa5D+\x99>\xb2\xf1\x99\x04f\xc0\xe1\xd2\xec\x81T\xe5\xc2\xdf]\xeb\xaf&\xbe\xf2\xb7\x80,7J\x16K\xdf/\x16\xd0-|.[1i-\x10\xb3\x8bb\x9d8\xeb\xdb\x1f\xb4\x8f\x9cP\x96 \bվ焳\x92NV7\xdaPe\x9e\x15\x8e\xb8\xeb@\x98\x88D`w\xf1I\xdc\xf9\x94\xbe;,\xea\xc6\xdf\xd1Y\x03^\x10\xcdD\xb8\x1dٕ\a8\xfe\x88f\x8b\x04\xdef(U\xbcbo<\xb4Н\xd0\xebF\x15\xf6\xb5\xae\xfdp\xb2\xd3\xf9\xf5\b\xc6\xd1\xc6_\xbc\xbc\xb2n\x12\x1c\xc5?\xb1X\xe4\x1bK=3;\x95?\xff\xe1\x1bz\xf7INM\x1c#c>\x0e\xba/\xc3\xceJ\xf7\xae\xca[\x0e\xd6\xf4\xd2\x00]\xf7i\x96\x9b\xbc?c\xdc\xe8\x9cA\xa3p\x0f\xf8y\xa2&\xfb3\x86\x8b^,Vt\xde)?R\xbcE\xf8\xa4U\xfbw\xffm$X\xe4\xc1\x9e;\\Ԋ\x16\x85\x81\xbfj\xbc(\xaa\x95z?\xa2\x9c\xce[\xd2\xc2\xf7\xe4\x7f\xf9\xdf\x00\x00\x00\xff\xffU\xf5M\xfa݀\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccYK\x93Z\xbb\x11\xde\xf3+\xba\xee]xc\xc0\xceM\xa5nؤ0\x93T\xb92\xceL\x99\xc9dk!5\xa0\x8b\x8et\xa2\a\x98<\xfe{\xaa\xf5\x80\xc3y\f\x8c\x93r\xae63\xe8\xd1\xeaw\x7f\xad3\x1e\x8fG\xac\x96\xcfh\x9d4z\x06\xac\x96\xf8գ\xa6_n\xb2\xfb\xd9M\xa4\x99\xeeߏvR\x8b\x19,\x82\xf3\xa6\xfa\x8c\xce\x04\xcb\xf1\x0e\xd7RK/\x8d\x1eU\xe8\x99`\x9e\xcdF\x00Lk\xe3\x19M;\xfa\t\xc0\x8d\xf6\xd6(\x85v\xbcA=م\x15\xae\x82T\x02m$^\xae\u07bf\x9b\xbc\xff\xed\xe4\xdd\b@\xb3\ng\xb0b|\x17j\xe7\x8de\x1bT\x86'\x92\x93=*\xb4f\"\xcd\xc8\xd5\xc8醍5\xa1\x9e\xc1y!Qȷ'\xce?Db\xcbD\xec>\x13\x8b\xebJ:\xff\xe7\xe1=\xf7\xd2\xf9\xb8\xafV\xc125\xc4V\xdc\xe2\xb6\xc6\xfa\xbf\x9c\xaf\x1e\xc3ʩ\xb4\"\xf5&(f\a\x8e\x8f\x00\x1c75\xce \x9e\xae\x19G1\x02Ȫ\x89\xd4\xc6\xc0\x84\x88\xcaf\xea\xd1J\xed\xd1.\x8c\n\x95>\xdd%\xd0q+k\x1f\x95\x99d\x81,\f\x14i\xc0y\xe6\x83\x03\x17\xf8\x16\x98\x83\xf9\x9eI\xc5V\n\xa7\x7fլ\xfc\x1f\xe9\x01\xfc\xe2\x8c~d~;\x83I:5\xa9\xb7̕\xd5d\xa3\xc7ƌ?\x92\x00\xce[\xa97},\xdd3矙\x92\"r\xf2$+\x04\xe9\xc0o\x11\x14s\x1eQ\x89:\xea\xe7Tu\xee\xba`\x9bX\x81\xe7\x16\x95\xc4?\xcdd\xee\x1bd\x8b\x7fO\xb8\xc5\x13I\xe7YU_Нop\x88\u0605*\xeep͂\xf2MQ\xc9J\xaa闗b\xd5\xc8'\"\x9d\xba\xb8\xf1\xeeb.ݺ2F!KTҮ\xfd\xfb\xe4\x85|\x8b\x15\x9b\xe5ͦF=\x7f\xfc\xf8\xfc\xd3\xf2b\x1a\xfa\x1c\xa9\x15\x14d8ְ\xcd\x16-\xc2s\x8c\xbfd7\x97E;\xd1\x040\xab_\x90\xfb\xb3\x11kkj\xb4^\x96`I\xa3\x91\x8b\x1a\xb3-\x9e\xfe5\xbeX\x03 1\xd2)\x10\x94\x940\xf9U\x8e\x1f\x14Yr0k\xf0[\xe9\xc0bmѡNi\x8a\xa6\x99\xce\fNZ\xa4\x97h\x89\f\xc5vP\x82r\xd9\x1e\xad\a\x8b\xdcl\xb4\xfclj\xb6\x03o\xb23{t\x1eb\x84j\xa6\xc8Y\x03\xbe\x05\xa6E\x8brŎ`\x91\ue120\x1b\xf4\xe2\x01\xd7\xe6\xe3\x13E\x83\xd4k3\x83\xad\xf7\xb5\x9bM\xa7\x1b\xe9K\x86榪\x82\x96\xfe8\x8d\xc9V\xae\x827\xd6M\x05\xeeQM\x9d܌\x99\xe5[\xe9\x91\xfb`q\xcaj9\x8e\x82\xe8\x94R+\xf1\xa3\xcd9\xdd]\\\xdb\t\xe94bJ}\x85y(\xbd&\x97I\xa4\x92\x88g+\xd0\x14\xa9\xee\xf3\x1f\x97OP8I\x96JF9o\xed\xe8\xa5؇\xb4)\xf5\x1am:\xb7\xb6\xa6\x8a4Q\x8b\xdaH\xed\xe3\x0f\xae$j\x0f.\xac*\xe9\xc9\r\xfe\x1e\xd0y2]\x9b\xec\"V1X!\x84:&\x89\xf6\x86\x8f\x1a\x16\xacB\xb5`\x0e\xbf\xb3\xad\xc8*nLF\xb8\xc9Z\xcd\xda\xdcޜ\xd4\xdbX(5u\xc0\xb4\xbd\xd9`Y#\xbf\x88;\x81NZ\x8a\f\xcf<\xc6\xe8j)(\xa7\x8a\xe1\xa2\\F\x7f\x92\xa0\xc18G\xe7>\x19\x81\xed\x95\x16\xcb\xf3\xd3\xc6\v\x1ek\xb4\x95t\xb1\xbc\xc2\xda\xd8v\xe5a\xa7L\xde\x1c%\xe3\xb5\r\x0e\x80:T]F\xc6\xf0\x19\x99x\xd0\xea8\xb0\xf47+}\xf7\xa2\x01C\xd2H,.\x8f\x9a?\xa2\x95F\\\x11\xfeCk\xfbI\x05[s\x80u\xf4\x7f\xedՑr\x97;j\xde\xcd\xdae\xcc\x1f?\x96\f\x9eb+\af\xd6\xd5\x04\xe69\xa8\xcd\x1aށ\x90\x8e\x80\x84\x8bD\xbb\xca\xd2AE\xa01\x03oë\xc4\xe7F\xaf\xe5\xa6+t\x13\x1b\ry\xcc\x15\xd2-\xcd-\xe2M\x94\xb5\xc8;jk\xf6R\xa0\x1dS|ȵ䙓`S\x05YKT\xa2\x93\x9b\x06\xa3,\x8abQPP3uņ\x8b\xd3ƈ\xa4\x99\xd4Ƀ\xcf\x04b\xae\xb1U.\xcdڣ\x16خ6\x91\x1b\x13\x13\x9aC\x01\a\xe9\xb7)S\xaa\xbe\xb8\x83\x17c\x8f\xc6\x0e\x8f}\xd3-ޟ\xb6H;S\xe1Ep\xc8-\xfa\xe8m\xa8\xc8}ȕ&\x00\x9f\x82\x8b\xb9\xb6\x9d'ʈ\x80\xaf\x9c\xdeᱫh\xb8f\xdc\f\x85\x06X\x8e j\x06?\xfcp]\xa4Nu+\x83\xa0{\x11\xd4\xe2\x1a-\xea\x0e\x9a(\xe3)\xd6(r\x1a\xf20\\\xaf\x91{\xb9Gu\x8c5\x89\x92\xe7[X\x05\x0f\"`\xb4\x1a\xe3\xbb\x03\xb3\xc2\x017Uͼ\\I%\xfd\x11\xa4\x1b\xa0ϔ2\a\x14\xd9\xe2X\xd5\xfe8\x81\x8f\xday\xa69\xba\x13\x0e\"\x8d%W`:\xed\xcaQ\x1c\x01\x1d\xb3}90\x91\xaf\x8c\xf3\xc0ђ;\xaa#\x1c\xacћAa\x1f\xee\x1ef0\x17\x02\x8cߢ%\a\\\aUb\xa6\x01\xf9\xdeFd\xf1\x16\x82\x14\x7f\x18\xa0\xd5SZ\xa9\x9f\xb4\x1a=\xc6\xea*\fwTX9\xd6\xdeM\xcd\x1e\xed^\xe2az0v'\xf5fL\u008es\"\x9b\xc6.q\xfac\xfc\xf3\"\xefw\xd6\xd4\xf0\xa5Ѹβ\xcb|!\xec\xab[\x1d.\b\x83N\xbf\xf1\xa0\x11\x05H\xdf\x04\x01۰\x9apS5\x98\x1e;\xb9q\xd3\x06\xed\xa9t.\xa0\x9b\xfe\xf4\xf3\xef\x7f\xf7M\x8en\xea\x94\no\x88\xcfeLgG\x92\"\x9a\x86|w\x99\xc2\xccX \x8cD\xd1[\xe5\xf0Lţ/\xb3\xf4\xb5\x1e\xcdQ\xbc\xba\xafD\xee\xb0[\x1d_ț\x00_\xc7\r\xedU\xac\x1e\xa7\xdd̛J\xf2Q[\xda\x14\xda/\xa7\xd8ҏI-$'\xfc~\x99\x1aK\x9f*.ڶ\x1e5\xb4\x1b\xb9\xa1\x82Я\xa6$n\x86CW8~h\xee=w\xf7\xa9:e\x88\xe3\xd0\x13\xb4v\xa0\x91 \x10\xb3]=ǚ\xc0\x8d֔\x8c\xbd\x01v\xaato\\\xbbĿ\xb2@\xac\x02\xdfa\x8f\xe2;\xa2|\x88\x1b\x8b\x8e\xd31\xe2%8\x8c\xb5\xf7\x1a\x1bp=\"8[\xa0\xbd\x85\x97Ŝ6\x9eP\x12\x83\xc5\x1cVA\v\x85\x85\xa3\x18\xef{\xb4r}쿋\xc6\xd3\xfd\xb2h5\x02\xcc\xdc\x1a\x16\xdd\xf6ːJ\xf8\fV\xc7\x1eHx\x83\x90\xb5ŵ\xfcz\x83\x90\x8fqcQx\xcd\xfc\x16\xa4vR \xb0\x1e\xf5'\xac> \xe8\t\xfe=\xe4\x9c\xf3\r\xe6y)7$v^\x93\x1e\x8a\x8e\xaf\xc4\xcfc\xdev\xd2B\xf9\x9d\v\xf8e+0\x14ǽ\x12\xedO\xefN\x7fJ\x00\x9b\xf7 \xa5\vf\x9e\xbb'^\x00\xea\xe5\xf5\xab/\x98\t\x16\x1ak\xd1\xd5F\vj\xabo\x83\xe9g\x96\xffw`\xbd߬\xe3\xcb,\xd7Z+V\xb8\xa9S\x8d/}\xaf\xeeU\xd3\xfbg\xb3\x134+\x87v\xdfhW[2~\x97.\xb5\x17U6ZW\xe9\b\xa2\x05\x1d\xc1{DN\x93Ѩ\xe7\xc8\x1d\xd6\x16\xa9\x84\x89\x19\tg\xe3Im\x0et\xbaA.AO\xa3S\xc1\xa7\xfe\x9di\x91_Nh\xa9\x87\xf2A*E \xc0beH[\xd4zXB\xac,\xe2\xc9\xfdo&\xef\xfe\x7fm\xb1b\xceS\x97\x8b\xe23\xeee\xf7\xf9\xf06}\xdfw\xa8\x94\xf4p\n\x1a\xfa\U00065f28Lm\xde\xf6\x05\xd6R\x11.m\xe4\x8e\x1b\xe0A\xcf\xe3\xf7\x87\xe5\xfd\x1b\x17\xf1$j\xef\xe0@\x16t\x91%j\xecL~\xc5\n\xceS\x15\xb9\xee\x00\xcd.C\x1bPFo\b\x80\xa7'-\x82xɟ\x8c\x05\x81\x9e\xaa\x95\xde\x00\xdf2\xbd\xa1\xd8\xe8K\xfa\x91\xe3\xcc~\x93Qr\x9fA\x0f\x91z\xc0=n\xb2\xe8\x93\xec\xeb\xdb^c\xcd\xe1o\r'\xfe\xb3i\xcfO\xda-\xc5\x0f%\xdbb\x8a\xf6b)\xe6\xa4\xe8\xb1?\x7f\x7f8\x8fo\x7f\x04\xe9~\xdc\xf8V\xf5\xfcW\x9fc:\x9fa~\x15ʩ\b\xe9^\x85ϟҮ\xf4(\x9d\x8f\x00[\x99\xe0{\xaa\x7f\xc3\xe1{\x83:~qz\r\x8f\xf1;\xda5\x80B{\x8aEx\xb06>\\\x97\a\u0558*\xfa\xea\xd2\xed)x\xde\xfa\xdc\xd7\\\xeb~\f\xbcA\xae\xde:ݙL\xb5\xb6a\u05ec\xe4\xe6LX\x9d>G\xcc\xe0\x9f\xff\x1e\xfd'\x00\x00\xff\xff\xa1\a\xb8\x04\xa5\x1e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVMs\xdb6\x10\xbd\xebW\xecL\xaf%\x15O{\xe8\xf0\xd689x\xdaf4v&w\bX\x89\x88A\x00\xdd]\xc8u?\xfe{\a\x00)K\x14\xe5$\x97\xf0&`\xb1\xfb\xf0\xde\ue0da\xa6Y\xa9h?!\xb1\r\xbe\x03\x15-\xfe%\xe8\xf3/n\x1f\x7f\xe1ֆ\xf5\xe1f\xf5h\xbd\xe9\xe06\xb1\x84\xe1\x1e9$\xd2\xf8\x0ew\xd6[\xb1\xc1\xaf\x06\x14e\x94\xa8n\x05\xa0\xbc\x0f\xa2\xf22\xe7\x9f\x00:x\xa1\xe0\x1cR\xb3G\xdf>\xa6-n\x93u\x06\xa9$\x9fJ\x1f\u07b47?\xb7oV\x00^\r\u0601A\x87\x82[\xa5\x1fS$\xfc3!\v\xb7\atH\xa1\xb5a\xc5\x11uο\xa7\x90b\a/\x1b\xf5\xfcX\xbb\xe2~WR\xbd-\xa9\xeek\xaa\xb2\xeb,\xcbo\xd7\"~\xb7cTt\x89\x94[\x06T\x02\xd8\xfa}r\x8a\x16CV\x00\xacC\xc4\x0e>dXQi4+\x80\xf1\xda\x05f\x03ʘB\xa4r\x1b\xb2^\x90n\x83K\xc3D`\x03\x06Y\x93\x8dR\x88\xfa\xd8c\xb9\"\x84\x1dH\x8fPˁ\x04\xd8\xe2\x88\xc0\x94s\x00\x9f9\xf8\x8d\x92\xbe\x836\xf3\xd5\xd6\xd0\fd\f\xa8T\xbf\x9d/\xcbs\x06\xccB\xd6\xef\xafA`Q\x92x\x02Q\xea\xda\xe0\x81N\xf8=\aP\xe2\xdb\xd8+>\xaf\xfeP6\xaeU\xae1\x87\x9bʴ\xeeqP\xdd\x18\x1b\"\xfa_7w\x9f~z8[\x86s\xac\v҂eP\x13\xd2L\\e\r\x82G\b\x04C\xa0\x89Un\x8fI#\x85\x88$vj\xad\xfa\x9d\f\xcf\xc9\xea\f¿\xcd\xd9\x1e@F]O\x81\xc9S\x84\\H\x1c\x9b\x02\xcdx\xd1J\xaee \x8c\x84\x8c\xbe\xceU^V\x1e\xc2\xf63jig\xa9\x1f\x90r\x1a\xe0>$g\xf2\xf0\x1d\x90\x04\bu\xd8{\xfb\xf717\xe7{\xe7\xa2NI\xa1$\xb7\x9dW\x0e\x0e\xca%\xfc\x11\x947\xb3̃z\x06\xc2\\\x13\x92?\xc9W\x0e\xf0\x1c\xc7\x1f\x99D\xebw\xa1\x83^$r\xb7^\xef\xadL\x96\xa2\xc30$o\xe5y]\xdc\xc1n\x93\x04\xe2\xb5\xc1\x03\xba5\xdb}\xa3H\xf7VPK\"\\\xabh\x9br\x11_l\xa5\x1d\xcc\x0f4\x9a\x10\x9f\x95\xbd\xe8\x9e\xfa\x15\x17\xf8\x06y\xb2'\xd4\x1e\xa9\xa9\xea\x15_T\xc8K\x99\xba\xfb\xf7\x0f\x1faBR\x95\xaa\xa2\xbc\x84^\xf02\xe9\x93ٴ~\x87T\xcf\xed(\f%'z\x13\x83\xf5R~hg\xd1\vp\xda\x0eVx\xea\xd8,\xdd<\xedm\xb1\xdd\xec\x00)\x1a%h\xe6\x01w\x1enՀ\xeeV1~g\xad\xb2*\xdcd\x11\xbeJ\xad\xd3\xc7d\x1e\\\xe9=٘\x9e\x81+\xd2.\f\xffCD\x9d\xc5\xcd\xfc\xe6\xd3vgu\x1d\xab] x\xea\xad\xee\xa7\xe1\x9f\xd1t4\x8as\xfe\x96\x8d!\x7f/v;߹zy(\"[\xc2Y\xc36p\xe1ݯ\xf3RL\xf5\x1b\x99\xa9\x8e>r\xa3\x13Qi\xbe\xa3ϫ\xa5C_\xcb\x05\x12\x05\xbaX\x9d\x81z_\x82\xca?\x06e=\x83\xf2\xcf\xe3A\x90^\t Date: Mon, 23 Dec 2024 15:37:27 +0800 Subject: [PATCH 06/16] add repo maintain result in history Signed-off-by: Lyndon-Li --- .../backup_repository_controller.go | 22 ++++++------------- .../backup_repository_controller_test.go | 14 ++++++------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/pkg/controller/backup_repository_controller.go b/pkg/controller/backup_repository_controller.go index 70331d686..d41f54779 100644 --- a/pkg/controller/backup_repository_controller.go +++ b/pkg/controller/backup_repository_controller.go @@ -328,27 +328,19 @@ func (r *BackupRepoReconciler) runMaintenanceIfDue(ctx context.Context, req *vel } func updateRepoMaintenanceHistory(repo *velerov1api.BackupRepository, result velerov1api.BackupRepositoryMaintenanceResult, startTime time.Time, completionTime time.Time, message string) { - length := defaultMaintenanceStatusQueueLength - if len(repo.Status.RecentMaintenanceStatus) < defaultMaintenanceStatusQueueLength { - length = len(repo.Status.RecentMaintenanceStatus) + 1 - } - - lru := make([]velerov1api.BackupRepositoryMaintenanceStatus, length) - - if len(repo.Status.RecentMaintenanceStatus) >= defaultMaintenanceStatusQueueLength { - copy(lru[:length-1], repo.Status.RecentMaintenanceStatus[len(repo.Status.RecentMaintenanceStatus)-defaultMaintenanceStatusQueueLength+1:]) - } else { - copy(lru[:length-1], repo.Status.RecentMaintenanceStatus[:]) - } - - lru[length-1] = velerov1api.BackupRepositoryMaintenanceStatus{ + latest := velerov1api.BackupRepositoryMaintenanceStatus{ Result: result, StartTimestamp: &metav1.Time{Time: startTime}, CompleteTimestamp: &metav1.Time{Time: completionTime}, Message: message, } - repo.Status.RecentMaintenanceStatus = lru + startingPos := 0 + if len(repo.Status.RecentMaintenance) >= defaultMaintenanceStatusQueueLength { + startingPos = len(repo.Status.RecentMaintenance) - defaultMaintenanceStatusQueueLength + 1 + } + + repo.Status.RecentMaintenance = append(repo.Status.RecentMaintenance[startingPos:], latest) } func dueForMaintenance(req *velerov1api.BackupRepository, now time.Time) bool { diff --git a/pkg/controller/backup_repository_controller_test.go b/pkg/controller/backup_repository_controller_test.go index 5b6c7c8cb..376b17ce8 100644 --- a/pkg/controller/backup_repository_controller_test.go +++ b/pkg/controller/backup_repository_controller_test.go @@ -503,7 +503,7 @@ func TestUpdateRepoMaintenanceHistory(t *testing.T) { Name: "repo", }, Status: velerov1api.BackupRepositoryStatus{ - RecentMaintenanceStatus: []velerov1api.BackupRepositoryMaintenanceStatus{ + RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, @@ -519,7 +519,7 @@ func TestUpdateRepoMaintenanceHistory(t *testing.T) { Name: "repo", }, Status: velerov1api.BackupRepositoryStatus{ - RecentMaintenanceStatus: []velerov1api.BackupRepositoryMaintenanceStatus{ + RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, @@ -545,7 +545,7 @@ func TestUpdateRepoMaintenanceHistory(t *testing.T) { Name: "repo", }, Status: velerov1api.BackupRepositoryStatus{ - RecentMaintenanceStatus: []velerov1api.BackupRepositoryMaintenanceStatus{ + RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{ { StartTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 24)}, CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)}, @@ -655,10 +655,10 @@ func TestUpdateRepoMaintenanceHistory(t *testing.T) { t.Run(test.name, func(t *testing.T) { updateRepoMaintenanceHistory(test.backupRepo, test.result, standardTime, standardTime.Add(time.Hour), "fake-message-0") - for at := range test.backupRepo.Status.RecentMaintenanceStatus { - assert.Equal(t, test.expectedHistory[at].StartTimestamp.Time, test.backupRepo.Status.RecentMaintenanceStatus[at].StartTimestamp.Time) - assert.Equal(t, test.expectedHistory[at].CompleteTimestamp.Time, test.backupRepo.Status.RecentMaintenanceStatus[at].CompleteTimestamp.Time) - assert.Equal(t, test.expectedHistory[at].Message, test.backupRepo.Status.RecentMaintenanceStatus[at].Message) + for at := range test.backupRepo.Status.RecentMaintenance { + assert.Equal(t, test.expectedHistory[at].StartTimestamp.Time, test.backupRepo.Status.RecentMaintenance[at].StartTimestamp.Time) + assert.Equal(t, test.expectedHistory[at].CompleteTimestamp.Time, test.backupRepo.Status.RecentMaintenance[at].CompleteTimestamp.Time) + assert.Equal(t, test.expectedHistory[at].Message, test.backupRepo.Status.RecentMaintenance[at].Message) } }) } From 14e71fa2cdba2a536cc52c269a9122523e71d048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wenkai=20Yin=28=E5=B0=B9=E6=96=87=E5=BC=80=29?= Date: Mon, 23 Dec 2024 16:26:22 +0800 Subject: [PATCH 07/16] Bug fix: increase the WaitGroup counter before start the goroutine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fix: increase the WaitGroup counter before start the goroutine Signed-off-by: Wenkai Yin(尹文开) --- pkg/backup/backup.go | 4 +- pkg/backup/backup_test.go | 86 +++++++++++++++++++-------------------- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index 3d7a3a6e2..a430712fd 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -697,6 +697,7 @@ func (kb *kubernetesBackupper) backupItemBlock(ctx context.Context, itemBlock Ba if len(postHookPods) > 0 { itemBlock.Log.Debug("Executing post hooks") + itemBlock.itemBackupper.hookTracker.AsyncItemBlocks.Add(1) go kb.handleItemBlockPostHooks(ctx, itemBlock, postHookPods) } @@ -735,7 +736,6 @@ func (kb *kubernetesBackupper) handleItemBlockPreHooks(itemBlock BackupItemBlock // The hooks cannot execute until the PVBs to be processed func (kb *kubernetesBackupper) handleItemBlockPostHooks(ctx context.Context, itemBlock BackupItemBlock, hookPods []itemblock.ItemBlockItem) { log := itemBlock.Log - itemBlock.itemBackupper.hookTracker.AsyncItemBlocks.Add(1) defer itemBlock.itemBackupper.hookTracker.AsyncItemBlocks.Done() if err := kb.waitUntilPVBsProcessed(ctx, log, itemBlock, hookPods); err != nil { @@ -806,7 +806,7 @@ func (kb *kubernetesBackupper) waitUntilPVBsProcessed(ctx context.Context, log l return allProcessed, nil } - return wait.PollUntilContextCancel(ctx, 5*time.Second, false, checkFunc) + return wait.PollUntilContextCancel(ctx, 5*time.Second, true, checkFunc) } func (kb *kubernetesBackupper) backupItem(log logrus.FieldLogger, gr schema.GroupResource, itemBackupper *itemBackupper, unstructured *unstructured.Unstructured, preferredGVR schema.GroupVersionResource, itemBlock *BackupItemBlock) bool { diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index b6355b1fd..eb25d65f0 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -3464,59 +3464,57 @@ func TestBackupWithHooks(t *testing.T) { wantBackedUp []string wantHookExecutionLog []test.HookExecutionEntry }{ - /* - { - name: "pre hook with no resource filters runs for all pods", - backup: defaultBackup(). - Hooks(velerov1.BackupHooks{ - Resources: []velerov1.BackupResourceHookSpec{ - { - Name: "hook-1", - PreHooks: []velerov1.BackupResourceHook{ - { - Exec: &velerov1.ExecHook{ - Command: []string{"ls", "/tmp"}, - }, + { + name: "pre hook with no resource filters runs for all pods", + backup: defaultBackup(). + Hooks(velerov1.BackupHooks{ + Resources: []velerov1.BackupResourceHookSpec{ + { + Name: "hook-1", + PreHooks: []velerov1.BackupResourceHook{ + { + Exec: &velerov1.ExecHook{ + Command: []string{"ls", "/tmp"}, }, }, }, }, - }). - Result(), - apiResources: []*test.APIResource{ - test.Pods( - builder.ForPod("ns-1", "pod-1").Result(), - builder.ForPod("ns-2", "pod-2").Result(), - ), - }, - wantExecutePodCommandCalls: []*expectedCall{ - { - podNamespace: "ns-1", - podName: "pod-1", - hookName: "hook-1", - hook: &velerov1.ExecHook{ - Command: []string{"ls", "/tmp"}, - }, - err: nil, }, - { - podNamespace: "ns-2", - podName: "pod-2", - hookName: "hook-1", - hook: &velerov1.ExecHook{ - Command: []string{"ls", "/tmp"}, - }, - err: nil, + }). + Result(), + apiResources: []*test.APIResource{ + test.Pods( + builder.ForPod("ns-1", "pod-1").Result(), + builder.ForPod("ns-2", "pod-2").Result(), + ), + }, + wantExecutePodCommandCalls: []*expectedCall{ + { + podNamespace: "ns-1", + podName: "pod-1", + hookName: "hook-1", + hook: &velerov1.ExecHook{ + Command: []string{"ls", "/tmp"}, }, + err: nil, }, - wantBackedUp: []string{ - "resources/pods/namespaces/ns-1/pod-1.json", - "resources/pods/namespaces/ns-2/pod-2.json", - "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", - "resources/pods/v1-preferredversion/namespaces/ns-2/pod-2.json", + { + podNamespace: "ns-2", + podName: "pod-2", + hookName: "hook-1", + hook: &velerov1.ExecHook{ + Command: []string{"ls", "/tmp"}, + }, + err: nil, }, }, - */ + wantBackedUp: []string{ + "resources/pods/namespaces/ns-1/pod-1.json", + "resources/pods/namespaces/ns-2/pod-2.json", + "resources/pods/v1-preferredversion/namespaces/ns-1/pod-1.json", + "resources/pods/v1-preferredversion/namespaces/ns-2/pod-2.json", + }, + }, { name: "post hook with no resource filters runs for all pods", backup: defaultBackup(). From 623e023bb38d672e15a2985a8b4929eff499de66 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 23 Dec 2024 19:04:40 +0800 Subject: [PATCH 08/16] wait node-agent for Windows Signed-off-by: Lyndon-Li --- pkg/cmd/cli/install/install.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/cli/install/install.go b/pkg/cmd/cli/install/install.go index e725c913d..e0a19faf1 100644 --- a/pkg/cmd/cli/install/install.go +++ b/pkg/cmd/cli/install/install.go @@ -398,7 +398,9 @@ func (o *Options) Run(c *cobra.Command, f client.Factory) error { if _, err = install.NodeAgentIsReady(dynamicFactory, o.Namespace); err != nil { return errors.Wrap(err, errorMsg) } + } + if o.UseNodeAgentWindows { fmt.Println("Waiting for node-agent-windows daemonset to be ready.") if _, err = install.NodeAgentWindowsIsReady(dynamicFactory, o.Namespace); err != nil { return errors.Wrap(err, errorMsg) From 938dd3c6615837ee19f06249f546eb019f5a62b5 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Mon, 23 Dec 2024 10:52:59 +0800 Subject: [PATCH 09/16] Modify the Init logic to fix the migration case error. Signed-off-by: Xun Jiang --- test/e2e/migration/migration.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/e2e/migration/migration.go b/test/e2e/migration/migration.go index 8999a805f..7eb57daae 100644 --- a/test/e2e/migration/migration.go +++ b/test/e2e/migration/migration.go @@ -67,8 +67,12 @@ func MigrationWithFS() { } func (m *migrationE2E) Init() error { + By("Call the base E2E init", func() { + Expect(m.TestCase.Init()).To(Succeed()) + }) + By("Skip check", func() { - if m.VeleroCfg.DefaultClusterContext == "" && m.VeleroCfg.StandbyClusterContext == "" { + if m.VeleroCfg.DefaultClusterContext == "" || m.VeleroCfg.StandbyClusterContext == "" { Skip("Migration test needs 2 clusters") } @@ -81,10 +85,6 @@ func (m *migrationE2E) Init() error { } }) - By("Call the base E2E init", func() { - Expect(m.TestCase.Init()).To(Succeed()) - }) - m.kibishiiData = *kibishii.DefaultKibishiiData m.kibishiiData.ExpectedNodes = 3 m.CaseBaseName = "migration-" + m.UUIDgen From 9486bd0acb117867faed054f67e1fc443837bd0b Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Mon, 23 Dec 2024 14:58:52 +0800 Subject: [PATCH 10/16] Skip the deprecation message for the dry-run install CLI JSON output. Signed-off-by: Xun Jiang --- test/util/velero/install.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/util/velero/install.go b/test/util/velero/install.go index 25e38fbaf..027ca4304 100644 --- a/test/util/velero/install.go +++ b/test/util/velero/install.go @@ -23,6 +23,7 @@ import ( "fmt" "os" "os/exec" + "strings" "time" "github.com/pkg/errors" @@ -413,6 +414,15 @@ func createVeleroResources(ctx context.Context, cli, namespace string, args []st return errors.Wrapf(err, "failed to run velero install dry run command, stdout=%s, stderr=%s", stdout, stderr) } + // From v1.15, the Restic uploader is deprecated, + // and a warning message is printed for the install CLI. + // Need to skip the deprecation of Restic message before the generated JSON. + // Redirect to the stdout to the first curly bracket to skip the warning. + if stdout[0] != '{' { + newIndex := strings.Index(stdout, "{") + stdout = stdout[newIndex:] + } + resources := &unstructured.UnstructuredList{} if err := json.Unmarshal([]byte(stdout), resources); err != nil { return errors.Wrapf(err, "failed to unmarshal the resources: %s", stdout) From e68dca0112724c4b8af924f55be704831d4dae44 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Tue, 24 Dec 2024 11:19:02 +0800 Subject: [PATCH 11/16] Bump Restic go.mod to fix CVEs. Signed-off-by: Xun Jiang --- hack/fix_restic_cve.txt | 49 ++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/hack/fix_restic_cve.txt b/hack/fix_restic_cve.txt index aca798259..f974f098b 100644 --- a/hack/fix_restic_cve.txt +++ b/hack/fix_restic_cve.txt @@ -1,5 +1,5 @@ diff --git a/go.mod b/go.mod -index 5f939c481..1caa51275 100644 +index 5f939c481..95d29c82b 100644 --- a/go.mod +++ b/go.mod @@ -24,32 +24,32 @@ require ( @@ -9,17 +9,18 @@ index 5f939c481..1caa51275 100644 - golang.org/x/crypto v0.5.0 - golang.org/x/net v0.5.0 - golang.org/x/oauth2 v0.4.0 -+ golang.org/x/crypto v0.21.0 -+ golang.org/x/net v0.23.0 -+ golang.org/x/oauth2 v0.7.0 - golang.org/x/sync v0.1.0 +- golang.org/x/sync v0.1.0 - golang.org/x/sys v0.4.0 - golang.org/x/term v0.4.0 - golang.org/x/text v0.6.0 - google.golang.org/api v0.106.0 -+ golang.org/x/sys v0.18.0 -+ golang.org/x/term v0.18.0 -+ golang.org/x/text v0.14.0 ++ golang.org/x/crypto v0.31.0 ++ golang.org/x/net v0.33.0 ++ golang.org/x/oauth2 v0.7.0 ++ golang.org/x/sync v0.10.0 ++ golang.org/x/sys v0.28.0 ++ golang.org/x/term v0.27.0 ++ golang.org/x/text v0.21.0 + google.golang.org/api v0.114.0 ) @@ -62,7 +63,7 @@ index 5f939c481..1caa51275 100644 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum -index 026e1d2fa..27d4207f4 100644 +index 026e1d2fa..d164b17e6 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ @@ -126,19 +127,19 @@ index 026e1d2fa..27d4207f4 100644 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= ++golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= ++golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -@@ -189,11 +189,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL +@@ -189,17 +189,17 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= ++golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= ++golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= @@ -147,27 +148,35 @@ index 026e1d2fa..27d4207f4 100644 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ++golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= ++golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= + golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -214,17 +214,17 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= ++golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= ++golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= ++golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= ++golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= ++golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= ++golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= From 4e0a0e0b7233a8a1f7b2ed6a4645800d8d3d175b Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 24 Dec 2024 14:26:02 +0800 Subject: [PATCH 12/16] fail fs-backup for windows nodes Signed-off-by: Lyndon-Li --- pkg/util/kube/node.go | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/pkg/util/kube/node.go b/pkg/util/kube/node.go index ec0005b53..30eaefb13 100644 --- a/pkg/util/kube/node.go +++ b/pkg/util/kube/node.go @@ -31,13 +31,17 @@ func IsLinuxNode(ctx context.Context, nodeName string, client client.Client) err return errors.Wrapf(err, "error getting node %s", nodeName) } - if os, found := node.Labels["kubernetes.io/os"]; !found { + os, found := node.Labels["kubernetes.io/os"] + + if !found { return errors.Errorf("no os type label for node %s", nodeName) - } else if os != "linux" { - return errors.Errorf("os type %s for node %s is not linux", os, nodeName) - } else { - return nil } + + if os != "linux" { + return errors.Errorf("os type %s for node %s is not linux", os, nodeName) + } + + return nil } func WithLinuxNode(ctx context.Context, client client.Client, log logrus.FieldLogger) bool { @@ -51,16 +55,25 @@ func WithWindowsNode(ctx context.Context, client client.Client, log logrus.Field func withOSNode(ctx context.Context, client client.Client, osType string, log logrus.FieldLogger) bool { nodeList := new(corev1api.NodeList) if err := client.List(ctx, nodeList); err != nil { - log.Warn("Failed to list nodes, cannot decide existence of windows nodes") + log.Warnf("Failed to list nodes, cannot decide existence of nodes of OS %s", osType) return false } + allNodeLabeled := true for _, node := range nodeList.Items { - if os, found := node.Labels["kubernetes.io/os"]; !found { - log.Warnf("Node %s doesn't have os type label, cannot decide existence of windows nodes") - } else if os == osType { + os, found := node.Labels["kubernetes.io/os"] + + if os == osType { return true } + + if !found { + allNodeLabeled = false + } + } + + if !allNodeLabeled { + log.Warnf("Not all nodes have os type label, cannot decide existence of nodes of OS %s", osType) } return false From f5d13aeb179f3c68a77654fd41813a88e129738a Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 24 Dec 2024 16:19:31 +0800 Subject: [PATCH 13/16] data mover backup for Windows nodes Signed-off-by: Lyndon-Li --- changelogs/unreleased/8555-Lyndon-Li | 1 + pkg/cmd/cli/datamover/backup.go | 19 ++- pkg/controller/data_download_controller.go | 2 +- pkg/controller/data_upload_controller.go | 8 +- pkg/exposer/csi_snapshot.go | 52 ++++++-- pkg/exposer/generic_restore.go | 2 +- pkg/exposer/image.go | 4 +- pkg/exposer/image_test.go | 4 +- pkg/nodeagent/node_agent.go | 22 +++- pkg/nodeagent/node_agent_test.go | 5 +- pkg/util/kube/node.go | 31 ++++- pkg/util/kube/node_test.go | 51 ++++++++ pkg/util/kube/pvc_pv.go | 40 +++++++ pkg/util/kube/pvc_pv_test.go | 131 +++++++++++++++++++++ 14 files changed, 339 insertions(+), 33 deletions(-) create mode 100644 changelogs/unreleased/8555-Lyndon-Li diff --git a/changelogs/unreleased/8555-Lyndon-Li b/changelogs/unreleased/8555-Lyndon-Li new file mode 100644 index 000000000..b209289b7 --- /dev/null +++ b/changelogs/unreleased/8555-Lyndon-Li @@ -0,0 +1 @@ +Fix issue #8418, support data mover backup for Windows nodes \ No newline at end of file diff --git a/pkg/cmd/cli/datamover/backup.go b/pkg/cmd/cli/datamover/backup.go index 4d704b04c..7511fef8e 100644 --- a/pkg/cmd/cli/datamover/backup.go +++ b/pkg/cmd/cli/datamover/backup.go @@ -168,7 +168,24 @@ func newdataMoverBackup(logger logrus.FieldLogger, factory client.Factory, confi return nil, errors.Wrap(err, "error to create client") } - cache, err := ctlcache.New(clientConfig, cacheOption) + var cache ctlcache.Cache + retry := 10 + for { + cache, err = ctlcache.New(clientConfig, cacheOption) + if err == nil { + break + } + + retry-- + if retry == 0 { + break + } + + logger.WithError(err).Warn("Failed to create client cache, need retry") + + time.Sleep(time.Second) + } + if err != nil { cancelFunc() return nil, errors.Wrap(err, "error to create client cache") diff --git a/pkg/controller/data_download_controller.go b/pkg/controller/data_download_controller.go index 347bcfed5..45b367cd8 100644 --- a/pkg/controller/data_download_controller.go +++ b/pkg/controller/data_download_controller.go @@ -182,7 +182,7 @@ func (r *DataDownloadReconciler) Reconcile(ctx context.Context, req ctrl.Request hostingPodLabels := map[string]string{velerov1api.DataDownloadLabel: dd.Name} for _, k := range util.ThirdPartyLabels { - if v, err := nodeagent.GetLabelValue(ctx, r.kubeClient, dd.Namespace, k); err != nil { + if v, err := nodeagent.GetLabelValue(ctx, r.kubeClient, dd.Namespace, k, kube.NodeOSLinux); err != nil { if err != nodeagent.ErrNodeAgentLabelNotFound { log.WithError(err).Warnf("Failed to check node-agent label, skip adding host pod label %s", k) } diff --git a/pkg/controller/data_upload_controller.go b/pkg/controller/data_upload_controller.go index 41795f9d1..6b3464983 100644 --- a/pkg/controller/data_upload_controller.go +++ b/pkg/controller/data_upload_controller.go @@ -803,6 +803,11 @@ func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload return nil, errors.Wrapf(err, "failed to get PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC) } + nodeOS, err := kube.GetPVCAttachingNodeOS(pvc, r.kubeClient.CoreV1(), r.kubeClient.StorageV1(), r.logger) + if err != nil { + return nil, errors.Wrapf(err, "failed to get attaching node OS for PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC) + } + accessMode := exposer.AccessModeFileSystem if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == corev1.PersistentVolumeBlock { accessMode = exposer.AccessModeBlock @@ -810,7 +815,7 @@ func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload hostingPodLabels := map[string]string{velerov1api.DataUploadLabel: du.Name} for _, k := range util.ThirdPartyLabels { - if v, err := nodeagent.GetLabelValue(context.Background(), r.kubeClient, du.Namespace, k); err != nil { + if v, err := nodeagent.GetLabelValue(context.Background(), r.kubeClient, du.Namespace, k, nodeOS); err != nil { if err != nodeagent.ErrNodeAgentLabelNotFound { r.logger.WithError(err).Warnf("Failed to check node-agent label, skip adding host pod label %s", k) } @@ -831,6 +836,7 @@ func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload Affinity: r.loadAffinity, BackupPVCConfig: r.backupPVCConfig, Resources: r.podResources, + NodeOS: nodeOS, }, nil } return nil, nil diff --git a/pkg/exposer/csi_snapshot.go b/pkg/exposer/csi_snapshot.go index 9b9ebe547..043462792 100644 --- a/pkg/exposer/csi_snapshot.go +++ b/pkg/exposer/csi_snapshot.go @@ -73,6 +73,9 @@ type CSISnapshotExposeParam struct { // Resources defines the resource requirements of the hosting pod Resources corev1.ResourceRequirements + + // NodeOS specifies the OS of node that the source volume is attaching + NodeOS string } // CSISnapshotExposeWaitParam define the input param for WaitExposed of CSI snapshots @@ -212,6 +215,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1.Obje csiExposeParam.Resources, backupPVCReadOnly, spcNoRelabeling, + csiExposeParam.NodeOS, ) if err != nil { return errors.Wrap(err, "error to create backup pod") @@ -517,13 +521,14 @@ func (e *csiSnapshotExposer) createBackupPod( resources corev1.ResourceRequirements, backupPVCReadOnly bool, spcNoRelabeling bool, + nodeOS string, ) (*corev1.Pod, error) { podName := ownerObject.Name containerName := string(ownerObject.UID) volumeName := string(ownerObject.UID) - podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace) + podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS) if err != nil { return nil, errors.Wrap(err, "error to get inherited pod info from node-agent") } @@ -567,13 +572,40 @@ func (e *csiSnapshotExposer) createBackupPod( args = append(args, podInfo.logFormatArgs...) args = append(args, podInfo.logLevelArgs...) - userID := int64(0) - affinityList := make([]*kube.LoadAffinity, 0) if affinity != nil { affinityList = append(affinityList, affinity) } + var securityCtx *corev1.PodSecurityContext + nodeSelector := map[string]string{} + podOS := corev1.PodOS{} + if nodeOS == kube.NodeOSWindows { + userID := "ContainerAdministrator" + securityCtx = &corev1.PodSecurityContext{ + WindowsOptions: &corev1.WindowsSecurityContextOptions{ + RunAsUserName: &userID, + }, + } + + nodeSelector[kube.NodeOSLabel] = kube.NodeOSWindows + podOS.Name = kube.NodeOSWindows + } else { + userID := int64(0) + securityCtx = &corev1.PodSecurityContext{ + RunAsUser: &userID, + } + + if spcNoRelabeling { + securityCtx.SELinuxOptions = &corev1.SELinuxOptions{ + Type: "spc_t", + } + } + + nodeSelector[kube.NodeOSLabel] = kube.NodeOSLinux + podOS.Name = kube.NodeOSLinux + } + pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, @@ -602,7 +634,9 @@ func (e *csiSnapshotExposer) createBackupPod( }, }, }, - Affinity: kube.ToSystemAffinity(affinityList), + NodeSelector: nodeSelector, + OS: &podOS, + Affinity: kube.ToSystemAffinity(affinityList), Containers: []corev1.Container{ { Name: containerName, @@ -625,17 +659,9 @@ func (e *csiSnapshotExposer) createBackupPod( TerminationGracePeriodSeconds: &gracePeriod, Volumes: volumes, RestartPolicy: corev1.RestartPolicyNever, - SecurityContext: &corev1.PodSecurityContext{ - RunAsUser: &userID, - }, + SecurityContext: securityCtx, }, } - if spcNoRelabeling { - pod.Spec.SecurityContext.SELinuxOptions = &corev1.SELinuxOptions{ - Type: "spc_t", - } - } - return e.kubeClient.CoreV1().Pods(ownerObject.Namespace).Create(ctx, pod, metav1.CreateOptions{}) } diff --git a/pkg/exposer/generic_restore.go b/pkg/exposer/generic_restore.go index 7a7df9038..b85775389 100644 --- a/pkg/exposer/generic_restore.go +++ b/pkg/exposer/generic_restore.go @@ -354,7 +354,7 @@ func (e *genericRestoreExposer) createRestorePod(ctx context.Context, ownerObjec containerName := string(ownerObject.UID) volumeName := string(ownerObject.UID) - podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace) + podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, kube.NodeOSLinux) if err != nil { return nil, errors.Wrap(err, "error to get inherited pod info from node-agent") } diff --git a/pkg/exposer/image.go b/pkg/exposer/image.go index daf6c1adc..da399cce5 100644 --- a/pkg/exposer/image.go +++ b/pkg/exposer/image.go @@ -38,10 +38,10 @@ type inheritedPodInfo struct { logFormatArgs []string } -func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veleroNamespace string) (inheritedPodInfo, error) { +func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veleroNamespace string, osType string) (inheritedPodInfo, error) { podInfo := inheritedPodInfo{} - podSpec, err := nodeagent.GetPodSpec(ctx, client, veleroNamespace) + podSpec, err := nodeagent.GetPodSpec(ctx, client, veleroNamespace, osType) if err != nil { return podInfo, errors.Wrap(err, "error to get node-agent pod template") } diff --git a/pkg/exposer/image_test.go b/pkg/exposer/image_test.go index 1a2e038f0..18626174b 100644 --- a/pkg/exposer/image_test.go +++ b/pkg/exposer/image_test.go @@ -26,6 +26,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" + "github.com/vmware-tanzu/velero/pkg/util/kube" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/fake" @@ -322,7 +324,7 @@ func TestGetInheritedPodInfo(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) - info, err := getInheritedPodInfo(context.Background(), fakeKubeClient, test.namespace) + info, err := getInheritedPodInfo(context.Background(), fakeKubeClient, test.namespace, kube.NodeOSLinux) if test.expectErr == "" { assert.NoError(t, err) diff --git a/pkg/nodeagent/node_agent.go b/pkg/nodeagent/node_agent.go index 8ed6aacdd..b31b1dda5 100644 --- a/pkg/nodeagent/node_agent.go +++ b/pkg/nodeagent/node_agent.go @@ -157,10 +157,15 @@ func isRunningInNode(ctx context.Context, namespace string, nodeName string, crC return errors.Errorf("daemonset pod not found in running state in node %s", nodeName) } -func GetPodSpec(ctx context.Context, kubeClient kubernetes.Interface, namespace string) (*v1.PodSpec, error) { - ds, err := kubeClient.AppsV1().DaemonSets(namespace).Get(ctx, daemonSet, metav1.GetOptions{}) +func GetPodSpec(ctx context.Context, kubeClient kubernetes.Interface, namespace string, osType string) (*v1.PodSpec, error) { + dsName := daemonSet + if osType == kube.NodeOSWindows { + dsName = daemonsetWindows + } + + ds, err := kubeClient.AppsV1().DaemonSets(namespace).Get(ctx, dsName, metav1.GetOptions{}) if err != nil { - return nil, errors.Wrap(err, "error to get node-agent daemonset") + return nil, errors.Wrapf(err, "error to get %s daemonset", dsName) } return &ds.Spec.Template.Spec, nil @@ -190,10 +195,15 @@ func GetConfigs(ctx context.Context, namespace string, kubeClient kubernetes.Int return configs, nil } -func GetLabelValue(ctx context.Context, kubeClient kubernetes.Interface, namespace string, key string) (string, error) { - ds, err := kubeClient.AppsV1().DaemonSets(namespace).Get(ctx, daemonSet, metav1.GetOptions{}) +func GetLabelValue(ctx context.Context, kubeClient kubernetes.Interface, namespace string, key string, osType string) (string, error) { + dsName := daemonSet + if osType == kube.NodeOSWindows { + dsName = daemonsetWindows + } + + ds, err := kubeClient.AppsV1().DaemonSets(namespace).Get(ctx, dsName, metav1.GetOptions{}) if err != nil { - return "", errors.Wrap(err, "error getting node-agent daemonset") + return "", errors.Wrapf(err, "error getting %s daemonset", dsName) } if ds.Spec.Template.Labels == nil { diff --git a/pkg/nodeagent/node_agent_test.go b/pkg/nodeagent/node_agent_test.go index 11cb83359..a153e1e8c 100644 --- a/pkg/nodeagent/node_agent_test.go +++ b/pkg/nodeagent/node_agent_test.go @@ -31,6 +31,7 @@ import ( clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/util/kube" ) type reactor struct { @@ -229,7 +230,7 @@ func TestGetPodSpec(t *testing.T) { t.Run(test.name, func(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) - spec, err := GetPodSpec(context.TODO(), fakeKubeClient, test.namespace) + spec, err := GetPodSpec(context.TODO(), fakeKubeClient, test.namespace, kube.NodeOSLinux) if test.expectErr == "" { assert.NoError(t, err) assert.Equal(t, *spec, test.expectSpec) @@ -450,7 +451,7 @@ func TestGetLabelValue(t *testing.T) { t.Run(test.name, func(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) - value, err := GetLabelValue(context.TODO(), fakeKubeClient, test.namespace, "fake-label") + value, err := GetLabelValue(context.TODO(), fakeKubeClient, test.namespace, "fake-label", kube.NodeOSLinux) if test.expectErr == "" { assert.NoError(t, err) assert.Equal(t, test.expectedValue, value) diff --git a/pkg/util/kube/node.go b/pkg/util/kube/node.go index 30eaefb13..62ecd4a2f 100644 --- a/pkg/util/kube/node.go +++ b/pkg/util/kube/node.go @@ -21,23 +21,31 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) +const ( + NodeOSLinux = "linux" + NodeOSWindows = "windows" + NodeOSLabel = "kubernetes.io/os" +) + func IsLinuxNode(ctx context.Context, nodeName string, client client.Client) error { node := &corev1api.Node{} if err := client.Get(ctx, types.NamespacedName{Name: nodeName}, node); err != nil { return errors.Wrapf(err, "error getting node %s", nodeName) } - os, found := node.Labels["kubernetes.io/os"] + os, found := node.Labels[NodeOSLabel] if !found { return errors.Errorf("no os type label for node %s", nodeName) } - if os != "linux" { + if os != NodeOSLinux { return errors.Errorf("os type %s for node %s is not linux", os, nodeName) } @@ -45,11 +53,11 @@ func IsLinuxNode(ctx context.Context, nodeName string, client client.Client) err } func WithLinuxNode(ctx context.Context, client client.Client, log logrus.FieldLogger) bool { - return withOSNode(ctx, client, "linux", log) + return withOSNode(ctx, client, NodeOSLinux, log) } func WithWindowsNode(ctx context.Context, client client.Client, log logrus.FieldLogger) bool { - return withOSNode(ctx, client, "windows", log) + return withOSNode(ctx, client, NodeOSWindows, log) } func withOSNode(ctx context.Context, client client.Client, osType string, log logrus.FieldLogger) bool { @@ -61,7 +69,7 @@ func withOSNode(ctx context.Context, client client.Client, osType string, log lo allNodeLabeled := true for _, node := range nodeList.Items { - os, found := node.Labels["kubernetes.io/os"] + os, found := node.Labels[NodeOSLabel] if os == osType { return true @@ -78,3 +86,16 @@ func withOSNode(ctx context.Context, client client.Client, osType string, log lo return false } + +func GetNodeOS(ctx context.Context, nodeName string, nodeClient corev1client.CoreV1Interface) (string, error) { + node, err := nodeClient.Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err != nil { + return "", errors.Wrapf(err, "error getting node %s", nodeName) + } + + if node.Labels == nil { + return "", nil + } + + return node.Labels[NodeOSLabel], nil +} diff --git a/pkg/util/kube/node_test.go b/pkg/util/kube/node_test.go index 9463938eb..e24aa6284 100644 --- a/pkg/util/kube/node_test.go +++ b/pkg/util/kube/node_test.go @@ -26,6 +26,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/builder" + kubeClientFake "k8s.io/client-go/kubernetes/fake" clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" velerotest "github.com/vmware-tanzu/velero/pkg/test" @@ -130,3 +131,53 @@ func TestWithLinuxNode(t *testing.T) { }) } } + +func TestGetNodeOSType(t *testing.T) { + nodeNoOSLabel := builder.ForNode("fake-node").Result() + nodeWindows := builder.ForNode("fake-node").Labels(map[string]string{"kubernetes.io/os": "windows"}).Result() + nodeLinux := builder.ForNode("fake-node").Labels(map[string]string{"kubernetes.io/os": "linux"}).Result() + scheme := runtime.NewScheme() + corev1.AddToScheme(scheme) + tests := []struct { + name string + kubeClientObj []runtime.Object + err string + expectedOSType string + }{ + { + name: "error getting node", + err: "error getting node fake-node: nodes \"fake-node\" not found", + }, + { + name: "no os label", + kubeClientObj: []runtime.Object{ + nodeNoOSLabel, + }, + }, + { + name: "windows node", + kubeClientObj: []runtime.Object{ + nodeWindows, + }, + expectedOSType: "windows", + }, + { + name: "linux node", + kubeClientObj: []runtime.Object{ + nodeLinux, + }, + expectedOSType: "linux", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := kubeClientFake.NewSimpleClientset(test.kubeClientObj...) + osType, err := GetNodeOS(context.TODO(), "fake-node", fakeKubeClient.CoreV1()) + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.Equal(t, test.expectedOSType, osType) + } + }) + } +} diff --git a/pkg/util/kube/pvc_pv.go b/pkg/util/kube/pvc_pv.go index bf7779aaa..bfa7cb32a 100644 --- a/pkg/util/kube/pvc_pv.go +++ b/pkg/util/kube/pvc_pv.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" jsonpatch "github.com/evanphx/json-patch/v5" @@ -427,3 +428,42 @@ func DiagnosePV(pv *corev1api.PersistentVolume) string { diag := fmt.Sprintf("PV %s, phase %s, reason %s, message %s\n", pv.Name, pv.Status.Phase, pv.Status.Reason, pv.Status.Message) return diag } + +func GetPVCAttachingNodeOS(pvc *corev1api.PersistentVolumeClaim, nodeClient corev1client.CoreV1Interface, + storageClient storagev1.StorageV1Interface, log logrus.FieldLogger) (string, error) { + var nodeOS string + var scFsType string + + if value := pvc.Annotations[KubeAnnSelectedNode]; value != "" { + os, err := GetNodeOS(context.Background(), value, nodeClient) + if err != nil { + return "", errors.Wrapf(err, "error to get os from node %s for PVC %s/%s", value, pvc.Namespace, pvc.Name) + } + + nodeOS = os + } + + if pvc.Spec.StorageClassName != nil { + sc, err := storageClient.StorageClasses().Get(context.Background(), *pvc.Spec.StorageClassName, metav1.GetOptions{}) + if err != nil { + return "", errors.Wrapf(err, "error to get storage class %s", *pvc.Spec.StorageClassName) + } + + if sc.Parameters != nil { + scFsType = strings.ToLower(sc.Parameters["csi.storage.k8s.io/fstype"]) + } + } + + if nodeOS != "" { + log.Infof("Deduced node os %s from selected node for PVC %s/%s (fsType %s)", nodeOS, pvc.Namespace, pvc.Name, scFsType) + return nodeOS, nil + } + + if scFsType == "ntfs" { + log.Infof("Deduced Windows node os from fsType for PVC %s/%s", pvc.Namespace, pvc.Name) + return NodeOSWindows, nil + } + + log.Warnf("Cannot deduce node os for PVC %s/%s, default to linux", pvc.Namespace, pvc.Name) + return NodeOSLinux, nil +} diff --git a/pkg/util/kube/pvc_pv_test.go b/pkg/util/kube/pvc_pv_test.go index 52e01ee69..5d0091789 100644 --- a/pkg/util/kube/pvc_pv_test.go +++ b/pkg/util/kube/pvc_pv_test.go @@ -33,6 +33,7 @@ import ( clientTesting "k8s.io/client-go/testing" + "github.com/vmware-tanzu/velero/pkg/builder" velerotest "github.com/vmware-tanzu/velero/pkg/test" ) @@ -1550,3 +1551,133 @@ func TestDiagnosePV(t *testing.T) { }) } } + +func TestGetPVCAttachingNodeOS(t *testing.T) { + storageClass := "fake-storage-class" + nodeNoOSLabel := builder.ForNode("fake-node").Result() + nodeWindows := builder.ForNode("fake-node").Labels(map[string]string{"kubernetes.io/os": "windows"}).Result() + + pvcObj := &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-namespace", + Name: "fake-pvc", + }, + } + + pvcObjWithNode := &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-namespace", + Name: "fake-pvc", + Annotations: map[string]string{KubeAnnSelectedNode: "fake-node"}, + }, + } + + pvcObjWithStorageClass := &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-namespace", + Name: "fake-pvc", + }, + Spec: corev1api.PersistentVolumeClaimSpec{ + StorageClassName: &storageClass, + }, + } + + pvcObjWithBoth := &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-namespace", + Name: "fake-pvc", + Annotations: map[string]string{KubeAnnSelectedNode: "fake-node"}, + }, + Spec: corev1api.PersistentVolumeClaimSpec{ + StorageClassName: &storageClass, + }, + } + + scObjWithoutFSType := &storagev1api.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-storage-class", + }, + } + + scObjWithFSType := &storagev1api.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-storage-class", + }, + Parameters: map[string]string{"csi.storage.k8s.io/fstype": "ntfs"}, + } + + tests := []struct { + name string + pvc *corev1api.PersistentVolumeClaim + kubeClientObj []runtime.Object + expectedNodeOS string + err string + }{ + { + name: "no selected node and storage class", + pvc: pvcObj, + expectedNodeOS: NodeOSLinux, + }, + { + name: "node doesn't exist", + pvc: pvcObjWithNode, + err: "error to get os from node fake-node for PVC fake-namespace/fake-pvc: error getting node fake-node: nodes \"fake-node\" not found", + }, + { + name: "node without os label", + pvc: pvcObjWithNode, + kubeClientObj: []runtime.Object{ + nodeNoOSLabel, + }, + expectedNodeOS: NodeOSLinux, + }, + { + name: "sc doesn't exist", + pvc: pvcObjWithStorageClass, + err: "error to get storage class fake-storage-class: storageclasses.storage.k8s.io \"fake-storage-class\" not found", + }, + { + name: "sc without fsType", + pvc: pvcObjWithStorageClass, + kubeClientObj: []runtime.Object{ + scObjWithoutFSType, + }, + expectedNodeOS: NodeOSLinux, + }, + { + name: "deduce from node os", + pvc: pvcObjWithBoth, + kubeClientObj: []runtime.Object{ + nodeWindows, + scObjWithFSType, + }, + expectedNodeOS: NodeOSWindows, + }, + { + name: "deduce from sc", + pvc: pvcObjWithBoth, + kubeClientObj: []runtime.Object{ + nodeNoOSLabel, + scObjWithFSType, + }, + expectedNodeOS: NodeOSWindows, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) + + var kubeClient kubernetes.Interface = fakeKubeClient + + nodeOS, err := GetPVCAttachingNodeOS(test.pvc, kubeClient.CoreV1(), kubeClient.StorageV1(), velerotest.NewLogger()) + + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, test.expectedNodeOS, nodeOS) + }) + } +} From bc6414672e7e39ba699f10fd18cf48fd81404bed Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 28 Nov 2024 16:02:03 +0800 Subject: [PATCH 14/16] disable block volume data mover on windows Signed-off-by: Lyndon-Li --- pkg/controller/data_upload_controller.go | 12 ++-- pkg/util/kube/node.go | 18 ++++++ pkg/util/kube/node_test.go | 78 ++++++++++++++++++++++++ pkg/util/kube/pvc_pv.go | 7 ++- pkg/util/kube/pvc_pv_test.go | 9 ++- 5 files changed, 118 insertions(+), 6 deletions(-) diff --git a/pkg/controller/data_upload_controller.go b/pkg/controller/data_upload_controller.go index 6b3464983..5d1073b2f 100644 --- a/pkg/controller/data_upload_controller.go +++ b/pkg/controller/data_upload_controller.go @@ -803,14 +803,18 @@ func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload return nil, errors.Wrapf(err, "failed to get PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC) } - nodeOS, err := kube.GetPVCAttachingNodeOS(pvc, r.kubeClient.CoreV1(), r.kubeClient.StorageV1(), r.logger) + accessMode := exposer.AccessModeFileSystem + if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == corev1.PersistentVolumeBlock { + accessMode = exposer.AccessModeBlock + } + + nodeOS, err := kube.GetPVCAttachingNodeOS(pvc, (accessMode == exposer.AccessModeBlock), r.kubeClient.CoreV1(), r.kubeClient.StorageV1(), r.logger) if err != nil { return nil, errors.Wrapf(err, "failed to get attaching node OS for PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC) } - accessMode := exposer.AccessModeFileSystem - if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == corev1.PersistentVolumeBlock { - accessMode = exposer.AccessModeBlock + if err := kube.HasNodeWithOS(context.Background(), nodeOS, r.kubeClient.CoreV1()); err != nil { + return nil, errors.Wrapf(err, "no appropriate node to run data upload for PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC) } hostingPodLabels := map[string]string{velerov1api.DataUploadLabel: du.Name} diff --git a/pkg/util/kube/node.go b/pkg/util/kube/node.go index 62ecd4a2f..da68183a5 100644 --- a/pkg/util/kube/node.go +++ b/pkg/util/kube/node.go @@ -17,6 +17,7 @@ package kube import ( "context" + "fmt" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -99,3 +100,20 @@ func GetNodeOS(ctx context.Context, nodeName string, nodeClient corev1client.Cor return node.Labels[NodeOSLabel], nil } + +func HasNodeWithOS(ctx context.Context, os string, nodeClient corev1client.CoreV1Interface) error { + if os == "" { + return errors.New("invalid node OS") + } + + nodes, err := nodeClient.Nodes().List(ctx, metav1.ListOptions{LabelSelector: fmt.Sprintf("%s=%s", NodeOSLabel, os)}) + if err != nil { + return errors.Wrapf(err, "error listing nodes with OS %s", os) + } + + if len(nodes.Items) == 0 { + return errors.Errorf("node with OS %s doesn't exist", os) + } + + return nil +} diff --git a/pkg/util/kube/node_test.go b/pkg/util/kube/node_test.go index e24aa6284..a26285f5f 100644 --- a/pkg/util/kube/node_test.go +++ b/pkg/util/kube/node_test.go @@ -20,6 +20,7 @@ import ( "context" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -27,6 +28,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/builder" kubeClientFake "k8s.io/client-go/kubernetes/fake" + clientTesting "k8s.io/client-go/testing" clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" velerotest "github.com/vmware-tanzu/velero/pkg/test" @@ -181,3 +183,79 @@ func TestGetNodeOSType(t *testing.T) { }) } } + +func TestHasNodeWithOS(t *testing.T) { + nodeNoOSLabel := builder.ForNode("fake-node-1").Result() + nodeWindows := builder.ForNode("fake-node-2").Labels(map[string]string{"kubernetes.io/os": "windows"}).Result() + nodeLinux := builder.ForNode("fake-node-3").Labels(map[string]string{"kubernetes.io/os": "linux"}).Result() + + scheme := runtime.NewScheme() + corev1.AddToScheme(scheme) + + tests := []struct { + name string + kubeClientObj []runtime.Object + kubeReactors []reactor + os string + err string + }{ + { + name: "os is empty", + err: "invalid node OS", + }, + { + name: "error to list node", + kubeReactors: []reactor{ + { + verb: "list", + resource: "nodes", + reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("fake-list-error") + }, + }, + }, + os: "linux", + err: "error listing nodes with OS linux: fake-list-error", + }, + { + name: "no expected node - no node", + os: "linux", + err: "node with OS linux doesn't exist", + }, + { + name: "no expected node - no node with label", + kubeClientObj: []runtime.Object{ + nodeNoOSLabel, + nodeWindows, + }, + os: "linux", + err: "node with OS linux doesn't exist", + }, + { + name: "succeed", + kubeClientObj: []runtime.Object{ + nodeNoOSLabel, + nodeWindows, + nodeLinux, + }, + os: "windows", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := kubeClientFake.NewSimpleClientset(test.kubeClientObj...) + + for _, reactor := range test.kubeReactors { + fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc) + } + + err := HasNodeWithOS(context.TODO(), test.os, fakeKubeClient.CoreV1()) + if test.err != "" { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/util/kube/pvc_pv.go b/pkg/util/kube/pvc_pv.go index bfa7cb32a..eab07f25b 100644 --- a/pkg/util/kube/pvc_pv.go +++ b/pkg/util/kube/pvc_pv.go @@ -429,11 +429,16 @@ func DiagnosePV(pv *corev1api.PersistentVolume) string { return diag } -func GetPVCAttachingNodeOS(pvc *corev1api.PersistentVolumeClaim, nodeClient corev1client.CoreV1Interface, +func GetPVCAttachingNodeOS(pvc *corev1api.PersistentVolumeClaim, blockAccess bool, nodeClient corev1client.CoreV1Interface, storageClient storagev1.StorageV1Interface, log logrus.FieldLogger) (string, error) { var nodeOS string var scFsType string + if blockAccess { + log.Infof("Use linux node for block access for PVC %s/%s", pvc.Namespace, pvc.Name) + return NodeOSLinux, nil + } + if value := pvc.Annotations[KubeAnnSelectedNode]; value != "" { os, err := GetNodeOS(context.Background(), value, nodeClient) if err != nil { diff --git a/pkg/util/kube/pvc_pv_test.go b/pkg/util/kube/pvc_pv_test.go index 5d0091789..ea3195966 100644 --- a/pkg/util/kube/pvc_pv_test.go +++ b/pkg/util/kube/pvc_pv_test.go @@ -1610,6 +1610,7 @@ func TestGetPVCAttachingNodeOS(t *testing.T) { name string pvc *corev1api.PersistentVolumeClaim kubeClientObj []runtime.Object + blockAccess bool expectedNodeOS string err string }{ @@ -1662,6 +1663,12 @@ func TestGetPVCAttachingNodeOS(t *testing.T) { }, expectedNodeOS: NodeOSWindows, }, + { + name: "block access", + pvc: pvcObjWithBoth, + blockAccess: true, + expectedNodeOS: NodeOSLinux, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -1669,7 +1676,7 @@ func TestGetPVCAttachingNodeOS(t *testing.T) { var kubeClient kubernetes.Interface = fakeKubeClient - nodeOS, err := GetPVCAttachingNodeOS(test.pvc, kubeClient.CoreV1(), kubeClient.StorageV1(), velerotest.NewLogger()) + nodeOS, err := GetPVCAttachingNodeOS(test.pvc, test.blockAccess, kubeClient.CoreV1(), kubeClient.StorageV1(), velerotest.NewLogger()) if err != nil { assert.EqualError(t, err, test.err) From d2a25cd446c2aa2efd2023eb8da6859b07d03ca1 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 28 Nov 2024 17:10:58 +0800 Subject: [PATCH 15/16] fs uploader skip system folders on windows Signed-off-by: Lyndon-Li --- pkg/uploader/kopia/progress.go | 4 +++- pkg/uploader/kopia/snapshot.go | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/uploader/kopia/progress.go b/pkg/uploader/kopia/progress.go index 7f0619f57..9f2498379 100644 --- a/pkg/uploader/kopia/progress.go +++ b/pkg/uploader/kopia/progress.go @@ -138,7 +138,9 @@ func (p *Progress) HashingFile(fname string) {} func (p *Progress) ExcludedFile(fname string, numBytes int64) {} // ExcludedDir statistic the dir been excluded currently -func (p *Progress) ExcludedDir(dirname string) {} +func (p *Progress) ExcludedDir(dirname string) { + p.log.Infof("Excluded dir %s", dirname) +} // FinishedHashingFile which will called when specific file finished hash func (p *Progress) FinishedHashingFile(fname string, numBytes int64) { diff --git a/pkg/uploader/kopia/snapshot.go b/pkg/uploader/kopia/snapshot.go index c80ab155f..fce620eb7 100644 --- a/pkg/uploader/kopia/snapshot.go +++ b/pkg/uploader/kopia/snapshot.go @@ -127,6 +127,10 @@ func setupPolicy(ctx context.Context, rep repo.RepositoryWriter, sourceInfo snap curPolicy.UploadPolicy.ParallelUploadAboveSize = newOptionalInt64(2 << 30) } + if runtime.GOOS == "windows" { + curPolicy.FilesPolicy.IgnoreRules = []string{"/System Volume Information/", "/$Recycle.Bin/"} + } + err := setPolicyFunc(ctx, rep, sourceInfo, curPolicy) if err != nil { return nil, errors.Wrap(err, "error to set policy") From cb22dfc4826146a30b5cceb526b463c65b700302 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 2 Jan 2025 11:35:32 +0800 Subject: [PATCH 16/16] fs uploader and block uploader support Windows nodes Signed-off-by: Lyndon-Li --- changelogs/unreleased/8569-Lyndon-Li | 1 + pkg/controller/data_upload_controller.go | 12 ++++++------ pkg/controller/data_upload_controller_test.go | 5 ++++- pkg/util/kube/pvc_pv.go | 6 +++--- pkg/util/kube/pvc_pv_test.go | 17 +++++++++++++---- 5 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/8569-Lyndon-Li diff --git a/changelogs/unreleased/8569-Lyndon-Li b/changelogs/unreleased/8569-Lyndon-Li new file mode 100644 index 000000000..336a9856c --- /dev/null +++ b/changelogs/unreleased/8569-Lyndon-Li @@ -0,0 +1 @@ +fs uploader and block uploader support Windows nodes \ No newline at end of file diff --git a/pkg/controller/data_upload_controller.go b/pkg/controller/data_upload_controller.go index 5d1073b2f..66f5b67f7 100644 --- a/pkg/controller/data_upload_controller.go +++ b/pkg/controller/data_upload_controller.go @@ -803,12 +803,7 @@ func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload return nil, errors.Wrapf(err, "failed to get PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC) } - accessMode := exposer.AccessModeFileSystem - if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == corev1.PersistentVolumeBlock { - accessMode = exposer.AccessModeBlock - } - - nodeOS, err := kube.GetPVCAttachingNodeOS(pvc, (accessMode == exposer.AccessModeBlock), r.kubeClient.CoreV1(), r.kubeClient.StorageV1(), r.logger) + nodeOS, err := kube.GetPVCAttachingNodeOS(pvc, r.kubeClient.CoreV1(), r.kubeClient.StorageV1(), r.logger) if err != nil { return nil, errors.Wrapf(err, "failed to get attaching node OS for PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC) } @@ -817,6 +812,11 @@ func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload return nil, errors.Wrapf(err, "no appropriate node to run data upload for PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC) } + accessMode := exposer.AccessModeFileSystem + if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == corev1.PersistentVolumeBlock { + accessMode = exposer.AccessModeBlock + } + hostingPodLabels := map[string]string{velerov1api.DataUploadLabel: du.Name} for _, k := range util.ThirdPartyLabels { if v, err := nodeagent.GetLabelValue(context.Background(), r.kubeClient, du.Namespace, k, nodeOS); err != nil { diff --git a/pkg/controller/data_upload_controller_test.go b/pkg/controller/data_upload_controller_test.go index 8e3b1688b..f480a692c 100644 --- a/pkg/controller/data_upload_controller_test.go +++ b/pkg/controller/data_upload_controller_test.go @@ -59,6 +59,7 @@ import ( velerotest "github.com/vmware-tanzu/velero/pkg/test" "github.com/vmware-tanzu/velero/pkg/uploader" "github.com/vmware-tanzu/velero/pkg/util/boolptr" + "github.com/vmware-tanzu/velero/pkg/util/kube" ) const dataUploadName = "dataupload-1" @@ -187,6 +188,8 @@ func initDataUploaderReconcilerWithError(needError ...error) (*DataUploadReconci }, } + node := builder.ForNode("fake-node").Labels(map[string]string{kube.NodeOSLabel: kube.NodeOSLinux}).Result() + dataPathMgr := datapath.NewManager(1) now, err := time.Parse(time.RFC1123, time.RFC1123) @@ -229,7 +232,7 @@ func initDataUploaderReconcilerWithError(needError ...error) (*DataUploadReconci } fakeSnapshotClient := snapshotFake.NewSimpleClientset(vsObject, vscObj) - fakeKubeClient := clientgofake.NewSimpleClientset(daemonSet) + fakeKubeClient := clientgofake.NewSimpleClientset(daemonSet, node) return NewDataUploadReconciler( fakeClient, diff --git a/pkg/util/kube/pvc_pv.go b/pkg/util/kube/pvc_pv.go index eab07f25b..e91e5dab3 100644 --- a/pkg/util/kube/pvc_pv.go +++ b/pkg/util/kube/pvc_pv.go @@ -429,13 +429,13 @@ func DiagnosePV(pv *corev1api.PersistentVolume) string { return diag } -func GetPVCAttachingNodeOS(pvc *corev1api.PersistentVolumeClaim, blockAccess bool, nodeClient corev1client.CoreV1Interface, +func GetPVCAttachingNodeOS(pvc *corev1api.PersistentVolumeClaim, nodeClient corev1client.CoreV1Interface, storageClient storagev1.StorageV1Interface, log logrus.FieldLogger) (string, error) { var nodeOS string var scFsType string - if blockAccess { - log.Infof("Use linux node for block access for PVC %s/%s", pvc.Namespace, pvc.Name) + if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == corev1api.PersistentVolumeBlock { + log.Infof("Use linux node for block mode PVC %s/%s", pvc.Namespace, pvc.Name) return NodeOSLinux, nil } diff --git a/pkg/util/kube/pvc_pv_test.go b/pkg/util/kube/pvc_pv_test.go index ea3195966..2a5c2d826 100644 --- a/pkg/util/kube/pvc_pv_test.go +++ b/pkg/util/kube/pvc_pv_test.go @@ -1564,6 +1564,17 @@ func TestGetPVCAttachingNodeOS(t *testing.T) { }, } + blockMode := corev1api.PersistentVolumeBlock + pvcObjBlockMode := &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-namespace", + Name: "fake-pvc", + }, + Spec: corev1api.PersistentVolumeClaimSpec{ + VolumeMode: &blockMode, + }, + } + pvcObjWithNode := &corev1api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fake-namespace", @@ -1610,7 +1621,6 @@ func TestGetPVCAttachingNodeOS(t *testing.T) { name string pvc *corev1api.PersistentVolumeClaim kubeClientObj []runtime.Object - blockAccess bool expectedNodeOS string err string }{ @@ -1665,8 +1675,7 @@ func TestGetPVCAttachingNodeOS(t *testing.T) { }, { name: "block access", - pvc: pvcObjWithBoth, - blockAccess: true, + pvc: pvcObjBlockMode, expectedNodeOS: NodeOSLinux, }, } @@ -1676,7 +1685,7 @@ func TestGetPVCAttachingNodeOS(t *testing.T) { var kubeClient kubernetes.Interface = fakeKubeClient - nodeOS, err := GetPVCAttachingNodeOS(test.pvc, test.blockAccess, kubeClient.CoreV1(), kubeClient.StorageV1(), velerotest.NewLogger()) + nodeOS, err := GetPVCAttachingNodeOS(test.pvc, kubeClient.CoreV1(), kubeClient.StorageV1(), velerotest.NewLogger()) if err != nil { assert.EqualError(t, err, test.err)