From 4220c7abe8ac9c6e424811ef3b38a7ee14b0986d Mon Sep 17 00:00:00 2001 From: chlins Date: Thu, 30 Jul 2026 16:37:18 +0800 Subject: [PATCH 1/4] Cancel hook exec stream on timeout and bound hook timeouts Hook timeouts come from pod annotations via time.ParseDuration, which accepts negative and arbitrarily large values, and the exec stream was never cancelled. Signed-off-by: chlins --- changelogs/unreleased/10125-chlins | 1 + pkg/podexec/pod_command_executor.go | 39 +++-- pkg/podexec/pod_command_executor_test.go | 18 +++ .../pod_command_executor_timeout_test.go | 136 ++++++++++++++++++ 4 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/10125-chlins create mode 100644 pkg/podexec/pod_command_executor_timeout_test.go diff --git a/changelogs/unreleased/10125-chlins b/changelogs/unreleased/10125-chlins new file mode 100644 index 000000000..1a1b00371 --- /dev/null +++ b/changelogs/unreleased/10125-chlins @@ -0,0 +1 @@ +Cancel hook exec stream on timeout and bound hook timeouts diff --git a/pkg/podexec/pod_command_executor.go b/pkg/podexec/pod_command_executor.go index 4ba4d4dc9..71894a489 100644 --- a/pkg/podexec/pod_command_executor.go +++ b/pkg/podexec/pod_command_executor.go @@ -36,6 +36,10 @@ import ( const defaultTimeout = 30 * time.Second +// maxHookTimeout bounds a user-supplied hook timeout, which can come from a pod +// annotation, so a single hook cannot hold up a backup for an unbounded time. +const maxHookTimeout = 4 * time.Hour + // PodCommandExecutor is capable of executing a command in a container in a pod. type PodCommandExecutor interface { // ExecutePodCommand executes a command in a container in a pod. If the command takes longer than @@ -112,9 +116,15 @@ func (e *defaultPodCommandExecutor) ExecutePodCommand(log logrus.FieldLogger, it localHook.OnError = api.HookErrorModeFail } - if localHook.Timeout.Duration == 0 { + // A non-positive timeout is not a valid bound. Timeouts sourced from pod annotations are + // parsed with time.ParseDuration, which accepts negative values, and a negative duration + // would otherwise leave the hook without any timeout at all. + if localHook.Timeout.Duration <= 0 { localHook.Timeout.Duration = defaultTimeout } + if localHook.Timeout.Duration > maxHookTimeout { + localHook.Timeout.Duration = maxHookTimeout + } hookLog := log.WithFields( logrus.Fields{ @@ -158,23 +168,28 @@ func (e *defaultPodCommandExecutor) ExecutePodCommand(log logrus.FieldLogger, it Stderr: &stderr, } - errCh := make(chan error) + // The timeout drives the context so the exec stream is actually cancelled, rather than + // being left running on the API server after this function has returned. + ctx, cancel := context.WithTimeout(context.Background(), localHook.Timeout.Duration) + defer cancel() + + // Buffered so the goroutine below can always send its result and exit, even when this + // function has already returned on the timeout path. + errCh := make(chan error, 1) go func() { - err = executor.StreamWithContext(context.Background(), streamOptions) - errCh <- err + errCh <- executor.StreamWithContext(ctx, streamOptions) }() - var timeoutCh <-chan time.Time - if localHook.Timeout.Duration > 0 { - timer := time.NewTimer(localHook.Timeout.Duration) - defer timer.Stop() - timeoutCh = timer.C - } - select { case err = <-errCh: - case <-timeoutCh: + // On a timeout the stream returns because the context expired, so both this case + // and ctx.Done() are ready and the select picks one at random. Report the timeout + // either way instead of surfacing the context error only some of the time. + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return errors.Errorf("timed out after %v", localHook.Timeout.Duration) + } + case <-ctx.Done(): return errors.Errorf("timed out after %v", localHook.Timeout.Duration) } diff --git a/pkg/podexec/pod_command_executor_test.go b/pkg/podexec/pod_command_executor_test.go index 13b00877a..de32fc605 100644 --- a/pkg/podexec/pod_command_executor_test.go +++ b/pkg/podexec/pod_command_executor_test.go @@ -177,6 +177,24 @@ func TestExecutePodCommand(t *testing.T) { hookError: errors.New("hook error"), expectedError: "hook error", }, + { + // Timeouts from pod annotations go through time.ParseDuration, which accepts + // negative values. Without clamping, the hook would run with no timeout at all. + name: "negative timeout falls back to the default", + command: []string{"some", "command"}, + expectedContainerName: "foo", + expectedErrorMode: v1.HookErrorModeFail, + timeout: -1 * time.Second, + expectedTimeout: 30 * time.Second, + }, + { + name: "timeout above the maximum is capped", + command: []string{"some", "command"}, + expectedContainerName: "foo", + expectedErrorMode: v1.HookErrorModeFail, + timeout: 100000 * time.Hour, + expectedTimeout: maxHookTimeout, + }, } for _, test := range tests { diff --git a/pkg/podexec/pod_command_executor_timeout_test.go b/pkg/podexec/pod_command_executor_timeout_test.go new file mode 100644 index 000000000..88cb3ed92 --- /dev/null +++ b/pkg/podexec/pod_command_executor_timeout_test.go @@ -0,0 +1,136 @@ +package podexec + +import ( + "context" + "net/url" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/mock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + + v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerotest "github.com/vmware-tanzu/velero/pkg/test" +) + +const timeoutTestPodJSON = `{ + "metadata": {"namespace": "ns", "name": "pod-1"}, + "spec": {"containers": [{"name": "container-1"}]} +}` + +// contextAwareExecutor returns once its context is cancelled, like the SPDY executor does. +type contextAwareExecutor struct { + cancelled chan struct{} + cancelledOnce bool +} + +func (e *contextAwareExecutor) Stream(options remotecommand.StreamOptions) error { return nil } + +func (e *contextAwareExecutor) StreamWithContext(ctx context.Context, options remotecommand.StreamOptions) error { + <-ctx.Done() + if !e.cancelledOnce { + e.cancelledOnce = true + close(e.cancelled) + } + return ctx.Err() +} + +func newTimeoutTestExecutor(t *testing.T, exec remotecommand.Executor) (*defaultPodCommandExecutor, map[string]any) { + t.Helper() + + clientConfig := &rest.Config{} + poster := &mockPoster{} + podCommandExecutor := NewPodCommandExecutor(clientConfig, poster).(*defaultPodCommandExecutor) + + factory := &mockStreamExecutorFactory{} + podCommandExecutor.streamExecutorFactory = factory + + baseURL, _ := url.Parse("https://some.server") + contentConfig := rest.ClientContentConfig{GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}} + poster.On("Post").Return(rest.NewRequestWithClient(baseURL, "/api/v1", contentConfig, nil)) + factory.On("NewSPDYExecutor", clientConfig, "POST", mock.Anything).Return(exec, nil) + + pod, err := velerotest.GetAsMap(timeoutTestPodJSON) + if err != nil { + t.Fatal(err) + } + + return podCommandExecutor, pod +} + +func timeoutTestHook(timeout time.Duration) *v1.ExecHook { + return &v1.ExecHook{ + Container: "container-1", + Command: []string{"sh", "-c", "sleep 60"}, + Timeout: metav1.Duration{Duration: timeout}, + } +} + +// A hook that times out must have its exec stream cancelled, otherwise the command keeps +// running on the API server after ExecutePodCommand has returned. +func TestExecutePodCommandCancelsStreamOnTimeout(t *testing.T) { + exec := &contextAwareExecutor{cancelled: make(chan struct{})} + podCommandExecutor, pod := newTimeoutTestExecutor(t, exec) + + err := podCommandExecutor.ExecutePodCommand(velerotest.NewLogger(), pod, "ns", "pod-1", "hookName", timeoutTestHook(100*time.Millisecond)) + if err == nil { + t.Fatal("expected a timeout error") + } + + select { + case <-exec.cancelled: + case <-time.After(2 * time.Second): + t.Fatal("stream was not cancelled after the hook timed out") + } +} + +// When the stream returns because the context expired, both select cases are ready and one +// is picked at random, so the reported error must not depend on which one wins. +func TestExecutePodCommandTimeoutErrorIsDeterministic(t *testing.T) { + const rounds = 50 + + messages := map[string]int{} + for range rounds { + exec := &contextAwareExecutor{cancelled: make(chan struct{})} + podCommandExecutor, pod := newTimeoutTestExecutor(t, exec) + + err := podCommandExecutor.ExecutePodCommand(velerotest.NewLogger(), pod, "ns", "pod-1", "hookName", timeoutTestHook(time.Millisecond)) + if err == nil { + t.Fatal("expected a timeout error") + } + messages[err.Error()]++ + } + + if len(messages) != 1 { + t.Fatalf("expected one error message, got %d: %v", len(messages), messages) + } +} + +func TestExecutePodCommandDoesNotLeakOnTimeout(t *testing.T) { + const rounds = 10 + + runtime.GC() + time.Sleep(200 * time.Millisecond) + before := runtime.NumGoroutine() + + for range rounds { + exec := &contextAwareExecutor{cancelled: make(chan struct{})} + podCommandExecutor, pod := newTimeoutTestExecutor(t, exec) + + if err := podCommandExecutor.ExecutePodCommand(velerotest.NewLogger(), pod, "ns", "pod-1", "hookName", timeoutTestHook(50*time.Millisecond)); err == nil { + t.Fatal("expected a timeout error") + } + } + + time.Sleep(time.Second) + runtime.GC() + time.Sleep(200 * time.Millisecond) + + if leaked := runtime.NumGoroutine() - before; leaked >= rounds { + t.Fatalf("%d goroutines leaked over %d timed out hooks", leaked, rounds) + } +} From 2649b2554c05ba4dc35d8a9facf5943b7d1e45e3 Mon Sep 17 00:00:00 2001 From: chlins Date: Mon, 3 Aug 2026 13:28:31 +0800 Subject: [PATCH 2/4] Fix hook timeout review feedback Signed-off-by: chlins --- pkg/podexec/pod_command_executor.go | 22 ++++-- pkg/podexec/pod_command_executor_test.go | 57 +++++++++++++++ .../pod_command_executor_timeout_test.go | 73 +++++++++++++++---- 3 files changed, 130 insertions(+), 22 deletions(-) diff --git a/pkg/podexec/pod_command_executor.go b/pkg/podexec/pod_command_executor.go index 71894a489..997795c35 100644 --- a/pkg/podexec/pod_command_executor.go +++ b/pkg/podexec/pod_command_executor.go @@ -168,7 +168,7 @@ func (e *defaultPodCommandExecutor) ExecutePodCommand(log logrus.FieldLogger, it Stderr: &stderr, } - // The timeout drives the context so the exec stream is actually cancelled, rather than + // The timeout drives the context so the exec stream is actually canceled, rather than // being left running on the API server after this function has returned. ctx, cancel := context.WithTimeout(context.Background(), localHook.Timeout.Duration) defer cancel() @@ -178,17 +178,15 @@ func (e *defaultPodCommandExecutor) ExecutePodCommand(log logrus.FieldLogger, it errCh := make(chan error, 1) go func() { - errCh <- executor.StreamWithContext(ctx, streamOptions) + streamErr := executor.StreamWithContext(ctx, streamOptions) + // Inspect the local context as soon as the stream returns. Otherwise a stream error + // completed before the deadline could be misclassified if this goroutine sends its + // result before the caller is scheduled to receive it. + errCh <- normalizeExecHookError(streamErr, ctx.Err(), localHook.Timeout.Duration) }() select { case err = <-errCh: - // On a timeout the stream returns because the context expired, so both this case - // and ctx.Done() are ready and the select picks one at random. Report the timeout - // either way instead of surfacing the context error only some of the time. - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return errors.Errorf("timed out after %v", localHook.Timeout.Duration) - } case <-ctx.Done(): return errors.Errorf("timed out after %v", localHook.Timeout.Duration) } @@ -199,6 +197,14 @@ func (e *defaultPodCommandExecutor) ExecutePodCommand(log logrus.FieldLogger, it return err } +func normalizeExecHookError(streamErr, contextErr error, timeout time.Duration) error { + if errors.Is(contextErr, context.DeadlineExceeded) { + return errors.Errorf("timed out after %v", timeout) + } + + return streamErr +} + func ensureContainerExists(pod *corev1api.Pod, container string) error { existsAsMainContainer := slices.ContainsFunc(pod.Spec.Containers, func(c corev1api.Container) bool { return c.Name == container diff --git a/pkg/podexec/pod_command_executor_test.go b/pkg/podexec/pod_command_executor_test.go index de32fc605..e30911a20 100644 --- a/pkg/podexec/pod_command_executor_test.go +++ b/pkg/podexec/pod_command_executor_test.go @@ -177,6 +177,15 @@ func TestExecutePodCommand(t *testing.T) { hookError: errors.New("hook error"), expectedError: "hook error", }, + { + name: "stream deadline exceeded before local timeout", + command: []string{"some", "command"}, + expectedContainerName: "foo", + expectedErrorMode: v1.HookErrorModeFail, + expectedTimeout: defaultTimeout, + hookError: context.DeadlineExceeded, + expectedError: context.DeadlineExceeded.Error(), + }, { // Timeouts from pod annotations go through time.ParseDuration, which accepts // negative values. Without clamping, the hook would run with no timeout at all. @@ -264,6 +273,54 @@ func TestExecutePodCommand(t *testing.T) { } } +func TestNormalizeExecHookError(t *testing.T) { + hookErr := errors.New("hook error") + tests := []struct { + name string + streamErr error + contextErr error + expectedError string + preserveStreamErr bool + }{ + { + name: "local context deadline exceeded", + streamErr: context.DeadlineExceeded, + contextErr: context.DeadlineExceeded, + expectedError: "timed out after 30s", + }, + { + name: "stream deadline exceeded before local timeout", + streamErr: context.DeadlineExceeded, + expectedError: context.DeadlineExceeded.Error(), + preserveStreamErr: true, + }, + { + name: "ordinary hook error", + streamErr: hookErr, + expectedError: hookErr.Error(), + preserveStreamErr: true, + }, + { + name: "no errors", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := normalizeExecHookError(test.streamErr, test.contextErr, defaultTimeout) + if test.expectedError == "" { + require.NoError(t, err) + return + } + + require.EqualError(t, err, test.expectedError) + if test.preserveStreamErr && err != test.streamErr { + t.Fatalf("expected stream error to be returned unchanged") + } + }) + } +} + func TestEnsureContainerExists(t *testing.T) { pod := &corev1api.Pod{ Spec: corev1api.PodSpec{ diff --git a/pkg/podexec/pod_command_executor_timeout_test.go b/pkg/podexec/pod_command_executor_timeout_test.go index 88cb3ed92..a79389481 100644 --- a/pkg/podexec/pod_command_executor_timeout_test.go +++ b/pkg/podexec/pod_command_executor_timeout_test.go @@ -1,9 +1,26 @@ +/* +Copyright 2026 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 podexec import ( "context" "net/url" "runtime" + "sync" "testing" "time" @@ -22,23 +39,38 @@ const timeoutTestPodJSON = `{ "spec": {"containers": [{"name": "container-1"}]} }` -// contextAwareExecutor returns once its context is cancelled, like the SPDY executor does. +// contextAwareExecutor returns once its context is canceled, like the SPDY executor does. type contextAwareExecutor struct { - cancelled chan struct{} - cancelledOnce bool + canceled chan struct{} + canceledOnce bool } func (e *contextAwareExecutor) Stream(options remotecommand.StreamOptions) error { return nil } func (e *contextAwareExecutor) StreamWithContext(ctx context.Context, options remotecommand.StreamOptions) error { <-ctx.Done() - if !e.cancelledOnce { - e.cancelledOnce = true - close(e.cancelled) + if !e.canceledOnce { + e.canceledOnce = true + close(e.canceled) } return ctx.Err() } +// contextIgnoringExecutor lets the outer timeout path return before the stream does. +// Once released, the stream goroutine can only exit if its result channel is buffered. +type contextIgnoringExecutor struct { + release <-chan struct{} + returned *sync.WaitGroup +} + +func (e *contextIgnoringExecutor) Stream(options remotecommand.StreamOptions) error { return nil } + +func (e *contextIgnoringExecutor) StreamWithContext(ctx context.Context, options remotecommand.StreamOptions) error { + defer e.returned.Done() + <-e.release + return nil +} + func newTimeoutTestExecutor(t *testing.T, exec remotecommand.Executor) (*defaultPodCommandExecutor, map[string]any) { t.Helper() @@ -70,10 +102,10 @@ func timeoutTestHook(timeout time.Duration) *v1.ExecHook { } } -// A hook that times out must have its exec stream cancelled, otherwise the command keeps +// A hook that times out must have its exec stream canceled, otherwise the command keeps // running on the API server after ExecutePodCommand has returned. func TestExecutePodCommandCancelsStreamOnTimeout(t *testing.T) { - exec := &contextAwareExecutor{cancelled: make(chan struct{})} + exec := &contextAwareExecutor{canceled: make(chan struct{})} podCommandExecutor, pod := newTimeoutTestExecutor(t, exec) err := podCommandExecutor.ExecutePodCommand(velerotest.NewLogger(), pod, "ns", "pod-1", "hookName", timeoutTestHook(100*time.Millisecond)) @@ -82,26 +114,32 @@ func TestExecutePodCommandCancelsStreamOnTimeout(t *testing.T) { } select { - case <-exec.cancelled: + case <-exec.canceled: case <-time.After(2 * time.Second): - t.Fatal("stream was not cancelled after the hook timed out") + t.Fatal("stream was not canceled after the hook timed out") } } // When the stream returns because the context expired, both select cases are ready and one // is picked at random, so the reported error must not depend on which one wins. func TestExecutePodCommandTimeoutErrorIsDeterministic(t *testing.T) { - const rounds = 50 + const ( + rounds = 50 + expectedError = "timed out after 1ms" + ) messages := map[string]int{} for range rounds { - exec := &contextAwareExecutor{cancelled: make(chan struct{})} + exec := &contextAwareExecutor{canceled: make(chan struct{})} podCommandExecutor, pod := newTimeoutTestExecutor(t, exec) err := podCommandExecutor.ExecutePodCommand(velerotest.NewLogger(), pod, "ns", "pod-1", "hookName", timeoutTestHook(time.Millisecond)) if err == nil { t.Fatal("expected a timeout error") } + if err.Error() != expectedError { + t.Fatalf("expected %q, got %q", expectedError, err) + } messages[err.Error()]++ } @@ -117,8 +155,11 @@ func TestExecutePodCommandDoesNotLeakOnTimeout(t *testing.T) { time.Sleep(200 * time.Millisecond) before := runtime.NumGoroutine() + release := make(chan struct{}) + returned := &sync.WaitGroup{} for range rounds { - exec := &contextAwareExecutor{cancelled: make(chan struct{})} + returned.Add(1) + exec := &contextIgnoringExecutor{release: release, returned: returned} podCommandExecutor, pod := newTimeoutTestExecutor(t, exec) if err := podCommandExecutor.ExecutePodCommand(velerotest.NewLogger(), pod, "ns", "pod-1", "hookName", timeoutTestHook(50*time.Millisecond)); err == nil { @@ -126,7 +167,11 @@ func TestExecutePodCommandDoesNotLeakOnTimeout(t *testing.T) { } } - time.Sleep(time.Second) + // Every ExecutePodCommand call has already taken the timeout path. Releasing the + // streams now forces their goroutines to send into an errCh with no receiver. + close(release) + returned.Wait() + time.Sleep(200 * time.Millisecond) runtime.GC() time.Sleep(200 * time.Millisecond) From 5a615ad580af17706332a5494fb5664cd2f11d11 Mon Sep 17 00:00:00 2001 From: chlins Date: Tue, 4 Aug 2026 17:19:25 +0800 Subject: [PATCH 3/4] Verify build tool downloads Pin architecture-specific SHA-256 checksums for kubebuilder, protoc, and GoReleaser before installation. Signed-off-by: chlins --- .../unreleased/RS-MIRRORS_GITHUB_VELERO-22 | 1 + hack/build-image/Dockerfile | 81 +++++++------ hack/verify-build-image-tool-checksums.sh | 111 ++++++++++++++++++ 3 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 changelogs/unreleased/RS-MIRRORS_GITHUB_VELERO-22 create mode 100755 hack/verify-build-image-tool-checksums.sh diff --git a/changelogs/unreleased/RS-MIRRORS_GITHUB_VELERO-22 b/changelogs/unreleased/RS-MIRRORS_GITHUB_VELERO-22 new file mode 100644 index 000000000..b17917098 --- /dev/null +++ b/changelogs/unreleased/RS-MIRRORS_GITHUB_VELERO-22 @@ -0,0 +1 @@ +Verify downloaded build tools against architecture-specific SHA-256 checksums before installation. diff --git a/hack/build-image/Dockerfile b/hack/build-image/Dockerfile index 4f34ba470..8978855b2 100644 --- a/hack/build-image/Dockerfile +++ b/hack/build-image/Dockerfile @@ -29,8 +29,18 @@ RUN go install sigs.k8s.io/controller-runtime/tools/setup-envtest@v0.0.0-2026030 ENVTEST_ASSETS_DIR=$(setup-envtest use 1.33.0 --bin-dir /usr/local/kubebuilder/bin -p path) && \ cp -r ${ENVTEST_ASSETS_DIR}/* /usr/local/kubebuilder/bin/ -RUN wget --quiet https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.2.0/kubebuilder_linux_$(go env GOARCH) && \ - mv kubebuilder_linux_$(go env GOARCH) /usr/local/kubebuilder/bin/kubebuilder && \ +RUN set -eux; \ + ARCH="$(go env GOARCH)"; \ + case "$ARCH" in \ + amd64) KUBEBUILDER_SHA256="102bb0f586dcb50951aded67856483a2ee114057c56475b3cda6051a12832a72" ;; \ + arm64) KUBEBUILDER_SHA256="0a340ea925c801aa71344becdefce96eda6fa0bc92352b9c7bcb36a4f8c56314" ;; \ + ppc64le) KUBEBUILDER_SHA256="74473d094908caad852a77088f64bb64eb4c79497f6695eb5e9e8bc4bacd9409" ;; \ + *) echo "Unsupported kubebuilder architecture: $ARCH" >&2; exit 1 ;; \ + esac; \ + FILE="kubebuilder_linux_$ARCH"; \ + wget --quiet "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.2.0/$FILE"; \ + echo "$KUBEBUILDER_SHA256 $FILE" | sha256sum -c -; \ + mv "$FILE" /usr/local/kubebuilder/bin/kubebuilder; \ chmod +x /usr/local/kubebuilder/bin/kubebuilder # get controller-tools @@ -52,26 +62,27 @@ RUN apt-get update && apt-get install -y unzip # cpu = "ppcle_64" # snippet from: https://github.com/protocolbuffers/protobuf/blob/d445953603e66eb8992a39b4e10fcafec8501f24/protobuf_release.bzl#L18-L24 # cpu names: https://github.com/bazelbuild/platforms/blob/main/cpu/BUILD -RUN ARCH=$(go env GOARCH) && \ - if [ "$ARCH" = "s390x" ] ; then \ - ARCH="s390_64"; \ - elif [ "$ARCH" = "arm64" ] ; then \ - ARCH="aarch_64"; \ - elif [ "$ARCH" = "ppc64le" ] ; then \ - ARCH="ppcle_64"; \ - elif [ "$ARCH" = "ppc64" ] ; then \ - ARCH="ppcle_64"; \ - else \ - ARCH=$(uname -m); \ - fi && echo "ARCH=$ARCH" && \ - wget --quiet https://github.com/protocolbuffers/protobuf/releases/download/v25.2/protoc-25.2-linux-$ARCH.zip && \ - unzip protoc-25.2-linux-$ARCH.zip; \ - rm *.zip && \ - mv bin/protoc /usr/bin/protoc && \ - mv include/google /usr/include && \ - chmod a+x /usr/include/google && \ - chmod a+x /usr/include/google/protobuf && \ - chmod a+r -R /usr/include/google && \ +RUN set -eux; \ + GOARCH="$(go env GOARCH)"; \ + case "$GOARCH" in \ + amd64) ARCH="x86_64"; PROTOC_SHA256="78ab9c3288919bdaa6cfcec6127a04813cf8a0ce406afa625e48e816abee2878" ;; \ + 386) ARCH="x86_32"; PROTOC_SHA256="cc1c6e31a9b333c3e6d026aac5fdc1f7d70c6cd8851631505188ca9826acee5a" ;; \ + arm64) ARCH="aarch_64"; PROTOC_SHA256="07683afc764e4efa3fa969d5f049fbc2bdfc6b4e7786a0b233413ac0d8753f6b" ;; \ + ppc64|ppc64le) ARCH="ppcle_64"; PROTOC_SHA256="cea283337101ed08ff6c76a98461b1d871bac21f41dc1dabdfddaa5d99df9339" ;; \ + s390x) ARCH="s390_64"; PROTOC_SHA256="8a13ec6518585f7664d58f929417c9e6d0c4aeedf3bcdd854aeafceb5ef0a389" ;; \ + *) echo "Unsupported protoc architecture: $GOARCH" >&2; exit 1 ;; \ + esac; \ + echo "ARCH=$ARCH"; \ + FILE="protoc-25.2-linux-$ARCH.zip"; \ + wget --quiet "https://github.com/protocolbuffers/protobuf/releases/download/v25.2/$FILE"; \ + echo "$PROTOC_SHA256 $FILE" | sha256sum -c -; \ + unzip "$FILE"; \ + rm "$FILE"; \ + mv bin/protoc /usr/bin/protoc; \ + mv include/google /usr/include; \ + chmod a+x /usr/include/google; \ + chmod a+x /usr/include/google/protobuf; \ + chmod a+r -R /usr/include/google; \ chmod +x /usr/bin/protoc RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@${PROTOC_GEN_GO_VERSION} \ && go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 @@ -84,17 +95,21 @@ RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@${PROTOC_GEN_GO_VERS # {{- else if eq .Arch "386" }}i386 # {{- else }}{{ .Arch }}{{ end }} # {{- if .Arm }}v{{ .Arm }}{{ end -}} -RUN ARCH=$(go env GOARCH) && \ - if [ "$ARCH" = "amd64" ] ; then \ - ARCH="x86_64"; \ - elif [ "$ARCH" = "386" ] ; then \ - ARCH="i386"; \ - elif [ "$ARCH" = "ppc64le" ] ; then \ - ARCH="ppc64"; \ - fi && \ - wget --quiet "https://github.com/goreleaser/goreleaser/releases/download/v1.26.2/goreleaser_Linux_$ARCH.tar.gz" && \ - tar xvf goreleaser_Linux_$ARCH.tar.gz; \ - mv goreleaser /usr/bin/goreleaser && \ +RUN set -eux; \ + GOARCH="$(go env GOARCH)"; \ + case "$GOARCH" in \ + amd64) ARCH="x86_64"; GORELEASER_SHA256="cfbdf12e3ea20e4c3a209d07311f43c2e0baf20d5cce09bcdc232567e0f34307" ;; \ + 386) ARCH="i386"; GORELEASER_SHA256="21c236575cccd29588182b570b4ffe83ad8fb96cd3b13b2af79feafd8ae37b1b" ;; \ + arm64) ARCH="arm64"; GORELEASER_SHA256="2b984e2932b24be0d638c7dab7357a59d86eb79ca7fee1afd31be5ebb1847cbb" ;; \ + arm) ARCH="armv7"; GORELEASER_SHA256="6db2899885be19f123b36192a42dcfb3bb2b3e1009fec7277517969e96d8a7c6" ;; \ + ppc64|ppc64le) ARCH="ppc64"; GORELEASER_SHA256="76d060ebb8d48e76fde45983f87040fe3ac0ca37c5ace4648a956959b81bfdf0" ;; \ + *) echo "Unsupported goreleaser architecture: $GOARCH" >&2; exit 1 ;; \ + esac; \ + FILE="goreleaser_Linux_$ARCH.tar.gz"; \ + wget --quiet "https://github.com/goreleaser/goreleaser/releases/download/v1.26.2/$FILE"; \ + echo "$GORELEASER_SHA256 $FILE" | sha256sum -c -; \ + tar xvf "$FILE"; \ + mv goreleaser /usr/bin/goreleaser; \ chmod +x /usr/bin/goreleaser # get golangci-lint diff --git a/hack/verify-build-image-tool-checksums.sh b/hack/verify-build-image-tool-checksums.sh new file mode 100755 index 000000000..a11f258ab --- /dev/null +++ b/hack/verify-build-image-tool-checksums.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# Copyright 2026 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. + +set -euo pipefail + +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +DOCKERFILE="${ROOT_DIR}/hack/build-image/Dockerfile" + +verify_block() { + local tool=$1 + local start=$2 + local end=$3 + local hash_variable=$4 + local install_pattern=$5 + shift 5 + local expected_arches=("$@") + local block + + block=$(awk -v start="${start}" -v end="${end}" ' + $0 ~ start { printing = 1 } + printing { print } + printing && $0 ~ end { exit } + ' "${DOCKERFILE}") + + if [[ -z "${block}" ]]; then + echo "Unable to find ${tool} install block" >&2 + return 1 + fi + + local actual_arches + actual_arches=$(printf '%s\n' "${block}" | + sed -nE "s/^[[:space:]]*([[:alnum:]_|]+)\).*${hash_variable}=\"([[:xdigit:]]+)\".*/\1 \2/p") + + local expected_arch + for expected_arch in "${expected_arches[@]}"; do + if ! printf '%s\n' "${actual_arches}" | awk -v arch="${expected_arch}" ' + $1 == arch && length($2) == 64 && $2 ~ /^[0-9a-f]+$/ { found = 1 } + END { exit !found } + '; then + echo "${tool} is missing a lowercase 64-hex SHA-256 for ${expected_arch}" >&2 + return 1 + fi + done + + local actual_count + actual_count=$(printf '%s\n' "${actual_arches}" | sed '/^$/d' | wc -l | tr -d ' ') + if [[ "${actual_count}" -ne "${#expected_arches[@]}" ]]; then + echo "${tool} architecture mapping changed; update this verification gate" >&2 + printf '%s\n' "${actual_arches}" >&2 + return 1 + fi + + if ! printf '%s\n' "${block}" | grep -Eq '^ \*\).*Unsupported .+ architecture:.+exit 1'; then + echo "${tool} does not fail closed for unknown architectures" >&2 + return 1 + fi + + local download_line checksum_line install_line + download_line=$(printf '%s\n' "${block}" | grep -n 'wget --quiet' | head -1 | cut -d: -f1) + checksum_line=$(printf '%s\n' "${block}" | grep -n "echo \"\$${hash_variable} \$FILE\" | sha256sum -c -" | head -1 | cut -d: -f1) + install_line=$(printf '%s\n' "${block}" | grep -nE "${install_pattern}" | head -1 | cut -d: -f1) + + if [[ -z "${download_line}" || -z "${checksum_line}" || -z "${install_line}" || + "${download_line}" -ge "${checksum_line}" || "${checksum_line}" -ge "${install_line}" ]]; then + echo "${tool} must download, verify, then install/extract in that order" >&2 + return 1 + fi + + if ! printf '%s\n' "${block}" | grep -q '^RUN set -eux;'; then + echo "${tool} install block must use strict shell error handling" >&2 + return 1 + fi +} + +verify_block \ + kubebuilder \ + '^RUN set -eux;.*$' \ + '^# get controller-tools$' \ + KUBEBUILDER_SHA256 \ + 'mv "\$FILE"' \ + amd64 arm64 ppc64le + +verify_block \ + protoc \ + '^# cpu names:' \ + '^RUN go install google.golang.org/protobuf' \ + PROTOC_SHA256 \ + 'unzip "\$FILE"' \ + amd64 386 arm64 'ppc64|ppc64le' s390x + +verify_block \ + goreleaser \ + '^# goreleaser name template' \ + '^# get golangci-lint$' \ + GORELEASER_SHA256 \ + 'tar xvf "\$FILE"' \ + amd64 386 arm64 arm 'ppc64|ppc64le' + +echo "Verified pinned build-tool checksums and fail-closed install ordering" From 93df34d2ea6187b74aee94bc81c36c6366190685 Mon Sep 17 00:00:00 2001 From: Ralthos <161431341+Ralthos@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:34:26 +0530 Subject: [PATCH 4/4] Add printer columns for VolumeSnapshotLocation (#10216) kubectl get volumesnapshotlocation falls back to NAME and AGE, while BackupStorageLocation beside it shows provider and phase. This follows the same pattern for the remaining location type. Phase is worth surfacing here because the CLI does not print it. velero snapshot-location get shows only NAME and PROVIDER, so status.phase, which carries the same Available/Unavailable enum as BackupStorageLocation, is currently not visible from either tool. Raised as an open question on #10199 and left out of #10200 to keep that change to the two types the issue was filed about. Signed-off-by: saral --- changelogs/unreleased/10211-Ralthos | 1 + .../bases/velero.io_volumesnapshotlocations.yaml | 15 ++++++++++++++- config/crd/v1/crds/crds.go | 2 +- .../velero/v1/volume_snapshot_location_type.go | 3 +++ 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/10211-Ralthos diff --git a/changelogs/unreleased/10211-Ralthos b/changelogs/unreleased/10211-Ralthos new file mode 100644 index 000000000..ab77622f2 --- /dev/null +++ b/changelogs/unreleased/10211-Ralthos @@ -0,0 +1 @@ +Add printer columns for VolumeSnapshotLocation so kubectl shows provider and phase diff --git a/config/crd/v1/bases/velero.io_volumesnapshotlocations.yaml b/config/crd/v1/bases/velero.io_volumesnapshotlocations.yaml index 111a19df5..4fe7338ea 100644 --- a/config/crd/v1/bases/velero.io_volumesnapshotlocations.yaml +++ b/config/crd/v1/bases/velero.io_volumesnapshotlocations.yaml @@ -16,7 +16,19 @@ spec: singular: volumesnapshotlocation scope: Namespaced versions: - - name: v1 + - additionalPrinterColumns: + - description: Provider is the provider of the volume storage + jsonPath: .spec.provider + name: Provider + type: string + - description: Volume Snapshot Location status such as Available/Unavailable + jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: description: VolumeSnapshotLocation is a location where Velero stores volume @@ -93,3 +105,4 @@ spec: type: object served: true storage: true + subresources: {} diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index d910e72f3..0f645d6c1 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -39,7 +39,7 @@ var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ks\xdc8sw\xff\n\x94sؤJ#ǕG\xa5tSd;V\xbeoe\x95\xe4\xd8g\f\xd93\xc4'\x10\xe0\x02\xa0ƳI\xfe{\n\x8d\a\x1f\x03\x92\x98\xd1cwSˋJ$\xd0\x00\xfaݍ\x06f\xb5Z\xbd\xa1\r\xfb\x06J3).\bm\x18\xfc0 \xec\x7f\xfa\xfc\xe1\xdf\xf49\x93\xef\x1e߿y`\xa2\xbc W\xad6\xb2\xbe\x03-[U\xc0\a\xd80\xc1\f\x93\xe2M\r\x86\x96\xd4Ћ7\x84P!\xa4\xa1\xf6\xb5\xb6\xff\x12RHa\x94\xe4\x1c\xd4j\v\xe2\xfc\xa1]úe\xbc\x04\x85\xc0\xc3Џ\xffx\xfe\xfe_\xcf\xff\xe5\r!\x82\xd6pA\x14h#\x15\xe8\xf3G\xe0\xa0\xe49\x93ot\x03\x85\x85\xb9U\xb2m.H\xf7\xc1\xf5\xf1㹹\u07b9\xee\xf8\x863m\xfe\xd2\x7f\xfbW\xa6\r~ix\xab(\xef\x06×\xba\x92\xca\xdct\x00WD\xf9暉m˩\x8a\x1d\xde\x10\xa2\v\xd9\xc0\x05\xc1\xf6\r-\xa0|C\x88_\x14\xf6_\xf9\xf5<\xbew \x8a\nj\xea\x00\x13\"\x1b\x10\x97\xb7\xd7\xdf\xfe\xe9~\xf0\x9a\x90\x12t\xa1Xc\x105\xff\xb3\x8a\xefIX\x02a\x9aP\xf2\rQ`g\x83$!\xa6\xa2\x86(h\x14h\x10F\x13S\x01\xa1M\xc3Y\x81\x14!rӃ\x14zi\xb2Q\xb2\ue82di\xf1\xd06\xc4HB\x89\xa1j\v\x86\xfc\xa5]\x83\x12`@\x93\x82\xb7ڀ:\x8f\x80\x1a%\x1bP\x86\x05t\xb9\xa7\xc7U\xbd\xb7s\v\xb3\x8fŅ\xebEJ\xcb^\xe0\x96\xe0\xf1\t\xa5G\x1f\x91\x1bb*\xa6\xbb\xa5\x86\xe5\x11*\x88\\\xff\r\ns>\x02}\x0fʂ\xb1\xd4myi\xb9\xf2\x11\x94EV!\xb7\x82\xfd\x1aak\xbbp;(\xa7\x06\xb4!L\x18P\x82r\xf2Hy\vg\x84\x8ar\x04\xb9\xa6{\xa2\xc0\x8eIZу\x87\x1d\xf4x\x1e?#\xf1\xc4F^\x90ʘF_\xbc{\xb7e&\xc8Z!\xeb\xba\x15\xcc\xecߡذuk\xa4\xd2\xefJx\x04\xfeN\xb3튪\xa2b\x06\n\xd3*xG\x1b\xb6\u0085\b\x94\xb7\xf3\xba\xfc\xbbH\xd4\xc1\xb0foyT\x1b\xc5Ķ\xf7\x01E\xe5\b\xf2X!r\x8c\xe7@\xb9%vT\xb0\xaf,\xea\xee>\xde\x7f\xed3%Ӟ(=ޜ\xa2\x8f\xc5&\x13\x1bP\xae\x1f\xb2\xa6\x85\t\xa2l$\x13\x06\xff)8\x03a\x88n\xd753\x96\r~iA[~\x97c\xb0W\xa8\x8f\xc8\x1aH۔\xd4@9np-\xc8\x15\xad\x81_Q\r\xafL+K\x15\xbd\xb2DȢV_ˎ\x1b;\xf4\xf6>\x04]9AZ\xafE\xee\x1b(\x06\x92f\xbb\xb1MP\x17\x1b\xa9\x06J\xc6v\x19\xe2(-\xfc\xf6qZĪ\xc5\xf1\x97%.\xb3Ͽ\xc7ޖ\xdf\xec\xccZ\xc1~i\x01\x95\xa9\x13\x7f8\xd4W\xaa\xa7\xf4\x87\x8fe\xa31u'\x11m\x1f\xf8Q\xf0\xb6\x842\xea\xf5\x83\x05\xe6,\xe3\xe3\x01\x144\x87\x94\t+D\xd6.ٵ\x88\xee+*p\xaa\x80\bi\x12\xf0\x98p\xf0\b\x13\x88\x81$M\xb0\xa1\x81:1\xe3\xd9%\x13\"Z\xce\xe9\x9a\xc3\x051\xaa=D\xa3\xebK\x95\xa2\xfb\tl\x05\xdf\xe0IȊ@\xbc\xaa\xe1\xac@\x92G\x85\x82\xf8\xfa㢊i\xab(\xc3*o%g\xc5~\x01_\x1f\x93\x9d\x82\xb4z\xd9\xf5+$k\xa8\xe8#\x93*%\x06RaӞ=\xefԴ\xb4Z\xd2\x03\x19۸\xcc\x05'\x91UI\xf9\xb0\xc4\x10\x9fm\x9b\xce:\x90\x02]\u0378\x14Omo\xbb\xd7@\xe0\a\x14\xadIL\x93\x90\xb2E\xd3$\x15i\xa46\xd3t\x9fV]\xa4\xef\x1c\xa5>\xce0\xcd\xc1ʒ\xac\xee\x1e\xaf\x84\x03Q-\x0e\x06\nY\n\xb0˨-Q\xbb\xb6J\xb6\xae\xed$RȚj(\x89\x14\x93##\xbb\xb4\x1c\xb4\x1f\xabD\xce\xe8\xf4\xd0Y\xb7~\xf4x\b\xa7k\xe0D\x03\x87\xc2Hu\x88\xcc\x1c\x94\xba'G\xb1N\xa02\xa1M\x87\x12\xd0-`\x06$\xb1\x9c\xbe\xabXQ9\x0fò'\xc2!\xa5\x04m\xb5\t\xba\xcc\xfb\xa9E\x92%\xf2\xfbA\xe6\xb4G\xf7,\x88\xd5\x18^J\xa3tO\x86\x1a\xee\x9e$j;\xdd{\xa0[\xfc{#g\x97\xfd\xff\x13\xb1\xc1\x98\x9c\xc0\xb43\xf2O\xd0\xfd\xcc\xe6\xe9I\xbe\xc5\b\x0f\xf49\xb9\xde\x10\xa8\x1b\xb3?#̄\xb7K\x92@9\xef\x8d\xf1\a\xa6\xcd\xf1L\x9fI\x9a\x1c\x99x!\xc2\xc4!\xfe\x80tA\x93q\xef-F6M\xfe\xda\xefuF\xd8&\"\xbd<#\x1b\xc6\r\xa8\x11\xf6OR\xf5\x812ρ\x8c\x1c\xabG0O`\x8a\xea\xe3\x0f\xeb\xe2\xe8.=\x96\x89\x97qg\xe7\x1b\x87\bbh\x9e\x17\xe0\x12\x8c\x97\x99\x82\x1a\xe3p\xf2\x15\xb1ٽA\xa7\xfa\xf2\xe6\xc3a\xac<~28\xef`!\vB\xe7\x9e\xcbъ\xfa\xf3\xf3QA\xf8\x82>P\f\xaa\\\xce\xe5\x8cP\xf2\x00{\xe7\xbaPA,}hh\x9c1\xbc\x02L\xfe \x9f=\xc0\x1e\xc1\xa4\xb39\x87O.7\xb8\xe7\x01\x12\xae\x7f\xea\x19\xe0\xd0\xceɇ\xc5\x0eO\xf6\x05\"\x02c\xf8\\6p\x8f\x17\x85D\xee$\xfdd\xea\x92\xf0\x04ܟ\xb0\xcc,V\xe9\x8f\xd1O}\"\a\xfc\xa4\x1d-\xad\xc4T\xcc\xe745\xa0\xcc\xe4\x12\xd4=\xdf(ge\x1c\xc8\xc9ȵ8#7\xd2\xd8?\x18\xa0id\x94\x0f\x12\xf4\x8d4\xf8\xe6E0\xea&\xfe\x92\xf8t#\xa0\xa0\t\xa7\xe5-\xc2\xfa9?g\xd3,\xb7E\xdc3M\xae\x85\x8dW\x1cJ2\x87\xc2\xf4\xae\x1b\xce\rT\xb7\x1a\xd3uB\x8a\x15\xda\xcc\xe4H\x1e\xdfR\r\xd0\xfd\xe4A\xfd\x80_\xad\xb1p_\\\x92\x99\xd3\x02\xca\x10Yb\xf6\x93\x1aز\"s\xbc\x1a\xd4\x16HcUx\x1eGd*V\xbf\x9a\xe3\xd8'\xcfz\xf7\x9f\x1f\xab\x87\x98/XY\x93\xb3\xf2\x10\x8c\xac3p\xe0uw\xb9\xbc\x9e\x95\x95ٌV\x81\x13\x16\x9bN$G\xa7\x9b\xe6 \xe5\t\xe8@+\x8e.\xce\"uiY\xe2\xe6\x1a\xe5\xb7GX\x94#x\xe1X\xd5Л\xbb3\xc15m\xacZ\xf8okiQ\x9a\xfe\x974\x94)}N.q\xa7\x8c\xc3\xe0\x9b\xcf\xc3\xf5\xc0d\f\xd9ء,\xff\x8f\x16\xf5\xbdu\"7\x06\x94\xcf%:\x1b\x10\xe2\x8f'Ff\xa9]\x99\xfedc2\x90\xc6\xfc\xaeE\xf0\x027\xb9\x8d\x9b\x9c)\x1e\xe3\xb0Z\xbc\x1c\xe9\xed\x7f\xfc\xd1\xcbgZɵ\xff\xf7\x17\xf2\xdc\x0eu!뚎w5\xb3\xa6z\xe5z\x06\x9e\xf6\x80\x1c\xf5նEyε\xc8\x1d\x0f\xe1\xfe厙\x8a\tB\x83\xda\x00\xe5\x19\x8a\x92F\xa6rة\xa7\xa2\x9a\xac\x01DL\xd1\xff\x1e\\\x89\x9a\x89k\x1c\x80\xbc\x7f\x01\xd7#\xa2\xeb%\x9dݫH\x93H\xf9\xf8\u0099\xacF\x96dW\x81\x82\x01c\x1c\xe6\xdd\xd1S\x15\xd2\xf4R\x16G8\xa4\x8d,\x7f\xd2dÔ6\xfd)h\xd2\xea\\Z\x1fI>;ﯬ\x06ٚ\x97D\xf0\xc7n\x98\xc1^sM\x7f\xb0\xba\xad\t\xade댹au\xdc\xd5\xf5\xe8\xddQf\xe2\xb6\x15\xe6o\x8c\xb4$h8\x18 kؤ\xf7{SO!\x85f%\xa8P\xa5\xe0\xc8Ƥ\x15\xcc\re\xbcM\xed\x12\xa5\x9ec#`\xf1Q\xa9\x93\x02\xe0/\xaeg/\xefX\xc9\xdd\x10A\x99kǍ4 lC\x98! \n\x8bqPN%\xe3\x10\x1e\x19\x88\x1a\x96\xab\xe7\xf2\x14\xb8}@\xb4u\x1e\x02V(\x90L̦\xdc\xfa\xcd?Q\xc6_\x82l\x96\xf3>Iu\a\xb4<%G\xf3\xbdם\x80Э\xc2\xcd\x7f\xa7;v\x8c\xe7\xcd\xd9R\x8epڊ\xa2\x02TBb\xa8\x1b\x1cx&\xb4\x01\x9a\xcb\v\xd6+j\x85`b\x9bG\xbb\xecDh\xf78T\xaf\xa5\xe4@\xa7w!\xbb\xc7\xe2\xfa\x154\xd1\xf7n\x98'j\xa2\x8e\bn\xdb\x1c\xe9\x90MQ\xab\xb4\b5\x06\xeaƉ\x9c$\xaa\x15}\xeb\xf2\x02\x8a\xe8\x980\xdc\xcf\xe29\xe3k&X\x06m\at\xbd\x16\xcc\xf4\x9dG\v\xe2E\x9dG;@t\aNɰ]\x0f\x00X\x01\rq\b\xce=r\xcd\x11\x8e\xe4\x1a\b-K(]\xeeҺ\">,q\x85o\x13\xc5\r\xc9\xd5\x1d\xef\tfQ6<\x83\xa0\x13\xf3\xb0\xea\x11V\xadx\x10r'V\x18\x8c\xeb\xa3uȉY\xaa\xa7\x0eoNVF\xcb\xfa%_M/i\xa1!\xbf\xe6\xf3T\xf0\x9f^@\xcbd\xf3\xcdQ\t\x8f9.X\xd2k\xae\x00{\xe2\xe3\xe2,\xe6Ɵ\xe9\xec7\xa5\xaf\\\xb1\xf4\x93\xca\xe2\xaeӠzN\xe1\xae\x02S\x81\n\xa5\xd9+,I/gwH\xbb\xe0%\xd6\xc9Y\xa6\n.\xb2+\xff\x1cU\xceat\xd3r~fy\x9b\xb6<\x19\x0e\x1b\x89\"v\xc8YY\xf5ci\x8f!\xa7\xfa\"\x1b\x8f\xfdJ\x8ba}a\xac\x82\b\x05\x862\x8c\xeci\x9cZ/\x16\x96\xf6\xf6\xf7\x87\xe5\x14\x98\xff\v\xd3\xff\xcdK\x0f3*%\xf2ј[\xa5\x19\x91\x98\x80\x95`\xb0\x1e\x1a\xbb\xfa\n\xdf\xce\x17\xfa\xfe\xbepj\xa0\xfe\xd2x\x89\x99ta3К\x803\xaa7Ak\xd0j\xe7\nD;\xe0s\x86\xb6\xffe\xe1NA\x040)~\xfdZA\x10__\xbd\xcf4\xf9gR\xc96Q\xd57\x83\xb2\x85\xea\x8e\xe5\x05\x0f\n=\xfc\x86\x02\x18\xfa\xf8\xfe|\xf8\xc5H_\xf6\x81Y\xb4\x04 \f\x8a\xba\xcc,\x13%{deKy\x90\xda\xee\f\x81c\xa0\x8e\xcf\x12Ф\"\x82qǀ\xa1\xff\x80\xe1ȗ\xc6m\xcb\x1c\xad\xe2\xe6}ѼꐓkB\x865\x1f\x13\xd6\xf0\xd8\xed\x8bg\xa9\x82\xfdMj=\x8e\xaf\xf0ȉ$\x16\xaa9N\xa8\xe1\xc8,\x16{\xf2~KN\x95\xc611\xf7\x8bUd<\x7f\x1dF\x16~\x96k.\x8e\xc1\u038b\xd7W\xbcbU\xc5\xeb\xd4RdVP<_)d^\xf4yR)\xc0r\xc02]\x05\xb1X\xfb\xf0\xa4\x80\xe6\xa4%-\xd64\x1cSɰH\x9d<1{\xb5Z\x85W\xabPxݺ\x84Y.\x9a\xfdxL\xe5A\x8c\x93~\xa6M\xc3\xc4\xf6\x90)rYg\x96m\x96Y\xe6f4\x91\x01\xcf\xf4Ù.:\x9c\b}\xddq\xe9D$\x19ҖL\x18yN.\xc5\xde\xc3M\xc0酏B\x9a\x83\x83lvZ;\xc6y\xff\xb4\x16\x82\x9d\a\xe5\xcfLjZ\xbbYMy\xfbI\xbaJ5p\xcaO\n\x1c\xbf\x8c`\xf4\xb3\xa3\xaf\xe9\xf9\xd7-7\xac\xe1`=\xbaGV&ϐ\x99\n\xf6\x11\xc9\x7f\x93xBj\xbdGH_\xee\xa2,\x9e\x8f\x82\x18\xaa\xc9\x0e8'4\xc5\x1d\a\xcb/\xdc\xc9\xe4B\xae\xf0H\xa0%o`\x12\x7f\x9e\xf9\xccI1\x1e\x03C\xea\xd5\t\xb8\x05\x15x\xbaY'\x162i\x0es\xb4\xe8\x81_\xee\xa2\v|\xf7K\vjO\xe4#\x960x\xef\xad;\xab\xe0Ս\xb61fP\x80^\x19Om*\x1c\x842\x9d\x82\"\x97\xc2\xf9\x12\xe3\xf9`\x1f\xab\xf9\xbaPͪs\x1b\x85%ǘ\xe8.d\xec\x9d\xe8\xb6\xe4\xf6\xe7\x16\xf5\xbfl\xe0v|\xe8\xb6\xe8+\xe5\xfb\xb3\xbfQ\xb1\xfe)E\xfay\xdbA\x8bE\xf9/\x15\xc8-\x85r\xd9\xdek^\xd1\xfdq\x9b\xa8/Xd\xff\x12\xc5\xf5\x99\x98\xca)\xa6?\x0eO\xafP<\xff\xaaE\xf3\xafU,\x9f]$\x9f\xb5\x8f\x99\xbdi\x95\xbb\xcdxb\xd5\xf7\xf2\xae\xfb|\xd1{F\xb1{\xc6N\xda\xf2\"OX^F1\xfbqE\xec\x194\xcb\x15\xc5W,V\x7f\xc5\"\xf5\xd7.N_ଅ\xcf\xc7\x15\xa1\x9f\xbc\x03\x13\xb6\xfaod\t\xb7R\x99\xa5\xe0\xe4v\xdc>\xb1\x93\xda\v\xd8$/\x89\bM\x13\xab\xc4\x10Ç\x17\xa7-*\xbd\xe9\x19\xdc\xe9\x9fei綴\xc7r7j~pVy\x03\n\x84\xbb\xe6\xe3?\xef\xbf\xdcD\xf8)\x9f\xd7{ƣ\xeb%\x9c\aSz\xe4\xf8\xad9_\xcc䰅>\xc03\xef\x8bІ\xfd\a\xde\xf7\xf6\x84t\xd0\xe5\xed5\xc2\b~\x1a^ \x17\xab(\xe2\x8e\xe5\x1a\xacŊ\xa8\x9a\x14\x8b\xeb\xcd\x00\xe2\xb0\xe2\xb7\x7f\x8d\x12\x94\xeeʬ`1Y\xa8\xf1\xb2\x82w{\xed\xe615\xca'\xeb4\x8a=\x91\x8e#+\xa6\xcaUC\x95\xd9#\xdb\xe8\xb3\xc1\x1c\x82\x99\x99K\xe7L*\xd6\xc3k\xc0\x92\xe8\r\xb7\x7f\xe1^\xe4\xbe\x19\xee\xf6\x8eqw\xca<\xa6ϟ,\x9ee\x91\x0e\xc0`\x9d\xac\xa8z\x1e\xe4\x0e\x82\x8f\x19\x96\x8dҊݒ\x1a\x1c\xb8?i\xc5\xf8Y/{\xfb:e:\x99Wl\x9d|\xb9\x96Cτ\xfa\xc1\x1d\t\xab\xda\x0e1uB\x81\xceb\xb8\x9dq\xf0c>\xb1\x90y5S\x9e\xc18\xe1:&\xc4W.\xaeH\xf2\x96\xa6̛\x98~SD\xcfh5]TP\xb6\x1cN\xbd\x87\xf5\xbe\xd7\x7f\xf9&\xd60Z\xc6]\xac\x16\xd9=\x03m=\xacᝯ\x9e\x12\x1er\x9f\x92SA8&lܕ\x8f\x85\xbb\x1d\xb8(@\xebM\xcbC\xe5h\xa1\x80\x1a(Cs\xa6㌏\xaa}\xd4\x0f\xac\xf9\xe0j(\xc7a\xf7I8\x9e\x06\x17.O\xefY\a\xdcSP\x8f\xa0V\x05z\x84\xad\x822Tt\xcex\x91\xa4\x0e \x99\xeeG\xf2\x03W=\xd1\xff{\x05\x02\xb9\xd29Q\xa1t\xb4\x0f͢\xa3\x81\x92\xc0#\b\xc26\xa47/)z\x13N\x81\xffLq\x03\x0e6\x1b(\x8c\xdbХ\xe80\a\xa9=\xc0\b\xeb\xe4>\xe1Y=\xc1\xe0\xb5\r\x97\xb4\x04\xe5\x1c\xed\x05B\xfeנ\xf1H\x13\x05\x04t\x97(\xcf^@\xfb${\xd4PE9\a\xfe\x89q\xd0\x1f\xe4N\xd8ye\xa8\xd9\xdbT\xbf\xde\t\xe8\xa2U\xd6Y\xdb\x13\xd1\xd6kPD\x831\xd3iٍT\xf3g\x91\x1c\xe2\x990\xb0\x85T&{\xa7\x98\x81\xfb\x86*\r8\xa3\x8c\x15|\x1fuqy\xde\r\xa7[Wt^\xb2\x82\x1a\x88\x82\x83#LM\x1f\xfbk\x84\xc5\xf7X\x03,'\xb6\x97\xb2U\xf5\xd4\xe1\xc7Ie=u\x91w\xc2\x01K^\xe5\xed\xfc\xac\x826\x06\x8f\x9a\"\x1d\x91\x88\xc6\xc3\xc0\xeb\xf1G\xb7y\x0f\xc0Ns\x9a?0\xe4Kӵ\xa1u\"\xf6[\xd6tW\x87`\xf0\x02~U\xf6*\xdc\xfbW\x19\xc7Rv\xb2\xa3:\x1e[JFT\x1dl\a\x06՚\x05\x1d4\x93\x15E\xca8\x94s\x9c\xfa5j\xab\x9ft\x84\x835\xf7\x96\xc5\xef\rU&N\xfd\xd0;u\x91\xf9\x05)\xa9\x81\x95\xed}\x9a~J_H\xaeԉ\x857x\x86܋G\x11\x0e\xb8Z\x9fƝ\xfc\xaeAk\xba\r\xe9\xde\x1d( [\x10\x16\xefq\x17/\xe9\a\x87\xc3\xf3\xde\x05\x18\xa4{haZ\xea\ap\x8ey\xacS\n\xbf\x04\x80\xf9\xe2\xed\xa4\xe1M\xab\n\x7fL\xff\x0e\xa8\x1e\xff\xb0\xc4\x01.>\xf5\xdb\xfa\xedX\xb7bW\x85@\xddQ\n\xfci\x01\xc3b\x0e;%\xd3F\xe2\xc8G9\t\x95\x94\x0fY\xc1\xd3\xe7ذ۸a±\x12^N\xb0\x96\xad\xe9y\xaf\x1e\xe1\x89i\xe2E\xdb\xcfl_\x10\xe6\xa5;\xaa<\xb5\x8b\x99\xe7\xbf\x7f\x1e@\x8aI\vi(\x0fF\xc6\xf2elP\xcd\\\xd5s\x1f~\xa6\x80\xf3\xfd\xd9\x18\xf2\xe8\xf7O:\xd8Uwi\xb6\xd7\x04\xddE-\x13\x03\x85\xfd\xb5$\x90x\xdfv\xe7iN\xddn\xbcd\xff\x10\xea'\x9cT\x06\x8e?w\xad\xa7\xf0\xe8\xa6\xe9\xc2 \x10\xe9\xfc\x01\xc1\x90\xd2TQ2N\x98\xfaL\xec\xd1TT/\x05\x1d\xb7\xb6Mt;z\xe6*\x86\x16w\x13R\x99\xbeQbEn`\x97x됅u&(U\x89&\xd7\xe2Vɭ\x02}\xc8t+\xbc9\x80\x89\xed'\xa9ny\xbbe\xe2\xcb\xf4\x19\xab\xb9ƷT\x19f\x99\xd6\xcd'\xd1\xf7*ظķ\xe5\xde\xd3\x1f\x98\xa0\x9c\xfd\x9a\xd2\xe5\xfd\x8fK#\xcc\xe8\xbb\xc6#\xef\x14\v\x15\x10\xbf\xa4\x00\xbd\x86\xfeI\xf7\xccO\x18\xf7\x9c\xdcȤ\x18\xfbR,6\x04\xca4Y\x836+\xd8l\xa42n\xa7|\xb5\xb2\xe1\x8bw\x90\xac\x86\xc0\xe8\xdf\xfdn\fa\xa9\xe8*\x16\xb9\x04\x87e\xe3\x13\xc4\n\xad\x0e&\x12j\xbawyfZ\x146&\x80w\xda\xd0T\xc4\xf9$=\x8d\t\b/+9*\xe4\xba\xdf>fn\xa3\xfa@p\x0euxq\x8c3\xe8|\xaazdp/\x15\xd1\x16{\xa7(\x13\xe2\xd4\xd8\xf5t2%\xcf\xd4|\x8dP\xa6ԣ_\xdf\xe0'/|)\x93od\xc9VTTl'\x8f\x8eWJ\xb6\xdb*\xf0\xe6\x94CD\xca\x16\xf3!\r\xaa\x02\x1d~\xa2˴J\xf4\xcac|5㔖\x8eӝ\xf6Q\x9e\xa0\xa8Uw\x84\xb4SU36?;\xf7;\x01q\xd1\xf6' R\xbd\x17\xc5\xeca\xd7Ýǣ\\\xcb$\x12\xa26~6$D\x88SH\xe8\xfb\x12]\xc4\xf3\xbb\xc1Ȕ\x8fr\":\xe6\x9d\x18\\\xe2<\xa8\xe5E\xf7\x9d\xa0\xa1\xbbs\x1c:\xf4 \xf8;)\xd17\x80pL\xe4\x8bc\xa7\xe3\xde\xdfo\xc4\xfa\x18\xbd\xad\x8f'Ǯ\xdfF0F\x97\r\xd8(\xb6\x1b&ě\x7f\xcf6)yq\xbf\x83\xb8\xe6\xf0\x0f\a__\xf9Ҁ\x1dU\x82\x89\xedI\x18\xf9\xee\xfb&\xe2y\x0f\xf6%#\xfa0\xf3g\x8b\xe9\x93f\xe9\xe0%2x\xd9ó\x1fɿ\xf9\xbf\x00\x00\x00\xff\xff\x9d=\x85\t\xc7t\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=\xdbr\x1c+\x92\xef\xfa\nB\xfb0\x97P\xb7DZ\x97\xd8ЛG\xb6g;\xc6c+,\x1d=\x0f]\x95\xad\xe6\xa8\n\xea\x00%\xb9wv\xfe}#\x13\xa8[\x17]T\xab%ϙ1/\xb6\xaa!I2\x93\xbc@\x02\x8b\xc5\xe2\x8cW\xe2\x0e\xb4\x11J^2^\t\xf8fA\xe2_f\xf9\xf0\xdff)ԛǷg\x0fB\xe6\x97\xec\xaa6V\x95_\xc1\xa8Zg\xf0\x1e6B\n+\x94<+\xc1\xf2\x9c[~y\xc6\x18\x97RY\x8e\x9f\r\xfe\xc9X\xa6\xa4ժ(@/\xeeA.\x1f\xea5\xackQ\xe4\xa0\tx\xe8\xfa\xf1\x0f˷\xff\xb5\xfc\xcf3\xc6$/ᒙl\vy]\x80Y>B\x01Z-\x85:3\x15d\b\xf4^\xab\xba\xbad\xed\x0f\xae\x91\xef\xd0!{\xe3\xdbӧB\x18\xfb\xe7\xde\xe7O\xc2X\xfa\xa9*j͋N\x7f\xf4\xd5l\x95\xb6\x9f[\x98\v\xf7\xbb\xfbM\xc8\xfb\xba\xe0\xbamsƘ\xc9T\x05\x97\x8c\x9aT<\xa3\xaa~l\x04b\xc1x\x9e\x13\xb5xq\xad\x85\xb4\xa0\xafTQ\x97\xb2\xe9 \a\x93iQY\xa2ƍ\xe5\xb66Lm\x98\xddB\xb7\x1f,?\x1b%\xaf\xb9\xdd^\xb2\xa5\xa1z\xcbj\xcbM\xf8Ց\xcf\x01\xf0\x9f\xec\x0eq3V\vy?\xd6\xdb;v\xa5\x95d\xf0\xad\xd2`\x10e\x96\x13s\xe5={ڂdV1]KB\xe5\x8f<{\xa8\xab\x11D*Ȗ\x03<=&\xfd\x8fS\xb8\xdcn\x81\x15\xdcXfE\t\x8c\xfb\x0e\xd9\x137\x84\xc3Fif\xb7\xc2L\xd3\x04\x81\xf4\xb0u\xe8|\x1a~v\b\xe5܂G\xa7\x03*\b\xf62\xd3@2}+J0\x96\x97}\x98\xef\xee!\x01\x18\x91\xa8\xe2\xb5\xf1r\x14Z_w?9\x00k\xa5\n\xe0\xf2\xac\xad\xf4\xf8\xd6\xc9^\xb6\x85\x92_\xfaʪ\x02\xf9\xeezu\xf7\xef7\xbdϬO\xd1\xff[4\xdfY\xc3\r&\f\xe3\xec\x8ef\x10\xd3~J3\xbb\xe5\x96i@1\x00i\xb1F\xa5a\x11H\x9d3\xa5;\xa0*\xd0B\xe5\"\v,\xa2\xc6f\xab\xea\"gk@n-\x9bڕV\x15h+\xc2|r\xa5\xa3z:_\x0f\xa1\x8f\x05G\xecZ91\x05C\x92\xe9g\x1b\xe4\x9eHn\xf2\bӎ\x878\x88\x9f\xb9dj\xfd3dv9\x00}\x03\x1a\xc1\x84QdJ>\x82F\x8ad\xea^\x8a\xffm`\x1b\x9c\x12\x96$Ղ\xb1\x8c\xe6\xb3\xe4\x05{\xe4E\r\x17\x8c\xcb|\x00\xb9\xe4;\xa6\x01\xfbd\xb5\xec\xc0\xa3\x06f\x88\xc7_\x94\x06&\xe4F]\xb2\xad\xb5\x95\xb9|\xf3\xe6^ؠ\x903U\x96\xb5\x14v\xf7\x86t\xabX\xd7Vi\xf3&\x87G(\xde\x18q\xbf\xe0:\xdb\n\v\x99\xad5\xbc\xe1\x95X\xd0@$)\xe5e\x99\xff[\xe0\xb7\xe9u\xbb73]!u:\x83=\xa8g\x9dt9Pn\x88-\x17\xf0\x13\x92\xee뇛ۮ\xe4\t\xe3\x99\xd2\x11\xc0\x18\x7f\x90\x9aBn\xc0낍V%\xc1\x04\x99WJHK\x7fd\x85\x00i\x99\xa9ץ\xb0(\x06\xbf\xd4`,\xb2n\b\xf6\x8a\x8c\x16\nm]\xe1\xdc͇\x15V\x92]\xf1\x12\x8a+n\xe0\x95y\x85\\1\vdB\x12\xb7\xba\xa6xXّ\xb7\xf3C\xb0\xa7\x11\xd6\x06]qSA֛j\xd8NlD\xe6&\x14\xaa\xe4F\x95\fԲ+㳟~!\xdd7\xfc:\xc0\xc3)\xc8\xd0+\x184JvK\xccom#\x8a\x9c\x83ƔfR\xd9=\x98\xfb\xaa\xb5C\t\x0fe\x02\x93=ag{*5Œ\x8e\x00im\xebP\xbe\xa2\xac&\xbc\x1fD\xb5*K\xc8\x05\xb7P\xec\x8eB\xbf\x0fb\x8c̊\xfaak\xa7\xe7ŦG\xf4\xbc\x06&:\xedi2\xfe5\xd4ط\xc6\x7f%\xcbNF\xd4Ѥ\v\xac\x96-\x0f\a\xfdHx\xda'\rc\xab\r\xb3\x1au\xae\xc7\xeeI\x14\x05\xcedĸ\x82\xbc\x87Z\xbc;\xb1a\u0086Ѭ9a \xd9\xd2yQ\xcb\xd6gh\xec?\"8\xc0\x8eԾ\xeb\x1f=\x15n\x99\x84o\xb6\xad\x85Î\x8c`\xc3\v3\x18\x82WH\xb3\x86q\xc1ֵ=\x0e\x03(+\xbb\xbbpm7\xaa(\xd4\x133\xa4l\xd1\bn\xc4}\xad\xddd\xffm\x0e\x1b^\x17\xf6\xd2\xe1\xfc\xbb\x98\xb4\x8eO3\ve\x85&\xf3\x189\xbd\xf5mq\xc08[\xf2&\xfe\bnr\xf0C\x94w?F\x80(\xe7\xc5VZ=\x8aܛ\xf3=u\xc5\x0e\xaa,,\x8eQ\xb78ґ_\a\xa3\xf9cS\xb93\xb7\xb6\xea\x89=b\x00\x00\xe8-r\x1c\x14\x02\x85\x9c\xd5\xd5\xc5(Lƞ\x84ݲJ\x19#\xd6\x05xρ\t\x99\x15u\x8e\n\xe6c]\x14$\x82+\x99i(\xd1\xe1)Ƹ\xcd\x18Ⱥ\x1cG|AP\"?u\xe0\x8e\xd68\xa0\xa7\xb0dF\xdcH^\x99\xad\xb28\x8dTm\x13\x887*\nX\xaenV\x03h\x1d\xea\"\x8f)~\xa0\xc9m\x15{\xe2\u0092\xa1\xba\xbaY\xb1;\xa2{h͜\x86b\xb6\xd6\x12\x9d\x83H\x7f_\x81\xe7\xbb[\xf5\x93\x01\x96\xd7\xe4ׄ\x98\xe0\x82\xada\x83N\x80\x06\x84\x81?\x81\xd6h\x14\r!\xa1\xea=?3\x94['\xc78\xa1\xbc\x9b$\f{\xfb\aV\nY\xdbѩ:Ie\x94\xa6R=\x82~\x0eq\xdfs\xcb\xff\x82@\x064%Q%\xe8~\x96\x11}\xd7;\xfaq\x1d1_\xae\xac6\x1d\xa8°\xf3sT\xa1\xe7.\x86?\xbf\bS\x9aH\xb1\xaeEa\x17Bv\xfb\vz\x1d{<\x8e0\x0e\xb8c\xbe\xb9U\x1f\x8d\x9b\x98ϢS\x04\xe6\x88\x11\xadT\x1e&\xfcF\x14\xc0\xcc\xceX(\x83\xcaoæN,8,\xe4h\x15\x85\ac\x90\xee~P\xe3\x04\x91uQ\xf0u\x01\x97d!\x0f\xd0l\\Y\x8f\x11\xed+\x18+\xb2S\x92\xccA\x1c!\x98\xf6?\xf4(Cq\x17\x7f\x00\xc6#\xe0==1\xc8+\x8a\x0e\xd1\xfbԊ\xe2Vi\xc80\x00\xb8\U00101140\x82\x82\x19\xa9X\xa1\xe4=h\x87Ec\xe8\xd1\xd0\x00\nh\xce\xd0g\xd7h\x9e\x85d\x9b\x1a\xdd\xf9%Cm\x11\x95\x11!\x8d\x05\x1e\x11\xe6\x13\xf0\x0e\xbe\xa1e\x80\xfc\xaa\xa8\x8d\x05}\x93\xa9\n\xf2\xb0z7j\xd6Ry\xf8\xe1 d\x1f\xfc\x15\"\x03\xe4C\xe6*-h\x85,&\xdam\x1c\x88f\x92\x16\xf3\x90\xd5~\bm\x807\xa9c\fXlx\xfe\xfb\xf3\v\x92\x80~\xef\xfd~\f\xe3\x1a\x1a2\xcd\xd2\xd1\xe4.\x8d\xb7\x10\x16\xca\bu'u\xd4\f\xbes\xad\xf9\xee\x00כ\x95\xc8\x17\xe0{\f\xf6\x80\xf32T\xfbN\xbc\x1f\xf6\xff\xaf\xc8\xfd\xd3\xf2\xdb\xd0j>\x17\x12\xf9\\\bc{l6n\t\x10\xc9:\x16\x7f{\x02I\a\x13\xd5\xe4\x14W\xffA\x88yҹ\x13\x9b,\x8dl\xfa\t\xf0OEɭR\x0f)\xd4\xfb\x1f\xac\u05ee\xff\xb1\x8cv\x9c\xd8\x1a\xb6\xfcQ(m\x86k\xcc\xf0\r\xb2\xdaF5\v\xb7,\x17\x9b\rh\x84E{$͖\xca!b\x1d\x8e\xfdXGeE+\f\xc6\xd52\x1dYJԈ\r\x85\xa2\xfb(T\xe7\xe0`\x88A\x0eD.\x1eE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7d\xa8$\xa0\xaf_b\x8c\xb4_5N\x89\xb0\x0es\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccr\v4\x05_C\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fu\xb5\x83\x99\x00\xcb(\xd4݊l\xeb\xdcW\x144\x82\xc5r\x05\x86\x96\x94xU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8&\xc9(\xbe\xffJD\x0fV\xe7Ha\x9f\xd0$\x8c6[\x92\xe7C\x94\xf4Hq\x01f\xd9Y\xda\x146|Mah\xcf\x7f\xdcۇ\xda#ʯ\x8bw\xc7M\x98\x19\xac\x9b\x9cS/˸\xa6\x9b\x7f\x12\xbe\x91ɺ\xf1\x16k\x16\xcf>u[^Ж\x8agH~\xc16\xa2\xb0@N\xd5\x14\xa2l\x06\xe7NI\xa0T\v\xcch\x87\xddf\xdb\x0f\xcd\xc6[B\x8b\x01\xad\x86\x00\x9c\x83\x1e\xa2\x1c\xe2A\x02Hָ\x16\xb4\xe3,܊\xb9YR$\xd9\xfdB\xae\xe0\xbb\xcf\xef\xe3\xb1g\xb7$J\xeaޠ\x12&\xad+\xef\x06\x8eQ\x17W\x1f\xaa\x84_\xc8_k\x02A\xb7\x0fq\xc18{\x80\x9ds\xb1\xb8d\xc87\x1e*'\xa2\xa0\x81\xd2)HS<\xc0\x8e@\x8d\xe7G\x8c\x979\xd2\xe2\xca\x03\x8cl\x99\xc6J\x8f\xae\x88\x9f߈rt\xc3\x0fD\x98\x94\xd9Ԗ\x86\xa8~\xfa\x8cd'\xc4\xcb\f\xbd\x14J\xe0ˑ\xc3N\x16\xa7n_\xfd\x8c\xa2\a\xd8\xfd\xc68^\xe3,\xdb\nڱ\xe3\xb4z\xa36\xb3\x18\xee\xca\x1d/D\xdet\xe6\xe6\xd5J^\xb0\xcf\xca\xe2?\x1f\xbe\t\x83\x1d˜\xbdW`>+K_^\x94\xcan\x10\xafA\xe3\xb0\v\x88\x03t\x96\x04\x89\xd8ͼq\xb6\x14\x05\xb5\xe1\x870l%1$s$\x9a\xd1\x1d%Z\xb9.]gemh\x9fZ*\xb9p\xcbbc\xbdy\x1e(\xddc\xc1I:\xf6\x9dޢ1r\xbf\xb8\x94\xaf\x82g\x90\x87\xad:\xcaE\xe2\x16\xeeE6\xa3\xcf\x12\xf4=\xb0\n\xcdB\xba\xb4\xccP\xd4~d\xf3\xc5+\xdds\xe8\x96o\x8b\x87z\rZ\x82\x05\xb3@\xb3\xb6\xf0P\xac*\x13\xe9\xe2m\xc2H\xc2\xceXY\xe0\\O\xac\x19\xa4%\xa9z$\x9d\xe9p\xf5Tb=\x93L\xe4E\x90ە$\x05ݬ\xe0y\xd6k\xa6\xdc\x1c\xa3b:cq.@\xc9ik\xedoh\xe9i6\xfe\x9dU\\h\xb3d\xef(-\xba\x80\xdeo~a\xb2\x03&\xb1ۊV\xd9\x7f\xa9\xc5#/\xd0\xff@\x03!\x19\x14\xce\x1bQ\x9b=_\xed\x82=m\x95qnC\xb3iw\xfe\x00;\xb7\xb3\x9c\xd4mWa\x9d\xaf\xe4\xb9\xf3e\xf6\x14O\xe3\xf8(Y\xec\xd89\xfdv\xfe\\\xf7n\x86DϨ\xda\x13\xe5\x92W\xe9\x92LI\xc7s\x02\r\fփC\x84\x8d\x9b\xec[\f\x10\xa6(\x90,ʕ2\x91\xa4\x91\bZ\t\x82~\xad\x8cu\xeb\x90=\x7f\x7ft\xa1R\x85\xc5I\xc67\x1643V\xe9\x90ϊ\x8a?e)\xbe[n\xb7`\xc0\xefC\xf9EO\a\x18\xa3\xd8\xf3V78\xabr\xee\xf6¨#\x9e\x91\xf7Dm+\xad20Ѽ\x88\xb6$ڦ\x91\xec\xa5.\x1d\x9au]\ue8bfM\x92\xd6NY\x94\x0ee\x9e#\x8f\xa4;\"2\xfa\xf0\xad\xb3D\x8d\xda\x05\xffN\x91\xd6cpdt\x10\xa6,\xf90\x97:\x19\xdd+\xd7:\xcc1\x0f̅[\xfa\xbe&\x9d3\xc7\xebhD\xf9\x1f͵)\x85\\QG\xec\xed\v\xbaC^\x8b\xc7Ҥ\xc6\xca\xf1N\xfaU\xe8\xac\xe5^\xf3\xc1'$*\xda\xf8\xd1\xd0c\xee\xfe\x9e\by\xd7R\xd9\xce2\xceL'\xbaR\xf9o\f\xdb\bml\x17\rs \xb1j\x14\xd4\x11\xa1\xa7\xfc\xa0\xf5ё\xe7\x17\xd7z\x90B\xe9\xb2\xce\xe7\xc4ہ\xa4[\xfe\b>\xed\x17d\xa6jIKa\xa8\a\xb0\x9b\x19\x10\x1dk\x9c\x15H\xb4w\x9d\xc6\xd1Ḻ\xb2 I\x12rrݬ\xdb\xe4#\x17i\xebV\xec8\xb6\xdaC\xb9\x9cc\xe5\xf8y\x14\x12=\xbbg\x11J\xfeM\x94u\xc9x\x89<$\xb7C\x94\xd0\x1cGp\xecn\xd2?\xb1\x05\x19-\xabp\x96U\x05X\xf0\xe9\x9b3\xf0Ȕ4\"\x87\xc6\xf4{\x11P\x92q\xb6ᢨ\xf5\f\xad:\x9b\xe4s\x830\xafMN\x1fY\xa5#\xb2 \x12%\xae\xb3\xcf\xf0\x82\xa75~\xa5\xe7\xf9\xb1)\x0e\xa3\x86\xf9\xfeb\xa5\x85r')N\xef2\xfa\xf4c.w?|\xc6\x1f>\xe3\x0f\x9fqNG?|Ɖ\xf2\xc3g\xfc\xe13\x1e.?|Ɣ\xf2\xc3g\x9c\x89\xc8\xf7\xf2\x19S0\\\xd0\x1a\xe7\x81\nIX%\xa6BL\xa1=їO\xfa\xf1g5N\x92˼\x1a\a9r\x88'r\xfc\"\xe6u\xb4ƫIn\xc6\x19\x18\xe6\x8e;\x82\x9a\xe00\x9f\xe0\xf4L@\xe0\xf4\xa7gV\a!\x9f\xf0\xf4\x8c\x1fBZ\x84q\xd4ٙ@\xa4\xf9\xa7'.|\x12Q\t\xaaA\xfe\x83mU\x1d9\xb51Aڄ,\xda4\x82\xf4\x92j}b\x04X\xfe\xf8v\xd9\xff\xc5*\x9fbK77D\x80\xd1e\x1e<\xa7\x1b\x1c:\az\xbc\x1e\b\xf7L\r\x852\x02Li&E\xe1$6@\xe8\xc9+\xfbR\xb9\xd5\xc1\xa3\xfd\xa6\xe95\xac\xf4Dܹ\xe9\xb7M\xb6\xe4\xb4\xfb\xfe\x8c\xa4ۓ\x1e\x8d\xfani\xb5\xc7%Ӧ\xaeP&$Φ\xa7˦\xb0Օ\xf4$\xd9\xe4\b95!v\xee\nċ&\xbf\xbeL\xcak2\xcd\xd2\xd2[\xe7R\xecURY_9\x81\xf5\xf5\xd2Vg$\xab\x9e\xfe\xd4K\xfaZ\xfa\xd1ٕi\xcb2\x87\x13N\x93\xd2L\x93\x96nR\x06|\xd4P\x93\xd2G\xe7&\x8d&q2}\xba\xbejZ\xe8\xab&\x83\xbe~\n褴MV\x98\x9b\xe49~Cd(\xd3\x0e\xc0\xde\x05ϣ\x95^BBg\xad\x97\x1e\x9c\xc0\xc5\xf7\x98`\xcfe\xb5ҽ\xf0\xe2Y\xb1\xf3\x97\x01,\x14\xf8\xe0j\xbfb,Sօ\x15U\xd1^\xc8\x17\v\x9a\xb7\xb0k.\\\xfaY\xd11\x7f\x7f\xebؗ\xafͬ]\x0e\"3n\xd8\x13\x14\x05\xe31\xfd\xb2G\x85\xcc]\x04\x9b\xa9\x05\xa0}GM\xe5/\x94\xf2\xb7\xc7^\xb8)O7\x1a\x90\x97PƖ+\xb9<|[\xd9A#\x9c\xaa\x8b\xf7\xa2\v\x173ѷ_j\xd0;Fw\xa75\xfee{0\xd6++\x83\xc1uP\xa1^\xa5\x1f\xda\xf7\xd9\v\xd2Z\x15\xc7\xdeI\xe7\xd5\fq\xa26\xa8;۠\x14\r\x83\x8c]4\xc8\xc2\x04\xdd\a!U\x03!\xd24%\x80\x99sR\xf4%B\xd4S\x04\xa9I^\xdc<\x0f\xfc;\x9e\x00=\xf6\xe4gzBM\xd2Iϗ\bY\xe7\x04\xad\xb3|\xee\xf4\x93\x9c\xf37\xcf_\xf8\xe4\xe6K\x9d\u061cA\xbd\xd4\x13\x9a\xf3i\xf7J'2_\xfd$\xe6k\x9e\xc0\x9cu\xf229\xc5lV\xd6Ĝ\xf4\xb0g\x1c\x19Lˇ\x98>I\x99x\x8221[\"m\xf0G\x0e;\xf1\x84\xe4\xfc\x93\x91\x89\xfc\x9d3\xa5_\xf9\x04\xe4+\x9f|\xfc\x1e'\x1e\x13$0\xa1\xca\xfc\x93\x8d\xcf\xdeVS:\a=\xb9u9Gj'\xe555\x96\xeb#6؛\v7\xe2b\xad^\f@fɿ\xe4@\xafv\x1c\xda\xcaG\xc9\xecxD\xbd\xbd\xd5\xd6]\xeb;\xc4\xfe9\x0f\xb7\xfdj\xa0\xe2h\x00(p\xa3\xf4\xb2\xa8\xab\xf0\x81g\xdbA\x0f[n\xd8F\xe9\x92[v\xdelx\xbfq\x1d\xe0\xdf\xe7K\xc6>\xaa&ߨ{\xe7\x9b\x11eU\xec0\x12c\xe7\xdd\x06ϓ\x92\xa8t\x86\x9e\xafU!\xb2\x88\xcf9z7\xa0k\xb0wa\x12\xdd^\x98u2^b\x81\x0f6\x17\xe1\xe6\xc8\xfe\xb5\xd2\xeeB\xffc\xd7{*\xf1'zo\xeb\x04+\x87\xef\xaeW\x04+\x88\x11=\xe4\xd5$Y6,_\x03\xba\f\xed\xd8\x0f\xe9\x93զ\a\xb5\x9f\xe7\xdc}\xad\x04r\xf74Mp[\xbcj\xce\x14j\xad\xeb\x95\xc3\xe5PO(_\\\xee\x98\xf2o\x8f\b\x9d/*\xae\xed\xce%D]\xf4\xf0\bv}j\xe5\uf835\xda\x7fz\xa7[zd\x0f\xaf\xee\xd0n\xfc\xae\xea'@\f\xe9\xf9\x1c\x9c\x0e\x9f\f\x9f<\x13\xfe\x028\x1dv\xa1\x16D\xc5\xc8O\xd1,Γ\xaf\xba\x1a\xff\xda\xc0_\xd4#\xbc\x8f\xae\xbe\xf6\x9f\xdf\x194\x19I\xaf\fP\xe9\xa2\xfc\b\x05ۜJ\xba\xa7\xfcyj/\x9e/\x19P\xf1\xf7\x9c?gq\xf2\xa6\x0fj\xfcE\x1a\xba\x05>t\x1a\xf3\xaa譯\x1d\xbb\xbe\xa3\xb8\xb5Q\xa5~\xea\xfb\xb85,O\x86$\x89\b,!\x0f>\xd2s*2Z\xa5\xf9=|R\xeeq\xa5\x141\xe9\xb7\xe8=\xbd\xe5=\xb7\x90s\xee'aL\xd1\xfb\xb1\r\x01\xb6gL\xf6\x1e+@l\x8f|\x8e\xc1\xda\xe292r{\xfbɍ\x94\u07b4y\uf7e7A}l\x00Y\x10(࠭\xf1\xbf[\xf5D\x97\xf8\xc7ט\xc3\v2\x9dG\xec\x80\x0e\xbbP\x1a\xf2Qì\xabB\xf1\x1c\xf4\x15\xbd\xa2\x930\xe2\x9fz\r\x06\xee@\xff-\x1eo7#\xe3\t=\xbf`\xa6\x0fztE\x01\xc5GQ\x80q\x88'\x9a\x86\xeb\xfd\x96\x8d\xa5\xa8˵\xf3T7\xf8c\xd3\xc9\x01\xcb\xec\x86J\x1b\f\x15h\xf4\x13\xddVDm\x82\xe4\x1f&\x06k\xf8(\xa4\x85{\x18\x8f\xa1'l\x82{g\x82\x1c\x80\xa0\xc0(\xe2\xfbsl\xe5\xb1G\x90\xbbx\xeb\x81\f4\x8b\x9119Vޭ\xba\xbe\xbb2\xac\x969m\x00\xdc\xfd\xe9\xe6(\xf9}콕\x13tB\x8az\xbf\x1bo\xd9\t\x11:ډ|\xfa\xb8\x12\x8f\xc1\xe2ƨLPTA\x8f#\xd1i\xaa\x97\xbb\a\xfdP\x80x@:j\x03_\x9e$\xe8\xaf\xc1\x02\x99\x95\x8c\xbd=3\xad\xfd~ڃ\x16}s\xc6*\xec{\x04\xc6\x00\x00Sa\x9f˸W\x8d\xc2\xf6\x9a0\xcd\xebv\xfb\xf4\x9cP!qK7\xee\xb0-\xc6\x1f\xe3Z4\x8f\x86\x9d%\x90۽\xe5\xd4\a<\xfe\xa6\xa1{\xf4)㕭uЮ\xb5\xa6\x9b\xe2\x11\b\xb8\x8bԏ{հ}\xec\xee\x18\x06\xb7\xaf͵\xfb\x0f\x93\xef\xe1\x8e\xc0i\xde%\x8c>r\xe6\"j\xf7^\xed\x02\xe1\x1f\xc7\xe3\xd1\x19\x838߸\xc7\xeb&\x88\xf0\xa9\xad96\xe0f\x188d\xff\x1cޫ\x8e\x84\x1e\x0e\x98\x18\xc35\xd6iN\xeaz9\xa2\x86\xe1\xc1\x81\x9b\x18\x13Əs.\xd8g؏\xd8\x17\xec\x83\xc4A\xec\x13\xc0\x9dل\x9c\xb6VH;\xce\x19\xe2cӊ\x0e̎h\xc8i\xb1\xbd\x1b\xc0\x18d\xe3\xd3\xc3UM\x15wbְߊ1o\x94v\xcc2\x1c\xe8\xef\xf6~\x8dj\xf0\x83\xda;\xa6\xb9G\xd5\xc8\xdeGz\t1\xefH\x8e\xf7һ_\xeau\xfb(\x04\xfb\xdb\xdf\xcf\xfe?\x00\x00\xff\xff\xeaC\x1a-[}\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcV\xcfo+5\x10\xbe\xe7\xaf\x18\x89+\xbb\xa1B \x94\x1b*\x1c*\xe0\xa9j\x9ezw\xbc\x93d\xa8\xd7^f\xc6)A\xfc\xf1\xc8\xf6n\x9b\xee:\xb4\x8f\x03\xbe\xad\xed\xf9\xe6\x9bo~x\x9b\xa6Y\x99\x81\x1e\x91\x85\x82߀\x19\b\xffT\xf4\xe9Kڧ\x1f\xa4\xa5\xb0>ݬ\x9e\xc8w\x1b\xb8\x8d\xa2\xa1\x7f@\t\x91-\xfe\x84{\xf2\xa4\x14\xfc\xaaG5\x9dQ\xb3Y\x01\x18\uf0da\xb4-\xe9\x13\xc0\x06\xaf\x1c\x9cCn\x0e\xe8ۧ\xb8\xc3]$\xd7!g\xf0\xc9\xf5\xe9\x9b\xf6\xe6\xfb\xf6\xbb\x15\x807=n@\x90ә\x1a\x8d\xc2\xf8GDQiO\xe8\x90CKa%\x03ڄ\x7f\xe0\x10\x87\r\xbc\x1e\x14\xfb\xd1w\xe1\xbd\xcdP\xdb\f\xf5P\xa0\xf2\xa9#\xd1_\xae\xdd\xf8\x95\xc6[\x83\x8bl\\\x9dP\xbe \xc7\xc0\xfa\xe9\xd5i\x03\"\\N\xc8\x1f\xa23\\5^\x01\x88\r\x03n \xdb\x0e\xc6b\xb7\x02\x18\x05\xc9Xͨ\xc5\xe9\xa6\xc0\xd9#\xf6\xa68\x01\b\x03\xfa\x1f\xef\xef\x1e\xbfݾ\xd9\x06\xe8P,ӠYֿ\x9b\x97}\xa8\x85\t$``\xa4\x04\x1a\xc0X\x8b\"`#3z\x85B\x19\xc8\xef\x03\xf79\xad`v!\xea\x05\xaa\x1e\x11\x1e\xb3\xfec\x98\xed\xcb\xe1\xc0a@V\x9a\xa4)\xeb\xa2\xe2.v\xff\x8dxZ)\xd6b\x05]*=\x94\xecy\xd4\v\xbbQ\x1e\b{\xd0#\t0\x0e\x8c\x82\xbe\x14c\xda6\x1e\xc2\xeew\xb4\xdaΠ\x8b.\x922\x19]\x97*\xf6\x84\xac\xc0h\xc3\xc1\xd3_/ؒ\x04JN\x9dѬ\x9dWdo\x1c\x9c\x8c\x8b\xf85\x18\xdf͐{s\x06\xc6\xe4\x13\xa2\xbf\xc0\xcb\x062\xe7\xf1[`\xccRo\xe0\xa8:\xc8f\xbd>\x90N}hC\xdfGOz^疢]\xd4\xc0\xb2\xee\xf0\x84n-th\f\xdb#)Z\x8d\x8ck3P\x93\x03\xf1\xb9\x17۾\xfb\x8a\xc7Ε7n\xf5\x9cjP\x94\xc9\x1f.\x0er\xeb|AzR#\x95b*P%\xc4\xd7,\xa4\xad$\xdd\xc3\xcf\xdb\xcf01)\x99*Iy\xbd\xba\xd0e\xcaOR\x93\xfc\x1e\xb9\xd8\xed9\xf4\x19\x13}7\x04\xf2\x9a?\xac\xa3\\\xb8qד\xcaT\xda)us\xd8\xdb<\xab`\x87\x10\x87\xce(v\xf3\vw\x1enM\x8f\xee\xd6\b\xfeϹJY\x91&%\xe1Cٺ\x9c\xc0\xf3\xcbEދ\x83iv^ImeJl\a\xb4)\xb9I\xdfdM{\xb2\xa5\xad\xf6\x81\xc1\xd4L\xda\x0f1\xc9\x16_\xc8e\x9cH\x85\xcdlN\xa5.\x7f\x9fM},哣\x11\x9co\xce8ݧ;s\xff\x8e\xf6h\xcf\xd6a\x81(S\bߧ\x92\x16\xfa\xd8/}6\xf0\t\x9f+\xbb\xf7\x1c҄\xc6\xf9\xa8\xb9Z\x1bP\x1e\xb1\x03\xf9E\xb8\xf3\xc8ʭ\xfc0.G~\x0eh\x04\x02\x8eާ\x96\x0e~\x01Yy\x11\x16wH\xb1\xaf\xb0\xa9\xf2\xb9\xf3\xfb\x90\xff\"Lrl\xb4\xb4\x13\x8e\xc9\x1e\xfd\x14^\x15\xc0\xeb\xb9.k9\xe7>$hY\xf9y\xfeo\xc6i.\x11c\xd5w\x93YU\x0f\x92ǚ\xe2\xf5\xfe\x1aYF\xe7\xcc\xce\xe1\x06\x94\xe3Һ\xd8\x1afs\x9eW\xcdTj\x9f\xa9GQ\xd3\x0f\xef\x14\xd0\xe2UH\xeb~\x81\x92\x9a\xe7\xf9\x88\xfeZ\x8b\xc0\xb3\x91W\xe7\x15\xc8\xdd\xf9\x9a\xe9\xed\xcb\xdf\xe6\xb2\xcfJ=o \xcd\xfaF\xa9\"䇔\xaa\xa6\xb4\xd4y\xf5\xb7f\xa1\xd2\xf6\xf2\xee4H\xde\xf4\xcb\xf4W\xb3\x8c\xe1*\x85j\x05,63|w\x11\x9eh`s\x98\x02\xfe'\x00\x00\xff\xff\xef\xf8\xa6>\x10\f\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVM\x8f\xdb6\x10\xbd\xfbW\f\x92kd7(Z\x14\xbe\x05\xdb\x1e\x82&\xc5\"N\xf7N\x93#{j\x8ad\x87C9.\xfa\xe3\v\x92Ү-\xcb\xc9nQT\x17\xc3\xe4\xf0q>\u07bca\xd34\v\x15\xe8\x019\x92wkP\x81\xf0\x8b\xa0\xcb\xff\xe2\xf2\xf0S\\\x92_\xf5o\x17\arf\rw)\x8a\xef>a\xf4\x895\xfe\x8c-9\x12\xf2nѡ(\xa3D\xad\x17\x00\xca9/*/\xc7\xfc\x17@{'\xec\xadEnv薇\xb4\xc5m\"k\x90\v\xf8xu\xff\xdd\xf2\xed\x8f\xcb\x1f\x16\x00Nu\xb8\x86\xde\xdb\xd4at*Ľ\x17\xebu\xc5\\\xf6h\x91\xfd\x92\xfc\"\x06\xd4\xf9\x8a\x1d\xfb\x14\xd6\xf0\xb4Q!\x86\xeb\xab\xeb\x0f\x05m3\xa0}\x18Њ\x81\xa5(\xbf~\xc5\xe8\x03E)\x86\xc1&V\xf6\xa6g\xc5&\xee=\xcboO\xb77\xd0G[w\xc8\xed\x92U|\xeb\xfc\x02 j\x1fp\r\xe5xP\x1a\xcd\x02`\xc8O\x81k\xc6Լ\xad\x88z\x8f\x9d\xaa\xf7\x00\xf8\x80\xee\xdd\xfd\xfb\x87\xef7\x17\xcb\x00\x06\xa3f\nR\xb2<\x1f\"P\x04\x05\xa3'p\xdc##<\x94|B\x14\xcf\x18\a\xa7\x1fA\x01F\xff\xe3\xf2q1\xb0\x0f\xc8Bc\xf0\xf5;\xe3\xd7\xd9\xeaį\xbf\x9b\x8b=\x80\x1cJ=\x05&\x13\r#\xc8\x1e\xc7t\xa0\x19\xa2\a߂\xec)\x02c`\x8c\xe8*\xf5\xf2\xb2r\xe0\xb7\x7f\xa0\x96\xe5\x04z\x83\x9car\xad\x925\x99\x9f=\xb2\x00\xa3\xf6;G\x7f=bG\x10_.\xb5J0\n\x90\x13d\xa7,\xf4\xca&|\x03ʙ\tr\xa7N\xc0\x98\xef\x84\xe4\xce\xf0ʁ8\xf5\xe3\xa3g\x04r\xad_\xc3^$\xc4\xf5j\xb5#\x19\xbbN\xfb\xaeK\x8e\xe4\xb4*\rD\xdb$\x9e\xe3\xca`\x8fv\x15i\xd7(\xd6{\x12Ԓ\x18W*PS\x02q\xb5K:\xf3\x9a\x87>\x8d\x17\xd7\xca)S,\n\x93\u06ddm\x94.yAyr\xc3T\xd6T\xa8\x1a\xe2S\x15\xf2RNݧ_6\x9fa\xf4\xa4V\xaa\x16\xe5\xc9\xf4*/c}r6ɵ\xc8\xf5\\˾+\x98\xe8L\xf0\xe4\xa4\xfcі\xd0\tĴ\xedH2\r\xfeL\x18%\x97n\n{W\x94\t\xb6\b)\x18%h\xa6\x06\xef\x1dܩ\x0e흊\xf8?\xd7*W%6\xb9\bϪֹ\xdeN\x8dkz\xcf\x1bu\x90\xc9\x1b\xa5\x9dW\x84M@}\xd1x\x19\x85Z\x1a\x14\xa2\xf5i\x8b\x15\x10|;ý\x17\xb9\x9c?t\xa9\x9b#\xe2\xbb^\x91U[{-\t\r\xfc\xee\xd4\xcdݛş\xad\xe7\xd5b̏=\xb3\x06\xe1T\xb1\a\x96\r+\xff\x04\x00\x00\xff\xffNy\xc1Q\xa1\x0e\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcWM\x8f\xdb8\x12\xbd\xfbW\x14\x92k$o\xb0\xd8\xc5·F\xef\x1c\x82I\x06\x8d\xb8\xa7\xef4Y\xb2\x19S\xa4\xa6\xaa(\xc7\xf3\xf1\xdf\a$%\xb7-\xcb\xe9\xf4`0\xbat\x8b\"\x1f\xeb\xe3իrUU\v\xd5\xd9'$\xb6\xc1\xaf@u\x16\xbf\n\xfa\xf4\xc6\xf5\xfe\x7f\\۰\xec\xdf/\xf6֛\x15\xdcG\x96\xd0~F\x0e\x914\xfe\x1f\x1b\xeb\xad\xd8\xe0\x17-\x8a2J\xd4j\x01\xa0\xbc\x0f\xa2\xd22\xa7W\x00\x1d\xbcPp\x0e\xa9ڢ\xaf\xf7q\x83\x9bh\x9dA\xca\xe0\xe3\xd5\xfd\xbf\xea\xf7\xff\xad\xff\xb3\x00\xf0\xaa\xc5\x15\xf4\xc1\xc5\x16٫\x8ewA\\\xd0\x05\xb3\xee\xd1!\x85چ\x05w\xa8\xd3\x15[\n\xb1[\xc1\xf3\x87\x021\\_L\x7f\xcah\xeb\x01\xed〖78\xcb\xf2\xe376}\xb4,yc\xe7\")wӲ\xbc\x87w\x81\xe4\xa7\xe7\xdb+\xe8ٕ/\xd6o\xa3St\xeb\xfc\x02\x80u\xe8p\x05\xf9x\xa74\x9a\x05\xc0\x10\x9f\fW\x812&G\\\xb9\a\xb2^\x90\xee\x13\x96?]f\x905\xd9NrD\x1f(\xf4\xd6 \x81e\x90\x1dB7\xbe\x87&\xbf\x17;\x80%\x90\xdabF\x00\xf8\xc2\xc1?(٭\xa0N\xf1\xad\xc7C\xc3璛\x87\xcbE9&\xb3Y\xc8\xfa\xed\x9c!%\xae0\x06\x16\xc6\xc8\x02\x8b\x92\xc8\xc0Q\xef@1\xdc\xf5\xca:\xb5q\xb8\xfc٫\xf1\xff\x19\xbb\xf2\xa9\xba\xdb)\xc6K\xb3\xceVfl:\x83\x18\t[k\xc2lʣm\x91E\xb5\xdd\x05\xe0\xdd\xf6\x12\xce()\v\x03Eߗ\xcc\xea\x1d\xb6j5\xec\f\x1d\xfa\xbb\x87\x0fO\xff^_,\xc3\\H\xa6TK\x99R02\x02\x0e;$\x84\xa7\xcc\xeb\x9c&\xe4!i'P\x80\x91G\\\x9f\x16;\n\x1d\x92ؑ\x84\xe59\xab\xf3\xb3Չ]\xbfW\x17\xdf\x00\x92+\xe5\x14\x98T\xf0X\xb84\xd0\x12\xcd\xe0}\xe1\x94e \xec\b\x19}\x91\x80\xb4\xac<\x84\xcd\x17\xd4RO\xa0\xd7H\t&\xd5Lt&\xe9D\x8f$@\xa8\xc3\xd6\xdb_O\xd8\f\x12\xf2\xa5N\t\xb2@&\xbeW\x0ez\xe5\"\xbe\x03\xe5\xcd\x04\xb9UG LwB\xf4gx\xf9\x00O\xed\xf8\x14\b\xc1\xfa&\xac`'\xd2\xf1j\xb9\xdcZ\x19\xd5O\x87\xb6\x8d\xde\xcaq\x99\x85\xccn\xa2\x04\xe2\xa5\xc1\x1eݒ\xed\xb6R\xa4wVPK$\\\xaa\xceV\xd9\x11_Ԫ5oi\xd0K\xbe\xb8\xf6\x8a\x9f\xe5\xc9j\xf5\x8a\xf4$\xe1*\xac)P\xc5\xc5\xe7,\xa4\xa5\x14\xba\xcf?\xac\x1fa\xb4\xa4d\xaa$\xe5y\xebU\\\xc6\xfc\xa4hZ\xdf \x95s\r\x856c\xa27]\xb0^\xf2\x8bv\x16\xbd\x00\xc7Mk%\xd1\xe0\x97\x88,)uS\xd8\xfb\xdc!`\x83\x10\xbbTPf\xbaჇ{բ\xbbW\x8c\xffp\xaeRV\xb8JI\xf8\xael\x9d\xf7\xbd\xe9\xe6\x12\xde\xf3B\x1d\xdaՍ\xd4\xce+ºC}Qx\t\xc56vP\x88&\xd0$@jԋy\xbc\xcbx\xce\v\x05\x94\xa6\xdd\xd8\xedt\x15.\x1aЭ\xb3\xdf\b،\xdf\xf7\xf9\xa6\xc4\xe1&ЩGU\xa3\x9f\x83%\x91\x06\x87-:s\xc5ԛ1Ϯ\x10\x9a\x94b\xe5\xae\r\xbd\xb4\xe4\xb41\xcf,\xca\xfa\x12\xf2g\x80\xcc\x7fC\xfe%1\xa6\xc1\xd9\x06\xf5Q;,\x80\x10\x9a\x19\xee\xbd\xca\xe4\xf4\xa0\x8f\xed\x1c\x11\xef&?|ο]\xff,\x9a\xa6m&\xf9\xb3\xf9\xbcZ\xe44\xec\x99\x15\bł=\xb0\xec|%nN\xb3\xec\n~\xfbc\xf1g\x00\x00\x00\xff\xff+\xf2\xd32>\x10\x00\x00"), } var CRDs = crds() diff --git a/pkg/apis/velero/v1/volume_snapshot_location_type.go b/pkg/apis/velero/v1/volume_snapshot_location_type.go index 836701b77..1ff363a46 100644 --- a/pkg/apis/velero/v1/volume_snapshot_location_type.go +++ b/pkg/apis/velero/v1/volume_snapshot_location_type.go @@ -27,6 +27,9 @@ import ( // +kubebuilder:resource:shortName=vsl // +kubebuilder:object:generate=true // +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="Provider",type="string",JSONPath=".spec.provider",description="Provider is the provider of the volume storage" +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Volume Snapshot Location status such as Available/Unavailable" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // VolumeSnapshotLocation is a location where Velero stores volume snapshots. type VolumeSnapshotLocation struct {