From 302bfbaaac98fd889aefbabe0573946fe2b10b44 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 15 Jun 2026 12:16:23 -0700 Subject: [PATCH 001/194] Add design for server default restore resource modifier Introduces a --default-resource-modifier-configmap server flag that references a ConfigMap with resource modifier rules applied automatically to all restores. This eliminates per-restore configuration for common transformations like stripping stale CNI annotations (OVN-Kubernetes, Multus) that can break workloads after restore. Key design decisions: - Exclusive precedence: per-restore modifiers fully replace the default - Non-fatal default errors: misconfigured default does not break restores - Opt-out via SkipDefaultResourceModifier field in RestoreSpec - Ships with a curated example ConfigMap for CNI annotation stripping Signed-off-by: Shubham Pampattiwar --- design/default-resource-modifier_design.md | 400 +++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 design/default-resource-modifier_design.md diff --git a/design/default-resource-modifier_design.md b/design/default-resource-modifier_design.md new file mode 100644 index 000000000..0a7bffef7 --- /dev/null +++ b/design/default-resource-modifier_design.md @@ -0,0 +1,400 @@ +# Server Default Restore Resource Modifier + +- [Server Default Restore Resource Modifier](#server-default-restore-resource-modifier) + - [Abstract](#abstract) + - [Background](#background) + - [Goals](#goals) + - [Non Goals](#non-goals) + - [High-Level Design](#high-level-design) + - [Detailed Design](#detailed-design) + - [Server Configuration](#server-configuration) + - [Restore API Change](#restore-api-change) + - [Controller Logic](#controller-logic) + - [Restore CLI](#restore-cli) + - [Install Path](#install-path) + - [Curated Default ConfigMap Example](#curated-default-configmap-example) + - [Alternatives Considered](#alternatives-considered) + - [Security Considerations](#security-considerations) + - [Compatibility](#compatibility) + - [Implementation](#implementation) + - [Open Issues](#open-issues) + +## Abstract + +This proposal introduces a server-level default restore resource modifier for Velero. +A new `--default-resource-modifier-configmap` flag on the Velero server references a ConfigMap containing resource modifier rules that apply automatically to every restore, eliminating the need for per-restore configuration for common transformations like stripping stale CNI annotations. + +## Background + +When pods are backed up, CNI-managed annotations may be present that carry pod-specific networking state such as IP addresses, MAC addresses, and routes. +Restoring these stale values can cause networking failures because the CNI expects to inject fresh values and the restored annotations may conflict with the new cluster's network state. + +The following annotations are commonly affected: + +| Annotation | CNI | +|---|---| +| `k8s.ovn.org/pod-networks` | OVN-Kubernetes | +| `k8s.v1.cni.cncf.io/network-status` | Multus | +| `k8s.v1.cni.cncf.io/networks-status` | Multus | + +Today, users can strip these annotations using [Resource Modifiers](https://velero.io/docs/main/restore-resource-modifiers/), but this requires authoring a ConfigMap and referencing it on every restore via `--resource-modifier-configmap`. +This is not discoverable for users unfamiliar with the feature and adds friction for a problem that affects most OpenShift and multi-CNI deployments. + +Velero already strips certain annotations during restore as built-in behavior (e.g., `volume.kubernetes.io/selected-node` from PVCs). +This proposal extends that concept by allowing administrators to configure a default set of resource modifier rules at the server level. + +## Goals + +- Allow Velero administrators to configure a default resource modifier ConfigMap that applies to all restores without per-restore configuration. +- Provide a mechanism for individual restores to opt out of the default modifier. +- Ship a documented example ConfigMap that strips well-known CNI annotations. + +## Non Goals + +- Auto-creating a default ConfigMap during `velero install`. The mechanism is opt-in; administrators create and configure the ConfigMap. +- Merging default and per-restore resource modifier rules. When a per-restore modifier is specified, it takes exclusive precedence over the default. +- Supporting non-ConfigMap sources for default modifiers (e.g., CRDs, inline rules). +- Stripping CNI annotations via a built-in RestoreItemAction plugin. The resource modifier mechanism is the right abstraction for this. + +## High-Level Design + +A new `--default-resource-modifier-configmap` server flag references a ConfigMap name in the Velero namespace. +During restore, if no per-restore resource modifier is specified, the server loads and applies the default ConfigMap's rules. +When a per-restore modifier is specified via `--resource-modifier-configmap`, it takes exclusive precedence and the default is not applied. +A new `--skip-default-resource-modifier` flag on `velero restore create` allows opting out of the default per-restore. + +This follows the existing pattern used by `--backup-repository-configmap` and `--repo-maintenance-job-configmap`. + +## Detailed Design + +### Server Configuration + +Add a new field to the server `Config` struct and bind it as a CLI flag. + +In `pkg/cmd/server/config/config.go`: + +```go +type Config struct { + // ... existing fields ... + DefaultResourceModifierConfigMap string +} +``` + +```go +func (c *Config) BindFlags(flags *pflag.FlagSet) { + // ... existing flags ... + flags.StringVar( + &c.DefaultResourceModifierConfigMap, + "default-resource-modifier-configmap", + c.DefaultResourceModifierConfigMap, + "The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores. "+ + "Ignored when a per-restore resource modifier is specified.", + ) +} +``` + +The default value is an empty string, meaning no default modifier is configured. +No change to `GetDefaultConfig()` is needed. + +### Restore API Change + +Add a new field to `RestoreSpec` for opting out of the default modifier. + +In `pkg/apis/velero/v1/restore_types.go`: + +```go +type RestoreSpec struct { + // ... existing fields ... + + // SkipDefaultResourceModifier controls whether the server-configured default + // resource modifier is applied to this restore. + // When true, the default modifier is skipped even if configured on the server. + // Has no effect when a per-restore ResourceModifier is specified. + // +optional + SkipDefaultResourceModifier bool `json:"skipDefaultResourceModifier,omitempty"` +} +``` + + +### Controller Logic + +Thread the new config value through to the restore controller and implement the precedence logic. + +In `pkg/controller/restore_controller.go`, add a field to `restoreReconciler`: + +```go +type restoreReconciler struct { + // ... existing fields ... + defaultResourceModifierConfigMap string +} +``` + +Update `NewRestoreReconciler` to accept and store the new parameter. + +In `pkg/cmd/server/server.go`, pass `s.config.DefaultResourceModifierConfigMap` to `NewRestoreReconciler`. + +Refactor `validateAndComplete` to use a shared helper for ConfigMap loading and implement the precedence logic: + +```go +func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInfo, *resourcemodifiers.ResourceModifiers) { + // ... existing validation logic (unchanged) ... + + // Resource modifier resolution: per-restore takes exclusive precedence over default. + var resourceModifiers *resourcemodifiers.ResourceModifiers + + if restore.Spec.ResourceModifier != nil && + strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) { + // Per-restore modifier specified: use it exclusively, ignore default. + resourceModifiers = r.loadResourceModifierConfigMap( + restore, restore.Spec.ResourceModifier.Name, false, + ) + } else if r.defaultResourceModifierConfigMap != "" && !restore.Spec.SkipDefaultResourceModifier { + // No per-restore modifier: apply server default if configured and not skipped. + resourceModifiers = r.loadResourceModifierConfigMap( + restore, r.defaultResourceModifierConfigMap, true, + ) + } + + return info, resourceModifiers +} +``` + +Extract the ConfigMap loading into a helper to avoid code duplication: + +```go +// loadResourceModifierConfigMap loads and validates a resource modifier ConfigMap. +// When isDefault is true, errors are non-fatal (logged as warnings, returns nil). +// When isDefault is false, errors are added to restore.Status.ValidationErrors. +func (r *restoreReconciler) loadResourceModifierConfigMap( + restore *api.Restore, cmName string, isDefault bool, +) *resourcemodifiers.ResourceModifiers { + cm := &corev1api.ConfigMap{} + if err := r.kbClient.Get( + context.Background(), + client.ObjectKey{Namespace: restore.Namespace, Name: cmName}, + cm, + ); err != nil { + if isDefault { + r.logger.WithError(err).Warnf( + "Default resource modifier configmap %s/%s not found, skipping", + restore.Namespace, cmName, + ) + return nil + } + restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, + fmt.Sprintf("failed to get resource modifiers configmap %s/%s", restore.Namespace, cmName)) + return nil + } + + modifiers, err := resourcemodifiers.GetResourceModifiersFromConfig(cm) + if err != nil { + if isDefault { + r.logger.WithError(err).Warnf( + "Error parsing default resource modifier configmap %s/%s, skipping", + restore.Namespace, cmName, + ) + return nil + } + restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, + errors.Wrapf(err, "Error in parsing resource modifiers provided in configmap %s/%s", + restore.Namespace, cmName).Error()) + return nil + } + + if err = modifiers.Validate(); err != nil { + if isDefault { + r.logger.WithError(err).Warnf( + "Validation error in default resource modifier configmap %s/%s, skipping", + restore.Namespace, cmName, + ) + return nil + } + restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, + errors.Wrapf(err, "Validation error in resource modifiers provided in configmap %s/%s", + restore.Namespace, cmName).Error()) + return nil + } + + source := "per-restore" + if isDefault { + source = "default" + } + r.logger.Infof("Retrieved %s resource modifiers from configmap %s/%s", source, restore.Namespace, cmName) + return modifiers +} +``` + +Key design decisions in this logic: + +1. **Exclusive precedence**: When a per-restore modifier is specified, the default is not applied at all. +This is the simplest mental model and avoids complex merge semantics. +Users who want both default and custom rules can copy the default rules into their per-restore ConfigMap. + +2. **Non-fatal default errors**: If the default ConfigMap is missing or invalid, log a warning and proceed without it. +A misconfigured default should not break all restores cluster-wide. +Per-restore modifier errors remain fatal (validation errors), preserving current behavior. + +3. **SkipDefaultResourceModifier**: Allows opting out per-restore without specifying a per-restore modifier. +Has no effect when a per-restore modifier is specified (it already takes precedence). + +### Restore CLI + +Add a `--skip-default-resource-modifier` flag to `velero restore create`. + +In `pkg/cmd/cli/restore/create.go`: + +```go +type CreateOptions struct { + // ... existing fields ... + SkipDefaultResourceModifier bool +} +``` + +```go +func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { + // ... existing flags ... + flags.BoolVar(&o.SkipDefaultResourceModifier, "skip-default-resource-modifier", false, + "Skip applying the server-configured default resource modifier for this restore") +} +``` + +Set the field on the RestoreSpec when building the Restore object: + +```go +Spec: api.RestoreSpec{ + // ... existing fields ... + SkipDefaultResourceModifier: o.SkipDefaultResourceModifier, +} +``` + +Update the restore describer in `pkg/cmd/util/output/restore_describer.go` to display the field when set. + +### Install Path + +Add the flag to the install CLI and deployment builder so administrators can configure it during installation. + +In `pkg/install/deployment.go`, add a `defaultResourceModifierConfigMap` field to `podTemplateConfig` with an option function: + +```go +func WithDefaultResourceModifierConfigMap(name string) podTemplateOption { + return func(c *podTemplateConfig) { + c.defaultResourceModifierConfigMap = name + } +} +``` + +In the `Deployment()` function, append the CLI arg: + +```go +if len(c.defaultResourceModifierConfigMap) > 0 { + args = append(args, fmt.Sprintf("--default-resource-modifier-configmap=%s", + c.defaultResourceModifierConfigMap)) +} +``` + +Wire it through `VeleroOptions` in `pkg/install/resources.go` and the install CLI in `pkg/cmd/cli/install/install.go`. + +Add a builder method to `pkg/builder/restore_builder.go`: + +```go +func (b *RestoreBuilder) SkipDefaultResourceModifier(val bool) *RestoreBuilder { + b.object.Spec.SkipDefaultResourceModifier = val + return b +} +``` + +### Curated Default ConfigMap Example + +Provide a ready-to-use ConfigMap in `examples/default-resource-modifier-cni.yaml` that strips well-known CNI annotations: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: default-restore-resource-modifiers + namespace: velero +data: + resource-modifiers.yaml: | + version: v1 + resourceModifierRules: + - conditions: + groupResource: pods + mergePatches: + - patchData: | + metadata: + annotations: + k8s.ovn.org/pod-networks: null + k8s.v1.cni.cncf.io/network-status: null + k8s.v1.cni.cncf.io/networks-status: null +``` + +This uses JSON Merge Patch to remove annotations by setting them to `null`. +Administrators can extend this ConfigMap with additional CNI-specific annotations (Calico, Cilium, etc.) or other stale metadata as needed. + +Usage: +```bash +# Create the ConfigMap +kubectl apply -f examples/default-resource-modifier-cni.yaml + +# Configure the Velero server to use it +# Option 1: During install +velero install --default-resource-modifier-configmap=default-restore-resource-modifiers ... + +# Option 2: Edit existing deployment +kubectl -n velero edit deploy velero +# Add: --default-resource-modifier-configmap=default-restore-resource-modifiers +``` + +## Alternatives Considered + +**Merge default and per-restore rules**: Instead of exclusive precedence, concatenate default and per-restore rules so both apply. +This avoids users having to copy default rules when specifying per-restore modifiers. +However, it introduces complexity around rule ordering and makes it harder to reason about what transformations will be applied. +It also makes it impossible to fully override the default for a specific restore without the `SkipDefaultResourceModifier` flag. +Exclusive precedence was chosen for simplicity. +Merge semantics can be revisited in a future enhancement if user demand warrants it. + +**Built-in RestoreItemAction plugin**: Implement CNI annotation stripping as a built-in RIA plugin rather than using the resource modifier mechanism. +This would hard-code the logic and make it less configurable. +The resource modifier mechanism already supports this use case and is more flexible. + +**Validate default ConfigMap at server startup**: Validate the ConfigMap when the server starts rather than at restore time. +Rejected because the ConfigMap may be created after the server starts and should not require a server restart to take effect. + +**Auto-create default ConfigMap during install**: Have `velero install` automatically create the CNI-stripping ConfigMap. +Rejected for the initial release to minimize the change surface and let administrators opt in. +Can be added later as a default behavior or install flag. + +## Security Considerations + +No new security surface. +The default ConfigMap resides in the Velero namespace and is subject to the same RBAC controls as existing resource modifier ConfigMaps. +Only users with access to create/edit ConfigMaps in the Velero namespace can modify the default modifier rules. + +## Compatibility + +Fully backward compatible. +When `--default-resource-modifier-configmap` is not set (the default), behavior is identical to current Velero. +No changes to existing per-restore resource modifier behavior. +The new `SkipDefaultResourceModifier` field in RestoreSpec defaults to `false` and has no effect when no default modifier is configured. + +## Implementation + +1. Add `DefaultResourceModifierConfigMap` to `Config` struct and bind the CLI flag. +2. Add `SkipDefaultResourceModifier` to `RestoreSpec` and regenerate deepcopy/CRD. +3. Thread the config to `restoreReconciler` via `NewRestoreReconciler`. +4. Refactor `validateAndComplete` with `loadResourceModifierConfigMap` helper. +5. Add `--skip-default-resource-modifier` to the restore CLI. +6. Wire through the install path (deployment builder, install CLI). +7. Add unit tests for all precedence and error scenarios. +8. Create the example ConfigMap. +9. Update user documentation. +10. Add E2E test for default resource modifier. + + +## Open Issues + +- Should additional CNI annotations (Calico, Cilium) be included in the curated example ConfigMap? +Feedback from the community on which annotations are commonly problematic would be helpful. +- Should `velero restore describe` show which resource modifier was used (default vs per-restore)? +This would improve observability but is a minor enhancement that can be added separately. From 89dc9b06b26b9bb62e703963c2d9177ddff98f4a Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 15 Jun 2026 12:20:13 -0700 Subject: [PATCH 002/194] Add changelog for PR #9921 Signed-off-by: Shubham Pampattiwar --- changelogs/unreleased/9921-shubham-pampattiwar | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/9921-shubham-pampattiwar diff --git a/changelogs/unreleased/9921-shubham-pampattiwar b/changelogs/unreleased/9921-shubham-pampattiwar new file mode 100644 index 000000000..6475cfe58 --- /dev/null +++ b/changelogs/unreleased/9921-shubham-pampattiwar @@ -0,0 +1 @@ +Design: Server default restore resource modifier From eda35227bb409b6d34ae21635263df60285bb51d Mon Sep 17 00:00:00 2001 From: Lubron Zhan Date: Wed, 24 Jun 2026 11:08:16 -0700 Subject: [PATCH 003/194] Make pkg/apis its own Go module Extract pkg/apis into a standalone Go module with its own go.mod/go.sum, and wire it back into the main module via a local replace directive. Signed-off-by: Lubron Zhan Co-Authored-By: Claude Sonnet 4.6 --- changelogs/unreleased/9943-lubronzhan | 1 + go.mod | 6 ++- pkg/apis/go.mod | 28 +++++++++++ pkg/apis/go.sum | 68 +++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/9943-lubronzhan create mode 100644 pkg/apis/go.mod create mode 100644 pkg/apis/go.sum diff --git a/changelogs/unreleased/9943-lubronzhan b/changelogs/unreleased/9943-lubronzhan new file mode 100644 index 000000000..3c63bffdd --- /dev/null +++ b/changelogs/unreleased/9943-lubronzhan @@ -0,0 +1 @@ +Extract pkg/apis into its own Go module with a local replace directive in the root go.mod \ No newline at end of file diff --git a/go.mod b/go.mod index fc19be69f..a2c41faf6 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/vmware-tanzu/crash-diagnostics v0.4.3 + github.com/vmware-tanzu/velero/pkg/apis v0.0.0 go.uber.org/zap v1.28.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/mod v0.36.0 @@ -220,4 +221,7 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) -replace github.com/kopia/kopia => github.com/project-velero/kopia v0.0.0-20260616052725-d83462d382c9 +replace ( + github.com/kopia/kopia => github.com/project-velero/kopia v0.0.0-20260616052725-d83462d382c9 + github.com/vmware-tanzu/velero/pkg/apis => ./pkg/apis +) diff --git a/pkg/apis/go.mod b/pkg/apis/go.mod new file mode 100644 index 000000000..364a1129f --- /dev/null +++ b/pkg/apis/go.mod @@ -0,0 +1,28 @@ +module github.com/vmware-tanzu/velero/pkg/apis + +go 1.26.0 + +require ( + k8s.io/api v0.36.0 + k8s.io/apimachinery v0.36.0 +) + +require ( + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/text v0.33.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect +) diff --git a/pkg/apis/go.sum b/pkg/apis/go.sum new file mode 100644 index 000000000..f679a531e --- /dev/null +++ b/pkg/apis/go.sum @@ -0,0 +1,68 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= +k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 2e5ef987d5ff3162a848bf5e037ea9b559e64419 Mon Sep 17 00:00:00 2001 From: Lubron Zhan Date: Wed, 24 Jun 2026 11:29:36 -0700 Subject: [PATCH 004/194] ci: retrigger CI for flaky test Signed-off-by: Lubron Zhan From f6243627fca513679bb541042f6eb35aef394d5b Mon Sep 17 00:00:00 2001 From: Lubron Zhan Date: Wed, 24 Jun 2026 17:19:20 -0700 Subject: [PATCH 005/194] fix: make TestWaitExecHandleHooks deterministic for 2-container hook ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a flaky test in TestWaitExecHandleHooks: "should return no error with 2 spec hooks in 2 different containers, 1st container starts running after 10ms, 2nd container after 20ms, both succeed" Observed failure (CI run https://github.com/velero-io/velero/actions/runs/28119573625/job/83267758421): mock: Unexpected Method Call ExecutePodCommand called with resourceVersion:"3" (both containers running), but mock was registered expecting resourceVersion:"2" (container1 running, container2 still waiting). Root cause: the two source.Modify calls were 10ms apart. The informer's DeltaFIFO queue can coalesce rapid updates, delivering only the latest pod state (resourceVersion:3) to the handler before the hook for container1 fires. The comment "each of these states will be seen by the UpdateFunc handler" was incorrect — intermediate states can be silently skipped under load. Fix: add waitForSignal chan struct{} to the change struct and onCalled func() to the expectedExecution struct. The goroutine now blocks after sending the first change until the mock signals that the hook has fired (via close), then sends the second change. This guarantees the handler observes the intermediate pod state (resourceVersion:2) when executing container1's hook. Signed-off-by: Lubron Zhan Co-Authored-By: Claude Sonnet 4.6 --- changelogs/unreleased/9944-lubronzhan | 1 + internal/hook/wait_exec_hook_handler_test.go | 32 ++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/9944-lubronzhan diff --git a/changelogs/unreleased/9944-lubronzhan b/changelogs/unreleased/9944-lubronzhan new file mode 100644 index 000000000..25fb1f376 --- /dev/null +++ b/changelogs/unreleased/9944-lubronzhan @@ -0,0 +1 @@ +Fix flaky TestWaitExecHandleHooks test for 2-container hook ordering by synchronizing pod state changes with hook execution using channels \ No newline at end of file diff --git a/internal/hook/wait_exec_hook_handler_test.go b/internal/hook/wait_exec_hook_handler_test.go index bb0a7c8b1..0bcfafb61 100644 --- a/internal/hook/wait_exec_hook_handler_test.go +++ b/internal/hook/wait_exec_hook_handler_test.go @@ -53,13 +53,25 @@ func TestWaitExecHandleHooks(t *testing.T) { // delta to wait since last change applied or pod added wait time.Duration updated *corev1api.Pod + // waitForSignal, if set, blocks the goroutine after applying this change + // until the channel is closed. Use this to ensure the handler processes + // an intermediate pod state before the next change is applied. + waitForSignal chan struct{} } type expectedExecution struct { hook *velerov1api.ExecHook name string error error pod *corev1api.Pod + // onCalled, if set, is invoked by the mock when ExecutePodCommand is called. + // Use this together with change.waitForSignal to synchronize state transitions. + onCalled func() } + // hookFired is used by the test case that has two containers with hooks in + // different containers. It ensures the second pod state change is only sent + // after the first hook has fired, preventing the informer from coalescing + // both updates and skipping the intermediate state. + hookFired := make(chan struct{}) tests := []struct { name string // Used as argument to HandleHooks and first state added to ListerWatcher @@ -622,6 +634,8 @@ func TestWaitExecHandleHooks(t *testing.T) { }, }). Result(), + // Signal after this hook fires so the goroutine can apply the next change. + onCalled: func() { close(hookFired) }, }, { name: "my-hook-1", @@ -678,6 +692,10 @@ func TestWaitExecHandleHooks(t *testing.T) { }, }). Result(), + // Block until the hook for container1 has fired before sending the + // next change. Without this, the informer may coalesce both updates + // and deliver only resourceVersion:3, skipping the intermediate state. + waitForSignal: hookFired, }, // 2nd modification: container2 starts running, resourceVersion 3 { @@ -838,11 +856,15 @@ func TestWaitExecHandleHooks(t *testing.T) { go func() { // This is the state of the pod that will be seen by the AddFunc handler. source.Add(test.initialPod) - // Changes holds the versions of the pod over time. Each of these states - // will be seen by the UpdateFunc handler. + // Changes holds the versions of the pod over time. The informer may + // coalesce rapid updates, so use waitForSignal when a test requires the + // handler to observe a specific intermediate state before the next change. for _, change := range test.changes { time.Sleep(change.wait) source.Modify(change.updated) + if change.waitForSignal != nil { + <-change.waitForSignal + } } }() @@ -857,7 +879,11 @@ func TestWaitExecHandleHooks(t *testing.T) { for _, e := range test.expectedExecutions { obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(e.pod) require.NoError(t, err) - podCommandExecutor.On("ExecutePodCommand", mock.Anything, obj, e.pod.Namespace, e.pod.Name, e.name, e.hook).Return(e.error) + call := podCommandExecutor.On("ExecutePodCommand", mock.Anything, obj, e.pod.Namespace, e.pod.Name, e.name, e.hook).Return(e.error) + if e.onCalled != nil { + onCalled := e.onCalled + call.Run(func(mock.Arguments) { onCalled() }) + } } ctx := t.Context() From 845bd2dc4f8a8b17aa0c1fc107008d11d7081bac Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 12 May 2026 17:57:31 +0800 Subject: [PATCH 006/194] block uploader snapshot implementation Signed-off-by: Lyndon-Li --- pkg/uploader/block/dev_linux.go | 30 ++ pkg/uploader/block/dev_other.go | 29 ++ pkg/uploader/block/snapshot.go | 266 ++++++++++++ pkg/uploader/block/snapshot_test.go | 625 ++++++++++++++++++++++++++++ pkg/uploader/block/uploader.go | 54 +++ pkg/uploader/provider/block.go | 83 +++- pkg/uploader/provider/block_test.go | 287 +++++++++++++ pkg/uploader/types.go | 7 +- 8 files changed, 1375 insertions(+), 6 deletions(-) create mode 100644 pkg/uploader/block/dev_linux.go create mode 100644 pkg/uploader/block/dev_other.go create mode 100644 pkg/uploader/block/snapshot.go create mode 100644 pkg/uploader/block/snapshot_test.go create mode 100644 pkg/uploader/block/uploader.go diff --git a/pkg/uploader/block/dev_linux.go b/pkg/uploader/block/dev_linux.go new file mode 100644 index 000000000..4d49442b3 --- /dev/null +++ b/pkg/uploader/block/dev_linux.go @@ -0,0 +1,30 @@ +//go:build linux +// +build linux + +/* +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 block + +import ( + "os" + + "github.com/pkg/errors" +) + +func openBlockDevice(path string, read bool) (*os.File, error) { + return nil, errors.New("Not implemented") +} diff --git a/pkg/uploader/block/dev_other.go b/pkg/uploader/block/dev_other.go new file mode 100644 index 000000000..60689a3d6 --- /dev/null +++ b/pkg/uploader/block/dev_other.go @@ -0,0 +1,29 @@ +//go:build !linux +// +build !linux + +/* +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 block + +import ( + "fmt" + "os" +) + +func openBlockDevice(_ string, _ bool) (*os.File, error) { + return nil, fmt.Errorf("block mode is not supported for Windows") +} diff --git a/pkg/uploader/block/snapshot.go b/pkg/uploader/block/snapshot.go new file mode 100644 index 000000000..272b6dd16 --- /dev/null +++ b/pkg/uploader/block/snapshot.go @@ -0,0 +1,266 @@ +/* +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 block + +import ( + "context" + "io" + "maps" + "path/filepath" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/vmware-tanzu/velero/pkg/cbtservice" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + "github.com/vmware-tanzu/velero/pkg/uploader" + "github.com/vmware-tanzu/velero/pkg/uploader/cbt" +) + +var openBlockDeviceFunc = openBlockDevice + +type parentBackupInfo struct { + parentObject udmrepo.ID + changeID string + volumeID string +} + +// Backup backup specific sourcePath and update progress +func Backup(ctx context.Context, blkup Uploader, repoWriter udmrepo.BackupRepo, sourcePath string, realSource string, cbtSource cbtservice.SourceInfo, + forceFull bool, parentSnapshot string, cbtservice cbtservice.Service, uploaderCfg map[string]string, tags map[string]string, log logrus.FieldLogger) (uploader.SnapshotInfo, bool, error) { + if blkup == nil { + return uploader.SnapshotInfo{}, false, errors.New("get empty block uploader") + } + + source, err := filepath.Abs(sourcePath) + if err != nil { + return uploader.SnapshotInfo{}, false, errors.Wrapf(err, "invalid source path %s", sourcePath) + } + + source = filepath.Clean(source) + + sourceInfo := sourceInfo{ + realSource: filepath.Clean(realSource), + } + + if realSource == "" { + sourceInfo.realSource = source + } + + sourceInfo.dev, err = openBlockDeviceFunc(source, true) + if err != nil { + return uploader.SnapshotInfo{}, false, errors.Wrapf(err, "error opening block device %s", source) + } + + sourceInfo.size, err = sourceInfo.dev.Seek(0, io.SeekEnd) + if err != nil { + return uploader.SnapshotInfo{}, false, errors.Wrapf(err, "error getting length of block device %s", source) + } + + _, err = sourceInfo.dev.Seek(0, io.SeekStart) + if err != nil { + return uploader.SnapshotInfo{}, false, errors.Wrapf(err, "error reset pos of block device %s", source) + } + + snapID, backupSize, err := snapshotSource(ctx, repoWriter, blkup, sourceInfo, forceFull, parentSnapshot, cbtSource, cbtservice, tags, uploaderCfg, log, "Block Uploader") + snapshotInfo := uploader.SnapshotInfo{ + ID: snapID, + Size: sourceInfo.size, + IncrementalSize: backupSize, + } + + return snapshotInfo, false, err +} + +func snapshotSource( + ctx context.Context, + rep udmrepo.BackupRepo, + u Uploader, + source sourceInfo, + forceFull bool, + parentSnapshot string, + cbtSource cbtservice.SourceInfo, + cbtservice cbtservice.Service, + snapshotTags map[string]string, + uploaderCfg map[string]string, + log logrus.FieldLogger, + description string, +) (string, int64, error) { + log.Info("Start to snapshot...") + snapshotStartTime := time.Now() + + parentBackup := getParentBackupInfo(ctx, rep, forceFull, parentSnapshot, cbtSource.VolumeID, source.realSource, snapshotTags, log) + + bitmap := cbt.NewBitmap(blockSize, uint64(source.size), cbtSource.Snapshot, parentBackup.changeID, parentBackup.volumeID) + + err := cbt.SetBitmapOrFull(ctx, cbtservice, bitmap) + if err != nil { + parentBackup.parentObject = "" + log.WithError(err).Warnf("Failed to create CBT with source %v, fallback to real full backup", cbtSource) + } + + snap, backupSize, err := u.Backup(source, parentBackup.parentObject, bitmap.Iterator(), uploaderCfg) + if err != nil { + return "", 0, errors.Wrapf(err, "Failed to run uploader backup for si %v", source) + } + + snap.Tags = make(map[string]string) + snap.Tags[uploader.CBTChangeIDTag] = cbtSource.ChangeID + snap.Tags[uploader.CBTVolumeIDTag] = cbtSource.VolumeID + if snapshotTags != nil { + maps.Copy(snap.Tags, snapshotTags) + } + + snap.Description = description + + snapID, err := rep.SaveSnapshot(ctx, snap) + if err != nil { + return "", 0, errors.Wrapf(err, "Failed to save snapshot %v", snap) + } + + if err = rep.Flush(ctx); err != nil { + return "", 0, errors.Wrapf(err, "Failed to flush repository") + } + + log.Infof("Created snapshot with root %v and ID %v in %v", snap.RootObject, snapID, time.Since(snapshotStartTime).Truncate(time.Second)) + + return string(snapID), backupSize, nil +} + +func getParentBackupInfo(ctx context.Context, rep udmrepo.BackupRepo, forceFull bool, parentSnapshot string, volumeID string, realSource string, snapshotTags map[string]string, log logrus.FieldLogger) parentBackupInfo { + var previous *udmrepo.Snapshot + if !forceFull { + if parentSnapshot != "" { + snap, err := rep.GetSnapshot(ctx, udmrepo.ID(parentSnapshot)) + if err != nil { + log.WithError(err).Warn("Failed to load previous snapshot, fallback to full backup") + } else { + previous = &snap + log.Infof("Using provided parent snapshot %s", parentSnapshot) + } + } else { + log.Infof("Searching for parent snapshot") + + snap, err := findPreviousSnapshot(ctx, rep, realSource, snapshotTags, nil, log) + if err != nil { + log.WithError(err).Warn("Failed to search previous snapshot, fallback to full backup") + } else { + previous = &snap + log.Infof("Using previous snapshot %s", snap.RootObject.ID) + } + } + } else { + log.Info("Forcing full snapshot") + } + + parentInfo := parentBackupInfo{} + if previous != nil { + if previous.Tags == nil { + log.Warnf("No tag from parent snapshot %s, fallback to full backup", parentSnapshot) + } else if previous.Tags[uploader.CBTChangeIDTag] == "" { + log.Warnf("No ChangeID tag from parent snapshot %s, fallback to full backup", parentSnapshot) + } else if previous.Tags[uploader.CBTVolumeIDTag] == "" { + log.Warnf("No VolumeID tag from parent snapshot %s, fallback to full backup", parentSnapshot) + } else if previous.Tags[uploader.CBTVolumeIDTag] != volumeID { + log.Warnf("VolumeID %s from parent snapshot %s is not expected as %s, fallback to full backup", previous.Tags[uploader.CBTVolumeIDTag], parentSnapshot, volumeID) + } else { + parentInfo.parentObject = previous.RootObject.ID + parentInfo.changeID = previous.Tags[uploader.CBTChangeIDTag] + parentInfo.volumeID = previous.Tags[uploader.CBTVolumeIDTag] + + log.Infof("Using parent snapshot %s, start time %v, end time %v, description %s", parentSnapshot, previous.StartTime, previous.EndTime, previous.Description) + } + } + + return parentInfo +} + +// Restore restore specific sourcePath with given snapshotID and update progress +func Restore(ctx context.Context, blkup Uploader, rep udmrepo.BackupRepo, snapshotID, dest string, uploaderCfg map[string]string, log logrus.FieldLogger) (int64, error) { + log.Info("Start to restore...") + + snapshot, err := rep.GetSnapshot(ctx, udmrepo.ID(snapshotID)) + if err != nil { + return 0, errors.Wrapf(err, "Unable to load snapshot %v", snapshotID) + } + + log.Infof("Restore from snapshot %s, description %s, created time %v, tags %v", snapshotID, snapshot.Description, snapshot.EndTime, snapshot.Tags) + + destPath, err := filepath.Abs(dest) + if err != nil { + return 0, errors.Wrapf(err, "invalid dest path '%s'", dest) + } + + destPath = filepath.Clean(destPath) + + destDev, err := openBlockDeviceFunc(destPath, false) + if err != nil { + return 0, errors.Wrapf(err, "error opening block device '%s'", destPath) + } + + size, err := blkup.Restore(snapshot, destInfo{dev: destDev, path: destPath}, uploaderCfg) + if err != nil { + return 0, errors.Wrapf(err, "error restoring to block dev %s", destPath) + } + + return size, nil +} + +func findPreviousSnapshot(ctx context.Context, rep udmrepo.BackupRepo, path string, snapshotTags map[string]string, noLaterThan *time.Time, log logrus.FieldLogger) (udmrepo.Snapshot, error) { + snaps, err := rep.ListSnapshot(ctx, path) + if err != nil { + return udmrepo.Snapshot{}, errors.Wrapf(err, "error list snapshots for %s", path) + } + + var previous *udmrepo.Snapshot + + for _, snap := range snaps { + log.Debugf("Found one snapshot %s, start time %v, tags %v", snap.RootObject.ID, snap.StartTime, snap.Tags) + + requester, found := snap.Tags[uploader.SnapshotRequesterTag] + if !found { + continue + } + + if requester != snapshotTags[uploader.SnapshotRequesterTag] { + continue + } + + uploaderName, found := snap.Tags[uploader.SnapshotUploaderTag] + if !found { + continue + } + + if uploaderName != uploader.BlockType { + continue + } + + if noLaterThan != nil && snap.StartTime.After(*noLaterThan) { + continue + } + + if previous == nil || snap.StartTime.After(previous.StartTime) { + previous = &snap + } + } + + if previous == nil { + return udmrepo.Snapshot{}, errors.Errorf("no matching snapshot found for source %s", path) + } + + return *previous, nil +} diff --git a/pkg/uploader/block/snapshot_test.go b/pkg/uploader/block/snapshot_test.go new file mode 100644 index 000000000..1e609eb2f --- /dev/null +++ b/pkg/uploader/block/snapshot_test.go @@ -0,0 +1,625 @@ +/* +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. +*/ + +// Tests live in package block (not block_test) so they can access unexported +// types sourceInfo and destInfo, which appear in the Uploader interface. +package block + +import ( + "context" + "os" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/vmware-tanzu/velero/pkg/cbtservice" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + udmrepomocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/mocks" + "github.com/vmware-tanzu/velero/pkg/uploader" + cbttypes "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types" +) + +type mockUploader struct { + mock.Mock +} + +func (m *mockUploader) Backup(src sourceInfo, parent udmrepo.ID, iter cbttypes.Iterator, cfg map[string]string) (udmrepo.Snapshot, int64, error) { + args := m.Called(src, parent, iter, cfg) + return args.Get(0).(udmrepo.Snapshot), args.Get(1).(int64), args.Error(2) +} + +func (m *mockUploader) Restore(snap udmrepo.Snapshot, dest destInfo, cfg map[string]string) (int64, error) { + args := m.Called(snap, dest, cfg) + return args.Get(0).(int64), args.Error(1) +} + +func testLog() logrus.FieldLogger { + l := logrus.New() + l.SetLevel(logrus.DebugLevel) + return l +} + +func tempFile(t *testing.T, content string) *os.File { + t.Helper() + f, err := os.CreateTemp("", "blktest-*") + require.NoError(t, err) + if content != "" { + _, err = f.WriteString(content) + require.NoError(t, err) + } + t.Cleanup(func() { + f.Close() + os.Remove(f.Name()) + }) + return f +} + +func TestBackup(t *testing.T) { + testCases := []struct { + name string + useNilBlkup bool + setupOpenDev func(t *testing.T) *os.File + setupMocks func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) + expectedErrStr string + checkInfo func(*testing.T, uploader.SnapshotInfo) + }{ + { + name: "nil uploader returns error", + useNilBlkup: true, + expectedErrStr: "get empty block uploader", + }, + { + name: "openBlockDevice error", + expectedErrStr: "error opening block device", + }, + { + name: "SnapshotSource error propagates", + setupOpenDev: func(t *testing.T) *os.File { + return tempFile(t, "") + }, + setupMocks: func(blkup *mockUploader, _ *udmrepomocks.BackupRepo) { + blkup.On("Backup", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(udmrepo.Snapshot{}, int64(0), errors.New("I/O error")) + }, + expectedErrStr: "Failed to run uploader backup", + }, + { + name: "success returns correct SnapshotInfo", + setupOpenDev: func(t *testing.T) *os.File { + return tempFile(t, "test-block-data") + }, + setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { + blkup.On("Backup", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(udmrepo.Snapshot{RootObject: udmrepo.ObjectMetadata{ID: "root"}}, int64(8), nil) + repo.On("SaveSnapshot", mock.Anything, mock.Anything).Return(udmrepo.ID("snap-001"), nil) + repo.On("Flush", mock.Anything).Return(nil) + }, + checkInfo: func(t *testing.T, info uploader.SnapshotInfo) { + assert.Equal(t, "snap-001", info.ID) + assert.Equal(t, int64(8), info.IncrementalSize) + assert.Greater(t, info.Size, int64(0)) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + mockBlkup := &mockUploader{} + mockRepo := udmrepomocks.NewBackupRepo(t) + + var blkup Uploader + if !tc.useNilBlkup { + blkup = mockBlkup + } + + if tc.setupOpenDev != nil { + f := tc.setupOpenDev(t) + openBlockDeviceFunc = func(_ string, _ bool) (*os.File, error) { + return f, nil + } + } else { + openBlockDeviceFunc = func(_ string, _ bool) (*os.File, error) { + return nil, errors.New("device not available") + } + } + + if tc.setupMocks != nil { + tc.setupMocks(mockBlkup, mockRepo) + } + + info, isEmpty, err := Backup( + ctx, blkup, mockRepo, + "/dev/sda", "", + cbtservice.SourceInfo{}, + true, "", nil, + map[string]string{}, map[string]string{}, + testLog(), + ) + + if tc.expectedErrStr != "" { + require.Error(t, err) + assert.ErrorContains(t, err, tc.expectedErrStr) + } else { + require.NoError(t, err) + assert.False(t, isEmpty) + } + + if tc.checkInfo != nil { + tc.checkInfo(t, info) + } + + mockBlkup.AssertExpectations(t) + }) + } +} + +func TestSnapshotSource(t *testing.T) { + baseSource := sourceInfo{realSource: "/test/vol", size: 1024} + + testCases := []struct { + name string + setupMocks func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) + expectedErrStr string + expectedSnapID string + expectedSize int64 + }{ + { + name: "uploader Backup error", + setupMocks: func(blkup *mockUploader, _ *udmrepomocks.BackupRepo) { + blkup.On("Backup", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(udmrepo.Snapshot{}, int64(0), errors.New("uploader error")) + }, + expectedErrStr: "Failed to run uploader backup", + }, + { + name: "SaveSnapshot error", + setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { + blkup.On("Backup", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(udmrepo.Snapshot{}, int64(0), nil) + repo.On("SaveSnapshot", mock.Anything, mock.Anything). + Return(udmrepo.ID(""), errors.New("save failed")) + }, + expectedErrStr: "Failed to save snapshot", + }, + { + name: "Flush error", + setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { + blkup.On("Backup", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(udmrepo.Snapshot{}, int64(0), nil) + repo.On("SaveSnapshot", mock.Anything, mock.Anything).Return(udmrepo.ID("snap-001"), nil) + repo.On("Flush", mock.Anything).Return(errors.New("flush failed")) + }, + expectedErrStr: "Failed to flush repository", + }, + { + name: "success with nil cbtService falls back to full bitmap", + setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { + blkup.On("Backup", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(udmrepo.Snapshot{RootObject: udmrepo.ObjectMetadata{ID: "root"}}, int64(512), nil) + repo.On("SaveSnapshot", mock.Anything, mock.Anything).Return(udmrepo.ID("snap-success"), nil) + repo.On("Flush", mock.Anything).Return(nil) + }, + expectedSnapID: "snap-success", + expectedSize: 512, + }, + { + name: "tags from cbtSource and snapshotTags are merged onto snapshot", + setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { + blkup.On("Backup", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(udmrepo.Snapshot{}, int64(0), nil) + repo.On("SaveSnapshot", mock.Anything, mock.MatchedBy(func(snap udmrepo.Snapshot) bool { + return snap.Tags[uploader.CBTChangeIDTag] == "cid-1" && + snap.Tags[uploader.CBTVolumeIDTag] == "vid-1" && + snap.Tags["custom"] == "val" && + snap.Description == "Block Uploader" + })).Return(udmrepo.ID("snap-tags"), nil) + repo.On("Flush", mock.Anything).Return(nil) + }, + expectedSnapID: "snap-tags", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + mockBlkup := &mockUploader{} + mockRepo := udmrepomocks.NewBackupRepo(t) + + tc.setupMocks(mockBlkup, mockRepo) + + cbtSrc := cbtservice.SourceInfo{ChangeID: "cid-1", VolumeID: "vid-1"} + snapshotTags := map[string]string{"custom": "val"} + + snapID, size, err := snapshotSource( + ctx, mockRepo, mockBlkup, + baseSource, + true, "", + cbtSrc, nil, + snapshotTags, map[string]string{}, + testLog(), "Block Uploader", + ) + + if tc.expectedErrStr != "" { + require.Error(t, err) + assert.ErrorContains(t, err, tc.expectedErrStr) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedSnapID, snapID) + assert.Equal(t, tc.expectedSize, size) + } + + mockBlkup.AssertExpectations(t) + }) + } +} + +func TestGetParentBackupInfo(t *testing.T) { + const volumeID = "vol-123" + const realSource = "/test/source" + + snapshotTags := map[string]string{ + uploader.SnapshotRequesterTag: "test-requester", + uploader.SnapshotUploaderTag: uploader.BlockType, + } + + validSnap := udmrepo.Snapshot{ + RootObject: udmrepo.ObjectMetadata{ID: "root-obj"}, + Tags: map[string]string{ + uploader.CBTChangeIDTag: "cid-abc", + uploader.CBTVolumeIDTag: volumeID, + uploader.SnapshotRequesterTag: "test-requester", + uploader.SnapshotUploaderTag: uploader.BlockType, + }, + } + + testCases := []struct { + name string + forceFull bool + parentSnapshot string + setupMocks func(repo *udmrepomocks.BackupRepo) + expectEmpty bool + expectedParent udmrepo.ID + expectedCID string + expectedVID string + }{ + { + name: "forceFull skips all parent lookup", + forceFull: true, + expectEmpty: true, + }, + { + name: "GetSnapshot fails — falls back to full", + parentSnapshot: "snap-parent", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-parent")). + Return(udmrepo.Snapshot{}, errors.New("not found")) + }, + expectEmpty: true, + }, + { + name: "parent snapshot has nil tags — falls back to full", + parentSnapshot: "snap-notags", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-notags")). + Return(udmrepo.Snapshot{Tags: nil}, nil) + }, + expectEmpty: true, + }, + { + name: "parent snapshot missing ChangeID tag — falls back to full", + parentSnapshot: "snap-nocid", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-nocid")). + Return(udmrepo.Snapshot{Tags: map[string]string{uploader.CBTVolumeIDTag: volumeID}}, nil) + }, + expectEmpty: true, + }, + { + name: "parent snapshot missing VolumeID tag — falls back to full", + parentSnapshot: "snap-novid", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-novid")). + Return(udmrepo.Snapshot{Tags: map[string]string{uploader.CBTChangeIDTag: "cid"}}, nil) + }, + expectEmpty: true, + }, + { + name: "parent snapshot VolumeID mismatch — falls back to full", + parentSnapshot: "snap-vidmismatch", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-vidmismatch")). + Return(udmrepo.Snapshot{Tags: map[string]string{ + uploader.CBTChangeIDTag: "cid", + uploader.CBTVolumeIDTag: "different-vol", + }}, nil) + }, + expectEmpty: true, + }, + { + name: "valid parent snapshot — returns parent info", + parentSnapshot: "snap-valid", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-valid")). + Return(validSnap, nil) + }, + expectedParent: "root-obj", + expectedCID: "cid-abc", + expectedVID: volumeID, + }, + { + name: "no parentSnapshot — ListSnapshot fails — falls back to full", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ListSnapshot", mock.Anything, realSource). + Return(nil, errors.New("list error")) + }, + expectEmpty: true, + }, + { + name: "no parentSnapshot — no matching snapshot — falls back to full", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ListSnapshot", mock.Anything, realSource). + Return([]udmrepo.Snapshot{{Tags: map[string]string{"other": "tag"}}}, nil) + }, + expectEmpty: true, + }, + { + name: "no parentSnapshot — matching snapshot found — returns parent info", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ListSnapshot", mock.Anything, realSource). + Return([]udmrepo.Snapshot{validSnap}, nil) + }, + expectedParent: "root-obj", + expectedCID: "cid-abc", + expectedVID: volumeID, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + mockRepo := udmrepomocks.NewBackupRepo(t) + + if tc.setupMocks != nil { + tc.setupMocks(mockRepo) + } + + info := getParentBackupInfo(ctx, mockRepo, tc.forceFull, tc.parentSnapshot, volumeID, realSource, snapshotTags, testLog()) + + if tc.expectEmpty { + assert.Empty(t, info.parentObject) + assert.Empty(t, info.changeID) + assert.Empty(t, info.volumeID) + } else { + assert.Equal(t, tc.expectedParent, info.parentObject) + assert.Equal(t, tc.expectedCID, info.changeID) + assert.Equal(t, tc.expectedVID, info.volumeID) + } + }) + } +} + +func TestFindPreviousSnapshot(t *testing.T) { + snapshotTags := map[string]string{ + uploader.SnapshotRequesterTag: "test-requester", + uploader.SnapshotUploaderTag: uploader.BlockType, + } + + matchingSnap := func(id string, start time.Time) udmrepo.Snapshot { + return udmrepo.Snapshot{ + RootObject: udmrepo.ObjectMetadata{ID: udmrepo.ID(id)}, + StartTime: start, + Tags: map[string]string{ + uploader.SnapshotRequesterTag: "test-requester", + uploader.SnapshotUploaderTag: uploader.BlockType, + }, + } + } + + testCases := []struct { + name string + setupMocks func(repo *udmrepomocks.BackupRepo) + expectedErrStr string + expectedID string + }{ + { + name: "ListSnapshot error", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ListSnapshot", mock.Anything, "source"). + Return(nil, errors.New("list error")) + }, + expectedErrStr: "error list snapshots", + }, + { + name: "empty snapshot list — no match", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ListSnapshot", mock.Anything, "source"). + Return([]udmrepo.Snapshot{}, nil) + }, + expectedErrStr: "no matching snapshot found", + }, + { + name: "snapshots without matching tags are filtered", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ListSnapshot", mock.Anything, "source"). + Return([]udmrepo.Snapshot{ + {Tags: map[string]string{"unrelated": "tag"}}, + {Tags: nil}, + }, nil) + }, + expectedErrStr: "no matching snapshot found", + }, + { + name: "snapshot with wrong requester tag is filtered", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ListSnapshot", mock.Anything, "source"). + Return([]udmrepo.Snapshot{{ + Tags: map[string]string{ + uploader.SnapshotRequesterTag: "other-requester", + uploader.SnapshotUploaderTag: uploader.BlockType, + }, + }}, nil) + }, + expectedErrStr: "no matching snapshot found", + }, + { + name: "snapshot with wrong uploader tag is filtered", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ListSnapshot", mock.Anything, "source"). + Return([]udmrepo.Snapshot{{ + Tags: map[string]string{ + uploader.SnapshotRequesterTag: "test-requester", + uploader.SnapshotUploaderTag: "kopia", + }, + }}, nil) + }, + expectedErrStr: "no matching snapshot found", + }, + { + name: "single matching snapshot is returned", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ListSnapshot", mock.Anything, "source"). + Return([]udmrepo.Snapshot{matchingSnap("snap-a", time.Now())}, nil) + }, + expectedID: "snap-a", + }, + { + name: "most recent of multiple matching snapshots is returned", + setupMocks: func(repo *udmrepomocks.BackupRepo) { + now := time.Now() + repo.On("ListSnapshot", mock.Anything, "source"). + Return([]udmrepo.Snapshot{ + matchingSnap("snap-old", now.Add(-2*time.Hour)), + matchingSnap("snap-new", now.Add(-time.Minute)), + matchingSnap("snap-mid", now.Add(-time.Hour)), + }, nil) + }, + expectedID: "snap-new", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + mockRepo := udmrepomocks.NewBackupRepo(t) + tc.setupMocks(mockRepo) + + snap, err := findPreviousSnapshot(ctx, mockRepo, "source", snapshotTags, nil, testLog()) + + if tc.expectedErrStr != "" { + require.Error(t, err) + assert.ErrorContains(t, err, tc.expectedErrStr) + } else { + require.NoError(t, err) + assert.Equal(t, udmrepo.ID(tc.expectedID), snap.RootObject.ID) + } + }) + } +} + +func TestRestore(t *testing.T) { + storedSnap := udmrepo.Snapshot{Description: "test snapshot"} + + testCases := []struct { + name string + setupMocks func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) + setupOpenDev func(t *testing.T) *os.File + expectedErrStr string + expectedSize int64 + }{ + { + name: "GetSnapshot error", + setupMocks: func(_ *mockUploader, repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-001")). + Return(udmrepo.Snapshot{}, errors.New("not found")) + }, + expectedErrStr: "Unable to load snapshot", + }, + { + name: "openBlockDevice error", + setupMocks: func(_ *mockUploader, repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-001")). + Return(storedSnap, nil) + }, + expectedErrStr: "error opening block device", + }, + { + name: "Restore error", + setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-001")). + Return(storedSnap, nil) + blkup.On("Restore", mock.Anything, mock.Anything, mock.Anything). + Return(int64(0), errors.New("restore I/O error")) + }, + setupOpenDev: func(t *testing.T) *os.File { + return tempFile(t, "") + }, + expectedErrStr: "error restoring to block dev", + }, + { + name: "success returns size", + setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { + repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-001")). + Return(storedSnap, nil) + blkup.On("Restore", mock.Anything, mock.Anything, mock.Anything). + Return(int64(4096), nil) + }, + setupOpenDev: func(t *testing.T) *os.File { + return tempFile(t, "") + }, + expectedSize: 4096, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + mockBlkup := &mockUploader{} + mockRepo := udmrepomocks.NewBackupRepo(t) + + tc.setupMocks(mockBlkup, mockRepo) + + if tc.setupOpenDev != nil { + f := tc.setupOpenDev(t) + openBlockDeviceFunc = func(_ string, _ bool) (*os.File, error) { + return f, nil + } + } else { + openBlockDeviceFunc = func(_ string, _ bool) (*os.File, error) { + return nil, errors.New("device not available") + } + } + + size, err := Restore(ctx, mockBlkup, mockRepo, "snap-001", "/dev/sdb", map[string]string{}, testLog()) + + if tc.expectedErrStr != "" { + require.Error(t, err) + assert.ErrorContains(t, err, tc.expectedErrStr) + assert.Equal(t, int64(0), size) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedSize, size) + } + + mockBlkup.AssertExpectations(t) + }) + } +} diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go new file mode 100644 index 000000000..118a09713 --- /dev/null +++ b/pkg/uploader/block/uploader.go @@ -0,0 +1,54 @@ +/* +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 block + +import ( + "context" + "os" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + "github.com/vmware-tanzu/velero/pkg/uploader" + cbt "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types" +) + +var ErrCanceled = errors.New("uploader is canceled") + +const ( + blockSize = (1 << 20) +) + +type sourceInfo struct { + dev *os.File + realSource string + size int64 +} + +type destInfo struct { + dev *os.File + path string +} + +type Uploader interface { + Backup(sourceInfo, udmrepo.ID, cbt.Iterator, map[string]string) (udmrepo.Snapshot, int64, error) + Restore(udmrepo.Snapshot, destInfo, map[string]string) (int64, error) +} + +func NewUploader(ctx context.Context, repoWriter udmrepo.BackupRepo, progress uploader.ProgressUpdater, log logrus.FieldLogger) Uploader { + return nil +} diff --git a/pkg/uploader/provider/block.go b/pkg/uploader/provider/block.go index dc5028040..427d3fae3 100644 --- a/pkg/uploader/provider/block.go +++ b/pkg/uploader/provider/block.go @@ -18,6 +18,7 @@ package provider import ( "context" + "fmt" "strings" "github.com/cockroachdb/errors" @@ -28,8 +29,12 @@ import ( repokeys "github.com/vmware-tanzu/velero/pkg/repository/keys" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" "github.com/vmware-tanzu/velero/pkg/uploader" + "github.com/vmware-tanzu/velero/pkg/uploader/block" ) +var blockBackupFunc = block.Backup +var blockRestoreFunc = block.Restore + type blockProvider struct { requestorType string bkRepo udmrepo.BackupRepo @@ -88,7 +93,6 @@ func (bp *blockProvider) GetPassword(param any) (string, error) { return strings.TrimSpace(rawPass), nil } -// TODO: implement in the following PRs func (bp *blockProvider) RunBackup( ctx context.Context, path string, @@ -100,10 +104,55 @@ func (bp *blockProvider) RunBackup( volMode uploader.PersistentVolumeMode, uploaderCfg map[string]string, updater uploader.ProgressUpdater) (string, bool, int64, int64, error) { - return "", false, 0, 0, errors.New("block backup not implemented") + if updater == nil { + return "", false, 0, 0, errors.New("Need to initial backup progress updater first") + } + + if path == "" { + return "", false, 0, 0, errors.New("path is empty") + } + + log := bp.log.WithFields(logrus.Fields{ + "path": path, + "realSource": realSource, + "parentSnapshot": parentSnapshot, + }) + + blkUploader := block.NewUploader(ctx, bp.bkRepo, updater, log) + + if tags == nil { + tags = make(map[string]string) + } + tags[uploader.SnapshotRequesterTag] = bp.requestorType + tags[uploader.SnapshotUploaderTag] = uploader.BlockType + + if realSource != "" { + realSource = fmt.Sprintf("%s/%s/%s", bp.requestorType, uploader.BlockType, realSource) + } + + snapshotInfo, _, err := blockBackupFunc(ctx, blkUploader, bp.bkRepo, path, realSource, cbtParam.Source, forceFull, parentSnapshot, cbtParam.Service, uploaderCfg, tags, log) + + if err == block.ErrCanceled { + log.Warn("Block backup is canceled") + return snapshotInfo.ID, false, snapshotInfo.Size, snapshotInfo.IncrementalSize, ErrorCanceled + } + + if err != nil { + return snapshotInfo.ID, false, snapshotInfo.Size, snapshotInfo.IncrementalSize, errors.Wrapf(err, "Failed to run block backup") + } + + updater.UpdateProgress( + &uploader.Progress{ + TotalBytes: snapshotInfo.Size, + BytesDone: snapshotInfo.Size, + }, + ) + + log.Infof("Block backup finished, snapshot ID %s, backup size %d", snapshotInfo.ID, snapshotInfo.Size) + + return snapshotInfo.ID, false, snapshotInfo.Size, snapshotInfo.IncrementalSize, nil } -// TODO: implement in the following PRs func (bp *blockProvider) RunRestore( ctx context.Context, snapshotID string, @@ -111,5 +160,31 @@ func (bp *blockProvider) RunRestore( volMode uploader.PersistentVolumeMode, uploaderCfg map[string]string, updater uploader.ProgressUpdater) (int64, error) { - return 0, errors.New("block restore not implemented") + log := bp.log.WithFields(logrus.Fields{ + "snapshotID": snapshotID, + "volumePath": volumePath, + }) + log.Info("Starting restore") + + blkUploader := block.NewUploader(ctx, bp.bkRepo, updater, log) + + size, err := blockRestoreFunc(ctx, blkUploader, bp.bkRepo, snapshotID, volumePath, uploaderCfg, log) + + if err == block.ErrCanceled { + log.Warn("Block restore is canceled") + return 0, ErrorCanceled + } + + if err != nil { + return 0, errors.Wrapf(err, "Failed to run block restore") + } + + updater.UpdateProgress(&uploader.Progress{ + TotalBytes: size, + BytesDone: size, + }) + + log.Infof("Block restore finished, restore size %v", size) + + return size, nil } diff --git a/pkg/uploader/provider/block_test.go b/pkg/uploader/provider/block_test.go index 1c180513e..e7af93855 100644 --- a/pkg/uploader/provider/block_test.go +++ b/pkg/uploader/provider/block_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "testing" "github.com/cockroachdb/errors" @@ -29,9 +30,12 @@ import ( "github.com/vmware-tanzu/velero/internal/credentials" "github.com/vmware-tanzu/velero/internal/credentials/mocks" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/cbtservice" "github.com/vmware-tanzu/velero/pkg/repository" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" udmrepomocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/mocks" + "github.com/vmware-tanzu/velero/pkg/uploader" + "github.com/vmware-tanzu/velero/pkg/uploader/block" ) func TestNewBlockUploaderProvider(t *testing.T) { @@ -125,6 +129,16 @@ func TestBlockProviderClose(t *testing.T) { mockBRepo.AssertExpectations(t) } +type blockMockProgressUpdater struct { + lastProgress *uploader.Progress + callCount int +} + +func (u *blockMockProgressUpdater) UpdateProgress(p *uploader.Progress) { + u.lastProgress = p + u.callCount++ +} + func TestBlockProviderGetPassword(t *testing.T) { testCases := []struct { name string @@ -185,3 +199,276 @@ func TestBlockProviderGetPassword(t *testing.T) { }) } } + +func TestBlockProviderRunBackup(t *testing.T) { + const requestorType = "test-requestor" + + testCases := []struct { + name string + path string + realSource string + tags map[string]string + updater uploader.ProgressUpdater + mockBackupResult uploader.SnapshotInfo + mockBackupErr error + expectedID string + expectedSize int64 + expectedIncrSize int64 + expectError bool + expectedErrStr string + skipMock bool + checkCaptures func(*testing.T, string, map[string]string) + }{ + { + name: "nil updater returns error", + path: "/dev/sda", + updater: nil, + expectError: true, + expectedErrStr: "Need to initial backup progress updater first", + skipMock: true, + }, + { + name: "empty path returns error", + path: "", + updater: &FakeBackupProgressUpdater{}, + expectError: true, + expectedErrStr: "path is empty", + skipMock: true, + }, + { + name: "success returns correct snapshot info and updates progress", + path: "/dev/sda", + updater: &blockMockProgressUpdater{}, + mockBackupResult: uploader.SnapshotInfo{ + ID: "snap-001", + Size: 1024, + IncrementalSize: 512, + }, + expectedID: "snap-001", + expectedSize: 1024, + expectedIncrSize: 512, + }, + { + name: "canceled backup returns ErrorCanceled with partial snapshot info", + path: "/dev/sda", + updater: &FakeBackupProgressUpdater{}, + mockBackupResult: uploader.SnapshotInfo{ + ID: "snap-canceled", + Size: 2048, + IncrementalSize: 1024, + }, + mockBackupErr: block.ErrCanceled, + expectedID: "snap-canceled", + expectedSize: 2048, + expectedIncrSize: 1024, + expectError: true, + expectedErrStr: "uploader is canceled", + }, + { + name: "generic backup error is wrapped", + path: "/dev/sda", + updater: &FakeBackupProgressUpdater{}, + mockBackupErr: errors.New("disk I/O error"), + expectError: true, + expectedErrStr: "Failed to run block backup", + }, + { + name: "nil tags are initialized with required tags", + path: "/dev/sda", + tags: nil, + updater: &FakeBackupProgressUpdater{}, + mockBackupResult: uploader.SnapshotInfo{ID: "snap-tags"}, + expectedID: "snap-tags", + checkCaptures: func(t *testing.T, _ string, tags map[string]string) { + assert.Equal(t, requestorType, tags[uploader.SnapshotRequesterTag]) + assert.Equal(t, uploader.BlockType, tags[uploader.SnapshotUploaderTag]) + }, + }, + { + name: "non-empty realSource is prefixed with requestorType and BlockType", + path: "/dev/sda", + realSource: "my-volume", + updater: &FakeBackupProgressUpdater{}, + mockBackupResult: uploader.SnapshotInfo{ID: "snap-source"}, + expectedID: "snap-source", + checkCaptures: func(t *testing.T, realSource string, _ map[string]string) { + assert.Equal(t, requestorType+"/"+uploader.BlockType+"/my-volume", realSource) + }, + }, + { + name: "empty realSource is passed through unchanged", + path: "/dev/sda", + realSource: "", + updater: &FakeBackupProgressUpdater{}, + mockBackupResult: uploader.SnapshotInfo{ID: "snap-nosource"}, + expectedID: "snap-nosource", + checkCaptures: func(t *testing.T, realSource string, _ map[string]string) { + assert.Equal(t, "", realSource) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockBRepo := udmrepomocks.NewBackupRepo(t) + + var capturedRealSrc string + var capturedTags map[string]string + + if !tc.skipMock { + blockBackupFunc = func(_ context.Context, _ block.Uploader, _ udmrepo.BackupRepo, _ string, realSource string, _ cbtservice.SourceInfo, _ bool, _ string, _ cbtservice.Service, _ map[string]string, tags map[string]string, _ logrus.FieldLogger) (uploader.SnapshotInfo, bool, error) { + capturedRealSrc = realSource + capturedTags = tags + return tc.mockBackupResult, false, tc.mockBackupErr + } + } + + bp := &blockProvider{ + requestorType: requestorType, + bkRepo: mockBRepo, + log: logrus.New(), + } + + snapshotID, isEmpty, size, incrSize, err := bp.RunBackup( + t.Context(), + tc.path, + tc.realSource, + tc.tags, + false, + "", + CBTParam{}, + uploader.PersistentVolumeBlock, + map[string]string{}, + tc.updater, + ) + + assert.Equal(t, tc.expectedID, snapshotID) + assert.Equal(t, tc.expectedSize, size) + assert.Equal(t, tc.expectedIncrSize, incrSize) + + if tc.expectError { + require.Error(t, err) + if tc.expectedErrStr != "" { + assert.ErrorContains(t, err, tc.expectedErrStr) + } + } else { + require.NoError(t, err) + assert.False(t, isEmpty) + if mu, ok := tc.updater.(*blockMockProgressUpdater); ok { + assert.Equal(t, 1, mu.callCount) + require.NotNil(t, mu.lastProgress) + assert.Equal(t, tc.expectedSize, mu.lastProgress.TotalBytes) + assert.Equal(t, tc.expectedSize, mu.lastProgress.BytesDone) + } + } + + if tc.checkCaptures != nil { + tc.checkCaptures(t, capturedRealSrc, capturedTags) + } + }) + } +} + +func TestBlockProviderRunRestore(t *testing.T) { + testCases := []struct { + name string + snapshotID string + volumePath string + updater uploader.ProgressUpdater + mockRestoreSize int64 + mockRestoreErr error + expectedSize int64 + expectError bool + expectedErrStr string + checkCaptures func(*testing.T, string, string) + }{ + { + name: "success returns size and updates progress", + snapshotID: "snap-001", + volumePath: "/dev/sdb", + updater: &blockMockProgressUpdater{}, + mockRestoreSize: 4096, + expectedSize: 4096, + }, + { + name: "canceled restore returns ErrorCanceled", + snapshotID: "snap-canceled", + volumePath: "/dev/sdb", + updater: &FakeRestoreProgressUpdater{}, + mockRestoreErr: block.ErrCanceled, + expectError: true, + expectedErrStr: "uploader is canceled", + }, + { + name: "generic restore error is wrapped", + snapshotID: "snap-error", + volumePath: "/dev/sdb", + updater: &FakeRestoreProgressUpdater{}, + mockRestoreErr: errors.New("disk read error"), + expectError: true, + expectedErrStr: "Failed to run block restore", + }, + { + name: "snapshotID and volumePath are forwarded to restore func", + snapshotID: "snap-fwd", + volumePath: "/dev/sdc", + updater: &FakeRestoreProgressUpdater{}, + mockRestoreSize: 512, + expectedSize: 512, + checkCaptures: func(t *testing.T, snapshotID, volumePath string) { + assert.Equal(t, "snap-fwd", snapshotID) + assert.Equal(t, "/dev/sdc", volumePath) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockBRepo := udmrepomocks.NewBackupRepo(t) + + var capturedSnapshotID string + var capturedVolumePath string + + blockRestoreFunc = func(_ context.Context, _ block.Uploader, _ udmrepo.BackupRepo, snapshotID string, volumePath string, _ map[string]string, _ logrus.FieldLogger) (int64, error) { + capturedSnapshotID = snapshotID + capturedVolumePath = volumePath + return tc.mockRestoreSize, tc.mockRestoreErr + } + + bp := &blockProvider{ + bkRepo: mockBRepo, + log: logrus.New(), + } + + size, err := bp.RunRestore( + t.Context(), + tc.snapshotID, + tc.volumePath, + uploader.PersistentVolumeBlock, + map[string]string{}, + tc.updater, + ) + + if tc.expectError { + require.Error(t, err) + if tc.expectedErrStr != "" { + assert.ErrorContains(t, err, tc.expectedErrStr) + } + assert.Equal(t, int64(0), size) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedSize, size) + if mu, ok := tc.updater.(*blockMockProgressUpdater); ok { + assert.Equal(t, 1, mu.callCount) + require.NotNil(t, mu.lastProgress) + assert.Equal(t, tc.expectedSize, mu.lastProgress.TotalBytes) + assert.Equal(t, tc.expectedSize, mu.lastProgress.BytesDone) + } + } + + if tc.checkCaptures != nil { + tc.checkCaptures(t, capturedSnapshotID, capturedVolumePath) + } + }) + } +} diff --git a/pkg/uploader/types.go b/pkg/uploader/types.go index 12ff1dc52..9c700193f 100644 --- a/pkg/uploader/types.go +++ b/pkg/uploader/types.go @@ -26,6 +26,8 @@ const ( BlockType = "velero-block" SnapshotRequesterTag = "snapshot-requester" SnapshotUploaderTag = "snapshot-uploader" + CBTChangeIDTag = "cbt-change-id" + CBTVolumeIDTag = "cbt-volume-id" ) type PersistentVolumeMode string @@ -49,8 +51,9 @@ func ValidateUploaderType(t string) (string, error) { } type SnapshotInfo struct { - ID string `json:"id"` - Size int64 `json:"Size"` + ID string + Size int64 + IncrementalSize int64 } // Progress which defined two variables to record progress From f4f897f669ae10065599c4f2454c52766db2d27e Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Fri, 22 May 2026 18:19:02 +0800 Subject: [PATCH 007/194] load object from snapshot Signed-off-by: Lyndon-Li --- pkg/uploader/block/snapshot.go | 4 +++- pkg/uploader/block/uploader.go | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pkg/uploader/block/snapshot.go b/pkg/uploader/block/snapshot.go index 272b6dd16..a3bb92431 100644 --- a/pkg/uploader/block/snapshot.go +++ b/pkg/uploader/block/snapshot.go @@ -177,8 +177,10 @@ func getParentBackupInfo(ctx context.Context, rep udmrepo.BackupRepo, forceFull log.Warnf("No VolumeID tag from parent snapshot %s, fallback to full backup", parentSnapshot) } else if previous.Tags[uploader.CBTVolumeIDTag] != volumeID { log.Warnf("VolumeID %s from parent snapshot %s is not expected as %s, fallback to full backup", previous.Tags[uploader.CBTVolumeIDTag], parentSnapshot, volumeID) + } else if obj, err := loadObjectFromSnapshot(ctx, rep, previous); err != nil { + log.WithError(err).Warnf("Failed to load object from parent snapshot %s, fallback to full backup", parentSnapshot) } else { - parentInfo.parentObject = previous.RootObject.ID + parentInfo.parentObject = obj parentInfo.changeID = previous.Tags[uploader.CBTChangeIDTag] parentInfo.volumeID = previous.Tags[uploader.CBTVolumeIDTag] diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 118a09713..f487b39cb 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -52,3 +52,20 @@ type Uploader interface { func NewUploader(ctx context.Context, repoWriter udmrepo.BackupRepo, progress uploader.ProgressUpdater, log logrus.FieldLogger) Uploader { return nil } + +func loadObjectFromSnapshot(ctx context.Context, rep udmrepo.BackupRepo, snapshot *udmrepo.Snapshot) (udmrepo.ID, error) { + if snapshot == nil { + return "", errors.New("snapshot is empty") + } + + parentMeta, err := rep.ReadMetadata(ctx, snapshot.RootObject.ID) + if err != nil { + return "", errors.Wrapf(err, "error readding snapshot metadata for %s", snapshot.Description) + } + + if len(parentMeta.SubObjects) != 1 { + return "", errors.Wrapf(err, "unexpected number of bdev object (%d) for snapshot %s", len(parentMeta.SubObjects), snapshot.Description) + } + + return parentMeta.SubObjects[0].ID, nil +} From 5bef38dc9578221f5eb37e3dd045ba25d6171142 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 25 Jun 2026 15:42:08 +0800 Subject: [PATCH 008/194] block uploader snapshot implementation Signed-off-by: Lyndon-Li --- changelogs/unreleased/9945-Lyndon-Li | 1 + pkg/uploader/block/dev_linux.go | 1 + pkg/uploader/block/snapshot.go | 6 +- pkg/uploader/block/snapshot_test.go | 12 ++- pkg/uploader/block/uploader.go | 8 +- pkg/uploader/block/uploader_test.go | 112 +++++++++++++++++++++++++++ 6 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/9945-Lyndon-Li create mode 100644 pkg/uploader/block/uploader_test.go diff --git a/changelogs/unreleased/9945-Lyndon-Li b/changelogs/unreleased/9945-Lyndon-Li new file mode 100644 index 000000000..bdb4d8d5e --- /dev/null +++ b/changelogs/unreleased/9945-Lyndon-Li @@ -0,0 +1 @@ +Add snapshot operations for block uploader \ No newline at end of file diff --git a/pkg/uploader/block/dev_linux.go b/pkg/uploader/block/dev_linux.go index 4d49442b3..6383060fb 100644 --- a/pkg/uploader/block/dev_linux.go +++ b/pkg/uploader/block/dev_linux.go @@ -25,6 +25,7 @@ import ( "github.com/pkg/errors" ) +// implement in following PRs func openBlockDevice(path string, read bool) (*os.File, error) { return nil, errors.New("Not implemented") } diff --git a/pkg/uploader/block/snapshot.go b/pkg/uploader/block/snapshot.go index a3bb92431..41d42ba36 100644 --- a/pkg/uploader/block/snapshot.go +++ b/pkg/uploader/block/snapshot.go @@ -25,6 +25,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/vmware-tanzu/velero/pkg/cbtservice" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" "github.com/vmware-tanzu/velero/pkg/uploader" @@ -202,6 +203,9 @@ func Restore(ctx context.Context, blkup Uploader, rep udmrepo.BackupRepo, snapsh log.Infof("Restore from snapshot %s, description %s, created time %v, tags %v", snapshotID, snapshot.Description, snapshot.EndTime, snapshot.Tags) + bitmap := cbt.NewBitmap(blockSize, uint64(snapshot.TotalSize), "", "", "") + bitmap.SetFull() + destPath, err := filepath.Abs(dest) if err != nil { return 0, errors.Wrapf(err, "invalid dest path '%s'", dest) @@ -214,7 +218,7 @@ func Restore(ctx context.Context, blkup Uploader, rep udmrepo.BackupRepo, snapsh return 0, errors.Wrapf(err, "error opening block device '%s'", destPath) } - size, err := blkup.Restore(snapshot, destInfo{dev: destDev, path: destPath}, uploaderCfg) + size, err := blkup.Restore(snapshot, destInfo{dev: destDev, path: destPath}, bitmap.Iterator(), uploaderCfg) if err != nil { return 0, errors.Wrapf(err, "error restoring to block dev %s", destPath) } diff --git a/pkg/uploader/block/snapshot_test.go b/pkg/uploader/block/snapshot_test.go index 1e609eb2f..5d17ee0f4 100644 --- a/pkg/uploader/block/snapshot_test.go +++ b/pkg/uploader/block/snapshot_test.go @@ -46,8 +46,8 @@ func (m *mockUploader) Backup(src sourceInfo, parent udmrepo.ID, iter cbttypes.I return args.Get(0).(udmrepo.Snapshot), args.Get(1).(int64), args.Error(2) } -func (m *mockUploader) Restore(snap udmrepo.Snapshot, dest destInfo, cfg map[string]string) (int64, error) { - args := m.Called(snap, dest, cfg) +func (m *mockUploader) Restore(snap udmrepo.Snapshot, dest destInfo, iter cbttypes.Iterator, cfg map[string]string) (int64, error) { + args := m.Called(snap, dest, iter, cfg) return args.Get(0).(int64), args.Error(1) } @@ -360,6 +360,8 @@ func TestGetParentBackupInfo(t *testing.T) { setupMocks: func(repo *udmrepomocks.BackupRepo) { repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-valid")). Return(validSnap, nil) + repo.On("ReadMetadata", mock.Anything, udmrepo.ID("root-obj")). + Return(&udmrepo.Metadata{SubObjects: []udmrepo.ObjectMetadata{{ID: "root-obj"}}}, nil) }, expectedParent: "root-obj", expectedCID: "cid-abc", @@ -386,6 +388,8 @@ func TestGetParentBackupInfo(t *testing.T) { setupMocks: func(repo *udmrepomocks.BackupRepo) { repo.On("ListSnapshot", mock.Anything, realSource). Return([]udmrepo.Snapshot{validSnap}, nil) + repo.On("ReadMetadata", mock.Anything, udmrepo.ID("root-obj")). + Return(&udmrepo.Metadata{SubObjects: []udmrepo.ObjectMetadata{{ID: "root-obj"}}}, nil) }, expectedParent: "root-obj", expectedCID: "cid-abc", @@ -566,7 +570,7 @@ func TestRestore(t *testing.T) { setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-001")). Return(storedSnap, nil) - blkup.On("Restore", mock.Anything, mock.Anything, mock.Anything). + blkup.On("Restore", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(int64(0), errors.New("restore I/O error")) }, setupOpenDev: func(t *testing.T) *os.File { @@ -579,7 +583,7 @@ func TestRestore(t *testing.T) { setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-001")). Return(storedSnap, nil) - blkup.On("Restore", mock.Anything, mock.Anything, mock.Anything). + blkup.On("Restore", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(int64(4096), nil) }, setupOpenDev: func(t *testing.T) *os.File { diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index f487b39cb..7f089bd01 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -22,6 +22,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" "github.com/vmware-tanzu/velero/pkg/uploader" cbt "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types" @@ -46,9 +47,10 @@ type destInfo struct { type Uploader interface { Backup(sourceInfo, udmrepo.ID, cbt.Iterator, map[string]string) (udmrepo.Snapshot, int64, error) - Restore(udmrepo.Snapshot, destInfo, map[string]string) (int64, error) + Restore(udmrepo.Snapshot, destInfo, cbt.Iterator, map[string]string) (int64, error) } +// implement in following PRs func NewUploader(ctx context.Context, repoWriter udmrepo.BackupRepo, progress uploader.ProgressUpdater, log logrus.FieldLogger) Uploader { return nil } @@ -60,11 +62,11 @@ func loadObjectFromSnapshot(ctx context.Context, rep udmrepo.BackupRepo, snapsho parentMeta, err := rep.ReadMetadata(ctx, snapshot.RootObject.ID) if err != nil { - return "", errors.Wrapf(err, "error readding snapshot metadata for %s", snapshot.Description) + return "", errors.Wrapf(err, "error reading snapshot metadata for %s", snapshot.Description) } if len(parentMeta.SubObjects) != 1 { - return "", errors.Wrapf(err, "unexpected number of bdev object (%d) for snapshot %s", len(parentMeta.SubObjects), snapshot.Description) + return "", errors.Errorf("unexpected number of bdev object (%d) for snapshot %s", len(parentMeta.SubObjects), snapshot.Description) } return parentMeta.SubObjects[0].ID, nil diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go new file mode 100644 index 000000000..ea1986197 --- /dev/null +++ b/pkg/uploader/block/uploader_test.go @@ -0,0 +1,112 @@ +/* +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 block + +import ( + "context" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + udmrepomocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/mocks" +) + +func TestLoadObjectFromSnapshot(t *testing.T) { + testCases := []struct { + name string + snapshot *udmrepo.Snapshot + setupMocks func(repo *udmrepomocks.BackupRepo) + expectedErrStr string + expectedID udmrepo.ID + }{ + { + name: "nil snapshot", + snapshot: nil, + expectedErrStr: "snapshot is empty", + }, + { + name: "ReadMetadata error", + snapshot: &udmrepo.Snapshot{ + RootObject: udmrepo.ObjectMetadata{ID: "root-obj"}, + }, + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ReadMetadata", mock.Anything, udmrepo.ID("root-obj")). + Return(nil, errors.New("read error")) + }, + expectedErrStr: "error reading snapshot metadata", + }, + { + name: "unexpected number of subobjects (0)", + snapshot: &udmrepo.Snapshot{ + RootObject: udmrepo.ObjectMetadata{ID: "root-obj"}, + }, + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ReadMetadata", mock.Anything, udmrepo.ID("root-obj")). + Return(&udmrepo.Metadata{SubObjects: []udmrepo.ObjectMetadata{}}, nil) + }, + expectedErrStr: "unexpected number of bdev object", + }, + { + name: "unexpected number of subobjects (2)", + snapshot: &udmrepo.Snapshot{ + RootObject: udmrepo.ObjectMetadata{ID: "root-obj"}, + }, + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ReadMetadata", mock.Anything, udmrepo.ID("root-obj")). + Return(&udmrepo.Metadata{SubObjects: []udmrepo.ObjectMetadata{{ID: "obj-1"}, {ID: "obj-2"}}}, nil) + }, + expectedErrStr: "unexpected number of bdev object", + }, + { + name: "success", + snapshot: &udmrepo.Snapshot{ + RootObject: udmrepo.ObjectMetadata{ID: "root-obj"}, + }, + setupMocks: func(repo *udmrepomocks.BackupRepo) { + repo.On("ReadMetadata", mock.Anything, udmrepo.ID("root-obj")). + Return(&udmrepo.Metadata{SubObjects: []udmrepo.ObjectMetadata{{ID: "bdev-obj"}}}, nil) + }, + expectedID: "bdev-obj", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + mockRepo := udmrepomocks.NewBackupRepo(t) + + if tc.setupMocks != nil { + tc.setupMocks(mockRepo) + } + + id, err := loadObjectFromSnapshot(ctx, mockRepo, tc.snapshot) + + if tc.expectedErrStr != "" { + require.Error(t, err) + assert.ErrorContains(t, err, tc.expectedErrStr) + assert.Empty(t, id) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedID, id) + } + }) + } +} From b41e5df294676b4bba33b33921c065b97b8b776d Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Thu, 25 Jun 2026 16:51:11 +0800 Subject: [PATCH 009/194] restore filters via resource policy restore filters via resource policy, support ClusterScopedFilterPolicy and NamespaceFilterPolicies. Signed-off-by: Adam Zhang --- changelogs/unreleased/9946-adam-jian-zhang | 1 + pkg/controller/restore_controller.go | 48 ++- pkg/controller/restore_controller_test.go | 144 +++++++- pkg/restore/request.go | 2 + pkg/restore/restore.go | 362 +++++++++++++++++++-- pkg/restore/restore_policies_test.go | 206 ++++++++++++ 6 files changed, 720 insertions(+), 43 deletions(-) create mode 100644 changelogs/unreleased/9946-adam-jian-zhang create mode 100644 pkg/restore/restore_policies_test.go diff --git a/changelogs/unreleased/9946-adam-jian-zhang b/changelogs/unreleased/9946-adam-jian-zhang new file mode 100644 index 000000000..7d7a9db76 --- /dev/null +++ b/changelogs/unreleased/9946-adam-jian-zhang @@ -0,0 +1 @@ +Fix issue #9936, restore filters via resource policy implementation diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index 8e3daba07..5b055bc6c 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -44,6 +44,7 @@ import ( "github.com/vmware-tanzu/velero/internal/hook" "github.com/vmware-tanzu/velero/internal/resourcemodifiers" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" "github.com/vmware-tanzu/velero/internal/volume" api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/constant" @@ -232,7 +233,7 @@ func (r *restoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct original := restore.DeepCopy() // Validate the restore and fetch the backup - info, resourceModifiers := r.validateAndComplete(restore) + info, resourceModifiers, restoreResPolicies := r.validateAndComplete(ctx, restore) // Register attempts after validation so we don't have to fetch the backup multiple times backupScheduleName := restore.Spec.ScheduleName @@ -267,7 +268,7 @@ func (r *restoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, nil } - if err := r.runValidatedRestore(restore, info, resourceModifiers); err != nil { + if err := r.runValidatedRestore(restore, info, resourceModifiers, restoreResPolicies); err != nil { log.WithError(err).Debug("Restore failed") restore.Status.Phase = api.RestorePhaseFailed restore.Status.FailureReason = err.Error() @@ -303,7 +304,7 @@ func (r *restoreReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInfo, *resourcemodifiers.ResourceModifiers) { +func (r *restoreReconciler) validateAndComplete(ctx context.Context, restore *api.Restore) (backupInfo, *resourcemodifiers.ResourceModifiers, *resourcepolicies.Policies) { // add non-restorable resources to restore's excluded resources excludedResources := sets.NewString(restore.Spec.ExcludedResources...) for _, nonrestorable := range nonRestorableResources { @@ -338,7 +339,7 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf // validate that exactly one of BackupName and ScheduleName have been specified if !backupXorScheduleProvided(restore) { restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "Either a backup or schedule must be specified as a source for the restore, but not both") - return backupInfo{}, nil + return backupInfo{}, nil, nil } // validate Restore Init Hook's InitContainers @@ -372,9 +373,9 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf })) backupList := &api.BackupList{} - if err := r.kbClient.List(context.Background(), backupList, &client.ListOptions{LabelSelector: selector}); err != nil { + if err := r.kbClient.List(ctx, backupList, &client.ListOptions{LabelSelector: selector}); err != nil { restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "Unable to list backups for schedule") - return backupInfo{}, nil + return backupInfo{}, nil, nil } if len(backupList.Items) == 0 { restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "No backups found for schedule") @@ -384,19 +385,19 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf restore.Spec.BackupName = backup.Name } else { restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "No completed backups found for schedule") - return backupInfo{}, nil + return backupInfo{}, nil, nil } } info, err := r.fetchBackupInfo(restore.Spec.BackupName) if err != nil { restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("Error retrieving backup: %v", err)) - return backupInfo{}, nil + return backupInfo{}, nil, nil } if !veleroutil.BSLIsAvailable(*info.location) { restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("The BSL %s is unavailable, cannot retrieve the backup", info.location.Name)) - return backupInfo{}, nil + return backupInfo{}, nil, nil } // reject restores from backups that are not in a usable phase @@ -407,7 +408,7 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("backup %q is in phase %q and cannot be used as a restore source", info.backup.Name, info.backup.Status.Phase)) - return backupInfo{}, nil + return backupInfo{}, nil, nil } // Fill in the ScheduleName so it's easier to consume for metrics. @@ -415,26 +416,40 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf restore.Spec.ScheduleName = info.backup.GetLabels()[api.ScheduleNameLabel] } + var restoreResPolicies *resourcepolicies.Policies + if restore.Spec.ResourcePolicy != nil { + var err error + restoreResPolicies, err = resourcepolicies.GetResourcePoliciesFromRestore( + ctx, restore, r.kbClient, r.logger, + ) + if err != nil { + restore.Status.ValidationErrors = append( + restore.Status.ValidationErrors, err.Error(), + ) + return backupInfo{}, nil, nil + } + } + var resourceModifiers *resourcemodifiers.ResourceModifiers if restore.Spec.ResourceModifier != nil && strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) { ResourceModifierConfigMap := &corev1api.ConfigMap{} - err := r.kbClient.Get(context.Background(), client.ObjectKey{Namespace: restore.Namespace, Name: restore.Spec.ResourceModifier.Name}, ResourceModifierConfigMap) + err := r.kbClient.Get(ctx, client.ObjectKey{Namespace: restore.Namespace, Name: restore.Spec.ResourceModifier.Name}, ResourceModifierConfigMap) if err != nil { restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("failed to get resource modifiers configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name)) - return backupInfo{}, nil + return backupInfo{}, nil, nil } resourceModifiers, err = resourcemodifiers.GetResourceModifiersFromConfig(ResourceModifierConfigMap) if err != nil { restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, "Error in parsing resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name).Error()) - return backupInfo{}, nil + return backupInfo{}, nil, nil } else if err = resourceModifiers.Validate(); err != nil { restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, "Validation error in resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name).Error()) - return backupInfo{}, nil + return backupInfo{}, nil, nil } r.logger.Infof("Retrieved Resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name) } - return info, resourceModifiers + return info, resourceModifiers, restoreResPolicies } // backupXorScheduleProvided returns true if exactly one of BackupName and @@ -507,7 +522,7 @@ func fetchBackupInfoInternal(kbClient client.Client, namespace, backupName strin // The log and results files are uploaded to backup storage. Any error returned from this function // means that the restore failed. This function updates the restore API object with warning and error // counts, but *does not* update its phase or patch it via the API. -func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backupInfo, resourceModifiers *resourcemodifiers.ResourceModifiers) error { +func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backupInfo, resourceModifiers *resourcemodifiers.ResourceModifiers, restoreResPolicies *resourcepolicies.Policies) error { // instantiate the per-restore logger that will output both to a temp file // (for upload to object storage) and to stdout. restoreLog, err := logging.NewTempFileLogger(r.restoreLogLevel, r.logFormat, nil, logrus.Fields{"restore": kubeutil.NamespaceAndName(restore)}) @@ -586,6 +601,7 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu VolumeSnapshots: volumeSnapshots, BackupReader: backupFile, ResourceModifiers: resourceModifiers, + ResPolicies: restoreResPolicies, DisableInformerCache: r.disableInformerCache, CSIVolumeSnapshots: csiVolumeSnapshots, BackupVolumeInfoMap: backupVolumeInfoMap, diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index 111407f3e..062edf9dd 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -18,6 +18,7 @@ package controller import ( "bytes" + "context" "io" "testing" "time" @@ -785,7 +786,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) { Phase(velerov1api.BackupPhaseCompleted). Result())) - r.validateAndComplete(restore) + r.validateAndComplete(context.Background(), restore) assert.Contains(t, restore.Status.ValidationErrors, "No backups found for schedule") assert.Empty(t, restore.Spec.BackupName) @@ -801,7 +802,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) { Result(), )) - r.validateAndComplete(restore) + r.validateAndComplete(context.Background(), restore) assert.Contains(t, restore.Status.ValidationErrors, "No completed backups found for schedule") assert.Empty(t, restore.Spec.BackupName) @@ -832,11 +833,140 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) { ScheduleName: "schedule-1", }, } - r.validateAndComplete(restore) + r.validateAndComplete(context.Background(), restore) assert.Nil(t, restore.Status.ValidationErrors) assert.Equal(t, "foo", restore.Spec.BackupName) } +func TestValidateAndCompleteWithResourcePolicySpecified(t *testing.T) { + formatFlag := logging.FormatText + + var ( + logger = velerotest.NewLogger() + pluginManager = &pluginmocks.Manager{} + fakeClient = velerotest.NewFakeControllerRuntimeClient(t) + fakeGlobalClient = velerotest.NewFakeControllerRuntimeClient(t) + backupStore = &persistencemocks.BackupStore{} + ) + + r := NewRestoreReconciler( + t.Context(), + velerov1api.DefaultNamespace, + nil, + fakeClient, + logger, + logrus.DebugLevel, + func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, + NewFakeSingleObjectBackupStoreGetter(backupStore), + metrics.NewServerMetrics(), + formatFlag, + 60*time.Minute, + false, + fakeGlobalClient, + 10*time.Minute, + ) + + restore := &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-1", + }, + Spec: velerov1api.RestoreSpec{ + BackupName: "backup-1", + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: "configmap", + Name: "test-configmap", + }, + }, + } + + location := builder.ForBackupStorageLocation("velero", "default").Provider("myCloud").Bucket("bucket").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result() + require.NoError(t, r.kbClient.Create(t.Context(), location)) + + require.NoError(t, r.kbClient.Create( + t.Context(), + defaultBackup(). + ObjectMeta( + builder.WithName("backup-1"), + ).StorageLocation("default"). + Phase(velerov1api.BackupPhaseCompleted). + Result(), + )) + + r.validateAndComplete(context.Background(), restore) + assert.Contains(t, restore.Status.ValidationErrors[0], "fail to get ResourcePolicies velero/test-configmap ConfigMap") + + restore1 := &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-1", + }, + Spec: velerov1api.RestoreSpec{ + BackupName: "backup-1", + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: "configmap", + Name: "test-configmap", + }, + }, + } + + cm1 := &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: velerov1api.DefaultNamespace, + }, + Data: map[string]string{ + "policy.yaml": `version: v1 +clusterScopedFilterPolicy: + resourceFilters: + - kinds: + - pods +`, + }, + } + require.NoError(t, r.kbClient.Create(t.Context(), cm1)) + + r.validateAndComplete(context.Background(), restore1) + assert.Nil(t, restore1.Status.ValidationErrors) + + restore2 := &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-1", + }, + Spec: velerov1api.RestoreSpec{ + BackupName: "backup-1", + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + // intentional to ensure case insensitivity works as expected + Kind: "confIGMaP", + Name: "test-configmap-invalid", + }, + }, + } + + cm2 := &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap-invalid", + Namespace: velerov1api.DefaultNamespace, + }, + Data: map[string]string{ + "policy.yaml": `version: v1 +volumePolicies: + - conditions: + capacity: '0,10Gi' + csi: + driver: disks.csi.driver + action: + type: invalid_action +`, + }, + } + require.NoError(t, r.kbClient.Create(t.Context(), cm2)) + + r.validateAndComplete(context.Background(), restore2) + assert.Contains(t, restore2.Status.ValidationErrors[0], "fail to validate ResourcePolicies in ConfigMap velero/test-configmap-invalid") +} + func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { formatFlag := logging.FormatText @@ -892,7 +1022,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { Result(), )) - r.validateAndComplete(restore) + r.validateAndComplete(context.Background(), restore) assert.Contains(t, restore.Status.ValidationErrors[0], "failed to get resource modifiers configmap") restore1 := &velerov1api.Restore{ @@ -920,7 +1050,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { } require.NoError(t, r.kbClient.Create(t.Context(), cm1)) - r.validateAndComplete(restore1) + r.validateAndComplete(context.Background(), restore1) assert.Nil(t, restore1.Status.ValidationErrors) restore2 := &velerov1api.Restore{ @@ -949,7 +1079,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { } require.NoError(t, r.kbClient.Create(t.Context(), invalidVersionCm)) - r.validateAndComplete(restore2) + r.validateAndComplete(context.Background(), restore2) assert.Contains(t, restore2.Status.ValidationErrors[0], "Error in parsing resource modifiers provided in configmap") restore3 := &velerov1api.Restore{ @@ -977,7 +1107,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { } require.NoError(t, r.kbClient.Create(t.Context(), invalidOperatorCm)) - r.validateAndComplete(restore3) + r.validateAndComplete(context.Background(), restore3) assert.Contains(t, restore3.Status.ValidationErrors[0], "Validation error in resource modifiers provided in configmap") } diff --git a/pkg/restore/request.go b/pkg/restore/request.go index 239d65df9..57ab6f119 100644 --- a/pkg/restore/request.go +++ b/pkg/restore/request.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "github.com/vmware-tanzu/velero/internal/resourcemodifiers" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" "github.com/vmware-tanzu/velero/internal/volume" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/itemoperation" @@ -61,6 +62,7 @@ type Request struct { RestoredItems map[itemKey]restoredItemStatus itemOperationsList *[]*itemoperation.RestoreOperation ResourceModifiers *resourcemodifiers.ResourceModifiers + ResPolicies *resourcepolicies.Policies DisableInformerCache bool CSIVolumeSnapshots []*snapshotv1api.VolumeSnapshot BackupVolumeInfoMap map[string]volume.BackupVolumeInfo diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index afb5f3775..5c15bf80e 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -32,6 +32,7 @@ import ( "time" "github.com/cockroachdb/errors" + "github.com/gobwas/glob" "github.com/google/uuid" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" "github.com/sirupsen/logrus" @@ -55,6 +56,7 @@ import ( "github.com/vmware-tanzu/velero/internal/credentials" "github.com/vmware-tanzu/velero/internal/hook" "github.com/vmware-tanzu/velero/internal/resourcemodifiers" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" "github.com/vmware-tanzu/velero/internal/volume" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/archive" @@ -237,6 +239,43 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( Includes(req.Restore.Spec.IncludedNamespaces...). Excludes(req.Restore.Spec.ExcludedNamespaces...) + var clusterScopedFilterMap map[string]*resolvedResourceFilter + var namespacedFilterMap map[string]*resolvedNamespaceFilter + var namespacedFilterPatterns []namespacedFilterPattern + + if req.ResPolicies != nil { + if kr.discoveryHelper == nil { + return results.Result{}, results.Result{Velero: []string{"failed to resolve namespace filter policies: discovery client unavailable"}} + } + + // Resolve clusterScopedFilterPolicy + csPolicy := req.ResPolicies.GetClusterScopedFilterPolicy() + if csPolicy != nil { + clusterScopedFilterMap, err = resolveRestoreClusterScopedFilterPolicy( + csPolicy, + kr.discoveryHelper, + req.Log, + ) + if err != nil { + return results.Result{}, results.Result{Velero: []string{err.Error()}} + } + } + + // Resolve namespacedFilterPolicies + nfPolicies := req.ResPolicies.GetNamespacedFilterPolicies() + if len(nfPolicies) > 0 { + namespacedFilterMap, namespacedFilterPatterns, err = resolveRestoreNamespacedFilterPolicies( + nfPolicies, + req.Restore.Spec.ExcludedResources, + kr.discoveryHelper, + req.Log, + ) + if err != nil { + return results.Result{}, results.Result{Velero: []string{err.Error()}} + } + } + } + resolvedActions, err := restoreItemActionResolver.ResolveActions(kr.discoveryHelper, kr.logger) if err != nil { return results.Result{}, results.Result{Velero: []string{err.Error()}} @@ -333,6 +372,10 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( restoreVolumeInfoTracker: req.RestoreVolumeInfoTracker, hooksWaitExecutor: hooksWaitExecutor, resourceDeletionStatusTracker: req.ResourceDeletionStatusTracker, + clusterScopedFilterMap: clusterScopedFilterMap, + namespacedFilterMap: namespacedFilterMap, + namespacedFilterPatterns: namespacedFilterPatterns, + namespaceFilterCache: make(map[string]*resolvedNamespaceFilter), } return restoreCtx.execute() @@ -382,6 +425,216 @@ type restoreContext struct { restoreVolumeInfoTracker *volume.RestoreVolumeInfoTracker hooksWaitExecutor *hooksWaitExecutor resourceDeletionStatusTracker kube.ResourceDeletionStatusTracker + + // clusterScopedFilterMap holds resolved per-kind filters for cluster-scoped resources. + // Key is the resolved group-resource string. + clusterScopedFilterMap map[string]*resolvedResourceFilter + + // namespacedFilterMap holds resolved per-namespace filters. + // Key is either an exact namespace name or a glob pattern string. + namespacedFilterMap map[string]*resolvedNamespaceFilter + + // namespacedFilterPatterns preserves the order of patterns for first-match + // semantics and caches pre-compiled globs to avoid repeated compilation. + namespacedFilterPatterns []namespacedFilterPattern + + // namespaceFilterCache memoizes the resolved filter for a given namespace + // to avoid re-evaluating glob patterns on every call. + namespaceFilterCache map[string]*resolvedNamespaceFilter +} + +type resolvedResourceFilter struct { + labelSelector labels.Selector + orLabelSelectors []labels.Selector + nameIE *collections.IncludesExcludes +} + +type resolvedNamespaceFilter struct { + // resourceFilterMap is keyed by the resolved group-resource string + resourceFilterMap map[string]*resolvedResourceFilter + // catchAllFilter holds the resolved filter for a catch-all entry (empty kinds or ["*"]). + // nil when no catch-all entry is defined. + catchAllFilter *resolvedResourceFilter +} + +// namespacedFilterPattern pairs a namespace pattern string with its pre-compiled +// glob so that getNamespaceFilter does not recompile on every call. +type namespacedFilterPattern struct { + pattern string + compiled glob.Glob // compiled once at restore start; nil for exact-match patterns +} + +func (ctx *restoreContext) getNamespaceFilter(namespace string) *resolvedNamespaceFilter { + if ctx.namespacedFilterMap == nil { + return nil + } + + // 1. Check the cache first + if filter, ok := ctx.namespaceFilterCache[namespace]; ok { + return filter + } + + // 2. Walk patterns in definition order (first-match semantics) + for _, p := range ctx.namespacedFilterPatterns { + if p.compiled != nil { + if p.compiled.Match(namespace) { + filter := ctx.namespacedFilterMap[p.pattern] + ctx.namespaceFilterCache[namespace] = filter + return filter + } + } else if p.pattern == namespace { + filter := ctx.namespacedFilterMap[p.pattern] + ctx.namespaceFilterCache[namespace] = filter + return filter + } + } + + // 3. Cache the miss so we don't re-evaluate failed matches + ctx.namespaceFilterCache[namespace] = nil + return nil +} + +// resolveRestoreClusterScopedFilterPolicy resolves the cluster-scoped filter policy +// into a map keyed by group-resource string. Note: catch-all entries (empty or ["*"] kinds) +// are NOT supported in clusterScopedFilterPolicy — validation rejects them earlier. +// Cluster-scoped filtering is a refinement overlay; unlisted kinds fall back to global +// filters via the existing pipeline, so there is no catchAllFilter field on this map. +func resolveRestoreClusterScopedFilterPolicy( + policy *resourcepolicies.ClusterScopedFilterPolicy, + helper discovery.Helper, + log logrus.FieldLogger, +) (map[string]*resolvedResourceFilter, error) { + result := make(map[string]*resolvedResourceFilter) + for _, rf := range policy.ResourceFilters { + resolved, err := resolveResourceFilter(rf) + if err != nil { + return nil, err + } + for _, kind := range rf.Kinds { + gr, resource, err := helper.ResourceFor(schema.GroupVersionResource{Resource: kind}) + if err != nil { + log.WithField("kind", kind).Warnf("Cannot resolve kind via discovery, using as-is") + result[kind] = resolved + continue + } + if resource.Namespaced { + log.Warnf("kind %q in clusterScopedFilterPolicy is a namespace-scoped resource; it will never match in a cluster-scoped filter — did you mean namespacedFilterPolicies?", kind) + } + result[gr.GroupResource().String()] = resolved + } + } + return result, nil +} + +func resolveRestoreNamespacedFilterPolicies( + policies []resourcepolicies.NamespacedFilterPolicy, + excludedResources []string, + helper discovery.Helper, + log logrus.FieldLogger, +) (map[string]*resolvedNamespaceFilter, []namespacedFilterPattern, error) { + result := make(map[string]*resolvedNamespaceFilter) + var patternOrder []namespacedFilterPattern + + // Build a quick lookup map for globally excluded resources + globalExcludes := make(map[string]bool) + for _, ex := range excludedResources { + globalExcludes[ex] = true + } + + for _, policy := range policies { + rfMap := make(map[string]*resolvedResourceFilter) + var catchAll *resolvedResourceFilter + + for _, rf := range policy.ResourceFilters { + resolved, err := resolveResourceFilter(rf) + if err != nil { + return nil, nil, err + } + + if rf.IsCatchAll() { + catchAll = resolved + continue + } + + for _, kind := range rf.Kinds { + gr, resource, err := helper.ResourceFor( + schema.GroupVersionResource{Resource: kind}, + ) + if err != nil { + log.WithField("kind", kind).Warnf( + "Cannot resolve kind via discovery, using as-is") + rfMap[kind] = resolved + continue + } + + if !resource.Namespaced { + log.Warnf("kind %q in namespacedFilterPolicies is a cluster-scoped resource; it will never match in a namespace-scoped filter — did you mean clusterScopedFilterPolicy?", kind) + } + + if globalExcludes[kind] || globalExcludes[gr.GroupResource().String()] { + log.WithFields(logrus.Fields{ + "kind": kind, + "namespacePattern": strings.Join(policy.Namespaces, ","), + }).Warn("namespacedFilterPolicies entry lists a kind that is globally excluded by RestoreSpec.ExcludedResources; the per-namespace filter entry has no effect") + } + + rfMap[gr.GroupResource().String()] = resolved + } + } + + nsFilter := &resolvedNamespaceFilter{ + resourceFilterMap: rfMap, + catchAllFilter: catchAll, + } + for _, nsPattern := range policy.Namespaces { + result[nsPattern] = nsFilter + var compiled glob.Glob + if strings.ContainsAny(nsPattern, "*?[") { + var err error + compiled, err = glob.Compile(nsPattern) + if err != nil { + log.WithError(err).Warnf("Failed to compile namespace glob pattern %q, falling back to exact match", nsPattern) + } + } + patternOrder = append(patternOrder, namespacedFilterPattern{ + pattern: nsPattern, + compiled: compiled, + }) + } + } + return result, patternOrder, nil +} + +// resolveResourceFilter converts a ResourceFilter's label selectors and name patterns +// into their runtime representations. +func resolveResourceFilter( + rf resourcepolicies.ResourceFilter, +) (*resolvedResourceFilter, error) { + var selector labels.Selector + if len(rf.LabelSelector) > 0 { + var err error + selector, err = labels.ValidatedSelectorFromSet(labels.Set(rf.LabelSelector)) + if err != nil { + return nil, fmt.Errorf("invalid label selector in resource filter: %w", err) + } + } + var orSelectors []labels.Selector + for _, ols := range rf.OrLabelSelectors { + s, err := labels.ValidatedSelectorFromSet(labels.Set(ols)) + if err != nil { + return nil, fmt.Errorf("invalid OR label selector in resource filter: %w", err) + } + orSelectors = append(orSelectors, s) + } + var nameIE *collections.IncludesExcludes + if len(rf.Names) > 0 || len(rf.ExcludedNames) > 0 { + nameIE = collections.NewIncludesExcludes().Includes(rf.Names...).Excludes(rf.ExcludedNames...) + } + return &resolvedResourceFilter{ + labelSelector: selector, + orLabelSelectors: orSelectors, + nameIE: nameIE, + }, nil } type resourceClientKey struct { @@ -1128,6 +1381,10 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso // and should be excluded. Note that we're checking the object's namespace ( // via obj.GetNamespace()) instead of the namespace parameter, because we want // to check the *original* namespace, not the remapped one if it's been remapped. + // + // Note: Additional items intentionally bypass fine-grained resource filter policies + // (like per-namespace label/name selectors) to avoid breaking semantic dependencies, + // but they must still pass the global exclusions enforced below. if namespace != "" { if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) { restoreLogger.Info("Not restoring item because namespace is excluded") @@ -2277,6 +2534,18 @@ func (ctx *restoreContext) getOrderedResourceCollection( continue } + // Per-namespace resource type check from restore filter policy + if namespace != "" && !ctx.resourceMustHave.Has(groupResource.String()) { + if nsFilter := ctx.getNamespaceFilter(namespace); nsFilter != nil { + _, kindListed := nsFilter.resourceFilterMap[groupResource.String()] + if !kindListed && nsFilter.catchAllFilter == nil { + ctx.log.Infof("Skipping resource %s in namespace %s: not in resourceFilters", + resource, namespace) + continue + } + } + } + res, w, e := ctx.getSelectedRestoreableItems(groupResource.String(), namespace, items) warnings.Merge(&w) errs.Merge(&e) @@ -2330,6 +2599,30 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original resourceForPath = filepath.Join(resource, cgv.Dir) } + var rf *resolvedResourceFilter + var useFilterPolicy bool + + if !ctx.resourceMustHave.Has(resource) { + if originalNamespace != "" { + // Namespace-scoped path + if nsFilter := ctx.getNamespaceFilter(originalNamespace); nsFilter != nil { + // Resolve effective filter: kind-specific takes precedence over catch-all + rf = nsFilter.resourceFilterMap[resource] + if rf == nil { + rf = nsFilter.catchAllFilter // may be nil if no catch-all + } + useFilterPolicy = true + } + } else if ctx.clusterScopedFilterMap != nil { + // Cluster-scoped path: only applies if kind is listed (refinement overlay) + if listedRF, ok := ctx.clusterScopedFilterMap[resource]; ok { + rf = listedRF + useFilterPolicy = true + } + // If kind not listed, fall through to global selectors below + } + } + for _, item := range items { itemPath := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, item) @@ -2347,29 +2640,58 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original } if !ctx.resourceMustHave.Has(resource) { - if !ctx.selector.Matches(labels.Set(obj.GetLabels())) { - continue - } - - // Processing OrLabelSelectors when specified in the restore request. LabelSelectors as well as OrLabelSelectors - // cannot co-exist, only one of them can be specified - var skipItem = false - var skip = 0 - ctx.log.Debugf("orSelectors specified: %s for item: %s", ctx.OrSelectors, item) - for _, s := range ctx.OrSelectors { - if !s.Matches(labels.Set(obj.GetLabels())) { - skip++ + if useFilterPolicy { + if rf != nil { + // Per-kind label selector + if rf.labelSelector != nil && !rf.labelSelector.Matches(labels.Set(obj.GetLabels())) { + continue + } + // Per-kind OR label selectors + if len(rf.orLabelSelectors) > 0 { + matched := false + for _, s := range rf.orLabelSelectors { + if s.Matches(labels.Set(obj.GetLabels())) { + matched = true + break + } + } + if !matched { + ctx.log.Infof("Excluding item %s: no OR label selector matched (restore filter policy)", item) + continue + } + } + // Per-kind name filter + if rf.nameIE != nil && !rf.nameIE.ShouldInclude(obj.GetName()) { + ctx.log.Infof("Excluding item %s: name does not match restore filter policy", obj.GetName()) + continue + } + } + } else { + // Existing global selector logic + if !ctx.selector.Matches(labels.Set(obj.GetLabels())) { + continue } - if len(ctx.OrSelectors) == skip && skip > 0 { - ctx.log.Infof("setting skip flag to true for item: %s", item) - skipItem = true - } - } + // Processing OrLabelSelectors when specified in the restore request. LabelSelectors as well as OrLabelSelectors + // cannot co-exist, only one of them can be specified + var skipItem = false + var skip = 0 + ctx.log.Debugf("orSelectors specified: %s for item: %s", ctx.OrSelectors, item) + for _, s := range ctx.OrSelectors { + if !s.Matches(labels.Set(obj.GetLabels())) { + skip++ + } - if skipItem { - ctx.log.Infof("restore orSelector labels did not match, skipping restore of item: %s", skipItem, item) - continue + if len(ctx.OrSelectors) == skip && skip > 0 { + ctx.log.Infof("setting skip flag to true for item: %s", item) + skipItem = true + } + } + + if skipItem { + ctx.log.Infof("restore orSelector labels did not match, skipping restore of item: %s", skipItem, item) + continue + } } } diff --git a/pkg/restore/restore_policies_test.go b/pkg/restore/restore_policies_test.go new file mode 100644 index 000000000..26fe20aab --- /dev/null +++ b/pkg/restore/restore_policies_test.go @@ -0,0 +1,206 @@ +package restore + +import ( + "context" + "io" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + corev1api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/vmware-tanzu/velero/internal/resourcepolicies" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/test" +) + +func TestRestoreResourcePoliciesFiltering(t *testing.T) { + tests := []struct { + name string + restore *velerov1api.Restore + backup *velerov1api.Backup + policyYAML string + apiResources []*test.APIResource + tarball io.Reader + want map[*test.APIResource][]string + }{ + { + name: "namespaced filter policy with exact namespace match", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + policyYAML: `version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-1 + resourceFilters: + - kinds: + - pods + names: + - pod-1 +`, + tarball: test.NewTarWriter(t). + AddItems("pods", + builder.ForPod("ns-1", "pod-1").Result(), + builder.ForPod("ns-1", "pod-2").Result(), + builder.ForPod("ns-2", "pod-1").Result(), + ). + Done(), + apiResources: []*test.APIResource{ + test.Pods(), + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1", "ns-2/pod-1"}, // ns-2 is not filtered, ns-1 only includes pod-1 + }, + }, + { + name: "namespaced filter policy with glob namespace match and first-match semantics", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + policyYAML: `version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-* + resourceFilters: + - kinds: + - pods + names: + - pod-1 + - namespaces: + - ns-1 + resourceFilters: + - kinds: + - pods + names: + - pod-2 +`, + tarball: test.NewTarWriter(t). + AddItems("pods", + builder.ForPod("ns-1", "pod-1").Result(), + builder.ForPod("ns-1", "pod-2").Result(), + builder.ForPod("ns-2", "pod-1").Result(), + builder.ForPod("ns-2", "pod-2").Result(), + ). + Done(), + apiResources: []*test.APIResource{ + test.Pods(), + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1", "ns-2/pod-1"}, + }, + }, + { + name: "cluster scoped filter policy", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + policyYAML: `version: v1 +clusterScopedFilterPolicy: + resourceFilters: + - kinds: + - persistentvolumes + names: + - pv-1 +`, + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", + builder.ForPersistentVolume("pv-1").Result(), + builder.ForPersistentVolume("pv-2").Result(), + ). + Done(), + apiResources: []*test.APIResource{ + test.PVs(), + }, + want: map[*test.APIResource][]string{ + test.PVs(): {"/pv-1"}, + }, + }, + { + name: "catch-all filter", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + policyYAML: `version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-1 + resourceFilters: + - kinds: + - '*' + labelSelector: + app: test +`, + tarball: test.NewTarWriter(t). + AddItems("pods", + builder.ForPod("ns-1", "pod-1").ObjectMeta(builder.WithLabels("app", "test")).Result(), + builder.ForPod("ns-1", "pod-2").Result(), + ). + AddItems("deployments.apps", + builder.ForDeployment("ns-1", "deploy-1").ObjectMeta(builder.WithLabels("app", "test")).Result(), + builder.ForDeployment("ns-1", "deploy-2").Result(), + ). + Done(), + apiResources: []*test.APIResource{ + test.Pods(), + test.Deployments(), + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1"}, + test.Deployments(): {"ns-1/deploy-1"}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + h := newHarness(t) + + for _, r := range tc.apiResources { + h.DiscoveryClient.WithAPIResource(r) + } + require.NoError(t, h.restorer.discoveryHelper.Refresh()) + + var resPolicies *resourcepolicies.Policies + if tc.policyYAML != "" { + cm := &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policies", + Namespace: "velero", + }, + Data: map[string]string{ + "policy.yaml": tc.policyYAML, + }, + } + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cm).Build() + restore := tc.restore.DeepCopy() + restore.Namespace = "velero" + restore.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{ + Kind: "configmap", + Name: "test-policies", + } + var err error + resPolicies, err = resourcepolicies.GetResourcePoliciesFromRestore(context.Background(), restore, client, logrus.New()) + require.NoError(t, err) + } + + data := &Request{ + Log: h.log, + Restore: tc.restore, + Backup: tc.backup, + PodVolumeBackups: nil, + VolumeSnapshots: nil, + BackupReader: tc.tarball, + ResPolicies: resPolicies, + } + warnings, errs := h.restorer.Restore( + data, + nil, // restoreItemActions + nil, // volume snapshotter getter + ) + + assertEmptyResults(t, warnings, errs) + assertAPIContents(t, h, tc.want) + }) + } +} From 3deb7d14042eaf7ad88c48e2d5eebd1d257a4370 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 25 Jun 2026 15:24:44 -0700 Subject: [PATCH 010/194] Address review comments on design doc - Use *bool for SkipDefaultResourceModifier to match RestoreSpec conventions - Use boolptr.IsSetToTrue for nil-safe bool check in controller logic - Fix warning message to say "failed to retrieve" instead of "not found" - Add Restore Describe Output subsection covering all resolved states - Only set SkipDefaultResourceModifier from CLI when flag is true Signed-off-by: Shubham Pampattiwar --- design/default-resource-modifier_design.md | 39 ++++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/design/default-resource-modifier_design.md b/design/default-resource-modifier_design.md index 0a7bffef7..5f10b3b6b 100644 --- a/design/default-resource-modifier_design.md +++ b/design/default-resource-modifier_design.md @@ -11,6 +11,7 @@ - [Restore API Change](#restore-api-change) - [Controller Logic](#controller-logic) - [Restore CLI](#restore-cli) + - [Restore Describe Output](#restore-describe-output) - [Install Path](#install-path) - [Curated Default ConfigMap Example](#curated-default-configmap-example) - [Alternatives Considered](#alternatives-considered) @@ -111,10 +112,14 @@ type RestoreSpec struct { // When true, the default modifier is skipped even if configured on the server. // Has no effect when a per-restore ResourceModifier is specified. // +optional - SkipDefaultResourceModifier bool `json:"skipDefaultResourceModifier,omitempty"` + // +nullable + SkipDefaultResourceModifier *bool `json:"skipDefaultResourceModifier,omitempty"` } ``` +This follows the existing RestoreSpec convention where optional booleans use `*bool` with `+nullable` (e.g., `RestorePVs`, `PreserveNodePorts`, `IncludeClusterResources`). +This preserves the ability to distinguish "unset" from "explicit false" if needed in the future. + ### Controller Logic @@ -148,7 +153,7 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf resourceModifiers = r.loadResourceModifierConfigMap( restore, restore.Spec.ResourceModifier.Name, false, ) - } else if r.defaultResourceModifierConfigMap != "" && !restore.Spec.SkipDefaultResourceModifier { + } else if r.defaultResourceModifierConfigMap != "" && !boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) { // No per-restore modifier: apply server default if configured and not skipped. resourceModifiers = r.loadResourceModifierConfigMap( restore, r.defaultResourceModifierConfigMap, true, @@ -176,7 +181,7 @@ func (r *restoreReconciler) loadResourceModifierConfigMap( ); err != nil { if isDefault { r.logger.WithError(err).Warnf( - "Default resource modifier configmap %s/%s not found, skipping", + "Failed to retrieve default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName, ) return nil @@ -258,16 +263,30 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { } ``` -Set the field on the RestoreSpec when building the Restore object: +Set the field on the RestoreSpec when building the Restore object. +Only set it when the flag is true (using `boolptr.True()`) to leave it nil otherwise, consistent with how other `*bool` fields are handled: ```go -Spec: api.RestoreSpec{ - // ... existing fields ... - SkipDefaultResourceModifier: o.SkipDefaultResourceModifier, +if o.SkipDefaultResourceModifier { + restore.Spec.SkipDefaultResourceModifier = boolptr.True() } ``` -Update the restore describer in `pkg/cmd/util/output/restore_describer.go` to display the field when set. +### Restore Describe Output + +Update the restore describer in `pkg/cmd/util/output/restore_describer.go` to show which resource modifier was applied and its source. +The describe output should reflect the resolved state: + +- When the default resource modifier was applied, display its ConfigMap name and source: + ``` + Default Resource Modifier: default-restore-resource-modifiers + ``` +- When the default was skipped because `SkipDefaultResourceModifier` is true: + ``` + Default Resource Modifier: skipped (SkipDefaultResourceModifier=true) + ``` +- When the default was skipped because a per-restore modifier was specified, no extra output is needed since the per-restore modifier is already displayed under the existing `Resource Modifier` field. +- When the default was ignored due to a validation or retrieval error, the warning is already logged to the restore log. The describe output should not surface transient errors. ### Install Path @@ -298,7 +317,7 @@ Add a builder method to `pkg/builder/restore_builder.go`: ```go func (b *RestoreBuilder) SkipDefaultResourceModifier(val bool) *RestoreBuilder { - b.object.Spec.SkipDefaultResourceModifier = val + b.object.Spec.SkipDefaultResourceModifier = &val return b } ``` @@ -396,5 +415,3 @@ The new `SkipDefaultResourceModifier` field in RestoreSpec defaults to `false` a - Should additional CNI annotations (Calico, Cilium) be included in the curated example ConfigMap? Feedback from the community on which annotations are commonly problematic would be helpful. -- Should `velero restore describe` show which resource modifier was used (default vs per-restore)? -This would improve observability but is a minor enhancement that can be added separately. From f38bc20a0aec2a9e7dac3506061698b298236c0a Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 25 Jun 2026 17:01:17 +0800 Subject: [PATCH 011/194] block uploader snapshot implementation Signed-off-by: Lyndon-Li --- pkg/uploader/block/dev_linux.go | 2 +- pkg/uploader/block/dev_other.go | 2 +- pkg/uploader/block/snapshot.go | 18 +++++++++--------- pkg/uploader/block/snapshot_test.go | 19 ++++++++++++------- pkg/uploader/block/uploader.go | 10 +++++----- pkg/uploader/block/uploader_test.go | 4 ++-- pkg/uploader/provider/block_test.go | 10 +++++++--- 7 files changed, 37 insertions(+), 28 deletions(-) diff --git a/pkg/uploader/block/dev_linux.go b/pkg/uploader/block/dev_linux.go index 6383060fb..85b378c55 100644 --- a/pkg/uploader/block/dev_linux.go +++ b/pkg/uploader/block/dev_linux.go @@ -22,7 +22,7 @@ package block import ( "os" - "github.com/pkg/errors" + "github.com/cockroachdb/errors" ) // implement in following PRs diff --git a/pkg/uploader/block/dev_other.go b/pkg/uploader/block/dev_other.go index 60689a3d6..c8a55cab2 100644 --- a/pkg/uploader/block/dev_other.go +++ b/pkg/uploader/block/dev_other.go @@ -25,5 +25,5 @@ import ( ) func openBlockDevice(_ string, _ bool) (*os.File, error) { - return nil, fmt.Errorf("block mode is not supported for Windows") + return nil, fmt.Errorf("block mode is not supported for non-linux platforms") } diff --git a/pkg/uploader/block/snapshot.go b/pkg/uploader/block/snapshot.go index 41d42ba36..30626da53 100644 --- a/pkg/uploader/block/snapshot.go +++ b/pkg/uploader/block/snapshot.go @@ -23,7 +23,7 @@ import ( "path/filepath" "time" - "github.com/pkg/errors" + "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/vmware-tanzu/velero/pkg/cbtservice" @@ -41,9 +41,9 @@ type parentBackupInfo struct { } // Backup backup specific sourcePath and update progress -func Backup(ctx context.Context, blkup Uploader, repoWriter udmrepo.BackupRepo, sourcePath string, realSource string, cbtSource cbtservice.SourceInfo, - forceFull bool, parentSnapshot string, cbtservice cbtservice.Service, uploaderCfg map[string]string, tags map[string]string, log logrus.FieldLogger) (uploader.SnapshotInfo, bool, error) { - if blkup == nil { +func Backup(ctx context.Context, blkUp Uploader, repoWriter udmrepo.BackupRepo, sourcePath string, realSource string, cbtSource cbtservice.SourceInfo, + forceFull bool, parentSnapshot string, cbtService cbtservice.Service, uploaderCfg map[string]string, tags map[string]string, log logrus.FieldLogger) (uploader.SnapshotInfo, bool, error) { + if blkUp == nil { return uploader.SnapshotInfo{}, false, errors.New("get empty block uploader") } @@ -77,7 +77,7 @@ func Backup(ctx context.Context, blkup Uploader, repoWriter udmrepo.BackupRepo, return uploader.SnapshotInfo{}, false, errors.Wrapf(err, "error reset pos of block device %s", source) } - snapID, backupSize, err := snapshotSource(ctx, repoWriter, blkup, sourceInfo, forceFull, parentSnapshot, cbtSource, cbtservice, tags, uploaderCfg, log, "Block Uploader") + snapID, backupSize, err := snapshotSource(ctx, repoWriter, blkUp, sourceInfo, forceFull, parentSnapshot, cbtSource, cbtService, tags, uploaderCfg, log, "Block Uploader") snapshotInfo := uploader.SnapshotInfo{ ID: snapID, Size: sourceInfo.size, @@ -95,7 +95,7 @@ func snapshotSource( forceFull bool, parentSnapshot string, cbtSource cbtservice.SourceInfo, - cbtservice cbtservice.Service, + cbtService cbtservice.Service, snapshotTags map[string]string, uploaderCfg map[string]string, log logrus.FieldLogger, @@ -108,7 +108,7 @@ func snapshotSource( bitmap := cbt.NewBitmap(blockSize, uint64(source.size), cbtSource.Snapshot, parentBackup.changeID, parentBackup.volumeID) - err := cbt.SetBitmapOrFull(ctx, cbtservice, bitmap) + err := cbt.SetBitmapOrFull(ctx, cbtService, bitmap) if err != nil { parentBackup.parentObject = "" log.WithError(err).Warnf("Failed to create CBT with source %v, fallback to real full backup", cbtSource) @@ -193,7 +193,7 @@ func getParentBackupInfo(ctx context.Context, rep udmrepo.BackupRepo, forceFull } // Restore restore specific sourcePath with given snapshotID and update progress -func Restore(ctx context.Context, blkup Uploader, rep udmrepo.BackupRepo, snapshotID, dest string, uploaderCfg map[string]string, log logrus.FieldLogger) (int64, error) { +func Restore(ctx context.Context, blkUp Uploader, rep udmrepo.BackupRepo, snapshotID, dest string, uploaderCfg map[string]string, log logrus.FieldLogger) (int64, error) { log.Info("Start to restore...") snapshot, err := rep.GetSnapshot(ctx, udmrepo.ID(snapshotID)) @@ -218,7 +218,7 @@ func Restore(ctx context.Context, blkup Uploader, rep udmrepo.BackupRepo, snapsh return 0, errors.Wrapf(err, "error opening block device '%s'", destPath) } - size, err := blkup.Restore(snapshot, destInfo{dev: destDev, path: destPath}, bitmap.Iterator(), uploaderCfg) + size, err := blkUp.Restore(snapshot, destInfo{dev: destDev, path: destPath}, bitmap.Iterator(), uploaderCfg) if err != nil { return 0, errors.Wrapf(err, "error restoring to block dev %s", destPath) } diff --git a/pkg/uploader/block/snapshot_test.go b/pkg/uploader/block/snapshot_test.go index 5d17ee0f4..8f6338311 100644 --- a/pkg/uploader/block/snapshot_test.go +++ b/pkg/uploader/block/snapshot_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - "github.com/pkg/errors" + "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -59,7 +59,7 @@ func testLog() logrus.FieldLogger { func tempFile(t *testing.T, content string) *os.File { t.Helper() - f, err := os.CreateTemp("", "blktest-*") + f, err := os.CreateTemp(t.TempDir(), "blktest-*") require.NoError(t, err) if content != "" { _, err = f.WriteString(content) @@ -93,6 +93,7 @@ func TestBackup(t *testing.T) { { name: "SnapshotSource error propagates", setupOpenDev: func(t *testing.T) *os.File { + t.Helper() return tempFile(t, "") }, setupMocks: func(blkup *mockUploader, _ *udmrepomocks.BackupRepo) { @@ -104,6 +105,7 @@ func TestBackup(t *testing.T) { { name: "success returns correct SnapshotInfo", setupOpenDev: func(t *testing.T) *os.File { + t.Helper() return tempFile(t, "test-block-data") }, setupMocks: func(blkup *mockUploader, repo *udmrepomocks.BackupRepo) { @@ -113,9 +115,10 @@ func TestBackup(t *testing.T) { repo.On("Flush", mock.Anything).Return(nil) }, checkInfo: func(t *testing.T, info uploader.SnapshotInfo) { + t.Helper() assert.Equal(t, "snap-001", info.ID) assert.Equal(t, int64(8), info.IncrementalSize) - assert.Greater(t, info.Size, int64(0)) + assert.Positive(t, info.Size) }, }, } @@ -157,7 +160,7 @@ func TestBackup(t *testing.T) { if tc.expectedErrStr != "" { require.Error(t, err) - assert.ErrorContains(t, err, tc.expectedErrStr) + require.ErrorContains(t, err, tc.expectedErrStr) } else { require.NoError(t, err) assert.False(t, isEmpty) @@ -260,7 +263,7 @@ func TestSnapshotSource(t *testing.T) { if tc.expectedErrStr != "" { require.Error(t, err) - assert.ErrorContains(t, err, tc.expectedErrStr) + require.ErrorContains(t, err, tc.expectedErrStr) } else { require.NoError(t, err) assert.Equal(t, tc.expectedSnapID, snapID) @@ -530,7 +533,7 @@ func TestFindPreviousSnapshot(t *testing.T) { if tc.expectedErrStr != "" { require.Error(t, err) - assert.ErrorContains(t, err, tc.expectedErrStr) + require.ErrorContains(t, err, tc.expectedErrStr) } else { require.NoError(t, err) assert.Equal(t, udmrepo.ID(tc.expectedID), snap.RootObject.ID) @@ -574,6 +577,7 @@ func TestRestore(t *testing.T) { Return(int64(0), errors.New("restore I/O error")) }, setupOpenDev: func(t *testing.T) *os.File { + t.Helper() return tempFile(t, "") }, expectedErrStr: "error restoring to block dev", @@ -587,6 +591,7 @@ func TestRestore(t *testing.T) { Return(int64(4096), nil) }, setupOpenDev: func(t *testing.T) *os.File { + t.Helper() return tempFile(t, "") }, expectedSize: 4096, @@ -616,7 +621,7 @@ func TestRestore(t *testing.T) { if tc.expectedErrStr != "" { require.Error(t, err) - assert.ErrorContains(t, err, tc.expectedErrStr) + require.ErrorContains(t, err, tc.expectedErrStr) assert.Equal(t, int64(0), size) } else { require.NoError(t, err) diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 7f089bd01..233d72a17 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -20,7 +20,7 @@ import ( "context" "os" - "github.com/pkg/errors" + "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" @@ -60,14 +60,14 @@ func loadObjectFromSnapshot(ctx context.Context, rep udmrepo.BackupRepo, snapsho return "", errors.New("snapshot is empty") } - parentMeta, err := rep.ReadMetadata(ctx, snapshot.RootObject.ID) + meta, err := rep.ReadMetadata(ctx, snapshot.RootObject.ID) if err != nil { return "", errors.Wrapf(err, "error reading snapshot metadata for %s", snapshot.Description) } - if len(parentMeta.SubObjects) != 1 { - return "", errors.Errorf("unexpected number of bdev object (%d) for snapshot %s", len(parentMeta.SubObjects), snapshot.Description) + if len(meta.SubObjects) != 1 { + return "", errors.Errorf("unexpected number of bdev object (%d) for snapshot %s", len(meta.SubObjects), snapshot.Description) } - return parentMeta.SubObjects[0].ID, nil + return meta.SubObjects[0].ID, nil } diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index ea1986197..8209569e1 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -20,7 +20,7 @@ import ( "context" "testing" - "github.com/pkg/errors" + "github.com/cockroachdb/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -101,7 +101,7 @@ func TestLoadObjectFromSnapshot(t *testing.T) { if tc.expectedErrStr != "" { require.Error(t, err) - assert.ErrorContains(t, err, tc.expectedErrStr) + require.ErrorContains(t, err, tc.expectedErrStr) assert.Empty(t, id) } else { require.NoError(t, err) diff --git a/pkg/uploader/provider/block_test.go b/pkg/uploader/provider/block_test.go index e7af93855..8ec445168 100644 --- a/pkg/uploader/provider/block_test.go +++ b/pkg/uploader/provider/block_test.go @@ -280,6 +280,7 @@ func TestBlockProviderRunBackup(t *testing.T) { mockBackupResult: uploader.SnapshotInfo{ID: "snap-tags"}, expectedID: "snap-tags", checkCaptures: func(t *testing.T, _ string, tags map[string]string) { + t.Helper() assert.Equal(t, requestorType, tags[uploader.SnapshotRequesterTag]) assert.Equal(t, uploader.BlockType, tags[uploader.SnapshotUploaderTag]) }, @@ -292,6 +293,7 @@ func TestBlockProviderRunBackup(t *testing.T) { mockBackupResult: uploader.SnapshotInfo{ID: "snap-source"}, expectedID: "snap-source", checkCaptures: func(t *testing.T, realSource string, _ map[string]string) { + t.Helper() assert.Equal(t, requestorType+"/"+uploader.BlockType+"/my-volume", realSource) }, }, @@ -303,7 +305,8 @@ func TestBlockProviderRunBackup(t *testing.T) { mockBackupResult: uploader.SnapshotInfo{ID: "snap-nosource"}, expectedID: "snap-nosource", checkCaptures: func(t *testing.T, realSource string, _ map[string]string) { - assert.Equal(t, "", realSource) + t.Helper() + assert.Empty(t, realSource) }, }, } @@ -349,7 +352,7 @@ func TestBlockProviderRunBackup(t *testing.T) { if tc.expectError { require.Error(t, err) if tc.expectedErrStr != "" { - assert.ErrorContains(t, err, tc.expectedErrStr) + require.ErrorContains(t, err, tc.expectedErrStr) } } else { require.NoError(t, err) @@ -416,6 +419,7 @@ func TestBlockProviderRunRestore(t *testing.T) { mockRestoreSize: 512, expectedSize: 512, checkCaptures: func(t *testing.T, snapshotID, volumePath string) { + t.Helper() assert.Equal(t, "snap-fwd", snapshotID) assert.Equal(t, "/dev/sdc", volumePath) }, @@ -452,7 +456,7 @@ func TestBlockProviderRunRestore(t *testing.T) { if tc.expectError { require.Error(t, err) if tc.expectedErrStr != "" { - assert.ErrorContains(t, err, tc.expectedErrStr) + require.ErrorContains(t, err, tc.expectedErrStr) } assert.Equal(t, int64(0), size) } else { From 82dbef2cd9bee892ee42d4c6c039932d9eb5f4f7 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 30 Jun 2026 11:06:46 +0800 Subject: [PATCH 012/194] block uploader snapshot implementation Signed-off-by: Lyndon-Li --- pkg/uploader/provider/block.go | 6 +++++- pkg/uploader/provider/block_test.go | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/uploader/provider/block.go b/pkg/uploader/provider/block.go index 427d3fae3..4bc26f9e1 100644 --- a/pkg/uploader/provider/block.go +++ b/pkg/uploader/provider/block.go @@ -105,7 +105,7 @@ func (bp *blockProvider) RunBackup( uploaderCfg map[string]string, updater uploader.ProgressUpdater) (string, bool, int64, int64, error) { if updater == nil { - return "", false, 0, 0, errors.New("Need to initial backup progress updater first") + return "", false, 0, 0, errors.New("backup progress updater is invalid") } if path == "" { @@ -160,6 +160,10 @@ func (bp *blockProvider) RunRestore( volMode uploader.PersistentVolumeMode, uploaderCfg map[string]string, updater uploader.ProgressUpdater) (int64, error) { + if updater == nil { + return 0, errors.New("restore progress updater is invalid") + } + log := bp.log.WithFields(logrus.Fields{ "snapshotID": snapshotID, "volumePath": volumePath, diff --git a/pkg/uploader/provider/block_test.go b/pkg/uploader/provider/block_test.go index 8ec445168..ad8f68b52 100644 --- a/pkg/uploader/provider/block_test.go +++ b/pkg/uploader/provider/block_test.go @@ -224,7 +224,7 @@ func TestBlockProviderRunBackup(t *testing.T) { path: "/dev/sda", updater: nil, expectError: true, - expectedErrStr: "Need to initial backup progress updater first", + expectedErrStr: "backup progress updater is invalid", skipMock: true, }, { @@ -385,6 +385,12 @@ func TestBlockProviderRunRestore(t *testing.T) { expectedErrStr string checkCaptures func(*testing.T, string, string) }{ + { + name: "nil updater returns error", + updater: nil, + expectError: true, + expectedErrStr: "restore progress updater is invalid", + }, { name: "success returns size and updates progress", snapshotID: "snap-001", From 8df8709a8a522c9477b615eee609e2cd67834fc6 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 30 Jun 2026 14:14:23 +0800 Subject: [PATCH 013/194] block uploader snapshot implementation Signed-off-by: Lyndon-Li --- pkg/uploader/provider/block.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/uploader/provider/block.go b/pkg/uploader/provider/block.go index 4bc26f9e1..9135bb67b 100644 --- a/pkg/uploader/provider/block.go +++ b/pkg/uploader/provider/block.go @@ -118,6 +118,8 @@ func (bp *blockProvider) RunBackup( "parentSnapshot": parentSnapshot, }) + log.Infof("Run block backup, CBT source info: %v", cbtParam.Source) + blkUploader := block.NewUploader(ctx, bp.bkRepo, updater, log) if tags == nil { From df21463629fd31c103aa6c2a2c553e2898b57662 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 14 May 2026 10:23:08 +0800 Subject: [PATCH 014/194] block uploader backup implementation Signed-off-by: Lyndon-Li --- pkg/repository/udmrepo/kopialib/lib_repo.go | 2 +- .../udmrepo/kopialib/lib_repo_ex_test.go | 2 +- pkg/uploader/block/uploader.go | 267 ++++++++++++- pkg/uploader/block/uploader_test.go | 353 +++++++++++++++++- .../kopialib => util}/freelist/freelist.go | 0 .../freelist/freelist_test.go | 0 6 files changed, 617 insertions(+), 7 deletions(-) rename pkg/{repository/udmrepo/kopialib => util}/freelist/freelist.go (100%) rename pkg/{repository/udmrepo/kopialib => util}/freelist/freelist_test.go (100%) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo.go b/pkg/repository/udmrepo/kopialib/lib_repo.go index 29ff02eea..151bf1cb2 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo.go @@ -44,7 +44,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/kopia" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/backend" - "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/freelist" + "github.com/vmware-tanzu/velero/pkg/util/freelist" ) type kopiaRepoService struct { diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go index fdaeb9f69..a42c02c14 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go @@ -15,8 +15,8 @@ import ( "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" repomocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/backend/mocks" - "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/freelist" velerotest "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/util/freelist" ) type mockDirectRepository struct { diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 233d72a17..318e4a7f3 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -18,7 +18,10 @@ package block import ( "context" + "io" "os" + "runtime" + "strings" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" @@ -26,12 +29,14 @@ import ( "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" "github.com/vmware-tanzu/velero/pkg/uploader" cbt "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types" + "github.com/vmware-tanzu/velero/pkg/util/freelist" ) var ErrCanceled = errors.New("uploader is canceled") const ( - blockSize = (1 << 20) + blockSize = (1 << 20) + bufferSize = 100 << 20 ) type sourceInfo struct { @@ -50,9 +55,265 @@ type Uploader interface { Restore(udmrepo.Snapshot, destInfo, cbt.Iterator, map[string]string) (int64, error) } -// implement in following PRs +type blockUploader struct { + ctx context.Context + repoWriter udmrepo.BackupRepo + progress uploader.ProgressUpdater + log logrus.FieldLogger +} + func NewUploader(ctx context.Context, repoWriter udmrepo.BackupRepo, progress uploader.ProgressUpdater, log logrus.FieldLogger) Uploader { - return nil + return &blockUploader{ + ctx: ctx, + repoWriter: repoWriter, + progress: progress, + log: log, + } +} + +func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitmap cbt.Iterator, configs map[string]string) (udmrepo.Snapshot, int64, error) { + snapStart := bu.repoWriter.Time() + + if bitmap == nil { + return udmrepo.Snapshot{}, 0, errors.New("bitmap is not available") + } + + backupMode := udmrepo.ObjectDataBackupModeInc + if parentObject == "" { + backupMode = udmrepo.ObjectDataBackupModeFull + } + + destObj, err := bu.repoWriter.NewObjectWriter(bu.ctx, udmrepo.ObjectWriteOptions{ + Description: "BDEV:" + getObjectName(source.realSource), + DataType: udmrepo.ObjectDataTypeData, + AccessMode: udmrepo.ObjectDataAccessModeBlock, + ParentObject: parentObject, + BackupMode: backupMode, + AsyncWrites: runtime.NumCPU(), + }) + if err != nil { + return udmrepo.Snapshot{}, 0, errors.Wrap(err, "error creating object writer") + } + + defer destObj.Close() + + id, backupSize, objectSize, err := bu.backupObject(source.dev, destObj, bitmap, source.size) + if err != nil { + return udmrepo.Snapshot{}, 0, errors.Wrap(err, "error to backup file with incremental") + } + + entryId, err := bu.repoWriter.WriteMetadata(bu.ctx, &udmrepo.Metadata{ + SubObjects: []udmrepo.ObjectMetadata{ + { + ID: id, + Name: getObjectName(source.realSource), + Type: udmrepo.ObjectDataTypeData, + Size: objectSize, + Permissions: 0o777, + }, + }, + }, + udmrepo.ObjectWriteOptions{ + Description: "bdev-root", + }) + if err != nil { + return udmrepo.Snapshot{}, 0, errors.Wrap(err, "error to write metadata") + } + + snapEnd := bu.repoWriter.Time() + + return udmrepo.Snapshot{ + Source: source.realSource, + StartTime: snapStart, + EndTime: snapEnd, + Description: source.realSource, + RootObject: udmrepo.ObjectMetadata{ + ID: entryId, + Name: "bdev-root", + Type: udmrepo.ObjectDataTypeMetadata, + Permissions: 0o777, + }, + }, backupSize, nil +} + +// TODO implement in following PRs +func (bu *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bitmap cbt.Iterator, configs map[string]string) (int64, error) { + return 0, nil +} + +func (bu *blockUploader) backupObject(dev *os.File, dest udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (udmrepo.ID, int64, int64, error) { + backupSize, objectSize, err := bu.backupData(dev, dest, bitmap, totalLength) + if err != nil { + return "", backupSize, objectSize, errors.Wrap(err, "error copying file data incremental") + } + + id, err := dest.Result() + return id, backupSize, objectSize, err +} + +type readResult struct { + buffer []byte + offset int64 + err error +} + +func (r *readResult) resetBuffer(list *freelist.FreeList) { + if r.buffer != nil { + list.Return(r.buffer) + r.buffer = nil + } +} + +func (bu *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (int64, int64, error) { + blockSize := bitmap.BlockSize() + list := freelist.New(bufferSize, int(blockSize)) + resultChan := make(chan readResult, list.Capacity()) + totalCount := bitmap.Count() + aligned := (totalLength + int64(blockSize) - 1) / int64(blockSize) * int64(blockSize) + + quit := make(chan struct{}) + defer close(quit) + + go func() { + defer close(resultChan) + + offset, valid := bitmap.Next() + var buffer []byte + for valid { + select { + case <-bu.ctx.Done(): + return + case <-quit: + return + case buffer = <-list.Chunks(): + } + + length := blockSize + if offset+uint64(length) > uint64(totalLength) { + length = uint(uint64(totalLength) - offset) + clear(buffer) + } + + readBytes, err := reader.ReadAt(buffer[:length], int64(offset)) + if err == nil && readBytes <= 0 { + err = io.ErrUnexpectedEOF + } + + r := readResult{ + buffer: buffer, + offset: int64(offset), + err: err, + } + + if r.err != nil { + r.resetBuffer(list) + } + + resultChan <- r + + if r.err != nil { + return + } + + offset, valid = bitmap.Next() + } + }() + + var lastPos int64 + var result readResult + var written int64 + var curCount int64 + var writeErr error + var readerRunning bool + + for curCount < int64(totalCount) { + select { + case <-bu.ctx.Done(): + writeErr = ErrCanceled + case result, readerRunning = <-resultChan: + if !readerRunning { + if bu.ctx.Err() != nil { + writeErr = ErrCanceled + } else { + writeErr = io.ErrUnexpectedEOF + } + } + } + + if writeErr != nil { + break + } + + if result.err != nil { + writeErr = result.err + break + } + + n, err := writer.WriteAt(result.buffer, result.offset) + if err != nil { + writeErr = err + break + } + + if blockSize != uint(n) { + writeErr = io.ErrShortWrite + break + } + + written += int64(blockSize) + lastPos = result.offset + int64(blockSize) + result.resetBuffer(list) + curCount++ + + bu.progress.UpdateProgress(&uploader.Progress{BytesDone: lastPos, TotalBytes: aligned}) + } + + result.resetBuffer(list) + + if writeErr != nil { + return written, aligned, writeErr + } + + if lastPos < aligned { + s, err := copyTailData(reader, writer, totalLength, int64(blockSize)) + if err != nil { + return written, aligned, errors.Wrapf(err, "unable to write tail data at %v", lastPos) + } + + written += s + + bu.progress.UpdateProgress(&uploader.Progress{BytesDone: aligned, TotalBytes: aligned}) + } + + return written, aligned, nil +} + +func copyTailData(source io.ReaderAt, writer udmrepo.ObjectWriter, totalLength int64, blockSize int64) (int64, error) { + roundUp := (totalLength + blockSize - 1) / blockSize * blockSize + roundDown := totalLength / blockSize * blockSize + length := totalLength - roundDown + + if length == 0 { + if _, err := writer.WriteAt(nil, roundUp); err != nil { + return -1, errors.Wrapf(err, "error writing sparse to %v", roundUp) + } + } else { + buffer := make([]byte, blockSize) + if _, err := source.ReadAt(buffer[:length], roundDown); err != nil { + return -1, errors.Wrapf(err, "error reading tail data with length %v", length) + } + + if _, err := writer.WriteAt(buffer, roundDown); err != nil { + return -1, errors.Wrapf(err, "error writing tail data at %v", roundDown) + } + } + + return length, nil +} + +func getObjectName(source string) string { + s := strings.ReplaceAll(source, "/", "-") + return strings.ReplaceAll(s, "\\", "-") } func loadObjectFromSnapshot(ctx context.Context, rep udmrepo.BackupRepo, snapshot *udmrepo.Snapshot) (udmrepo.ID, error) { diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index 8209569e1..d6e2e3d90 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -5,7 +5,7 @@ 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 + 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, @@ -17,18 +17,367 @@ limitations under the License. package block import ( + "bytes" "context" + "io" + "os" "testing" + "time" - "github.com/cockroachdb/errors" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" udmrepomocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/mocks" + "github.com/vmware-tanzu/velero/pkg/uploader" + cbt "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types" + cbtmocks "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types/mocks" ) +type mockProgressUpdater struct { + mock.Mock +} + +func (m *mockProgressUpdater) UpdateProgress(p *uploader.Progress) { + m.Called(p) +} + +func TestNewUploader(t *testing.T) { + ctx := context.Background() + repoWriter := udmrepomocks.NewBackupRepo(t) + progress := &mockProgressUpdater{} + log := logrus.New() + + uploader := NewUploader(ctx, repoWriter, progress, log) + + bu, ok := uploader.(*blockUploader) + assert.True(t, ok) + assert.Equal(t, ctx, bu.ctx) + assert.Equal(t, repoWriter, bu.repoWriter) + assert.Equal(t, progress, bu.progress) + assert.Equal(t, log, bu.log) +} + +func TestGetObjectName(t *testing.T) { + testCases := []struct { + name string + source string + expected string + }{ + { + name: "no slashes", + source: "test", + expected: "test", + }, + { + name: "unix path", + source: "/var/lib/kubelet/pods/uuid/volumes/test", + expected: "-var-lib-kubelet-pods-uuid-volumes-test", + }, + { + name: "windows path", + source: `c:\var\lib\kubelet\pods\uuid\volumes\test`, + expected: `c:-var-lib-kubelet-pods-uuid-volumes-test`, + }, + { + name: "mixed slashes", + source: `c:\var/lib\kubelet/pods`, + expected: `c:-var-lib-kubelet-pods`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := getObjectName(tc.source) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestCopyTailData(t *testing.T) { + testCases := []struct { + name string + totalLength int64 + blockSize int64 + sourceData []byte + writeErr error + readErr error + expected int64 + expectErr bool + }{ + { + name: "tail length 0", + totalLength: 2048, + blockSize: 1024, + expected: 0, + }, + { + name: "tail length 512 with 1024 block size", + totalLength: 1536, + blockSize: 1024, + sourceData: make([]byte, 1536), + expected: 512, + }, + { + name: "tail length with write error", + totalLength: 1536, + blockSize: 1024, + sourceData: make([]byte, 1536), + writeErr: errors.New("write error"), + expectErr: true, + }, + { + name: "tail length 0 with sparse write error", + totalLength: 2048, + blockSize: 1024, + writeErr: errors.New("write error"), + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + writer := udmrepomocks.NewObjectWriter(t) + var source io.ReaderAt + + if tc.totalLength%tc.blockSize == 0 { + writer.On("WriteAt", []byte(nil), tc.totalLength).Return(0, tc.writeErr) + } else { + length := tc.totalLength - (tc.totalLength/tc.blockSize)*tc.blockSize + paddedData := make([]byte, tc.blockSize) + copy(paddedData[:length], tc.sourceData) + + source = bytes.NewReader(tc.sourceData) + writer.On("WriteAt", paddedData, (tc.totalLength/tc.blockSize)*tc.blockSize).Return(int(tc.blockSize), tc.writeErr) + } + + n, err := copyTailData(source, writer, tc.totalLength, tc.blockSize) + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, n) + } + }) + } +} + +func TestBlockUploaderBackup(t *testing.T) { + testCases := []struct { + name string + nilBitmap bool + createObjErr error + writeMetaErr error + writeObjErr error + parentObj udmrepo.ID + cancelCtx bool + cancelInProgress bool + readDataErr bool + shortWrite bool + fewerBlocks bool + expectErr bool + expectErrStr string + }{ + { + name: "nil bitmap", + nilBitmap: true, + expectErr: true, + }, + { + name: "canceled context", + cancelCtx: true, + expectErr: true, + expectErrStr: "uploader is canceled", + }, + { + name: "canceled in progress", + cancelInProgress: true, + expectErr: true, + expectErrStr: "error copying file data incremental: uploader is canceled", + }, + { + name: "create object writer err", + createObjErr: errors.New("create obj err"), + expectErr: true, + }, + { + name: "read data err", + readDataErr: true, + expectErr: true, + expectErrStr: "EOF", + }, + { + name: "short write err", + shortWrite: true, + expectErr: true, + expectErrStr: "short write", + }, + { + name: "unexpected EOF fewer blocks", + fewerBlocks: true, + expectErr: true, + expectErrStr: "unexpected EOF", + }, + { + name: "write meta err", + writeMetaErr: errors.New("write meta err"), + expectErr: true, + }, + { + name: "success full backup", + parentObj: "", + expectErr: false, + }, + { + name: "success inc backup", + parentObj: "parent-01", + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + + if tc.cancelCtx { + cancel() + } else if tc.cancelInProgress { + go func() { + time.Sleep(100 * time.Millisecond) + cancel() + }() + } else { + defer cancel() + } + + repoWriter := udmrepomocks.NewBackupRepo(t) + progress := &mockProgressUpdater{} + progress.On("UpdateProgress", mock.Anything).Return() + log := logrus.New() + log.Out = io.Discard + + bu := NewUploader(ctx, repoWriter, progress, log) + + f, err := os.CreateTemp("", "blktest-*") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + + if tc.cancelInProgress { + require.NoError(t, f.Truncate(2*1048576)) + } else if tc.readDataErr { + // Don't truncate so that reading hits EOF immediately + } else { + require.NoError(t, f.Truncate(1048576)) + } + + fi, err := f.Stat() + require.NoError(t, err) + + srcInfo := sourceInfo{ + dev: f, + realSource: "/data/volume1", + size: fi.Size(), + } + + if tc.readDataErr { + srcInfo.size = 1048576 + } + + repoWriter.On("Time").Return(time.Now()) + + var iterator cbt.Iterator + if !tc.nilBitmap { + iterMock := cbtmocks.NewIterator(t) + iterator = iterMock + + backupMode := udmrepo.ObjectDataBackupModeInc + if tc.parentObj == "" { + backupMode = udmrepo.ObjectDataBackupModeFull + } + + objWriter := udmrepomocks.NewObjectWriter(t) + if tc.createObjErr == nil { + objWriter.On("Close").Return(nil) + + if tc.cancelInProgress { + iterMock.On("BlockSize").Return(uint(1048576)) + iterMock.On("Count").Return(uint64(1000)) + iterMock.On("Next").Return(uint64(0), true) + + objWriter.On("WriteAt", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + <-ctx.Done() + }).Return(1048576, nil) + objWriter.On("Result").Return(udmrepo.ID(""), errors.New("write failed")).Maybe() + } else if tc.cancelCtx { + iterMock.On("BlockSize").Return(uint(1048576)) + iterMock.On("Count").Return(uint64(1)) + iterMock.On("Next").Return(uint64(0), true) + + objWriter.On("Result").Return(udmrepo.ID(""), errors.New("write failed")).Maybe() + } else if tc.shortWrite { + iterMock.On("BlockSize").Return(uint(1048576)) + iterMock.On("Count").Return(uint64(1)) + iterMock.On("Next").Return(uint64(0), true) + + objWriter.On("WriteAt", mock.Anything, mock.Anything).Return(512, nil) + objWriter.On("Result").Return(udmrepo.ID(""), errors.New("write failed")).Maybe() + } else if tc.fewerBlocks { + iterMock.On("BlockSize").Return(uint(1048576)) + iterMock.On("Count").Return(uint64(5)) + iterMock.On("Next").Return(uint64(0), false) + + objWriter.On("Result").Return(udmrepo.ID(""), errors.New("write failed")).Maybe() + } else if tc.readDataErr { + iterMock.On("BlockSize").Return(uint(1048576)) + iterMock.On("Count").Return(uint64(1)) + iterMock.On("Next").Return(uint64(0), true) + + objWriter.On("Result").Return(udmrepo.ID(""), errors.New("write failed")).Maybe() + } else { + // Setup backupData sequence: next returns false immediately + iterMock.On("BlockSize").Return(uint(1048576)) + iterMock.On("Count").Return(uint64(0)) + iterMock.On("Next").Return(uint64(0), false) + + if tc.writeObjErr != nil { + objWriter.On("WriteAt", mock.Anything, mock.Anything).Return(0, tc.writeObjErr) + objWriter.On("Result").Return(udmrepo.ID(""), errors.New("write failed")) + } else { + objWriter.On("WriteAt", mock.Anything, mock.Anything).Return(1048576, nil) + objWriter.On("Result").Return(udmrepo.ID("obj-01"), nil) + repoWriter.On("WriteMetadata", mock.Anything, mock.Anything, mock.Anything).Return(udmrepo.ID("meta-01"), tc.writeMetaErr) + } + } + } + + repoWriter.On("NewObjectWriter", mock.Anything, mock.MatchedBy(func(opt udmrepo.ObjectWriteOptions) bool { + return opt.Description == "BDEV:-data-volume1" && opt.BackupMode == backupMode + })).Return(objWriter, tc.createObjErr) + } + + snap, size, err := bu.Backup(srcInfo, tc.parentObj, iterator, nil) + + if tc.expectErr { + assert.Error(t, err) + if tc.expectErrStr != "" { + assert.Contains(t, err.Error(), tc.expectErrStr) + } + } else { + assert.NoError(t, err) + assert.Equal(t, "/data/volume1", snap.Source) + assert.Equal(t, udmrepo.ID("meta-01"), snap.RootObject.ID) + assert.Equal(t, int64(0), size) + } + }) + } +} + func TestLoadObjectFromSnapshot(t *testing.T) { testCases := []struct { name string diff --git a/pkg/repository/udmrepo/kopialib/freelist/freelist.go b/pkg/util/freelist/freelist.go similarity index 100% rename from pkg/repository/udmrepo/kopialib/freelist/freelist.go rename to pkg/util/freelist/freelist.go diff --git a/pkg/repository/udmrepo/kopialib/freelist/freelist_test.go b/pkg/util/freelist/freelist_test.go similarity index 100% rename from pkg/repository/udmrepo/kopialib/freelist/freelist_test.go rename to pkg/util/freelist/freelist_test.go From 39c745ef612eb81b77ebe64d854f8d27573b5e53 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 2 Jun 2026 17:25:41 +0800 Subject: [PATCH 015/194] set totalSize from uploader Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 318e4a7f3..d58b004b9 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -127,6 +127,7 @@ func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitm StartTime: snapStart, EndTime: snapEnd, Description: source.realSource, + TotalSize: objectSize, RootObject: udmrepo.ObjectMetadata{ ID: entryId, Name: "bdev-root", From 502ec5f08666429c41011ffc4fc90bde3b4c344d Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 4 Jun 2026 15:56:57 +0800 Subject: [PATCH 016/194] remove leading and trailing separator Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index d58b004b9..bb8195e31 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -314,7 +314,8 @@ func copyTailData(source io.ReaderAt, writer udmrepo.ObjectWriter, totalLength i func getObjectName(source string) string { s := strings.ReplaceAll(source, "/", "-") - return strings.ReplaceAll(s, "\\", "-") + s = strings.ReplaceAll(s, "\\", "-") + return strings.Trim(s, "-") } func loadObjectFromSnapshot(ctx context.Context, rep udmrepo.BackupRepo, snapshot *udmrepo.Snapshot) (udmrepo.ID, error) { From 8d23c7e813183f3db67cd04f9a047265d5e93638 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 30 Jun 2026 16:27:18 +0800 Subject: [PATCH 017/194] block uploader backup implementation Signed-off-by: Lyndon-Li --- .../udmrepo/kopialib/lib_repo_ex_test.go | 16 ++++++++++++++++ pkg/uploader/block/uploader.go | 10 +++++----- pkg/uploader/block/uploader_test.go | 6 +++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go index a42c02c14..3294063a6 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go @@ -1,3 +1,19 @@ +/* +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 kopialib import ( diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index bb8195e31..9d4dde9cb 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -5,7 +5,7 @@ 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 + 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, @@ -99,7 +99,7 @@ func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitm id, backupSize, objectSize, err := bu.backupObject(source.dev, destObj, bitmap, source.size) if err != nil { - return udmrepo.Snapshot{}, 0, errors.Wrap(err, "error to backup file with incremental") + return udmrepo.Snapshot{}, 0, errors.Wrapf(err, "error backing up bdev %s", source.realSource) } entryId, err := bu.repoWriter.WriteMetadata(bu.ctx, &udmrepo.Metadata{ @@ -117,7 +117,7 @@ func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitm Description: "bdev-root", }) if err != nil { - return udmrepo.Snapshot{}, 0, errors.Wrap(err, "error to write metadata") + return udmrepo.Snapshot{}, 0, errors.Wrap(err, "error writing metadata") } snapEnd := bu.repoWriter.Time() @@ -139,13 +139,13 @@ func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitm // TODO implement in following PRs func (bu *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bitmap cbt.Iterator, configs map[string]string) (int64, error) { - return 0, nil + return 0, errors.New("not implemented") } func (bu *blockUploader) backupObject(dev *os.File, dest udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (udmrepo.ID, int64, int64, error) { backupSize, objectSize, err := bu.backupData(dev, dest, bitmap, totalLength) if err != nil { - return "", backupSize, objectSize, errors.Wrap(err, "error copying file data incremental") + return "", backupSize, objectSize, err } id, err := dest.Result() diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index d6e2e3d90..2032e31ad 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -75,7 +75,7 @@ func TestGetObjectName(t *testing.T) { { name: "unix path", source: "/var/lib/kubelet/pods/uuid/volumes/test", - expected: "-var-lib-kubelet-pods-uuid-volumes-test", + expected: "var-lib-kubelet-pods-uuid-volumes-test", }, { name: "windows path", @@ -196,7 +196,7 @@ func TestBlockUploaderBackup(t *testing.T) { name: "canceled in progress", cancelInProgress: true, expectErr: true, - expectErrStr: "error copying file data incremental: uploader is canceled", + expectErrStr: "error backing up bdev /data/volume1: uploader is canceled", }, { name: "create object writer err", @@ -357,7 +357,7 @@ func TestBlockUploaderBackup(t *testing.T) { } repoWriter.On("NewObjectWriter", mock.Anything, mock.MatchedBy(func(opt udmrepo.ObjectWriteOptions) bool { - return opt.Description == "BDEV:-data-volume1" && opt.BackupMode == backupMode + return opt.Description == "BDEV:data-volume1" && opt.BackupMode == backupMode })).Return(objWriter, tc.createObjErr) } From 0bc06323bf400f8b17956b6da60af5c4360be162 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 28 May 2026 17:47:33 +0800 Subject: [PATCH 018/194] Support change-id and volume-id in backup workflow. * Add change-id and volume-id retrieve logic for both vks and vanilla k8s environment. * Add change-id and volume-id support code in exposer. Signed-off-by: Xun Jiang --- changelogs/unreleased/9863-blackpiglet | 1 + pkg/backup/actions/csi/pvc_action.go | 10 +- pkg/cbtservice/csi_service_impl.go | 4 +- pkg/cbtservice/csi_service_impl_test.go | 2 +- pkg/cmd/cli/datamover/backup.go | 34 ++- pkg/controller/data_upload_controller.go | 10 +- pkg/controller/data_upload_controller_test.go | 31 ++- pkg/datamover/backup_micro_service.go | 12 +- pkg/datamover/backup_micro_service_test.go | 22 +- pkg/datapath/data_path.go | 24 +- pkg/exposer/csi_snapshot.go | 66 ++++++ pkg/exposer/csi_snapshot_priority_test.go | 2 + pkg/exposer/csi_snapshot_test.go | 208 +++++++++++++++++- pkg/uploader/provider/kopia.go | 3 +- pkg/util/third_party.go | 2 + 15 files changed, 393 insertions(+), 38 deletions(-) create mode 100644 changelogs/unreleased/9863-blackpiglet diff --git a/changelogs/unreleased/9863-blackpiglet b/changelogs/unreleased/9863-blackpiglet new file mode 100644 index 000000000..49bae8d36 --- /dev/null +++ b/changelogs/unreleased/9863-blackpiglet @@ -0,0 +1 @@ +Support change-id and volume-id in backup workflow. \ No newline at end of file diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index 073ea4965..66c14b820 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -22,8 +22,6 @@ import ( "strconv" "time" - "k8s.io/client-go/util/retry" - "github.com/cockroachdb/errors" volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" @@ -31,6 +29,7 @@ import ( corev1api "k8s.io/api/core/v1" storagev1api "k8s.io/api/storage/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -39,11 +38,10 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "k8s.io/client-go/util/retry" crclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "k8s.io/apimachinery/pkg/api/resource" - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" veleroclient "github.com/vmware-tanzu/velero/pkg/client" @@ -160,7 +158,7 @@ func (p *pvcBackupItemAction) getOrCreateVolumeHelper(backup *velerov1api.Backup return p.getVolumeHelperWithCache(backup) } -func (p *pvcBackupItemAction) validatePVCandPV( +func (p *pvcBackupItemAction) validatePVCAndPV( pvc corev1api.PersistentVolumeClaim, item runtime.Unstructured, ) ( @@ -304,7 +302,7 @@ func (p *pvcBackupItemAction) Execute( return nil, nil, "", nil, errors.WithStack(err) } - valid, item, fsType, err := p.validatePVCandPV( + valid, item, fsType, err := p.validatePVCAndPV( pvc, item, ) diff --git a/pkg/cbtservice/csi_service_impl.go b/pkg/cbtservice/csi_service_impl.go index 8918d36fa..4d0ea3fca 100644 --- a/pkg/cbtservice/csi_service_impl.go +++ b/pkg/cbtservice/csi_service_impl.go @@ -111,8 +111,8 @@ func (s *ServiceImpl) GetChangedBlocks(ctx context.Context, snapshot string, cha } args := iterator.Args{ - SnapshotName: snapshot, - PrevSnapshotName: changeID, + SnapshotName: snapshot, + PrevSnapshotID: changeID, Emitter: &emitterImpl{ logger: s.logger, recordCallBack: record, diff --git a/pkg/cbtservice/csi_service_impl_test.go b/pkg/cbtservice/csi_service_impl_test.go index 6ecad0850..cb6b311a9 100644 --- a/pkg/cbtservice/csi_service_impl_test.go +++ b/pkg/cbtservice/csi_service_impl_test.go @@ -234,7 +234,7 @@ func TestServiceImplGetChangedBlocks(t *testing.T) { require.NoError(t, err) assert.Equal(t, "snap-2", capturedArgs.SnapshotName) - assert.Equal(t, "snap-1", capturedArgs.PrevSnapshotName) + assert.Equal(t, "snap-1", capturedArgs.PrevSnapshotID) assert.Equal(t, "velero-ns", capturedArgs.Namespace) assert.Equal(t, iterator.DefaultTokenExpirySeconds, capturedArgs.TokenExpirySecs) assert.Zero(t, capturedArgs.MaxResults) diff --git a/pkg/cmd/cli/datamover/backup.go b/pkg/cmd/cli/datamover/backup.go index 2da71879c..f352c0aad 100644 --- a/pkg/cmd/cli/datamover/backup.go +++ b/pkg/cmd/cli/datamover/backup.go @@ -58,6 +58,9 @@ type dataMoverBackupConfig struct { duName string resourceTimeout time.Duration cbtSAName string + changeID string + volumeID string + snapshotID string } func NewBackupCommand(f client.Factory) *cobra.Command { @@ -79,7 +82,7 @@ func NewBackupCommand(f client.Factory) *cobra.Command { logger.Infof("Starting Velero data-mover backup %s (%s)", buildinfo.Version, buildinfo.FormattedGitSHA()) f.SetBasename(fmt.Sprintf("%s-%s", c.Parent().Name(), c.Name())) - s, err := newdataMoverBackup(logger, f, config) + s, err := newDataMoverBackup(logger, f, config) if err != nil { kube.ExitPodWithMessage(logger, false, "Failed to create data mover backup, %v", err) } @@ -95,6 +98,9 @@ func NewBackupCommand(f client.Factory) *cobra.Command { command.Flags().StringVar(&config.duName, "data-upload", config.duName, "The data upload name") command.Flags().DurationVar(&config.resourceTimeout, "resource-timeout", config.resourceTimeout, "How long to wait for resource processes which are not covered by other specific timeout parameters.") command.Flags().StringVar(&config.cbtSAName, "cbt-sa-name", config.cbtSAName, "The name of the service account used by CSI's CBT service") + command.Flags().StringVar(&config.changeID, "change-id", config.changeID, "The change ID of the snapshot") + command.Flags().StringVar(&config.volumeID, "volume-id", config.volumeID, "The volume ID of the snapshot") + command.Flags().StringVar(&config.snapshotID, "snapshot-id", config.snapshotID, "The ID of the snapshot") _ = command.MarkFlagRequired("volume-path") _ = command.MarkFlagRequired("volume-mode") @@ -118,7 +124,7 @@ type dataMoverBackup struct { cbtService cbtservice.Service } -func newdataMoverBackup(logger logrus.FieldLogger, factory client.Factory, config dataMoverBackupConfig) (*dataMoverBackup, error) { +func newDataMoverBackup(logger logrus.FieldLogger, factory client.Factory, config dataMoverBackupConfig) (*dataMoverBackup, error) { ctx, cancelFunc := context.WithCancel(context.Background()) clientConfig, err := factory.ClientConfig() @@ -303,8 +309,24 @@ func (s *dataMoverBackup) createDataPathService() (dataPathService, error) { repoEnsurer := repository.NewEnsurer(s.client, s.logger, s.config.resourceTimeout) - return datamover.NewBackupMicroService(s.ctx, s.client, s.kubeClient, s.config.duName, s.namespace, s.nodeName, datapath.AccessPoint{ - ByPath: s.config.volumePath, - VolMode: uploader.PersistentVolumeMode(s.config.volumeMode), - }, s.dataPathMgr, repoEnsurer, credGetter, duInformer, s.logger), nil + return datamover.NewBackupMicroService( + s.ctx, + s.client, + s.kubeClient, + s.config.duName, + s.namespace, + s.nodeName, + datapath.AccessPoint{ + ByPath: s.config.volumePath, + VolMode: uploader.PersistentVolumeMode(s.config.volumeMode), + }, + s.dataPathMgr, + repoEnsurer, + credGetter, + duInformer, + s.config.changeID, + s.config.volumeID, + s.config.snapshotID, + s.logger, + ), nil } diff --git a/pkg/controller/data_upload_controller.go b/pkg/controller/data_upload_controller.go index c7bf07f89..9b2d9a2e3 100644 --- a/pkg/controller/data_upload_controller.go +++ b/pkg/controller/data_upload_controller.go @@ -463,9 +463,13 @@ func (r *DataUploadReconciler) initCancelableDataPath(ctx context.Context, async func (r *DataUploadReconciler) startCancelableDataPath(asyncBR datapath.AsyncBR, du *velerov2alpha1api.DataUpload, res *exposer.ExposeResult, log logrus.FieldLogger) error { log.Info("Start cancelable dataUpload") - if err := asyncBR.StartBackup(datapath.AccessPoint{ - ByPath: res.ByPod.VolumeName, - }, du.Spec.DataMoverConfig, nil); err != nil { + if err := asyncBR.StartBackup( + datapath.AccessPoint{ + ByPath: res.ByPod.VolumeName, + }, + du.Spec.DataMoverConfig, + nil, + ); err != nil { return errors.Wrapf(err, "error starting async backup for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName) } diff --git a/pkg/controller/data_upload_controller_test.go b/pkg/controller/data_upload_controller_test.go index d17ed527d..9703abe92 100644 --- a/pkg/controller/data_upload_controller_test.go +++ b/pkg/controller/data_upload_controller_test.go @@ -72,6 +72,7 @@ type FakeClient struct { patchError error updateConflict error listError error + getErrorMap map[string]error // key: object kind or name } func (c *FakeClient) Get(ctx context.Context, key kbclient.ObjectKey, obj kbclient.Object, opts ...kbclient.GetOption) error { @@ -79,6 +80,19 @@ func (c *FakeClient) Get(ctx context.Context, key kbclient.ObjectKey, obj kbclie return c.getError } + // Check if there's a specific error for this object type + if c.getErrorMap != nil { + objType := fmt.Sprintf("%T", obj) + if err, ok := c.getErrorMap[objType]; ok { + return err + } + + // Check if there's a specific error for this object name + if err, ok := c.getErrorMap[key.Name]; ok { + return err + } + } + return c.Client.Get(ctx, key, obj) } @@ -209,9 +223,13 @@ func initDataUploaderReconcilerWithError(needError ...error) (*DataUploadReconci if err != nil { return nil, err } + err = snapshotv1api.AddToScheme(scheme) + if err != nil { + return nil, err + } fakeClient := &FakeClient{ - Client: fake.NewClientBuilder().WithScheme(scheme).Build(), + Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(vsObject, node).Build(), } for k := range needError { @@ -505,7 +523,7 @@ func TestReconcile(t *testing.T) { { name: "du succeeds for accepted", du: dataUploadBuilder().Finalizers([]string{DataUploadDownloadFinalizer}).SnapshotType(fakeSnapshotType).Result(), - pvc: builder.ForPersistentVolumeClaim("fake-ns", "test-pvc").Result(), + pvc: builder.ForPersistentVolumeClaim("fake-ns", "test-pvc").VolumeName("test-pv").Result(), expected: dataUploadBuilder().Finalizers([]string{DataUploadDownloadFinalizer}).Phase(velerov2alpha1api.DataUploadPhaseAccepted).Result(), }, { @@ -636,6 +654,15 @@ func TestReconcile(t *testing.T) { if test.pvc != nil { err = r.client.Create(ctx, test.pvc) require.NoError(t, err) + + // Create the corresponding PV if PVC references one + if test.pvc.Spec.VolumeName != "" { + pv := builder.ForPersistentVolume(test.pvc.Spec.VolumeName). + CSI("csi.driver", "test-volume-id"). + ClaimRef(test.pvc.Namespace, test.pvc.Name).Result() + err = r.client.Create(ctx, pv) + require.NoError(t, err) + } } if test.dataMgr != nil { diff --git a/pkg/datamover/backup_micro_service.go b/pkg/datamover/backup_micro_service.go index 6b719c792..08a005217 100644 --- a/pkg/datamover/backup_micro_service.go +++ b/pkg/datamover/backup_micro_service.go @@ -67,6 +67,10 @@ type BackupMicroService struct { duInformer cache.Informer duHandler cachetool.ResourceEventHandlerRegistration nodeName string + + changeID string + volumeID string + snapshotID string } type dataPathResult struct { @@ -76,7 +80,7 @@ type dataPathResult struct { func NewBackupMicroService(ctx context.Context, client client.Client, kubeClient kubernetes.Interface, dataUploadName string, namespace string, nodeName string, sourceTargetPath datapath.AccessPoint, dataPathMgr *datapath.Manager, repoEnsurer *repository.Ensurer, cred *credentials.CredentialGetter, - duInformer cache.Informer, log logrus.FieldLogger) *BackupMicroService { + duInformer cache.Informer, changeID string, volumeID string, snapshotID string, log logrus.FieldLogger) *BackupMicroService { return &BackupMicroService{ ctx: ctx, client: client, @@ -91,6 +95,9 @@ func NewBackupMicroService(ctx context.Context, client client.Client, kubeClient nodeName: nodeName, resultSignal: make(chan dataPathResult), duInformer: duInformer, + changeID: changeID, + volumeID: volumeID, + snapshotID: snapshotID, } } @@ -200,6 +207,9 @@ func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string, ParentSnapshot: "", ForceFull: false, Tags: tags, + VolumeID: r.volumeID, + ChangeID: r.changeID, + SnapshotID: r.snapshotID, }); err != nil { return "", errors.Wrap(err, "error starting data path backup") } diff --git a/pkg/datamover/backup_micro_service_test.go b/pkg/datamover/backup_micro_service_test.go index ab664df71..e6291244b 100644 --- a/pkg/datamover/backup_micro_service_test.go +++ b/pkg/datamover/backup_micro_service_test.go @@ -29,21 +29,16 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" - - "github.com/vmware-tanzu/velero/pkg/builder" - "github.com/vmware-tanzu/velero/pkg/datapath" - "github.com/vmware-tanzu/velero/pkg/uploader" - - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - + kbclient "sigs.k8s.io/controller-runtime/pkg/client" clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" - velerotest "github.com/vmware-tanzu/velero/pkg/test" - - kbclient "sigs.k8s.io/controller-runtime/pkg/client" - + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/datapath" datapathmockes "github.com/vmware-tanzu/velero/pkg/datapath/mocks" + velerotest "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/uploader" ) type backupMsTestHelper struct { @@ -294,7 +289,10 @@ func TestCancelDataUpload(t *testing.T) { func TestRunCancelableDataPath(t *testing.T) { dataUploadName := "fake-data-upload" du := builder.ForDataUpload(velerov1api.DefaultNamespace, dataUploadName).Phase(velerov2alpha1api.DataUploadPhaseNew).Result() - duInProgress := builder.ForDataUpload(velerov1api.DefaultNamespace, dataUploadName).Phase(velerov2alpha1api.DataUploadPhaseInProgress).Result() + duInProgress := builder.ForDataUpload(velerov1api.DefaultNamespace, dataUploadName).Phase(velerov2alpha1api.DataUploadPhaseInProgress).CSISnapshot( + &velerov2alpha1api.CSISnapshotSpec{ + VolumeSnapshot: "fake-snapshot", + }).Result() ctxTimeout, cancel := context.WithTimeout(t.Context(), time.Second) tests := []struct { diff --git a/pkg/datapath/data_path.go b/pkg/datapath/data_path.go index 71b8e0690..6cef1af26 100644 --- a/pkg/datapath/data_path.go +++ b/pkg/datapath/data_path.go @@ -26,6 +26,7 @@ import ( "github.com/vmware-tanzu/velero/internal/credentials" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/cbtservice" "github.com/vmware-tanzu/velero/pkg/repository" repokey "github.com/vmware-tanzu/velero/pkg/repository/keys" repoProvider "github.com/vmware-tanzu/velero/pkg/repository/provider" @@ -53,6 +54,9 @@ type BackupStartParam struct { ParentSnapshot string ForceFull bool Tags map[string]string + VolumeID string + ChangeID string + SnapshotID string } type generalDataPath struct { @@ -182,8 +186,24 @@ func (dp *generalDataPath) StartBackup(source AccessPoint, uploaderConfig map[st dp.wgDataPath.Done() }() - snapshotID, emptySnapshot, totalBytes, incrementalBytes, err := dp.uploaderProv.RunBackup(dp.ctx, source.ByPath, backupParam.RealSource, backupParam.Tags, backupParam.ForceFull, - backupParam.ParentSnapshot, provider.CBTParam{}, source.VolMode, uploaderConfig, dp) + snapshotID, emptySnapshot, totalBytes, incrementalBytes, err := dp.uploaderProv.RunBackup( + dp.ctx, + source.ByPath, + backupParam.RealSource, + backupParam.Tags, + backupParam.ForceFull, + backupParam.ParentSnapshot, + provider.CBTParam{ + Source: cbtservice.SourceInfo{ + Snapshot: backupParam.SnapshotID, + VolumeID: backupParam.VolumeID, + ChangeID: backupParam.ChangeID, + }, + }, + source.VolMode, + uploaderConfig, + dp, + ) if err == provider.ErrorCanceled { dp.callbacks.OnCancelled(context.Background(), dp.namespace, dp.jobName) diff --git a/pkg/exposer/csi_snapshot.go b/pkg/exposer/csi_snapshot.go index 4582c1e62..6c92a6973 100644 --- a/pkg/exposer/csi_snapshot.go +++ b/pkg/exposer/csi_snapshot.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "maps" + "strings" "time" "github.com/cockroachdb/errors" @@ -110,6 +111,12 @@ type CSISnapshotExposeWaitParam struct { NodeName string } +type cbtInfo struct { + changeID string + volumeID string + snapshotID string +} + // NewCSISnapshotExposer create a new instance of CSI snapshot exposer func NewCSISnapshotExposer(kubeClient kubernetes.Interface, csiSnapshotClient snapshotter.SnapshotV1Interface, log logrus.FieldLogger) SnapshotExposer { return &csiSnapshotExposer{ @@ -256,6 +263,14 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O affinity := kube.GetLoadAffinityByStorageClass(csiExposeParam.Affinity, backupPVCStorageClass, curLog) + var cbtInfo cbtInfo + if csiExposeParam.DataMover == datamover.DataMoverTypeVeleroBlock { + cbtInfo, err = e.getCBTInfo(ctx, backupVS, backupVSC, csiExposeParam.SourcePVName) + if err != nil { + return errors.Wrap(err, "error to get CBT info") + } + } + backupPod, err := e.createBackupPod( ctx, ownerObject, @@ -273,6 +288,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O intoleratableNodes, volumeTopology, csiExposeParam.SnapshotMetadataServiceConfigs, + &cbtInfo, ) if err != nil { return errors.Wrap(err, "error to create backup pod") @@ -289,6 +305,49 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O return nil } +func (e *csiSnapshotExposer) getCBTInfo(ctx context.Context, vs *snapshotv1api.VolumeSnapshot, vsc *snapshotv1api.VolumeSnapshotContent, sourcePVName string) (cbtInfo, error) { + cbtInfo := cbtInfo{} + if vs == nil || vsc == nil { + return cbtInfo, errors.New("vs or vsc is nil") + } + + cbtInfo.snapshotID = vs.Name + + if vs.Annotations != nil && + (vs.Annotations[util.VSphereCNSChangeIDAnno] != "" || + vs.Annotations[util.VSphereCNSSnapshotAnno] != "") { + cbtInfo.changeID = vs.Annotations[util.VSphereCNSChangeIDAnno] + + splitSnapshotAnno := strings.Split(vs.Annotations[util.VSphereCNSSnapshotAnno], "+") + if len(splitSnapshotAnno) >= 2 { + cbtInfo.volumeID = splitSnapshotAnno[0] + } + + e.log.Debugf("volumeID %s and changeID %s are read from VKS annotations.", cbtInfo.volumeID, cbtInfo.changeID) + } else { + pv, err := e.kubeClient.CoreV1().PersistentVolumes().Get(ctx, sourcePVName, metav1.GetOptions{}) + if err != nil { + return cbtInfo, fmt.Errorf("failed to get pv %s: %w", sourcePVName, err) + } + + if vsc.Status != nil && vsc.Status.SnapshotHandle != nil { + cbtInfo.changeID = *vsc.Status.SnapshotHandle + } + + if pv.Spec.CSI != nil && pv.Spec.CSI.VolumeHandle != "" { + cbtInfo.volumeID = pv.Spec.CSI.VolumeHandle + } + + e.log.Debugf("volumeID %s and changeID %s are read from PV and VS's handles.", cbtInfo.volumeID, cbtInfo.changeID) + } + + if cbtInfo.volumeID == "" { + return cbtInfo, fmt.Errorf("volumeID must not be empty for CBT") + } + + return cbtInfo, nil +} + func (e *csiSnapshotExposer) GetExposed(ctx context.Context, ownerObject corev1api.ObjectReference, timeout time.Duration, param any) (*ExposeResult, error) { exposeWaitParam := param.(*CSISnapshotExposeWaitParam) @@ -618,6 +677,7 @@ func (e *csiSnapshotExposer) createBackupPod( intoleratableNodes []string, volumeTopology *corev1api.NodeSelector, csiSnapshotMetadataServiceConfigs *velerotypes.CSISnapshotMetadataService, + cbtInfo *cbtInfo, ) (*corev1api.Pod, error) { podName := ownerObject.Name @@ -670,6 +730,12 @@ func (e *csiSnapshotExposer) createBackupPod( fmt.Sprintf("--resource-timeout=%s", operationTimeout.String()), } + if cbtInfo != nil { + args = append(args, fmt.Sprintf("--change-id=%s", cbtInfo.changeID)) + args = append(args, fmt.Sprintf("--volume-id=%s", cbtInfo.volumeID)) + args = append(args, fmt.Sprintf("--snapshot-id=%s", cbtInfo.snapshotID)) + } + args = append(args, podInfo.logFormatArgs...) args = append(args, podInfo.logLevelArgs...) diff --git a/pkg/exposer/csi_snapshot_priority_test.go b/pkg/exposer/csi_snapshot_priority_test.go index 8c3086f76..f05ab6007 100644 --- a/pkg/exposer/csi_snapshot_priority_test.go +++ b/pkg/exposer/csi_snapshot_priority_test.go @@ -156,6 +156,7 @@ func TestCreateBackupPodWithPriorityClass(t *testing.T) { nil, nil, nil, + nil, ) require.NoError(t, err, tc.description) @@ -243,6 +244,7 @@ func TestCreateBackupPodWithMissingConfigMap(t *testing.T) { nil, nil, nil, + nil, ) // Should succeed even when config map is missing diff --git a/pkg/exposer/csi_snapshot_test.go b/pkg/exposer/csi_snapshot_test.go index bf3b08066..e1512e633 100644 --- a/pkg/exposer/csi_snapshot_test.go +++ b/pkg/exposer/csi_snapshot_test.go @@ -17,34 +17,38 @@ limitations under the License. package exposer import ( + "context" "fmt" "maps" + "strings" "testing" "time" "github.com/cockroachdb/errors" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" snapshotFake "github.com/kubernetes-csi/external-snapshotter/client/v8/clientset/versioned/fake" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1api "k8s.io/api/apps/v1" corev1api "k8s.io/api/core/v1" + storagev1api "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" + kubefake "k8s.io/client-go/kubernetes/fake" clientTesting "k8s.io/client-go/testing" "k8s.io/utils/ptr" clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/datamover" velerotest "github.com/vmware-tanzu/velero/pkg/test" velerotypes "github.com/vmware-tanzu/velero/pkg/types" "github.com/vmware-tanzu/velero/pkg/util" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/kube" - - storagev1api "k8s.io/api/storage/v1" ) type reactor struct { @@ -191,6 +195,19 @@ func TestExpose(t *testing.T) { }, } + sourcePV := &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeSource: corev1api.PersistentVolumeSource{ + CSI: &corev1api.CSIPersistentVolumeSource{ + VolumeHandle: "csi-volume-handle", + }, + }, + }, + } + tests := []struct { name string snapshotClientObj []runtime.Object @@ -1015,6 +1032,46 @@ func TestExpose(t *testing.T) { }, expectedPVCAnnotation: map[string]string{util.VSphereCNSFastCloneAnno: "true"}, }, + { + name: "block data mover success", + ownerBackup: backup, + exposeParam: CSISnapshotExposeParam{ + SnapshotName: "fake-vs", + SourceNamespace: "fake-ns", + AccessMode: AccessModeFileSystem, + OperationTimeout: time.Millisecond, + ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", + SourcePVName: "fake-pv", + DataMover: datamover.DataMoverTypeVeleroBlock, + }, + snapshotClientObj: []runtime.Object{ + vsObject, + vscObj, + }, + kubeClientObj: []runtime.Object{ + daemonSet, + scObj, + sourcePV, + }, + expectedAffinity: &corev1api.Affinity{ + NodeAffinity: &corev1api.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{ + NodeSelectorTerms: []corev1api.NodeSelectorTerm{ + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "kubernetes.io/os", + Operator: corev1api.NodeSelectorOpNotIn, + Values: []string{"windows"}, + }, + }, + }, + }, + }, + }, + }, + }, } for _, test := range tests { @@ -1994,3 +2051,150 @@ end diagnose CSI exposer`, }) } } + +func TestGetCBTInfo(t *testing.T) { + handle := "snapshot-handle-1" + + tests := []struct { + name string + vs *snapshotv1api.VolumeSnapshot + vsc *snapshotv1api.VolumeSnapshotContent + pv *corev1api.PersistentVolume + sourcePVName string + want cbtInfo + wantErrSubstr string + }{ + { + name: "return error when vs is nil", + vs: nil, + vsc: &snapshotv1api.VolumeSnapshotContent{}, + sourcePVName: "pv-1", + wantErrSubstr: "vs or vsc is nil", + }, + { + name: "use annotations when change-id and snapshot annotation exist", + vs: &snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs-anno", + Annotations: map[string]string{ + util.VSphereCNSChangeIDAnno: "change-id-1", + util.VSphereCNSSnapshotAnno: "volume-id-1+snapshot-id-1", + }, + }, + }, + vsc: &snapshotv1api.VolumeSnapshotContent{}, + sourcePVName: "pv-ignored", + want: cbtInfo{ + changeID: "change-id-1", + volumeID: "volume-id-1", + snapshotID: "vs-anno", + }, + }, + { + name: "fallback to pv and vsc snapshot handle", + vs: &snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{Name: "vs-fallback"}, + }, + vsc: &snapshotv1api.VolumeSnapshotContent{ + Status: &snapshotv1api.VolumeSnapshotContentStatus{ + SnapshotHandle: &handle, + }, + }, + pv: &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "pv-1"}, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeSource: corev1api.PersistentVolumeSource{ + CSI: &corev1api.CSIPersistentVolumeSource{ + VolumeHandle: "csi-volume-handle-1", + }, + }, + }, + }, + sourcePVName: "pv-1", + want: cbtInfo{ + changeID: "snapshot-handle-1", + volumeID: "csi-volume-handle-1", + snapshotID: "vs-fallback", + }, + }, + { + name: "return error when pv not found in fallback path", + vs: &snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{Name: "vs-no-pv"}, + }, + vsc: &snapshotv1api.VolumeSnapshotContent{}, + sourcePVName: "pv-not-found", + wantErrSubstr: "failed to get pv pv-not-found", + }, + { + name: "return error when pv has no csi volume handle", + vs: &snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{Name: "vs-no-volume-handle"}, + }, + vsc: &snapshotv1api.VolumeSnapshotContent{}, + pv: &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "pv-no-handle"}, + Spec: corev1api.PersistentVolumeSpec{}, + }, + sourcePVName: "pv-no-handle", + wantErrSubstr: "volumeID must not be empty for CBT", + }, + { + name: "return error when snapshot annotation is invalid", + vs: &snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs-no-volume-handle", + Annotations: map[string]string{ + util.VSphereCNSChangeIDAnno: "change-id-1", + util.VSphereCNSSnapshotAnno: "volume-id-1:snapshot-id-1", + }, + }, + }, + vsc: &snapshotv1api.VolumeSnapshotContent{}, + pv: &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "pv-1"}, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeSource: corev1api.PersistentVolumeSource{ + CSI: &corev1api.CSIPersistentVolumeSource{ + VolumeHandle: "csi-volume-handle-1", + }, + }, + }, + }, + sourcePVName: "pv-1", + wantErrSubstr: "volumeID must not be empty for CBT", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var objs []runtime.Object + if tc.pv != nil { + objs = append(objs, tc.pv) + } + exposer := &csiSnapshotExposer{ + kubeClient: kubefake.NewSimpleClientset(objs...), + log: logrus.StandardLogger(), + } + + got, err := exposer.getCBTInfo(context.Background(), tc.vs, tc.vsc, tc.sourcePVName) + + if tc.wantErrSubstr != "" { + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantErrSubstr) + } + if !strings.Contains(err.Error(), tc.wantErrSubstr) { + t.Fatalf("expected error containing %q, got %q", tc.wantErrSubstr, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.changeID != tc.want.changeID || got.volumeID != tc.want.volumeID || got.snapshotID != tc.want.snapshotID { + t.Fatalf("unexpected cbtInfo, want %+v, got %+v", tc.want, got) + } + }) + } +} diff --git a/pkg/uploader/provider/kopia.go b/pkg/uploader/provider/kopia.go index ba86c977c..682b2053e 100644 --- a/pkg/uploader/provider/kopia.go +++ b/pkg/uploader/provider/kopia.go @@ -120,7 +120,8 @@ func (kp *kopiaProvider) RunBackup( _ CBTParam, volMode uploader.PersistentVolumeMode, uploaderCfg map[string]string, - updater uploader.ProgressUpdater) (string, bool, int64, int64, error) { + updater uploader.ProgressUpdater, +) (string, bool, int64, int64, error) { if updater == nil { return "", false, 0, 0, errors.New("Need to initial backup progress updater first") } diff --git a/pkg/util/third_party.go b/pkg/util/third_party.go index 400c7a898..81b964454 100644 --- a/pkg/util/third_party.go +++ b/pkg/util/third_party.go @@ -31,4 +31,6 @@ var ThirdPartyTolerations = []string{ const ( VSphereCNSFastCloneAnno = "csi.vsphere.volume/fast-provisioning" + VSphereCNSSnapshotAnno = "csi.vsphere.volume/snapshot" + VSphereCNSChangeIDAnno = "csi.vsphere.volume/change-id" ) From fa20e46016da7574cc7e956f3e0e094d65f966cf Mon Sep 17 00:00:00 2001 From: Scott Seago Date: Tue, 10 Mar 2026 15:41:17 -0400 Subject: [PATCH 019/194] refactor: Optimize VSC handle readiness polling for VSS backups Co-authored-by: aider (gemini/gemini-2.5-pro) Signed-off-by: Scott Seago --- changelogs/unreleased/9602-sseago | 1 + pkg/util/csi/volume_snapshot.go | 143 ++++++++++++++++++------------ 2 files changed, 85 insertions(+), 59 deletions(-) create mode 100644 changelogs/unreleased/9602-sseago diff --git a/changelogs/unreleased/9602-sseago b/changelogs/unreleased/9602-sseago new file mode 100644 index 000000000..6bed2f243 --- /dev/null +++ b/changelogs/unreleased/9602-sseago @@ -0,0 +1 @@ +Optimize VSC handle readiness polling for VSS backups diff --git a/pkg/util/csi/volume_snapshot.go b/pkg/util/csi/volume_snapshot.go index 8cc7c043a..69f4b1a67 100644 --- a/pkg/util/csi/volume_snapshot.go +++ b/pkg/util/csi/volume_snapshot.go @@ -598,72 +598,97 @@ func WaitUntilVSCHandleIsReady( log logrus.FieldLogger, csiSnapshotTimeout time.Duration, ) (*snapshotv1api.VolumeSnapshotContent, error) { - // We'll wait 10m for the VSC to be reconciled polling - // every 5s unless backup's csiSnapshotTimeout is set - interval := 5 * time.Second + // We'll wait for the VSC to be reconciled, trying a fast poll interval first + // before falling back to a slower poll interval for the full csiSnapshotTimeout. vsc := new(snapshotv1api.VolumeSnapshotContent) + var interval time.Duration + pollFunc := func(ctx context.Context) (bool, error) { + vs := new(snapshotv1api.VolumeSnapshot) + if err := crClient.Get( + ctx, + crclient.ObjectKeyFromObject(volSnap), + vs, + ); err != nil { + return false, + errors.Wrapf( + err, + "failed to get volumesnapshot %s/%s", + volSnap.Namespace, volSnap.Name, + ) + } + + if vs.Status == nil || vs.Status.BoundVolumeSnapshotContentName == nil { + log.Infof("Waiting for CSI driver to reconcile volumesnapshot %s/%s. Retrying in %ds", + volSnap.Namespace, volSnap.Name, interval/time.Second) + return false, nil + } + + if err := crClient.Get( + ctx, + crclient.ObjectKey{ + Name: *vs.Status.BoundVolumeSnapshotContentName, + }, + vsc, + ); err != nil { + return false, + errors.Wrapf( + err, + "failed to get VolumeSnapshotContent %s for VolumeSnapshot %s/%s", + *vs.Status.BoundVolumeSnapshotContentName, vs.Namespace, vs.Name, + ) + } + + // we need to wait for the VolumeSnapshotContent + // to have a snapshot handle because during restore, + // we'll use that snapshot handle as the source for + // the VolumeSnapshotContent so it's statically + // bound to the existing snapshot. + if vsc.Status == nil || + vsc.Status.SnapshotHandle == nil { + log.Infof( + "Waiting for VolumeSnapshotContents %s to have snapshot handle. Retrying in %ds", + vsc.Name, interval/time.Second) + if vsc.Status != nil && + vsc.Status.Error != nil { + log.Warnf("VolumeSnapshotContent %s has error: %v", + vsc.Name, *vsc.Status.Error.Message) + } + return false, nil + } + + return true, nil + } + + // The short interval for the first ten seconds is due to the fact that + // Microsoft VSS backups have a hard-coded unfreeze call after 10 seconds, + // so we need to minimize waiting time during the first 10 seconds. + // First poll with a short interval and timeout. + interval = 1 * time.Second + timeout := 10 * time.Second err := wait.PollUntilContextTimeout( + context.Background(), + interval, + timeout, + true, + pollFunc, + ) + + if err == nil { + return vsc, nil + } + if !wait.Interrupted(err) { + return nil, err + } + + // If the first poll timed out, poll with a longer interval and the full timeout. + interval = 5 * time.Second + err = wait.PollUntilContextTimeout( context.Background(), interval, csiSnapshotTimeout, true, - func(ctx context.Context) (bool, error) { - vs := new(snapshotv1api.VolumeSnapshot) - if err := crClient.Get( - ctx, - crclient.ObjectKeyFromObject(volSnap), - vs, - ); err != nil { - return false, - errors.Wrapf( - err, - "failed to get volumesnapshot %s/%s", - volSnap.Namespace, volSnap.Name, - ) - } - - if vs.Status == nil || vs.Status.BoundVolumeSnapshotContentName == nil { - log.Infof("Waiting for CSI driver to reconcile volumesnapshot %s/%s. Retrying in %ds", - volSnap.Namespace, volSnap.Name, interval/time.Second) - return false, nil - } - - if err := crClient.Get( - ctx, - crclient.ObjectKey{ - Name: *vs.Status.BoundVolumeSnapshotContentName, - }, - vsc, - ); err != nil { - return false, - errors.Wrapf( - err, - "failed to get VolumeSnapshotContent %s for VolumeSnapshot %s/%s", - *vs.Status.BoundVolumeSnapshotContentName, vs.Namespace, vs.Name, - ) - } - - // we need to wait for the VolumeSnapshotContent - // to have a snapshot handle because during restore, - // we'll use that snapshot handle as the source for - // the VolumeSnapshotContent so it's statically - // bound to the existing snapshot. - if vsc.Status == nil || - vsc.Status.SnapshotHandle == nil { - log.Infof( - "Waiting for VolumeSnapshotContents %s to have snapshot handle. Retrying in %ds", - vsc.Name, interval/time.Second) - if vsc.Status != nil && - vsc.Status.Error != nil { - log.Warnf("VolumeSnapshotContent %s has error: %v", - vsc.Name, *vsc.Status.Error.Message) - } - return false, nil - } - - return true, nil - }, + pollFunc, ) if err != nil { From c60a5bcc7c7d3d9791ab6a3214d5a03c228e1f9f Mon Sep 17 00:00:00 2001 From: Scott Seago Date: Wed, 18 Mar 2026 18:07:18 -0400 Subject: [PATCH 020/194] feat: Implement early frequent polling for CSI snapshots Co-authored-by: aider (gemini/gemini-2.5-pro) Signed-off-by: Scott Seago --- .../unreleased/{9602-sseago => 9955-sseago} | 0 pkg/cmd/cli/install/install.go | 24 ++++++----- pkg/install/deployment.go | 16 +++++++ pkg/install/resources.go | 5 +++ pkg/util/csi/volume_snapshot.go | 43 +++++++++++-------- 5 files changed, 60 insertions(+), 28 deletions(-) rename changelogs/unreleased/{9602-sseago => 9955-sseago} (100%) diff --git a/changelogs/unreleased/9602-sseago b/changelogs/unreleased/9955-sseago similarity index 100% rename from changelogs/unreleased/9602-sseago rename to changelogs/unreleased/9955-sseago diff --git a/pkg/cmd/cli/install/install.go b/pkg/cmd/cli/install/install.go index 26b4f9384..0df53eb32 100644 --- a/pkg/cmd/cli/install/install.go +++ b/pkg/cmd/cli/install/install.go @@ -81,6 +81,7 @@ type Options struct { DefaultVolumesToFsBackup bool UploaderType string DefaultSnapshotMoveData bool + CSISnapshotEarlyFrequentPolling bool DisableInformerCache bool ScheduleSkipImmediately bool PodResources kubeutil.PodResources @@ -141,6 +142,7 @@ func (o *Options) BindFlags(flags *pflag.FlagSet) { flags.BoolVar(&o.DefaultVolumesToFsBackup, "default-volumes-to-fs-backup", o.DefaultVolumesToFsBackup, "Bool flag to configure Velero server to use pod volume file system backup by default for all volumes on all backups. Optional.") flags.StringVar(&o.UploaderType, "uploader-type", o.UploaderType, fmt.Sprintf("The type of uploader to transfer the data of pod volumes, supported value: '%s'", uploader.KopiaType)) flags.BoolVar(&o.DefaultSnapshotMoveData, "default-snapshot-move-data", o.DefaultSnapshotMoveData, "Bool flag to configure Velero server to move data by default for all snapshots supporting data movement. Optional.") + flags.BoolVar(&o.CSISnapshotEarlyFrequentPolling, "csi-snapshot-early-frequent-polling", o.CSISnapshotEarlyFrequentPolling, "Bool flag to configure Velero server to use early frequent polling by default for all CSI snapshots. Optional.") flags.BoolVar(&o.DisableInformerCache, "disable-informer-cache", o.DisableInformerCache, "Disable informer cache for Get calls on restore. With this enabled, it will speed up restore in cases where there are backup resources which already exist in the cluster, but for very large clusters this will increase velero memory usage. Default is false (don't disable). Optional.") flags.BoolVar(&o.ScheduleSkipImmediately, "schedule-skip-immediately", o.ScheduleSkipImmediately, "Skip the first scheduled backup immediately after creating a schedule. Default is false (don't skip).") flags.BoolVar(&o.NodeAgentDisableHostPath, "node-agent-disable-host-path", o.NodeAgentDisableHostPath, "Don't mount the pod volume host path to node-agent. Optional. Pod volume host path mount is required by fs-backup but could be disabled for other backup methods.") @@ -238,16 +240,17 @@ func NewInstallOptions() *Options { NodeAgentPodCPULimit: install.DefaultNodeAgentPodCPULimit, NodeAgentPodMemLimit: install.DefaultNodeAgentPodMemLimit, // Default to creating a VSL unless we're told otherwise - UseVolumeSnapshots: true, - NoDefaultBackupLocation: false, - CRDsOnly: false, - DefaultVolumesToFsBackup: false, - UploaderType: uploader.KopiaType, - DefaultSnapshotMoveData: false, - DisableInformerCache: false, - ScheduleSkipImmediately: false, - kubeletRootDir: install.DefaultKubeletRootDir, - NodeAgentDisableHostPath: false, + UseVolumeSnapshots: true, + NoDefaultBackupLocation: false, + CRDsOnly: false, + DefaultVolumesToFsBackup: false, + UploaderType: uploader.KopiaType, + DefaultSnapshotMoveData: false, + CSISnapshotEarlyFrequentPolling: false, + DisableInformerCache: false, + ScheduleSkipImmediately: false, + kubeletRootDir: install.DefaultKubeletRootDir, + NodeAgentDisableHostPath: false, } } @@ -324,6 +327,7 @@ func (o *Options) AsVeleroOptions() (*install.VeleroOptions, error) { DefaultVolumesToFsBackup: o.DefaultVolumesToFsBackup, UploaderType: o.UploaderType, DefaultSnapshotMoveData: o.DefaultSnapshotMoveData, + CSISnapshotEarlyFrequentPolling: o.CSISnapshotEarlyFrequentPolling, DisableInformerCache: o.DisableInformerCache, ScheduleSkipImmediately: o.ScheduleSkipImmediately, PodResources: o.PodResources, diff --git a/pkg/install/deployment.go b/pkg/install/deployment.go index 7af17bc53..4ce4b5a4f 100644 --- a/pkg/install/deployment.go +++ b/pkg/install/deployment.go @@ -50,6 +50,7 @@ type podTemplateConfig struct { serviceAccountName string uploaderType string defaultSnapshotMoveData bool + csiSnapshotEarlyFrequentPolling bool privilegedNodeAgent bool disableInformerCache bool scheduleSkipImmediately bool @@ -166,6 +167,12 @@ func WithDefaultSnapshotMoveData(b bool) podTemplateOption { } } +func WithCSISnapshotEarlyFrequentPolling(b bool) podTemplateOption { + return func(c *podTemplateConfig) { + c.csiSnapshotEarlyFrequentPolling = b + } +} + func WithDisableInformerCache(b bool) podTemplateOption { return func(c *podTemplateConfig) { c.disableInformerCache = b @@ -489,6 +496,15 @@ func Deployment(namespace string, opts ...podTemplateOption) *appsv1api.Deployme }...) } + if c.csiSnapshotEarlyFrequentPolling { + deployment.Spec.Template.Spec.Containers[0].Env = append(deployment.Spec.Template.Spec.Containers[0].Env, []corev1api.EnvVar{ + { + Name: "CSI_SNAPSHOT_EARLY_FREQUENT_POLLING", + Value: "true", + }, + }...) + } + deployment.Spec.Template.Spec.Containers[0].Env = append(deployment.Spec.Template.Spec.Containers[0].Env, c.envVars...) if len(c.plugins) > 0 { diff --git a/pkg/install/resources.go b/pkg/install/resources.go index 5c7534774..c4ec6f1bc 100644 --- a/pkg/install/resources.go +++ b/pkg/install/resources.go @@ -263,6 +263,7 @@ type VeleroOptions struct { DefaultVolumesToFsBackup bool UploaderType string DefaultSnapshotMoveData bool + CSISnapshotEarlyFrequentPolling bool DisableInformerCache bool ScheduleSkipImmediately bool PodResources kube.PodResources @@ -390,6 +391,10 @@ func AllResources(o *VeleroOptions) *unstructured.UnstructuredList { deployOpts = append(deployOpts, WithDefaultSnapshotMoveData(true)) } + if o.CSISnapshotEarlyFrequentPolling { + deployOpts = append(deployOpts, WithCSISnapshotEarlyFrequentPolling(true)) + } + if o.DisableInformerCache { deployOpts = append(deployOpts, WithDisableInformerCache(true)) } diff --git a/pkg/util/csi/volume_snapshot.go b/pkg/util/csi/volume_snapshot.go index 69f4b1a67..b78455bc8 100644 --- a/pkg/util/csi/volume_snapshot.go +++ b/pkg/util/csi/volume_snapshot.go @@ -20,6 +20,8 @@ import ( "context" "encoding/json" "fmt" + "os" + "strconv" "strings" "time" @@ -660,25 +662,30 @@ func WaitUntilVSCHandleIsReady( return true, nil } - // The short interval for the first ten seconds is due to the fact that - // Microsoft VSS backups have a hard-coded unfreeze call after 10 seconds, - // so we need to minimize waiting time during the first 10 seconds. - // First poll with a short interval and timeout. - interval = 1 * time.Second - timeout := 10 * time.Second - err := wait.PollUntilContextTimeout( - context.Background(), - interval, - timeout, - true, - pollFunc, - ) + var err error + frequentPolling, err := strconv.ParseBool(os.Getenv("CSI_SNAPSHOT_EARLY_FREQUENT_POLLING")) - if err == nil { - return vsc, nil - } - if !wait.Interrupted(err) { - return nil, err + if err == nil && frequentPolling { + // The short interval for the first ten seconds is due to the fact that + // Microsoft VSS backups have a hard-coded unfreeze call after 10 seconds, + // so we need to minimize waiting time during the first 10 seconds. + // First poll with a short interval and timeout. + interval = 1 * time.Second + timeout := 10 * time.Second + err = wait.PollUntilContextTimeout( + context.Background(), + interval, + timeout, + true, + pollFunc, + ) + + if err == nil { + return vsc, nil + } + if !wait.Interrupted(err) { + return nil, err + } } // If the first poll timed out, poll with a longer interval and the full timeout. From daef5f5cf721481548ee7349cc77e54be8d759e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:50:39 +0000 Subject: [PATCH 021/194] Bump golang.org/x/net from 0.49.0 to 0.55.0 in /pkg/apis Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.55.0. - [Commits](https://github.com/golang/net/compare/v0.49.0...v0.55.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.55.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- pkg/apis/go.mod | 4 ++-- pkg/apis/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/apis/go.mod b/pkg/apis/go.mod index 364a1129f..eb7f20924 100644 --- a/pkg/apis/go.mod +++ b/pkg/apis/go.mod @@ -16,8 +16,8 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/text v0.37.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect diff --git a/pkg/apis/go.sum b/pkg/apis/go.sum index f679a531e..ec45c153b 100644 --- a/pkg/apis/go.sum +++ b/pkg/apis/go.sum @@ -37,10 +37,10 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From fbafece999c55743fa558384a79aa342e333cc33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:55:15 +0000 Subject: [PATCH 022/194] Initial plan From 24550ddaddebd038a8e31acafb448a3cb443e11a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:56:16 +0000 Subject: [PATCH 023/194] Ensure Dependabot PRs get changelog-not-required label --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 45332806b..682c01231 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,20 @@ updates: schedule: interval: "weekly" labels: + - "Dependencies" + - "go" + - "kind/changelog-not-required" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"] + # Dependencies listed in pkg/apis/go.mod + - package-ecosystem: "gomod" + directory: "/pkg/apis" # Location of package manifests + schedule: + interval: "weekly" + labels: + - "Dependencies" + - "go" - "kind/changelog-not-required" ignore: - dependency-name: "*" From 4cf1dd9df628e0aef5afd878082347dc04299db1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:12:11 +0000 Subject: [PATCH 024/194] Bump actions/upload-artifact from 5 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/e2e-test-kind.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index 760686911..96198a0dc 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -187,7 +187,7 @@ jobs: timeout-minutes: 30 - name: Upload debug bundle if: ${{ failure() }} - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: DebugBundle-k8s-${{ matrix.k8s }}-job-${{ strategy.job-index }} path: /home/runner/work/velero/velero/test/e2e/debug-bundle* From 0d6b5a4f9b3d3d7e4ba366dfdc5f6bc08027eff2 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Wed, 8 Jul 2026 10:58:26 +0800 Subject: [PATCH 025/194] add fallback for unresolved kinds via peek-and-map When a user specifies a Custom Resource Kind in a restore filter policy (e.g., kinds: [MyCustomKind]), the discovery helper fails to resolve it if the CRD hasn't been restored yet. This adds a peek-and-map fallback: if a resource type in the backup tarball doesn't match the resolved filters, Velero peeks at the actual Kind of the first item in the tarball and matches it against the user's original policy strings. Signed-off-by: Adam Zhang --- .../fine-grained-restore-filters-design.md | 14 +++++ pkg/restore/restore.go | 46 ++++++++++++++++ pkg/restore/restore_test.go | 52 +++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md index 9b02d4c31..4f1de06d5 100644 --- a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md +++ b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md @@ -146,6 +146,20 @@ namespacedFilterPolicies: Only resource kinds listed in `resourceFilters` entries are restored for the matched namespaces; unlisted kinds are implicitly excluded (globally excluded kinds cannot be re-included — see precedence model). +#### Peek-and-Map Fallback for Unresolved Kinds + +The `kinds` field accepts both plural resource names (e.g., `configmaps`, `mycustomkinds.mygroup.io`) and singular `Kind` names (e.g., `ConfigMap`, `MyCustomKind`). + +During a restore, Velero attempts to resolve `Kind` names to fully-qualified plural resource names using the cluster's discovery helper. However, for Custom Resources (CRDs), the CRD might not exist in the cluster yet when the restore begins. + +To handle this, Velero implements a **peek-and-map fallback**: +1. If a `Kind` cannot be resolved via the discovery helper at the start of the restore, Velero stores the raw string as provided in the policy. +2. Later, when iterating through the backup tarball, if Velero encounters a resource type (e.g., `mycustomkinds.mygroup.io`) that doesn't match any resolved filters, it peeks at the `Kind` of the first item in the tarball for that resource type. +3. It then checks if this actual `Kind` matches any of the unresolved strings in the user's policy (case-insensitive). +4. If a match is found, the filter is applied and cached for subsequent lookups. + +This ensures that users can intuitively write `kinds: [MyCustomKind]` and it will work reliably, even if the CRD hasn't been restored yet. This logic applies to both `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. + #### Catch-All Resource Filter (Empty `kinds` or `["*"]`) A `ResourceFilter` entry with an empty (or omitted) `kinds` field, or a field explicitly set to `["*"]`, acts as a **catch-all**. Its `labelSelector` or `orLabelSelectors` (if provided) is applied to **all resource types in the namespace that are not already matched by a kind-specific filter entry**. If no selectors are provided, all unlisted resources are included. Using `["*"]` is highly recommended as it makes the catch-all intention explicit and self-documenting. diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 5c15bf80e..1c93383a4 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -447,6 +447,7 @@ type resolvedResourceFilter struct { labelSelector labels.Selector orLabelSelectors []labels.Selector nameIE *collections.IncludesExcludes + originalKinds []string } type resolvedNamespaceFilter struct { @@ -634,6 +635,7 @@ func resolveResourceFilter( labelSelector: selector, orLabelSelectors: orSelectors, nameIE: nameIE, + originalKinds: rf.Kinds, }, nil } @@ -2608,6 +2610,29 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original if nsFilter := ctx.getNamespaceFilter(originalNamespace); nsFilter != nil { // Resolve effective filter: kind-specific takes precedence over catch-all rf = nsFilter.resourceFilterMap[resource] + + // Peek-and-map logic for unresolvable kinds + if rf == nil && len(items) > 0 { + peekPath := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, items[0]) + // Ignore unmarshal errors during peek; the main restore loop will catch and report them + if obj, err := archive.Unmarshal(ctx.fileSystem, peekPath); err == nil { + actualKind := obj.GroupVersionKind().Kind + for _, filter := range nsFilter.resourceFilterMap { + for _, k := range filter.originalKinds { + if strings.EqualFold(k, actualKind) { + rf = filter + // Cache it for future lookups of this resource + nsFilter.resourceFilterMap[resource] = rf + break + } + } + if rf != nil { + break + } + } + } + } + if rf == nil { rf = nsFilter.catchAllFilter // may be nil if no catch-all } @@ -2618,6 +2643,27 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original if listedRF, ok := ctx.clusterScopedFilterMap[resource]; ok { rf = listedRF useFilterPolicy = true + } else if len(items) > 0 { + // Peek-and-map logic for unresolvable kinds + peekPath := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, items[0]) + // Ignore unmarshal errors during peek; the main restore loop will catch and report them + if obj, err := archive.Unmarshal(ctx.fileSystem, peekPath); err == nil { + actualKind := obj.GroupVersionKind().Kind + for _, filter := range ctx.clusterScopedFilterMap { + for _, k := range filter.originalKinds { + if strings.EqualFold(k, actualKind) { + rf = filter + // Cache it + ctx.clusterScopedFilterMap[resource] = rf + useFilterPolicy = true + break + } + } + if rf != nil { + break + } + } + } } // If kind not listed, fall through to global selectors below } diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index f8a484d58..59e5d17dd 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -25,6 +25,7 @@ import ( "testing" "time" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/collections" @@ -753,6 +754,26 @@ func TestRestoreResourceFiltering(t *testing.T) { apiResources: []*test.APIResource{test.ServiceAccounts()}, want: map[*test.APIResource][]string{test.ServiceAccounts(): {"ns-1/sa-1"}}, }, + { + name: "unresolved kind in namespaced filter policy is still restored via peek-and-map", + restore: defaultRestore().ResourcePoliciesConfigmap("test-policy").Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t).AddItems("mycustomkinds.mygroup.io", + &unstructured.Unstructured{Object: map[string]any{"apiVersion": "mygroup.io/v1", "kind": "MyCustomKind", "metadata": map[string]any{"namespace": "ns-1", "name": "my-cr"}}}, + ).Done(), + apiResources: []*test.APIResource{}, // Empty to simulate discovery failure + want: map[*test.APIResource][]string{}, // We can't assert on the API contents because the fake dynamic client doesn't know about this resource type, but we can verify it doesn't error out and the code path is hit. + }, + { + name: "unresolved kind in cluster-scoped filter policy is still restored via peek-and-map", + restore: defaultRestore().ResourcePoliciesConfigmap("test-policy").Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t).AddItems("myclustercustomkinds.mygroup.io", + &unstructured.Unstructured{Object: map[string]any{"apiVersion": "mygroup.io/v1", "kind": "MyClusterCustomKind", "metadata": map[string]any{"name": "my-cluster-cr"}}}, + ).Done(), + apiResources: []*test.APIResource{}, // Empty to simulate discovery failure + want: map[*test.APIResource][]string{}, // Same here + }, } for _, tc := range tests { @@ -764,6 +785,36 @@ func TestRestoreResourceFiltering(t *testing.T) { } require.NoError(t, h.restorer.discoveryHelper.Refresh()) + if tc.restore.Spec.ResourcePolicy != nil { + var yamlData string + if tc.name == "unresolved kind in namespaced filter policy is still restored via peek-and-map" { + yamlData = ` +version: v1 +namespacedFilterPolicies: + - namespaces: ["ns-1"] + resourceFilters: + - kinds: ["MyCustomKind"] +` + } else if tc.name == "unresolved kind in cluster-scoped filter policy is still restored via peek-and-map" { + yamlData = ` +version: v1 +clusterScopedFilterPolicy: + resourceFilters: + - kinds: ["MyClusterCustomKind"] +` + } + + if yamlData != "" { + cm := builder.ForConfigMap(tc.restore.Namespace, tc.restore.Spec.ResourcePolicy.Name).Data("yaml", yamlData).Result() + err := h.restorer.kbClient.Create(context.TODO(), cm) + require.NoError(t, err) + } + } + + // We need to fetch the policies using the actual function + resPolicies, err := resourcepolicies.GetResourcePoliciesFromRestore(context.TODO(), tc.restore, h.restorer.kbClient, h.log) + require.NoError(t, err) + data := &Request{ Log: h.log, Restore: tc.restore, @@ -771,6 +822,7 @@ func TestRestoreResourceFiltering(t *testing.T) { PodVolumeBackups: nil, VolumeSnapshots: nil, BackupReader: tc.tarball, + ResPolicies: resPolicies, } warnings, errs := h.restorer.Restore( data, From 02b6e16088c0369780df2ab951c43ffe97761a36 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Wed, 8 Jul 2026 11:48:30 +0800 Subject: [PATCH 026/194] add notes about potential data race Signed-off-by: Adam Zhang --- pkg/restore/restore.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 1c93383a4..a4c1369cd 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -476,6 +476,9 @@ func (ctx *restoreContext) getNamespaceFilter(namespace string) *resolvedNamespa } // 2. Walk patterns in definition order (first-match semantics) + // Note: namespaceFilterCache is mutated below without synchronization. This is safe + // today because resource collection runs sequentially. If the restore loop is + // parallelized in the future, these map writes will need a lock to prevent data races. for _, p := range ctx.namespacedFilterPatterns { if p.compiled != nil { if p.compiled.Match(namespace) { @@ -2622,6 +2625,9 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original if strings.EqualFold(k, actualKind) { rf = filter // Cache it for future lookups of this resource + // Note: resourceFilterMap is mutated in place without synchronization. + // This is safe today because resource collection runs sequentially. + // If parallelized in the future, this will need a lock to prevent data races. nsFilter.resourceFilterMap[resource] = rf break } @@ -2653,7 +2659,10 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original for _, k := range filter.originalKinds { if strings.EqualFold(k, actualKind) { rf = filter - // Cache it + // Cache it for future lookups of this resource + // Note: clusterScopedFilterMap is mutated in place without synchronization. + // This is safe today because resource collection runs sequentially. + // If parallelized in the future, this will need a lock to prevent data races. ctx.clusterScopedFilterMap[resource] = rf useFilterPolicy = true break From a9545d785f0d433d1ba7552503c8bdadc8921870 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 8 Jul 2026 17:37:15 +0800 Subject: [PATCH 027/194] Disable fips140 enforcement because Kopia doesn't support it. Signed-off-by: Xun Jiang --- changelogs/unreleased/9974-blackpiglet | 1 + pkg/cmd/cli/datamover/backup.go | 6 +++++- pkg/cmd/cli/datamover/restore.go | 6 +++++- pkg/cmd/cli/podvolume/backup.go | 6 +++++- pkg/cmd/cli/podvolume/restore.go | 6 +++++- pkg/cmd/cli/repomantenance/maintenance.go | 6 +++++- pkg/repository/manager/manager.go | 22 +++++++++++++++++++--- 7 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/9974-blackpiglet diff --git a/changelogs/unreleased/9974-blackpiglet b/changelogs/unreleased/9974-blackpiglet new file mode 100644 index 000000000..5a7d47668 --- /dev/null +++ b/changelogs/unreleased/9974-blackpiglet @@ -0,0 +1 @@ +Disable fips140 enforcement because Kopia doesn't support it. \ No newline at end of file diff --git a/pkg/cmd/cli/datamover/backup.go b/pkg/cmd/cli/datamover/backup.go index f352c0aad..07ac7dc18 100644 --- a/pkg/cmd/cli/datamover/backup.go +++ b/pkg/cmd/cli/datamover/backup.go @@ -15,6 +15,7 @@ package datamover import ( "context" + "crypto/fips140" "fmt" "os" "strings" @@ -87,7 +88,10 @@ func NewBackupCommand(f client.Factory) *cobra.Command { kube.ExitPodWithMessage(logger, false, "Failed to create data mover backup, %v", err) } - s.run() + // Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet. + fips140.WithoutEnforcement(func() { + s.run() + }) }, } diff --git a/pkg/cmd/cli/datamover/restore.go b/pkg/cmd/cli/datamover/restore.go index 1d3cf84f4..ed6867e96 100644 --- a/pkg/cmd/cli/datamover/restore.go +++ b/pkg/cmd/cli/datamover/restore.go @@ -15,6 +15,7 @@ package datamover import ( "context" + "crypto/fips140" "fmt" "os" "strings" @@ -81,7 +82,10 @@ func NewRestoreCommand(f client.Factory) *cobra.Command { kube.ExitPodWithMessage(logger, false, "Failed to create data mover restore, %v", err) } - s.run() + // Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet. + fips140.WithoutEnforcement(func() { + s.run() + }) }, } diff --git a/pkg/cmd/cli/podvolume/backup.go b/pkg/cmd/cli/podvolume/backup.go index 8bef9c574..93014a789 100644 --- a/pkg/cmd/cli/podvolume/backup.go +++ b/pkg/cmd/cli/podvolume/backup.go @@ -15,6 +15,7 @@ package podvolume import ( "context" + "crypto/fips140" "fmt" "os" "strings" @@ -80,7 +81,10 @@ func NewBackupCommand(f client.Factory) *cobra.Command { kube.ExitPodWithMessage(logger, false, "Failed to create pod volume backup, %v", err) } - s.run() + // Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet. + fips140.WithoutEnforcement(func() { + s.run() + }) }, } diff --git a/pkg/cmd/cli/podvolume/restore.go b/pkg/cmd/cli/podvolume/restore.go index ab6554999..f982a5871 100644 --- a/pkg/cmd/cli/podvolume/restore.go +++ b/pkg/cmd/cli/podvolume/restore.go @@ -15,6 +15,7 @@ package podvolume import ( "context" + "crypto/fips140" "fmt" "os" "strings" @@ -79,7 +80,10 @@ func NewRestoreCommand(f client.Factory) *cobra.Command { kube.ExitPodWithMessage(logger, false, "Failed to create pod volume restore, %v", err) } - s.run() + // Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet. + fips140.WithoutEnforcement(func() { + s.run() + }) }, } diff --git a/pkg/cmd/cli/repomantenance/maintenance.go b/pkg/cmd/cli/repomantenance/maintenance.go index f89aba257..d541427a6 100644 --- a/pkg/cmd/cli/repomantenance/maintenance.go +++ b/pkg/cmd/cli/repomantenance/maintenance.go @@ -2,6 +2,7 @@ package repomantenance import ( "context" + "crypto/fips140" "fmt" "os" "strings" @@ -57,7 +58,10 @@ func NewCommand(f velerocli.Factory) *cobra.Command { Hidden: true, Short: "VELERO INTERNAL COMMAND ONLY - not intended to be run directly by users", Run: func(c *cobra.Command, args []string) { - o.Run(f) + // Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet. + fips140.WithoutEnforcement(func() { + o.Run(f) + }) }, } diff --git a/pkg/repository/manager/manager.go b/pkg/repository/manager/manager.go index d34c97624..f8b10db5e 100644 --- a/pkg/repository/manager/manager.go +++ b/pkg/repository/manager/manager.go @@ -18,6 +18,7 @@ package repository import ( "context" + "crypto/fips140" "fmt" "time" @@ -173,7 +174,13 @@ func (m *manager) PrepareRepo(repo *velerov1api.BackupRepository) error { if err != nil { return errors.WithStack(err) } - return prd.PrepareRepo(context.Background(), param) + + // Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet. + var prepareErr error + fips140.WithoutEnforcement(func() { + prepareErr = prd.PrepareRepo(context.Background(), param) + }) + return prepareErr } func (m *manager) PruneRepo(repo *velerov1api.BackupRepository) error { @@ -244,11 +251,20 @@ func (m *manager) BatchForget(ctx context.Context, repo *velerov1api.BackupRepos return []error{errors.WithStack(err)} } - if err := prd.BoostRepoConnect(context.Background(), param); err != nil { + // Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet. + var connectErr error + fips140.WithoutEnforcement(func() { + connectErr = prd.BoostRepoConnect(context.Background(), param) + }) + if connectErr != nil { return []error{errors.WithStack(err)} } - return prd.BatchForget(context.Background(), snapshots, param) + forgetErr := make([]error, 0) + fips140.WithoutEnforcement(func() { + forgetErr = prd.BatchForget(context.Background(), snapshots, param) + }) + return forgetErr } func (m *manager) DefaultMaintenanceFrequency(repo *velerov1api.BackupRepository) (time.Duration, error) { From 56b6ba6b107066bf6ba1ca2948783eed5a2282d7 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 8 Jul 2026 11:36:17 -0700 Subject: [PATCH 028/194] Add image volume type support to volume policies The Kubernetes image volume type (GA in k8s 1.31) was not recognized by Velero's volume type detection logic, causing volume policies with volumeTypes condition set to "image" to be silently ignored. This led to failed fs-backups when defaultVolumesToFsBackup was enabled, since image volumes have no host path for the node agent to back up. Add the "image" SupportedVolume constant and detection in getVolumeTypeFromVolume() so that volume policies can properly match and skip image volumes. Fixes velero-io/velero#9977 Signed-off-by: Shubham Pampattiwar --- internal/resourcepolicies/volume_types_conditions.go | 4 ++++ .../resourcepolicies/volume_types_conditions_test.go | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/internal/resourcepolicies/volume_types_conditions.go b/internal/resourcepolicies/volume_types_conditions.go index 0ee57166b..400af387a 100644 --- a/internal/resourcepolicies/volume_types_conditions.go +++ b/internal/resourcepolicies/volume_types_conditions.go @@ -45,6 +45,7 @@ const ( Glusterfs SupportedVolume = "glusterfs" GCEPersistentDisk SupportedVolume = "gcePersistentDisk" HostPath SupportedVolume = "hostPath" + Image SupportedVolume = "image" ISCSI SupportedVolume = "iscsi" Local SupportedVolume = "local" NFS SupportedVolume = "nfs" @@ -243,5 +244,8 @@ func getVolumeTypeFromVolume(vol *corev1api.Volume) SupportedVolume { if vol.EmptyDir != nil { return EmptyDir } + if vol.Image != nil { + return Image + } return "" } diff --git a/internal/resourcepolicies/volume_types_conditions_test.go b/internal/resourcepolicies/volume_types_conditions_test.go index 7b7be97ee..03f5bbb0b 100644 --- a/internal/resourcepolicies/volume_types_conditions_test.go +++ b/internal/resourcepolicies/volume_types_conditions_test.go @@ -563,6 +563,15 @@ func TestGetVolumeTypeFromVolume(t *testing.T) { }, expected: Ephemeral, }, + { + name: "Test Image", + inputVol: &corev1api.Volume{ + VolumeSource: corev1api.VolumeSource{ + Image: &corev1api.ImageVolumeSource{}, + }, + }, + expected: Image, + }, } for _, tc := range testCases { From e3a3c8902c60cd8c369cb5fc26fc4405d415825b Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 8 Jul 2026 11:39:06 -0700 Subject: [PATCH 029/194] Add changelog for PR #9978 Signed-off-by: Shubham Pampattiwar --- changelogs/unreleased/9978-shubham-pampattiwar | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/9978-shubham-pampattiwar diff --git a/changelogs/unreleased/9978-shubham-pampattiwar b/changelogs/unreleased/9978-shubham-pampattiwar new file mode 100644 index 000000000..856fe087f --- /dev/null +++ b/changelogs/unreleased/9978-shubham-pampattiwar @@ -0,0 +1 @@ +Add image volume type support to volume policies From f3beea83da1aa2a0ba7b9a9cbff84a3aea5f95b9 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Thu, 9 Jul 2026 10:54:16 +0800 Subject: [PATCH 030/194] address review comments - normalized the input to lower case for consistency - added validations for kind collision - add flag for unresolved kinds, and defer skip decision base on that - move peek-and-map test cases to restore_policies_test.go Signed-off-by: Adam Zhang --- .../fine-grained-restore-filters-design.md | 6 +- pkg/controller/restore_controller_test.go | 21 +++-- pkg/restore/restore.go | 84 +++++++++++++------ pkg/restore/restore_policies_test.go | 83 +++++++++++++++++- pkg/restore/restore_test.go | 48 +---------- pkg/test/api_server.go | 2 + 6 files changed, 156 insertions(+), 88 deletions(-) diff --git a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md index 4f1de06d5..0e4107171 100644 --- a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md +++ b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md @@ -150,12 +150,14 @@ Only resource kinds listed in `resourceFilters` entries are restored for the mat The `kinds` field accepts both plural resource names (e.g., `configmaps`, `mycustomkinds.mygroup.io`) and singular `Kind` names (e.g., `ConfigMap`, `MyCustomKind`). +To ensure consistent case-insensitive behavior across all code paths, Velero normalizes all input `kinds` to lowercase *before* attempting discovery or fallback matching. + During a restore, Velero attempts to resolve `Kind` names to fully-qualified plural resource names using the cluster's discovery helper. However, for Custom Resources (CRDs), the CRD might not exist in the cluster yet when the restore begins. To handle this, Velero implements a **peek-and-map fallback**: -1. If a `Kind` cannot be resolved via the discovery helper at the start of the restore, Velero stores the raw string as provided in the policy. +1. If a normalized `Kind` cannot be resolved via the discovery helper at the start of the restore, Velero stores the normalized string as provided in the policy. 2. Later, when iterating through the backup tarball, if Velero encounters a resource type (e.g., `mycustomkinds.mygroup.io`) that doesn't match any resolved filters, it peeks at the `Kind` of the first item in the tarball for that resource type. -3. It then checks if this actual `Kind` matches any of the unresolved strings in the user's policy (case-insensitive). +3. It then checks if this actual `Kind` (case-insensitively) matches any of the unresolved normalized strings in the user's policy. 4. If a match is found, the filter is applied and cached for subsequent lookups. This ensures that users can intuitively write `kinds: [MyCustomKind]` and it will work reliably, even if the CRD hasn't been restored yet. This logic applies to both `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index 062edf9dd..6a2f4d8d1 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -18,7 +18,6 @@ package controller import ( "bytes" - "context" "io" "testing" "time" @@ -786,7 +785,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) { Phase(velerov1api.BackupPhaseCompleted). Result())) - r.validateAndComplete(context.Background(), restore) + r.validateAndComplete(t.Context(), restore) assert.Contains(t, restore.Status.ValidationErrors, "No backups found for schedule") assert.Empty(t, restore.Spec.BackupName) @@ -802,7 +801,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) { Result(), )) - r.validateAndComplete(context.Background(), restore) + r.validateAndComplete(t.Context(), restore) assert.Contains(t, restore.Status.ValidationErrors, "No completed backups found for schedule") assert.Empty(t, restore.Spec.BackupName) @@ -833,7 +832,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) { ScheduleName: "schedule-1", }, } - r.validateAndComplete(context.Background(), restore) + r.validateAndComplete(t.Context(), restore) assert.Nil(t, restore.Status.ValidationErrors) assert.Equal(t, "foo", restore.Spec.BackupName) } @@ -893,7 +892,7 @@ func TestValidateAndCompleteWithResourcePolicySpecified(t *testing.T) { Result(), )) - r.validateAndComplete(context.Background(), restore) + r.validateAndComplete(t.Context(), restore) assert.Contains(t, restore.Status.ValidationErrors[0], "fail to get ResourcePolicies velero/test-configmap ConfigMap") restore1 := &velerov1api.Restore{ @@ -926,7 +925,7 @@ clusterScopedFilterPolicy: } require.NoError(t, r.kbClient.Create(t.Context(), cm1)) - r.validateAndComplete(context.Background(), restore1) + r.validateAndComplete(t.Context(), restore1) assert.Nil(t, restore1.Status.ValidationErrors) restore2 := &velerov1api.Restore{ @@ -963,7 +962,7 @@ volumePolicies: } require.NoError(t, r.kbClient.Create(t.Context(), cm2)) - r.validateAndComplete(context.Background(), restore2) + r.validateAndComplete(t.Context(), restore2) assert.Contains(t, restore2.Status.ValidationErrors[0], "fail to validate ResourcePolicies in ConfigMap velero/test-configmap-invalid") } @@ -1022,7 +1021,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { Result(), )) - r.validateAndComplete(context.Background(), restore) + r.validateAndComplete(t.Context(), restore) assert.Contains(t, restore.Status.ValidationErrors[0], "failed to get resource modifiers configmap") restore1 := &velerov1api.Restore{ @@ -1050,7 +1049,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { } require.NoError(t, r.kbClient.Create(t.Context(), cm1)) - r.validateAndComplete(context.Background(), restore1) + r.validateAndComplete(t.Context(), restore1) assert.Nil(t, restore1.Status.ValidationErrors) restore2 := &velerov1api.Restore{ @@ -1079,7 +1078,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { } require.NoError(t, r.kbClient.Create(t.Context(), invalidVersionCm)) - r.validateAndComplete(context.Background(), restore2) + r.validateAndComplete(t.Context(), restore2) assert.Contains(t, restore2.Status.ValidationErrors[0], "Error in parsing resource modifiers provided in configmap") restore3 := &velerov1api.Restore{ @@ -1107,7 +1106,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { } require.NoError(t, r.kbClient.Create(t.Context(), invalidOperatorCm)) - r.validateAndComplete(context.Background(), restore3) + r.validateAndComplete(t.Context(), restore3) assert.Contains(t, restore3.Status.ValidationErrors[0], "Validation error in resource modifiers provided in configmap") } diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index a4c1369cd..7ff1c3031 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -456,6 +456,9 @@ type resolvedNamespaceFilter struct { // catchAllFilter holds the resolved filter for a catch-all entry (empty kinds or ["*"]). // nil when no catch-all entry is defined. catchAllFilter *resolvedResourceFilter + // hasUnresolvedKinds is true if any kind in the policy failed discovery. + // This is used to bypass the fast-path skip so the peek-and-map fallback can run. + hasUnresolvedKinds bool } // namespacedFilterPattern pairs a namespace pattern string with its pre-compiled @@ -514,17 +517,24 @@ func resolveRestoreClusterScopedFilterPolicy( if err != nil { return nil, err } - for _, kind := range rf.Kinds { - gr, resource, err := helper.ResourceFor(schema.GroupVersionResource{Resource: kind}) + for _, kind := range resolved.originalKinds { + gr, resource, err := helper.ResourceFor(schema.ParseGroupResource(kind).WithVersion("")) + + key := kind if err != nil { log.WithField("kind", kind).Warnf("Cannot resolve kind via discovery, using as-is") - result[kind] = resolved - continue + } else { + if resource.Namespaced { + log.Warnf("kind %q in clusterScopedFilterPolicy is a namespace-scoped resource; it will never match in a cluster-scoped filter — did you mean namespacedFilterPolicies?", kind) + } + key = gr.GroupResource().String() } - if resource.Namespaced { - log.Warnf("kind %q in clusterScopedFilterPolicy is a namespace-scoped resource; it will never match in a cluster-scoped filter — did you mean namespacedFilterPolicies?", kind) + + if _, exists := result[key]; exists { + return nil, fmt.Errorf("ambiguous policy: duplicate kind %q detected", key) } - result[gr.GroupResource().String()] = resolved + + result[key] = resolved } } return result, nil @@ -548,6 +558,7 @@ func resolveRestoreNamespacedFilterPolicies( for _, policy := range policies { rfMap := make(map[string]*resolvedResourceFilter) var catchAll *resolvedResourceFilter + hasUnresolvedKinds := false for _, rf := range policy.ResourceFilters { resolved, err := resolveResourceFilter(rf) @@ -560,35 +571,42 @@ func resolveRestoreNamespacedFilterPolicies( continue } - for _, kind := range rf.Kinds { + for _, kind := range resolved.originalKinds { gr, resource, err := helper.ResourceFor( - schema.GroupVersionResource{Resource: kind}, + schema.ParseGroupResource(kind).WithVersion(""), ) + + key := kind if err != nil { log.WithField("kind", kind).Warnf( "Cannot resolve kind via discovery, using as-is") - rfMap[kind] = resolved - continue + hasUnresolvedKinds = true + } else { + if !resource.Namespaced { + log.Warnf("kind %q in namespacedFilterPolicies is a cluster-scoped resource; it will never match in a namespace-scoped filter — did you mean clusterScopedFilterPolicy?", kind) + } + + if globalExcludes[kind] || globalExcludes[gr.GroupResource().String()] { + log.WithFields(logrus.Fields{ + "kind": kind, + "namespacePattern": strings.Join(policy.Namespaces, ","), + }).Warn("namespacedFilterPolicies entry lists a kind that is globally excluded by RestoreSpec.ExcludedResources; the per-namespace filter entry has no effect") + } + key = gr.GroupResource().String() } - if !resource.Namespaced { - log.Warnf("kind %q in namespacedFilterPolicies is a cluster-scoped resource; it will never match in a namespace-scoped filter — did you mean clusterScopedFilterPolicy?", kind) + if _, exists := rfMap[key]; exists { + return nil, nil, fmt.Errorf("ambiguous policy: duplicate kind %q detected", key) } - if globalExcludes[kind] || globalExcludes[gr.GroupResource().String()] { - log.WithFields(logrus.Fields{ - "kind": kind, - "namespacePattern": strings.Join(policy.Namespaces, ","), - }).Warn("namespacedFilterPolicies entry lists a kind that is globally excluded by RestoreSpec.ExcludedResources; the per-namespace filter entry has no effect") - } - - rfMap[gr.GroupResource().String()] = resolved + rfMap[key] = resolved } } nsFilter := &resolvedNamespaceFilter{ - resourceFilterMap: rfMap, - catchAllFilter: catchAll, + resourceFilterMap: rfMap, + catchAllFilter: catchAll, + hasUnresolvedKinds: hasUnresolvedKinds, } for _, nsPattern := range policy.Namespaces { result[nsPattern] = nsFilter @@ -634,11 +652,17 @@ func resolveResourceFilter( if len(rf.Names) > 0 || len(rf.ExcludedNames) > 0 { nameIE = collections.NewIncludesExcludes().Includes(rf.Names...).Excludes(rf.ExcludedNames...) } + + normalizedKinds := make([]string, len(rf.Kinds)) + for i, k := range rf.Kinds { + normalizedKinds[i] = strings.ToLower(k) + } + return &resolvedResourceFilter{ labelSelector: selector, orLabelSelectors: orSelectors, nameIE: nameIE, - originalKinds: rf.Kinds, + originalKinds: normalizedKinds, }, nil } @@ -2543,7 +2567,7 @@ func (ctx *restoreContext) getOrderedResourceCollection( if namespace != "" && !ctx.resourceMustHave.Has(groupResource.String()) { if nsFilter := ctx.getNamespaceFilter(namespace); nsFilter != nil { _, kindListed := nsFilter.resourceFilterMap[groupResource.String()] - if !kindListed && nsFilter.catchAllFilter == nil { + if !kindListed && nsFilter.catchAllFilter == nil && !nsFilter.hasUnresolvedKinds { ctx.log.Infof("Skipping resource %s in namespace %s: not in resourceFilters", resource, namespace) continue @@ -2643,6 +2667,11 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original rf = nsFilter.catchAllFilter // may be nil if no catch-all } useFilterPolicy = true + + if rf == nil { + ctx.log.Infof("Skipping resource %s in namespace %s: not in resourceFilters", resource, originalNamespace) + return restorable, warnings, errs + } } } else if ctx.clusterScopedFilterMap != nil { // Cluster-scoped path: only applies if kind is listed (refinement overlay) @@ -2650,7 +2679,10 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original rf = listedRF useFilterPolicy = true } else if len(items) > 0 { - // Peek-and-map logic for unresolvable kinds + // Peek-and-map logic for unresolvable kinds. + // Note: Unlike the namespaced path, this fallback is always reachable + // because the main restore loop does not have a fast-path skip for + // unlisted cluster-scoped resources. peekPath := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, items[0]) // Ignore unmarshal errors during peek; the main restore loop will catch and report them if obj, err := archive.Unmarshal(ctx.fileSystem, peekPath); err == nil { diff --git a/pkg/restore/restore_policies_test.go b/pkg/restore/restore_policies_test.go index 26fe20aab..795b0315b 100644 --- a/pkg/restore/restore_policies_test.go +++ b/pkg/restore/restore_policies_test.go @@ -1,7 +1,6 @@ package restore import ( - "context" "io" "testing" @@ -9,6 +8,7 @@ import ( "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -19,6 +19,9 @@ import ( ) func TestRestoreResourcePoliciesFiltering(t *testing.T) { + customKindRes := &test.APIResource{Group: "mygroup.io", Version: "v1", Name: "mycustomkinds", Kind: "MyCustomKind", Namespaced: true} + clusterCustomKindRes := &test.APIResource{Group: "mygroup.io", Version: "v1", Name: "myclustercustomkinds", Kind: "MyClusterCustomKind", Namespaced: false} + tests := []struct { name string restore *velerov1api.Restore @@ -150,6 +153,45 @@ namespacedFilterPolicies: test.Deployments(): {"ns-1/deploy-1"}, }, }, + { + name: "unresolved kind in namespaced filter policy is still restored via peek-and-map", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + policyYAML: `version: v1 +namespacedFilterPolicies: + - namespaces: ["ns-1"] + resourceFilters: + - kinds: ["MyCustomKind"] +`, + tarball: test.NewTarWriter(t).AddItems("mycustomkinds.mygroup.io", + &unstructured.Unstructured{Object: map[string]any{"apiVersion": "mygroup.io/v1", "kind": "MyCustomKind", "metadata": map[string]any{"namespace": "ns-1", "name": "my-cr"}}}, + ).Done(), + apiResources: []*test.APIResource{ + customKindRes, + }, + want: map[*test.APIResource][]string{ + customKindRes: {"ns-1/my-cr"}, + }, + }, + { + name: "unresolved kind in cluster-scoped filter policy is still restored via peek-and-map", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + policyYAML: `version: v1 +clusterScopedFilterPolicy: + resourceFilters: + - kinds: ["MyClusterCustomKind"] +`, + tarball: test.NewTarWriter(t).AddItems("myclustercustomkinds.mygroup.io", + &unstructured.Unstructured{Object: map[string]any{"apiVersion": "mygroup.io/v1", "kind": "MyClusterCustomKind", "metadata": map[string]any{"name": "my-cluster-cr"}}}, + ).Done(), + apiResources: []*test.APIResource{ + clusterCustomKindRes, + }, + want: map[*test.APIResource][]string{ + clusterCustomKindRes: {"/my-cluster-cr"}, + }, + }, } for _, tc := range tests { @@ -180,7 +222,7 @@ namespacedFilterPolicies: Name: "test-policies", } var err error - resPolicies, err = resourcepolicies.GetResourcePoliciesFromRestore(context.Background(), restore, client, logrus.New()) + resPolicies, err = resourcepolicies.GetResourcePoliciesFromRestore(t.Context(), restore, client, logrus.New()) require.NoError(t, err) } @@ -204,3 +246,40 @@ namespacedFilterPolicies: }) } } + +func TestResolveRestoreNamespacedFilterPolicies_Validation(t *testing.T) { + log := logrus.New() + helper := test.NewFakeDiscoveryHelper(true, nil) + + policies := []resourcepolicies.NamespacedFilterPolicy{ + { + Namespaces: []string{"ns-1"}, + ResourceFilters: []resourcepolicies.ResourceFilter{ + { + Kinds: []string{"MyKind", "mykind"}, + }, + }, + }, + } + + _, _, err := resolveRestoreNamespacedFilterPolicies(policies, nil, helper, log) + require.Error(t, err) + require.Contains(t, err.Error(), "ambiguous policy: duplicate kind") +} + +func TestResolveRestoreClusterScopedFilterPolicy_Validation(t *testing.T) { + log := logrus.New() + helper := test.NewFakeDiscoveryHelper(true, nil) + + policy := &resourcepolicies.ClusterScopedFilterPolicy{ + ResourceFilters: []resourcepolicies.ResourceFilter{ + { + Kinds: []string{"MyKind", "mykind"}, + }, + }, + } + + _, err := resolveRestoreClusterScopedFilterPolicy(policy, helper, log) + require.Error(t, err) + require.Contains(t, err.Error(), "ambiguous policy: duplicate kind") +} diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 59e5d17dd..6863784fb 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -754,26 +754,6 @@ func TestRestoreResourceFiltering(t *testing.T) { apiResources: []*test.APIResource{test.ServiceAccounts()}, want: map[*test.APIResource][]string{test.ServiceAccounts(): {"ns-1/sa-1"}}, }, - { - name: "unresolved kind in namespaced filter policy is still restored via peek-and-map", - restore: defaultRestore().ResourcePoliciesConfigmap("test-policy").Result(), - backup: defaultBackup().Result(), - tarball: test.NewTarWriter(t).AddItems("mycustomkinds.mygroup.io", - &unstructured.Unstructured{Object: map[string]any{"apiVersion": "mygroup.io/v1", "kind": "MyCustomKind", "metadata": map[string]any{"namespace": "ns-1", "name": "my-cr"}}}, - ).Done(), - apiResources: []*test.APIResource{}, // Empty to simulate discovery failure - want: map[*test.APIResource][]string{}, // We can't assert on the API contents because the fake dynamic client doesn't know about this resource type, but we can verify it doesn't error out and the code path is hit. - }, - { - name: "unresolved kind in cluster-scoped filter policy is still restored via peek-and-map", - restore: defaultRestore().ResourcePoliciesConfigmap("test-policy").Result(), - backup: defaultBackup().Result(), - tarball: test.NewTarWriter(t).AddItems("myclustercustomkinds.mygroup.io", - &unstructured.Unstructured{Object: map[string]any{"apiVersion": "mygroup.io/v1", "kind": "MyClusterCustomKind", "metadata": map[string]any{"name": "my-cluster-cr"}}}, - ).Done(), - apiResources: []*test.APIResource{}, // Empty to simulate discovery failure - want: map[*test.APIResource][]string{}, // Same here - }, } for _, tc := range tests { @@ -785,34 +765,8 @@ func TestRestoreResourceFiltering(t *testing.T) { } require.NoError(t, h.restorer.discoveryHelper.Refresh()) - if tc.restore.Spec.ResourcePolicy != nil { - var yamlData string - if tc.name == "unresolved kind in namespaced filter policy is still restored via peek-and-map" { - yamlData = ` -version: v1 -namespacedFilterPolicies: - - namespaces: ["ns-1"] - resourceFilters: - - kinds: ["MyCustomKind"] -` - } else if tc.name == "unresolved kind in cluster-scoped filter policy is still restored via peek-and-map" { - yamlData = ` -version: v1 -clusterScopedFilterPolicy: - resourceFilters: - - kinds: ["MyClusterCustomKind"] -` - } - - if yamlData != "" { - cm := builder.ForConfigMap(tc.restore.Namespace, tc.restore.Spec.ResourcePolicy.Name).Data("yaml", yamlData).Result() - err := h.restorer.kbClient.Create(context.TODO(), cm) - require.NoError(t, err) - } - } - // We need to fetch the policies using the actual function - resPolicies, err := resourcepolicies.GetResourcePoliciesFromRestore(context.TODO(), tc.restore, h.restorer.kbClient, h.log) + resPolicies, err := resourcepolicies.GetResourcePoliciesFromRestore(t.Context(), tc.restore, h.restorer.kbClient, h.log) require.NoError(t, err) data := &Request{ diff --git a/pkg/test/api_server.go b/pkg/test/api_server.go index dd5b0a07a..63975014a 100644 --- a/pkg/test/api_server.go +++ b/pkg/test/api_server.go @@ -56,6 +56,8 @@ func NewAPIServer(t *testing.T) *APIServer { {Group: "extensions", Version: "v1", Resource: "deployments"}: "ExtDeploymentsList", {Group: "velero.io", Version: "v1", Resource: "deployments"}: "VeleroDeploymentsList", {Group: "velero.io", Version: "v2alpha1", Resource: "datauploads"}: "DataUploadsList", + {Group: "mygroup.io", Version: "v1", Resource: "mycustomkinds"}: "MyCustomKindList", + {Group: "mygroup.io", Version: "v1", Resource: "myclustercustomkinds"}: "MyClusterCustomKindList", }) discoveryClient = &DiscoveryClient{FakeDiscovery: kubeClient.Discovery().(*discoveryfake.FakeDiscovery)} ) From 84bee825758ef06c288fd3744c528023f615833e Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 9 Jul 2026 11:49:18 +0800 Subject: [PATCH 031/194] block uploader backup implementation Signed-off-by: Lyndon-Li --- changelogs/unreleased/9979-Lyndon-Li | 1 + pkg/uploader/block/uploader.go | 32 ++++++++++++++-------------- pkg/uploader/block/uploader_test.go | 26 +++++++++++----------- 3 files changed, 30 insertions(+), 29 deletions(-) create mode 100644 changelogs/unreleased/9979-Lyndon-Li diff --git a/changelogs/unreleased/9979-Lyndon-Li b/changelogs/unreleased/9979-Lyndon-Li new file mode 100644 index 000000000..78134da35 --- /dev/null +++ b/changelogs/unreleased/9979-Lyndon-Li @@ -0,0 +1 @@ +Add the backup implementation for block data mover \ No newline at end of file diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 9d4dde9cb..75e913cb7 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -71,8 +71,8 @@ func NewUploader(ctx context.Context, repoWriter udmrepo.BackupRepo, progress up } } -func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitmap cbt.Iterator, configs map[string]string) (udmrepo.Snapshot, int64, error) { - snapStart := bu.repoWriter.Time() +func (blkup *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitmap cbt.Iterator, configs map[string]string) (udmrepo.Snapshot, int64, error) { + snapStart := blkup.repoWriter.Time() if bitmap == nil { return udmrepo.Snapshot{}, 0, errors.New("bitmap is not available") @@ -83,7 +83,7 @@ func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitm backupMode = udmrepo.ObjectDataBackupModeFull } - destObj, err := bu.repoWriter.NewObjectWriter(bu.ctx, udmrepo.ObjectWriteOptions{ + destObj, err := blkup.repoWriter.NewObjectWriter(blkup.ctx, udmrepo.ObjectWriteOptions{ Description: "BDEV:" + getObjectName(source.realSource), DataType: udmrepo.ObjectDataTypeData, AccessMode: udmrepo.ObjectDataAccessModeBlock, @@ -97,12 +97,12 @@ func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitm defer destObj.Close() - id, backupSize, objectSize, err := bu.backupObject(source.dev, destObj, bitmap, source.size) + id, backupSize, objectSize, err := blkup.backupObject(source.dev, destObj, bitmap, source.size) if err != nil { return udmrepo.Snapshot{}, 0, errors.Wrapf(err, "error backing up bdev %s", source.realSource) } - entryId, err := bu.repoWriter.WriteMetadata(bu.ctx, &udmrepo.Metadata{ + entryID, err := blkup.repoWriter.WriteMetadata(blkup.ctx, &udmrepo.Metadata{ SubObjects: []udmrepo.ObjectMetadata{ { ID: id, @@ -120,7 +120,7 @@ func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitm return udmrepo.Snapshot{}, 0, errors.Wrap(err, "error writing metadata") } - snapEnd := bu.repoWriter.Time() + snapEnd := blkup.repoWriter.Time() return udmrepo.Snapshot{ Source: source.realSource, @@ -129,7 +129,7 @@ func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitm Description: source.realSource, TotalSize: objectSize, RootObject: udmrepo.ObjectMetadata{ - ID: entryId, + ID: entryID, Name: "bdev-root", Type: udmrepo.ObjectDataTypeMetadata, Permissions: 0o777, @@ -138,12 +138,12 @@ func (bu *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, bitm } // TODO implement in following PRs -func (bu *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bitmap cbt.Iterator, configs map[string]string) (int64, error) { +func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bitmap cbt.Iterator, configs map[string]string) (int64, error) { return 0, errors.New("not implemented") } -func (bu *blockUploader) backupObject(dev *os.File, dest udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (udmrepo.ID, int64, int64, error) { - backupSize, objectSize, err := bu.backupData(dev, dest, bitmap, totalLength) +func (blkup *blockUploader) backupObject(dev *os.File, dest udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (udmrepo.ID, int64, int64, error) { + backupSize, objectSize, err := blkup.backupData(dev, dest, bitmap, totalLength) if err != nil { return "", backupSize, objectSize, err } @@ -165,7 +165,7 @@ func (r *readResult) resetBuffer(list *freelist.FreeList) { } } -func (bu *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (int64, int64, error) { +func (blkup *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (int64, int64, error) { blockSize := bitmap.BlockSize() list := freelist.New(bufferSize, int(blockSize)) resultChan := make(chan readResult, list.Capacity()) @@ -182,7 +182,7 @@ func (bu *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.ObjectWri var buffer []byte for valid { select { - case <-bu.ctx.Done(): + case <-blkup.ctx.Done(): return case <-quit: return @@ -229,11 +229,11 @@ func (bu *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.ObjectWri for curCount < int64(totalCount) { select { - case <-bu.ctx.Done(): + case <-blkup.ctx.Done(): writeErr = ErrCanceled case result, readerRunning = <-resultChan: if !readerRunning { - if bu.ctx.Err() != nil { + if blkup.ctx.Err() != nil { writeErr = ErrCanceled } else { writeErr = io.ErrUnexpectedEOF @@ -266,7 +266,7 @@ func (bu *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.ObjectWri result.resetBuffer(list) curCount++ - bu.progress.UpdateProgress(&uploader.Progress{BytesDone: lastPos, TotalBytes: aligned}) + blkup.progress.UpdateProgress(&uploader.Progress{BytesDone: lastPos, TotalBytes: aligned}) } result.resetBuffer(list) @@ -283,7 +283,7 @@ func (bu *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.ObjectWri written += s - bu.progress.UpdateProgress(&uploader.Progress{BytesDone: aligned, TotalBytes: aligned}) + blkup.progress.UpdateProgress(&uploader.Progress{BytesDone: aligned, TotalBytes: aligned}) } return written, aligned, nil diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index 2032e31ad..88fd4771e 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - "github.com/pkg/errors" + "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -53,12 +53,12 @@ func TestNewUploader(t *testing.T) { uploader := NewUploader(ctx, repoWriter, progress, log) - bu, ok := uploader.(*blockUploader) + blkup, ok := uploader.(*blockUploader) assert.True(t, ok) - assert.Equal(t, ctx, bu.ctx) - assert.Equal(t, repoWriter, bu.repoWriter) - assert.Equal(t, progress, bu.progress) - assert.Equal(t, log, bu.log) + assert.Equal(t, ctx, blkup.ctx) + assert.Equal(t, repoWriter, blkup.repoWriter) + assert.Equal(t, progress, blkup.progress) + assert.Equal(t, log, blkup.log) } func TestGetObjectName(t *testing.T) { @@ -158,7 +158,7 @@ func TestCopyTailData(t *testing.T) { if tc.expectErr { assert.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, tc.expected, n) } }) @@ -261,9 +261,9 @@ func TestBlockUploaderBackup(t *testing.T) { log := logrus.New() log.Out = io.Discard - bu := NewUploader(ctx, repoWriter, progress, log) + blkup := NewUploader(ctx, repoWriter, progress, log) - f, err := os.CreateTemp("", "blktest-*") + f, err := os.CreateTemp(t.TempDir(), "blktest-*") require.NoError(t, err) defer os.Remove(f.Name()) defer f.Close() @@ -317,7 +317,7 @@ func TestBlockUploaderBackup(t *testing.T) { } else if tc.cancelCtx { iterMock.On("BlockSize").Return(uint(1048576)) iterMock.On("Count").Return(uint64(1)) - iterMock.On("Next").Return(uint64(0), true) + iterMock.On("Next").Return(uint64(0), true).Maybe() objWriter.On("Result").Return(udmrepo.ID(""), errors.New("write failed")).Maybe() } else if tc.shortWrite { @@ -361,15 +361,15 @@ func TestBlockUploaderBackup(t *testing.T) { })).Return(objWriter, tc.createObjErr) } - snap, size, err := bu.Backup(srcInfo, tc.parentObj, iterator, nil) + snap, size, err := blkup.Backup(srcInfo, tc.parentObj, iterator, nil) if tc.expectErr { - assert.Error(t, err) + require.Error(t, err) if tc.expectErrStr != "" { assert.Contains(t, err.Error(), tc.expectErrStr) } } else { - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "/data/volume1", snap.Source) assert.Equal(t, udmrepo.ID("meta-01"), snap.RootObject.ID) assert.Equal(t, int64(0), size) From d2342532f4c7186c4e0ae6de9464431c65b4a147 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: Thu, 9 Jul 2026 17:41:55 +0800 Subject: [PATCH 032/194] Use forward slash as the path separator to make sure it works on both Linux and Windows nodes (#9968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use forward slash as the path separator to make sure it works on both Linux and Windows nodes Signed-off-by: Wenkai Yin(尹文开) --- changelogs/unreleased/9968-ywk253100 | 1 + pkg/install/daemonset.go | 6 +++--- pkg/install/daemonset_test.go | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/9968-ywk253100 diff --git a/changelogs/unreleased/9968-ywk253100 b/changelogs/unreleased/9968-ywk253100 new file mode 100644 index 000000000..e152a5f49 --- /dev/null +++ b/changelogs/unreleased/9968-ywk253100 @@ -0,0 +1 @@ +Use forward slash as the path separator to make sure it works on both Linux and Windows nodes \ No newline at end of file diff --git a/pkg/install/daemonset.go b/pkg/install/daemonset.go index 10d2764e6..190e785d8 100644 --- a/pkg/install/daemonset.go +++ b/pkg/install/daemonset.go @@ -18,7 +18,7 @@ package install import ( "fmt" - "path/filepath" + "path" "strings" appsv1api "k8s.io/api/apps/v1" @@ -68,8 +68,8 @@ func DaemonSet(namespace string, opts ...podTemplateOption) *appsv1api.DaemonSet if c.forWindows { dsName = "node-agent-windows" } - hostPodsVolumePath := filepath.Join(c.kubeletRootDir, "pods") - hostPluginsVolumePath := filepath.Join(c.kubeletRootDir, "plugins") + hostPodsVolumePath := path.Join(strings.ReplaceAll(c.kubeletRootDir, "\\", "/"), "pods") + hostPluginsVolumePath := path.Join(strings.ReplaceAll(c.kubeletRootDir, "\\", "/"), "plugins") volumes := []corev1api.Volume{} volumeMounts := []corev1api.VolumeMount{} if !c.nodeAgentDisableHostPath { diff --git a/pkg/install/daemonset_test.go b/pkg/install/daemonset_test.go index 0f4de11bd..6cab7f063 100644 --- a/pkg/install/daemonset_test.go +++ b/pkg/install/daemonset_test.go @@ -86,6 +86,10 @@ func TestDaemonSet(t *testing.T) { assert.Equal(t, "/data/test/kubelet/pods", ds.Spec.Template.Spec.Volumes[0].HostPath.Path) assert.Equal(t, "/data/test/kubelet/plugins", ds.Spec.Template.Spec.Volumes[1].HostPath.Path) + ds = DaemonSet("velero", WithKubeletRootDir(`C:\var\lib\kubelet`)) + assert.Equal(t, "C:/var/lib/kubelet/pods", ds.Spec.Template.Spec.Volumes[0].HostPath.Path) + assert.Equal(t, "C:/var/lib/kubelet/plugins", ds.Spec.Template.Spec.Volumes[1].HostPath.Path) + ds = DaemonSet("velero", WithNodeAgentDisableHostPath(true)) assert.Len(t, ds.Spec.Template.Spec.Volumes, 1) assert.Len(t, ds.Spec.Template.Spec.Containers[0].VolumeMounts, 1) From c1cd00ff0700a84c4103d9b1d785e75648f907d1 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Fri, 10 Jul 2026 10:56:56 +0800 Subject: [PATCH 033/194] add cli for create/view restore resource policies (#9966) Added CLI for creating restore resource policies, and view the resource policies associated with resource if present. Only list the name of the configmap for now. Signed-off-by: Adam Zhang --- changelogs/unreleased/9966-adam-jian-zhang | 1 + pkg/cmd/cli/restore/create.go | 22 +++++++++++- pkg/cmd/cli/restore/create_test.go | 39 ++++++++++++++++++++++ pkg/cmd/util/output/restore_describer.go | 5 +++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/9966-adam-jian-zhang diff --git a/changelogs/unreleased/9966-adam-jian-zhang b/changelogs/unreleased/9966-adam-jian-zhang new file mode 100644 index 000000000..c95540c80 --- /dev/null +++ b/changelogs/unreleased/9966-adam-jian-zhang @@ -0,0 +1 @@ +Fix issue #9937, add CLI support for restore filters via resource policy diff --git a/pkg/cmd/cli/restore/create.go b/pkg/cmd/cli/restore/create.go index 580bb36b9..3f59b6a6b 100644 --- a/pkg/cmd/cli/restore/create.go +++ b/pkg/cmd/cli/restore/create.go @@ -32,6 +32,7 @@ import ( kbclient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/internal/resourcemodifiers" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" @@ -61,7 +62,13 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command { velero restore create --from-schedule schedule-1 --allow-partially-failed # Create a restore for only persistentvolumeclaims and persistentvolumes within a backup. - velero restore create --from-backup backup-2 --include-resources persistentvolumeclaims,persistentvolumes`, + velero restore create --from-backup backup-2 --include-resources persistentvolumeclaims,persistentvolumes + +Notes: +- Global filters (--include-resources, --selector, etc.) apply to all included namespaces +- Namespace-scoped filters defined in --resource-policies-configmap refine global filters for matching namespaces (globally excluded kinds cannot be re-included) +- Fine-grained global filter policies defined in --resource-policies-configmap refine global filters for cluster-scoped resources +- Use 'velero restore describe' to view the referenced resource policies ConfigMap after restore creation`, Args: cobra.MaximumNArgs(1), Run: func(c *cobra.Command, args []string) { cmd.CheckError(o.Complete(args, f)) @@ -100,6 +107,7 @@ type CreateOptions struct { AllowPartiallyFailed flag.OptionalBool ItemOperationTimeout time.Duration ResourceModifierConfigMap string + ResourcePoliciesConfigMap string WriteSparseFiles flag.OptionalBool ParallelFilesDownload int client kbclient.WithWatch @@ -154,6 +162,8 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { flags.StringVar(&o.ResourceModifierConfigMap, "resource-modifier-configmap", "", "Reference to the resource modifier configmap that restore will use") + flags.StringVar(&o.ResourcePoliciesConfigMap, "resource-policies-configmap", "", "Reference to the ConfigMap containing restore resource filter policies") + f = flags.VarPF(&o.WriteSparseFiles, "write-sparse-files", "", "Whether to write sparse files during restoring volumes") f.NoOptDefVal = cmd.TRUE @@ -310,6 +320,15 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { } } + var resPolicies *corev1api.TypedLocalObjectReference + + if o.ResourcePoliciesConfigMap != "" { + resPolicies = &corev1api.TypedLocalObjectReference{ + Kind: resourcepolicies.ConfigmapRefType, + Name: o.ResourcePoliciesConfigMap, + } + } + restore := &api.Restore{ ObjectMeta: metav1.ObjectMeta{ Namespace: f.Namespace(), @@ -332,6 +351,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { PreserveNodePorts: o.PreserveNodePorts.Value, IncludeClusterResources: o.IncludeClusterResources.Value, ResourceModifier: resModifiers, + ResourcePolicy: resPolicies, ItemOperationTimeout: metav1.Duration{ Duration: o.ItemOperationTimeout, }, diff --git a/pkg/cmd/cli/restore/create_test.go b/pkg/cmd/cli/restore/create_test.go index 8cc369dea..9a6a92608 100644 --- a/pkg/cmd/cli/restore/create_test.go +++ b/pkg/cmd/cli/restore/create_test.go @@ -77,6 +77,8 @@ func TestCreateCommand(t *testing.T) { includeClusterResources := "true" allowPartiallyFailed := "true" itemOperationTimeout := "10m0s" + resourceModifierConfigMap := "modifier-cm" + ResourcePoliciesConfigMap := "policies-cm" writeSparseFiles := "true" parallel := 2 flags := new(pflag.FlagSet) @@ -101,6 +103,8 @@ func TestCreateCommand(t *testing.T) { flags.Parse([]string{"--include-cluster-resources", includeClusterResources}) flags.Parse([]string{"--allow-partially-failed", allowPartiallyFailed}) flags.Parse([]string{"--item-operation-timeout", itemOperationTimeout}) + flags.Parse([]string{"--resource-modifier-configmap", resourceModifierConfigMap}) + flags.Parse([]string{"--resource-policies-configmap", ResourcePoliciesConfigMap}) flags.Parse([]string{"--write-sparse-files", writeSparseFiles}) flags.Parse([]string{"--parallel-files-download", "2"}) client := velerotest.NewFakeControllerRuntimeClient(t).(kbclient.WithWatch) @@ -139,6 +143,8 @@ func TestCreateCommand(t *testing.T) { require.Equal(t, includeClusterResources, o.IncludeClusterResources.String()) require.Equal(t, allowPartiallyFailed, o.AllowPartiallyFailed.String()) require.Equal(t, itemOperationTimeout, o.ItemOperationTimeout.String()) + require.Equal(t, resourceModifierConfigMap, o.ResourceModifierConfigMap) + require.Equal(t, ResourcePoliciesConfigMap, o.ResourcePoliciesConfigMap) require.Equal(t, writeSparseFiles, o.WriteSparseFiles.String()) require.Equal(t, parallel, o.ParallelFilesDownload) }) @@ -189,4 +195,37 @@ func TestCreateCommand(t *testing.T) { err := o.Validate(c, []string{}, f) require.Equal(t, "backups.velero.io \"not-exist\" not found", err.Error()) }) + + t.Run("create a restore with resource policies configmap", func(t *testing.T) { + f := &factorymocks.Factory{} + c := NewCreateCommand(f, "") + require.Equal(t, "Create a restore", c.Short) + flags := new(pflag.FlagSet) + o := NewCreateOptions() + o.BindFlags(flags) + + backupName := "backup-with-policies" + ResourcePoliciesConfigMap := "test-policies-cm" + flags.Parse([]string{"--from-backup", backupName}) + flags.Parse([]string{"--resource-policies-configmap", ResourcePoliciesConfigMap}) + + kbclient := velerotest.NewFakeControllerRuntimeClient(t).(kbclient.WithWatch) + backup := builder.ForBackup(cmdtest.VeleroNameSpace, backupName).Phase(velerov1api.BackupPhaseCompleted).Result() + require.NoError(t, kbclient.Create(t.Context(), backup, &controllerclient.CreateOptions{})) + + f.On("Namespace").Return(cmdtest.VeleroNameSpace) + f.On("KubebuilderWatchClient").Return(kbclient, nil) + + require.NoError(t, o.Complete(args, f)) + require.NoError(t, o.Validate(c, []string{}, f)) + require.NoError(t, o.Run(c, f)) + + // Verify the created restore object + createdRestore := &velerov1api.Restore{} + err := kbclient.Get(t.Context(), controllerclient.ObjectKey{Namespace: cmdtest.VeleroNameSpace, Name: name}, createdRestore) + require.NoError(t, err) + require.NotNil(t, createdRestore.Spec.ResourcePolicy) + require.Equal(t, "configmap", createdRestore.Spec.ResourcePolicy.Kind) + require.Equal(t, ResourcePoliciesConfigMap, createdRestore.Spec.ResourcePolicy.Name) + }) } diff --git a/pkg/cmd/util/output/restore_describer.go b/pkg/cmd/util/output/restore_describer.go index a89943e74..c33da9f69 100644 --- a/pkg/cmd/util/output/restore_describer.go +++ b/pkg/cmd/util/output/restore_describer.go @@ -219,6 +219,11 @@ func DescribeRestore( DescribeResourceModifier(d, restore.Spec.ResourceModifier) } + if restore.Spec.ResourcePolicy != nil { + d.Println() + DescribeResourcePolicies(d, restore.Spec.ResourcePolicy) + } + describeUploaderConfigForRestore(d, restore.Spec) d.Println() From 0f50e9eeac8a0b1d11da27fd40e3ba0c80bddaec Mon Sep 17 00:00:00 2001 From: James Hewitt Date: Fri, 10 Jul 2026 10:27:40 +0100 Subject: [PATCH 034/194] File system restore happens in parallel Signed-off-by: James Hewitt --- site/content/docs/main/restore-reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/content/docs/main/restore-reference.md b/site/content/docs/main/restore-reference.md index eec8ad965..82bb9d505 100644 --- a/site/content/docs/main/restore-reference.md +++ b/site/content/docs/main/restore-reference.md @@ -27,7 +27,7 @@ The following is an overview of Velero's restore process that starts after you r 1. The Velero client makes a call to the Kubernetes API server to create a [`Restore`](api-types/restore.md) object. -1. The `RestoreController` notices the new Restore object and performs validation. This includes verifying that the referenced backup is in a usable phase. Only backups in `Completed` or `PartiallyFailed` phase are accepted as restore sources. +1. The `RestoreController` notices the new `Restore` object and performs validation. This includes verifying that the referenced backup is in a usable phase. Only backups in `Completed` or `PartiallyFailed` phase are accepted as restore sources. 1. The `RestoreController` fetches basic information about the backup being restored, like the [BackupStorageLocation](locations.md) (BSL). It also fetches a tarball of the cluster resources in the backup, any volumes that will be restored using File System Backup, and any volume snapshots to be restored. @@ -63,7 +63,7 @@ The following is an overview of Velero's restore process that starts after you r 1. Once the resource is created on the target cluster, Velero may take some additional steps or wait for additional processes to complete before moving onto the next resource to restore. * If the resource is a Pod, the `RestoreController` will execute any [Restore Hooks](restore-hooks.md) and wait for the hook to finish. - * If the resource is a PV restored by File System Backup, the `RestoreController` waits for File System Backup’s restore to complete. The `RestoreController` sets a timeout for any resources restored with File System Backup during a restore. The default timeout is 4 hours, but you can configure this be setting using `--fs-backup-timeout` restore option. + * If the resource is a PV restored by File System Backup, the `RestoreController` starts a File System Backup’s restore. Velero continues to restore more resources while the file system restore is running. The `RestoreController` sets a timeout for any resources restored with File System Backup during a restore. The default timeout is 4 hours, but you can configure this be setting using `--fs-backup-timeout` restore option. The restore will not finish until either the file system restore is completed or times out. * If the resource is a Custom Resource Definition, the `RestoreController` waits for its availability in the cluster. The timeout is 1 minute. If any failures happen finishing these steps, the `RestoreController` will log an error in the restore result and will continue restoring. From ae06d40c690743e76757e5f8854b7d503d1232b9 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Fri, 10 Jul 2026 10:00:43 -0700 Subject: [PATCH 035/194] Validate user-provided labels and annotations in maintenance job (#9982) * Validate user-provided labels and annotations in maintenance job User-provided labels and annotations from maintenance JobConfigs are now validated before being applied to the maintenance Job pod template. Invalid label keys, label values, and annotation keys are skipped with a warning log. This prevents the Kubernetes API from rejecting the entire Job when a user provides labels or annotations that violate naming rules. Additionally, user-provided labels can no longer overwrite the internal RepositoryNameLabel used for job tracking. Fixes velero-io/velero#9981 Signed-off-by: Shubham Pampattiwar * Add tests for label and annotation validation in maintenance job Add test cases to TestBuildJob covering: - Invalid label key is skipped - Invalid label value is skipped - Label value exceeding 63 characters is skipped - User-provided label cannot overwrite RepositoryNameLabel - Invalid annotation key is skipped Also fix a latent test issue where param.BackupRepo was not reset between test cases, and add the missing assertion for expectedPodAnnotation which was defined but never checked. Signed-off-by: Shubham Pampattiwar * Fix gofmt formatting in maintenance test file Signed-off-by: Shubham Pampattiwar * Add changelog for PR #9982 Signed-off-by: Shubham Pampattiwar --------- Signed-off-by: Shubham Pampattiwar --- .../unreleased/9982-shubham-pampattiwar | 1 + pkg/repository/maintenance/maintenance.go | 17 ++ .../maintenance/maintenance_test.go | 276 ++++++++++++++++++ 3 files changed, 294 insertions(+) create mode 100644 changelogs/unreleased/9982-shubham-pampattiwar diff --git a/changelogs/unreleased/9982-shubham-pampattiwar b/changelogs/unreleased/9982-shubham-pampattiwar new file mode 100644 index 000000000..aeb80e8da --- /dev/null +++ b/changelogs/unreleased/9982-shubham-pampattiwar @@ -0,0 +1 @@ +Validate user-provided labels and annotations in maintenance job diff --git a/pkg/repository/maintenance/maintenance.go b/pkg/repository/maintenance/maintenance.go index 2c33c83e2..33c3fb1f8 100644 --- a/pkg/repository/maintenance/maintenance.go +++ b/pkg/repository/maintenance/maintenance.go @@ -34,6 +34,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" @@ -610,6 +611,18 @@ func buildJob( } if config != nil && len(config.PodLabels) > 0 { for k, v := range config.PodLabels { + if k == RepositoryNameLabel { + logger.Warnf("Skipping user-provided label with reserved key %q; this label is managed internally by Velero", k) + continue + } + if errs := validation.IsQualifiedName(k); len(errs) > 0 { + logger.Warnf("Skipping user-provided label with invalid key %q: %s", k, strings.Join(errs, "; ")) + continue + } + if errs := validation.IsValidLabelValue(v); len(errs) > 0 { + logger.Warnf("Skipping user-provided label %q with invalid value %q: %s", k, v, strings.Join(errs, "; ")) + continue + } podLabels[k] = v } } else { @@ -623,6 +636,10 @@ func buildJob( podAnnotations := map[string]string{} if config != nil && len(config.PodAnnotations) > 0 { for k, v := range config.PodAnnotations { + if errs := validation.IsQualifiedName(k); len(errs) > 0 { + logger.Warnf("Skipping user-provided annotation with invalid key %q: %s", k, strings.Join(errs, "; ")) + continue + } podAnnotations[k] = v } } else { diff --git a/pkg/repository/maintenance/maintenance_test.go b/pkg/repository/maintenance/maintenance_test.go index 97eee1148..05fce89e9 100644 --- a/pkg/repository/maintenance/maintenance_test.go +++ b/pkg/repository/maintenance/maintenance_test.go @@ -1224,6 +1224,274 @@ func TestBuildJob(t *testing.T) { }, }, }, + { + name: "Invalid label key is skipped", + m: &velerotypes.JobConfigs{ + PodResources: &kube.PodResources{ + CPURequest: "100m", + MemoryRequest: "128Mi", + CPULimit: "200m", + MemoryLimit: "256Mi", + }, + PodLabels: map[string]string{ + "valid-label": "valid-value", + "INVALID KEY!!": "some-value", + }, + }, + deploy: deploy2, + logLevel: logrus.InfoLevel, + logFormat: logging.NewFormatFlag(), + expectedJobName: "test-123-maintain-job", + expectedError: false, + expectedEnv: []corev1api.EnvVar{ + { + Name: "test-name", + Value: "test-value", + }, + }, + expectedEnvFrom: []corev1api.EnvFromSource{ + { + ConfigMapRef: &corev1api.ConfigMapEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-configmap", + }, + }, + }, + { + SecretRef: &corev1api.SecretEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-secret", + }, + }, + }, + }, + expectedPodLabel: map[string]string{ + RepositoryNameLabel: "test-123", + "valid-label": "valid-value", + }, + expectedSecurityContext: nil, + expectedPodSecurityContext: nil, + expectedImagePullSecrets: []corev1api.LocalObjectReference{ + { + Name: "imagePullSecret1", + }, + }, + }, + { + name: "Invalid label value is skipped", + m: &velerotypes.JobConfigs{ + PodResources: &kube.PodResources{ + CPURequest: "100m", + MemoryRequest: "128Mi", + CPULimit: "200m", + MemoryLimit: "256Mi", + }, + PodLabels: map[string]string{ + "valid-label": "valid-value", + "another-label": "this value has spaces and is invalid", + }, + }, + deploy: deploy2, + logLevel: logrus.InfoLevel, + logFormat: logging.NewFormatFlag(), + expectedJobName: "test-123-maintain-job", + expectedError: false, + expectedEnv: []corev1api.EnvVar{ + { + Name: "test-name", + Value: "test-value", + }, + }, + expectedEnvFrom: []corev1api.EnvFromSource{ + { + ConfigMapRef: &corev1api.ConfigMapEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-configmap", + }, + }, + }, + { + SecretRef: &corev1api.SecretEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-secret", + }, + }, + }, + }, + expectedPodLabel: map[string]string{ + RepositoryNameLabel: "test-123", + "valid-label": "valid-value", + }, + expectedSecurityContext: nil, + expectedPodSecurityContext: nil, + expectedImagePullSecrets: []corev1api.LocalObjectReference{ + { + Name: "imagePullSecret1", + }, + }, + }, + { + name: "Label value exceeding 63 characters is skipped", + m: &velerotypes.JobConfigs{ + PodResources: &kube.PodResources{ + CPURequest: "100m", + MemoryRequest: "128Mi", + CPULimit: "200m", + MemoryLimit: "256Mi", + }, + PodLabels: map[string]string{ + "valid-label": "valid-value", + "long-value-label": "this-value-is-way-too-long-for-a-kubernetes-label-value-and-exceeds-sixty-three-characters", + }, + }, + deploy: deploy2, + logLevel: logrus.InfoLevel, + logFormat: logging.NewFormatFlag(), + expectedJobName: "test-123-maintain-job", + expectedError: false, + expectedEnv: []corev1api.EnvVar{ + { + Name: "test-name", + Value: "test-value", + }, + }, + expectedEnvFrom: []corev1api.EnvFromSource{ + { + ConfigMapRef: &corev1api.ConfigMapEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-configmap", + }, + }, + }, + { + SecretRef: &corev1api.SecretEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-secret", + }, + }, + }, + }, + expectedPodLabel: map[string]string{ + RepositoryNameLabel: "test-123", + "valid-label": "valid-value", + }, + expectedSecurityContext: nil, + expectedPodSecurityContext: nil, + expectedImagePullSecrets: []corev1api.LocalObjectReference{ + { + Name: "imagePullSecret1", + }, + }, + }, + { + name: "User-provided label cannot overwrite RepositoryNameLabel", + m: &velerotypes.JobConfigs{ + PodResources: &kube.PodResources{ + CPURequest: "100m", + MemoryRequest: "128Mi", + CPULimit: "200m", + MemoryLimit: "256Mi", + }, + PodLabels: map[string]string{ + RepositoryNameLabel: "user-override-attempt", + "valid-label": "valid-value", + }, + }, + deploy: deploy2, + logLevel: logrus.InfoLevel, + logFormat: logging.NewFormatFlag(), + expectedJobName: "test-123-maintain-job", + expectedError: false, + expectedEnv: []corev1api.EnvVar{ + { + Name: "test-name", + Value: "test-value", + }, + }, + expectedEnvFrom: []corev1api.EnvFromSource{ + { + ConfigMapRef: &corev1api.ConfigMapEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-configmap", + }, + }, + }, + { + SecretRef: &corev1api.SecretEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-secret", + }, + }, + }, + }, + expectedPodLabel: map[string]string{ + RepositoryNameLabel: "test-123", + "valid-label": "valid-value", + }, + expectedSecurityContext: nil, + expectedPodSecurityContext: nil, + expectedImagePullSecrets: []corev1api.LocalObjectReference{ + { + Name: "imagePullSecret1", + }, + }, + }, + { + name: "Invalid annotation key is skipped", + m: &velerotypes.JobConfigs{ + PodResources: &kube.PodResources{ + CPURequest: "100m", + MemoryRequest: "128Mi", + CPULimit: "200m", + MemoryLimit: "256Mi", + }, + PodAnnotations: map[string]string{ + "valid-annotation": "any value is fine for annotations, even with spaces!", + "INVALID KEY ANNO!": "some-value", + }, + }, + deploy: deploy2, + logLevel: logrus.InfoLevel, + logFormat: logging.NewFormatFlag(), + expectedJobName: "test-123-maintain-job", + expectedError: false, + expectedEnv: []corev1api.EnvVar{ + { + Name: "test-name", + Value: "test-value", + }, + }, + expectedEnvFrom: []corev1api.EnvFromSource{ + { + ConfigMapRef: &corev1api.ConfigMapEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-configmap", + }, + }, + }, + { + SecretRef: &corev1api.SecretEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-secret", + }, + }, + }, + }, + expectedPodLabel: map[string]string{ + RepositoryNameLabel: "test-123", + "azure.workload.identity/use": "fake-label-value", + }, + expectedPodAnnotation: map[string]string{ + "valid-annotation": "any value is fine for annotations, even with spaces!", + }, + expectedSecurityContext: nil, + expectedPodSecurityContext: nil, + expectedImagePullSecrets: []corev1api.LocalObjectReference{ + { + Name: "imagePullSecret1", + }, + }, + }, } param := provider.RepoParam{ @@ -1245,10 +1513,14 @@ func TestBuildJob(t *testing.T) { }, } + defaultBackupRepo := param.BackupRepo + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if tc.backupRepository != nil { param.BackupRepo = tc.backupRepository + } else { + param.BackupRepo = defaultBackupRepo } // Create a fake clientset with resources @@ -1328,6 +1600,10 @@ func TestBuildJob(t *testing.T) { assert.Equal(t, tc.expectedPodLabel, job.Spec.Template.Labels) + if tc.expectedPodAnnotation != nil { + assert.Equal(t, tc.expectedPodAnnotation, job.Spec.Template.Annotations) + } + assert.Equal(t, tc.expectedImagePullSecrets, job.Spec.Template.Spec.ImagePullSecrets) } }) From 8c79adde743bdfe8b545bdb4ab0ddaf5e1934ed2 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Sat, 11 Jul 2026 09:39:41 +0800 Subject: [PATCH 036/194] fix globalExcludes lookup The kind is normalized to lower case, so should the lookup. Signed-off-by: Adam Zhang --- changelogs/unreleased/9989-adam-jian-zhang | 1 + pkg/restore/restore.go | 4 +- pkg/restore/restore_policies_test.go | 47 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/9989-adam-jian-zhang diff --git a/changelogs/unreleased/9989-adam-jian-zhang b/changelogs/unreleased/9989-adam-jian-zhang new file mode 100644 index 000000000..ab0954c20 --- /dev/null +++ b/changelogs/unreleased/9989-adam-jian-zhang @@ -0,0 +1 @@ +Fix globalExcludes lookup, it should be lookup against lower case diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 7ff1c3031..a1213eec0 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -552,7 +552,9 @@ func resolveRestoreNamespacedFilterPolicies( // Build a quick lookup map for globally excluded resources globalExcludes := make(map[string]bool) for _, ex := range excludedResources { - globalExcludes[ex] = true + // We lowercase the excluded resources here because the kinds in the resource filters + // are lowercased during resolution, and we want to ensure case-insensitive matching. + globalExcludes[strings.ToLower(ex)] = true } for _, policy := range policies { diff --git a/pkg/restore/restore_policies_test.go b/pkg/restore/restore_policies_test.go index 795b0315b..42b8fb11f 100644 --- a/pkg/restore/restore_policies_test.go +++ b/pkg/restore/restore_policies_test.go @@ -2,9 +2,11 @@ package restore import ( "io" + "strings" "testing" "github.com/sirupsen/logrus" + logrustest "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -283,3 +285,48 @@ func TestResolveRestoreClusterScopedFilterPolicy_Validation(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "ambiguous policy: duplicate kind") } + +func TestResolveRestoreNamespacedFilterPolicies_GlobalExcludesWarning(t *testing.T) { + log, hook := logrustest.NewNullLogger() + helper := test.NewFakeDiscoveryHelper(true, nil) + + policies := []resourcepolicies.NamespacedFilterPolicy{ + { + Namespaces: []string{"ns-1"}, + ResourceFilters: []resourcepolicies.ResourceFilter{ + { + Kinds: []string{"ConfigMaps"}, + }, + }, + }, + } + + excludedResources := []string{"ConfigMaps"} // Same case + _, _, err := resolveRestoreNamespacedFilterPolicies(policies, excludedResources, helper, log) + require.NoError(t, err) + + // Check if a warning was emitted + found := false + for _, entry := range hook.Entries { + if entry.Level == logrus.WarnLevel && strings.Contains(entry.Message, "namespacedFilterPolicies entry lists a kind that is globally excluded") { + found = true + break + } + } + require.True(t, found, "expected warning about globally excluded resource") + + hook.Reset() + + excludedResourcesDiffCase := []string{"configmaps"} // Different case + _, _, err = resolveRestoreNamespacedFilterPolicies(policies, excludedResourcesDiffCase, helper, log) + require.NoError(t, err) + + found = false + for _, entry := range hook.Entries { + if entry.Level == logrus.WarnLevel && strings.Contains(entry.Message, "namespacedFilterPolicies entry lists a kind that is globally excluded") { + found = true + break + } + } + require.True(t, found, "expected warning about globally excluded resource even if case differs") +} From cc41347ddf9e841078539e667a5489088ca0df9b Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Sat, 11 Jul 2026 10:29:36 +0800 Subject: [PATCH 037/194] fix rate limit issue for e2e-test-kind job curl api.github.com is subject to rate limit(60 requests per hour), provide GitHub token increase the rate limits. Signed-off-by: Adam Zhang --- .github/workflows/e2e-test-kind.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index 96198a0dc..fc77cb4d3 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -59,8 +59,10 @@ jobs: # Check and build MinIO image once for all e2e tests - name: Check Bitnami MinIO Dockerfile version id: minio-version + env: + GH_TOKEN: ${{ github.token }} run: | - DOCKERFILE_SHA=$(curl -s https://api.github.com/repos/bitnami/containers/commits?path=bitnami/minio/2026/debian-12/Dockerfile\&per_page=1 | jq -r '.[0].sha') + DOCKERFILE_SHA=$(curl -s -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/repos/bitnami/containers/commits?path=bitnami/minio/2026/debian-12/Dockerfile\&per_page=1 | jq -r '.[0].sha') echo "dockerfile_sha=${DOCKERFILE_SHA}" >> $GITHUB_OUTPUT - name: Cache MinIO Image uses: actions/cache@v4 From fa2b37c36b81cf29096c420630e1501288dd5a5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:58:03 +0800 Subject: [PATCH 038/194] Merge pull request #9992 from velero-io/dependabot/github_actions/docker/setup-qemu-action-4 Bump docker/setup-qemu-action from 3 to 4 --- .github/workflows/pr-containers.yml | 2 +- .github/workflows/push.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-containers.yml b/.github/workflows/pr-containers.yml index c2fea1386..910192171 100644 --- a/.github/workflows/pr-containers.yml +++ b/.github/workflows/pr-containers.yml @@ -19,7 +19,7 @@ jobs: - name: Set up QEMU id: qemu - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 with: platforms: all diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 511113264..b45af38d9 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -29,7 +29,7 @@ jobs: - name: Set up QEMU id: qemu - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 with: platforms: all - name: Set up Docker Buildx From c825e3c136bc40bcd9bb6d5b180546c3029beeb2 Mon Sep 17 00:00:00 2001 From: Chlins Zhang Date: Tue, 14 Jul 2026 05:51:54 +0800 Subject: [PATCH 039/194] fix(delete): surface DeleteItemAction plugin errors from InvokeDeleteActions (#9993) Signed-off-by: chlins --- changelogs/unreleased/9993-chlins | 1 + internal/delete/delete_item_action_handler.go | 18 ++++++- .../delete/delete_item_action_handler_test.go | 52 +++++++++++++++++++ pkg/controller/backup_deletion_controller.go | 2 +- 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/9993-chlins diff --git a/changelogs/unreleased/9993-chlins b/changelogs/unreleased/9993-chlins new file mode 100644 index 000000000..79c5236bb --- /dev/null +++ b/changelogs/unreleased/9993-chlins @@ -0,0 +1 @@ +Surface DeleteItemAction plugin errors from InvokeDeleteActions so backup deletion fails and retries instead of silently orphaning data mover snapshots and other private artifacts diff --git a/internal/delete/delete_item_action_handler.go b/internal/delete/delete_item_action_handler.go index 4837d0243..2a16044ee 100644 --- a/internal/delete/delete_item_action_handler.go +++ b/internal/delete/delete_item_action_handler.go @@ -25,6 +25,7 @@ import ( "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" + kubeerrs "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -80,6 +81,15 @@ func InvokeDeleteActions(ctx *Context) error { } processdResources := sets.NewString() + // deleteErrs collects errors returned by DeleteItemAction plugins. We keep + // looping over the remaining items even when a plugin fails, but we must not + // swallow these errors: a DIA failure means the private artifacts it manages + // (e.g. data mover repository snapshots) may not have been deleted. If we + // returned nil here, the caller would proceed to delete the backup and its + // metadata, orphaning those artifacts forever. Returning the aggregated error + // makes the caller fail the deletion so it can be retried. + var deleteErrs []error + for resource := range backupResources { groupResource := schema.ParseGroupResource(resource) @@ -124,15 +134,19 @@ func InvokeDeleteActions(ctx *Context) error { Item: obj, Backup: ctx.Backup, }) - // Since we want to keep looping even on errors, log them instead of just returning. + // Keep looping even on errors so a single failing plugin + // doesn't prevent the remaining items from being cleaned up, + // but record the error so it can be surfaced to the caller. if err != nil { itemLog.WithError(err).Error("plugin error") + deleteErrs = append(deleteErrs, errors.Wrapf(err, + "error executing DeleteItemAction for %s %s", groupResource.String(), obj.GetName())) } } } } } - return nil + return kubeerrs.NewAggregate(deleteErrs) } // getApplicableActions takes resolved DeleteItemActions and filters them for a given group/resource and namespace. diff --git a/internal/delete/delete_item_action_handler_test.go b/internal/delete/delete_item_action_handler_test.go index 6743cd1f9..b7d4e6e6a 100644 --- a/internal/delete/delete_item_action_handler_test.go +++ b/internal/delete/delete_item_action_handler_test.go @@ -17,6 +17,7 @@ limitations under the License. package delete import ( + "errors" "io" "sort" "testing" @@ -276,3 +277,54 @@ func TestInvokeDeleteItemActionsWithNoPlugins(t *testing.T) { err := InvokeDeleteActions(c) require.NoError(t, err) } + +// failingAction is a DeleteItemAction that always returns an error from Execute. +// It is used to verify that InvokeDeleteActions surfaces plugin errors instead +// of swallowing them. +type failingAction struct { + selector velero.ResourceSelector + err error + executed int +} + +func (a *failingAction) AppliesTo() (velero.ResourceSelector, error) { + return a.selector, nil +} + +func (a *failingAction) Execute(input *velero.DeleteItemActionExecuteInput) error { + a.executed++ + return a.err +} + +func TestInvokeDeleteActionsReturnsPluginErrors(t *testing.T) { + fs := test.NewFakeFileSystem() + log := logrus.StandardLogger() + + tarball := test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + Done() + + action := &failingAction{err: errors.New("could not delete artifact")} + + h := newHarness(t) + h.addResource(t, test.Pods()) + + c := &Context{ + Backup: builder.ForBackup("velero", "velero").Result(), + BackupReader: tarball, + Filesystem: fs, + DiscoveryHelper: h.discoveryHelper, + Actions: []velero.DeleteItemAction{action}, + Log: log, + } + + err := InvokeDeleteActions(c) + + // The plugin error must be surfaced so the caller can fail the deletion + // rather than orphaning the artifacts the plugin failed to delete. + require.Error(t, err) + assert.Contains(t, err.Error(), "could not delete artifact") + // The loop must keep going: the action should run for every matching item, + // not stop at the first failure. + assert.Equal(t, 2, action.executed) +} diff --git a/pkg/controller/backup_deletion_controller.go b/pkg/controller/backup_deletion_controller.go index ccac5cd85..cd74a3a27 100644 --- a/pkg/controller/backup_deletion_controller.go +++ b/pkg/controller/backup_deletion_controller.go @@ -295,7 +295,7 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque err = delete.InvokeDeleteActions(deleteCtx) if err != nil { log.WithError(err).Error("Error invoking delete item actions") - err2 := r.patchDeleteBackupRequestWithError(ctx, dbr, errors.New("error invoking delete item actions")) + err2 := r.patchDeleteBackupRequestWithError(ctx, dbr, errors.Wrap(err, "error invoking delete item actions")) return ctrl.Result{}, err2 } } From e593ba73f98cbea9ffa679bb2a880300709ddc9a Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 09:22:52 -0700 Subject: [PATCH 040/194] Fix PodVolumeBackup metadata loss on fs-backup timeout (#9995) * Fix PodVolumeBackup metadata loss on fs-backup timeout When a backup hits the fs-backup timeout, WaitAllPodVolumesProcessed returned nil because PVBs were only collected from the indexer in the done branch of the select. This discarded all PVB metadata including already-completed PVBs, making their data unrestorable. Move the PVB collection loop to run after the select so tracked PVBs are always persisted regardless of timeout. Fixes #9986 Signed-off-by: Shubham Pampattiwar * Add changelog for PR #9995 Signed-off-by: Shubham Pampattiwar * Filter non-completed PVBs in hasPodVolumeBackup After preserving tracked PVBs on timeout, non-completed PVBs (in-progress or with no snapshot ID) would cause hasPodVolumeBackup to return true, leading the restore to skip the original PV and dynamically re-provision it without any data to restore from. Only match PVBs that are Completed with a valid SnapshotID. Signed-off-by: Shubham Pampattiwar * Add unit tests for hasPodVolumeBackup phase filtering Verify that hasPodVolumeBackup only matches PVBs that are Completed with a valid SnapshotID, and rejects in-progress, failed, or empty-snapshot PVBs. Signed-off-by: Shubham Pampattiwar --------- Signed-off-by: Shubham Pampattiwar --- .../unreleased/9995-shubham-pampattiwar | 1 + pkg/podvolume/backupper.go | 27 ++++--- pkg/podvolume/backupper_test.go | 14 +++- pkg/restore/restore.go | 3 + pkg/restore/restore_test.go | 81 +++++++++++++++++++ 5 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 changelogs/unreleased/9995-shubham-pampattiwar diff --git a/changelogs/unreleased/9995-shubham-pampattiwar b/changelogs/unreleased/9995-shubham-pampattiwar new file mode 100644 index 000000000..691ab8d1a --- /dev/null +++ b/changelogs/unreleased/9995-shubham-pampattiwar @@ -0,0 +1 @@ +Fix PodVolumeBackup metadata loss on fs-backup timeout, which caused all fs-backup volumes to become unrestorable diff --git a/pkg/podvolume/backupper.go b/pkg/podvolume/backupper.go index c99ab8a77..5864a2090 100644 --- a/pkg/podvolume/backupper.go +++ b/pkg/podvolume/backupper.go @@ -412,18 +412,21 @@ func (b *backupper) WaitAllPodVolumesProcessed(log logrus.FieldLogger) []*velero case <-b.ctx.Done(): log.Error("timed out waiting for all PodVolumeBackups to complete") case <-done: - for _, obj := range b.pvbIndexer.List() { - pvb, ok := obj.(*velerov1api.PodVolumeBackup) - if !ok { - log.Errorf("expected PodVolumeBackup, but got %T", obj) - continue - } - podVolumeBackups = append(podVolumeBackups, pvb) - if pvb.Status.Phase == velerov1api.PodVolumeBackupPhaseFailed { - log.Errorf("pod volume backup failed: %s", pvb.Status.Message) - } else if pvb.Status.Phase == velerov1api.PodVolumeBackupPhaseCanceled { - log.Errorf("pod volume backup canceled: %s", pvb.Status.Message) - } + } + + // Collect tracked PVBs regardless of whether we timed out or completed normally. + // On timeout, already-completed PVBs must still be persisted so their data remains restorable. + for _, obj := range b.pvbIndexer.List() { + pvb, ok := obj.(*velerov1api.PodVolumeBackup) + if !ok { + log.Errorf("expected PodVolumeBackup, but got %T", obj) + continue + } + podVolumeBackups = append(podVolumeBackups, pvb) + if pvb.Status.Phase == velerov1api.PodVolumeBackupPhaseFailed { + log.Errorf("pod volume backup failed: %s", pvb.Status.Message) + } else if pvb.Status.Phase == velerov1api.PodVolumeBackupPhaseCanceled { + log.Errorf("pod volume backup canceled: %s", pvb.Status.Message) } } return podVolumeBackups diff --git a/pkg/podvolume/backupper_test.go b/pkg/podvolume/backupper_test.go index e6042ede1..66ad9e5ae 100644 --- a/pkg/podvolume/backupper_test.go +++ b/pkg/podvolume/backupper_test.go @@ -757,16 +757,18 @@ func TestWaitAllPodVolumesProcessed(t *testing.T) { statusToBeUpdated *velerov1api.PodVolumeBackupStatus expectedErr string expectedPVBPhase velerov1api.PodVolumeBackupPhase + expectedPVBCount int }{ { name: "contains no pvb should report no error", ctx: timeoutCtx, }, { - name: "context canceled", - ctx: timeoutCtx, - pvb: pvb, - expectedErr: "timed out waiting for all PodVolumeBackups to complete", + name: "context canceled should still return tracked pvbs", + ctx: timeoutCtx, + pvb: pvb, + expectedErr: "timed out waiting for all PodVolumeBackups to complete", + expectedPVBCount: 1, }, { name: "failed pvbs", @@ -834,6 +836,10 @@ func TestWaitAllPodVolumesProcessed(t *testing.T) { assert.Nil(t, logHook.entry) } + if c.expectedPVBCount > 0 { + require.Len(t, pvbs, c.expectedPVBCount) + } + if c.expectedPVBPhase != "" { require.Len(t, pvbs, 1) assert.Equal(t, c.expectedPVBPhase, pvbs[0].Status.Phase) diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index a1213eec0..a71fc4b23 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -2347,6 +2347,9 @@ func hasPodVolumeBackup(unstructuredPV *unstructured.Unstructured, ctx *restoreC var found bool for _, pvb := range ctx.podVolumeBackups { + if pvb.Status.Phase != velerov1api.PodVolumeBackupPhaseCompleted || pvb.Status.SnapshotID == "" { + continue + } if pvb.Spec.Pod.Namespace == pv.Spec.ClaimRef.Namespace && pvb.GetAnnotations()[configs.PVCNameAnnotation] == pv.Spec.ClaimRef.Name { found = true break diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 6863784fb..fc4051387 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -4246,3 +4246,84 @@ func TestDetermineRestoreStatus(t *testing.T) { }) } } + +func TestHasPodVolumeBackup(t *testing.T) { + pvUnstructured := func() *unstructured.Unstructured { + pv := &corev1api.PersistentVolume{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "PersistentVolume"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pv", + }, + Spec: corev1api.PersistentVolumeSpec{ + ClaimRef: &corev1api.ObjectReference{ + Namespace: "test-ns", + Name: "test-pvc", + }, + }, + } + obj, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(pv) + return &unstructured.Unstructured{Object: obj} + } + + makePVB := func(phase velerov1api.PodVolumeBackupPhase, snapshotID string) *velerov1api.PodVolumeBackup { + return &velerov1api.PodVolumeBackup{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "velero.io/pvc-name": "test-pvc", + }, + }, + Spec: velerov1api.PodVolumeBackupSpec{ + Pod: corev1api.ObjectReference{ + Namespace: "test-ns", + }, + }, + Status: velerov1api.PodVolumeBackupStatus{ + Phase: phase, + SnapshotID: snapshotID, + }, + } + } + + tests := []struct { + name string + pvbs []*velerov1api.PodVolumeBackup + expected bool + }{ + { + name: "no pvbs", + pvbs: nil, + expected: false, + }, + { + name: "completed pvb with snapshot ID", + pvbs: []*velerov1api.PodVolumeBackup{makePVB(velerov1api.PodVolumeBackupPhaseCompleted, "snap-123")}, + expected: true, + }, + { + name: "in-progress pvb should not match", + pvbs: []*velerov1api.PodVolumeBackup{makePVB(velerov1api.PodVolumeBackupPhaseInProgress, "")}, + expected: false, + }, + { + name: "completed pvb with empty snapshot ID should not match", + pvbs: []*velerov1api.PodVolumeBackup{makePVB(velerov1api.PodVolumeBackupPhaseCompleted, "")}, + expected: false, + }, + { + name: "failed pvb should not match", + pvbs: []*velerov1api.PodVolumeBackup{makePVB(velerov1api.PodVolumeBackupPhaseFailed, "")}, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := &restoreContext{ + podVolumeBackups: tc.pvbs, + log: logrus.New(), + } + result := hasPodVolumeBackup(pvUnstructured(), ctx) + assert.Equal(t, tc.expected, result) + }) + } +} From 61043244360baba35fb4b466d2bf71aef34e5a77 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 11:06:16 -0700 Subject: [PATCH 041/194] Skip upstream-only workflows on forks Add repository guard (github.repository == 'velero-io/velero') to workflows that should only run on the upstream repo. This prevents unnecessary CI runs on forks like openshift/velero where these workflows either fail due to missing secrets/config or duplicate fork-specific CI. Guarded workflows: auto_assign_prs, auto_label_prs, auto_request_review, e2e-test-kind, nightly-trivy-scan, pr-changelog-check, pr-codespell, pr-filepath-check, pr-linter-check, prow-action, rebase, stale-issues. Intentionally left unguarded: pr-ci-check (useful for contributors on forks), get-go-version (reusable workflow_call only). Signed-off-by: Shubham Pampattiwar --- .github/workflows/auto_assign_prs.yml | 1 + .github/workflows/auto_label_prs.yml | 1 + .github/workflows/auto_request_review.yml | 1 + .github/workflows/e2e-test-kind.yaml | 3 +++ .github/workflows/nightly-trivy-scan.yml | 1 + .github/workflows/pr-changelog-check.yml | 1 + .github/workflows/pr-codespell.yml | 1 + .github/workflows/pr-filepath-check.yml | 1 + .github/workflows/pr-linter-check.yml | 1 + .github/workflows/prow-action.yml | 1 + .github/workflows/rebase.yml | 2 +- .github/workflows/stale-issues.yml | 1 + 12 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto_assign_prs.yml b/.github/workflows/auto_assign_prs.yml index 9b915533c..8966b235e 100644 --- a/.github/workflows/auto_assign_prs.yml +++ b/.github/workflows/auto_assign_prs.yml @@ -14,6 +14,7 @@ permissions: jobs: # Automatically assigns reviewers and owner add-reviews: + if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest steps: - name: Set the author of a PR as the assignee diff --git a/.github/workflows/auto_label_prs.yml b/.github/workflows/auto_label_prs.yml index 042cc7e95..21540d8cb 100644 --- a/.github/workflows/auto_label_prs.yml +++ b/.github/workflows/auto_label_prs.yml @@ -15,6 +15,7 @@ permissions: jobs: # Automatically labels PRs based on file globs in the change. triage: + if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest steps: - uses: actions/labeler@v5 diff --git a/.github/workflows/auto_request_review.yml b/.github/workflows/auto_request_review.yml index 47844bc6c..096e4bdbc 100644 --- a/.github/workflows/auto_request_review.yml +++ b/.github/workflows/auto_request_review.yml @@ -11,6 +11,7 @@ permissions: jobs: auto-request-review: + if: github.repository == 'velero-io/velero' name: Auto Request Review runs-on: ubuntu-latest steps: diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index fc77cb4d3..6e3e4b447 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -17,6 +17,7 @@ jobs: # Build the Velero CLI and image once for all Kubernetes versions, and cache it so the fan-out workers can get it. build: + if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest needs: get-go-version outputs: @@ -81,6 +82,7 @@ jobs: # Create json of k8s versions to test # from guide: https://stackoverflow.com/a/65094398/4590470 setup-test-matrix: + if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} @@ -106,6 +108,7 @@ jobs: # Run E2E test against all Kubernetes versions on kind run-e2e-test: + if: github.repository == 'velero-io/velero' needs: - build - setup-test-matrix diff --git a/.github/workflows/nightly-trivy-scan.yml b/.github/workflows/nightly-trivy-scan.yml index 85ce3cdc5..dc4fa8b9f 100644 --- a/.github/workflows/nightly-trivy-scan.yml +++ b/.github/workflows/nightly-trivy-scan.yml @@ -5,6 +5,7 @@ on: jobs: nightly-scan: + if: github.repository == 'velero-io/velero' name: Trivy nightly scan runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/pr-changelog-check.yml b/.github/workflows/pr-changelog-check.yml index 0f296853a..f9fb14f37 100644 --- a/.github/workflows/pr-changelog-check.yml +++ b/.github/workflows/pr-changelog-check.yml @@ -7,6 +7,7 @@ on: jobs: build: + if: github.repository == 'velero-io/velero' name: Run Changelog Check runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pr-codespell.yml b/.github/workflows/pr-codespell.yml index 65d2a1885..b65ae7ae5 100644 --- a/.github/workflows/pr-codespell.yml +++ b/.github/workflows/pr-codespell.yml @@ -3,6 +3,7 @@ on: [pull_request] jobs: codespell: + if: github.repository == 'velero-io/velero' name: Run Codespell runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pr-filepath-check.yml b/.github/workflows/pr-filepath-check.yml index 260a09dc4..9b8ca593d 100644 --- a/.github/workflows/pr-filepath-check.yml +++ b/.github/workflows/pr-filepath-check.yml @@ -3,6 +3,7 @@ on: [pull_request] jobs: filepath-check: + if: github.repository == 'velero-io/velero' name: Check for invalid characters in file paths runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pr-linter-check.yml b/.github/workflows/pr-linter-check.yml index 6ed7f073d..761cf2fe4 100644 --- a/.github/workflows/pr-linter-check.yml +++ b/.github/workflows/pr-linter-check.yml @@ -13,6 +13,7 @@ jobs: ref: ${{ github.event.pull_request.base.ref }} build: + if: github.repository == 'velero-io/velero' name: Run Linter Check runs-on: ubuntu-latest needs: get-go-version diff --git a/.github/workflows/prow-action.yml b/.github/workflows/prow-action.yml index e247590fe..871f69f8f 100644 --- a/.github/workflows/prow-action.yml +++ b/.github/workflows/prow-action.yml @@ -11,6 +11,7 @@ permissions: jobs: execute: + if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest steps: - uses: jpmcb/prow-github-actions@v1.1.3 diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml index 07c86b534..064bef70a 100644 --- a/.github/workflows/rebase.yml +++ b/.github/workflows/rebase.yml @@ -5,7 +5,7 @@ name: Automatic Rebase jobs: rebase: name: Rebase - if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') + if: github.repository == 'velero-io/velero' && github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') runs-on: ubuntu-latest steps: - name: Checkout the latest code diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 0dffc96c4..99a74872b 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -5,6 +5,7 @@ on: jobs: stale: + if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest steps: - uses: actions/stale@v10.1.1 From 97858c327336f78fd98de8814e5caf223bb1db7e Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 11:08:04 -0700 Subject: [PATCH 042/194] Add changelog for PR #10001 Signed-off-by: Shubham Pampattiwar --- changelogs/unreleased/10001-shubham-pampattiwar | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/10001-shubham-pampattiwar diff --git a/changelogs/unreleased/10001-shubham-pampattiwar b/changelogs/unreleased/10001-shubham-pampattiwar new file mode 100644 index 000000000..d21f5cae5 --- /dev/null +++ b/changelogs/unreleased/10001-shubham-pampattiwar @@ -0,0 +1 @@ +Skip upstream-only workflows on forks From 2edf8f8260ddc85872709c2dc64255f4f0c1efdf Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 12:36:01 -0700 Subject: [PATCH 043/194] Remove guards from e2e-test-kind, pr-linter-check, nightly-trivy-scan Per review feedback, these workflows are useful on forks: - e2e-test-kind: tests pass on downstream forks - pr-linter-check: keeps lint up to date for upstream-bound features - nightly-trivy-scan: wanted in downstream forks Also remove changelog file per reviewer request. Signed-off-by: Shubham Pampattiwar --- .github/workflows/e2e-test-kind.yaml | 3 --- .github/workflows/nightly-trivy-scan.yml | 1 - .github/workflows/pr-linter-check.yml | 1 - changelogs/unreleased/10001-shubham-pampattiwar | 1 - 4 files changed, 6 deletions(-) delete mode 100644 changelogs/unreleased/10001-shubham-pampattiwar diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index 6e3e4b447..fc77cb4d3 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -17,7 +17,6 @@ jobs: # Build the Velero CLI and image once for all Kubernetes versions, and cache it so the fan-out workers can get it. build: - if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest needs: get-go-version outputs: @@ -82,7 +81,6 @@ jobs: # Create json of k8s versions to test # from guide: https://stackoverflow.com/a/65094398/4590470 setup-test-matrix: - if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} @@ -108,7 +106,6 @@ jobs: # Run E2E test against all Kubernetes versions on kind run-e2e-test: - if: github.repository == 'velero-io/velero' needs: - build - setup-test-matrix diff --git a/.github/workflows/nightly-trivy-scan.yml b/.github/workflows/nightly-trivy-scan.yml index dc4fa8b9f..85ce3cdc5 100644 --- a/.github/workflows/nightly-trivy-scan.yml +++ b/.github/workflows/nightly-trivy-scan.yml @@ -5,7 +5,6 @@ on: jobs: nightly-scan: - if: github.repository == 'velero-io/velero' name: Trivy nightly scan runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/pr-linter-check.yml b/.github/workflows/pr-linter-check.yml index 761cf2fe4..6ed7f073d 100644 --- a/.github/workflows/pr-linter-check.yml +++ b/.github/workflows/pr-linter-check.yml @@ -13,7 +13,6 @@ jobs: ref: ${{ github.event.pull_request.base.ref }} build: - if: github.repository == 'velero-io/velero' name: Run Linter Check runs-on: ubuntu-latest needs: get-go-version diff --git a/changelogs/unreleased/10001-shubham-pampattiwar b/changelogs/unreleased/10001-shubham-pampattiwar deleted file mode 100644 index d21f5cae5..000000000 --- a/changelogs/unreleased/10001-shubham-pampattiwar +++ /dev/null @@ -1 +0,0 @@ -Skip upstream-only workflows on forks From 30d05a3e408de8f212fa1829317b9dbe0de53148 Mon Sep 17 00:00:00 2001 From: Daniel Jiang Date: Wed, 15 Jul 2026 07:56:26 +0800 Subject: [PATCH 044/194] Add maintainers as code owners (#9998) Signed-off-by: Daniel Jiang --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..bcc7f34dd --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# maintainers are the overall code owners +* @velero-io/Maintainer \ No newline at end of file From a94e01760b5ef0f9724a1949b7f03097b7f3b83d Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Wed, 15 Jul 2026 11:13:22 +0800 Subject: [PATCH 045/194] fail earlier when PVR pod is not ready Signed-off-by: Lyndon-Li --- changelogs/unreleased/10005-Lyndon-Li | 1 + .../pod_volume_restore_controller.go | 73 ++- .../pod_volume_restore_controller_test.go | 447 +++++++++++------- 3 files changed, 329 insertions(+), 192 deletions(-) create mode 100644 changelogs/unreleased/10005-Lyndon-Li diff --git a/changelogs/unreleased/10005-Lyndon-Li b/changelogs/unreleased/10005-Lyndon-Li new file mode 100644 index 000000000..cd654e978 --- /dev/null +++ b/changelogs/unreleased/10005-Lyndon-Li @@ -0,0 +1 @@ +Fix issue #9973, fail earlier when PVR pod is not ready \ No newline at end of file diff --git a/pkg/controller/pod_volume_restore_controller.go b/pkg/controller/pod_volume_restore_controller.go index 3e2fba39c..12ba49d10 100644 --- a/pkg/controller/pod_volume_restore_controller.go +++ b/pkg/controller/pod_volume_restore_controller.go @@ -236,9 +236,9 @@ func (r *PodVolumeRestoreReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, nil } - shouldProcess, pod, err := shouldProcess(ctx, r.client, log, pvr) + shouldProcess, pod, err := shouldProcess(ctx, r.client, log, pvr, r.resourceTimeout) if err != nil { - return ctrl.Result{}, err + return r.errorOut(ctx, pvr, err, "Pod for this PVR is not ready", log) } if !shouldProcess { return ctrl.Result{}, nil @@ -565,7 +565,7 @@ func UpdatePVRStatusToFailed(ctx context.Context, c client.Client, pvr *velerov1 return err } -func shouldProcess(ctx context.Context, client client.Client, log logrus.FieldLogger, pvr *velerov1api.PodVolumeRestore) (bool, *corev1api.Pod, error) { +func shouldProcess(ctx context.Context, client client.Client, log logrus.FieldLogger, pvr *velerov1api.PodVolumeRestore, timeout time.Duration) (bool, *corev1api.Pod, error) { if !isPVRNew(pvr) { log.Debug("PVR is not new, skip") return false, nil, nil @@ -573,22 +573,63 @@ func shouldProcess(ctx context.Context, client client.Client, log logrus.FieldLo // we filter the pods during the initialization of cache, if we can get a pod here, the pod must be in the same node with the controller // so we don't need to compare the node anymore - pod := &corev1api.Pod{} - if err := client.Get(ctx, types.NamespacedName{Namespace: pvr.Spec.Pod.Namespace, Name: pvr.Spec.Pod.Name}, pod); err != nil { - if apierrors.IsNotFound(err) { - log.WithError(err).Debug("Pod not found on this node, skip") - return false, nil, nil + var targetPod *corev1api.Pod + err := wait.PollUntilContextTimeout(ctx, time.Millisecond*100, timeout, true, func(ctx context.Context) (bool, error) { + updated := &corev1api.Pod{} + if err := client.Get(ctx, types.NamespacedName{Namespace: pvr.Spec.Pod.Namespace, Name: pvr.Spec.Pod.Name}, updated); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + + return false, err + } + + targetPod = updated + + return true, nil + }) + + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return false, nil, errors.Errorf("timeout to wait for pod %s/%s", pvr.Spec.Pod.Namespace, pvr.Spec.Pod.Name) + } else { + return false, nil, errors.Wrapf(err, "error waiting for pod %s/%s", pvr.Spec.Pod.Namespace, pvr.Spec.Pod.Name) } - log.WithError(err).Error("Unable to get pod") - return false, nil, err } - if !isInitContainerRunning(pod) { + if targetPod.Status.Phase == corev1api.PodFailed || targetPod.Status.Phase == corev1api.PodUnknown { + return false, nil, errors.Errorf("unexpected state for pod %s/%s", targetPod.Namespace, targetPod.Name) + } + + idx := getInitContainerIndex(targetPod) + if idx < 0 { + return false, nil, errors.Errorf("no restore-wait init container in pod %s/%s", targetPod.Namespace, targetPod.Name) + } + + if len(targetPod.Status.InitContainerStatuses) <= idx { + log.Debug("Pod init container statuses are not fully populated yet, skip") + return false, nil, nil + } + + containerStatus := targetPod.Status.InitContainerStatuses[idx] + + if containerStatus.State.Terminated != nil { + return false, nil, errors.Errorf("restore-wait init container has already completed in pod %s/%s", targetPod.Namespace, targetPod.Name) + } + + if containerStatus.State.Waiting != nil { + reason := containerStatus.State.Waiting.Reason + if reason == "ImagePullBackOff" || reason == "ErrImageNeverPull" || reason == "CreateContainerConfigError" || reason == "CreateContainerError" || reason == "InvalidImageName" || reason == "ErrImagePull" { + return false, nil, errors.Errorf("restore-wait init container in pod %s/%s is in unrecoverable waiting state with reason %s", targetPod.Namespace, targetPod.Name, reason) + } + } + + if containerStatus.State.Running == nil { log.Debug("Pod is not running restore-wait init container, skip") return false, nil, nil } - return true, pod, nil + return true, targetPod, nil } func (r *PodVolumeRestoreReconciler) closeDataPath(ctx context.Context, pvrName string) { @@ -770,14 +811,6 @@ func isPVRNew(pvr *velerov1api.PodVolumeRestore) bool { return pvr.Status.Phase == "" || pvr.Status.Phase == velerov1api.PodVolumeRestorePhaseNew } -func isInitContainerRunning(pod *corev1api.Pod) bool { - // Pod volume wait container can be anywhere in the list of init containers, but must be running. - i := getInitContainerIndex(pod) - return i >= 0 && - len(pod.Status.InitContainerStatuses)-1 >= i && - pod.Status.InitContainerStatuses[i].State.Running != nil -} - func getInitContainerIndex(pod *corev1api.Pod) int { // Pod volume wait container can be anywhere in the list of init containers so locate it. for i, initContainer := range pod.Spec.InitContainers { diff --git a/pkg/controller/pod_volume_restore_controller_test.go b/pkg/controller/pod_volume_restore_controller_test.go index 4401a7c32..61d34fae3 100644 --- a/pkg/controller/pod_volume_restore_controller_test.go +++ b/pkg/controller/pod_volume_restore_controller_test.go @@ -65,6 +65,8 @@ func TestShouldProcess(t *testing.T) { obj *velerov1api.PodVolumeRestore pod *corev1api.Pod shouldProcessed bool + expectError bool + errString string }{ { name: "InProgress phase pvr should not be processed", @@ -115,6 +117,8 @@ func TestShouldProcess(t *testing.T) { }, }, shouldProcessed: false, + expectError: true, + errString: "timeout to wait for pod ns-1/pod-1", }, { name: "Empty phase pvr with pod on node not running init container should not be processed", @@ -200,6 +204,268 @@ func TestShouldProcess(t *testing.T) { }, shouldProcessed: true, }, + { + name: "pod is in failed phase should return error", + obj: &velerov1api.PodVolumeRestore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "velero", + Name: "pvr-1", + }, + Spec: velerov1api.PodVolumeRestoreSpec{ + Pod: corev1api.ObjectReference{ + Namespace: "ns-1", + Name: "pod-1", + }, + }, + Status: velerov1api.PodVolumeRestoreStatus{ + Phase: "", + }, + }, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "pod-1", + }, + Status: corev1api.PodStatus{ + Phase: corev1api.PodFailed, + }, + }, + shouldProcessed: false, + expectError: true, + errString: "unexpected state for pod", + }, + { + name: "pod is in unknown phase should return error", + obj: &velerov1api.PodVolumeRestore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "velero", + Name: "pvr-1", + }, + Spec: velerov1api.PodVolumeRestoreSpec{ + Pod: corev1api.ObjectReference{ + Namespace: "ns-1", + Name: "pod-1", + }, + }, + Status: velerov1api.PodVolumeRestoreStatus{ + Phase: "", + }, + }, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "pod-1", + }, + Status: corev1api.PodStatus{ + Phase: corev1api.PodUnknown, + }, + }, + shouldProcessed: false, + expectError: true, + errString: "unexpected state for pod", + }, + { + name: "pod with no init containers should return error", + obj: &velerov1api.PodVolumeRestore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "velero", + Name: "pvr-1", + }, + Spec: velerov1api.PodVolumeRestoreSpec{ + Pod: corev1api.ObjectReference{ + Namespace: "ns-1", + Name: "pod-1", + }, + }, + Status: velerov1api.PodVolumeRestoreStatus{ + Phase: "", + }, + }, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "pod-1", + }, + Spec: corev1api.PodSpec{ + NodeName: controllerNode, + }, + }, + shouldProcessed: false, + expectError: true, + errString: "no restore-wait init container", + }, + { + name: "pod init container statuses are not fully populated yet should skip", + obj: &velerov1api.PodVolumeRestore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "velero", + Name: "pvr-1", + }, + Spec: velerov1api.PodVolumeRestoreSpec{ + Pod: corev1api.ObjectReference{ + Namespace: "ns-1", + Name: "pod-1", + }, + }, + Status: velerov1api.PodVolumeRestoreStatus{ + Phase: "", + }, + }, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "pod-1", + }, + Spec: corev1api.PodSpec{ + NodeName: controllerNode, + InitContainers: []corev1api.Container{ + { + Name: restorehelper.WaitInitContainer, + }, + }, + }, + Status: corev1api.PodStatus{ + InitContainerStatuses: []corev1api.ContainerStatus{}, + }, + }, + shouldProcessed: false, + }, + { + name: "restore-wait init container has already completed should return error", + obj: &velerov1api.PodVolumeRestore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "velero", + Name: "pvr-1", + }, + Spec: velerov1api.PodVolumeRestoreSpec{ + Pod: corev1api.ObjectReference{ + Namespace: "ns-1", + Name: "pod-1", + }, + }, + Status: velerov1api.PodVolumeRestoreStatus{ + Phase: "", + }, + }, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "pod-1", + }, + Spec: corev1api.PodSpec{ + NodeName: controllerNode, + InitContainers: []corev1api.Container{ + { + Name: restorehelper.WaitInitContainer, + }, + }, + }, + Status: corev1api.PodStatus{ + InitContainerStatuses: []corev1api.ContainerStatus{ + { + State: corev1api.ContainerState{ + Terminated: &corev1api.ContainerStateTerminated{ + ExitCode: 0, + }, + }, + }, + }, + }, + }, + shouldProcessed: false, + expectError: true, + errString: "restore-wait init container has already completed", + }, + { + name: "restore-wait init container is in unrecoverable waiting state should return error", + obj: &velerov1api.PodVolumeRestore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "velero", + Name: "pvr-1", + }, + Spec: velerov1api.PodVolumeRestoreSpec{ + Pod: corev1api.ObjectReference{ + Namespace: "ns-1", + Name: "pod-1", + }, + }, + Status: velerov1api.PodVolumeRestoreStatus{ + Phase: "", + }, + }, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "pod-1", + }, + Spec: corev1api.PodSpec{ + NodeName: controllerNode, + InitContainers: []corev1api.Container{ + { + Name: restorehelper.WaitInitContainer, + }, + }, + }, + Status: corev1api.PodStatus{ + InitContainerStatuses: []corev1api.ContainerStatus{ + { + State: corev1api.ContainerState{ + Waiting: &corev1api.ContainerStateWaiting{ + Reason: "ImagePullBackOff", + }, + }, + }, + }, + }, + }, + shouldProcessed: false, + expectError: true, + errString: "is in unrecoverable waiting state with reason ImagePullBackOff", + }, + { + name: "restore-wait init container is in normal waiting state should skip", + obj: &velerov1api.PodVolumeRestore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "velero", + Name: "pvr-1", + }, + Spec: velerov1api.PodVolumeRestoreSpec{ + Pod: corev1api.ObjectReference{ + Namespace: "ns-1", + Name: "pod-1", + }, + }, + Status: velerov1api.PodVolumeRestoreStatus{ + Phase: "", + }, + }, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "pod-1", + }, + Spec: corev1api.PodSpec{ + NodeName: controllerNode, + InitContainers: []corev1api.Container{ + { + Name: restorehelper.WaitInitContainer, + }, + }, + }, + Status: corev1api.PodStatus{ + InitContainerStatuses: []corev1api.ContainerStatus{ + { + State: corev1api.ContainerState{ + Waiting: &corev1api.ContainerStateWaiting{ + Reason: "ContainerCreating", + }, + }, + }, + }, + }, + }, + shouldProcessed: false, + }, } for _, ts := range tests { @@ -221,179 +487,16 @@ func TestShouldProcess(t *testing.T) { clock: &clocks.RealClock{}, } - shouldProcess, _, _ := shouldProcess(ctx, c.client, c.logger, ts.obj) + shouldProcess, _, err := shouldProcess(ctx, c.client, c.logger, ts.obj, time.Second) require.Equal(t, ts.shouldProcessed, shouldProcess) - }) - } -} - -func TestIsInitContainerRunning(t *testing.T) { - tests := []struct { - name string - pod *corev1api.Pod - expected bool - }{ - { - name: "pod with no init containers should return false", - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns-1", - Name: "pod-1", - }, - }, - expected: false, - }, - { - name: "pod with running init container that's not restore init should return false", - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns-1", - Name: "pod-1", - }, - Spec: corev1api.PodSpec{ - InitContainers: []corev1api.Container{ - { - Name: "non-restore-init", - }, - }, - }, - Status: corev1api.PodStatus{ - InitContainerStatuses: []corev1api.ContainerStatus{ - { - State: corev1api.ContainerState{ - Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}}, - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "pod with running init container that's not first should still work", - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns-1", - Name: "pod-1", - }, - Spec: corev1api.PodSpec{ - InitContainers: []corev1api.Container{ - { - Name: "non-restore-init", - }, - { - Name: restorehelper.WaitInitContainer, - }, - }, - }, - Status: corev1api.PodStatus{ - InitContainerStatuses: []corev1api.ContainerStatus{ - { - State: corev1api.ContainerState{ - Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}}, - }, - }, - { - State: corev1api.ContainerState{ - Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}}, - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "pod with init container as first initContainer that's not running should return false", - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns-1", - Name: "pod-1", - }, - Spec: corev1api.PodSpec{ - InitContainers: []corev1api.Container{ - { - Name: restorehelper.WaitInitContainer, - }, - { - Name: "non-restore-init", - }, - }, - }, - Status: corev1api.PodStatus{ - InitContainerStatuses: []corev1api.ContainerStatus{ - { - State: corev1api.ContainerState{}, - }, - { - State: corev1api.ContainerState{ - Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}}, - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "pod with running init container as first initContainer should return true", - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns-1", - Name: "pod-1", - }, - Spec: corev1api.PodSpec{ - InitContainers: []corev1api.Container{ - { - Name: restorehelper.WaitInitContainer, - }, - { - Name: "non-restore-init", - }, - }, - }, - Status: corev1api.PodStatus{ - InitContainerStatuses: []corev1api.ContainerStatus{ - { - State: corev1api.ContainerState{ - Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}}, - }, - }, - { - State: corev1api.ContainerState{ - Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}}, - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "pod with init container with empty InitContainerStatuses should return 0", - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns-1", - Name: "pod-1", - }, - Spec: corev1api.PodSpec{ - InitContainers: []corev1api.Container{ - { - Name: restorehelper.WaitInitContainer, - }, - }, - }, - Status: corev1api.PodStatus{ - InitContainerStatuses: []corev1api.ContainerStatus{}, - }, - }, - expected: false, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, isInitContainerRunning(test.pod)) + if ts.expectError { + require.Error(t, err) + if ts.errString != "" { + assert.Contains(t, err.Error(), ts.errString) + } + } else { + require.NoError(t, err) + } }) } } From 666d14de326154d854102612b53acfd419793f31 Mon Sep 17 00:00:00 2001 From: chlins Date: Wed, 15 Jul 2026 11:01:02 +0800 Subject: [PATCH 046/194] feat(resourcepolicies): support dataMover parameter in snapshot volume policy action Signed-off-by: chlins --- changelogs/unreleased/10004-chlins | 1 + .../resourcepolicies/resource_policies.go | 44 +++++++ .../resource_policies_test.go | 67 +++++++++++ .../volume_resources_validator.go | 20 +++- .../volume_resources_validator_test.go | 109 ++++++++++++++++++ pkg/controller/data_download_controller.go | 2 +- pkg/controller/data_upload_controller.go | 2 +- pkg/datamover/dataupload_delete_action.go | 3 +- pkg/datamover/util.go | 13 +-- pkg/datamover/util_test.go | 29 ----- pkg/exposer/csi_snapshot.go | 2 +- pkg/exposer/csi_snapshot_test.go | 2 +- pkg/exposer/generic_restore.go | 2 +- pkg/exposer/generic_restore_test.go | 2 +- pkg/util/datamover/datamover.go | 43 +++++++ pkg/util/datamover/datamover_test.go | 56 +++++++++ 16 files changed, 351 insertions(+), 46 deletions(-) create mode 100644 changelogs/unreleased/10004-chlins create mode 100644 pkg/util/datamover/datamover.go create mode 100644 pkg/util/datamover/datamover_test.go diff --git a/changelogs/unreleased/10004-chlins b/changelogs/unreleased/10004-chlins new file mode 100644 index 000000000..142a83705 --- /dev/null +++ b/changelogs/unreleased/10004-chlins @@ -0,0 +1 @@ +Support selecting the data mover type (velero-fs or velero-block) through the volume policy snapshot action's dataMover parameter diff --git a/internal/resourcepolicies/resource_policies.go b/internal/resourcepolicies/resource_policies.go index 867efc74a..235f48ed5 100644 --- a/internal/resourcepolicies/resource_policies.go +++ b/internal/resourcepolicies/resource_policies.go @@ -30,6 +30,7 @@ import ( crclient "sigs.k8s.io/controller-runtime/pkg/client" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + datamover "github.com/vmware-tanzu/velero/pkg/util/datamover" "github.com/vmware-tanzu/velero/pkg/util/wildcard" ) @@ -48,6 +49,20 @@ const ( Custom VolumeActionType = "custom" ) +const ( + // DataMoverParameter is the key of the action parameter that selects the data + // mover to be used for the matched volumes when the action type is snapshot. + DataMoverParameter = "dataMover" +) + +// validDataMovers is the set of data mover values accepted in the snapshot +// action's dataMover parameter. +var validDataMovers = map[string]struct{}{ + datamover.DataMoverTypeVelero: {}, + datamover.DataMoverTypeVeleroFs: {}, + datamover.DataMoverTypeVeleroBlock: {}, +} + // Action defined as one action for a specific way of backup type Action struct { // Type defined specific type of action, currently only support 'skip' @@ -56,6 +71,35 @@ type Action struct { Parameters map[string]any `yaml:"parameters,omitempty"` } +// GetDataMover returns the data mover configured in the snapshot action's +// dataMover parameter. The dataMover parameter is only meaningful for the +// snapshot action, so it returns an error when the action is nil or its type is +// not snapshot. When the parameter is absent, it returns the default built-in +// data mover. The empty string and "velero" both denote the default built-in +// data mover and are returned unchanged; normalizing them to the concrete +// default mover is the consuming workflow's responsibility (issue #9830). +func (a *Action) GetDataMover() (string, error) { + if a == nil || a.Type != Snapshot { + return "", fmt.Errorf("the %q parameter is only supported for the %q action", DataMoverParameter, Snapshot) + } + if len(a.Parameters) == 0 { + return datamover.GetDefaultBuiltInDataMover(), nil + } + raw, ok := a.Parameters[DataMoverParameter] + if !ok { + return datamover.GetDefaultBuiltInDataMover(), nil + } + dataMover, ok := raw.(string) + if !ok { + return "", fmt.Errorf("parameter %q must be a string, got %T", DataMoverParameter, raw) + } + if _, ok := validDataMovers[dataMover]; !ok { + return "", fmt.Errorf("invalid %q value %q, valid values are %q, %q, %q", + DataMoverParameter, dataMover, datamover.DataMoverTypeVelero, datamover.DataMoverTypeVeleroFs, datamover.DataMoverTypeVeleroBlock) + } + return dataMover, nil +} + // ResourceFilter defines a filter for specific resource kinds. type ResourceFilter struct { Kinds []string `yaml:"kinds"` diff --git a/internal/resourcepolicies/resource_policies_test.go b/internal/resourcepolicies/resource_policies_test.go index 4b03b833c..445b479f0 100644 --- a/internal/resourcepolicies/resource_policies_test.go +++ b/internal/resourcepolicies/resource_policies_test.go @@ -2845,3 +2845,70 @@ namespacedFilterPolicies: assert.Nil(t, p.GetIncludeExcludePolicy()) assert.Nil(t, p.GetClusterScopedFilterPolicy()) } + +func TestActionGetDataMover(t *testing.T) { + testCases := []struct { + name string + action *Action + expectedMove string + expectErr bool + }{ + { + name: "nil action", + action: nil, + expectErr: true, + }, + { + name: "snapshot action without parameters returns default mover", + action: &Action{Type: Snapshot}, + expectedMove: "velero-fs", + }, + { + name: "snapshot action without dataMover parameter returns default mover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"other": "value"}}, + expectedMove: "velero-fs", + }, + { + name: "snapshot action with velero dataMover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": "velero"}}, + expectedMove: "velero", + }, + { + name: "snapshot action with velero-fs dataMover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": "velero-fs"}}, + expectedMove: "velero-fs", + }, + { + name: "snapshot action with velero-block dataMover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": "velero-block"}}, + expectedMove: "velero-block", + }, + { + name: "non-snapshot action returns error", + action: &Action{Type: FSBackup, Parameters: map[string]any{"dataMover": "velero-fs"}}, + expectErr: true, + }, + { + name: "snapshot action with non-string dataMover returns error", + action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": 123}}, + expectErr: true, + }, + { + name: "snapshot action with invalid dataMover returns error", + action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": "unknown"}}, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dataMover, err := tc.action.GetDataMover() + if tc.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectedMove, dataMover) + }) + } +} diff --git a/internal/resourcepolicies/volume_resources_validator.go b/internal/resourcepolicies/volume_resources_validator.go index 928e17df6..332f98d2e 100644 --- a/internal/resourcepolicies/volume_resources_validator.go +++ b/internal/resourcepolicies/volume_resources_validator.go @@ -21,6 +21,8 @@ import ( "github.com/cockroachdb/errors" "go.yaml.in/yaml/v3" + + datamover "github.com/vmware-tanzu/velero/pkg/util/datamover" ) const currentSupportDataVersion = "v1" @@ -99,6 +101,22 @@ func (a *Action) validate() error { return fmt.Errorf("invalid action type %s", a.Type) } - // TODO validate parameters + // validate parameters + if raw, ok := a.Parameters[DataMoverParameter]; ok { + // the dataMover parameter is only meaningful for the snapshot action + if a.Type != Snapshot { + return fmt.Errorf("parameter %q is only supported for the %q action, but the action type is %q", + DataMoverParameter, Snapshot, a.Type) + } + dataMover, ok := raw.(string) + if !ok { + return fmt.Errorf("parameter %q must be a string, got %T", DataMoverParameter, raw) + } + if _, ok := validDataMovers[dataMover]; !ok { + return fmt.Errorf("invalid %q value %q, valid values are %q, %q, %q", + DataMoverParameter, dataMover, datamover.DataMoverTypeVelero, datamover.DataMoverTypeVeleroFs, datamover.DataMoverTypeVeleroBlock) + } + } + return nil } diff --git a/internal/resourcepolicies/volume_resources_validator_test.go b/internal/resourcepolicies/volume_resources_validator_test.go index f2e6bf0e0..489e9c653 100644 --- a/internal/resourcepolicies/volume_resources_validator_test.go +++ b/internal/resourcepolicies/volume_resources_validator_test.go @@ -549,6 +549,115 @@ func TestValidate(t *testing.T) { }, wantErr: false, }, + { + name: "snapshot action with valid dataMover velero-fs", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"dataMover": "velero-fs"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: false, + }, + { + name: "snapshot action with valid dataMover velero-block", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"dataMover": "velero-block"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: false, + }, + { + name: "snapshot action with valid dataMover velero", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"dataMover": "velero"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: false, + }, + { + name: "snapshot action with invalid dataMover value", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"dataMover": "unknown-mover"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: true, + }, + { + name: "snapshot action with non-string dataMover value", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"dataMover": 123}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: true, + }, + { + name: "dataMover parameter on non-snapshot action is rejected", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: FSBackup, + Parameters: map[string]any{"dataMover": "velero-fs"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: true, + }, + { + name: "snapshot action without parameters still valid", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{Type: Snapshot}, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: false, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/controller/data_download_controller.go b/pkg/controller/data_download_controller.go index 06ce3479e..fc7cb1a53 100644 --- a/pkg/controller/data_download_controller.go +++ b/pkg/controller/data_download_controller.go @@ -44,7 +44,6 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/constant" - datamover "github.com/vmware-tanzu/velero/pkg/datamover" "github.com/vmware-tanzu/velero/pkg/datapath" "github.com/vmware-tanzu/velero/pkg/exposer" "github.com/vmware-tanzu/velero/pkg/metrics" @@ -53,6 +52,7 @@ import ( velerotypes "github.com/vmware-tanzu/velero/pkg/types" "github.com/vmware-tanzu/velero/pkg/uploader" "github.com/vmware-tanzu/velero/pkg/util" + datamover "github.com/vmware-tanzu/velero/pkg/util/datamover" "github.com/vmware-tanzu/velero/pkg/util/kube" ) diff --git a/pkg/controller/data_upload_controller.go b/pkg/controller/data_upload_controller.go index 9b2d9a2e3..78e4d1ed3 100644 --- a/pkg/controller/data_upload_controller.go +++ b/pkg/controller/data_upload_controller.go @@ -45,7 +45,6 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/constant" - "github.com/vmware-tanzu/velero/pkg/datamover" "github.com/vmware-tanzu/velero/pkg/datapath" "github.com/vmware-tanzu/velero/pkg/exposer" "github.com/vmware-tanzu/velero/pkg/metrics" @@ -53,6 +52,7 @@ import ( velerotypes "github.com/vmware-tanzu/velero/pkg/types" "github.com/vmware-tanzu/velero/pkg/uploader" "github.com/vmware-tanzu/velero/pkg/util" + "github.com/vmware-tanzu/velero/pkg/util/datamover" "github.com/vmware-tanzu/velero/pkg/util/kube" ) diff --git a/pkg/datamover/dataupload_delete_action.go b/pkg/datamover/dataupload_delete_action.go index 681bb79de..a50d0fce2 100644 --- a/pkg/datamover/dataupload_delete_action.go +++ b/pkg/datamover/dataupload_delete_action.go @@ -17,6 +17,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/label" "github.com/vmware-tanzu/velero/pkg/plugin/velero" repotypes "github.com/vmware-tanzu/velero/pkg/repository/types" + datamoverutil "github.com/vmware-tanzu/velero/pkg/util/datamover" ) type DataUploadDeleteAction struct { @@ -88,7 +89,7 @@ func (d *DataUploadDeleteAction) Execute(input *velero.DeleteItemActionExecuteIn // generate the configmap which is to be created and used as a way to communicate the snapshot info to the backup deletion controller func genConfigmap(bak *velerov1.Backup, du velerov2alpha1.DataUpload) *corev1api.ConfigMap { - if !IsBuiltInDataMover(du.Spec.DataMover) || du.Status.SnapshotID == "" { + if !datamoverutil.IsBuiltInDataMover(du.Spec.DataMover) || du.Status.SnapshotID == "" { return nil } snapshot := repotypes.SnapshotIdentifier{ diff --git a/pkg/datamover/util.go b/pkg/datamover/util.go index 7e37695b6..ed66d497a 100644 --- a/pkg/datamover/util.go +++ b/pkg/datamover/util.go @@ -16,25 +16,20 @@ limitations under the License. package datamover -import "fmt" +import ( + "fmt" -const ( - DataMoverTypeVeleroFs string = "velero-fs" - DataMoverTypeVeleroBlock string = "velero-block" + datamoverutil "github.com/vmware-tanzu/velero/pkg/util/datamover" ) func GetUploaderType(dataMover string) string { - if dataMover == "" || dataMover == "velero" { + if datamoverutil.IsBuiltInDataMover(dataMover) { return "kopia" } else { return dataMover } } -func IsBuiltInDataMover(dataMover string) bool { - return dataMover == "" || dataMover == "velero" -} - func GetRealSource(sourceNamespace string, pvcName string) string { return fmt.Sprintf("%s/%s", sourceNamespace, pvcName) } diff --git a/pkg/datamover/util_test.go b/pkg/datamover/util_test.go index 80e2f4e16..d44f3c307 100644 --- a/pkg/datamover/util_test.go +++ b/pkg/datamover/util_test.go @@ -6,35 +6,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestIsBuiltInUploader(t *testing.T) { - testcases := []struct { - name string - dataMover string - want bool - }{ - { - name: "empty dataMover is builtin", - dataMover: "", - want: true, - }, - { - name: "velero dataMover is builtin", - dataMover: "velero", - want: true, - }, - { - name: "kopia dataMover is not builtin", - dataMover: "kopia", - want: false, - }, - } - for _, tc := range testcases { - t.Run(tc.name, func(tt *testing.T) { - assert.Equal(tt, tc.want, IsBuiltInDataMover(tc.dataMover)) - }) - } -} - func TestGetUploaderType(t *testing.T) { testcases := []struct { name string diff --git a/pkg/exposer/csi_snapshot.go b/pkg/exposer/csi_snapshot.go index 6c92a6973..ed510c798 100644 --- a/pkg/exposer/csi_snapshot.go +++ b/pkg/exposer/csi_snapshot.go @@ -35,12 +35,12 @@ import ( "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/vmware-tanzu/velero/pkg/datamover" "github.com/vmware-tanzu/velero/pkg/nodeagent" velerotypes "github.com/vmware-tanzu/velero/pkg/types" "github.com/vmware-tanzu/velero/pkg/util" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/csi" + "github.com/vmware-tanzu/velero/pkg/util/datamover" "github.com/vmware-tanzu/velero/pkg/util/kube" ) diff --git a/pkg/exposer/csi_snapshot_test.go b/pkg/exposer/csi_snapshot_test.go index e1512e633..e5a7aa9a7 100644 --- a/pkg/exposer/csi_snapshot_test.go +++ b/pkg/exposer/csi_snapshot_test.go @@ -43,11 +43,11 @@ import ( clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - "github.com/vmware-tanzu/velero/pkg/datamover" velerotest "github.com/vmware-tanzu/velero/pkg/test" velerotypes "github.com/vmware-tanzu/velero/pkg/types" "github.com/vmware-tanzu/velero/pkg/util" "github.com/vmware-tanzu/velero/pkg/util/boolptr" + "github.com/vmware-tanzu/velero/pkg/util/datamover" "github.com/vmware-tanzu/velero/pkg/util/kube" ) diff --git a/pkg/exposer/generic_restore.go b/pkg/exposer/generic_restore.go index 9a68b7157..0f4b9c5b4 100644 --- a/pkg/exposer/generic_restore.go +++ b/pkg/exposer/generic_restore.go @@ -31,10 +31,10 @@ import ( "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/vmware-tanzu/velero/pkg/datamover" "github.com/vmware-tanzu/velero/pkg/nodeagent" velerotypes "github.com/vmware-tanzu/velero/pkg/types" "github.com/vmware-tanzu/velero/pkg/util/boolptr" + "github.com/vmware-tanzu/velero/pkg/util/datamover" "github.com/vmware-tanzu/velero/pkg/util/kube" ) diff --git a/pkg/exposer/generic_restore_test.go b/pkg/exposer/generic_restore_test.go index 48526a5fd..b65863318 100644 --- a/pkg/exposer/generic_restore_test.go +++ b/pkg/exposer/generic_restore_test.go @@ -33,8 +33,8 @@ import ( clientTesting "k8s.io/client-go/testing" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - "github.com/vmware-tanzu/velero/pkg/datamover" velerotest "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/util/datamover" "github.com/vmware-tanzu/velero/pkg/util/kube" ) diff --git a/pkg/util/datamover/datamover.go b/pkg/util/datamover/datamover.go new file mode 100644 index 000000000..59dd1499b --- /dev/null +++ b/pkg/util/datamover/datamover.go @@ -0,0 +1,43 @@ +/* +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 datamover holds the shared data mover type identifiers and helpers. +// It must remain a leaf package (stdlib-only imports) so it can be referenced +// from anywhere in the codebase without introducing import cycles. +package datamover + +const ( + // DataMoverTypeVelero refers to the default built-in data mover. The default + // data mover may change among releases; see GetDefaultBuiltInDataMover. + DataMoverTypeVelero = "velero" + // DataMoverTypeVeleroFs refers to the Velero file system data mover. + DataMoverTypeVeleroFs = "velero-fs" + // DataMoverTypeVeleroBlock refers to the Velero block data mover. + DataMoverTypeVeleroBlock = "velero-block" +) + +// IsBuiltInDataMover reports whether the given data mover value refers to a +// Velero built-in data mover (an empty value or the default "velero" alias). +func IsBuiltInDataMover(dataMover string) bool { + return dataMover == "" || dataMover == DataMoverTypeVelero +} + +// GetDefaultBuiltInDataMover returns the data mover used when the default +// built-in data mover ("velero"/empty) is selected. The default may change +// between releases; currently it is the file system data mover. +func GetDefaultBuiltInDataMover() string { + return DataMoverTypeVeleroFs +} diff --git a/pkg/util/datamover/datamover_test.go b/pkg/util/datamover/datamover_test.go new file mode 100644 index 000000000..8576aed0e --- /dev/null +++ b/pkg/util/datamover/datamover_test.go @@ -0,0 +1,56 @@ +/* +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 datamover + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsBuiltInDataMover(t *testing.T) { + testcases := []struct { + name string + dataMover string + want bool + }{ + { + name: "empty dataMover is builtin", + dataMover: "", + want: true, + }, + { + name: "velero dataMover is builtin", + dataMover: "velero", + want: true, + }, + { + name: "kopia dataMover is not builtin", + dataMover: "kopia", + want: false, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + assert.Equal(tt, tc.want, IsBuiltInDataMover(tc.dataMover)) + }) + } +} + +func TestGetDefaultBuiltInDataMover(t *testing.T) { + assert.Equal(t, DataMoverTypeVeleroFs, GetDefaultBuiltInDataMover()) +} From d82c552aaa51036ea719713c2b937eaf237155fa Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 1 Jul 2026 17:41:55 +0800 Subject: [PATCH 047/194] Add BackupType in backup.spec. Signed-off-by: Xun Jiang --- changelogs/unreleased/9954-blackpiglet | 1 + config/crd/v1/bases/velero.io_backups.yaml | 7 ++ config/crd/v1/bases/velero.io_schedules.yaml | 7 ++ config/crd/v1/crds/crds.go | 4 +- pkg/apis/velero/v1/backup_types.go | 13 ++++ pkg/builder/backup_builder.go | 5 ++ pkg/cmd/cli/backup/create.go | 21 +++++- pkg/cmd/cli/backup/create_test.go | 39 ++++++++++ pkg/controller/backup_controller.go | 5 ++ pkg/controller/backup_controller_test.go | 76 ++++++++++++++++++++ site/content/docs/main/api-types/backup.md | 7 ++ 11 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/9954-blackpiglet diff --git a/changelogs/unreleased/9954-blackpiglet b/changelogs/unreleased/9954-blackpiglet new file mode 100644 index 000000000..d3215eb58 --- /dev/null +++ b/changelogs/unreleased/9954-blackpiglet @@ -0,0 +1 @@ +Add BackupType in backup.spec \ No newline at end of file diff --git a/config/crd/v1/bases/velero.io_backups.yaml b/config/crd/v1/bases/velero.io_backups.yaml index 794c342c8..3b98f2ad4 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -41,6 +41,13 @@ spec: spec: description: BackupSpec defines the specification for a Velero backup. properties: + backupType: + description: BackupType specifies how volume data is backed up, with + possible values including Full and Incremental. + enum: + - Full + - Incremental + type: string csiSnapshotTimeout: description: |- CSISnapshotTimeout specifies the time used to wait for CSI VolumeSnapshot status turns to diff --git a/config/crd/v1/bases/velero.io_schedules.yaml b/config/crd/v1/bases/velero.io_schedules.yaml index 7719a4b13..4b13ecec7 100644 --- a/config/crd/v1/bases/velero.io_schedules.yaml +++ b/config/crd/v1/bases/velero.io_schedules.yaml @@ -80,6 +80,13 @@ spec: Template is the definition of the Backup to be run on the provided schedule properties: + backupType: + description: BackupType specifies how volume data is backed up, + with possible values including Full and Incremental. + enum: + - Full + - Incremental + type: string csiSnapshotTimeout: description: |- CSISnapshotTimeout specifies the time used to wait for CSI VolumeSnapshot status turns to diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index a2947da00..44d2b378c 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -30,14 +30,14 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8adɡS\xf7\xe7\u074b!%[\x96dk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L8\xf5\x88>(k\x96 \x9c\xc2?\b\r\x7f\x85\xf9\xd3/a\xae\xec\xe2\xf0z\xf6\xa4L\xb9\x84U\fd\xeb5\x06\x1b\xbd\xc4w\xb8SF\x91\xb2fV#\x89R\x90X\xce\x00\x841\x96\x04o\a\xfe\x04\x90\u0590\xb7Z\xa3/*4\xf3\xa7\xb8\xc5mT\xbaD\x9f\x8c\xb7\xae\x0f?\xcc_\xff<\xffi\x06`D\x8dK\xd8\n\xf9\x14\x9dGg\x83\"\xeb\x15\x86\xf9\x015z;Wv\x16\x1cJ\xb6^y\x1b\xdd\x12\xce\aY\xbb\xf1\x9c\xa3~\x9b\f\xad[C\xc7t\xa4U\xa0\xdfF\x8f?\xaa@I\xc4\xe9\xe8\x85\x1e\v$\x1d\ae\xaa\xa8\x85\x1f\b\xb0\x83 \xad\xc3%|\xe6X\x9c\x90X\xce\x00\x9aLSl\x05\x88\xb2L\xb5\x13\xfa\xc1+C\xe8WVǺ\xadY\x01_\x835\x0f\x82\xf6K\x98\xb7՝K\x8f\xa9\xb0_T\x8d\x81D\xed\x92l[\xb07\x156\xdftd\xe7\xa5 \x1c\x1a\xe3\xca\xcdϱ~9:\xbc\xb0r.\x04tβ\xc5@^\x99jv\x16>\xbcΥ\x90{\xacŲ\x91\xb5\x0e͛\x87\x0f\x8f?n.\xb6\x01\x9c\xb7\x0e=\xa9\x16\x9e\xbc:\xed\xd7\xd9\x05(1H\xaf\x1c\xa5\xe6\xf8\xbb\xb88\x03`\aY\vJ\xeeC\f@{lk\x8ce\x13\x13\xd8\x1d\xd0^\x05\xf0\xe8<\x064\xb93y[\x18\xb0ۯ(i\xde3\xbdA\xcff \xecm\xd4%\xb7\xef\x01=\x81Gi+\xa3\xfe<\xd9\x0e@69Ղ0\x10$\x14\x8d\xd0p\x10:\xe2\xf7 Lٳ\\\x8b#xd\x9f\x10M\xc7^R\b\xfd8>Y\x8f\xa0\xcc\xce.aO\xe4\xc2r\xb1\xa8\x14\xb5C)m]G\xa3\xe8\xb8H\U000e5d91\xac\x0f\x8b\x12\x0f\xa8\x17AU\x85\xf0r\xaf\b%E\x8f\v\xe1T\x91\x121i0\xe7u\xf9\x9do\xc68\\\xb8\x1d\x00\x9dW\x9a\xa4;\xe0\xe1\xd1\x02\x15@4\xa6r\x8ag\x14x\x8bK\xb7\xfeu\xf3\x05\xdaH2R\x19\x94\xb3\xe8\xa0.->\\Mev\xe8\xb3\xde\xce\xdb:\xd9DS:\xab\f\xa5\x0f\xa9\x15\x1a\x82\x10\xb7\xb5\"n\x83\xdf#\x06b\xe8\xfafW\x89\xb8`\x8b\x10\x1d\x8fN\xd9\x17\xf8``%j\xd4+\x11\xf0?ƊQ\t\x05\x83\xf0,\xb4\xbat\xdc\x17\xce\xe5\xed\x1c\xb4Tz\x05\xda>=n\x1cJF\x96\x8b˪j\xa7d\x9e\xa9\x9d\xf5 \x06\xf2\x97\x95\x1a\xa7\x00^\x99D7d\xbd\xa8\xf0\xa3\xcd6\xfbBSm\xc7\xeb혡6b\xa6\xad\xcc\t8.8b\x90\xf6\x82:d@B\x99\x13\xa7\x8c&y\x03\x99\x84\x8e`\xa60\xc2H|\x9f\xfa\xd1\xc8\xe3D\xa2\x9fFT8\xa5\xbd\xfd\x06vGh\xbaF\x9bXG2\xd9\"\xf8h\xee\n\xf6\x9c\xe3ʚ\x9d\xaa\x86\x81v/\xb2k\xe0N8\xe9e\xbb\xee\xf9\xe4L\xb9\xb9α\x14m\xe71 ;UE\x7f\r\xbc\x9dB]\x0e(\x04\xc0D\xad\xc5V\xe3\x12\xc8G\xbcR\x91\xc1\xac\\V\x84\xef\xc7\t\xe0\xd6\x17\u00a0L\xc9\xd3\xd2\\V\xec\xa4mFn\x7f4%\xf8\xcbgJw\xa1\x89\xf5\xd0]\x01O\xd6)1\xb2\xef1\x90\x92#\a\xaf^\xdd\xd7\x01l\xe6C\xc9t\xb4S\xe8'2~Ǽ\xcd9\x0e\x1b\xf0\x86\x93\x03?~\xf0\xf4\\z\xc9\xdc?^\x9a\xe8N|\xdeH3\x9bi\xa6S\xe6v\xa4ÈIg\xcb&\xb2F/\xf5\xe1\x1d\xf3ó\xaa<\xf6\xae\xceb\x9c\xecz2c4\xd1\x13\xe9U\xedYlO\x82b\xb8\x87\xef\x93B[M\x19\xbdO\xf7i\xde\xe5gԋ\x19_\x8b@\x1db\xe3G\xed\x04\xee\x1f\x87\x1am`l\f\x887\x18\xdan\xf1Fp\rQJ\xc4rx\xc5\x03\xe3[\vʏ\xe7\x82\xed\xbd\x8c9Ɖ\x1fC\x10\xd5T\x92\x9f\xb2T~=5* \xb66\xd2\x15\x04h?\x96\xe3mT&\"u{\x11\xa6\xe2|`\x99\xb1\xbe\xe8]\xb0\xb7B\xb8Fi\x9f\xf1\xdb\xc8\xee\x1aE9\xa4\xc5\x02>[\x1a?\xba\xc9j\x12M\xb7\x99&\x89\xbc'ϙ_`И\x1c\xf4\xdf0kEX\x8f^\x91\xd7g%/ik\xa7\x91\xf0\xf4\xff7.\xd6\v}\xd5\xd7:\x81\x96\x0f\xf8y\x94&\xe7j/\xb5%\x9bJ,\xaf\xe9\x11\xcakb\x90\xf2\xba\xf9j\x80[C5R\x89{G\xebj)2\xdc\xcf+\xc7d\x06\x1eC\xd4\xf4\xac\x04\xd6I\xb4\xc5/+\x9e\xdb\xefy\xf1\x8c\xcf\\^\x05lZj\xbc*\xf1^(}\xf5x2\xd9@\xc2\xd3}\xfd\xbb\xb9P9\xfd{\xf0n\xb7o\xff\x97\xfdy\xe3\x1d\xd9\x1e\n\xef\xc5q\xfa\xea\x1el\x06\xfe\r.;\xc1\x85\xfc\x9c\xe8\xee\xc4\xed\xe9/\x7f\t\x7f\xfd3\xfb7\x00\x00\xff\xff\x96֥5\xef\x13\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\xdb8\x92\xef\xf9\x15(\xdd\xc3\xecnI\xf6\xa6\ue8ee\xfc\x96q\x92\x1d\xd5\xcc$\xde\xd8\xe3}\x86Ȗ\x841\bp\x00P\xb6\xf6\xee\xfe\xfb\x15\x1a\x00?DP\x04eٓ\xdd\r_\x12\x8b`\x03\xfd\xddh4\x80\xc5b\xf1\x86\x96\xec\x1e\x94fR\\\x11Z2x2 \xec_\xfa\xe2\xe1\xbf\xf5\x05\x93\x97\xbb\xb7o\x1e\x98ȯ\xc8u\xa5\x8d,\xbe\x80\x96\x95\xca\xe0=\xac\x99`\x86I\xf1\xa6\x00Csj\xe8\xd5\x1bB\xa8\x10\xd2P\xfb\xb3\xb6\x7f\x12\x92Ia\x94\xe4\x1c\xd4b\x03\xe2\xe2\xa1Z\xc1\xaab<\a\x85\xc0C\u05fb?_\xbc\xfd\xaf\x8b\xff|C\x88\xa0\x05\\\x91\x15\xcd\x1e\xaaR_쀃\x92\x17L\xbe\xd1%d\x16\xe4Fɪ\xbc\"\xcd\v\xf7\x89\xef\xce\r\xf5{\xfc\x1a\x7f\xe0L\x9b\x1f[?\xfeĴ\xc1\x17%\xaf\x14\xe5uO\xf8\x9bfbSq\xaa¯o\bљ,\xe1\x8a|\xb2]\x944\x83\xfc\r!~\xd4\xd8\xe5\xc2\x0fx\xf7\xd6AȶPP7\x16Bd\t\xe2\xdd\xcd\xf2\xfe\xdfo;?\x13\x92\x83\xce\x14+\r\xe2\xfe\xbf\x8b\xfaw\xe2GI\x98&\x94\xdc#\x8eDy\x92\x13\xb3\xa5\x86((\x15h\x10F\x13\xb3\x05\x92\xd1\xd2T\n\x88\\\x93\x1f\xab\x15(\x01\x06t\v^\xc6+m@\x11m\xa8\x01B\r\xa1\xa4\x94L\x18\xc2\x041\xac\x00\xf2\x87w7K\"W\xbfBf4\xa1\"'Tk\x991j ';ɫ\x02ܷ\x7f\xbc\xa8\xa1\x96J\x96\xa0\f\vDwOK\x92Z\xbf\x1e\xc3\xd5>\x96<\xee+\x92[\x91\x02\x87\x96'1䞢\x16?\xb3e\xbaA\x1f\x85\xcc\xfeL\x85\x1f\xfe\xc5\x01\xe8[P\x16\f\xd1[Y\xf1\xdcJ\xe2\x0e\x94%`&7\x82\xfd\xbd\x86\xad\x89\x91\xd8)\xa7\x06\xb4\xa5\x8c\x01%(';\xca+\x98[\xa2\x1c@.\xe8\x9e(\xb0}\x92J\xb4\xe0\xe1\a\xfap\x1c?K\x05\x84\x89\xb5\xbc\"[cJ}uy\xb9a&\xe8W&\x8b\xa2\x12\xcc\xec/QUت2R\xe9\xcb\x1cv\xc0/5\xdb,\xa8ʶ\xcc@f\xd9|IK\xb6@D\x04\xea\xd8E\x91\xff[\x10\x0f\xdd\xe9\xd6\xec\xad\xd8j\xa3\x98ش^\xa0~L`\x8fU\x1d'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xeeˇۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x06\xe5\xbe[+Y L\x10\xb9\x13U\x94s\xce@\x18\xa2\xabU\xc1\x8c\x15\x83\xdf*\xd0V\a\xe4!\xd8k\xb4Ad\x05\xa4*s+Ƈ\r\x96\x82\\\xd3\x02\xf85\xd5\xf0ʼ\xb2\\\xd1\v˄$n\xb5-\xebacG\xde\u058b` \aX\xeb\f\xcbm\tYG\xd1\xecWl\xcd2\xa7Nk\xa9\x1a\xbb\xe3l`\x97BqշO\xa6٭\xa0\xa5\xdeJs\xc7\n\x90\x959l1&kȼ\xdb\xe5\x01\x940B?^\xb4Y\x95\x86\xdc*\xed#e\x06\xc7|}\xbb$\xf7h\xac\xc2\xd7h\xb4*ML\xa5\x84\x95\x92H__\x80\xe6\xfb;\xf9\x8b\x06\x92W(ܙ\x02\xa4Ü\xac`m%A\x81\xfd\u07be\x02\xa5,m4\x0e@V=cc\x9f\xbb-X\xdaҊ\x1b\xaf'L\x93\xb7\x7f&\x05\x13\x95\xe9\x89\xda בR\xd4\xd0B\xee@\x9dB\xc4\xf7\xd4П\xed\xc7\a\xb4\xb3@\tB\xb5\xc4[y:\xae\xf6\xf82\xc6m\xf7,\xd7-\x88L\x93ٌHEf\xce\x03\xcf\xe6\xee\xeb\x8aq\xb3`\xa2\xdd\xc7#\xe3<\xf42\ryGC\xc7P}'?j'\xbc'\xd1b\x00V\x8b4\x8f[0[P\xa4\x94\xb5\xc7[3\x0eD﵁\xc2\x13&x\x11\x8fO\xa4'\xd4\x1d\xce=\bm\xe9\xea\x11\xe9#/*\xce\xe9\x8a\xc3\x151\xaa\x82\x01ڬ\xa4\xe4@\xc5\bq\xbe\x806,;\ai\x1c\xa4\ba\x94\x7fѡ\x00:M\xfa\x00\x84F@{\x9aY\xef\xccy\x8b\xb0]\xaaD\xc7T*Ȭվ\xf2ހ\x01G\x0f$$\xe1Rl@\xb9\xdem\xa4\x12\x04L\x81\x15\xb8\x9cXC\xab\x80[oB֕\xb5\xc1\x17\xc4j\xf7\xa0\f0\xa1\rЈp>\x83?\xf0\x94\xf1*\x87\xfc\xda\x05^\xb76~\xccC\xd4ܳ\x9a)|\xfap\x14\xa2\xf7Μe\x18\x04\xfaxo\x81qkLL\x1b'\xbd/\xc1\x85Ζ\x95~؍\xf7=j\x0f4\x18\xfb\xd1\xecO\xb39r\xb8\xdbk\xb7\x0fM\xa8\x82\x9a,\xc9v\x13\x8a\xd2\xec\xfb\xad\x99\x81\"Bţ\xf6$\x91\x9fT)\xba\x1f\xe0f\x1d\xff\x9f\x91\x9fC0\x0f8*B\xb3W\xe6\xe9a\xbf\xff\xcc\\=\x0f\x1f5\xcev)\x13\x96\x7fv\xe2\xd9a\x9fv\xf37K6!M\x04\x1e\x13\x0e\x1eN͎p\xebw\"\xd6Yd~H\xc8k\xd9\xf2\xc2\xfb\x0fI\xa9\xad\x94\x0fc\xd4\xf9\xc1\xb6i&E$ì\nY\xc1\x96\xee\x98T\x1e\xf5\xc6\xd5\xc2\x13d\x95\x89j=5$g\xeb5(\v\xa7\xdcR\r\xdaM\x93\x87\t2\x1c\xbe\x93\x96\x19\x89\xbe<\xc0\xa3a\xa4e\x13b>4t\x1bG\x1cz\xc9\xf0\u0601\xda\xf0\x1a\x9dq\xcev,\xaf(G\xbfLE\xe6\xf0\xa1\xf5\xb8bV\xe6\b\x93{c\x8eJ\xa6{\\@\x10\x90\xb2L\xea̔\xa4\x00\x1b\xf3\x16vN\xd0o:\x8c\xf9\x8a\xdaXE\x0eaO\x90Y\xaa\xe2\xa0}W9\x86\x91\x8d͘7L\xc1D\x04\xe1t\x05\x9ch\xe0\x90\x19\xa9\xe2\x14\x19\xe3\xb3{R\x8c\xe0\x00!#\x96\xaf;\xd3h\x108\x02\x92\xe0\x14n˲\xad\v\xf5\xac\x10!\x1c\x92K\xb0\x01\x9f!\xb4,y\xc4]4\xcfQ\xe6\xfbN\x8e\xe9z\xf3\x8ch\xfd!\xbc\x98\xfe7O\x82\xcdl\x9e(i\x1b\xfd\xeaR\xb6\x16\x87\xf8\x9c\xb6y\xfe9\t\x1b,\xff\tB{D\xfb\tf\x85\x92ezPn-U\x19\xe8\v\x1bNa\xa43'̄_\xc74\xa1\x13s\xf5\x92e\x1d\"|ݼ\x99.\xf4\x89\xacIщ\x17bL\xdd\xc5? _\xd0e\xdcz\x8f\x91̓\x9f\xda_\xcd\t[\xd7D\xcf\xe7d\u0378\x01u@\xfd\x93L}\xe0\xcc9\x88\x91\xe2\xf5\b\xa6\xefM\xb6\xfd\xf0dC0ݬT%\xd2\xe5\xf0c\x17Ȇh\xbf\xeb\x9eG\xe0\x12Lc3\x05\x05\xa6\xc7q\xc6\xd4\xfe\x05C\xabw\x9f\xde\xc7\xe7W\xed'A\xf2z\x88\x8c(\x9d{\xde\x1d`\xd4\x1e\x9f\x0f\xe1\xc3\x1b\x8c\x81\xea\t\x90[\n\x99\x13J\x1e`\xefB\x17*\x88\xe5\x0f\r\x8d\x13\xbaW\x80k2(g\x0f\xb0G0\xf1E\x96\xfe\x93*\r\xeey\x80}J\xb3\x03\x1a\xda11\xed\x17\x8f,\x9d\xec\x0fH\b̭\xa7\x8a\x81{\xbc*D\x964\xe2O\xa2-\tO\xa0\xfd\th&\x89J\xbb\x8f\xf6*%J\xc0w\xda\xf1\xd2j̖\x95hV1\xe3 \xd7\xc9\fu\xcf=\xe5,\xaf;r:\xb2\x14s\xf2I\x1a\xfbχ'\xa6\xfdB\xe6{\t\xfa\x934\xf8ˋP\xd4\r\xfc%\xe9\xe9z@E\x13\xce\xca[\x82\xb5\x97\xe2\x9cO\xb3\xd2VӞi\xb2\x14v\xba\xe2H\x92\xd8\x15\xae\xba\xba\xee\\GE\xa5q\x15MH\xb1pi\x9bXO\x9e\xdeRu\xc8\xfd\xecN}\x87w\xd6Y\xb87n\xed\x97\xd3\f\xf2\xb0\\\x83\x8b\x92\xd4\xc0\x86e\x89\xfd\x15\xa06@Jk\xc2\xd3$\"Ѱzl\xa6\x89O\x9a\xf7n?O\x8b\x87z\x8d\x7fa]\xce\xc2C0\xb2H\xa0\x81\xb7\xdd\xf98>\v\xab\xb3\t\xad\x82$\x8c6\x1dX\xb3\x1cn\x9aB\x94g\x90\x03\xbd8\x868\xa3ܥy\x8eu.\x94\xdfL\xf0(\x13da\xaaih\x8dݹ\xe0\x82\xe2R\xcb\xffXO\x8b\xda\xf4\x7f\xa4\xa4L\xe9\v\xf2\x0eKZ8t\xde\xf9\xa4Y\vLB\x97X\x92b\xe5gG\xb9\xf5\xfdր\v\x02\xdcE\x02r\u074b\x8b\xe6\xe4q+\xb5s\xdb\xf5\"\xce\xec\x01\xf6n\xc5p\xb4˶\x91\x99-\xc5\xcc\xc5\x10=\x83Q\a\x1cR\xf0=\x99\xe1\xbb\xd9sB\xa9DIMl\xd6\x11т\x96i\x12\x8a%E\xa9\x81\xba\x9d\xb0\x86 \xc4~X\x97\xca\xd8 \xfb\x18\xb6I\"ZJ\x1dY\xc8\x1f\x18ʈ\xf0\xdeHm\\\xbe\xac\x133G\x13j2$\xd1\b]\xbb\xfa%\xa9B\xb1\x895\xcac\xa9\xdf\xf6s\xb7\x05\r~\xbd\xc2'\xe6\x1cP;\xb3\x9b5\xfa\xed\xac\xfḓ\x97`'4È\x05\xbf-\x95\xcc@Gײ\x9b'\xc1_D\xaa2ڸ\xd79G\xeafI\xae$\xe3x\n4<\xe9!\xaf%\xc4\xc4\xf9\u0087\xa7VB\xd4\xea\xbe\xfd{LƦ\x8e\x8b`\xc9`Q\xd0\xc32\xa5\xa4!^\xbb/\x836x@n\xf2\xa16\x15Z\x82T_^\v\xe0\xd7\x10(\x14L,\xb1\x03\xf2\xf6\x05\x02\voCc\xc5&\xb1\xe7\xb4P\xf6:t\xd2p\xa7\xfe\xc1\xa9r)q\xa9@A\x87y\xfd\xac:ơB\x9aVBbB\xb8Y\xca\xfc;M\xd6Li\xd3\x1e\x82\x1e(S\x89\x82\x998\xf1\x12\x1f\x94:i\xde\xf5\xd9}\xd9Jwm\xe5c(\xcfr\x84I\xc4\x1cח\x80\xb05a\x86\x80\xc8d%0\x81c\xf5\x18\xbbp\xc4u\x16\x96\xa5*I\x9a\xf6\xdb\aDU\xa4\x11`\x81\x92\xc2\xc4\xd1LO\xbb\xf9G\xca\xf8K\xb0\xcd\fU\xb1Ş\xd3t\"\x94\xb8\xb5\v\xf2\n\xfaĊ\xaa \xb4\xb0\x1f\xc0\xb0\x8c\x0f\xa1\xdd+\xc5\xc8E\xc5\r+9.\xa4\xeeX\x1eM6\x98-\xec\xeb\x034~\x95\xb8\xf5ԟ\x04\xf3\xf9K-\xb5\x17\a\x91>\xd5\xe4\x118'4\xa6W=\xcc3w\x12S&\x17`\xfd\x91\xd5N\x7f0\x88?\xbei\xee\xc4\x1dwעW+b)&*\x86O\x91\x19t\x1c)\xf6\xa6\x17\xc1\xba8\x1c\x7f\xfb\xad\x02\xb5'x\x8eM\x1d\xe74\x9b\xc0\xbcbj;\x11\v\xa6\u009b\xad\xa1\xfcy/\xe8oT\x99\xbc\x13\xce\xeb\x1e\x8e\a\xbf\xb16\xa2\x99\xd4X\xc3g\xe7+\xd1>\x06>\x17\xb2\xfe:\xf2\xd9X\x80\x9c\xba[\xeae\xa78\xd3'9\xa3QEz\xe4\xf7;\xed\x82:e\xf7SZ\x01\xc0\xe8n\xa7\x97\x9a\xf2\x8cMz\x92㼴\xddL\xd3\x16\v_p\xf7\xd2K\xecZJ\xa4T\xca.\xa5itz\x85]I\xaf\xba\x1b\xe9\xb5v!%\xef>J*qI^\x05N-Q9q;\xcd\xf8\x1a\xef\xf1\xddD\t\xbb\x88\x12V\x7fǑ<\x01\xbd\x84]B\xd3v\a%\xf0,U\x15_q\x17\xd0+\xee\xfey\xed]?#\x925\xf2z\xda\ue793\x97,\xa4\xcaA\x1d]\xf6I\x95£\xf2\x972\xb7\xe9\x0e\xe4`\xbd#\x9c\xfag[u\xe2et\x0f\xfe\xa0Q\x97G\xb3\xa2\xe4{;C!\xb3\xf6\a\xa7I@T\xdaBo7\x92\xb3,\x12\xbbE\xcffr\x8d{\x87e\xe0\x89QY\xbbd\xa0\xb4\r\xe3\xa1\x1b\x86y\xdd#0גs\xf98q\xeeOK\xf6\x17<\xb9\xfb\x19١w7K\x84\x11\xc4\x03\x8f\x02\xaf\x8b\xb3jlV`\xddr\x83\xe7\x90\xee/\xd7\x1d\x88\xdd:\xc7\xf6Ḑ\xbbs\x90CX\xe0Mg&\xadu\xb9Y\xbaq\f\xf5be\x86\x8a=\x91XQc\xb6L勒*\xb3w\x85\x1a\xf3\xce\x18\x82/=\x96\xdd\x19\xf4\x1e\xfd\xb3\x9d\xa3\xe4\rG:\xe3\n\xe5\xbe\xec.\xfa\x1e\xd2\xee\x94q\f\xef^\x1cݷx\xc6q\f\x87%\v\xa4T\xe4\xe7h\xe5\xd7ٲfڟL\xfc\xb3\xdc\xc1\xfbh\xf6\xacC\x9eۃ\xe6\x91\xf2\xac\x00\xd1\x1d\xba;X\xa5\xba\x02<\x90\xb7\xff\xea\x19\xf5V\xa1k\x7f\xa6\xea)\x89\xb2\xdb.\x88\b~\xe1\x84\xd9\xd0Y\xcc>\xe1\x01\xf0{rs\x8fs\xb4ڴy\x15\xf5s\xb4\x90*\v\x8b\xc1\x118\xfe\x83\xef\xcf_\x9a\xa6\x8dTt\x03?Iw\xc6\xf6\x18ۻ\xad;g\xaf\xfb\xa8'ԏ\x06\xa5\x89\x1d\xc0\xebO\xfb>\x00\xd6\xd4|\xf7\x0e5\xb6\xa3\x9cxL\xb31\xfc\x14\xbe\xdf\xdd\xfd\xe4\xb02\xac\x80\x8b\xf7\x95+w\xb06Q\x83%q\xc0\xd6AZ\xd9\xffn\xe5#\x1e\xfe\x1b\xcfc\x86;\x13\x1ad\x14`\xb19\x96 NB\xa9*\xb9\xa49\xa8k)\xd6l3\x82\xdd/\x9d\xc6\an6\xc3\x1f=r\xb5\x8f\n\xf0\xcf\\\x83`c\x1e\u0381\x7fd\x1c\xb4\x1bV\x82\x01\xbe\xe9\x7fU\xdb\xe3\xaaX\xb9\x18nm_\xd6\x1d\f\xf88\x87\x16\xa6\xa2KP6\x8arI\xebJ\aY\x1dF\xbc\xe1\b\x13\x066П\x05\x1e\xb1\xc0\xeeTit\x9f\xc1\x9c\xe0\\\xe6\xc7X~\xab\x83\xfc\xfd\xf0\x97\a\x9cl\xa5\xbcb'\xee\xb9 \xe4\xe6\xfeZ\x93J\xe4\x98.\xbe\xff\xcb\xed$\xa9\xdbuN\xae\x0f\xda:fT\xef\xe3_\xb5\x82㖽pѱ\\G\x10\x18\x82Ӻ\a\xe4\x91\x19\x7fp\xd7yOZ\x1d\x9a\xf2\f\xddp\x80G\xfa\x8f\xdfq\xe0N\xfe\xf77\xa3xu\xac\x14\x1e\x93\xeao\x05\xc0cEO\xba\xe6`U\x17l\xd5\xc5_\xfa\x9d1P\x94&\x16k\x8c\x9b\xc3\xef\x8f\x01\xac\xe34i(oi%\r\rb\x91\xb6ދ\xecXa\x99\xb7FG\xb8yL\x1fc\x04\xb8\xf6\xfb!\xceF\x80\x1a\xe0\x10\x01t\x95e\xa0\xf5\xba\xe2|_o\xc7\xf8J\xa8\xf1\x912~>R8h\x83\x82`\xd1;\ni\x14a_\xee\r\"\x0f\x9a\x1e\xb6*M#\x85炯\x86Ԇ\x16']\xd8p\xdd\a\x83W\xf6\xa8\xbcUTI\xeb\xb1Sݰ?\xe6\\\x1ap\xeeK\x9cdYh\x90\x13\u0601 \xd6;;\x12\x87;\xa7&B\xf1;\\\x9d\x87\v\xfe.\xa4B\xa2\x17\x13\x11\x9f\xed\xd0x\x01\xcew\xba\x86\x89\xb5\xa2x\x9fI\x9f\b\xfd\xe0\xd7e+\xael\xf4\x0f\v\v\u2d285j\x9b3ͺ~\xe1yF\xee\xfav9\x04\xee\x14\x13\u05ff\xee\xe5\x99j\xdcG\xf7Y&\xad\x8f\xee$\x83\x16\x81X\xcb\xf8\xf9qGU?\xedPw\xfc\xd2\x05\x1cY\xd8CG9\xf7\x1b\x1d\vКn\xc2i\xee\x8fv\xea\xb1\x01\x01.=\xe7\x16O\"@\x9b]qݳ̝\xca\xd0\xccT\xd4w\x10\n|[\xad\xbeӄ\xcb\x18T\xbcЅ\x85\x9b\xc2\u009cl\"\xa1\x9eJ\xa6R\xe6p\x1fꆖ6\x18\t#w\x9a\xbb݀\xb3\r\xb3s\x1d˹\rU+\xba\x81E&9\a\xb4\xd6\xfdq\xbd\xa4\xae\xfb\xbd\x87_\x80\xeaQ\xd4>\xb6\xdb\xfa\x15@\xc7m\xb7\xf0M]\xb9;\xde\xdee\x98\x82\xe6\"\xbdހ$v<)PvT\x88\xde2\xd7\x1fi\xbbm\xd0:o\x96}\x9e\xd7_27\xf7y\x81\xb8<\x16\xf4W\xa9\xe6\xa4`\xc2\xfeCE\xee\x16\xf0\xc2Ǔƿ\x95\xf2\xe16\x12\xc4\xf6\x06\xffCݰY\xea`\xc2\r\x1b7\x8c\xaed\xe5W\xdf\xeb\x806\xbe\xac\x82'\xf3\x9fy\xba\x890\x8f\xf8\x83\x1e:\x83\x19\xdd\x1f:\x90F]\x81\xeby\x00\xd6m\xb8Ɍ\xf3\xfd\xfc\x10\xf2\xc1\xad\x89\r\xec\xd6\xcd\x05>\fh\xce#\x18\xe8(\xacHE\x81\xd4\a_\xb4\r\xfa)\xb3^O\xe6\xa1`\xb2G\xe3\x1f\x9a\xd6Ctt\xc3l\x85{\x03\bv\x82\xc0\xf3N\xd8\xf1\x9a\x8a\x11\u1ff1m\xea\xb3\vZ\x13\xb7P%6\x98\xa5\x8b\xef}_\x90O\xd0_\xaeX\x90\xbfVPEh\xb0\b\x17\xc3\xdd\x1a\xaa\xfa)_\xb7\r\x1er\xac\xe8@m\x8c4Y\x8a\x1b%7\nt_X\x17\xe4o\x94\x19&6\x1f\xa5\xba\xe1Ն\x89\xcf\xc3[~\x8e5\xbe\xa1\xca0+\xecn<\xb1\x812A9\xfb{̮\xb5_\x8e\x03\xba\x1e\x9c`-H\xc20\x86^\xbc\a\x1b\xe3\x0e\xe6\x05\xa2&\xb4\xf4t=%^\t<\x19\xb3\xa9u,\xd1\xc4\"\xa1\xdb\v\xf2IF\r\x83/\x87b]\x986$\x03m\x16\xb0^Ke\xdcj\xf5bA\xd8:$\x1f\xac\xcd\xc1\xbc\x99\xbb\xab\x92\xb0\xd82s]hҸ/Lz+\xf4\xc2x\x94}A\xf7ne\x8afYe#\xacKm(\x8f\x048\xcf2\xfc\x98\xe5\xb1\xca\a\xf9/\xcfZ\xc9[\xb6\x01\xf5\x93\x8e؏#)\x1e\xa6\xe1\xa2>nQ\x04A\x1e\x153\xc6\xc6T\xf2H)\x81'\x95\xb1\xb1\x15\xe7D[R\x9f\x94}$Ό.\x87Kr\xd2P\xbe\xab\xa1\f\x99g\x8f5\xde̸B\xda\x10\x1b\xf7b\xf5\x91oeٜm\xa9\xd8\f\x9eP\xb0U\xb2\xdal\x83$\x0f\x04\xd3$\xaf\x00\x93\xb5hRt\xb8X\xd8TJ\xb4J\t\x8el\xfb&A\x18p\xb84{ U9\xf7\x17\xf7\xfa{\x99/\xfd\x1d(\x8b\xb5\x92\xc5\xc2\xf7\x8b\xb9Թ_\xc9WL\xda\xc8\xc5l\xa3T'.j\xf7\xd7\f\xa0$\x94%\bB\xb5\xef9ᤨ\x93\xdd\xd4o\xd65\xdcH\xcd\x12\xa2\xfd(\xc7\xff\xda\x06\x10\x18^\x86\xbf\xbb\xcc\xf03\x18\xec3\x86\xc7g\xbf\x05\x1fvT\x187\x9d\xa8]\xe4\xcc9\xb1٤\x89\x8c\xb6\x8e\xedYI\x9a\xdb\x0e\x84\x91\xfc\fv\x17gѭ/\xd7p\a\x81]\xfb\xebWk\xc0s\xa2\x99\b\x17_\xbb\xd2\x0f'\xfdѕ@\x81\x17UJ\x15\xaf\xc6<\x9ep\xe9\"\xf4\xba\xb9\x96]\x1dI|8y*~\x7f\x00\xe3`S7\xdeKZ7\t\xd3\xe7?\xb0\xd8z\x00\x96\xf1f\x16\x95?\xfe\ue6f5wIS\xbd8E\x8e\xcd\xfcpR7<\x85\xeb\xdeCz\xc3\xc1j\x9b\x06\xe8N*'\xe9\xdc\xee\x8cٴs\xa6\xd2\xc2\x15\xef\xe7\xc9%\xedΘD{\xb1\f\xdayQ~\xa4xA\xf4IZ\xfb7\xffm$\x85\xe6\xc1\x9e;\x89\xd6ʡ\x85\x81\xbfj\x16-\xeas{?\xa2\x9d\xce[\xd6\xc2\xf7\xe4\x7f\xf9\xff\x00\x00\x00\xff\xff<\x82OF\xb8\x82\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfe\x15\x84\xeeav7\xba\xe5u\xdcG\\\xe8\xcd#\xdb;\x1d3ck-\x8d\xf6\x99\xae\xca\xeefDA\rP-\xf7\xde\xdd\x7f\xbf \x81\xfa袪\xa8VK\xe3\xdd5/\xb6\xba !?I\x92\x04\x96\xcb\xe5+Z\xb2{P\x9aIqEh\xc9\xe0\x8b\x01a\xffҗ\x0f\xff\xad/\x99|\xbd\x7f\xf3ꁉ\xfc\x8a\\W\xda\xc8\xe23hY\xa9\f\xde\xc1\x86\tf\x98\x14\xaf\n04\xa7\x86^\xbd\"\x84\n!\r\xb5?k\xfb'!\x99\x14FI\xceA-\xb7 .\x1f\xaa5\xac+\xc6sP\b\xa2\x1d\x8a\x1cB\xb5\xc4[{:\xae\x0f\xf81\xa6\x06\xae\xac6-\x88L\x93\x8b\v\"\x15\xb9p\xae\xc9\xc5µ\xae\x187K&\xda}<2\xceC/\xf3\x90w4t\f\xd5w\xf2\x83v\xcat\x12-\x06`\xb5H\xf3\xb8\x03\xb3\x03EJY\xbb\x02\x1bƁ\xe8\x836Px\u0084\xe9\xd5\xe3\x13\xe9\t\x8d\n\xe7\x1e\x84\xb6t\xf5\x88\xf4\x91\x17\x15\xe7t\xcd\xe1\x8a\x18U\xc1\x00m\xd6Rr\xa0b\x828\x9fA\x1b\x96\x9d\x834\x0eR\x840\xca\x7f\xe8P\x00\xbd\t\xfa\x00\x84F@{\x9aY\xb7\x85\xf3\x16a\xbbT\x89\x8e\xa9T\x90\xd9\xe9\xec\xcaO\x93\f8N\xcdB\x12.\xc5\x16\x94\xeb\xddZ\xbd `\n\xac\xc0\xe5\xc4\xce@\n\xb8\x9dfɦ\xb2\x93\xd3%\xb1\xda=(\x03Lh\x034\"\x9cO\xe0\x0f|\xb1\xd6\x19\xf2k\xe7\x91\xdeZ\xc7:\x0fˉ\xdet\x92§\xf7\xa3\x10\xbd\xdb\xc2Y\x86ޱw\x84\x97\xe8\xd0\xc7Ĵ\xf1^\xecԄk\n\xcbJ?\xec\xc6-\x19\xb5\a\x1a\x8cmt\xf1\xa7\x8b\x05r\xb8\xdbk\xb7\x0fM\xa8\x82\x9a,\xc9v\x13\x8a\xd2\x1c\xfa\xb5\x99\x81\"B\xc5Q{\x92\xc8O\xaa\x14=\fp\xb3^\x18\x9d\x91\x9fC0\x8f8*B\xb5\x17\xe6\xe9q\xbf\xff\xcc\\=\x0f\x1f5\x86\x01(\x13\x96\x7fvE\xdea\x9fv\v[K6!M\x04\x9e\xf3\xeb \xc75\xeb\b\xb7~'b\x9dE懄\xbc\x96-/\xbc\xff\x90\x94\xdaI\xf90E\x9d\x1fl\x9df\xb5H2\f7\x915\xec\xe8\x9eI\xe5Qo\xa6Z\xf8\x02Ye\xa2ZO\r\xc9\xd9f\x03\xca\xc2)wT\x83v\xf1\x83a\x82\f\xafkHˌD?\x1e\xe1\xd10Ҳ\t1\x1f\x1a\xba\xf5#\x8eg\xc9P\xec@\xad{\x8d\x93q\xce\xf6,\xaf(\xc7y\x99\x8a\xcc\xe1C\xebqŬ\xcc\b\x93{c\x8eJ\xa6+\xce!\bHY&u\x96\x90R\x80\xf5y\v\xbb&\xe8W\x1d\xc6|M\xad\xaf\"\x87\xb0'\xc8,Uqо\xab\x1c\xdd\xc8\xc6f,\x1a\xa6`\x84\x86p\xba\x06N4pȌTq\x8aL\xf1ٕ\x14#8@Ȉ\xe5\xeb\xae4\x1a\x04F@\x12\\\xc2\xedX\xb6s\xae\x9e\x15\"\x84Cr\t\xd6\xe13\x84\x96%\x8fL\x17M\x19e\xbe\xefdLכ2\xa1\xf5\xc7\xf0b\xfaߔ\x04\x9bٔ(i\x1b\xfd\xeaR\xb6\x16\x87\xf8\x9a\xb6)\xff\x9c\x84\r\x96\xff\x04\xa1\x1d\xd1~\x82\xe1\xb2d\x99\x1e\x94[KU\x06\xfaҺS\xe8\xe9,\b3\xe1\xd7)M\xe8\xf8\\\xbd(b\x87\b_7o\xe6\v}\"kRt\xe2\x99\x18Sw\xf1\x0f\xc8\x17\x9c2n\xfd\x8c\x91̓\x9fڭ\x16\x84mj\xa2\xe7\v\xb2a܀:\xa2\xfeI\xa6>p\xe6\x1c\xc4H\x99\xf5\b\xeek\x98l\xf7\xfe\x8bu\xc1t\xb3\x85\x97H\x97\xe3\xc6Α\r\xde~wz\x9e\x80K0\xbe\xcf\\\xb4U_⊩\xfd\v\xbaVo?\xbe\x8b\xaf\xaf\xda%A\xf2z\x88L(\x9d+o\x8f0j\x8fϻ\xf0\xe1\v\xfa@\xf5\x02\xc8Ū\x17\x84\x92\a88ׅ\nb\xf9CC\xe5\x84\xee\x15\xe0f\x15\xca\xd9\x03\x1c\x10L|\xf7\xa9_R\xa5\xc1\x95\a8\xa4T;\xa2\xa1\x1d\x13\xd3~W\xcd\xd2\xc9\xfe\x80\x84\xc0M\x87T1pūBd\xaf'^\x12mI(\x81\xf6'\xa0\x99$*\xed>\xda۷(\x01\xdfi\xc7K\xab1;V\xa2Yň\x83\xdc$3ԕ{\xcaY^w\xe4td%\x16\xe4\xa34\xf6\x9f\xf7_\x98\xf6;\xbc\xef$\xe8\x8f\xd2\xe0/\xcfBQ7\xf0\xe7\xa4g\xd8\xf1\xb1\b9+o\t\xd6ޣts\x9a\x95\xb6\x9a\xf6L\x93\x95\xb0\xcb\x15G\x92Įp;\xdau\xe7:**\x8dۋB\x8a\xa5\v\xdb\xc4z\xf2\xf4\x96\xaaC\xee'w\xea;\xbc\xb3\x93\x85\xfb\xe26\xc59\xcd \x0f\xdb5\xb8[K\rlY\x96\xd8_\x01j\v\xa4\xb4&\x8d\xcf\xd2\xealB\xad \t\x93U\a6s\x87\xab\xa6\x10\xe5\t\xe4\xc0Y\x1c]\x9cI\xee\xd2<\xc7\x04 \xcaof\xcc(3da\xaeih\x8d\xddM\xc1\x05ŭ\x96\xff\xb13-j\xd3\xff\x91\x922\xa5/\xc9[\xcc\xf5\xe1\xd0\xf9\xe6\x83f-0\t]b\xae\x8e\x95\x9f=\xe5v\xee\xb7\x06\\\x10\xe0\xce\x13\x90\x9b\x9e_\xb4 \x8f;\xa9ݴ]o\xe2\\<\xc0\xc1\xed\x18Nv\xd962\x17+q\xe1|\x88\x9e\xc1\xa8\x1d\x0e)\xf8\x81\\\u0dcb\xa7\xb8R\x89\x92\x9aX\xad#\xa2\x05-\xd3$\x14s\xadR\x1du\xbb`\rN\x88mX\xe7\x10Y'{\f\xdb$\x11-\xa5\x8el\xe4\x0f\feBxo\xa46.^\xd6\xf1\x99\xa3\x015\x19\x82h\x84n\\b\x97T!\v\xc7\x1a\xe5\xa9\xd0o\xbb\xdc\xed@\x83߯\xf0\x819\aԮ\xec.\x1a\xfdv\xd6\xfe\xc2\xed\x97`'4C\x8f\x05ۖJf\xa0\xa3{\xd9MI\x98/\"Y\"m\xdc\xeb\x98#u\xab$\x97\xab2\x1e\x02\r%\xdd嵄\x98\xb9^x\xff\xa5\x15\x10\xb5\xbao\xff\x9e\x92\xb1\xb9\xe3\"\x98KY\x14\xf48\x7f+i\x88\u05eee\xd0\x06\x0f\xc8->ԶBK\x90:\x97\xd7\x02\xf858\n\x05\x13+쀼y\x06\xc7\xc2\xdb\xd0X\xb2I\xac\x9c\xe6\xca^\x87N\x1a\xee\xd4?8U.%n\x15(\xe80\xaf\x1fUG?TH\xd3\nH\xccp7K\x99\x7f\xa7Ɇ)m\xdaC\xd0\x03i*Q03\x17^\xe2\xbdR'\xad\xbb>\xb9\x96G\td>o\xcd\x11&\x11s\xdc_\x02\xc26\x84\x19\x02\"\x93\x95\xc0\x00\x8e\xd5c\xec\xc2\x11\xd7YX\x96\xaa$i\xdaO\x06s\xd0be\x89\x92\xc2\xc4h\xa4\xa7]\xfd\x03e\xfdD\xb5X\x99\xc963\x94\xc5\x16+\xa7\xe9DHqkg*\x16\xf4\v+\xaa\x82\xd0\xc2\xf2\b'sV@\x97\xe9M\xe2\x9bm\x81ӄ\x91VcJ\x0e\x06|\xf2Z\xe2\x182)4ˡ\x9e\\\xbd HA(\xd9P\xc6+\x95h\x01g\x91w\xceR\xc4[\x82\xf3\xad1\xd2:_\")\x12\xa2\xb9\x89\xbe\xe2\xb85.U\xba\xc77\xe5f)\x98\xefe\x95\x8aIL\v<\xb3\xa3\xe5\x13)\xa98|\xf3\xb4R\x87\xfa\xcd\xd3\x1a+\xdf<\xad\x89\xf2\xcd\xd3\xfa\xe6i\xa5\xd4\xfc\xe6i}\xf3\xb4\xda\xe5_\xc2Ӛ\x1a\x91;\xe88\xf0qr\x14\t[\xd5cC\x1c\x81\xef\x93+|\x0e\xf8\x93r1WqP\x91\xc4\xff\x81\xb4\xee\x98\xd1j&\x8f:9\xd3jM\x90yw\xeej\u0095|B\xd6}\xe8\xf4|Y\xf7\xabQ\x88gʺ\xf7Þ\xf6\xb1Oʹ\x0fD\x99\x97\x9d\xbd\xf0\x89\x1a\x05\xd0\x10Vw\xdb\xf01\xbc\x86$d\xa2\xff\x17N\xcc\xede\x8d\x9dQ>\x9e=\x8b?YF\xa2,\xbd\xf8\xd3\xc5\xd7G\xfe\xf3\x10|\x90\xc4}\xda\xf9\x83\xdf\x11\xa8v\x05\xdaN\v\xebf\xe1}\x9db|\x16\xb9M\xcdį\x89\x18\x81\xd5\x15\xc9#*~\xad\xb6\xc0@\xf1\xa9\xf43\xd2\x13N\xaa\xae\"p\x92ΪR}\x10\xd9NI!+\xed\xa3\x12\x16\xd6\xdb̝\xf4\x0f c\xc2\x1a\xd5\xf0\xff ;YE2\xc1G\xc87\x91\x118\x8d|'9\xd0oB\x83\xa1\xfb7\x97\xdd/F\xfaT\xc1\xa1\xb3͏;\x10\xb8\xc3.\xb6\xed\x03\x00\xe1\xa2\x06\x7fc\xc1\xb1\x80E\x00IE\x04\xe3N\xf2\xeak\x1e\xdarG>\x95.\xf64\xdb\xef\x18\x8f\xa9\xa4%\x13\x9e\x9cB\xd8M\x11\x1c\xf0K\xe7\xeev\x9f\xe5\xc8\xc4\xef\x92\x1a8?!0%\"6\x91\xfcwB\xca_bn\xf1\x93\xb7\xe7S\x92\xfa欘\x9f-\x81\xef\xfci{I\xf4\x99NћC\x9dgO\xc7{\xc1$\xbc\x97I\xbdKL\xb8;_\xe6|Z<\xf6\xa4̱\xe9\xd0\xc1p\xd2\xdcd\xaa\xdcdha\n\xb1\xd9(M\xa6\xc0\xcdI|\x9b\xe4N\x9a\x9a\xbdXjۋ%\xb4\xbdl\x1aۨ\x14\x8d~\x9c\x93\xa8\x16\xbf\xaf\x87LN\xb6\xfc\xa5\x84\xedT2H\xd5q_OZ_}:\x82a\x19\x1f\\\xbb\x17\U000912ca\x1bVr\xdcHݳ<\x1al0;8\xd4\x17h\xfc*\xf1詿\t\xe6\xd3\xe7Zj/\x8f<}\xaa\xc9#pNhL\xafz\x98g\ue2aaL.\xc1\xceGV;\xfd\xc5 \xfe^\xab\x85\x13w<]\x8b\xb3Z\x11\v1Q1|\x8b\xcc\xe0đboz\x1e\xac\xf3\xc3\xf1\xb7\xdf*P\a\x82\xf7\xd8\xd4~Ns\b\xcc+\xa6\xb6\v\xb1`*\xbc\xd9\x1a\x8a\x9f\xf7\x9c\xfeF\x95\xc9[\xe1f\xdd\xe3\xf1`\x1bk#\x9aE\x8d5|\"vq\x13\t\n\xd6o.d\xdd:\xd2l\xcaAN=-\xf5\xbcK\x9c\xf9\x8b\x9cI\xaf\"\xdd\xf3\xfb\x9dNA\x9dr\xfa)-\x01`\xf2\xb4\xd3s-y\xa6\x16=\xc9~^\xdai\xa6y\x9b\x85\xcfxz\xe99N-%R*\xe5\x94\xd2<:\xbd\xc0\xa9\xa4\x17=\x8d\xf4R\xa7\x90\x92O\x1f%\xa5\xb8$\xef\x02\xa7\xa6\xa8\x9cx\x9cfz\x8fw\xfc4Q\xc2)\xa2\x84\xdd\xdfi$O@/\xe1\x94м\xd3A\t\xa9R8*\x7f)k\x9b\xee@\x8e\xf6;\u00ad\x7f\xb6V\xc7_\xc6\xe9\xc1\xdf\xc0\x8aw\xed\x0em_ZIky\x1b\x9d\xbd\xa8\xc6\xfd\xe9:\x93\xfe\x02^\xb7]\xa5\xa1\xa4\n/u^\x1f\\:Ktj~O\xb3\xdd\x11\xf4\x1d\xd5d#UA\r\xb9\xa87\x00_;\xe0\xf6\xef\x8bKB>\xc8:'\xa2}/\x8ffE\xc9\x0fv\x85B.\xda\rN\x93\x80\xa8\xb4\x85\xden$gY\xc4w\x8b\xde\xcd\xe4*\xf7.\xcb\xc0\x1b\xa3\xb2v\xca@i+\xc6]7t\xf3\xbaW`n$\xe7\xf2q\xe6ڟ\x96\xec/x\xa5\xf9\x13\xa2CooV\b#\x88\aޑ^'g\xd5ج\xc1N\xcb\r\x9eC\xba\xbf\xdat v\xf3\x1c۷\x06C\xee.\x88\x0en\x817\x9d\x99\xb4\xd6\xe5f\xe5\xc61ԋ\x95\x19*\x0eDbF\x8d\xd91\x95/K\xaa\xcc\xc1%j,:c\bs\xe9Xtgp\xf6\xe8_z\x1d%o\xb8\xeb\x1aw(\x0few\xd3\xf7\x98v\xa7\x8cc\xf8\xf4\xe2\xe4\xb9\xc53\x8ec\xd8-Y\"\xa5\"?G3\xbf\xce\x165\xd3\xfef\xe2\x9f\xe5\x1e\xdeE\xa3g\x1d\xf2\xdc\x1eU\x8f\xa4g\x05\x88\xee\xd2\xdd\xc1,\xd55\xe0\x85\xbc\xfdOOȷ\n]\xfb;UO\t\x94\xddvAD\xf0\v7̆\xceb\xf6\to\xc6?\x90\x9b{\\\xa3զͫ\xa8_\xa3\x85PY\xd8\f\x8e\xc0\xf1\r\xbe?\x7fj\x9a6R\xd1-\xfc$\xdd\xe5\xe3Sl\xef\xd6\xee\\J~\x90?\x1a\x94&v\x01\xaf\xbf\x06\xfd\bX\x93\xf3ݻ\xd4؎r\xe65\xcd\xc6\xf0S\xf8~w\xf7\x93\xc3ʰ\x02.\xdfU.\xdd\xc1\xdaD\r\x96\xc4\x01[\aim\xff\xbb\x93\x8fx\xf9o<\x8e\x19\x1e\x93h\x90Q\x80\xc9昂8\v\xa5\xaa\xe4\x92栮\xa5ذ\xed\x04v\xbft*\x1fM\xb3\x19\xfe葫\xe7\xa8\x00\xff\xcc9\b\xd6\xe7\xe1\x1c\xf8\a\xc6A\xbba%\x18\xe0\x9b~\xab\xda\x1eW\xc5\xda\xf9p\x1b\xfb\xb1\xee``\x8esha(\xba\x04e\xbd(\x17\xb4\xaet\x90\xd5a\xc4\x1b\x8e0a`\v\xfdU\xe0\x88\x05v\xb7J\xe3\xf4\x19\xcc\t\xaee~\x8cŷ:\xc8\xdf\x0f\xb7<\xe2d+\xe4\x15\xbbq\xcf9!7\xf7ךT\"\xc7p\xf1\xfd_ngIݾss}\xd0\xd6)\xa3z\x1fo\xd5r\x8e[\xf6\xc2y\xc7r\x13A`\bN끔Gf\xfc\xc5]\xe7\xbdiuh\xc93\xf4\xf4\x03^\xe9?\xfd\xf8\x83\xbb\xf9\xdf?\x19\xe3ձRxM\xaa\x7f\x15\x00\xaf\x15}\xc2\xfb\x0f\x9d\xe4/\xfd\xd6\x18(J\x13\xf35\xa6\xcd\xe1\xf7c\x00k?M\x1a\xca[ZIC\x85\x98\xa7\xad\x0f\"\x1bK,\xf3\xd6h\x84\x9bc\xfa\x18#\xc0\xb5?\x0fq6\x02\xd4\x00\x87\b\xa0\xab,\x03\xad7\x15\xe7\x87\xfa8\xc6WB\x8d\x0f\x94\xf1\xf3\x91\xc2A\x1b\x14\x04\x8b\xde(\xa4I\x84}\xba7\x88\x1bR\x1bZ\x9c\xf4`\xc3u\x1f\f\xbee\xa4\xf2VR%\xad\xc7Nu\xc3\xfe\xd8\xe4Ҁs-q\x91e\xa1AN`\x0f\x82\xd8\xd9ّ8<\xc65\x13\x8a?\xe1\xeaf\xb80߅PH\xf4\xc5&\xe2\xa3\x1d\x1a_\x06\xfaN\xd701W\x14\xdf3\xe9\x13\xa1\xef\xfc\xbahŕ\xf5\xfeaiA\x9c\xe6\xb5\x0e\xbd\xe6ҝ\x17\x9ef\xe4\xaeoWC\xe0N1q\xfd\xe7^\x9e\xa8\xc6}t\x9fd\xd2\xfa\xe8\xce2h\x11\x88\xb5\x8c\x9f\x1fwT\xf5\xd3.uǖ\xce\xe1\xc8\xc2\x19:ʹ?\xe8X\x80\xd6t\x1bns\x7f\xb4K\x8f-\bp\xe19\xb7y\x12\x01ڜ\x8a\xeb\xdee\xeeT\x86f\xa6\xa2\xbe\x83\x90\xe0۪\xf5\x9d&\\Ơ\xe2\x83.,<\xa1\x16\xd6d3\t\xf5\xa5d*e\r\xf7\xbe\xaehi\x83\x9e0r\xa7y\xf4\x0e8\xdb\xe2\x93N\x96s[\xaa\xd6t\v\xcbLr\x0eh\xad\xfb\xe3zN]\xf7g\x0f?\x03Փ\xa8}h\xd7\xf5;\x80\x8e\xdbn㛺tw|\xd6\xcc0\x05\xcd\v\x83\xbd\x01I\xecx\x96\xa3\xec\xa8\x10}~\xaf?\xd2vݠu\xde,\xfb8\xaf\x7f}oѼ\xa8\x15\x19gA\x7f\x95jA\n&\xec?T\xe4n\x03/4\x9e5\xfe\x9d\x94\x0f\xb7\x11'\xb67\xf8\x1f\xea\x8a\xcdV\a\x13n\xd8x`t-+\xbf\xfb^;\xb4\xf1m\x15\xbc\x99\xff\xcc\xcbM\x8492\x1f\xf4\xd0\x19\x8c\xe8\xfeЁ49\x15\xb8\x9e\a`݆'\xde8?,\x8e!\x1f='\xd9\xc0n\xbd\\\xe0݀\xe6>\x82\x81\x8e\u008eT\x14H}\xf1E۠\x9f\xb2\xea\xf5d\x1er&{4\xfe\xa1\xa9=DG7̖\xbb7\x80`\xc7\t<\xef\x82\x1d\x9f\xa9\x98\x10\xfe\x1b[\xa7\xbe\xbb\xa0\xb5p\vYb\x83Q\xba\xa1\x97\xee>B\x7f\xbbbI\xfeZA\x15\xa1\xc12<\fwk\xa8\xea\x87|\xdd1x\xc81\xa3\x03\xb51Re%n\x94\xdc*\xd0}a]\x92\xbfQf\x98\xd8~\x90\xea\x86W[&>\r\x1f\xf9\x19\xab|C\x95aV\xd8\xddxb\x03e\x82r\xf6\xf7\x98]k\x7f\x9c\x06t=\xb8\xc0Z\x92\x84a\f}x\a\xd6\xc7\x1d\x8c\vDMh\xe9\xe9z\x8a\xbf\x12x2eSk_\xa2\xf1EB\xb7\x97䣌\x1a\x06\x9f\x0eź0\xadK\x06\xda,a\xb3\x91ʸ\xdd\xea咰M\b>X\x9b\x83q3\xf7\x88'a\xb1m\xe6:Ѥ\x99\xbe0\xe8\xadp\x16ƫ\xec\vzp;S4\xcb*\xeba\xbdֆ\xf2\x88\x83\xf3$ÏQ\x9e\xef\xf1\xc1\xca_\x9e\xb4\x93\xb7j\x03\xea\a\x1d\xb1\x1fGR\xbcL\xc3y}ܢ\b\x82<*f\x8c\xf5\xa9\xe4H*\x81'\x95\xb1\xbe\x15\xe7D[R\x9f\x14}$Ό\xae\x86Sr\xd2P\xbe\xab\xa1\f\x99g\x8f5\xbe\xccX\xbf\n곏|-\xcb\xe6lG\xc5v\xf0\x86\x82\x9d\x92\xd5v\x17$y\xc0\x99&y\x05\x18\xacE\x93\xa2Ë˦R\xa2\x95J0r\xec\x9b\x04a\xc0\xe1\xd2\xec\x01\xdf/u/\x1a\xfb\a\xab_\xfb7P\x96\x1b%\x8b\xa5\xef\x17c\xa9\v\xbf\x93\xaf\x98\xb4\x9e\x8b\xd9E\xa9N\x9c\xd7\xee\x9f\x19@I(K\x10\x84j\xdfs\xc2MQ'OS\xbf٩\xe1Fj\x96\xe0\xedG9\xfe\xd76\x80\xc0\xf02\xfc\xdde\x86_\xc1`\x9f1<>\xf9#\xf8\xb0\xa7¸\xe5D=E^\xb8I\xecb\xd6BFۉ\xedIA\x9a\xdb\x0e\x84\x89\xf8\fv\x17gѭO\xd7p\x17\x81]\xfb\xe7Wk\xc0\v\xa2\x99\b/\x82\xbb\xd4\x0f'\xfdѝ@\x81\x0fUJ\x15\xcf\xc6\x1c\x0f\xb8t\x11z\xd9X˾\xf6$ޟ\xbc\x14\xbf?\x82qt\xa8\x1b\xdf%\xad\xab\x84\xe5\xf3\x1fXl?\x00\xd3x3\x8b\xca\x1f\x7f\xf7\xc3\xda\xfb\xa4\xa5^\x9c\"c+?\\\xd4\r/\xe1\xba\xef\x90\xdep\xb0ڦ\x01\xba\x8b\xcaY:\xb7?c4휡\xb4\xf0\xf6\xfdybI\xfb3\x06ў-\x82v^\x94\x1f)>\x10}\x92\xd6\xfeͷ\x8d\x84\xd0<\xd8s\a\xd1Z1\xb40\xf0\x17\x8d\xa2E\xe7\xdcޏh\xa7\xf3\x96\xb5\xf0=\xf9_\xfe?\x00\x00\xff\xffY\xa1\x05sу\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccZK\x93۸\x11\xbe\xebWt\xed\x1e\xf6\xb2\x94줒J\xe96\x96\x93*W\xc6\xf1\xd4hvr]\bhJ\xb0@\x80\x01@\xc9\xca㿧\x1a\x0f\x89\xe2C\x0f;q\u008b-\x12h\xf4\xf3\xeb\x0f\xc0\x14E1a\xb5|E\xeb\xa4\xd1s`\xb5\xc4/\x1e5\xfdr\xd3\xed\x1f\xdcT\x9a\xd9\xee\xedd+\xb5\x98âq\xdeT\xcf\xe8Lc9\xbe\xc7Rj\xe9\xa5ѓ\n=\x13̳\xf9\x04\x80im<\xa3\u05ce~\x02p\xa3\xbd5J\xa1-֨\xa7\xdbf\x85\xabF*\x816\b\xcfK\xef\xdeL\xdf\xfe~\xfa\xbb\t\x80f\x15\xcea\xc5\xf8\xb6\xa9\x9d7\x96\xadQ\x19\x1eENw\xa8К\xa94\x13W#\xa7\x15\xd6\xd64\xf5\x1cN\x1f\xa2\x84\xb4z\xd4\xfc]\x10\xb6\x8c\xc2\x1e\x93\xb0\xf0]I\xe7\xff<>\xe6Q:\x1f\xc6ժ\xb1L\x8d\xa9\x15\x86\xb8\x8d\xb1\xfe/\xa7\xa5\vX9\x15\xbfH\xbdn\x14\xb3#\xd3'\x00\x8e\x9b\x1a\xe7\x10f\u05cc\xa3\x98\x00$\xd7\x04i\x050!\x82\xb3\x99z\xb2R{\xb4\v\xa3\x9aJ\x1f\xd7\x12踕\xb5\x0fΌ\xb6@2\x06\xb25\xe0<\xf3\x8d\x03\xd7\xf0\r0\a\x0f;&\x15[)\x9c\xfd\xa2Y\xfe\x7f\x90\a\xf0\xd9\x19\xfd\xc4\xfcf\x0e\xd38kZo\x98\xcb_c\x8c\x9eZo\xfc\x81\fp\xdeJ\xbd\x1eR\xe9\x919\xffʔ\x14A\x93\x17Y!H\a~\x83\xa0\x98\xf3\xe0\xe9\x05\xfd\x8a\x1e\x02r\x11B\xf6\x10\xec\x99K\xeb\x00좔\xe0\xa3aMUo\xad3\xb5I\x15x\xedH\x89\xfaӛ\xa4}Kl\xce\xef)\xb7x\x14\xe9<\xab\xea3\xb9\x0fk\x1c\x13v\xe6\x8a\xf7X\xb2F\xf9\xb6\xa9\x14%\xd5\xce\xcbs\xb3j\xe4S\x11g\x9d\xad\xf8\xfe\xec]\\ue\x8cB\x16\xa5\xc4Q\xbb\xb71\v\xf9\x06+6O\x83M\x8d\xfa\xe1\xe9\xc3\xebo\x97g\xafa(\x91:EA\x81c\xad\xd8l\xd0\"\xbc\x86\xfa\x8bqsɴ\xa3L\x00\xb3\xfa\x8cܟ\x82X[S\xa3\xf52\x17K|ZX\xd4z\xdb\xd1\xe9\x9f\xc5\xd97\x002#\xce\x02A\xa0\x841\xafR\xfd\xa0H\x96\x83)\xc1o\xa4\x03\x8b\xb5E\x87:\xc2\x14\xbdf:)8\xed\x88^\xa2%1Tۍ\x12\x84e;\xb4\x1e,r\xb3\xd6\xf2\xefG\xd9\x0e\xbcI\xc9\xec\xd1y\b\x15\xaa\x99\xa2dm\xf0g`Zt$W\xec\x00\x16iMhtK^\x98\xe0\xbaz|\xa4j\x90\xba4s\xd8x_\xbb\xf9l\xb6\x96>#47U\xd5h\xe9\x0f\xb3\x00\xb6r\xd5xc\xddL\xe0\x0e\xd5\xcc\xc9u\xc1,\xdfH\x8f\xdc7\x16g\xac\x96E0DGH\xadď6a\xba;[\xb6W\xd2\xf1\t\x90zGx\b^c\xcaDQ\xd1\xc4S\x14\xe8\x15\xb9\xee\xf9\x8f\xcb\x17Ț\xc4HŠ\x9c\x86\xf6\xfc\x92\xe3Cޔ\xbaD\x1b\xe7\x95\xd6TA&jQ\x1b\xa9}\xf8\xc1\x95D\xed\xc15\xabJzJ\x83\xbf5\xe8<\x85\xae+v\x11\xba\x18\xac\x10\x9a:\x80Dw\xc0\a\r\vV\xa1Z0\x87\xdf9V\x14\x15WP\x10n\x8aV\xbb7w\aG\xf7\xb6>\xe4\x9e:\x12\xdaA4X\xd6\xc8\xcf\xeaN\xa0\x93\x96*\xc33\x8f\xa1\xba:\x0eJP1ޔ\xf33\f\x12\xf40\xceѹ\x8fF`\xf7KG\xe5\x87\xe3\xc03\x1dk\xb4\x95t\xa1\xbdBil\xb7\xf3\xb0#\x92\xb7\x9f\x8cx݀\x03\xa0n\xaa\xbe\"\x05<#\x13\x9f\xb4:\x8c|\xfa\xab\x95\xbe\xbf\xd0H \xe9\x89*.\x0f\x9a?\xa1\x95F\\1\xfe]g\xf8\xd1\x05\x1b\xb3\x872\xe4\xbf\xf6\xea@\xd8\xe5\x0e\x9a\xf7Q;?\x0fO\x1f2\x82\xc7\xdaJ\x85\x99|5\x85\x87TԦ\x847 \xa4#\"\xe1\x82о\xb3t\xa3\x02ј\x83\xb7\xcd]\xe6s\xa3K\xb9\xee\x1b\xdd\xe6Fc\x19sEt\xc7s\x8b\xb0\x12\xa1\x16eGm\xcdN\n\xb4\x05Շ,%O\x9a46v\x90R\xa2\x12=l\x1a\xad\xb2`\x8aEAE\xcdԕ\x18.\x8e\x03\x03\x93fR\xc7\f>\t\bXc\xabԚ\xb5G-\xb0\xdbm\x826&\x00\x9aC\x01{\xe97\x11)\xd5P\xdd\xc1\xc5ڣg\x8b\x87\xa1\xd7\x1d\xdd_6H#c\xe3Ep\xc8-\xfa\x90m\xa8(}(\x95\xa6\x00\x1f\x1b\x17\xb0\xb6\x8b\x13\xf9\t\x84/\xcf\xde\xe2\xa1\xefh\xb8\x16\xdcD\x85FT\x0e$j\x0e?\xfcpݤ^w\xcb\x0fQ\xf7l\xa8\xc5\x12-\xea\x1e\x9b\xc8\xcfK\xe8Q\x944\x94aX\x96Ƚܡ:\x84\x9eD\xe0\xf93\xac\x1a\x0f\xa2\xc1\x105Ʒ{f\x85\x03n\xaa\x9ay\xb9\x92J\xfa\x03H7\"\x9f)e\xf6(Rı\xaa\xfda\n\x1f\xb4\xf3LstG\x1eD\x1e\x8b\xa9\xc0t\x1c\x95\xaa8\x10:f\x8700\x8a\xaf\x8c\xf3\xc0\xd1R:\xaa\x03\xec\xad\xd1\xeb1c\a\xda!\xed\x01\xadF\x8f\xa1#\n\xc3\x1d5C\x8e\xb5w3\xb3C\xbb\x93\xb8\x9f\xed\x8d\xddJ\xbd.H\xc1\"\x81\xcf,\xec\xecf?\x86\x7f\xbe&\vL\x1dq\xe2\x86\xe4]\x86Z?\x10\xbd\xf5\x1b\x8c-b\x19s\xd0X \x02A\xa9]\xa5܍\xc8:TvC\xbc\xbc\xfd\xe4\x90\x0f\xf5\x8f-\xf6[\xc7\x05P\x01\xf8R\x9c|[T\xac.\xe2h\xe6M%\xf9\xa4km\xcc\xfb\xcb\xf8\x937+R\vɉܞ\xe3F\xdeĉ\xb3=̀\x1b\xba\xbb\x9c1\xb4\x1cvS47q\x85+\x1a\x7fj\x8f=m}#t\xa7\xfe\xef\xd0\x13\xeft\xa0\x91\xf8\x01\xb3}?\a\xc0\xe4FkB*o\x80\x1d\xdb\xc0O\xae\xdb\xff\xeeD\xcfU÷8\xe0\xf8\x9e)\xef\xc2\xc0\xec\xe38\x8dti\x1c\x86\xc6tM\r\xb8^\x11\x9c-\xd0ޢ\xcb(\xf2-\x1eH\u0091[0X<\xc0\xaa\xd1BaVu\xbfAM\xdb1Y\x1e\x88\xec\xbf<.\xb3c\x03\x01K[\xa7\xec\xde1 yO\xbb\x00JA1\x87_\x1c\xa6u\x9f\xb1\x04\xa9\x9dG\xd6#\xe9\xf1\x89\xbdq\x0e\xab\xc3\x00\u05fa\xd9A\xcfX~\xbb\x8f\x82\xae\xe4\xa1\xd4 8\xc6\xc4J\xb0\x92\xfa{\xde\x0f-\x1e\x02\xc4\x12\xdf \"}\xe6ґe\xeetth\xd0i\xf1\fdR\xc7\x02a\xd5\xd8\":\x1fR\x01\x8by9H䇃q\xb9.\xe0\x12\xb3\xe89\xfb>v1*\x13\x80\xdd\xc80\xe0z\xb2\xc0E\xa6\x017\xb0\x8d\x9e\x99\xa39\x05w\xb2\x0e\xf8\x0e\xcc\x03\xfe\xfb\xec\x03\xeef \xf0\xddY\bܖ)\x97\xd9\b|\x13#\xb9\xe0\x8bK\\\x05\xae\xf2\x15\xb8\xc8Y`\x94\xb7\xc05\xee\x02w\xf2\x17\bx\x82\xa5\xfcr\x032?\x85\x81\xb9\x93\xd6\xcco\xa8kH\x81\xc0\x06\xfaj<\xa1\x18q\xd0q\xd3\xfb)\x85\xef+\xfa\xee%\xd2\x17չ\x87\xf7e@\xbfB\x8c\x9eҰ\xa3\x17\xf2\xef\x04 \xe7\a c\x04mТ\xdd\xf1\xb4\xfdO\xf1X\x81\x0f\xa0\xf8\x992\xaf\xfd\x19\x17\x8e'\xf2\x99\xff\x10K\xa3Ͱ\xb1\x16]m\xb4\xa0\xb6w\xdb\xe1\xc4I\xe5\xff\xdc\x11\xc5pX\x8bs\xfa\xda\xf9\x96\xa3p\xd3\xf9\\\xb8߸\xfb\x84.\xde\xfa\xb4Ͽ\xccʡݵ\x0e\xe9:6~\x97\xb3\xb9\xc1\xce\xd6:\xb0#\xaa\xa4\xa1\xd1\xe1\xc8\"4\xad\xe9d`F\x9b\x17\xfa\xd0<\xa4\x03m\xf64\xb9%-v=\x13\xe9M8\xb4dZ\xa4\xe3b\xfa4 y/\x95\xa2\x1ef\xb12\xe4,\xd4^Zj\x96,\xb4\xb1\xddo\xa6o\xfewg\x81\x8a9\xbf\xc6Qq\xff\x99\xa6\x00[\x99\xc6\x0f\xf4\xfeV\xc2\x0f\xd6t\xb8e\xbfG\xc7\xf0\xb7\x03\xd7\xe8\t\x8d\xc9\x11፵\xe1\xb2._\"ݱ\xd1\x1cC\xe0\x87Ο8\xb4\xbf\xf5\xff\x00\xe2\x06\xbb\x06\xbbt\xefe촭\xb8&'\xb7\xdf4\xab\xe3\x15\xec\x1c\xfe\xf1\xafɿ\x03\x00\x00\xff\xff%\xff\\)\x99#\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVMo\x1b7\x10\xbd\xebW\f\xd0kwU\xa3hQ\xec\xadqr0\xda\x06\x82\x1d\xe4N\x91#-c.\xc9\xce\f\xe5\xba\x1f\xff\xbd \xb9+K\xab\x95\x93\\\xb27\x91Ù\xc7\xf7f\x1e\xd54\xcdJE\xfb\x11\x89m\xf0\x1d\xa8h\xf1/A\x9f\x7fq\xfb\xf8\v\xb76\xac\x0f7\xabG\xebM\a\xb7\x89%\f\xf7\xc8!\x91Ʒ\xb8\xb3ފ\r~5\xa0(\xa3Du+\x00\xe5}\x10\x95\x979\xff\x04\xd0\xc1\v\x05琚=\xfa\xf61mq\x9b\xac3H%\xf9T\xfa\xf0C{\xf3s\xfb\xd3\n\xc0\xab\x01;0\xe8Pp\xab\xf4c\x8a\x84\x7f&d\xe1\xf6\x80\x0e)\xb46\xac8\xa2\xce\xf9\xf7\x14R\xec\xe0e\xa3\x9e\x1fkW\xdcoK\xaa7%\xd5}MUv\x9de\xf9\xedZ\xc4\xefv\x8c\x8a.\x91rˀJ\x00[\xbfON\xd1b\xc8\n\x80u\x88\xd8\xc1\xfb\f+*\x8df\x050^\xbb\xc0l@\x19S\x88TnC\xd6\v\xd2mpi\x98\bl\xc0 k\xb2Q\nQ\x1fz,W\x84\xb0\x03\xe9\x11j9\x90\x00[\x1c\x11\x98r\x0e\xe0\x13\a\xbfQ\xd2w\xd0f\xbe\xda\x1a\x9a\x81\x8c\x01\x95\xea7\xf3ey\u0380Y\xc8\xfa\xfd5\b,J\x12O J]\x1b<\xd0\t\xbf\xe7\x00J|\x1b{\xc5\xe7\xd5\x1f\xcaƵ\xca5\xe6pS\x99\xd6=\x0e\xaa\x1bcCD\xff\xeb\xe6\xee\xe3\x8f\x0fg\xcbp\x8euAZ\xb0\fjB\x9a\x89\xab\xacA\xf0\b\x81`\b4\xb1\xca\xed1i\xa4\x10\x91\xc4N\xadU\xbf\x93\xe19Y\x9dA\xf8\xb79\xdb\x03Ȩ\xeb)0y\x8a\x90\v\x89cS\xa0\x19/Zɵ\f\x84\x91\x90\xd1\u05f9\xca\xcb\xcaC\xd8~B-\xed,\xf5\x03RN\x03܇\xe4L\x1e\xbe\x03\x92\x00\xa1\x0e{o\xff>\xe6\xe6|\xef\\\xd4))\x94\xe4\xb6\xf3\xca\xc1A\xb9\x84߃\xf2f\x96yP\xcf@\x98kB\xf2'\xf9\xca\x01\x9e\xe3\xf8#\x93h\xfd.tЋD\xee\xd6뽕\xc9Rt\x18\x86\xe4\xad<\xaf\x8b;\xd8m\x92@\xbc6x@\xb7f\xbbo\x14\xe9\xde\njI\x84k\x15mS.⋭\xb4\x83\xf9\x8eF\x13Ⳳ\x17\xddS\xbf\xe2\x02_!O\xf6\x84\xda#5U\xbd\xe2\x8b\ny)Sw\xff\xee\xe1\x03LH\xaaRU\x94\x97\xd0\v^&}2\x9b\xd6\xef\x90\xea\xb9\x1d\x85\xa1\xe4Dob\xb0^\xca\x0f\xed,z\x01N\xdb\xc1\nO\x1d\x9b\xa5\x9b\xa7\xbd-\xb6\x9b\x1d E\xa3\x04\xcd<\xe0\xceí\x1a\xd0\xdd*\xc6o\xacUV\x85\x9b,\xc2\x17\xa9u\xfa\x98̃+\xbd'\x1b\xd33pEڅ\xe1\x7f\x88\xa8\xb3\xb8\x99\xdf|\xda\ueb2ec\xb5\v\x04O\xbd\xd5\xfd4\xfc3\x9a\x8eFq\xce߲1\xe4\xef\xc5n\xe7;W/\x0fEdK8k\xd8\x06.\xbc\xfbu^\x8a\xa9~%3\xd5\xd1Gnt\"*\xcdw\xf4y\xb5t\xe8K\xb9@\xa2@\x17\xab3P\xefJP\xf9Ǡ\xacgP\xfey<\b\xd2+\x81'\xa4\r\x97\x95\x1ax\x8fO\v\xabw~CaO\xc8\xf3\x96ϛ\x9b\xca\x1e\xce߃WXZlʋE\xceVhNXd\t\xa4\xf6\xa7\xbcr\xda\x1e\x9d\xbe\x83\x7f\xfe[\xfd\x1f\x00\x00\xff\xff\xbeM\x1a\xea\xb1\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcWMo\xe36\x10\xbd\xfbW\f\xd0K\v\xac\xe4\x06E\x8b·\xd6\xd9C\xb0\xe96\x88\xb7\xb9S\xd4HbC\x91,9t6E\x7f|1\xa4\xe4\x0fYv\x9c\xcb\xea\xe6\xe1p\xf8\xe6\xcd\xcc#]\x14\xc5B8\xf5\x84>(kV \x9c¯\x84\x86\x7f\x85\xf2\xf9\xd7P*\xbb\xdc\xde,\x9e\x95\xa9W\xb0\x8e\x81l\xff\x88\xc1F/\xf1\x16\x1be\x14)k\x16=\x92\xa8\x05\x89\xd5\x02@\x18cI\xb09\xf0O\x00i\ry\xab5\xfa\xa2ES>\xc7\n\xab\xa8t\x8d>\x05\x1f\x8f\xde\xfeX\xde\xfcR\xfe\xbc\x000\xa2\xc7\x15\xd4\xf6\xc5h+j\x8f\xffD\f\x14\xca-j\xf4\xb6Tv\x11\x1cJ\x8e\xddz\x1b\xdd\n\xf6\vy\xefpn\xc6|;\x84y\xccaҊV\x81>ͭޫ\xc1\xc3\xe9\xe8\x85>\x05\x91\x16\x832m\xd4\u009f,/\x00\x82\xb4\x0eW\xf0\x99a8!\xb1^\x00\f)&XŐ\xdd\xf6&\x87\x92\x1d\xf6\"\xe3\x05\xb0\x0e\xcdo\x0fwO?m\x8e\xcc\x005\x06镣D\xd4\x7f\xc5\xce\x0e\xd3\x04@\x05\x100\xc0\x01\xb2;\x84 \f\bO\xaa\x11\x92\xa0\xf1\xb6\x87J\xc8\xe7\xe8\xc0V\x7f\xa3$\bd\xbdh\xf1\x03\x84(;\x10\x1c%;\x1c\x9c\xa5m\v\x8d\xd2X\xeel\xce[\x87\x9e\xd4Hy\xfe\x0e\x1a\xea\xc0z)\v\xfe8\xf1\xbc\vj\xee,\f@\x1d\x8e\xe4a=p\x05\xb6\x01\xeaT\x00\x8f\xcec@\x93{\x8d\xcd\xc2\fٔ\x93\xd0\x1b\xf4\x1c\x06Bg\xa3\xae\xb9!\xb7\xe8\t\x1aE\xaf\xcb41\xaa\x8ad}XָE\xbd\f\xaa-\x84\x97\x9d\"\x94\x14=.\x85SEJĤQ+\xfb\xfa;?\ff8:\x96^\xb9!\x03yeڃ\x854\x1d\xef(\x0f\xcfK\xee\xae\x1c*\xa7\xb8\xaf\x02\x9b\x98\xbaǏ\x9b/0\"ɕ\x1aZl\xe7z\xc2\xcbX\x1ffS\x99\x06}ޗڔc\xa2\xa9\x9dU\x86\xd2\x0f\xa9\x15\x1a\x82\x10\xab^Q\x18{\x9dK7\r\xbbNR\x04\x15Bt\xb5 \xac\xa7\x0ew\x06֢G\xbd\x16\x01\xbfq\xad\xb8*\xa1\xe0\"\\U\xadC\x81\x9d:gz\x0f\x16Fy&j^\x01\x128\xe1[\xa4\xa9u\x82\xe5Kr\xe2\xe3_:q,X\xdfcٖ\xac9a\x00\x92\xf5\xe8\x87i\xa1.a\x80\xd9F\x9fE2\xf67\xd3\xc0\xbc\xb2\xa0\xb0\xd8\x1db:=\x9a?4\xb1\x9f?\xa0\x80\xdf\x13\xe6{\xdb^\\_[C<\x17\x17\x9d\x9e\xac\x8e=n\x8cp\xa1\xb3o\xf8\xde\x11\xf6\x7f:\xf4\xf9\x1a\xbe\xe8:\xde滫\xef\x82c\xd4g\xcf}D\xbeA\xf0|\xa6\x83\xc3UQ\xae\xc04x^\x95\xe8zs\xf7\x1e\nϸ\xbf\xa3Hw\xa6\xb1o\xa4\xb8w\x9c\xf5;#\x03\xe3\x97\xde\x10o\xf74\xbfBƞ\xe6-\xf9\xeeD\xf8\x14+\xf4\x06\t\xc3^\xa9_\x14u\xb3\x11\x01^:%\xbb\xb41\r\x04_\x02!X\xa9\xe6$\xf5\n\xf8\xac#\xca\xe3\xccP\x16iXg\xcc\f\xfe\xc4|F\xfd\xce\x1dP\f\x8at\x95\x82\x92\xa0\x18ޡ\xa1\xc9\x7f\xa4ZF\xef\xd3\x15\x95\xad\xfc2\x99n\xb8VDG\xe5\xf9\xeb\xf1\xfe\r%\xbd\xdd{\xa6\x17\xb7P&\xa3q\x1e\x8b\xa0Z~A\xf1\x1akiҸS2\xf2w\xfc\xc2;&j\xb6\xa2\xf8թ<\x80o@\xfc\xb8ŝ\x8f&\xdf\xf3\xd37l\n\x88\x81\x9f[ \x85\x99\xc1X!Ԩ\x91\xb0\x86\xea5\xdf\\\xaf\x81\xb0?\xc5\xddX\xdf\vZ\x01\xdf\xff\x05\xa9\x9962QkQi\\\x01\xf9x\xae\xcbf\x13w\x9d\b3cx\x94\xf3\x03\xfb\xcc5\xc6n\x18/v\x06\x9c\xbd_\n\xf8\x8c/3\xd6\ao%\x86\x80\xa7ct6\x93\xd9!81\x06~\xa4\xd5\a,\r\x7f\x19\x06\xcb\xff\x01\x00\x00\xff\xffx\xae@\xbaJ\x0e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4:Ks\x1b7\xd2w\xfd\x8a.吤\xca$\xe3|ߦ\xb6x\xb3\xe5͖v\x13\xafʔ}I\xe5\xd0\x1c49\x88f\x00,\x80\x11\xcd\xcd\xe6\xbfo5\x80\xe1\xbc@R\xa2\x93\x18\x17\x89x4\xfa\xfd\xc2\xccf\xb3+4\xf2\x03Y'\xb5Z\x02\x1aI\x1f=)\xfe\xe5\xe6\x0f\x7fus\xa9\x17\x8f/\xaf\x1e\xa4\x12K\xb8i\x9c\xd7\xf5;r\xba\xb1\x05\xbd\xa1\x8dT\xd2K\xad\xaej\xf2(\xd0\xe3\xf2\n\x00\x95\xd2\x1ey\xda\xf1O\x80B+ouU\x91\x9dmI\xcd\x1f\x9a5\xad\x1bY\t\xb2\x01x{\xf5\xe37\xf3\x97\xdf\xcd\xffr\x05\xa0\xb0\xa6%\x18-\x1eu\xd5Դ\xc6\xe2\xa11n\xfeH\x15Y=\x97\xfa\xca\x19*\x18\xf6\xd6\xea\xc6,\xa1[\x88gӽ\x11\xe7;->\x040\xaf\x03\x98\xb0RI\xe7\xff\x99[\xfdA:\x1fv\x98\xaa\xb1XM\x91\b\x8bN\xaamS\xa1\x9d,_\x01\xb8B\x1bZ\xc2[F\xc3`A\xe2\n \x91\x18К\x01\n\x11\x98\x86՝\x95ʓ\xbda\b-\xb3f \xc8\x15V\x1a\x1f\x982\xc2\x0f\x9cG\xdf8pMQ\x02:xK\xbbŭ\xba\xb3zk\xc9E\xe4\x00~qZݡ/\x970\x8f\xdb\xe7\xa6DGi52w\x15\x16Ҕ\xdf3\xca\xce[\xa9\xb69$\xeeeM \x1a\x1b\x84\xca\xd4\x17\x04\xbe\x94n\x82\xdd\x0e\x1dch} ;\x8fKXg\x88\xcecm\xc6H\xf5\x8eF\xac\x04z\xca\xe1t\xa3kS\x91'\x01뽧\x96\x92\x8d\xb65\xfa%H\xe5\xbf\xfb\xff\xe3\xecH\xfc\x9a\x87\xa3o\xb4\x1a\xf2\xe65\xcfBo:b²ڒ\xcd2H{\xac>\x05\x11\xcf\x00^\xf7\xceGL\"\xdc\xfe\xfcYTnUa\xa9&u\x19B\xb2;=Ŧ\x0f\xba\xbfj\xac\xd4V\xfa\xfd\x12^~\xf3T4\xd9>@o\xc0\x97\x04IyV^[\xdc\x12\xfc\xa0\x8b\xa8h\xbb\x92lR\xb4u\xd2\xfeR7\x95\x80u+\x18\x00\xe7\xb5\xcd*\x9b\xa1b\x1eO%\xb8-ؑ\xc6\r\xef\xfc#\f\xa2\xb0\x84Y\x83h\x9d\xe6<\xec\x90Z\xe5\xad\xe2Ֆ\x9ed\x11}\x96*-\xe8\xc0?\x9a\xa0%\x1d\x18\xab\vr\ue1212\x8c\x01\"o\xbb\x89\xb3\f*)\xeci\xf1iL\xa5Q\x90\x05\xaf\xa1D%*b2\x10\xbcE\xe56IE\xa6\x02l\x8f\xdd\xef\xcd\x10\x95\xf7i\xe1\x18:q\xd7\xe3\xcb讋\x92j\\\xa6\xbdڐzuw\xfb\xe1\xffV\x83iVcm\xc8zن\x8f8z\xc1\xb17\vCr\xff;\x1b\xac\x01\xf0\x05\xf1\x14\b\x8e\x92\xe4\x02\x1bR \x91p\x8a\xec\x91\x0e,\x19K\x8eM+h\x94\xde\x00*\xd0\xeb_\xa8\xf0\xf3\x11\xe8\x15Y\x06\xd3\xdaB\xa1\xd5#Y\x0f\x96\n\xbdU\xf2?\a؎y͗V\xe8\xc9\xf9`\x8cVa\x05\x8fX5\xf4\x02P\x89\x11\xe4\x1a\xf7`\x89\xef\x84F\xf5\xe0\x85\x03n\x8cǏ\xda\x12H\xb5\xd1K(\xbd7n\xb9Xl\xa5oS\x86B\xd7u\xa3\xa4\xdf/B\xf4\x97\xeb\xc6k\xeb\x16\x82\x1e\xa9Z8\xb9\x9d\xa1-J\xe9\xa9\xf0\x8d\xa5\x05\x1a9\v\x84\xa8\x906\xcck\xf1\x85MI\x86\x1b\\;\x11t\x1c!\xd2?C<\x1c\xfb\xd9\b0\x81\x8a$vR\xe0)fݻ\xbf\xad\xee\xa1\xc5$J*\n\xa5\xdb:\xe1K+\x1f\xe6\xa6T\x1b\xd6y>\xb7\xb1\xba\x0e0I\t\xa3\xa5\xf2\xe1GQIR\x1e\\\xb3\xae\xa5g5\xf8wCγ\xe8\xc6`oBZ\x05k\xb6%\xf6\x00b\xbc\xe1V\xc1\r\xd6Tݠ\xa3?YV,\x157c!\x89wg\xf9\xc3c#\xa9\x12!s8\x7fwVsy\xdcn\"\x12!\"x\r\bFRA\x83h\fR9O(\xd2$;AKi\xedE\xf4\xf4G\x91\xe4\xd1Em\x96\t G\x1e)\xe0\x1f\xab\x7f\xbd]\xfc]G:\x00\vN\xcdB\xad\x17\xf2\xed\x17\x87zO\x90\x93\x96\x04Wo4\xafQ\xc9\r9?O\xd0Ⱥ\x9f\xbe\xfd9\xcf?\x80\xef\xb5\x05\xfa\x88\\5\xbd\x00\x19y~\bf\xad\xdaH\x17\t?@\x84\x9d\xf4e@\xd4h\x91\b\xdc\x05\x12<>\xb0%G\x12\x1a\x82J>d\xec'\x8e\xeb\x90\xcduh\xfe\xca\xd6\xf3\xdb5|\x15\x9d\xd75\xff\xbc\x8eh\x1cҖ\xbe\x81u\xe8D+\xb3r\xbb\xa5.\xef\x9f(\v\x87Y\x0eP_\x83\xb6L\xab\xd2=\x10\x010\xcb)\xc6\a\x12\x13\xf4~\xfa\xf6\xe7k\xf8jȃ#WI%\xe8#|\xcb\xde'\xf0\xc6h\xf1\xf5\x1c\xee\x83\x1e\xec\x95Ǐ|SQjG\n\xb4\xaa\xf61\x01~$p\xba&\xd8QU\xcdb\x82(`\x87{Л#\xf7\xb4\"b\xd5D0h\xfd\xc9$1\xf1\xe1\xb4\xd1L\xb3\xa6v<\xcd^B\x16\xf5$\xeb\xfdl\x19\xc8\x139\x11ʅO\xe0D\xbf\xf4\xba\x80\x13\x0f͚\xac\"O\x81\x19B\x17\x8e\xf9P\x90\xf1n\xa1\x1f\xc9>J\xda-v\xda>H\xb5\x9d\xb12\u03a2\xd4\xdd\"t\xbb\x16_\x84?\x97\x12\x1e\xdaT\x9fJ}\x00\xf2\xf9X\xc0\xb7\xbb\xc5%\x1ch\xb3\xfb\xa7Ǯ\xa3|X\xa5\x84s\f\x93m~Wʢlk\xbd\x9e\xb7\xadQDw\x8cj\xff\x99l\x87\xf9\xdcX\xc6h?K\xad\xda\x19*\xc1\xff;\xe9<\xcf_\xc2\xd8F~\x92sy\x7f\xfb\xe6sZT#/\xf1$Gj\x988>\xce:\xacf5\x9aY܍^ײ\x18\xed\xe6\x1c\xfeV\xb0\x906\x92\xec\x99\xf4\xef\xdd`s\x9b\xa0f\xaa\x81Þg\xe5\x9f\x1e\xb7\x99\x84\xaf\xdf\xc5>\x95\x16\x9e\xe4\xd7yU\xb8ǭ\x03\xb4\x04\b5\x1aֈ\a\xda\xcfb\xc6aPr\xba\xc0\x19\xc1\xa11\bhL\xc51=f\x11\x19\x88)\xffM\xecA\x17\xe8;Ɛ\xac(ۮԊ\xbc\x97\xea32\xe7\xfd\b\x91ߗQ\x87\x9e]\xa1\xd5FnS\xb7s\xca)\xd5T\x15\xae+Z\x82\xb7ͱ\x9a\xeb$#\xefy\xcbi\xfa\xdf\xf7\xb6\xb6\x1a~\xa6\xc1\x98\xa7j\xd0v\x9c\x12C\xaa\xa9\xa7\xa8\xcc\xe0A\x1b\x89\x99yK\xceO\xac\x97\x17\xae\xaf\x9fccQ)/)\xb9c\x19\x9c\xabJ\x93\xa2\xa7\x04\xbe\xadL\xbd\ueabc\xacП\xe1\x1b\xb8\xba\xe7rd\x88\xf7,\xdf.\x19\xed\xe9u\x97\xdb)\xa3\xc5hf\xe8\x06G\x8b\x91\xbe'\xf5\x90BC\xfb\x19]\xa4\xf8Ȗx\x1a\x83\xa3o\x9f\xde8\xed\xbe\xb4\x8fą\x9d\xf1$\x0e\x8d\xfeK$\xfej\f$\xf4~\xadHF!k:\x94\xfeC_\x17\x8b\xbb5\x81\xb1d0\xdb\x15\x82йw\xa1\x85\xf9\xa5\x8b\xc0\xa4\x83Ƒ\b\x1d\xb4\xc9\xdd\x13\b\xed;\x93@O3>\x7f\x99\xbf\xc87\xa6\xe2\x9b_\xff\xa5\xe4\xa2.\xd5\x14̔\x85\xd8r-<ᴏ\x8d9\x8eu\xe0\x0e\xfc\x8a\xd0H\x84*\x94\x8b\xe4\rʊ\x04\xb4/\xd9τ\xb2\xa6\r\xa78\xd1ǵ}\x9c\x84\xde\xf1\xfa\xef\xb4$3L\x98&<\x7f\xa40\xc7O\x8dg$y;\xda\x0e\xa5\xae\x92\xbcTS\xafɲa\x86\aOP\xb4㺿(Qm\xb3N\xae}\xb0#\xa8\xd0yXw\x1f\x06\xe4\x88\uffd8\x8e)\xeb\xbfpv\xa3&\xe7p{Ν\xff\x18w\xc5\xce]:\x02\xb8֍\xcf\xdb\xef\x97.\xb9\xa0\xe7u\x0f\xb3M\xb1\xa1\xf7C_\xb6\xcen\xd3TU8ӏ\x1b\xdd\a\x1c\x01\xab5\xe53\xfe\x13\xad\xc3S\b\x96\xe8α\xea\x8e\xf7\xe4\xfc\xf1!؝t\xc8p\"\xb0\xbf\xa5]f\xb6\xf5s\x99\xa5\xbb\xe4<3K\x93/1\xfa\x8b\xb17\x9e\xe3\\\xbb\x96\x85y\xf8\xce!\xb3\xf6}\xf0*\xcfbv\xc2\xef\x12\xb7y\xe8\xadw\x96\x17>[\x98\xd8\xdf0\xff@%\xfab\xcb5!\xba\xf3\xad\x06EH\xa9\x91\x96\x9e\x04\x82\xeb\xf2\x1a\x84t\xa6\xc2\xfd\x81\x96P\xfa\xb1\xa9\xe6\xdfG:\x8bj=\xa6\xa1c\xa9\xec\xe9\x0e\xf7\xe1k\x91|]{\xda_\xc0\x19\x9f\x11\xd6\xf5qg\xf8{\xdcp\"\x15w\n\x8d+\xb5\xbf}sF5V\x87\x8d\xad=vee\b,\xe1\xe9-mJ\xaa\x90A\xb5\xf3n\xcfr\x16Ï\x87.\xd1\xe2\xd5\x00\u0099\xb8\x9f\xbee\xcaE\xd7\x15{\x01v@\xe1a\xf7f\xfc\x05NjC\x90A\x9f\x1a\xe41\x1e\xe5\xba\nZ\x85:B\xdb\xe9+;\x9c\r\xe4C\x82\xfe\xcc\x18\x9eU\xa7\xc9d\xc0\\\xf4`\xa77\xcd\xfeL\xb3><\xf7/\xe1\xd7߮\xfe\x17\x00\x00\xff\xfff=C\x19\x96(\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z͒\x1b\xb7\x11\xbe\xefSt\xad\x0f\xb6\xab4d\xa4$\xae\x14o\xd2*Nmbo\xb6DI\x17\x97\x0f\xe0\xa09\x03s\x06\x80\x01\f\xb9\xb4\xe3wO5\x80\x19\xce\x0fH.\xa9\x925\x17i\xf1\xd3\xf8\xf0u\xa3\xbb\xd1`\x96e7L\x8b\x8fh\xacPr\x01L\v|r(\xe9/;\xdb\xfc\xc3΄\x9ao_\xdel\x84\xe4\v\xb8k\xacS\xf5;\xb4\xaa19\xbeŵ\x90\xc2\t%ojt\x8c3\xc7\x167\x00LJ\xe5\x185[\xfa\x13 W\xd2\x19UUh\xb2\x02\xe5lӬpՈ\x8a\xa3\xf1\xc2ۥ\xb7\x7f\x99\xbd\xfcn\xf6\xf7\x1b\x00\xc9j\\\x80V|\xab\xaa\xa6F\x83\xd6)\x83v\xb6\xc5\n\x8d\x9a\tuc5\xe6$\xbc0\xaa\xd1\v8t\x84\xc9q\xe1\x00\xfaQ\xf1\x8f^λ \xc7wUº\xff$\xbb\x7f\x10\xd6\xf9!\xbaj\f\xab\x128|\xaf\x15\xb2h*f\xa6\xfd7\x006W\x1a\x17\xf0@P4ˑ\xdf\x00\xc4}zh\x190\xce=s\xacz4B:4w$\xa2e,\x03\x8e67B;\xcf\xcc\x18\"X\xc7\\c\xc16y\t\xcc\xc2\x03\xee\xe6\xf7\xf2Ѩ\u00a0\r\xf0\x00~\xb1J>2W.`\x16\x86\xcft\xc9,\xc6\xde@\xf1\xd2w\xc4&\xb7'\xcc\xd6\x19!\x8b\x14\x8a\xf7\xa2F\xe0\x8d\xf1\xaa\xa5\xfd\xe7\b\xae\x14v\no\xc7,A4\xceo<\r\xc6\xf7\x93H\xebX\xadǨzS\x03,\xce\x1c\xa6@ݩZW\xe8\x90\xc3j\xef\xb0\xdd\xcaZ\x99\x9a\xb9\x05\b\xe9\xbe\xfb\xdbq>\"a3?\xf5\xad\x92Cr\xdeP+\xf4\x9a\x03\x12\xd2V\x81&ɐr\xac\xfa\x14 \x8e\x04\xbc\xe9\xcd\x0fH\x82\xdc~\xfbY(dz\xa0\xd6\xe0J\x847,\xdf4\x1a\x96N\x19V \xfc\xa0\xf2\xa0\xc2]\x89\x06\xfd\x88U\x18A'\x18\x04\xe9N\x99\xa4\xea4\xe6\xb306\nke\x8d\xf47\\\xe8\xb3\xd8Wn\x90%\xed\xabuE3?B(\x996\xb2\xd7\x05>\xcb\xc0\xfaDJű\xc7\xda\x04\x97\xb0\xa0\x8d\xca\xd1\xda\x13\x86OB\x06H\x1e\x0e\rg)*яi\x015\xbaR\x8c\xa3\x01\xa7\xa0d\x92W\x18t\xe8\f\x93v\x1d-c\xaa\xc2v\xda\xfb\xbd\x1eB\xf9\xd0\xca\xeb\xf5L0\x85\xa1ۗ\xc1\r\xe6%\xd6l\x11\xc7*\x8d\xf2\xf5\xe3\xfdǿ.\a\xcd@\xb4h4N\xb4\x9e9|\xbd\xc0\xd3k\x85\xe1\x9e\xff\x97\r\xfa\x00h\x810\v8E \xb4\x9e\x8b\xe8_\x91GL\x81#a\xc1\xa06hQ\x86\x98D\xcdL\x82Z\xfd\x82\xb9\x9b\x8dD/ѐ\x18\xb0\xa5j*N\x81k\x8bƁ\xc1\\\x15R\xfc\xd6ɶD8-Z1\x87\xd6\xf9\x83h$\xab`˪\x06_\x00\x93|$\xb9f{0HkB#{\xf2\xfc\x04;\xc6\xf1\xa3\xb7&\xb9V\v(\x9d\xd3v1\x9f\x17µ\xe18Wu\xddH\xe1\xf6s\x1fYŪq\xca\xd89\xc7-Vs+\x8a\x8c\x99\xbc\x14\x0es\xd7\x18\x9c3-2\xbf\x11\xe9C\xf2\xac\xe6_\x99\x18\xc0\xed`ى\xa2\xc3\xe7\x83\xe8\x05ꡨJ'\x81EQa\x8b\a-P\x13Q\xf7\xee\x9f\xcb\xf7\xd0\"\t\x9a\nJ9\f\x9d\xf0\xd2\xea\x87\xd8\x14rM\x86O\xf3\xd6F\xd5^&J\xae\x95\x90\xce\xff\x91W\x02\xa5\x03۬j\xe1\xc8\f~m\xd0:R\xddX\xec\x9dOY`E\a\x8a\xfc\x00\x1f\x0f\xb8\x97p\xc7j\xac\xee\x98\xc5?YW\xa4\x15\x9b\x91\x12\x9e\xa5\xad~\"6\x1e\x1c\xe8\xedu\xb4Y\xd4\x11Վ\xfd\xdbRcN\x9a%ri\xaaX\x8b\x18I\xd6\xca\x00\x9b\x8c\x1f2\x95v\x01\xf4%#\xcax\xd09\xb3\xa3\xefMJP\x8bX\xf6\x1cy\x8cw6\x06\xaaj\x18\xa8\xfa\xdf$F\x1a\xd4\xca\n\xa7\xcc\xfe\x10)\xc7&qT;\xf4\xe5L\xe6X]\xb3\xbd;?\x13\x84\xe4\xc4;v&M\xce(H\xf5@\x95,\x14\x1d\xb2\x89:\xe0\xde\xd18\xb2s\x8b.\xbdYy4\xb2\t\t\x87\x1c\x13\xfa\xb9\xe4x\xdb+\xa5*dc6\xb5\xe2g6\xfd\xa8\xa2\xe30\xb8F\x83>\xfe\a7\xab\x95wƎ\tٺ\x8f\x90r\x83S\x89}\xac\xc8\xdd\x1cS\xcdq;\x84\x13!)\t\xf8\xf5\xe3}\x1bvZˊ\xd0'\x91\xa5\xcfO\xd2,\xe8[\v\xac\xb8\x0f\xd4\xe7\xd7NZ\b}\xf7\xeb\x00\xc2\xfb^\xa7\x80\x81\x16\x98\xe3 \ue050\xd6!㱑܍\xc1\xd8\xf7\"\xf8ԣ \xe9;\xc4GR\t0\xf2\xf1\x82ÿ\x97\xff}\x98\xffK\x85}\x00\xcb)\x13\xf2w\x15\xacQ\xba\x17\xdd}\x85\xa3\x15\x069\xdd>pV3)\xd6h\xdd,JCc\x7fz\xf5s\x9a?\x80\xef\x95\x01|b\x94\xf4\xbf\x00\x118\xef\xc2Fk5\u0086\x8dw\x12a'\\\xe9\x81j\xc5\xe3\x06w~\v\x8em\xe8Ą-4\b\x95\xd8`\x9a}\x80[\x9f<\x1d`\xfeN.\xe5\x8f[\xf8&8\x89[\xfa\xf36\xc0\xe8\x12\x84\xbe\xd79\xc0q%s\xe0\x8c(\n<$\xda\x13c\xa1\x80F\xa1\xe0[P\x86\xf6*UO\x84\x17Lz\n\x8e\x18\xf9\x04\xdeO\xaf~\xbe\x85o\x86\x1c\x1cYJH\x8eO\xf0\x8aθ\xe7F+\xfe\xed\f\xde{;\xd8KǞh\xa5\xbcT\x16%(Y\xedC\xbe\xb9E\xb0\xaaF\xd8aUe!\x15\xe3\xb0c{P\xeb#\xeb\xb4*\"\xd3d\xa0\x99q'ӱ\xc8\xc3\xe9C3\xcdO\xda\xefy\xe7\xc5\xe7+\xcf:\xbd_,\xd6?\x93\t\x9f\x98\x7f\x02\x13\xfd\xab\xce\x15Ll\x9a\x15\x1a\x89\x0e=\x19\\\xe5\x96x\xc8Q;;W[4[\x81\xbb\xf9N\x99\x8d\x90EFƘ\x05\xad۹/\xd9̿\xf2\xff\\\xbbq_g\xf9\xd4\xdd{!_\x8e\x02Z\xddίa\xa0ͣ\x9f\x1f\xbb\x8e\U000b0319\xddX&\x9d\xf9])\xf2\xb2\xbdU\xf5\xbcm\xcdxp\xc7L\xee\xbf\xd0\xd9!\x9e\x1bC\x88\xf6Y,8fLr\xfa\xbf\x15\xd6Q\xfb5\xc46ⓜˇ\xfb\xb7_\xf2D5\xe2\x1aOr\xe4\xb6\x10\xbe\xa7\xec\x80*\xab\x99\xce\xc2h\xe6T-\xf2\xd1hʕ\xef9)i-М\xc9\xfe\xde\r\x06\xb7Y{\"\xeb\xee\xc6\\\x94v[ɴ-\x95\xbb\x7f{\x06Dz\x1b\xd8b8\xe80&\x9d\xad,:\x12's\xcdg\xe0Y\x8a\xdf\x12n+\x89\x88\x86\xb6\x98*U\x88\x9cU`}\x9b\x8c\xc5\xca\b\xb3\x95=\x05\x94\xaaG\x8e\xe1\xf6\xab\x8a=\xbc\xde\x17<\x1c\xf7\xb4C\xc8\xc3\xd1-jeD!$\xab\x0e\x1e\xdb_\x1d%\xab\x99\xff+a\xab5\xd3Z\xc8\xe2\"n\xdb\xfa\xd6\x12\x9d\x13\xb2H$\xfa\xfd\xf2\xfb\xa9\xeb\xc0\xc9sr\xde\x05|\x18\x01\x01f\x10\x18\xed\x89T\xb5\xc1}\x16\xb2N\xcd\x04\xa5\x8c\x94\x15\xc6\xd4z\x85\xc0\xb4\xae(\xaf\v\x99d\xca7\xb5պ\\ɵ(b\xe5tʔl\xaa\x8a\xad*\\\x803ͱK[\xf2\xb8\xf7\v\x85g4\xfe\xa17\xb4U\xf7\x99RezW\x83\x02\xe6t3(\x9bz\n%\x83\x8d҂%\xda\xe9pN\x1c\x13u\xdc\xde^bR\xe1\xe4\x9f\xe1 ܙS\x05\x87\xe88\xe25$^\xb1\x83\xfbHG\xf3K\x1d\x8a\xc1_\x1b\xbaS\r\x11f\xe9\xda\xcah\x8cV\xfcfLZ\xdf\x17\x8f:\x0f\x9et\xdc1<\xf4\xa3\xde@\xc1\xb3\xcaR\xbeP~Ia*<\x87E\xdeC\x1a\xe0\xdaG2\xba`\\]\x9a\xa2;\xacvȻ7\x84k\xea6\xaf\xc7B|A\xd9\xf0xHD\x8d]\x91#ډ9\x94]B\x88\xd1\x065KZ\x04\xf8G\x01\xeb\v\xa3_\xdb MXh,r\xef['\x8b\x1f\x8d\t\x9c9\xcch\xfeu\x0e$]\xec\n\xcfs\xfdW\x98\xab*_S1S\x0eYG\x9b\x7f\x1fj\x1f\x06S\x94\x1d\xe4u\x84\x05q\xc8\xfd\x95\x1b\x94\x845\x13\x15r\xe8\x1e\x9f/f>\x01z\x9a\x8c}N\xf2k\xb4\x96\x15\xe7\x9c֏aT\xa8\xbc\xc5)\xc0V\xaaqG\xac\xf2k\x1b\x8f\xd6E1Y*~\x0eɃ\xe2\x1e\x86<\xfe\xe46E\x93PK\xff\x19\xee\"\x8c\xbe\xa8y\xaeHIcR\xae\xa6\x83|\xda\xd7\xc0\x89\x18\xf6\x80\xbbDk{\x82\x13]\x8f\xd1-$\xba&\xbf\a\xe8w\x86Jr*\xa7i\xfb\x922\xbb\xc7\xf6D\xdf\xf7\xfe\xb8\\\xc4v\xc4w\x8dC\xe8\xeaХ\xaaZ\x1f\xe0\x1f\xc9eS\xafА*V\xa9\x8c\x18\x98\xe4}ͥ\x8a\t\x9d\x846\f\aQ\xb1\x1e\x16\v\xe8\xfe\x94;\x05\\X]\xb1}\xb7\x19\x7f\x83\xa3#\x9d~N8\x9c\xab\xd6WQ\xe49\x92\xb7\x9d\xaeTw?ZH\xdfOOg\xfap&\xdb\xf7\xfdݏ\x11>\xcf\n'\xf2\xce\xe1\x8fC\xae1\x90\xe5@¹`\x11\x7f\xacr\xb9\x8f\x1f.\xf3g\xba\xf7${\x93F\x8f\x9c\xf7d\xc7'\xaf~K\xb3\xeaރ\x17\xf0\xfb\x1f7\xff\x0f\x00\x00\xff\xff;\xa8N\xc3\x13&\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ks\xdc8sw\xff\n\x94sؤJ#ǕG\xa5ts\xb4v\xac|\xdf\xca*\xc9\xf1\x9e1d\xcf\x10\x9f@\x80\v\x80\x1a\xcf&\xf9\xef)4\x1e|\fHbF\x1a\xednjqQ\x89$\x1a@\xbf\xbb\xd1\xc0\xacV\xab7\xb4a\xdf@i&\xc5\x15\xa1\r\x83\xef\x06\x84\xfdO_>\xfe\x9b\xbed\xf2\xdd\xd3\xfb7\x8fL\x94W\xe4\xba\xd5F\xd6\xf7\xa0e\xab\n\xf8\x116L0äxS\x83\xa1%5\xf4\xea\r!T\bi\xa8}\xac\xed\xbf\x84\x14R\x18%9\a\xb5ڂ\xb8|lװn\x19/A!\xf00\xf4\xd3?^\xbe\xff\xd7\xcb\x7fyC\x88\xa05\\\x11\x05\xdaH\x05\xfa\xf2\t8(y\xc9\xe4\x1b\xdd@aan\x95l\x9b+ҽp}\xfcxn\xae\xf7\xae;>\xe1L\x9b\xbf\xf4\x9f\xfe\x95i\x83o\x1a\xde*ʻ\xc1\xf0\xa1fb\xdbr\xaa\xe2\xe37\x84\xe8B6pEn\xed0\r-\xa0|C\x88\x9f:\x0e\xbb\xf2\xb3~z\xef@\x14\x15\xd4\xd4͇\x10ـ\xf8pw\xf3\xed\x9f\x1e\x06\x8f\t)A\x17\x8a5\x06\x11\xf0?\xab\xf8\x9c\x84\x89\x12\xa6\t%\xdfp\xa1v6\x88xb*j\x88\x82F\x81\x06a41\x15\x10\xda4\x9c\x15\x88w\"7=H\xa1\x97&\x1b%\xeb\x0eښ\x16\x8fmC\x8c$\x94\x18\xaa\xb6`\xc8_\xda5(\x01\x064)x\xab\r\xa8\xcb\b\xa8Q\xb2\x01eX\xc0\xb2k=\xde\xe9=\x9d[\x98m\x16\x17\xae\x17)-\x13\x81[\x82\xc7'\x94\x1e}Dn\x88\xa9\x98\xee\x96\x1a\x96G\xa8 r\xfd7(\xcc\xe5\b\xf4\x03(\v\x86\xe8J\xb6\xbc\xb4\xbc\xf7\x04\xca\"\xab\x90[\xc1~\x8d\xb0\xb5]\xb8\x1d\x94S\x03\xda\x10&\f(A9y\xa2\xbc\x85\vBE9\x82\\\xd3=Q`\xc7$\xad\xe8\xc1\xc3\x0ez<\x8f\x9f\x90xb#\xafHeL\xa3\xaf\u07bd\xdb2\x13$\xaa\x90u\xdd\nf\xf6\xefP8غ5R\xe9w%<\x01\x7f\xa7\xd9vEUQ1\x03\x85i\x15\xbc\xa3\r[\xe1B\x04J\xd5e]\xfe]$\xea`X\xb3\xb7<\xaa\x8dbb\xdb{\x81\x02q\x04y\xac\xa88\xc6s\xa0\xdc\x12;*\xd8G\x16u\xf7\x1f\x1f\xbe\xf6\x99\x92iO\x94\x1eoN\xd1\xc7b\x93\x89\r(\xd7\x0fY\xd3\xc2\x04Q6\x92\t\x83\xff\x14\x9c\x810D\xb7\xeb\x9a\x19\xcb\x06\xbf\xb4\xa0-\xbf\xcb1\xd8k\xd4:d\r\xa4mJj\xa0\x1c\x7fp#\xc85\xad\x81_S\r\xafL+K\x15\xbd\xb2DȢV_\x97\x8e?v\xe8\xed\xbd\b\x1aq\x82\xb4^\x8b<4P\f$\xcdvc\x9b\xa0.6R\r\x94\x8c\xed2\xc4QZ\xf8msZĪ\xc5\xf1\x9b%.\xb3\xed\xdfco\xcbovf\xad`\xbf\xb4\x80\xcaԉ?\x1c\xea+\xd5S\xed\xc3f\xd9hL\xddID\xdb\x06\xdf\vޖPF\xbd~\xb0\xc0\x9ce|<\x80\x82F\x8f2a\x85\xc8Z\x1f\xbb\x16ѽE\x05N\x15\x10!M\x02\x1e\x13\x0e\x1ea\x021\x90\xa4\t~h\xa0N\xccxvɄ\x88\x96s\xba\xe6pE\x8cj\x0f\xd1\xe8\xfaR\xa5\xe8~\x02[\xc1\x03x\x16\xb2\"\x10\xafj8+\x90\xe4Q\xa1 \xbe\xfe\xb8\xa8b\xda*ʰ\xca;\xc9Y\xb1_\xc0\xd7\xc7d\xa7 \xad^v\xfd\n\xc9\x1a*\xfaĤJ\x89\x81T\xf8iϞwjZZ-遌m\\悓Ȫ\xa4|\\b\x88\xcf\xf6\x9b\xce:\x90\x02\x1dʸ\x14Omo\xbb\xd7@\xe0;\x14\xadIL\x93\x90\xb2E\xd3$\x15i\xa46\xd3t\x9fV]\xa4\xef\x1c\xa5^\xce0\xcd\xc1ʒ\xac\xee\x9aW\u0081\xa8\x16\a\x03\x85,\x05\xd8eԖ\xa8ݷJ\xb6\xee\xdbI\xa4\x905\xd5P\x12)&GFvi9h?V\x89\x9c\xd1顋n\xfd\xe8\xf1\x10N\xd7\xc0\x89\x06\x0e\x85\x91\xea\x10\x999(u-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\xd7\x16\xc4j\f/\xa5Q\xba\x96\xa1\x86\xbb\x96Dm\xa7{\x0ft\x8b\x7fn\xe4\xec\xb2\xff\x7f\"6\x18\x93\x13\x98vF\xfe\t\xba\x9f\xd9<=ɷ\x18ၾ$7\x1b\x02uc\xf6\x17\x84\x99\xf0tI\x12(\xe7\xbd1\xfe\xc0\xb49\x9e\xe93I\x93#\x13g\"L\x1c\xe2\x0fH\x174\x19\x0f\xdebd\xd3\xe4\xaf\xfd^\x17\x84m\"\xd2\xcb\v\xb2a܀\x1aa\xff$U\x1f(\xf3\x12\xc8ȱz\x04\xf3\x04\xa6\xa8>~\xb7.\x8e\xee\x92`\x99x\x19wv\xbeq\x88 \x86\xe6y\x01.\xc1x\x99)\xa81\x0e'_\x11\x9b\xdd\x13t\xaa?\xdc\xfex\x18+\x8f[\x06\xe7\x1d,dA\xe8\\\xfb0ZQ\x7f~>*\bo\xd0\a\x8aA\x95˹\\\x10J\x1ea\xef\\\x17*\x88\xa5\x0f\r\x1fg\f\xaf\x00\x93?\xc8g\x8f\xb0G0\xe9l\xcea\xcb\xe5\x06\xd7\x1e!\xe1\xfa\xa7\xda\x00\x87vN>,vx\xb2\x0f\x10\x11\x18\xc3粁k^\x14\x12\xb9\x93t\xcb\xd4%\xa1\x05ܟ\xb0\xcc,V\xe9\x8f\xd1O}\"\a\xfc\xa0\x1d-\xad\xc4T\xcc\xe745\xa0\xcc\xe4\x12Եo\x94\xb32\x0e\xe4d\xe4F\\\x90[i\xec\x1f\f\xd042ʏ\x12\xf4\xad4\xf8\xe4,\x18u\x13?'>\xdd\b(h\xc2iy\x8b\xb0~\xce\xcf\xd94\xcbm\x11\xf7L\x93\x1ba\xe3\x15\x87\x92̡0\xbd\xeb\x86s\x03խ\xc6t\x9d\x90b\x85639\x92ǷT\x03t?{P?\xe0Wk,\xdc\x1b\x97d洀2D\x96\x98\xfd\xa4\x06\xb6\xac\xc8\x1c\xaf\x06\xb5\x05\xd2X\x15\x9e\xc7\x11\x99\x8aկ\xe68\xf6ɳ\xde\xfd\xf6}\xf5\x18\xf3\x05+krV\x1e\x82\x91u\x06\x0e\xbc\xee.\x97׳\xb22\x9b\xf1U\xe0\x84\xc5O'\x92\xa3ӟ\xe6 \xe5\x19\xe8@+\x8e.\xce\"uiY\xe2\x16\x1a\xe5wGX\x94#x\xe1X\xd5Л\xbb3\xc15m\xacZ\xf8okiQ\x9a\xfe\x974\x94)}I>\xe0N\x19\x87\xc1;\x9f\x87\xeb\x81\xc9\x18\xb2\xb1CY\xfey\xa2\xdc\xda~\xab\xc0\x05\x01\xee<\x01\xb99\xf0\x8b.Ȯ\x92ڙ\xed\r\x03\x8e\xfb\x15o\x1fa\xff\xf6\xc2\x0e\xbf8d_ɼ\xbd\x11o\x9d\x0fq\xa00\xa2\xc3!\x05ߓ\xb7\xf8\xee\xeds\\\xa9LN\xcd\xfcl\xc0\xa25m\xf28T$\x93\xf5]\x1bpL?7\xdf%当=\xb7\xda,\x16m\xa46\x9f\xd3yÉ\xf9܅\x1eC\xcf8\x91c[\x8c\x18|\x1e-\xea{\xebDn\f(\x9fKt6 \xc4\x1fό\xccR\xbb2\xfd\xc9\xc6d \x8d\xf9]\x8b\xe0\x05nr\x1b79S<\xc6a\xb5x9\xd2\xdb\xff\xf8\xbd\x97ϴ\x92k\xff\xef/\xe4\xa5\x1d\xeaB\xd65\x1d\xefjfM\xf5\xda\xf5\f<\xed\x019\xea\xabm\x8b\xf2\x9ck\x91;\x1e\xc2\xfd\xcb\x1d3\x15\x13\x84\x06\xb5\x01\xca3\x14%\x8dL\xe5\xb0S\xad\xa2\x9a\xac\x01DL\xd1\xff\x1e\\\x89\x9a\x89\x1b\x1c\x80\xbc?\x83\xeb\x11\xd1uNg\xf7:\xd2$R>>p&\xab\x91%\xd9U\xa0`\xc0\x18\x87yw\xf4T\x854\xbd\x94\xc5\x11\x0ei#\xcb\x1f4\xd90\xa5M\x7f\n\x9a\xb4:\x97\xd6G\x92\xcf\xce\xfb+\xabA\xb6\xe6\x9c\b\xfe\xd8\r3\xd8k\xae\xe9wV\xb75\xa1\xb5l\x9d17\xac\x8e\xbb\xba\x1e\xbd;\xcaLܶ\xc2\xfc\x8d\x91\x96\x04\r\a\x03d\r\x9b\xf4~o\xaa\x15RhV\x82\nU\n\x8elLZ\xc1\xdcP\xc6\xdb\xd4.Q\xaa\x1d\x1b\x01\x8b\x8fJ\x9d\x14\x00\x7fq={y\xc7J\xee\x86\b\xca\\;n\xa4\x01a\x1b\xc2\f\x01QX\x8c\x83r*\x19\x87\xf0\xc8@\u0530\\=\x97\xa7\xc0m\x03\xd1\xd6y\bX\xa1@21\x9br\xeb\x7f\xfe\x892~\x0e\xb2Y\xce\xfb$\xd5=\xd0\xf2\x94\x1c\xcdϽ\xee\x04\x84n\x15n\xfe;ݱc,q\x85o\x13\xc5\r\xc9\xd5\x1d\xef\tfQ6\xb4AЉyX\xf5\x04\xabV<\n\xb9\x13+\f\xc6\xf5\xd1:\xe4\xc4,\xd5s\x877'+\xa3e\xfd\x92\xaf\xa6\x97\xb4А_\xf3y*\xf8Og\xd02\xd9|sT\xc2c\x8e\v\x96\xf4\x9a+\xc0\x9ex\xb98\x8b\xb9\xf1g:\xfbM\xe9kW,\xfd\xac\xb2\xb8\x9b4\xa8\x9eS\xb8\xab\xc0T\xa0Bi\xf6\nK\xd2\xcb\xd9\x1d\xd2.x\x89ur\x96\xa9\x82\x8b\xec\xca?G\x95s\x18ݴ\x9c_Xަ-O\x86\xc3F\xa2\x88\x1drVV\xfdX\xdacȩ\xbe\xc8\xc6c\xbf\xd2bX_\x18\xab B\x81\xa1\f#{\x1a\xa7\u058b\x85\xa5\xbd\xfd\xfda9\x05\xe6\xff\xc2\xf4\x7f\xf3\xd2ÌJ\x89|4\xe6ViF$&`%\x18\xac\x87Ʈ\xbe\xc2\x7f\xe7\v}\x7f_85P\x7fi\xbc\xc4L\xba\xb0\x19hM\xc0\x19՛\xa05h\xb5s\x05\xa2\x1d\xf09C\xdb\xffC\xe1NA\x040)~\xfdZA\x10__\xbd\xcf4\xf9gR\xc96Q\xd57\x83\xb2\x85\xea\x8e\xe5\x05\x0f\n=\xfc\x86\x02\x18\xfa\xf4\xfer\xf8\xc6H_\xf6\x81Y\xb4\x04 \f\x8a\xba\xcc,\x13%{beKy\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\x8b\x17\xa9\x82\xfdMj=\x8e\xaf\xf0ȉ$\x16\xaa9N\xa8\xe1\xc8,\x16{\xf6~KN\x95\xc611\xf7\xd9*2^\xbe\x0e#\v?\xcb5\x17\xc7`\xe7\xec\xf5\x15\xafXU\xf1:\xb5\x14\x99\x15\x14/W\n\x99\x17}\x9eT\n\xb0\x1c\xb0LWA,\xd6><+\xa09iI\x8b5\r\xc7T2,R'O\xcc^\xadV\xe1\xd5*\x14^\xb7.a\x96\x8bf_\x1eSy\x10㤟h\xd30\xb1=d\x8a\\֙e\x9be\x96\xb9\x1dMd\xc03\xfdp\xa6\x8b\x0e'B_w\\:\x11I\x86\xb4%\x13F^\x92\x0fb\xef\xe1&\xe0\xf4\xc2G!\xcd\xc1A6;\xad\x1d\xe3\xbc\x7fZ\v\xc1\u0383\xf2g&5\xadݬ\xa6\xbc\xfd$]\xa5\x1a8\xe5'\x05\x8e_F0\xfa\xd9\xd1\xd7\xf4\xfc\xeb\x96\x1b\xd6p\xb0\x1e\xdd\x13+\x93g\xc8L\x05\xfb\x88\xe4\xbfI\xfb\xa5\x05\xb5'\xf2\tK\x18\xbc\xf7֝U\xf0\xeaF\xdb\x183(@\xaf\x8c\xa76\x15\x0eB\x99NA\x91\x0f\xc2\xf9\x12\xe3\xf9`\x1f\xab\xf9\xbaPͪs\x1b\x85%ǘ\xe8.d\xec\x9d\xe8\xb6\xe4\xf6\xe7\x16\xf5\x9f7p;>t[\xf4\x95\xf2\xfd\xd9ߨX\xff\x94\"\xfd\xbc\xed\xa0Ţ\xfcs\x05rK\xa1\\\xb6\xf7\x9aWt\x7f\xdc&\xea\x19\x8b\xec\xcfQ\\\x9f\x89\xa9\x9cb\xfa\xe3\xf0\xf4\n\xc5\xf3\xafZ4\xffZ\xc5\xf2\xd9E\xf2Y\xfb\x98ٛV\xb9ی'V}/\xef\xba\xcf\x17\xbdg\x14\xbbg\xec\xa4-/\xf2\x84\xe5e\x14\xb3\x1fWĞA\xb3\\Q|\xc5b\xf5W,R\x7f\xed\xe2\xf4\x05\xceZx}\\\x11\xfa\xc9;0a\xab\xffV\x96p'\x95Y\nN\xee\xc6\xdf'vR{\x01\x9b\xe4%\x11\xe1\xd3\xc4*1\xc4\xf0\xe1\xc5i\x8bJoz\x06w\xfa'Yڹ-\xed\xb1\u070f>?8\xab\xbc\x01\x05\xc2]\xf3\xf1\x9f\x0f_n#\xfc\x94\xcf\xeb=\xe3\xd1\xf5\x12\u0383)=r\xfc֜/fr\xd8B\x1f\xe0\x85\xf7Eh\xc3\xfe\x03ou{F:\xe8\xc3\xdd\r\xc2\b~\x1a^\x13\x17\xab(\xe2\x8e\xe5\x1a\xacŊ\xa8\x9a\x14\x8b\x9b\xcd\x00\xe2\xb0\xe2\xb7\x7f\x8d\x12\x94\xeeʬ`1Y\xa8\xf1\xb2\x82ww\xe3\xe615\xca'\xeb4\x8a=\x91\x8e#+\xa6\xcaUC\x95\xd9#\xdb\xe8\x8b\xc1\x1c\x82\x99\x99K\xe7L*\xd6\xc3k\xc0\x92\xe8\r\xb7\x7f\xe1^\xe4\xbe\x19\xee\xf6\x8eqw\xca<\xa6ϟ,\x9eg\x91\x0e\xc0`\x9d\xac\xa8z\x1e\xe4\x0e\x82\x8f\x19\x96\x8dҊݒ\x1a\x1c\xb8?i\xc5\xf8E/{\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\xa1\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(\xc3\xe7L\xc7\x19\x1fU\xfb\xd86\\\xd2\x12\x94s\xc9\x16\xd0\xfa_\x83\x8fG<[\xe0\xc3Vu\xd7\xed\xce^U\xfa,\xcd\xd5PE9\a\xfe\x89q\xd0?ʝ\xb0\xf3\xca\x10ȻT\xbf\xdeY٢U֬\xef\x89h\xeb5(\xa2\xc1\x98\xe9\x04\xdeF\xaa\xf9S+\x0e\xefL\x18\xd8B*\xe7\xb9S\xcc\xc0CC\x95\x06\x9cQ\xc6\n~\x1euq\x19\xc1\r\xa7[W\x9e\\\xb2\x82\x1a\x88\x06\x18G\x98\x9a>\xf6\xd7\b\x8b\xef\xb1ZTNlDd\v\xf5\xd41\xb9I\xb1\x9e\xba\xf29a\xaa\x93\x97>;\x8b\\\xd0\xc6\xe0\xa1D\xa4#\x12\xd1x\x18x\x91\xfa\xe8\xde\xe7\x01\xd8iN\xf3GK|\x11\xb36\xb4ND\t\xcbz\xe7\xfa\x10\f^ծ\xca^-t\xff\xd2\xdbX\xf4LvT\xc7\x03.I\u07fb\x83\xed\xc0\xa0\xabnACI\xe0\t\x04\xb1\xa2H\x19\x87r\x8eS\xbf\xe2\xe6\x9ez\x02\xf5\x83\x8ep\xb0:۲\xf8\x83\xa1\xcaĩ\x1f\xfa1.\x86\xbb\"%5\xb0\xb2\xbdOs\xdd\xd2WW+ub\x89\x06\x9e6\xf6\xe2Q\x84\xa3\x90\xd6\xfa\xb93\xc25hM\xb7!1\xb8\x03\x05d\v\xc2\xe2=\xee\xf7$=\xa6p\xcc\xda\x1b\x8bAb\x80\x16\xa6\xa5~\x00\xe7\xc2Ŋ\x96pg\x0f \xc5\xf0V\x1aʃ\x91\xb1|\x19?\xa8f.uy\b\x17\xdas\xbe\xbf\x18C\x1e\xfdRF\a\xbb\xea\xaeW\xf6\x9a\xa0\xbb\xd2cb\xa0\xb0\x13\x93\x04\x12of\xee|\x92\xa9{p\x97\xec\x1fB\xfd\x84\x93\xca\xc0\xf1\xe7\xee\xeb)<\xbai:\x87\x19D:\xd2$\x18|\x98*J\xc6\tS\x9f\xf1R\x9b\x8a\xea%\xf7\xf4\xce~\x13ݎ\x9e\xb9\x8aN\xe8\xfd\x84T\xa6\xef\x1eX\x91[\xd8%\x9e:daE\x02JU\xe2\x93\x1bq\xa7\xe4V\x81>d\xba\x15\x9e1gb\xfbI\xaa;\xden\x99\xf82}\x1ag\xee\xe3;\xaa\f\xb3L\xeb\xe6\x93\xe8{\x1dl\\\xe2\xddr\xef\xe9\x17LP\xce~M\xe9\xf2\xfe˥\x11f\xf4]\xe3\x91w\x8a\x85\n\x88_R\x80^C\xff\xa0{\xe6'\x8c{IneR\x8c}\xd1\x0e\x1b\x02e\x9a\xacA\x9b\x15l6R\x19\xb7\xa7\xbaZ\x11\xb6\t\x0e\x92\xd5\x10\x18'\xba_\x18!,\xb5\x19\x1a\xcb!\x82ò\xf1\xa9D\x85V\aCΚ\xee]F\x92\x16\x85\x8d\t\xe0\x9d64\x15\x9b\x10\x9cC\x1d^1\xe2\f:\x9f\xaa3\x18\xdc`D\xb4\xc5\xde)ʄ85v3\x1dv癚\xaf\x11ʔz\xf4\xeb\x1b\xfc8\x82/z\xf1\x1fY\xb2\x15\x15\x15\xdb\xc9Cƕ\x92\xed\xb6\n\xbc9\xe5\x10\x91\xb2\xc5ȹAU\xa0Ï9\x99V\x89^!\x85\xaf{\x9b\xd2\xd2q\xba\xd3>\xca3\x14\xb5\xea\x0e\x1bv\xaaj\xc6\xe6gg\t' .\xda\xfe\x04D\xaa\xf7\xa2\x98=\x16y\xb8Gu\x94k\x99DB\xd4\xc6/\x86\x84\bq\n\t}_\xa2\x8bx~7\x18\x99\xf2QNDǼ\x13\x83K\x9c\a\xb5\xbc\xe8\xbe\x134tw\x8eC\x87\x1e\x04\x7f'\xa5\xdd\x06\x10\x8e\x89|q\xect\xdc\xfb\xfb\x8dX\x9f\xa2\xb7\xf5\xf1\xe4\xd8\xf5\xdb\b\xc6\xe8X\xba\x8db\xbbaB\xbc\xf9\xf7l\x93\x92\x17\xf7\x8byk\x0e\xffp\xf0\xf6\x95\x8f\x97\xef\xa8\x12LlO\xc2\xc8Ͼo\"\x9e\xf7`\xcf\x19ч\x99\xbfXL\x9f4K\a\x0f\x91\xc1\xcb\x1e\x9e\xfdH\xfe\xc9\xff\x05\x00\x00\xff\xff\xbc\x9a$\xa6\xd7r\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfa\x15\x84\xeea?B\xdd^\xc7}ą\xde|\xb2gO\xb1\x1e[ai\xf4\xbctU\xb6\x9aQ\x15\xd4\x00\xd5r\xdf\xde\xfe\xf7\x8dL\xa0\xbe\xba\xe8\xa2Z-ygǼت\x86$\xc9L\xf2\x03\x12X,\x16g\xbc\x12\xf7\xa0\x8dP\xf2\x92\xf1J\xc0W\v\x12\xff2\xcb\xc7\xff6K\xa1\xdelߞ=\n\x99_\xb2\xab\xdaXU~\x01\xa3j\x9d\xc1{X\v)\xacP\xf2\xac\x04\xcbsn\xf9\xe5\x19c\\Je9~6\xf8'c\x99\x92V\xab\xa2\x00\xbdx\x00\xb9|\xacW\xb0\xaaE\x91\x83&\xe0\xa1\xebퟖo\xffk\xf9\x9fg\x8cI^\xc2%3\xd9\x06\xf2\xba\x00\xb3\xdcB\x01Z-\x85:3\x15d\b\xf4A\xab\xba\xbad\xed\x0f\xae\x91\xef\xd0!{\xeb\xdbӧB\x18\xfb\x97\xde\xe7\x8f\xc2X\xfa\xa9*j͋N\x7f\xf4\xd5\b\xf9P\x17\\\xb7\xdf\xcf\x183\x99\xaa\xe0\x92}®*\x9eA~Ƙǟ\xba^0\x9e\xe7D\x11^\xdch!-\xe8+U\xd4e\xa0Ă\xe5`2-*K#\xbe\xb5\xdcֆ\xa95\xb3\x1b\xe8\xf6\x83\xe5g\xa3\xe4\r\xb7\x9bK\xb64ToYm\xb8\t\xbf:\x129\x00\xfe\x93\xdd!n\xc6j!\x1f\xc6z{Ǯ\xb4\x92\f\xbeV\x1a\f\xa2\xccrb\xa0|`O\x1b\x90\xcc*\xa6kI\xa8\xfc\x0f\xcf\x1e\xebj\x04\x91\n\xb2\xe5\x00O\x8fI\xff\xe3\x14.w\x1b`\x057\x96YQ\x02\xe3\xbeC\xf6\xc4\r\xe1\xb0V\x9aٍ0\xd34A =l\x1d:\x1f\x87\x9f\x1dB9\xb7\xe0\xd1\xe9\x80\n»\xcc4\x90\xdcމ\x12\x8c\xe5e\x1f\xe6\xbb\aH\x00F$\xaaxmH8\xda\xd67\xddO\x0e\xc0J\xa9\x02\xb8\x80vX4\xb6\nu%\xa0\x80\xe6\f\xddN\x8d\x16FH\xb6\xae\xd1#]2\xd4\x12Q\x19\x11\xd2X\xe0\x11a>\x01\xef\xe0kV\xd49\xe4WEm,\xe8\xdbLU\x90\x87E\xa6Q͜\xca\xc3\x0f\a!\xfb\xf8\xa5\x10\x19 \x1f2WiA\x8b<1\xd1nC\x99]\x05n\xcd\tY\xed\x87\xd0\xc6(\x93\xbaŀņ\xe7\x7f<\xbf \t\xe8\xf7\xde\xef\xc70\xae\xa1!\xd3,\xddL\x16\x7f\xbc\x85\xb0PF\xa8;\xa9\xa3f\xf0\x9dk\xcdw\a\xb8\xde,\xa6\xbd\x00\xdfc\xb0\a\x9c\x97\xa1\xda7\xe2\xfd\xb0\xff\xdf\"\xf7O\xcboC\x8b\xce\\H\xe4s!\x8c\xed\xb1ٸU,$\xebX\b\xe9\t$\x1dLT\x93S\\\xfd'!\xe6I\xe7Nl\xb24\xb2\xe9'\xc0\xbf\x14%7J=\xa6P\xef\x7f\xb1^\xbb\x84\xc52\xda\x18a+\xd8\xf0\xadP\xda\f\x97I\xe1+d\xb5\x8dj\x16nY.\xd6k\xd0\b\x8b\x96\xf9\x9b]\x81C\xc4:\x1c\xbe\xb0\x8eʊV\x18\x8c\xabe:\xb2\x94\xa8\x11\x1b\n\x05\xa8Q\xa8\xce\xc1\xc1Ђ\x1c\x88\\lE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7\xea\xa5$\xa0\x8f_bl\xb4_5N\x89\xb0\x94p\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccrk\f\x05_A\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fm\xb5\x83\x99\x00\xcb(\xc4݈l\xe3\xdcW\x144\x82\xc5r\x05\x86VExU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8:\xff(\xbe\xbf%\xa2\a\xabs\xa4\xb0Oh\x12F\xfb\x05\xc9\xf3!Jz\xa4\xb8\x00\xb3\xec\xac\xce\t\x1b\xbe\xa60\xb4\xe7?\xeem\xa5\xec\x11\xe5\xd7Ż\xe3&\xcc\f\xd6MΩ\x97e\\\xd3Ϳ\b\xdf\xc8d\xddz\x8b5\x8bg\x1f\xbb-/hW\xc03$\xbf`kQX \xa7j\nQ6\x83s\xa7$P\xaa\x05f\xb4Il\xb3͇f\xef(\xa1ŀVC\x00\xceA\x0fQ\x0e\xf1 \x01$k\\\v\xda4\x15\x1aJڌ\xa5H\xb2\xfb\x85\\\xc1w\x9f\xde\xc7c\xcfnI\x94ԽA%LZW\xde\r\x1c\xa3.\xae>T\t\xbf\x90\xbf\xd6\x04\x82n\x13\xfe\x82q\xf6\b;\xe7bqɐo)\x8b\xff|\xf8*\fv,s\xf6^\x81\xf9\xa4,}yQ*\xbbA\xbc\x06\x8d]O4A\xa5\xb3$H\xc4n\U00088ce5(\xa8\r?\x84a\xd7\x12C2G\xa2\x19\xddQ\xae\x90\xeb\xd2uVֆ\xb6Z\xa5\x92\v\xb7,6֛\xe7\x81\xd2=\x16\x9c\xa4c\xdf\xe9\x1d\x1a#\xf7\x8b\xcbZ*x\x06yآ\xa3t\x1an\xe1Ad3\xfa,A?\x00\xab\xd0,\xa4K\xcb\fE\xedG6_\xbc\xd2=\x87n\xf9\xbax\xacW\xa0%X0\v4k\v\x0fŪ2\x91.\xde&\x8c䜌\x95\x05\xce\xf5ĚAZ\x92\xaaG2r\x0eWO%\xd63\xc9D^\x04\xb9]IR\xd0Ml\x9dg\xbdf\xca\xcd1*\xa63\x16\xe7\x02\x94\x9c\xb6\xd6\xfe\x86\x96\x9ef\xe3\xdfYŅ6K\xf6\x8e2{\v\xe8\xfd\xe6\x17&;`\x12\xbb\xadh\x95\xfd\x97Zly\x81\xfe\a\x1a\bɠpވZ\xef\xf9j\x17\xeci\xa3\x8cs\x1b\x9aM\xbb\xf3Gع\x1d\xe5\xa4n\xbb\n\xeb\xfcZ\x9e;_fO\xf14\x8e\x8f\x92Ŏ\x9d\xd3o\xe7\xcfu\xeffH\xf4\x8c\xaa=Q.y\x95.ɔ7;'\xd0\xc0`=8DظI \xc5\x00a\x8a\x02ɢ\\)\x13I\x16\x89\xa0\x95 \xe87\xcaX\xb7\x0e\xd9\xf3\xf7G\x17*UX\x9cd|mA3c\x95\x0e)\x99\xa8\xf8S\x96\xe2\xbb\xe5n\x03\x06\xfc>\x94_\xf4t\x801\x8a=ou\x83\xb3*\xe7n/\x8c:\xe2\x19yOԶ\xd2*\x03\x13͋hK\xa2m\xeaQp\x9f\x0eͺ.w\xd1\xdf:Ik\xa7,J\x872ϑG\xd2\x1d\x11\x19}\xf8\xdaY\xa2F\xed\x82\x7f\xa7H\xeb182:\xafQ\x96|\x98\x0e\x9c\x8c\xee\x95k\x1d\xe6\x98\a\xe6\xc2-\xfdP\x93Ι\xe3u4\xa2\xfc\xcf\xe6ڔB^SG\xec\xed\v\xbaC^\x8b\xc7ң\xc6\xca\xf1N\xfaU\xe8\xac\xe5^\xf3\xc1\xe7\xd4)\xda\xf8\xd1\xd0c\xee\xfe\x9e\by\xd7R\xd9\xce2\xceL'\xbaR\xf9\xef\f[\vml\x17\rs \xb1j\x14\xd4\x11\xa1\xa7\xfc\xa0\xf5ё\xe7g\u05fa\xb3\xa0\xb8QO>qzN\xbc\x1dH\xba\xe1[\xf0\x99\xab 3UKZ\nC=\x80\xdd̀\xe8X\xe3\xac@\xa2\xbd\xeb4\x96u\x99N\x90\x05I\x92\x90\x93\xebf\xdd&?p\x91\xb6nŎc\xab=\x94\xc39V\x8e\x9fG!\xc1\xb3\x9bN_\U000af8acK\xc6K\xe4!\xb9\x1d\xa2\x84&\xa3ޱ\xbbI\xfb\xc4\x16d\xb4\xac\xc2YV\x15`\xc1\xa7m\xce\xc0#S҈\x1c\x1a\xd3\xefE@I\xc6ٚ\x8b\xa2\xd63\xb4\xeal\x92\xcf\r¼69}d\x95\x8eȂH\x94\xb8\xce>\xc3\v\x9e\xd6\xf8\x95\x9e\xe7Ǧ8\x8c\x1a\xe6\xfb\x8b\x95\x16\xca\x1d\x068\xbd\xcb\xe8ӎ\xb9\xdc}\xf7\x19\xbf\xfb\x8c\xdf}\xc69\x1d}\xf7\x19'\xcaw\x9f\xf1\xbb\xcfx\xb8|\xf7\x19S\xcaw\x9fq&\"\xdf\xcagL\xc1pAk\x9c\a*$a\x95\x98\n1\x85\xf6D_>\xe9ǟ\xd58I.\xf3\xf58ȑC<\x91\xe3\x171\xaf\xa35^Mr3\xce\xc00w\xdc)\xca\x04\x87\xf9\x04\xa7g\x02\x02\xa7?=s}\x10\xf2\tO\xcf\xf8!\xa4E\x18G\x9d\x9d\tD\x9a\x7fz\xe2\xc2'\x11\x95\xc0\xc3V\x8aK\xff\x88\x8d1&I\tx|\xe3\xe4\xf7\xbd\x8c\xc9\x17\x90\xa5W9\x913K\x9eFY\x7f\xfe\xc7\xf3_\a\x8bN˔(\x1b\xf6i\xeb\xd4xL?b,\xdfM\x8d\xecg\xa9\xfez\xa6\xc2Ie?\xf5DMC\xe4\b\xbc\xbeX\x0f\xa8\xfck\xd27\x16\xcaϕ\xb7\x96'8a\x7f=\x02/\xe9\x8c=7;\x99m\xb4\x92\xaa6~M\ba\xbd\xcbܽ\x03\x01dL\xd8G5\xc8\x7f\xb0\x8d\xaa#\xa76&H\x9b\x90E\x9bF\x90^R\xadO\x8c\x00˷o\x97\xfd_\xac\xf2)\xb6\xecI\xd8M\x04\x18\xddG\xc1\xf3\x1c\xe3\x82\u0381\x1e\xaf\a\xc2UIC\xa1\x8c\x00S\x9aIQ8\x89\r\x10z\xf2\xca>Wnu\xf0h\xbfiz\r+=\x11wn\xfam\x93-9\xed\xbe?#\xe9\xf6\xa4G\xa3\xbeYZ\xedqɴ\xa9+\x94\t\x89\xb3\xe9\xe9\xb2)lu%=I69BNM\x88\x9d\xbb\x02\xf1\xa2ɯ/\x93\xf2\x9aL\xb3\xb4\xf4ֹ\x14{\x95T\xd6WN`}\xbd\xb4\xd5\x19ɪ\xa7?\xf5\x92\xbe\x96~tveڲ\xcc\xe1\x84Ӥ4Ӥ\xa5\x9b\x94\x01\x1f5Ԥ\xf4ѹI\xa3I\x9cL\x9f\xae\xaf\x9a\x16\xfa\xaaɠ\xaf\x9f\x02:)m\x93\x15\xe6&y\x8e_r\x18ʴ\x03P|\v\xe1|.\x99\x94\xee\xb9\xe6ϊ;?\x0f`\xa1\xb0\x047\xf5\x15〲.\xac\xa8\x8a\xf6>\xb6X\xc0\xb9\x81]sY\xd1ϊ\x8e\xc8\xfb\x9b\xba>\x7fi$~9\x88j\xb8aOP\x14\x8c\xc7\xe6\xe6\x1e\x152w\x0fh\xa6\x16\x80\xb6\x11g\xb9\xbf\x8c\xc9_\x1ez\xe1\xa6\v\xdd\x06@\x16\xb6\x8c-\xf5qy\xf8\xa6\xaf\x83\x06,U\x8f\xedy\xe6.ޠo\xbfԠw\x8c\xee\x1dk|\xb3\xf6P\xa9\x9f\xe8\x06\x03Ӡ~\xbc:<\xb4g\xb2\x17\xe0\xb4ꁽ\x93\xce#\x18\xe2DmP\xef\xb4\x01\x1d*U\x8cӢ\xfdD@H\xd5@\x884Mq\xfe眲|\x89\xf0\xee\x14\x01^\x92\a4\xcf{\xfd\x86\xa7'\x8f=5\x99\x9e\x8c\x92tJ\xf2%½9\x01\xdf,\x7f5\xfd\x14\xe4\xfc\x8d\xe7\x17>\xf5\xf8R\xa7\x1dgP/\xf5t\xe3|ڽ\xd2i\xc6W?\xc5\xf8\x9a\xa7\x17g\x9dZLNϚ\x95q0'\xb5\xea\x19\xc7\xed\xd2r\t\xa6O!&\x9e>L\xcc4H\x1b\xfc\x91\xc3N<]8\xffTa\"\x7f\xe7L\xe9W>=\xf8ʧ\x06\xbf\xc5i\xc1\x04\tL\xa82\xffT\u0cf7\xa4\x94\xceAOn\xfb͑\xdaIyM\x8d\xe5\xfa\x88\r\xf6\xb5\xc2m\xb2X\xab\x17\x03\x90Y\xf2\x17\xf9ӣ\r\x87\xb6\xc1Q2;\x1eQo_\xb2u\xd7\xfa\x0e\xb1\x7f\xcd\xc1m]\x1a\xa88\x1a\x00\n\xdc(5+\xea*|\xe0\xd9f\xd0Æ\x1b\xb6V\xba䖝7\x9b\xc5o\\\a\xf8\xf7\xf9\x92\xb1\x1fT\x93\xabӽ/͈\xb2*v\x18\x89\xb1\xf3n\x83\xe7IIT:C\xcf7\xaa\x10Y\xc4\xe7\x1c\xbdW\xcf5ػl\x88n\xfe\xcb:\xd9\"\xb1\xc0\a\x9b\x8bp\xebb\xffJfw\x9f\xfb\x91k%\xbc\x12\x7f\xa6'\x95N\xb0\xea\xf6\xee\xe6\x9a`\x051\xa2\xb7\x9a\x9a\x04ņ\xe5+@\x97\xa1\x1d\xfb!}r\xbd\xeeA\xed\xe7\bw\x1f\xab\x80ܽL\x12\xdc\x16\xaf\x9a3\x85Z\xeb\xe6\xda\xe1r\xa8'\x94/.wL\xf9\xa7'\x84\xce\x17\x15\xd7v璉.zx\x04\xbb>\xb5jv\xd0Z\xed\xbf\xbc\xd2-=\xb2\x87GWh'{W\xf5\x93\a\x86\xf4|\x0eN\x87OUO\x9e\xa7~\x01\x9c\x0e\xbbP\v\xa2b\xe4\xa7h\x06\xe4\xc9W,\x8d\xbf\xa1\xffG\xb5\x85\xf7ѕ\xcb\xfe\xeb+\x83&#\xa9\x89\x01*]2\x1f\xa1`\x9b\x8fHw|?O\xed\xc5s\r\x03*\xfe\x8e\xf0\xe7,N\xde\xf6A\x8d?HB7\xa8\x87Nc^\x15=\xf5\xb4c7\xf7\x14\xb76\xaa\xd4O}\x1f\xb7\x86\xe5ɐ`\x10\x81%\xe4\xc17ZNEF\xab4\x7f\x80\x8fʽ\xad\x93\"&\xfd\x16\xbd\x97\x97\xbc\xe7\x16\xf2\xb5\xfd$\x8c)z?\xb6!\xc0\xf6|\xc6\xdeE\xff\x88\xed\x91O\x19X[\xba\x91ғ&\xef\xfd\xeb$\xa8\x8f\r \v\x02\x05\x1c\xb4\x15\xfew\xa3\x9e\xe8\x02\xfc\xf8\x1asx@\xa4\xf3\x86\x19\xd0A\x11J\xe1=j\x98uU(\x9e\x83\xbe\xa2GT\x12F\xfcS\xaf\xc1\xc0\x1d\xe8?\xc5\xe2\xedfd<\xa1\xe7\x17̒A\x8f\xae(\xa0\xf8A\x14`\x1c≦\xe1f\xbfec)\xear\xe5<\xd55\xfe\xd8tr\xc02\xbb\xa1\xd2\x06C\x05\x1a\xfdD\xb7\x15Q\x9b \xf9\x87\x89\xc1\x1a>\ni\xe1\x01\xc6c\xe8\t\x9b\xe0\xdeh \a (0\x8a\xf8\xfe\x12[y\xec\x11\xe4>\xdez \x03\xcdbdL\x8e\x95w\xabn\xee\xaf\f\xabeN\x1b\x00\xf7\x7f\xbe=J~\xb7\xbd\xf7e\x82NHQ\xef\xf7\xe3-;!BG;\x91O\x1fW\xe21X\xdc\x18\x95\t\x8a*\x9e\x84\xf5\xd79\xbe\xdc\x1d\xe2\x87\x02\xc4\x03\xd2Q\x1b\xf8\xfc$A\x7f\t\x16\xc8\\\xcbػ-\xd3\xda\xef\xa7=h\xd1\xf7Z\xac¾G`\f\x000\x15\xf6\xb9\x8c{\t(l\xaf\t\xd3H\x1c\xc4>\x01\xdcyG\xc8ik\x85\xb4\xe3\x9c!n\x9bVt\xd8tDCN\x8b\xed\xfd\x00\xc6 \x93\x9d\x1e}j\xaa\xb8Ӧ\x86\xfd^\x8cy\xa3\xb4c\x96\xe1@\xff\xb0\xf7kT\x83\x1f\xd4\xde1\xcd=\xaaF\xf6>\xd2CxyGr\xbc\x97\xde\xfdR\xaf\xda\a\x15\xd8\xdf\xfe~\xf6\x8f\x00\x00\x00\xff\xff)\x00\x87w>{\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\xd5\by_\x17\\\xb7\xdf\xcf\x183\x99\xaa\xe0\x92}Ʈ*\x9eA~Ƙǟ\xba^0\x9e\xe7D\x11^\\k!-\xe8+U\xd4e\xa0Ă\xe5`2-*K#\xbe\xb1\xdcֆ\xa9\r\xb3[\xe8\xf6\x83\xe5g\xa3\xe45\xb7\xdbK\xb64ToYm\xb9\t\xbf:\x129\x00\xfe\x93\xdd!n\xc6j!\xef\xc7z{Ǯ\xb4\x92\f\xbeU\x1a\f\xa2\xccrb\xa0\xbcgO[\x90\xcc*\xa6kI\xa8\xfc\x91g\x0fu5\x82H\x05\xd9r\x80\xa7Ǥ\xffq\n\x97\xdb-\xb0\x82\x1bˬ(\x81q\xdf!{\xe2\x86p\xd8(\xcd\xecV\x98i\x9a \x90\x1e\xb6\x0e\x9dO\xc3\xcf\x0e\xa1\x9c[\xf0\xe8t@\x05\xe1]f\x1aHnoE\t\xc6\xf2\xb2\x0f\xf3\xdd=$\x00#\x12U\xbc6$\x1cm\xeb\xeb\xee'\a`\xadT\x01\\\x9e\xb5\x95\x1e\xdf:\xd9˶P\xf2K_YU \xdf]\xaf\xee\xfe\xfd\xa6\xf7\x99\xf5)\xfa\x7f\x8b\xe6;k\xb8\xc1\x84a\x9c\xdd\xd1,a\xdaO[f\xb7\xdc2\r(\x06 -֨4,\x02\xa9s\xa6t\aT\x05Z\xa8\\d\x81E\xd4\xd8lU]\xe4l\rȭeS\xbbҪ\x02mE\x98\x87\xaet\xd4K\xe7\xeb!\xf4\xb1\xe0\x88]+'\xa6`H2\xfdl\x83\xdc\x13\xc9M\x1ea\xda\xf1\x10\a\xf13\x97L\xad\x7f\x86\xcc.\a\xa0o@#\x980\x8aL\xc9G\xd0H\x91L\xddK\xf1\xbf\rl\x83S\u0092\xa4Z0\x96\xd1|\x96\xbc`\x8f\xbc\xa8\xe1\x82q\x99\x0f \x97|\xc74`\x9f\xac\x96\x1dx\xd4\xc0\f\xf1\xf8\x8b\xd2\xc0\x84ܨK\xb6\xb5\xb62\x97o\xde\xdc\v\x1b\x94n\xa6ʲ\x96\xc2\xeeސ\xfe\x14\xeb\xda*m\xde\xe4\xf0\b\xc5\x1b#\xee\x17\\g[a!\xb3\xb5\x867\xbc\x12\v\x1a\x88$Ż,\xf3\x7f\v\xfc6\xbdn\xf7f\xa6+\xa42g\xb0\au\xa9\x93.\a\xca\r\xb1\xe5\x02~B\xd2}\xfdpsە\xa0\x10PP\x10#\x15+\x94\xbc\a\xed\xb0h\f<\x1a\x18@\x01\xcd\x19\xfa\xea\x1aͲ\x90lS\xa3\x1b\xbfd\xa8%\xa22\"\xa4\xb1\xc0#\xc2|\x02\xde\xc17\xb4\b\x90_\x15\xb5\xb1\xa0o2UA\x1eV\xe6F\xcdY*\x0f?\x1c\x84샾Bd\x80|\xc8\\\xa5\x05\xad\x8c\xc5D\xbb\x8d\xff\xd0<\xd2B\x1d\xb2\xda\x0f\xa1\r\xec&u\x8b\x01\x8b\r\xcf\x7f\x7f~A\x12\xd0\xef\xbdߏa\\CC\xa6Y\xba\x99ܤ\xf1\x16\xc2B\x19\xa1\ue90e\x9a\xc1w\xae5\xdf\x1d\xe0z\xb3\x02\xf9\x02|\x8f\xc1\x1ep^\x86j߉\xf7\xc3\xfe\xff\x15\xb9\x7fZ~\x1bZ\xa9\xe7B\"\x9f\val\x8f\xcd\xc6-\xfd!Y\xc7\xe2nO \xe9`\xa2\x9a\x9c\xe2\xea?\b1O:wb\x93\xa5\x91M?\x01\xfe\xa9(\xb9U\xea!\x85z\xff\x83\xf5\xdau?\x96\xd1n\x12[Ö?\n\xa5\xcdpm\x19\xbeAVۨf\xe1\x96\xe5b\xb3\x01\x8d\xb0ho\xa4\xd9J9D\xac\xc31\x1f먬h\x85\xc1\xb8Z\xa6#K\x89\x1a\xb1\xa1PT\x1f\x85\xea\x1c\x1c\f-ȁ\xc8ţ\xc8k^\x90/\xc1e\xe6\xc6\xc7\x1b\xfcbZmB \xf6\xf0\x8fJ\xb5+Ρ\t\x83D&\xf6\x96\n\x95\x04\xf4\xf1K\x8c\x8d\xf6\xab\xc6)\x11\xd6_\x0e\xf6\x8d\xcc\xd4u\x01\xc6w\x97\x93\x9b\xdcꤋ\x96Yna\xa6\xe0k(\x98\x81\x022\xabt\x9cB)r\xe0J\xaaҍ\x10wD\xcb\xf6\xa3\xadv0\x13`\x19\x85\xb8[\x91m\x9d\xfb\x8a\x82F\xb0X\xae\xc0\xd0R\x12\xaf\xaa\"b\xba\xda2)\x1c\xbe\xb3)\xbdі\x04\r2\x84\x1b\xd3%mI\xd4\xcfm\x19%{;7\xfbT\x1f\xdf\x1c\x19\xc5\xf7_\x89\xe8\xc1\xea\x1c)\xec\x13\x9a\x84\xd1&K\xf2|\x88\x92\x1e).\xc0,;K\x9a\u0086\xaf)\f\xed\xf9\x8f{\xfbO{D\xf9u\xf1\xee\xb8\t3\x83u\x93s\xeae\x19\xd7t\xf3O\xc272Y7\xdeb\xcd\xe2٧n\xcb\v\xdaJ\xf1\f\xc9/\xd8F\x14\x16ȩ\x9aB\x94\xcd\xe0\xdc)\t\x94j\x81\x19\xed\xac\xdbl\xfb\xa1\xd9pKh1\xa0\xd5\x10\x80s\xd0C\x94C\xf8DDE\x1b?\x1az\xcc\xdd\xdf\x13!\xefZ*\xdbYƙ\xe9DW*\xff\x8da\x1b\xa1\x8d\xed\xa2a\x0e$V\x8d\x82:\"\xf4\x94\x1f\xb4>:\xf2\xfc\xe2Z\x0fR']\xb6\xf9\x9cx;\x90t\xcb\x1f\xc1\xa7\xfb\x82\xccT-i)\f\xf5\x00v3\x03\xa2c\x8d\xb3\x02\x89\xf6\xae\xd38\x9a\x819V\x16$IBN\xae\x9bu\x9b|\xe4\"m݊\x1d\xc7V{(\x87s\xac\x1c?\x8fB\x82g\xf7\fBɿ\x89\xb2.\x19/\x91\x87\xe4v\x88\x12\x9ac\b\x8e\xddM\xda'\xb6 \xa3e\x15β\xaa\x00\v>ms\x06\x1e\x99\x92F\xe4И~/\x02J2\xce6\\\x14\xb5\x9e\xa1Ug\x93|n\x10\xe6\xb5\xc9\xe9#\xabtD\x16D\xa2\xc4u\xf6\x19^\xf0\xb4Ư\xf4c\n\x86\vZ\xe3\x97LJ\xf7\\\xf3gŝ_\x06\xb0PX\x82\x9b\xfa\x8aq@Y\x17VTE{\x89],\xe0\xdc®\xb9\xac\xe8gEG\xe4\xfdM]_\xbe6\x12\xbf\x1cD5ܰ'(\n\xc6css\x8f\n\x99\xbb<5S\v@ۈ\xb3\xdc_\xc6\xe4o\\\xbdpӅn\x03 \v[Ɩ\xfa\xb8<|\xd3\xd7A\x03\x96\xaa\xc7\xf6\x85\x98x\xfa01\xd3 m\xf0G\x0e;\xf1t\xe1\xfcS\x85\x89\xfc\x9d3\xa5_\xf9\xf4\xe0+\x9f\x1a\xfc\x1e\xa7\x05\x13$0\xa1\xca\xfcS\x81\xcfޒR:\a=\xb9\xed7Gj'\xe555\x96\xeb#6\xd8\xd7\n\xb7\xc9b\xad^\f@fɿ~@/]\x1c\xda\x06G\xc9\xecxD\xbd}\xc9\xd6]\xeb;\xc4\xfe\t\f\xb7ui\xa0\xe2h\x00(p\xa3Ԭ\xa8\xab\xf0\x81g\xdbA\x0f[n\xd8F\xe9\x92[v\xdel\x16\xbfq\x1d\xe0\xdf\xe7K\xc6>\xaa&W\xa7{_\x9a\x11eU\xec0\x12c\xe7\xdd\x06ϓ\x92\xa8t\x86\x9e\xafU!\xb2\x88\xcf9z\xaf\x9ek\xb0w\xd9\x10\xdd\xfc\x97u\xb2Eb\x81\x0f6\x17\xe1\xd6\xc5\xfe\x95\xcc\xee\x12\xfc#\xd7Jx%\xfeD\xefP\x9d`\xd5\xed\xdd\xf5\x8a`\x051\xa2\a\xae\x9a\x04ņ\xe5k@\x97\xa1\x1d\xfb!}\xb2\xda\xf4\xa0\xf6s\x84\xbb/|@\xee\x9es\tn\x8bW͙B\xadu\xbdr\xb8\x1c\xea\t\xe5\x8b\xcb\x1dS\xfe\xbd\x0e\xa1\xf3Eŵݹd\xa2\x8b\x1e\x1e\xc1\xaeO\xad\x9a\x1d\xb4V\xfb\xcf\xd5tK\x8f\xec\xe1\xa5\x1a\xda\xc9\xdeU\xfd\xe4\x81!=\x9f\x83\xd3\xe1SՓ\xe7\xa9_\x00\xa7\xc3.Ԃ\xa8\x18\xf9)\x9a\x01y\xf2\x15K\xe3o\xe8\xff\x8bz\x84\xf7ѕ\xcb\xfe\x935\x83&#\xa9\x89\x01*]2\x1f\xa1`\x9b\x8fHw|?O\xed\xc5s\r\x03*\xfe\x8e\xf0\xe7,N\xde\xf4A\x8d\xbf\xe2B7\xa8\x87Nc^\x15\xbd\x8f\xb5c\xd7w\x14\xb76\xaa\xd4O}\x1f\xb7\x86\xe5ɐ`\x10\x81%\xe4\xc1\x87mNEF\xab4\xbf\x87O\xca=H\x94\"&\xfd\x16\xbd窼\xe7\x16\xf2\xb5\xfd$\x8c)z?\xb6!\xc0\xf6|\xc6\xdeE\xff\x88\xed\x91O\x19X[\xb55\xc7\x06\xdc\f\x03\x87쟐{ՑХ\xfb\x13c\xb8\xc6:\xcd)W/G\xd40\\\xd6\x7f\x13c\xc2\xf8Q\xc8\x05\xfb\f\xfb\x11\xfb\x82}\x908\x88}\x02\xb8\xf3\x8e\x90\xd3\xd6\ni\xc79C|lZ\xd1a\xd3\x11\r9-\xb6w\x03\x18\x83Lvz\xf4\xa9\xa9\xe2N\x9b\x1a\xf6[1\xe6\x8dҎY\x86\x03\xfd\xddޯQ\r~P{\xc74\xf7\xa8\x1a\xd9\xfbH\xaf\a\xe6\x1d\xc9\xf1^z\xf7K\xbdn\x1fT`\x7f\xfb\xfb\xd9\xff\a\x00\x00\xff\xff\f/o%s|\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"), } diff --git a/pkg/apis/velero/v1/backup_types.go b/pkg/apis/velero/v1/backup_types.go index 24af8132d..435e88f30 100644 --- a/pkg/apis/velero/v1/backup_types.go +++ b/pkg/apis/velero/v1/backup_types.go @@ -184,6 +184,10 @@ type BackupSpec struct { // +optional // +nullable UploaderConfig *UploaderConfigForBackup `json:"uploaderConfig,omitempty"` + + // BackupType specifies how volume data is backed up, with possible values including Full and Incremental. + // +optional + BackupType BackupType `json:"backupType,omitempty"` } // UploaderConfigForBackup defines the configuration for the uploader when doing backup. @@ -357,6 +361,15 @@ const ( BackupPhaseDeleting BackupPhase = "Deleting" ) +// BackupType specifies how volume data is backed up, with possible values including Full and Incremental. +// +kubebuilder:validation:Enum=Full;Incremental +type BackupType string + +const ( + BackupTypeFull BackupType = "Full" + BackupTypeIncremental BackupType = "Incremental" +) + // BackupStatus captures the current status of a Velero backup. type BackupStatus struct { // Version is the backup format major version. diff --git a/pkg/builder/backup_builder.go b/pkg/builder/backup_builder.go index d5b955e43..0553116a4 100644 --- a/pkg/builder/backup_builder.go +++ b/pkg/builder/backup_builder.go @@ -321,6 +321,11 @@ func (b *BackupBuilder) ParallelFilesUpload(parallel int) *BackupBuilder { return b } +func (b *BackupBuilder) BackupType(backupType velerov1api.BackupType) *BackupBuilder { + b.object.Spec.BackupType = backupType + return b +} + // WithStatus sets the Backup's status. func (b *BackupBuilder) WithStatus(status velerov1api.BackupStatus) *BackupBuilder { b.object.Status = status diff --git a/pkg/cmd/cli/backup/create.go b/pkg/cmd/cli/backup/create.go index 31564aae8..5e18f468f 100644 --- a/pkg/cmd/cli/backup/create.go +++ b/pkg/cmd/cli/backup/create.go @@ -108,6 +108,7 @@ type CreateOptions struct { ResPoliciesConfigmap string client kbclient.WithWatch ParallelFilesUpload int + BackupType string } func NewCreateOptions() *CreateOptions { @@ -156,6 +157,7 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { flags.StringVar(&o.ResPoliciesConfigmap, "resource-policies-configmap", "", "Reference to the resource policies configmap that backup should use") flags.StringVar(&o.DataMover, "data-mover", "", "Specify the data mover to be used by the backup. If the parameter is not set or set as 'velero', the built-in data mover will be used") flags.IntVar(&o.ParallelFilesUpload, "parallel-files-upload", 0, "Number of files uploads simultaneously when running a backup. This is only applicable for the kopia uploader") + flags.StringVar(&o.BackupType, "backup-type", "", "Specify how volume data is backed up, with possible values including Full and Incremental.") } // BindWait binds the wait flag separately so it is not called by other create @@ -217,6 +219,10 @@ func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Facto } } + if err := o.validateBackupType(); err != nil { + return err + } + return nil } @@ -231,6 +237,18 @@ func (o *CreateOptions) validateFromScheduleFlag(c *cobra.Command) error { return nil } +func (o *CreateOptions) validateBackupType() error { + backupType := strings.TrimSpace(o.BackupType) + + switch backupType { + case "", "Incremental", "Full": + default: + return fmt.Errorf("invalid backup type %s - valid values are 'Incremental', and 'Full'", backupType) + } + + return nil +} + func (o *CreateOptions) Complete(args []string, f client.Factory) error { // If an explicit name is specified, use that name if len(args) > 0 { @@ -393,7 +411,8 @@ func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, erro VolumeSnapshotLocations(o.SnapshotLocations...). CSISnapshotTimeout(o.CSISnapshotTimeout). ItemOperationTimeout(o.ItemOperationTimeout). - DataMover(o.DataMover) + DataMover(o.DataMover). + BackupType(velerov1api.BackupType(o.BackupType)) if len(o.OrderedResources) > 0 { orders, err := ParseOrderedResources(o.OrderedResources) if err != nil { diff --git a/pkg/cmd/cli/backup/create_test.go b/pkg/cmd/cli/backup/create_test.go index c8fd15baa..718ab0e96 100644 --- a/pkg/cmd/cli/backup/create_test.go +++ b/pkg/cmd/cli/backup/create_test.go @@ -122,6 +122,42 @@ func TestCreateOptions_ValidateFromScheduleFlag(t *testing.T) { }) } +func TestCreateOptions_ValidateBackupType(t *testing.T) { + t.Run("valid backup types", func(t *testing.T) { + o := NewCreateOptions() + + o.BackupType = "" + err := o.validateBackupType() + require.NoError(t, err) + + o.BackupType = "Incremental" + err = o.validateBackupType() + require.NoError(t, err) + + o.BackupType = "Full" + err = o.validateBackupType() + require.NoError(t, err) + + o.BackupType = " Incremental " + err = o.validateBackupType() + require.NoError(t, err) + }) + + t.Run("invalid backup type", func(t *testing.T) { + o := NewCreateOptions() + + o.BackupType = "incremental" + err := o.validateBackupType() + require.Error(t, err) + require.Equal(t, "invalid backup type incremental - valid values are 'Incremental', and 'Full'", err.Error()) + + o.BackupType = "invalid" + err = o.validateBackupType() + require.Error(t, err) + require.Equal(t, "invalid backup type invalid - valid values are 'Incremental', and 'Full'", err.Error()) + }) +} + func TestCreateOptions_BuildBackupFromSchedule(t *testing.T) { o := NewCreateOptions() o.FromSchedule = "test" @@ -231,6 +267,7 @@ func TestCreateCommand(t *testing.T) { resPoliciesConfigmap := "cm-name-2" dataMover := "velero" parallelFilesUpload := 10 + backupType := "Incremental" flags := new(flag.FlagSet) o := NewCreateOptions() o.BindFlags(flags) @@ -260,6 +297,7 @@ func TestCreateCommand(t *testing.T) { flags.Parse([]string{"--resource-policies-configmap", resPoliciesConfigmap}) flags.Parse([]string{"--data-mover", dataMover}) flags.Parse([]string{"--parallel-files-upload", strconv.Itoa(parallelFilesUpload)}) + flags.Parse([]string{"--backup-type", backupType}) //flags.Parse([]string{"--wait"}) client := velerotest.NewFakeControllerRuntimeClient(t).(kbclient.WithWatch) @@ -310,6 +348,7 @@ func TestCreateCommand(t *testing.T) { require.Equal(t, resPoliciesConfigmap, o.ResPoliciesConfigmap) require.Equal(t, dataMover, o.DataMover) require.Equal(t, parallelFilesUpload, o.ParallelFilesUpload) + require.Equal(t, backupType, o.BackupType) //assert.Equal(t, true, o.Wait) // verify oldAndNewFilterParametersUsedTogether diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index b7222d489..74b857fd2 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -410,6 +410,11 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel request.Spec.ItemOperationTimeout.Duration = b.defaultItemOperationTimeout } + if len(request.Spec.BackupType) == 0 { + // default backup type to incremental if not specified + request.Spec.BackupType = velerov1api.BackupTypeIncremental + } + // calculate expiration request.Status.Expiration = &metav1.Time{Time: b.clock.Now().Add(request.Spec.TTL.Duration)} diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index a96a5d27c..bab98efb6 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -524,6 +524,63 @@ func TestDefaultBackupTTL(t *testing.T) { } } +func TestPrepareBackupRequest_SetBackupType(t *testing.T) { + now, err := time.Parse(time.RFC1123Z, time.RFC1123Z) + require.NoError(t, err) + now = now.Local() + + tests := []struct { + name string + backup *velerov1api.Backup + expectedBackupType velerov1api.BackupType + }{ + { + name: "default backup type is Incremental", + backup: defaultBackup().Result(), + expectedBackupType: velerov1api.BackupTypeIncremental, + }, + { + name: "backup type is set to Full", + backup: defaultBackup().BackupType(velerov1api.BackupTypeFull).Result(), + expectedBackupType: velerov1api.BackupTypeFull, + }, + { + name: "backup type is set to Incremental", + backup: defaultBackup().BackupType(velerov1api.BackupTypeIncremental).Result(), + expectedBackupType: velerov1api.BackupTypeIncremental, + }, + } + + for _, test := range tests { + formatFlag := logging.FormatText + var ( + fakeClient kbclient.Client + logger = logging.DefaultLogger(logrus.DebugLevel, formatFlag) + ) + + t.Run(test.name, func(t *testing.T) { + apiServer := velerotest.NewAPIServer(t) + discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger) + require.NoError(t, err) + // add the test's backup storage location if it's different than the default + fakeClient = velerotest.NewFakeControllerRuntimeClient(t) + c := &backupReconciler{ + logger: logger, + discoveryHelper: discoveryHelper, + kbClient: fakeClient, + formatFlag: formatFlag, + clock: testclocks.NewFakeClock(now), + } + + res := c.prepareBackupRequest(ctx, test.backup, logger) + defer res.WorkerPool.Stop() + assert.NotNil(t, res) + + assert.Equal(t, test.expectedBackupType, res.Spec.BackupType) + }) + } +} + func TestPrepareBackupRequest_SetsVGSLabelKey(t *testing.T) { now, err := time.Parse(time.RFC1123Z, time.RFC1123Z) require.NoError(t, err) @@ -746,6 +803,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -786,6 +844,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -830,6 +889,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -871,6 +931,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -912,6 +973,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -954,6 +1016,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -996,6 +1059,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1038,6 +1102,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1080,6 +1145,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1123,6 +1189,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFailed, @@ -1166,6 +1233,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFailed, @@ -1209,6 +1277,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.True(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1253,6 +1322,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1297,6 +1367,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1341,6 +1412,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.True(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1386,6 +1458,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1430,6 +1503,7 @@ func TestProcessBackupCompletions(t *testing.T) { SnapshotMoveData: boolptr.True(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1480,6 +1554,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: append([]string{"clusterroles"}, autoExcludeClusterScopedResources...), IncludedNamespaceScopedResources: []string{"pods"}, ExcludedNamespaceScopedResources: append([]string{"secrets"}, autoExcludeNamespaceScopedResources...), + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1530,6 +1605,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: append([]string{"clusterroles"}, autoExcludeClusterScopedResources...), IncludedNamespaceScopedResources: []string{"pods"}, ExcludedNamespaceScopedResources: append([]string{"secrets"}, autoExcludeNamespaceScopedResources...), + BackupType: velerov1api.BackupTypeIncremental, }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, diff --git a/site/content/docs/main/api-types/backup.md b/site/content/docs/main/api-types/backup.md index 3bad516e3..30aeb1180 100644 --- a/site/content/docs/main/api-types/backup.md +++ b/site/content/docs/main/api-types/backup.md @@ -178,6 +178,13 @@ spec: # processed. Only "exec" hooks are supported. post: # Same content as pre above. + # BackupType specifies how volume data is backed up, with possible values including Full and Incremental. + # BackupType is optional. If it's not set, it will default to Full. + # BackupType is only meaningful for data mover backup, including CSI snapshot fs backup, CSI snapshot block backup, and fs backup. + # For CSI only backup and Velero native backup, backupType doesn't take effect. + # Full means data mover will forcefully upload all data in volumes. + # Incremental means data mover will only upload data change since last snapshot. + backupType: Full # Status about the Backup. Users should not set any data here. status: # The version of this Backup. The only version supported is 1. From 8ade19adfcd11581f947d74ff0a1e8a2526c02c4 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Wed, 15 Jul 2026 17:15:28 +0800 Subject: [PATCH 048/194] issue 9997: cancel ongoing PVB on timeout Signed-off-by: Lyndon-Li --- changelogs/unreleased/10007-Lyndon-Li | 1 + pkg/podvolume/backupper.go | 49 ++++++++++++++++++++++++++- pkg/podvolume/backupper_test.go | 42 ++++++++++++++++++++--- 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/10007-Lyndon-Li diff --git a/changelogs/unreleased/10007-Lyndon-Li b/changelogs/unreleased/10007-Lyndon-Li new file mode 100644 index 000000000..808c5920b --- /dev/null +++ b/changelogs/unreleased/10007-Lyndon-Li @@ -0,0 +1 @@ +Fix issue #9997, cancel ongoing PVB on timeout and wait for all PVBs to terminal state \ No newline at end of file diff --git a/pkg/podvolume/backupper.go b/pkg/podvolume/backupper.go index 5864a2090..261f227f8 100644 --- a/pkg/podvolume/backupper.go +++ b/pkg/podvolume/backupper.go @@ -20,12 +20,15 @@ import ( "context" "fmt" "sync" + "time" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/cache" ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -181,7 +184,7 @@ func newBackupper( // the PVB in the indexer is already in final status, no need to call WaitGroup.Done() if ok && (existPVB.Status.Phase == velerov1api.PodVolumeBackupPhaseCompleted || existPVB.Status.Phase == velerov1api.PodVolumeBackupPhaseFailed || - pvb.Status.Phase == velerov1api.PodVolumeBackupPhaseCanceled) { + existPVB.Status.Phase == velerov1api.PodVolumeBackupPhaseCanceled) { statusChangedToFinal = false } } @@ -411,6 +414,24 @@ func (b *backupper) WaitAllPodVolumesProcessed(log logrus.FieldLogger) []*velero select { case <-b.ctx.Done(): log.Error("timed out waiting for all PodVolumeBackups to complete") + + for _, obj := range b.pvbIndexer.List() { + pvb, ok := obj.(*velerov1api.PodVolumeBackup) + if !ok { + log.Errorf("expected PVB, but got %T", obj) + continue + } + + if pvb.Status.Phase != velerov1api.PodVolumeBackupPhaseCompleted && + pvb.Status.Phase != velerov1api.PodVolumeBackupPhaseFailed && + pvb.Status.Phase != velerov1api.PodVolumeBackupPhaseCanceled { + log.Infof("Setting cancel flag for ongoing PVB %s/%s", pvb.Namespace, pvb.Name) + if err := updatePVBWithRetry(context.Background(), b.crClient, pvb.Namespace, pvb.Name); err != nil { + log.WithError(err).Errorf("Failed to set cancel flag for PVB %s/%s", pvb.Namespace, pvb.Name) + } + } + } + <-done case <-done: } @@ -432,6 +453,32 @@ func (b *backupper) WaitAllPodVolumesProcessed(log logrus.FieldLogger) []*velero return podVolumeBackups } +func updatePVBWithRetry(ctx context.Context, client ctrlclient.Client, namespace, name string) error { + return wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (bool, error) { + pvb := &velerov1api.PodVolumeBackup{} + if err := client.Get(ctx, ctrlclient.ObjectKey{Namespace: namespace, Name: name}, pvb); err != nil { + return false, errors.Wrap(err, "getting PVB") + } + + if pvb.Spec.Cancel { + return true, nil + } + + pvb.Spec.Cancel = true + pvb.Status.Message = "Cancel PVB on pod volume timeout" + + err := client.Update(ctx, pvb) + if err != nil { + if apierrors.IsConflict(err) { + return false, nil + } + return false, errors.Wrapf(err, "error updating PVB %s/%s", pvb.Namespace, pvb.Name) + } + + return true, nil + }) +} + func (b *backupper) GetPodVolumeBackupByPodAndVolume(podNamespace, podName, volume string) (*velerov1api.PodVolumeBackup, error) { obj, exist, err := b.pvbIndexer.GetByKey(fmt.Sprintf(pvbKeyPattern, podNamespace, podName, volume)) if err != nil { diff --git a/pkg/podvolume/backupper_test.go b/pkg/podvolume/backupper_test.go index 66ad9e5ae..59466e02a 100644 --- a/pkg/podvolume/backupper_test.go +++ b/pkg/podvolume/backupper_test.go @@ -733,14 +733,14 @@ func TestListPodVolumeBackupsByPodp(t *testing.T) { } type logHook struct { - entry *logrus.Entry + entries []*logrus.Entry } func (l *logHook) Levels() []logrus.Level { return []logrus.Level{logrus.ErrorLevel} } func (l *logHook) Fire(entry *logrus.Entry) error { - l.entry = entry + l.entries = append(l.entries, entry) return nil } @@ -808,12 +808,35 @@ func TestWaitAllPodVolumesProcessed(t *testing.T) { logHook := &logHook{} logger.Hooks.Add(logHook) - backuper := newBackupper(c.ctx, log, nil, nil, informer, nil, "", &velerov1api.Backup{}) + backuper := newBackupper(c.ctx, log, nil, nil, informer, client, "", &velerov1api.Backup{}) if c.pvb != nil { require.NoError(t, backuper.pvbIndexer.Add(c.pvb)) backuper.wg.Add(1) } + if c.ctx == timeoutCtx && c.pvb != nil { + // Start a goroutine to simulate the controller's cancellation behavior + go func() { + // Wait a short time for the cancel flag to be set + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + for range ticker.C { + pvb := &velerov1api.PodVolumeBackup{} + err := client.Get(t.Context(), ctrlclient.ObjectKey{Namespace: c.pvb.Namespace, Name: c.pvb.Name}, pvb) + if err == nil && pvb.Spec.Cancel { + oldPVB := pvb.DeepCopy() + pvb.Status.Phase = velerov1api.PodVolumeBackupPhaseCanceled + pvb.Status.Message = "canceled" + _ = client.Update(t.Context(), pvb) + if informer.handler != nil { + informer.handler.OnUpdate(oldPVB, pvb) + } + return + } + } + }() + } + if c.statusToBeUpdated != nil { pvb := &velerov1api.PodVolumeBackup{} err := client.Get(t.Context(), ctrlclient.ObjectKey{Namespace: c.pvb.Namespace, Name: c.pvb.Name}, pvb) @@ -831,9 +854,18 @@ func TestWaitAllPodVolumesProcessed(t *testing.T) { pvbs := backuper.WaitAllPodVolumesProcessed(logger) if c.expectedErr != "" { - assert.Equal(t, c.expectedErr, logHook.entry.Message) + found := false + var loggedMsgs []string + for _, entry := range logHook.entries { + loggedMsgs = append(loggedMsgs, entry.Message) + if entry.Message == c.expectedErr { + found = true + break + } + } + assert.True(t, found, "Expected error %q to be logged, but got %v", c.expectedErr, loggedMsgs) } else { - assert.Nil(t, logHook.entry) + assert.Empty(t, logHook.entries) } if c.expectedPVBCount > 0 { From b0d7ada06ad3d821de6e48b561df388c5fb5b17e Mon Sep 17 00:00:00 2001 From: lyndon-li <98304688+Lyndon-Li@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:44:41 +0800 Subject: [PATCH 049/194] Block dev for block uploader backup (#9994) * add block dev operations for block data mover backup Signed-off-by: Lyndon-Li * add block dev operations for block data mover backup Signed-off-by: Lyndon-Li * Add block device operations for block uploader backup Signed-off-by: Lyndon-Li * Add block device operations for block uploader backup Signed-off-by: Lyndon-Li --------- Signed-off-by: Lyndon-Li --- changelogs/unreleased/9994-Lyndon-Li | 1 + pkg/uploader/block/dev_linux.go | 51 +++- pkg/uploader/block/dev_linux_test.go | 358 +++++++++++++++++++++++++++ pkg/uploader/block/snapshot.go | 4 + 4 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/9994-Lyndon-Li create mode 100644 pkg/uploader/block/dev_linux_test.go diff --git a/changelogs/unreleased/9994-Lyndon-Li b/changelogs/unreleased/9994-Lyndon-Li new file mode 100644 index 000000000..81ea8a01e --- /dev/null +++ b/changelogs/unreleased/9994-Lyndon-Li @@ -0,0 +1 @@ +Add block device operations for block uploader backup \ No newline at end of file diff --git a/pkg/uploader/block/dev_linux.go b/pkg/uploader/block/dev_linux.go index 85b378c55..297815390 100644 --- a/pkg/uploader/block/dev_linux.go +++ b/pkg/uploader/block/dev_linux.go @@ -21,11 +21,58 @@ package block import ( "os" + "path/filepath" + "syscall" "github.com/cockroachdb/errors" ) -// implement in following PRs +var lstatFunc = os.Lstat +var openFileFunc = os.OpenFile + +// openBlockDevice opens a block device for read/write, caller needs to close the returned handle func openBlockDevice(path string, read bool) (*os.File, error) { - return nil, errors.New("Not implemented") + devPath, err := resolveSymlink(path) + if err != nil { + return nil, errors.Wrap(err, "resolveSymlink") + } + + fileInfo, err := lstatFunc(devPath) + if err != nil { + return nil, errors.Wrapf(err, "unable to get the device information %s", devPath) + } + + if (fileInfo.Sys().(*syscall.Stat_t).Mode & syscall.S_IFMT) != syscall.S_IFBLK { + return nil, errors.Errorf("path %s is not a block device", devPath) + } + + flag := os.O_RDWR + mode := os.FileMode(0666) + if read { + flag = os.O_RDONLY + mode = 0 + } + + device, err := openFileFunc(devPath, flag|syscall.O_DIRECT, mode) + if err != nil { + if os.IsPermission(err) || errors.Is(err, syscall.EPERM) { + return nil, errors.Wrapf(err, "no permission to open device %s with mode %v", devPath, mode) + } + return nil, errors.Wrapf(err, "unable to open device %s", devPath) + } + + return device, nil +} + +func resolveSymlink(path string) (string, error) { + st, err := os.Lstat(path) + if err != nil { + return "", errors.Wrap(err, "stat") + } + + if (st.Mode() & os.ModeSymlink) == 0 { + return path, nil + } + + return filepath.EvalSymlinks(path) } diff --git a/pkg/uploader/block/dev_linux_test.go b/pkg/uploader/block/dev_linux_test.go new file mode 100644 index 000000000..42f0dd83e --- /dev/null +++ b/pkg/uploader/block/dev_linux_test.go @@ -0,0 +1,358 @@ +//go:build linux +// +build linux + +/* +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 block + +import ( + "os" + "path/filepath" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type fakeBlockDevFileInfo struct{} + +func (fakeBlockDevFileInfo) Name() string { return "fake-blk" } +func (fakeBlockDevFileInfo) Size() int64 { return 0 } +func (fakeBlockDevFileInfo) Mode() os.FileMode { return os.ModeDevice } +func (fakeBlockDevFileInfo) ModTime() time.Time { return time.Time{} } +func (fakeBlockDevFileInfo) IsDir() bool { return false } +func (fakeBlockDevFileInfo) Sys() any { + return &syscall.Stat_t{Mode: syscall.S_IFBLK} +} + +func TestResolveSymlink(t *testing.T) { + testCases := []struct { + name string + setupPath func(t *testing.T) string + expectError bool + errContains string + checkResult func(t *testing.T, input, result string) + }{ + { + name: "path does not exist returns error", + setupPath: func(t *testing.T) string { + t.Helper() + return filepath.Join(t.TempDir(), "nonexistent") + }, + expectError: true, + errContains: "stat", + }, + { + name: "regular file returns same path", + setupPath: func(t *testing.T) string { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), "regular-*") + require.NoError(t, err) + f.Close() + return f.Name() + }, + checkResult: func(t *testing.T, input, result string) { + t.Helper() + assert.Equal(t, input, result) + }, + }, + { + name: "directory returns same path", + setupPath: func(t *testing.T) string { + t.Helper() + return t.TempDir() + }, + checkResult: func(t *testing.T, input, result string) { + t.Helper() + assert.Equal(t, input, result) + }, + }, + { + name: "symlink to existing file returns target real path", + setupPath: func(t *testing.T) string { + t.Helper() + dir := t.TempDir() + target, err := os.CreateTemp(dir, "target-*") + require.NoError(t, err) + target.Close() + linkPath := filepath.Join(dir, "link") + require.NoError(t, os.Symlink(target.Name(), linkPath)) + return linkPath + }, + checkResult: func(t *testing.T, input, result string) { + t.Helper() + assert.NotEqual(t, input, result) + fi, err := os.Lstat(result) + require.NoError(t, err) + assert.Zero(t, fi.Mode()&os.ModeSymlink) + }, + }, + { + name: "symlink to existing directory returns resolved path", + setupPath: func(t *testing.T) string { + t.Helper() + outer := t.TempDir() + inner := t.TempDir() + linkPath := filepath.Join(outer, "dirlink") + require.NoError(t, os.Symlink(inner, linkPath)) + return linkPath + }, + checkResult: func(t *testing.T, input, result string) { + t.Helper() + assert.NotEqual(t, input, result) + fi, err := os.Lstat(result) + require.NoError(t, err) + assert.True(t, fi.IsDir()) + }, + }, + { + name: "broken symlink — target does not exist — returns error", + setupPath: func(t *testing.T) string { + t.Helper() + dir := t.TempDir() + linkPath := filepath.Join(dir, "broken-link") + require.NoError(t, os.Symlink(filepath.Join(dir, "nonexistent-target"), linkPath)) + return linkPath + }, + expectError: true, + errContains: "no such file or directory", + }, + { + name: "chain of symlinks is fully resolved", + setupPath: func(t *testing.T) string { + t.Helper() + dir := t.TempDir() + // real → link1 → link2 (two-hop chain) + real, err := os.CreateTemp(dir, "real-*") + require.NoError(t, err) + real.Close() + link1 := filepath.Join(dir, "link1") + require.NoError(t, os.Symlink(real.Name(), link1)) + link2 := filepath.Join(dir, "link2") + require.NoError(t, os.Symlink(link1, link2)) + return link2 + }, + checkResult: func(t *testing.T, input, result string) { + t.Helper() + assert.NotEqual(t, input, result) + fi, err := os.Lstat(result) + require.NoError(t, err) + assert.Zero(t, fi.Mode()&os.ModeSymlink) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + input := tc.setupPath(t) + result, err := resolveSymlink(input) + + if tc.expectError { + require.Error(t, err) + if tc.errContains != "" { + require.ErrorContains(t, err, tc.errContains) + } + assert.Empty(t, result) + } else { + require.NoError(t, err) + if tc.checkResult != nil { + tc.checkResult(t, input, result) + } + } + }) + } +} + +func TestOpenBlockDevice(t *testing.T) { + testCases := []struct { + name string + setupPath func(t *testing.T) string + read bool + expectError bool + errContains string + injectLstat func(string) (os.FileInfo, error) + injectOpenFile func(string, int, os.FileMode) (*os.File, error) + }{ + { + name: "path does not exist — resolveSymlink fails", + setupPath: func(t *testing.T) string { + t.Helper() + return filepath.Join(t.TempDir(), "nonexistent") + }, + read: true, + expectError: true, + errContains: "resolveSymlink", + }, + { + name: "regular file is not a block device — read mode", + setupPath: func(t *testing.T) string { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), "regular-*") + require.NoError(t, err) + f.Close() + return f.Name() + }, + read: true, + expectError: true, + errContains: "is not a block device", + }, + { + name: "regular file is not a block device — write mode", + setupPath: func(t *testing.T) string { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), "regular-*") + require.NoError(t, err) + f.Close() + return f.Name() + }, + read: false, + expectError: true, + errContains: "is not a block device", + }, + { + name: "directory is not a block device", + setupPath: func(t *testing.T) string { + t.Helper() + return t.TempDir() + }, + read: true, + expectError: true, + errContains: "is not a block device", + }, + { + name: "symlink to regular file is not a block device", + setupPath: func(t *testing.T) string { + t.Helper() + dir := t.TempDir() + target, err := os.CreateTemp(dir, "target-*") + require.NoError(t, err) + target.Close() + linkPath := filepath.Join(dir, "link") + require.NoError(t, os.Symlink(target.Name(), linkPath)) + return linkPath + }, + read: true, + expectError: true, + errContains: "is not a block device", + }, + { + name: "broken symlink — resolveSymlink fails", + setupPath: func(t *testing.T) string { + t.Helper() + dir := t.TempDir() + linkPath := filepath.Join(dir, "broken-link") + require.NoError(t, os.Symlink(filepath.Join(dir, "ghost"), linkPath)) + return linkPath + }, + read: true, + expectError: true, + errContains: "resolveSymlink", + }, + { + name: "EACCES from OpenFile — permission denied message", + setupPath: func(t *testing.T) string { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), "blk-*") + require.NoError(t, err) + f.Close() + return f.Name() + }, + read: true, + expectError: true, + errContains: "no permission to open device", + injectLstat: func(_ string) (os.FileInfo, error) { + return fakeBlockDevFileInfo{}, nil + }, + injectOpenFile: func(name string, _ int, _ os.FileMode) (*os.File, error) { + t.Helper() + return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EACCES} + }, + }, + { + name: "EPERM from OpenFile — permission denied message", + setupPath: func(t *testing.T) string { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), "blk-*") + require.NoError(t, err) + f.Close() + return f.Name() + }, + read: false, + expectError: true, + errContains: "no permission to open device", + injectLstat: func(_ string) (os.FileInfo, error) { + return fakeBlockDevFileInfo{}, nil + }, + injectOpenFile: func(name string, _ int, _ os.FileMode) (*os.File, error) { + return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EPERM} + }, + }, + { + name: "generic OpenFile error — unable to open device message", + setupPath: func(t *testing.T) string { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), "blk-*") + require.NoError(t, err) + f.Close() + return f.Name() + }, + read: true, + expectError: true, + errContains: "unable to open device", + injectLstat: func(_ string) (os.FileInfo, error) { + t.Helper() + return fakeBlockDevFileInfo{}, nil + }, + injectOpenFile: func(name string, _ int, _ os.FileMode) (*os.File, error) { + t.Helper() + return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EIO} + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Cleanup(func() { + lstatFunc = os.Lstat + openFileFunc = os.OpenFile + }) + if tc.injectLstat != nil { + lstatFunc = tc.injectLstat + } + if tc.injectOpenFile != nil { + openFileFunc = tc.injectOpenFile + } + + path := tc.setupPath(t) + f, err := openBlockDevice(path, tc.read) + + if tc.expectError { + require.Error(t, err) + if tc.errContains != "" { + require.ErrorContains(t, err, tc.errContains) + } + assert.Nil(t, f) + } else { + require.NoError(t, err) + require.NotNil(t, f) + f.Close() + } + }) + } +} diff --git a/pkg/uploader/block/snapshot.go b/pkg/uploader/block/snapshot.go index 30626da53..e30f5c1bb 100644 --- a/pkg/uploader/block/snapshot.go +++ b/pkg/uploader/block/snapshot.go @@ -67,6 +67,8 @@ func Backup(ctx context.Context, blkUp Uploader, repoWriter udmrepo.BackupRepo, return uploader.SnapshotInfo{}, false, errors.Wrapf(err, "error opening block device %s", source) } + defer sourceInfo.dev.Close() + sourceInfo.size, err = sourceInfo.dev.Seek(0, io.SeekEnd) if err != nil { return uploader.SnapshotInfo{}, false, errors.Wrapf(err, "error getting length of block device %s", source) @@ -218,6 +220,8 @@ func Restore(ctx context.Context, blkUp Uploader, rep udmrepo.BackupRepo, snapsh return 0, errors.Wrapf(err, "error opening block device '%s'", destPath) } + defer destDev.Close() + size, err := blkUp.Restore(snapshot, destInfo{dev: destDev, path: destPath}, bitmap.Iterator(), uploaderCfg) if err != nil { return 0, errors.Wrapf(err, "error restoring to block dev %s", destPath) From 3870ae6d6565c6d40565eb27f87a22b54b92733e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:13:46 +0000 Subject: [PATCH 050/194] Initial plan From 6b68e1168b8927d942bc0ba8d4a78224f0dccaa3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:14:54 +0000 Subject: [PATCH 051/194] Remove Auto Request Review workflow in favor of CODEOWNERS (#10018) --- .github/workflows/auto_request_review.yml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 .github/workflows/auto_request_review.yml diff --git a/.github/workflows/auto_request_review.yml b/.github/workflows/auto_request_review.yml deleted file mode 100644 index 096e4bdbc..000000000 --- a/.github/workflows/auto_request_review.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: "Auto Request Review" - -on: - pull_request_target: - types: [opened, ready_for_review, reopened] - -permissions: - contents: read - pull-requests: write - -jobs: - auto-request-review: - if: github.repository == 'velero-io/velero' - name: Auto Request Review - runs-on: ubuntu-latest - steps: - - name: Request a PR review based on files types/paths, and/or groups the author belongs to - uses: necojackarc/auto-request-review@v0.13.0 - with: - config: .github/auto-assignees.yml - token: ${{ secrets.GITHUB_TOKEN }} From 893188aa638573b5acf19a761e21bd402934ddad Mon Sep 17 00:00:00 2001 From: Joseph Antony Vaikath Date: Thu, 16 Jul 2026 13:57:53 -0400 Subject: [PATCH 052/194] Add design doc for dynamic CLI resource autocompletion (#9969) * Add design doc for dynamic CLI resource autocompletion Proposes adding ValidArgsFunction and RegisterFlagCompletionFunc to all Velero CLI commands that accept existing resource names, covering 20 commands and 5 flags across 6 resource types. Signed-off-by: Joseph * Update design doc to reflect implementation details - Document the shared completeNames helper using apimachinery's meta.ExtractList/Accessor instead of six duplicated functions - Add 3-second timeout, deep-copy, and per-item error resilience details - Update generics alternative to explain why they were unnecessary - Add testing section describing unit test coverage Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joseph * Add RBAC and bash compatibility notes to design doc - Note that users without list permission receive empty completions - Document bash 4.0+ requirement and macOS bash 3.2 workarounds Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joseph * Add issue reference to design doc abstract Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joseph * Address PR review comments: add debug flag completion and arg deduplication - Add `debug --backup` and `--restore` to flag completion table (chlins) - Document deduplication of already-typed args in completeNames (chlins) Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joseph * Trim design doc to focus on reviewable decisions Remove implementation mechanics (code snippets, type alias justification, deep-copy rationale, closure internals) that are verifiable from code. Drop bash v1-to-v2 migration (v1 already supports ValidArgsFunction). Fix flag count from 7 to 9 (add schedule create inherited flags). Add Open Issues section for single-arg commands, comma-separated flag values, and optional v2 migration. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joseph --------- Signed-off-by: Joseph Co-authored-by: Claude Opus 4.6 (1M context) --- .../cli-dynamic-resource-completion_design.md | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 design/cli-dynamic-resource-completion_design.md diff --git a/design/cli-dynamic-resource-completion_design.md b/design/cli-dynamic-resource-completion_design.md new file mode 100644 index 000000000..67695c275 --- /dev/null +++ b/design/cli-dynamic-resource-completion_design.md @@ -0,0 +1,122 @@ +# Dynamic Resource Autocompletion for Velero CLI + +## Abstract + +Velero CLI has no dynamic shell completion for resource names ([#9782](https://github.com/vmware-tanzu/velero/issues/9782)). +Tab-completing `velero backup describe ` produces no suggestions, even when backups exist on the cluster. +This proposal adds dynamic completion for all commands that take Velero resource names as positional arguments or flag values (using cobra's built-in completion callbacks). + +## Background + +Shell completion is a standard UX feature in Kubernetes CLI tooling. +Tools like `kubectl`, `oc`, and `helm` all provide dynamic completions that query the cluster to suggest resource names. +Velero's `velero completion` command generates completion scripts, but the CLI does not register any completion callbacks, so tab-completing resource names produces no suggestions. +Cobra's completion infrastructure already supports dynamic completion across all shell types (bash, zsh, fish); Velero just needs to register the callbacks. + +## Goals + +- Add dynamic shell completion for all 20 commands that accept existing Velero resource names as positional arguments. +- Add dynamic flag completion for 9 flags that reference existing Velero resources. +- Fail silently when the cluster is unreachable, matching the behavior of `kubectl`. + +## Non Goals + +- Completing positional arguments for commands that take new resource names (e.g., `velero backup create `). +- Completing flags that take non-resource values (e.g., `--include-namespaces`, `--labels`). +- Adding completion for hidden internal commands (`data-mover`, `pod-volume`, `repo-maintenance`). +- Caching cluster state across tab presses. + +## High-Level Design + +A centralized set of completion functions is added to `pkg/cmd/cli/completion_functions.go`. +Each function takes a `client.Factory`, returns a closure matching cobra's completion function signature, and lists resources of a specific type from the cluster. +Each command constructor wires the appropriate completion function onto its `cobra.Command` via `ValidArgsFunction` or `RegisterFlagCompletionFunc`. + +## Detailed Design + +### Completion functions + +A new file `pkg/cmd/cli/completion_functions.go` provides six public functions: + +| Function | Resource listed | +|---|---| +| `CompleteBackupNames(f client.Factory)` | `BackupList` | +| `CompleteRestoreNames(f client.Factory)` | `RestoreList` | +| `CompleteScheduleNames(f client.Factory)` | `ScheduleList` | +| `CompleteBackupStorageLocationNames(f client.Factory)` | `BackupStorageLocationList` | +| `CompleteVolumeSnapshotLocationNames(f client.Factory)` | `VolumeSnapshotLocationList` | +| `CompleteBackupRepositoryNames(f client.Factory)` | `BackupRepositoryList` | + +All six delegate to a single private `completeNames` helper that uses `meta.ExtractList()` and `meta.Accessor()` to extract names from any `ObjectList` type. + +The completion closure: + +- Lists resources in the configured namespace with a **3-second context timeout**. +- Filters by `strings.HasPrefix(name, toComplete)`. +- Removes names already present in `args` to avoid re-suggesting previously typed arguments. +- Returns `cobra.ShellCompDirectiveNoFileComp` in all cases (success or failure). +- Fails silently on any error (client construction, API call, extraction), returning no suggestions. + +### Commands wired with `ValidArgsFunction` + +| Package | Commands | Completion function | +|---|---|---| +| `backup` | get, describe, delete, logs, download | `CompleteBackupNames` | +| `restore` | get, describe, delete, logs | `CompleteRestoreNames` | +| `schedule` | get, describe, delete, pause, unpause | `CompleteScheduleNames` | +| `backuplocation` | get, set, delete | `CompleteBackupStorageLocationNames` | +| `snapshotlocation` | get, set | `CompleteVolumeSnapshotLocationNames` | +| `repo` | get | `CompleteBackupRepositoryNames` | + +### Flags wired with `RegisterFlagCompletionFunc` + +| Command | Flag | Completion function | +|---|---|---| +| `backup create` | `--from-schedule` | `CompleteScheduleNames` | +| `backup create` | `--storage-location` | `CompleteBackupStorageLocationNames` | +| `backup create` | `--volume-snapshot-locations` * | `CompleteVolumeSnapshotLocationNames` | +| `schedule create` | `--storage-location` | `CompleteBackupStorageLocationNames` | +| `schedule create` | `--volume-snapshot-locations` * | `CompleteVolumeSnapshotLocationNames` | +| `restore create` | `--from-backup` | `CompleteBackupNames` | +| `restore create` | `--from-schedule` | `CompleteScheduleNames` | +| `debug` | `--backup` | `CompleteBackupNames` | +| `debug` | `--restore` | `CompleteRestoreNames` | + +\* See Open Issues — comma-separated values. + +## Alternatives Considered + +The approach follows the standard cobra pattern for dynamic completion. No alternative designs were considered. + +## Security Considerations + +Completion functions issue read-only list requests using the user's existing kubeconfig credentials. +No new permissions are required beyond what the user already has. +Users without list permission receive empty completions, consistent with kubectl's behavior. + +## Compatibility + +Existing command behavior is unaffected. +`ValidArgsFunction` is only invoked during shell completion; it has no effect on normal command execution. +Completion respects the `--namespace` flag and `VELERO_NAMESPACE` environment variable. + +## Testing + +Unit tests in `pkg/cmd/cli/completion_functions_test.go` cover: + +- **Core logic:** Table-driven tests across all six resource types: empty cluster, full match, prefix filtering, no match. +- **Error resilience:** Factory errors return nil completions without panicking. +- **Wrapper isolation:** Each `Complete*Names` wrapper returns only its own resource type. + +## Open Issues + +- **Single-argument commands:** Commands like `backup download` and `backup logs` accept exactly one positional argument, but cobra still calls the completion function after one arg is provided. +The completion function should check `len(args)` and return no suggestions when the maximum arg count is reached. +The approach (parameter on the helper vs. per-command wrapper) is TBD. + +- **Comma-separated flag values:** `--volume-snapshot-locations` accepts comma-separated values. +Completion only works for the first value because `toComplete` contains the full string including commas. +Completing subsequent values would require comma-aware splitting, similar to how kubectl handles this. + +- **Bash v1 to v2 migration:** The current bash completion generator already supports dynamic completion through cobra's `__complete` mechanism, so migration to v2 is not required for this feature. +A separate migration could be considered for other benefits (cleaner generated scripts, ActiveHelp support) but would require users to regenerate their completion scripts. From 7d300ea9f94c459f2e1f9996c9dd505ae8676306 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:12:03 -0700 Subject: [PATCH 053/194] Scrub Restic references from main Velero docs (#9886) * Initial plan * Remove restic references from main docs * Refine Kopia-only performance and maintenance docs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Shubham Pampattiwar --- .../main/backup-repository-configuration.md | 2 +- site/content/docs/main/file-system-backup.md | 38 +----------------- .../content/docs/main/performance-guidance.md | 39 ++++++------------- .../docs/main/repository-maintenance.md | 5 +-- .../docs/main/self-signed-certificates.md | 2 +- 5 files changed, 17 insertions(+), 69 deletions(-) diff --git a/site/content/docs/main/backup-repository-configuration.md b/site/content/docs/main/backup-repository-configuration.md index fd6cf0b78..14110621a 100644 --- a/site/content/docs/main/backup-repository-configuration.md +++ b/site/content/docs/main/backup-repository-configuration.md @@ -18,7 +18,7 @@ Conclusively, you have two ways to add/change/delete configurations of a backup - If the BackupRepository CR for the backup repository is already there, you should modify the `repositoryConfig` field. The new changes will be applied to the backup repository at the due time, it doesn't require Velero server to restart. - Otherwise, you can create the backup repository configMap as a template for the BackupRepository CRs that are going to be created. -The backup repository configMap is repository type (i.e., kopia, restic) specific, so for one repository type, you only need to create one set of configurations, they will be applied to all BackupRepository CRs of the same type. Whereas, the changes of `repositoryConfig` field apply to the specific BackupRepository CR only, you may need to change every BackupRepository CR of the same type. +The backup repository configMap is repository type specific (for example, `kopia`), so for one repository type, you only need to create one set of configurations, they will be applied to all BackupRepository CRs of the same type. Whereas, the changes of `repositoryConfig` field apply to the specific BackupRepository CR only, you may need to change every BackupRepository CR of the same type. Below is an example of the BackupRepository configMap with the configurations: ```yaml diff --git a/site/content/docs/main/file-system-backup.md b/site/content/docs/main/file-system-backup.md index 853ee715e..139b91438 100644 --- a/site/content/docs/main/file-system-backup.md +++ b/site/content/docs/main/file-system-backup.md @@ -5,7 +5,7 @@ layout: docs Velero supports backing up and restoring Kubernetes volumes attached to pods from the file system of the volumes, called File System Backup (FSB shortly) or Pod Volume Backup. The data movement is fulfilled by using modules from free open-source -backup tools [restic][1] and [kopia][2]. This support is considered beta quality. Please see the list of [limitations](#limitations) +backup tool [kopia][2]. This support is considered beta quality. Please see the list of [limitations](#limitations) to understand if it fits your use case. Velero allows you to take snapshots of persistent volumes as part of your backups if you’re using one of @@ -38,7 +38,6 @@ It's important to understand that File System Backup (FSB) and volume snapshots This behavior is automatic and ensures optimal backup performance and storage usage. **NOTE:** hostPath volumes are not supported, but the [local volume type][5] is supported. -**NOTE:** restic is under the deprecation process by following [Velero Deprecation Policy][17], for more details, see the Restic Deprecation section. ## Setup File System Backup @@ -710,39 +709,6 @@ For Kopia repository, by default, the cache is stored in the data mover pod's ro - configure a limit of the cache size per backup repository, for more details, check [Backup Repository Configuration][18]. - configure a dedicated volume for cache data, for more details, check [Data Movement Cache Volume][22]. -## Restic Deprecation - -According to the [Velero Deprecation Policy][17], restic path is being deprecated starting from v1.15, specifically: -- For 1.15 and 1.16, if restic path is used by a backup, the backup still creates and succeeds but you will see warnings -- For 1.17 and 1.18, backups with restic path are disabled, but you are still allowed to restore from your previous restic backups -- From 1.19, both backups and restores with restic path will be disabled, you are not able to use 1.19 or higher to restore your restic backup data - -From 1.17, backup from restic path is not allowed, though you can still restore from the existing backups created by restic path. -Velero could automatically identify the legacy backups and switch to restic path without user intervention. - -### How Velero integrates with Restic -Velero integrate Restic binary directly, so the operations are done by calling Restic commands: -- Run `restic init` command to initialize the [restic repository](https://restic.readthedocs.io/en/latest/100_references.html#terminology) -- Run `restic prune` command periodically to prune restic repository -- Run `restic restore` commands to restore pod volume data - -For a restore from restic path, restic commands are called by the node-agent itself; whereas, for kopia path backup/restore, the data path runs in the data mover pods. -Restore from restic path is handled by the legacy `PodVolumeRestore` controller, so Resume and Cancellation are not supported: -- When Velero server is restarted, the legacy `PodVolumeRestore` is left as orphan and contineue running, though the restore has already marked as `Failed` -- When node-agent is restarted, the `PodVolumeRestore` is marked as `Failed` directly - -### Restic Repository -To support restic repository, the BackupRepository CR should be specially configured: - - You need to set the `resticRepoPrefix` value in BackupStorageLocation. For example, on AWS, `resticRepoPrefix` is something like - `s3:s3-us-west-2.amazonaws.com/bucket` (note that `resticRepoPrefix` doesn't work for Kopia). - -Velero still effectively manage restic repository, though you cannot write any new backup to it: -- When you delete a backup, the restic repository snapshots (if any) could be deleted from restic repository -- Velero backup repository controller periodically runs mainteance jobs for BackupRepository CRs representing restic repositories - - - -[1]: https://github.com/restic/restic [2]: https://github.com/kopia/kopia [3]: customize-installation.md#enable-file-system-backup [4]: https://github.com/velero-io/velero/releases/ @@ -750,7 +716,6 @@ Velero still effectively manage restic repository, though you cannot write any n [6]: https://kubernetes.io/docs/concepts/storage/volumes/#mount-propagation [7]: https://github.com/bitsbeats/velero-pvc-watcher [8]: https://docs.microsoft.com/en-us/azure/aks/azure-files-dynamic-pv -[9]: https://github.com/restic/restic/issues/1800 [10]: customize-installation.md#default-pod-volume-backup-to-file-system-backup [11]: https://www.vcluster.com/ [12]: csi.md @@ -758,7 +723,6 @@ Velero still effectively manage restic repository, though you cannot write any n [14]: https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/ [15]: customize-installation.md#customize-resource-requests-and-limits [16]: performance-guidance.md -[17]: https://github.com/velero-io/velero/blob/main/GOVERNANCE.md#deprecation-policy [18]: backup-repository-configuration.md [19]: node-agent-concurrency.md [20]: node-agent-prepare-queue-length.md diff --git a/site/content/docs/main/performance-guidance.md b/site/content/docs/main/performance-guidance.md index 8596b4a52..b1b1f1fab 100644 --- a/site/content/docs/main/performance-guidance.md +++ b/site/content/docs/main/performance-guidance.md @@ -3,9 +3,9 @@ title: "Velero File System Backup Performance Guide" layout: docs --- -When using Velero to do file system backup & restore, Restic uploader or Kopia uploader are both supported now. But the resources used and time consumption are a big difference between them. +When using Velero to do file system backup & restore, Kopia uploader performance can vary based on data shape and resource settings. -We've done series rounds of tests against Restic uploader and Kopia uploader through Velero, which may give you some guidance. But the test results will vary from different infrastructures, and our tests are limited and couldn't cover a variety of data scenarios, **the test results and analysis are for reference only**. +We've done several rounds of tests against Kopia uploader through Velero, which may give you some guidance. But the test results will vary from different infrastructures, and our tests are limited and couldn't cover a variety of data scenarios, **the test results and analysis are for reference only**. ## Infrastructure @@ -79,25 +79,21 @@ Server: ## Test -Below we've done 6 groups of tests, for each single group of test, we used limited resources (1 core CPU 2 GB memory or 4 cores CPU 4 GB memory) to do Velero file system backup under Restic path and Kopia path, and then compare the results. +Below we've done 6 groups of tests. For each single group of test, we used limited resources (1 core CPU 2 GB memory or 4 cores CPU 4 GB memory) to do Velero file system backup under Kopia path. -Recorded the metrics of time consumption, maximum CPU usage, maximum memory usage, and minio storage usage for node-agent daemonset, and the metrics of Velero deployment are not included since the differences are not obvious by whether using Restic uploader or Kopia uploader. +Recorded the metrics of time consumption, maximum CPU usage, maximum memory usage, and minio storage usage for node-agent daemonset. The metrics of Velero deployment are not included. -Compression is either disabled or not unavailable for both uploader. +Compression is disabled for testing purposes. ### Case 1: 4194304(4M) files, 2396745(2M) directories, 0B per file total 0B content #### result: |Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage| |--------|----------|:----:|------:|:--------:|:--------:| | Kopia | 1c2g |24m54s| 65% |1530 MB |80 MB | -| Restic | 1c2g |52m31s| 55% |1708 MB |3.3 GB | | Kopia | 4c4g |24m52s| 63% |2216 MB |80 MB | -| Restic | 4c4g |52m28s| 54% |2329 MB |3.3 GB | #### conclusion: -- The memory usage is larger than Velero's default memory limit (1GB) for both Kopia and Restic under massive empty files. -- For both using Kopia uploader and Restic uploader, there is no significant time reduction by increasing resources from 1c2g to 4c4g. -- Restic uploader is one more time slower than Kopia uploader under the same specification resources. -- Restic has an **irrational** repository size (3.3GB) +- The memory usage is larger than Velero's default memory limit (1GB) for Kopia under massive empty files. +- There is no significant time reduction by increasing resources from 1c2g to 4c4g. ### Case 2: Using the same size (100B) of file and default Velero's resource configuration, the testing quantity of files from 20 thousand to 2 million, these groups of cases mainly test the behavior with the increasing quantity of files. @@ -106,58 +102,47 @@ Compression is either disabled or not unavailable for both uploader. | Uploader | Resources|Times |Max CPU|Max Memory|Repo Usage| |-------|----------|:----:|------:|:--------:|:--------:| | Kopia | 1c1g |2m34s | 70% |692 MB |108 MB | -| Restic| 1c1g |3m9s | 54% |714 MB |275 MB | ### Case 2.2 470596(40k) files, 137257 (10k)directories, 100B per file total 44.880MB content #### result: | Uploader | Resources|Times |Max CPU|Max Memory|Repo Usage| |-------|----------|:----:|------:|:--------:|:--------:| | Kopia | 1c1g |3m45s | 68% |831 MB |108 MB | -| Restic| 1c1g |4m53s | 57% |788 MB |275 MB | ### Case 2.3 705894(70k) files, 137257(10k) directories, 100B per file total 67.319MB content #### result: |Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage| |--------|----------|:----:|------:|:--------:|:--------:| | Kopia | 1c1g |5m06s | 71% |861 MB |108 MB | -| Restic | 1c1g |6m23s | 56% |810 MB |275 MB | ### Case 2.4 2097152(2M) files, 2396745(2M) directories, 100B per file total 200.000MB content #### result: |Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage| |--------|----------|:----:|------:|:--------:|:--------:| | Kopia | 1c1g |OOM | 74% |N/A |N/A | -| Restic | 1c1g |41m47s| 52% |904 MB |3.2 GB | #### conclusion: -- With the increasing number of files, there is no memory abnormal surge, the memory usage for both Kopia uploader and Restic uploader is linear increasing, until exceeds 1GB memory usage in Case 2.4 Kopia uploader OOM happened. +- With the increasing number of files, there is no memory abnormal surge, and memory usage is linearly increasing until it exceeds 1GB where Case 2.4 Kopia uploader OOM happened. - Kopia uploader gets increasingly faster along with the increasing number of files. -- Restic uploader repository size is still much larger than Kopia uploader repository. ### Case 3: 10625(10k) files, 781 directories, 1.000MB per file total 10.376GB content #### result: |Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage| |--------|----------|:----:|------:|:--------:|:--------:| | Kopia | 1c2g |1m37s | 75% |251 MB |10 GB | -| Restic | 1c2g |5m25s | 100% |153 MB |10 GB | | Kopia | 4c4g |1m35s | 75% |248 MB |10 GB | -| Restic | 4c4g |3m17s | 171% |126 MB |10 GB | #### conclusion: -- This case involves a relatively large backup size, there is no significant time reduction by increasing resources from 1c2g to 4c4g for Kopia uploader, but for Restic uploader when increasing CPU from 1 core to 4, backup time-consuming was shortened by one-third, which means in this scenario should allocate more CPU resources for Restic uploader. -- For the large backup size case, Restic uploader's repository size comes to normal +- This case involves a relatively large backup size, and there is no significant time reduction by increasing resources from 1c2g to 4c4g for Kopia uploader. ### Case 4: 900 files, 1 directory, 1.000GB per file total 900.000GB content #### result: |Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage| |--------|----------|:-----:|------:|:--------:|:--------:| | Kopia | 1c2g |2h30m | 100% |714 MB |900 GB | -| Restic | 1c2g |Timeout| 100% |416 MB |N/A | | Kopia | 4c4g |1h42m | 138% |786 MB |900 GB | -| Restic | 4c4g |2h15m | 351% |606 MB |900 GB | #### conclusion: -- When the target backup data is relatively large, Restic uploader starts to Timeout under 1c2g. So it's better to allocate more memory for Restic uploader when backup large sizes of data. -- For backup large amounts of data, Kopia uploader is both less time-consuming and less resource usage. +- For backup large amounts of data, allocating more resources can reduce backup time for Kopia uploader. ## Summary - With the same specification resources, Kopia uploader is less time-consuming when backup. -- Performance would be better if choosing Kopia uploader for the scenario in backup large mounts of data or massive small files. -- It's better to set one reasonable resource configuration instead of the default depending on your scenario. For default resource configuration, it's easy to be timeout with Restic uploader in backup large amounts of data, and it's easy to be OOM for both Kopia uploader and Restic uploader in backup of massive small files. \ No newline at end of file +- Kopia uploader performs well when backing up large amounts of data or massive small files. +- It's better to set one reasonable resource configuration instead of the default depending on your scenario. With default configuration, it's easy to hit timeout or OOM in large-scale backups. diff --git a/site/content/docs/main/repository-maintenance.md b/site/content/docs/main/repository-maintenance.md index 7aa07e940..fb8a2d4fb 100644 --- a/site/content/docs/main/repository-maintenance.md +++ b/site/content/docs/main/repository-maintenance.md @@ -23,7 +23,7 @@ If there is a key value as `global` in the map, the key's value is applied to al The other keys in the map is the combination of three elements of a BackupRepository, because those three keys can identify a unique BackupRepository: * The namespace in which BackupRepository backs up volume data. * The BackupRepository referenced BackupStorageLocation's name. -* The BackupRepository's type. Possible values are `kopia` and `restic`. +* The BackupRepository's type. Possible value is `kopia`. If there is a key match with BackupRepository, the key's value is applied to the BackupRepository's maintenance jobs. By this way, it's possible to let user configure before the BackupRepository is created. @@ -45,7 +45,6 @@ For example, the following BackupRepository's key should be `test-default-kopia` backupStorageLocation: default maintenanceFrequency: 1h0m0s repositoryType: kopia - resticIdentifier: gs:jxun:/restic/test volumeNamespace: test ``` @@ -135,7 +134,7 @@ The frequency of running maintenance jobs could be set by the below command when ```bash velero install --default-repo-maintain-frequency ``` -For Kopia the default maintenance frequency is 1 hour, and Restic is 7 * 24 hours. +For Kopia the default maintenance frequency is 1 hour. ### Full Maintenance Interval customization See [backup repository configuration][3] diff --git a/site/content/docs/main/self-signed-certificates.md b/site/content/docs/main/self-signed-certificates.md index e1ef5728b..41eb8b247 100644 --- a/site/content/docs/main/self-signed-certificates.md +++ b/site/content/docs/main/self-signed-certificates.md @@ -154,4 +154,4 @@ Velero provides a way for you to skip TLS verification on the object store when If true, the object store's TLS certificate will not be checked for validity before Velero or backup repository connects to the object storage. You can permanently skip TLS verification for an object store by setting `Spec.Config.InsecureSkipTLSVerify` to true in the [BackupStorageLocation](api-types/backupstoragelocation.md) CRD. -Note that Velero's File System Backup uses Restic or Kopia to do data transfer between object store and Kubernetes cluster disks. This means that when you specify `--insecure-skip-tls-verify` in Velero operations that involve File System Backup, Velero will convey this information to Restic or Kopia. For example, for Restic, Velero will add the Restic global command parameter `--insecure-tls` to Restic commands. +Note that Velero's File System Backup uses Kopia to do data transfer between object store and Kubernetes cluster disks. This means that when you specify `--insecure-skip-tls-verify` in Velero operations that involve File System Backup, Velero will convey this information to Kopia. From ddac19121aad03d75a6e72ab3c305368e4841318 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: Fri, 17 Jul 2026 11:00:59 +0800 Subject: [PATCH 054/194] Only run PVC CSI RIA for CSI snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Put the checking logic at the begining to make sure only run PVC CSI RIA for CSI snapshot Signed-off-by: Wenkai Yin(尹文开) --- pkg/restore/actions/csi/pvc_action.go | 17 ++++++++------- pkg/restore/actions/csi/pvc_action_test.go | 24 +++++++++++----------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/pkg/restore/actions/csi/pvc_action.go b/pkg/restore/actions/csi/pvc_action.go index 6dd98c6b6..2203682be 100644 --- a/pkg/restore/actions/csi/pvc_action.go +++ b/pkg/restore/actions/csi/pvc_action.go @@ -89,6 +89,14 @@ func (p *pvcRestoreItemAction) Execute( }) logger.Info("Starting PVCRestoreItemAction for PVC") + vsName, nameOK := pvcFromBackup.Annotations[velerov1api.VolumeSnapshotLabel] + if !nameOK { + logger.Info("Skipping PVCRestoreItemAction for PVC, PVC does not have a CSI VolumeSnapshot.") + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: input.Item, + }, nil + } + // If PVC already exists, returns early. if p.isResourceExist(pvc, *input.Restore) { logger.Warnf("PVC already exists. Skip restore this PVC.") @@ -155,15 +163,6 @@ func (p *pvcRestoreItemAction) Execute( logger.Infof("DataDownload %s/%s is created successfully.", dataDownload.Namespace, dataDownload.Name) } else { - //CSI restore - vsName, nameOK := pvcFromBackup.Annotations[velerov1api.VolumeSnapshotLabel] - if !nameOK { - logger.Info("Skipping PVCRestoreItemAction for PVC, PVC does not have a CSI VolumeSnapshot.") - return &velero.RestoreItemActionExecuteOutput{ - UpdatedItem: input.Item, - }, nil - } - //To avoid confilcs, vs and vsc get a new uniq name based in restore UID // and vs name old name newVSName := util.GenerateSha256FromRestoreUIDAndVsName(string(input.Restore.UID), vsName) diff --git a/pkg/restore/actions/csi/pvc_action_test.go b/pkg/restore/actions/csi/pvc_action_test.go index 4da3bec50..ea712c027 100644 --- a/pkg/restore/actions/csi/pvc_action_test.go +++ b/pkg/restore/actions/csi/pvc_action_test.go @@ -381,13 +381,13 @@ func TestExecute(t *testing.T) { { name: "Don't restore PV", restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").RestorePVs(false).Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").Result(), - expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("").Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName")).Result(), + expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName")).VolumeName("").Result(), }, { name: "restore's backup cannot be found", restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName")).Result(), expectedErr: "fail to get backup for restore: backups.velero.io \"testBackup\" not found", }, { @@ -408,15 +408,15 @@ func TestExecute(t *testing.T) { name: "Restore from VolumeSnapshot without volume-snapshot-name annotation", backup: builder.ForBackup("velero", "testBackup").Result(), restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(AnnSelectedNode, "node1")).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", AnnSelectedNode, "node1")).Result(), vs: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi")).Result(), - expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(AnnSelectedNode, "node1")).Result(), + expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", AnnSelectedNode, "node1")).Result(), }, { name: "DataUploadResult cannot be found", backup: builder.ForBackup("velero", "testBackup").SnapshotMoveData(true).Result(), restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").Result(), expectedErr: "fail get DataUploadResult for restore: testRestore: no DataUpload result cm found with labels velero.io/pvc-namespace-name=velero.testPVC,velero.io/restore-uid=,velero.io/resource-usage=DataUpload", }, @@ -424,9 +424,9 @@ func TestExecute(t *testing.T) { name: "Restore from DataUploadResult", backup: builder.ForBackup("velero", "testBackup").SnapshotMoveData(true).Result(), restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").ObjectMeta(builder.WithUID("uid")).Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), dataUploadResult: builder.ForConfigMap("velero", "testCM").Data("uid", "{}").ObjectMeta(builder.WithLabels(velerov1api.RestoreUIDLabel, "uid", velerov1api.PVCNamespaceNameLabel, "velero.testPVC", velerov1api.ResourceUsageLabel, label.GetValidName(string(velerov1api.VeleroResourceUsageDataUploadResult)))).Result(), - expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations("velero.io/csi-volumesnapshot-restore-size", "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), + expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", "velero.io/csi-volumesnapshot-restore-size", "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), expectedDataDownload: builder.ForDataDownload("velero", "name").TargetVolume(velerov2alpha1.TargetVolumeSpec{PVC: "testPVC", Namespace: "velero"}). ObjectMeta(builder.WithOwnerReference([]metav1.OwnerReference{{APIVersion: velerov1api.SchemeGroupVersion.String(), Kind: "Restore", Name: "testRestore", UID: "uid", Controller: boolptr.True()}}), builder.WithLabelsMap(map[string]string{velerov1api.AsyncOperationIDLabel: "dd-uid.", velerov1api.RestoreNameLabel: "testRestore", velerov1api.RestoreUIDLabel: "uid"}), @@ -436,9 +436,9 @@ func TestExecute(t *testing.T) { name: "Restore from DataUploadResult with long source PVC namespace and name", backup: builder.ForBackup("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "testBackup").SnapshotMoveData(true).Result(), restore: builder.ForRestore("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "testRestore").Backup("testBackup").ObjectMeta(builder.WithUID("uid")).Result(), - pvc: builder.ForPersistentVolumeClaim("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "kibishii-data-kibishii-deployment-0").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), + pvc: builder.ForPersistentVolumeClaim("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "kibishii-data-kibishii-deployment-0").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), dataUploadResult: builder.ForConfigMap("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "testCM").Data("uid", "{}").ObjectMeta(builder.WithLabels(velerov1api.RestoreUIDLabel, "uid", velerov1api.PVCNamespaceNameLabel, "migre209d0da-49c7-45ba-8d5a-3e59fd591ec1.kibishii-data-ki152333", velerov1api.ResourceUsageLabel, label.GetValidName(string(velerov1api.VeleroResourceUsageDataUploadResult)))).Result(), - expectedPVC: builder.ForPersistentVolumeClaim("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "kibishii-data-kibishii-deployment-0").ObjectMeta(builder.WithAnnotations("velero.io/csi-volumesnapshot-restore-size", "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), + expectedPVC: builder.ForPersistentVolumeClaim("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1", "kibishii-data-kibishii-deployment-0").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", "velero.io/csi-volumesnapshot-restore-size", "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), }, { name: "PVC had no DataUploadNameLabel annotation", @@ -450,14 +450,14 @@ func TestExecute(t *testing.T) { name: "Restore a PVC that already exists.", backup: builder.ForBackup("velero", "testBackup").SnapshotMoveData(true).Result(), restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").ObjectMeta(builder.WithUID("uid")).Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), preCreatePVC: true, }, { name: "Restore a PVC that already exists in the mapping namespace", backup: builder.ForBackup("velero", "testBackup").SnapshotMoveData(true).Result(), restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").NamespaceMappings("velero", "restore").ObjectMeta(builder.WithUID("uid")).Result(), - pvc: builder.ForPersistentVolumeClaim("restore", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), + pvc: builder.ForPersistentVolumeClaim("restore", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", velerov1api.VolumeSnapshotRestoreSize, "10Gi", velerov1api.DataUploadNameAnnotation, "velero/")).Result(), preCreatePVC: true, }, } From a0749e765868b948e5e98cd3cc582c38a3891bfe Mon Sep 17 00:00:00 2001 From: lyndon-li <98304688+Lyndon-Li@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:35:19 +0800 Subject: [PATCH 055/194] Block dev for restore (#10013) * add block dev operations for block uploader restore Signed-off-by: Lyndon-Li * add block dev operations for block uploader restore Signed-off-by: Lyndon-Li --------- Signed-off-by: Lyndon-Li --- changelogs/unreleased/10013-Lyndon-Li | 1 + pkg/uploader/block/dev_linux.go | 27 ++++++++++++++++++++++++++ pkg/uploader/block/dev_linux_test.go | 28 +++++++++++++++++++++++++++ pkg/uploader/block/dev_other.go | 4 ++++ 4 files changed, 60 insertions(+) create mode 100644 changelogs/unreleased/10013-Lyndon-Li diff --git a/changelogs/unreleased/10013-Lyndon-Li b/changelogs/unreleased/10013-Lyndon-Li new file mode 100644 index 000000000..501a3673b --- /dev/null +++ b/changelogs/unreleased/10013-Lyndon-Li @@ -0,0 +1 @@ +Add block dev restore operations for block data mover \ No newline at end of file diff --git a/pkg/uploader/block/dev_linux.go b/pkg/uploader/block/dev_linux.go index 297815390..689031acf 100644 --- a/pkg/uploader/block/dev_linux.go +++ b/pkg/uploader/block/dev_linux.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "syscall" + "unsafe" "github.com/cockroachdb/errors" ) @@ -76,3 +77,29 @@ func resolveSymlink(path string) (string, error) { return filepath.EvalSymlinks(path) } + +func blkZeroOut(dest *os.File, start int64, length int64) error { + const BLKZEROOUT = 0x127b + + zeroRange := [2]uint64{uint64(start), uint64(length)} + + rawConn, err := dest.SyscallConn() + if err != nil { + return errors.Wrap(err, "error getting raw connection") + } + + ioctlErr := syscall.Errno(0) + if err := rawConn.Control(func(fd uintptr) { + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, BLKZEROOUT, uintptr(unsafe.Pointer(&zeroRange[0]))); errno != 0 { + ioctlErr = errno + } + }); err != nil { + return errors.Wrap(err, "error controlling block dev") + } + + if ioctlErr != 0 { + return errors.Wrapf(ioctlErr, "error calling ioctl on block dev") + } + + return nil +} diff --git a/pkg/uploader/block/dev_linux_test.go b/pkg/uploader/block/dev_linux_test.go index 42f0dd83e..b5243cc69 100644 --- a/pkg/uploader/block/dev_linux_test.go +++ b/pkg/uploader/block/dev_linux_test.go @@ -26,6 +26,7 @@ import ( "testing" "time" + "github.com/cockroachdb/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -356,3 +357,30 @@ func TestOpenBlockDevice(t *testing.T) { }) } } + +func TestBlkZeroOut(t *testing.T) { + t.Run("closed file returns error", func(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "blkzeroout-test-*") + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + + err = blkZeroOut(f, 0, 1024) + assert.Error(t, err) + }) + + t.Run("regular file returns ioctl error", func(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "blkzeroout-test-*") + require.NoError(t, err) + defer f.Close() + + err = blkZeroOut(f, 0, 1024) + require.Error(t, err) + assert.Contains(t, err.Error(), "error calling ioctl on block dev") + + // On regular files, ioctl with BLKZEROOUT should fail with ENOTTY (inappropriate ioctl for device) or EINVAL + isENOTTY := errors.Is(err, syscall.ENOTTY) + isEINVAL := errors.Is(err, syscall.EINVAL) + assert.True(t, isENOTTY || isEINVAL, "expected error to be ENOTTY or EINVAL, got: %v", err) + }) +} diff --git a/pkg/uploader/block/dev_other.go b/pkg/uploader/block/dev_other.go index c8a55cab2..5a3516d50 100644 --- a/pkg/uploader/block/dev_other.go +++ b/pkg/uploader/block/dev_other.go @@ -27,3 +27,7 @@ import ( func openBlockDevice(_ string, _ bool) (*os.File, error) { return nil, fmt.Errorf("block mode is not supported for non-linux platforms") } + +func blkZeroOut(_ *os.File, _ int64, _ int64) error { + return fmt.Errorf("block mode is not supported for non-linux platforms") +} From deba91ab9c01cd9460f7688116f4262ffcb10bf4 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 15 Jul 2026 10:18:57 -0700 Subject: [PATCH 056/194] Add CRD short names for all Velero custom resources Only 3 of 13 Velero CRDs had short names (bsl, vsl, ssr). This adds short names to the remaining 10 CRDs for better kubectl usability: Backup=bkp, Restore=rst, Schedule=sched, BackupRepository=br, DeleteBackupRequest=dbr, DownloadRequest=dr, PodVolumeBackup=pvb, PodVolumeRestore=pvr, DataUpload=du, DataDownload=dd Signed-off-by: Shubham Pampattiwar --- .../v1/bases/velero.io_backuprepositories.yaml | 2 ++ config/crd/v1/bases/velero.io_backups.yaml | 2 ++ .../v1/bases/velero.io_deletebackuprequests.yaml | 2 ++ .../crd/v1/bases/velero.io_downloadrequests.yaml | 2 ++ .../crd/v1/bases/velero.io_podvolumebackups.yaml | 2 ++ .../v1/bases/velero.io_podvolumerestores.yaml | 2 ++ config/crd/v1/bases/velero.io_restores.yaml | 2 ++ config/crd/v1/bases/velero.io_schedules.yaml | 2 ++ config/crd/v1/crds/crds.go | 16 ++++++++-------- .../v2alpha1/bases/velero.io_datadownloads.yaml | 2 ++ .../v2alpha1/bases/velero.io_datauploads.yaml | 2 ++ config/crd/v2alpha1/crds/crds.go | 4 ++-- pkg/apis/velero/v1/backup_repository_types.go | 2 +- pkg/apis/velero/v1/backup_types.go | 1 + .../velero/v1/delete_backup_request_types.go | 1 + pkg/apis/velero/v1/download_request_types.go | 1 + pkg/apis/velero/v1/pod_volume_backup_types.go | 1 + pkg/apis/velero/v1/pod_volume_restore_type.go | 1 + pkg/apis/velero/v1/restore_types.go | 1 + pkg/apis/velero/v1/schedule_types.go | 1 + pkg/apis/velero/v2alpha1/data_download_types.go | 1 + pkg/apis/velero/v2alpha1/data_upload_types.go | 1 + 22 files changed, 40 insertions(+), 11 deletions(-) diff --git a/config/crd/v1/bases/velero.io_backuprepositories.yaml b/config/crd/v1/bases/velero.io_backuprepositories.yaml index a7a2510bd..ccc553b86 100644 --- a/config/crd/v1/bases/velero.io_backuprepositories.yaml +++ b/config/crd/v1/bases/velero.io_backuprepositories.yaml @@ -11,6 +11,8 @@ spec: kind: BackupRepository listKind: BackupRepositoryList plural: backuprepositories + shortNames: + - br singular: backuprepository scope: Namespaced versions: diff --git a/config/crd/v1/bases/velero.io_backups.yaml b/config/crd/v1/bases/velero.io_backups.yaml index 3b98f2ad4..cb20b5304 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -11,6 +11,8 @@ spec: kind: Backup listKind: BackupList plural: backups + shortNames: + - bkp singular: backup scope: Namespaced versions: diff --git a/config/crd/v1/bases/velero.io_deletebackuprequests.yaml b/config/crd/v1/bases/velero.io_deletebackuprequests.yaml index 582552478..65e895d25 100644 --- a/config/crd/v1/bases/velero.io_deletebackuprequests.yaml +++ b/config/crd/v1/bases/velero.io_deletebackuprequests.yaml @@ -11,6 +11,8 @@ spec: kind: DeleteBackupRequest listKind: DeleteBackupRequestList plural: deletebackuprequests + shortNames: + - dbr singular: deletebackuprequest scope: Namespaced versions: diff --git a/config/crd/v1/bases/velero.io_downloadrequests.yaml b/config/crd/v1/bases/velero.io_downloadrequests.yaml index 3b1f3f416..500158e5b 100644 --- a/config/crd/v1/bases/velero.io_downloadrequests.yaml +++ b/config/crd/v1/bases/velero.io_downloadrequests.yaml @@ -11,6 +11,8 @@ spec: kind: DownloadRequest listKind: DownloadRequestList plural: downloadrequests + shortNames: + - dr singular: downloadrequest scope: Namespaced versions: diff --git a/config/crd/v1/bases/velero.io_podvolumebackups.yaml b/config/crd/v1/bases/velero.io_podvolumebackups.yaml index 2e7fe7056..3f4c83deb 100644 --- a/config/crd/v1/bases/velero.io_podvolumebackups.yaml +++ b/config/crd/v1/bases/velero.io_podvolumebackups.yaml @@ -11,6 +11,8 @@ spec: kind: PodVolumeBackup listKind: PodVolumeBackupList plural: podvolumebackups + shortNames: + - pvb singular: podvolumebackup scope: Namespaced versions: diff --git a/config/crd/v1/bases/velero.io_podvolumerestores.yaml b/config/crd/v1/bases/velero.io_podvolumerestores.yaml index e2917ead2..015d143fe 100644 --- a/config/crd/v1/bases/velero.io_podvolumerestores.yaml +++ b/config/crd/v1/bases/velero.io_podvolumerestores.yaml @@ -11,6 +11,8 @@ spec: kind: PodVolumeRestore listKind: PodVolumeRestoreList plural: podvolumerestores + shortNames: + - pvr singular: podvolumerestore scope: Namespaced versions: diff --git a/config/crd/v1/bases/velero.io_restores.yaml b/config/crd/v1/bases/velero.io_restores.yaml index c41fe88de..89f4baff8 100644 --- a/config/crd/v1/bases/velero.io_restores.yaml +++ b/config/crd/v1/bases/velero.io_restores.yaml @@ -11,6 +11,8 @@ spec: kind: Restore listKind: RestoreList plural: restores + shortNames: + - rst singular: restore scope: Namespaced versions: diff --git a/config/crd/v1/bases/velero.io_schedules.yaml b/config/crd/v1/bases/velero.io_schedules.yaml index 4b13ecec7..7ec1b6025 100644 --- a/config/crd/v1/bases/velero.io_schedules.yaml +++ b/config/crd/v1/bases/velero.io_schedules.yaml @@ -11,6 +11,8 @@ spec: kind: Schedule listKind: ScheduleList plural: schedules + shortNames: + - sched singular: schedule scope: Namespaced versions: diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 44d2b378c..43c054c50 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -29,15 +29,15 @@ import ( ) var rawCRDs = [][]byte{ - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8adɡS\xf7\xe7\u074b!%[\x96dk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L8\xf5\x88>(k\x96 \x9c\xc2?\b\r\x7f\x85\xf9\xd3/a\xae\xec\xe2\xf0z\xf6\xa4L\xb9\x84U\fd\xeb5\x06\x1b\xbd\xc4w\xb8SF\x91\xb2fV#\x89R\x90X\xce\x00\x841\x96\x04o\a\xfe\x04\x90\u0590\xb7Z\xa3/*4\xf3\xa7\xb8\xc5mT\xbaD\x9f\x8c\xb7\xae\x0f?\xcc_\xff<\xffi\x06`D\x8dK\xd8\n\xf9\x14\x9dGg\x83\"\xeb\x15\x86\xf9\x015z;Wv\x16\x1cJ\xb6^y\x1b\xdd\x12\xce\aY\xbb\xf1\x9c\xa3~\x9b\f\xad[C\xc7t\xa4U\xa0\xdfF\x8f?\xaa@I\xc4\xe9\xe8\x85\x1e\v$\x1d\ae\xaa\xa8\x85\x1f\b\xb0\x83 \xad\xc3%|\xe6X\x9c\x90X\xce\x00\x9aLSl\x05\x88\xb2L\xb5\x13\xfa\xc1+C\xe8WVǺ\xadY\x01_\x835\x0f\x82\xf6K\x98\xb7՝K\x8f\xa9\xb0_T\x8d\x81D\xed\x92l[\xb07\x156\xdftd\xe7\xa5 \x1c\x1a\xe3\xca\xcdϱ~9:\xbc\xb0r.\x04tβ\xc5@^\x99jv\x16>\xbcΥ\x90{\xacŲ\x91\xb5\x0e͛\x87\x0f\x8f?n.\xb6\x01\x9c\xb7\x0e=\xa9\x16\x9e\xbc:\xed\xd7\xd9\x05(1H\xaf\x1c\xa5\xe6\xf8\xbb\xb88\x03`\aY\vJ\xeeC\f@{lk\x8ce\x13\x13\xd8\x1d\xd0^\x05\xf0\xe8<\x064\xb93y[\x18\xb0ۯ(i\xde3\xbdA\xcff \xecm\xd4%\xb7\xef\x01=\x81Gi+\xa3\xfe<\xd9\x0e@69Ղ0\x10$\x14\x8d\xd0p\x10:\xe2\xf7 Lٳ\\\x8b#xd\x9f\x10M\xc7^R\b\xfd8>Y\x8f\xa0\xcc\xce.aO\xe4\xc2r\xb1\xa8\x14\xb5C)m]G\xa3\xe8\xb8H\U000e5d91\xac\x0f\x8b\x12\x0f\xa8\x17AU\x85\xf0r\xaf\b%E\x8f\v\xe1T\x91\x121i0\xe7u\xf9\x9do\xc68\\\xb8\x1d\x00\x9dW\x9a\xa4;\xe0\xe1\xd1\x02\x15@4\xa6r\x8ag\x14x\x8bK\xb7\xfeu\xf3\x05\xdaH2R\x19\x94\xb3\xe8\xa0.->\\Mev\xe8\xb3\xde\xce\xdb:\xd9DS:\xab\f\xa5\x0f\xa9\x15\x1a\x82\x10\xb7\xb5\"n\x83\xdf#\x06b\xe8\xfafW\x89\xb8`\x8b\x10\x1d\x8fN\xd9\x17\xf8``%j\xd4+\x11\xf0?ƊQ\t\x05\x83\xf0,\xb4\xbat\xdc\x17\xce\xe5\xed\x1c\xb4Tz\x05\xda>=n\x1cJF\x96\x8b˪j\xa7d\x9e\xa9\x9d\xf5 \x06\xf2\x97\x95\x1a\xa7\x00^\x99D7d\xbd\xa8\xf0\xa3\xcd6\xfbBSm\xc7\xeb혡6b\xa6\xad\xcc\t8.8b\x90\xf6\x82:d@B\x99\x13\xa7\x8c&y\x03\x99\x84\x8e`\xa60\xc2H|\x9f\xfa\xd1\xc8\xe3D\xa2\x9fFT8\xa5\xbd\xfd\x06vGh\xbaF\x9bXG2\xd9\"\xf8h\xee\n\xf6\x9c\xe3ʚ\x9d\xaa\x86\x81v/\xb2k\xe0N8\xe9e\xbb\xee\xf9\xe4L\xb9\xb9α\x14m\xe71 ;UE\x7f\r\xbc\x9dB]\x0e(\x04\xc0D\xad\xc5V\xe3\x12\xc8G\xbcR\x91\xc1\xac\\V\x84\xef\xc7\t\xe0\xd6\x17\u00a0L\xc9\xd3\xd2\\V\xec\xa4mFn\x7f4%\xf8\xcbgJw\xa1\x89\xf5\xd0]\x01O\xd6)1\xb2\xef1\x90\x92#\a\xaf^\xdd\xd7\x01l\xe6C\xc9t\xb4S\xe8'2~Ǽ\xcd9\x0e\x1b\xf0\x86\x93\x03?~\xf0\xf4\\z\xc9\xdc?^\x9a\xe8N|\xdeH3\x9bi\xa6S\xe6v\xa4ÈIg\xcb&\xb2F/\xf5\xe1\x1d\xf3ó\xaa<\xf6\xae\xceb\x9c\xecz2c4\xd1\x13\xe9U\xedYlO\x82b\xb8\x87\xef\x93B[M\x19\xbdO\xf7i\xde\xe5gԋ\x19_\x8b@\x1db\xe3G\xed\x04\xee\x1f\x87\x1am`l\f\x887\x18\xdan\xf1Fp\rQJ\xc4rx\xc5\x03\xe3[\vʏ\xe7\x82\xed\xbd\x8c9Ɖ\x1fC\x10\xd5T\x92\x9f\xb2T~=5* \xb66\xd2\x15\x04h?\x96\xe3mT&\"u{\x11\xa6\xe2|`\x99\xb1\xbe\xe8]\xb0\xb7B\xb8Fi\x9f\xf1\xdb\xc8\xee\x1aE9\xa4\xc5\x02>[\x1a?\xba\xc9j\x12M\xb7\x99&\x89\xbc'ϙ_`И\x1c\xf4\xdf0kEX\x8f^\x91\xd7g%/ik\xa7\x91\xf0\xf4\xff7.\xd6\v}\xd5\xd7:\x81\x96\x0f\xf8y\x94&\xe7j/\xb5%\x9bJ,\xaf\xe9\x11\xcakb\x90\xf2\xba\xf9j\x80[C5R\x89{G\xebj)2\xdc\xcf+\xc7d\x06\x1eC\xd4\xf4\xac\x04\xd6I\xb4\xc5/+\x9e\xdb\xefy\xf1\x8c\xcf\\^\x05lZj\xbc*\xf1^(}\xf5x2\xd9@\xc2\xd3}\xfd\xbb\xb9P9\xfd{\xf0n\xb7o\xff\x97\xfdy\xe3\x1d\xd9\x1e\n\xef\xc5q\xfa\xea\x1el\x06\xfe\r.;\xc1\x85\xfc\x9c\xe8\xee\xc4\xed\xe9/\x7f\t\x7f\xfd3\xfb7\x00\x00\xff\xff\x96֥5\xef\x13\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfe\x15\x84\xeeav7\xba\xe5u\xdcG\\\xe8\xcd#\xdb;\x1d3ck-\x8d\xf6\x99\xae\xca\xeefDA\rP-\xf7\xde\xdd\x7f\xbf \x81\xfa袪\xa8VK\xe3\xdd5/\xb6\xba !?I\x92\x04\x96\xcb\xe5+Z\xb2{P\x9aIqEh\xc9\xe0\x8b\x01a\xffҗ\x0f\xff\xad/\x99|\xbd\x7f\xf3ꁉ\xfc\x8a\\W\xda\xc8\xe23hY\xa9\f\xde\xc1\x86\tf\x98\x14\xaf\n04\xa7\x86^\xbd\"\x84\n!\r\xb5?k\xfb'!\x99\x14FI\xceA-\xb7 .\x1f\xaa5\xac+\xc6sP\b\xa2\x1d\x8a\x1cB\xb5\xc4[{:\xae\x0f\xf81\xa6\x06\xae\xac6-\x88L\x93\x8b\v\"\x15\xb9p\xae\xc9\xc5µ\xae\x187K&\xda}<2\xceC/\xf3\x90w4t\f\xd5w\xf2\x83v\xcat\x12-\x06`\xb5H\xf3\xb8\x03\xb3\x03EJY\xbb\x02\x1bƁ\xe8\x836Px\u0084\xe9\xd5\xe3\x13\xe9\t\x8d\n\xe7\x1e\x84\xb6t\xf5\x88\xf4\x91\x17\x15\xe7t\xcd\xe1\x8a\x18U\xc1\x00m\xd6Rr\xa0b\x828\x9fA\x1b\x96\x9d\x834\x0eR\x840\xca\x7f\xe8P\x00\xbd\t\xfa\x00\x84F@{\x9aY\xb7\x85\xf3\x16a\xbbT\x89\x8e\xa9T\x90\xd9\xe9\xec\xcaO\x93\f8N\xcdB\x12.\xc5\x16\x94\xeb\xddZ\xbd `\n\xac\xc0\xe5\xc4\xce@\n\xb8\x9dfɦ\xb2\x93\xd3%\xb1\xda=(\x03Lh\x034\"\x9cO\xe0\x0f|\xb1\xd6\x19\xf2k\xe7\x91\xdeZ\xc7:\x0fˉ\xdet\x92§\xf7\xa3\x10\xbd\xdb\xc2Y\x86ޱw\x84\x97\xe8\xd0\xc7Ĵ\xf1^\xecԄk\n\xcbJ?\xec\xc6-\x19\xb5\a\x1a\x8cmt\xf1\xa7\x8b\x05r\xb8\xdbk\xb7\x0fM\xa8\x82\x9a,\xc9v\x13\x8a\xd2\x1c\xfa\xb5\x99\x81\"B\xc5Q{\x92\xc8O\xaa\x14=\fp\xb3^\x18\x9d\x91\x9fC0\x8f8*B\xb5\x17\xe6\xe9q\xbf\xff\xcc\\=\x0f\x1f5\x86\x01(\x13\x96\x7fvE\xdea\x9fv\v[K6!M\x04\x9e\xf3\xeb \xc75\xeb\b\xb7~'b\x9dE懄\xbc\x96-/\xbc\xff\x90\x94\xdaI\xf90E\x9d\x1fl\x9df\xb5H2\f7\x915\xec\xe8\x9eI\xe5Qo\xa6Z\xf8\x02Ye\xa2ZO\r\xc9\xd9f\x03\xca\xc2)wT\x83v\xf1\x83a\x82\f\xafkHˌD?\x1e\xe1\xd10Ҳ\t1\x1f\x1a\xba\xf5#\x8eg\xc9P\xec@\xad{\x8d\x93q\xce\xf6,\xaf(\xc7y\x99\x8a\xcc\xe1C\xebqŬ\xcc\b\x93{c\x8eJ\xa6+\xce!\bHY&u\x96\x90R\x80\xf5y\v\xbb&\xe8W\x1d\xc6|M\xad\xaf\"\x87\xb0'\xc8,Uqо\xab\x1c\xdd\xc8\xc6f,\x1a\xa6`\x84\x86p\xba\x06N4pȌTq\x8aL\xf1ٕ\x14#8@Ȉ\xe5\xeb\xae4\x1a\x04F@\x12\\\xc2\xedX\xb6s\xae\x9e\x15\"\x84Cr\t\xd6\xe13\x84\x96%\x8fL\x17M\x19e\xbe\xefdLכ2\xa1\xf5\xc7\xf0b\xfaߔ\x04\x9bٔ(i\x1b\xfd\xeaR\xb6\x16\x87\xf8\x9a\xb6)\xff\x9c\x84\r\x96\xff\x04\xa1\x1d\xd1~\x82\xe1\xb2d\x99\x1e\x94[KU\x06\xfaҺS\xe8\xe9,\b3\xe1\xd7)M\xe8\xf8\\\xbd(b\x87\b_7o\xe6\v}\"kRt\xe2\x99\x18Sw\xf1\x0f\xc8\x17\x9c2n\xfd\x8c\x91̓\x9fڭ\x16\x84mj\xa2\xe7\v\xb2a܀:\xa2\xfeI\xa6>p\xe6\x1c\xc4H\x99\xf5\b\xeek\x98l\xf7\xfe\x8bu\xc1t\xb3\x85\x97H\x97\xe3\xc6Α\r\xde~wz\x9e\x80K0\xbe\xcf\\\xb4U_⊩\xfd\v\xbaVo?\xbe\x8b\xaf\xaf\xda%A\xf2z\x88L(\x9d+o\x8f0j\x8fϻ\xf0\xe1\v\xfa@\xf5\x02\xc8Ū\x17\x84\x92\a88ׅ\nb\xf9CC\xe5\x84\xee\x15\xe0f\x15\xca\xd9\x03\x1c\x10L|\xf7\xa9_R\xa5\xc1\x95\a8\xa4T;\xa2\xa1\x1d\x13\xd3~W\xcd\xd2\xc9\xfe\x80\x84\xc0M\x87T1pūBd\xaf'^\x12mI(\x81\xf6'\xa0\x99$*\xed>\xda۷(\x01\xdfi\xc7K\xab1;V\xa2Yň\x83\xdc$3ԕ{\xcaY^w\xe4td%\x16\xe4\xa34\xf6\x9f\xf7_\x98\xf6;\xbc\xef$\xe8\x8f\xd2\xe0/\xcfBQ7\xf0\xe7\xa4g\xd8\xf1\xb1\b9+o\t\xd6ޣts\x9a\x95\xb6\x9a\xf6L\x93\x95\xb0\xcb\x15G\x92Įp;\xdau\xe7:**\x8dۋB\x8a\xa5\v\xdb\xc4z\xf2\xf4\x96\xaaC\xee'w\xea;\xbc\xb3\x93\x85\xfb\xe26\xc59\xcd \x0f\xdb5\xb8[K\rlY\x96\xd8_\x01j\v\xa4\xb4&\x8d\xcf\xd2\xealB\xad \t\x93U\a6s\x87\xab\xa6\x10\xe5\t\xe4\xc0Y\x1c]\x9cI\xee\xd2<\xc7\x04 \xcaof\xcc(3da\xaeih\x8d\xddM\xc1\x05ŭ\x96\xff\xb13-j\xd3\xff\x91\x922\xa5/\xc9[\xcc\xf5\xe1\xd0\xf9\xe6\x83f-0\t]b\xae\x8e\x95\x9f=\xe5v\xee\xb7\x06\\\x10\xe0\xce\x13\x90\x9b\x9e_\xb4 \x8f;\xa9ݴ]o\xe2\\<\xc0\xc1\xed\x18Nv\xd962\x17+q\xe1|\x88\x9e\xc1\xa8\x1d\x0e)\xf8\x81\\\u0dcb\xa7\xb8R\x89\x92\x9aX\xad#\xa2\x05-\xd3$\x14s\xadR\x1du\xbb`\rN\x88mX\xe7\x10Y'{\f\xdb$\x11-\xa5\x8el\xe4\x0f\feBxo\xa46.^\xd6\xf1\x99\xa3\x015\x19\x82h\x84n\\b\x97T!\v\xc7\x1a\xe5\xa9\xd0o\xbb\xdc\xed@\x83߯\xf0\x819\aԮ\xec.\x1a\xfdv\xd6\xfe\xc2\xed\x97`'4C\x8f\x05ۖJf\xa0\xa3{\xd9MI\x98/\"Y\"m\xdc\xeb\x98#u\xab$\x97\xab2\x1e\x02\r%\xdd嵄\x98\xb9^x\xff\xa5\x15\x10\xb5\xbao\xff\x9e\x92\xb1\xb9\xe3\"\x98KY\x14\xf48\x7f+i\x88\u05eee\xd0\x06\x0f\xc8->ԶBK\x90:\x97\xd7\x02\xf858\n\x05\x13+쀼y\x06\xc7\xc2\xdb\xd0X\xb2I\xac\x9c\xe6\xca^\x87N\x1a\xee\xd4?8U.%n\x15(\xe80\xaf\x1fUG?TH\xd3\nH\xccp7K\x99\x7f\xa7Ɇ)m\xdaC\xd0\x03i*Q03\x17^\xe2\xbdR'\xad\xbb>\xb9\x96G\td>o\xcd\x11&\x11s\xdc_\x02\xc26\x84\x19\x02\"\x93\x95\xc0\x00\x8e\xd5c\xec\xc2\x11\xd7YX\x96\xaa$i\xdaO\x06s\xd0be\x89\x92\xc2\xc4h\xa4\xa7]\xfd\x03e\xfdD\xb5X\x99\xc963\x94\xc5\x16+\xa7\xe9DHqkg*\x16\xf4\v+\xaa\x82\xd0\xc2\xf2\b'sV@\x97\xe9M\xe2\x9bm\x81ӄ\x91VcJ\x0e\x06|\xf2Z\xe2\x182)4ˡ\x9e\\\xbd HA(\xd9P\xc6+\x95h\x01g\x91w\xceR\xc4[\x82\xf3\xad1\xd2:_\")\x12\xa2\xb9\x89\xbe\xe2\xb85.U\xba\xc77\xe5f)\x98\xefe\x95\x8aIL\v<\xb3\xa3\xe5\x13)\xa98|\xf3\xb4R\x87\xfa\xcd\xd3\x1a+\xdf<\xad\x89\xf2\xcd\xd3\xfa\xe6i\xa5\xd4\xfc\xe6i}\xf3\xb4\xda\xe5_\xc2Ӛ\x1a\x91;\xe88\xf0qr\x14\t[\xd5cC\x1c\x81\xef\x93+|\x0e\xf8\x93r1WqP\x91\xc4\xff\x81\xb4\xee\x98\xd1j&\x8f:9\xd3jM\x90yw\xeej\u0095|B\xd6}\xe8\xf4|Y\xf7\xabQ\x88gʺ\xf7Þ\xf6\xb1Oʹ\x0fD\x99\x97\x9d\xbd\xf0\x89\x1a\x05\xd0\x10Vw\xdb\xf01\xbc\x86$d\xa2\xff\x17N\xcc\xede\x8d\x9dQ>\x9e=\x8b?YF\xa2,\xbd\xf8\xd3\xc5\xd7G\xfe\xf3\x10|\x90\xc4}\xda\xf9\x83\xdf\x11\xa8v\x05\xdaN\v\xebf\xe1}\x9db|\x16\xb9M\xcdį\x89\x18\x81\xd5\x15\xc9#*~\xad\xb6\xc0@\xf1\xa9\xf43\xd2\x13N\xaa\xae\"p\x92ΪR}\x10\xd9NI!+\xed\xa3\x12\x16\xd6\xdb̝\xf4\x0f c\xc2\x1a\xd5\xf0\xff ;YE2\xc1G\xc87\x91\x118\x8d|'9\xd0oB\x83\xa1\xfb7\x97\xdd/F\xfaT\xc1\xa1\xb3͏;\x10\xb8\xc3.\xb6\xed\x03\x00\xe1\xa2\x06\x7fc\xc1\xb1\x80E\x00IE\x04\xe3N\xf2\xeak\x1e\xdarG>\x95.\xf64\xdb\xef\x18\x8f\xa9\xa4%\x13\x9e\x9cB\xd8M\x11\x1c\xf0K\xe7\xeev\x9f\xe5\xc8\xc4\xef\x92\x1a8?!0%\"6\x91\xfcwB\xca_bn\xf1\x93\xb7\xe7S\x92\xfa欘\x9f-\x81\xef\xfci{I\xf4\x99NћC\x9dgO\xc7{\xc1$\xbc\x97I\xbdKL\xb8;_\xe6|Z<\xf6\xa4̱\xe9\xd0\xc1p\xd2\xdcd\xaa\xdcdha\n\xb1\xd9(M\xa6\xc0\xcdI|\x9b\xe4N\x9a\x9a\xbdXjۋ%\xb4\xbdl\x1aۨ\x14\x8d~\x9c\x93\xa8\x16\xbf\xaf\x87LN\xb6\xfc\xa5\x84\xedT2H\xd5q_OZ_}:\x82a\x19\x1f\\\xbb\x17\U000912ca\x1bVr\xdcHݳ<\x1al0;8\xd4\x17h\xfc*\xf1詿\t\xe6\xd3\xe7Zj/\x8f<}\xaa\xc9#pNhL\xafz\x98g\ue2aaL.\xc1\xceGV;\xfd\xc5 \xfe^\xab\x85\x13w<]\x8b\xb3Z\x11\v1Q1|\x8b\xcc\xe0đboz\x1e\xac\xf3\xc3\xf1\xb7\xdf*P\a\x82\xf7\xd8\xd4~Ns\b\xcc+\xa6\xb6\v\xb1`*\xbc\xd9\x1a\x8a\x9f\xf7\x9c\xfeF\x95\xc9[\xe1f\xdd\xe3\xf1`\x1bk#\x9aE\x8d5|\"vq\x13\t\n\xd6o.d\xdd:\xd2l\xcaAN=-\xf5\xbcK\x9c\xf9\x8b\x9cI\xaf\"\xdd\xf3\xfb\x9dNA\x9dr\xfa)-\x01`\xf2\xb4\xd3s-y\xa6\x16=\xc9~^\xdai\xa6y\x9b\x85\xcfxz\xe99N-%R*\xe5\x94\xd2<:\xbd\xc0\xa9\xa4\x17=\x8d\xf4R\xa7\x90\x92O\x1f%\xa5\xb8$\xef\x02\xa7\xa6\xa8\x9cx\x9cfz\x8fw\xfc4Q\xc2)\xa2\x84\xdd\xdfi$O@/\xe1\x94м\xd3A\t\xa9R8*\x7f)k\x9b\xee@\x8e\xf6;\u00ad\x7f\xb6V\xc7_\xc6\xe9\xc1\xdf\xc0\x8aw\xed\x0em_ZIky\x1b\x9d\xbd\xa8\xc6\xfd\xe9:\x93\xfe\x02^\xb7]\xa5\xa1\xa4\n/u^\x1f\\:Ktj~O\xb3\xdd\x11\xf4\x1d\xd5d#UA\r\xb9\xa87\x00_;\xe0\xf6\xef\x8bKB>\xc8:'\xa2}/\x8ffE\xc9\x0fv\x85B.\xda\rN\x93\x80\xa8\xb4\x85\xden$gY\xc4w\x8b\xde\xcd\xe4*\xf7.\xcb\xc0\x1b\xa3\xb2v\xca@i+\xc6]7t\xf3\xbaW`n$\xe7\xf2q\xe6ڟ\x96\xec/x\xa5\xf9\x13\xa2CooV\b#\x88\aޑ^'g\xd5ج\xc1N\xcb\r\x9eC\xba\xbf\xdat v\xf3\x1c۷\x06C\xee.\x88\x0en\x817\x9d\x99\xb4\xd6\xe5f\xe5\xc61ԋ\x95\x19*\x0eDbF\x8d\xd91\x95/K\xaa\xcc\xc1%j,:c\bs\xe9Xtgp\xf6\xe8_z\x1d%o\xb8\xeb\x1aw(\x0few\xd3\xf7\x98v\xa7\x8cc\xf8\xf4\xe2\xe4\xb9\xc53\x8ec\xd8-Y\"\xa5\"?G3\xbf\xce\x165\xd3\xfef\xe2\x9f\xe5\x1e\xdeE\xa3g\x1d\xf2\xdc\x1eU\x8f\xa4g\x05\x88\xee\xd2\xdd\xc1,\xd55\xe0\x85\xbc\xfdOOȷ\n]\xfb;UO\t\x94\xddvAD\xf0\v7̆\xceb\xf6\to\xc6?\x90\x9b{\\\xa3զͫ\xa8_\xa3\x85PY\xd8\f\x8e\xc0\xf1\r\xbe?\x7fj\x9a6R\xd1-\xfc$\xdd\xe5\xe3Sl\xef\xd6\xee\\J~\x90?\x1a\x94&v\x01\xaf\xbf\x06\xfd\bX\x93\xf3ݻ\xd4؎r\xe65\xcd\xc6\xf0S\xf8~w\xf7\x93\xc3ʰ\x02.\xdfU.\xdd\xc1\xdaD\r\x96\xc4\x01[\aim\xff\xbb\x93\x8fx\xf9o<\x8e\x19\x1e\x93h\x90Q\x80\xc9昂8\v\xa5\xaa\xe4\x92栮\xa5ذ\xed\x04v\xbft*\x1fM\xb3\x19\xfe葫\xe7\xa8\x00\xff\xcc9\b\xd6\xe7\xe1\x1c\xf8\a\xc6A\xbba%\x18\xe0\x9b~\xab\xda\x1eW\xc5\xda\xf9p\x1b\xfb\xb1\xee``\x8esha(\xba\x04e\xbd(\x17\xb4\xaet\x90\xd5a\xc4\x1b\x8e0a`\v\xfdU\xe0\x88\x05v\xb7J\xe3\xf4\x19\xcc\t\xaee~\x8cŷ:\xc8\xdf\x0f\xb7<\xe2d+\xe4\x15\xbbq\xcf9!7\xf7ךT\"\xc7p\xf1\xfd_ngIݾss}\xd0\xd6)\xa3z\x1fo\xd5r\x8e[\xf6\xc2y\xc7r\x13A`\bN끔Gf\xfc\xc5]\xe7\xbdiuh\xc93\xf4\xf4\x03^\xe9?\xfd\xf8\x83\xbb\xf9\xdf?\x19\xe3ձRxM\xaa\x7f\x15\x00\xaf\x15}\xc2\xfb\x0f\x9d\xe4/\xfd\xd6\x18(J\x13\xf35\xa6\xcd\xe1\xf7c\x00k?M\x1a\xca[ZIC\x85\x98\xa7\xad\x0f\"\x1bK,\xf3\xd6h\x84\x9bc\xfa\x18#\xc0\xb5?\x0fq6\x02\xd4\x00\x87\b\xa0\xab,\x03\xad7\x15\xe7\x87\xfa8\xc6WB\x8d\x0f\x94\xf1\xf3\x91\xc2A\x1b\x14\x04\x8b\xde(\xa4I\x84}\xba7\x88\x1bR\x1bZ\x9c\xf4`\xc3u\x1f\f\xbee\xa4\xf2VR%\xad\xc7Nu\xc3\xfe\xd8\xe4Ҁs-q\x91e\xa1AN`\x0f\x82\xd8\xd9ّ8<\xc65\x13\x8a?\xe1\xeaf\xb80߅PH\xf4\xc5&\xe2\xa3\x1d\x1a_\x06\xfaN\xd701W\x14\xdf3\xe9\x13\xa1\xef\xfc\xbahŕ\xf5\xfeaiA\x9c\xe6\xb5\x0e\xbd\xe6ҝ\x17\x9ef\xe4\xaeoWC\xe0N1q\xfd\xe7^\x9e\xa8\xc6}t\x9fd\xd2\xfa\xe8\xce2h\x11\x88\xb5\x8c\x9f\x1fwT\xf5\xd3.uǖ\xce\xe1\xc8\xc2\x19:ʹ?\xe8X\x80\xd6t\x1bns\x7f\xb4K\x8f-\bp\xe19\xb7y\x12\x01ڜ\x8a\xeb\xdee\xeeT\x86f\xa6\xa2\xbe\x83\x90\xe0۪\xf5\x9d&\\Ơ\xe2\x83.,<\xa1\x16\xd6d3\t\xf5\xa5d*e\r\xf7\xbe\xaehi\x83\x9e0r\xa7y\xf4\x0e8\xdb\xe2\x93N\x96s[\xaa\xd6t\v\xcbLr\x0eh\xad\xfb\xe3zN]\xf7g\x0f?\x03Փ\xa8}h\xd7\xf5;\x80\x8e\xdbn㛺tw|\xd6\xcc0\x05\xcd\v\x83\xbd\x01I\xecx\x96\xa3\xec\xa8\x10}~\xaf?\xd2vݠu\xde,\xfb8\xaf\x7f}oѼ\xa8\x15\x19gA\x7f\x95jA\n&\xec?T\xe4n\x03/4\x9e5\xfe\x9d\x94\x0f\xb7\x11'\xb67\xf8\x1f\xea\x8a\xcdV\a\x13n\xd8x`t-+\xbf\xfb^;\xb4\xf1m\x15\xbc\x99\xff\xcc\xcbM\x8492\x1f\xf4\xd0\x19\x8c\xe8\xfeЁ49\x15\xb8\x9e\a`݆'\xde8?,\x8e!\x1f='\xd9\xc0n\xbd\\\xe0݀\xe6>\x82\x81\x8e\u008eT\x14H}\xf1E۠\x9f\xb2\xea\xf5d\x1er&{4\xfe\xa1\xa9=DG7̖\xbb7\x80`\xc7\t<\xef\x82\x1d\x9f\xa9\x98\x10\xfe\x1b[\xa7\xbe\xbb\xa0\xb5p\vYb\x83Q\xba\xa1\x97\xee>B\x7f\xbbbI\xfeZA\x15\xa1\xc12<\fwk\xa8\xea\x87|\xdd1x\xc81\xa3\x03\xb51Re%n\x94\xdc*\xd0}a]\x92\xbfQf\x98\xd8~\x90\xea\x86W[&>\r\x1f\xf9\x19\xab|C\x95aV\xd8\xddxb\x03e\x82r\xf6\xf7\x98]k\x7f\x9c\x06t=\xb8\xc0Z\x92\x84a\f}x\a\xd6\xc7\x1d\x8c\vDMh\xe9\xe9z\x8a\xbf\x12x2eSk_\xa2\xf1EB\xb7\x97䣌\x1a\x06\x9f\x0eź0\xadK\x06\xda,a\xb3\x91ʸ\xdd\xea咰M\b>X\x9b\x83q3\xf7\x88'a\xb1m\xe6:Ѥ\x99\xbe0\xe8\xadp\x16ƫ\xec\vzp;S4\xcb*\xeba\xbdֆ\xf2\x88\x83\xf3$ÏQ\x9e\xef\xf1\xc1\xca_\x9e\xb4\x93\xb7j\x03\xea\a\x1d\xb1\x1fGR\xbcL\xc3y}ܢ\b\x82<*f\x8c\xf5\xa9\xe4H*\x81'\x95\xb1\xbe\x15\xe7D[R\x9f\x14}$Ό\xae\x86Sr\xd2P\xbe\xab\xa1\f\x99g\x8f5\xbe\xccX\xbf\n곏|-\xcb\xe6lG\xc5v\xf0\x86\x82\x9d\x92\xd5v\x17$y\xc0\x99&y\x05\x18\xacE\x93\xa2Ë˦R\xa2\x95J0r\xec\x9b\x04a\xc0\xe1\xd2\xec\x01\xdf/u/\x1a\xfb\a\xab_\xfb7P\x96\x1b%\x8b\xa5\xef\x17c\xa9\v\xbf\x93\xaf\x98\xb4\x9e\x8b\xd9E\xa9N\x9c\xd7\xee\x9f\x19@I(K\x10\x84j\xdfs\xc2MQ'OS\xbf٩\xe1Fj\x96\xe0\xedG9\xfe\xd76\x80\xc0\xf02\xfc\xdde\x86_\xc1`\x9f1<>\xf9#\xf8\xb0\xa7¸\xe5D=E^\xb8I\xecb\xd6BFۉ\xedIA\x9a\xdb\x0e\x84\x89\xf8\fv\x17gѭO\xd7p\x17\x81]\xfb\xe7Wk\xc0\v\xa2\x99\b/\x82\xbb\xd4\x0f'\xfdѝ@\x81\x0fUJ\x15\xcf\xc6\x1c\x0f\xb8t\x11z\xd9X˾\xf6$ޟ\xbc\x14\xbf?\x82qt\xa8\x1b\xdf%\xad\xab\x84\xe5\xf3\x1fXl?\x00\xd3x3\x8b\xca\x1f\x7f\xf7\xc3\xda\xfb\xa4\xa5^\x9c\"c+?\\\xd4\r/\xe1\xba\xef\x90\xdep\xb0ڦ\x01\xba\x8b\xcaY:\xb7?c4휡\xb4\xf0\xf6\xfdybI\xfb3\x06ў-\x82v^\x94\x1f)>\x10}\x92\xd6\xfeͷ\x8d\x84\xd0<\xd8s\a\xd1Z1\xb40\xf0\x17\x8d\xa2E\xe7\xdcޏh\xa7\xf3\x96\xb5\xf0=\xf9_\xfe?\x00\x00\xff\xffY\xa1\x05sу\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8adɡS\xf7\xe7\u074b!%[\x96dk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L8\xf5\x88>(k\x96 \x9c\xc2?\b\r\x7f\x85\xf9\xd3/a\xae\xec\xe2\xf0z\xf6\xa4L\xb9\x84U\fd\xeb5\x06\x1b\xbd\xc4w\xb8SF\x91\xb2fV#\x89R\x90X\xce\x00\x841\x96\x04o\a\xfe\x04\x90\u0590\xb7Z\xa3/*4\xf3\xa7\xb8\xc5mT\xbaD\x9f\x8c\xb7\xae\x0f?\xcc_\xff<\xffi\x06`D\x8dK\xd8\n\xf9\x14\x9dGg\x83\"\xeb\x15\x86\xf9\x015z;Wv\x16\x1cJ\xb6^y\x1b\xdd\x12\xce\aY\xbb\xf1\x9c\xa3~\x9b\f\xad[C\xc7t\xa4U\xa0\xdfF\x8f?\xaa@I\xc4\xe9\xe8\x85\x1e\v$\x1d\x87\xbd\xf5\xf4\xf9쬀\xad\xcf\a\xcaTQ\v?\xd0d\xcfAZ\x87KHzNH,g\x00M\t\x92\x9d\x02DY\xa6\xa2\n\xfd\xe0\x95!\xf4+\xabcmN^\xbe\x06k\x1e\x04\xed\x970o\xcb>\x97\x1eSſ\xa8\x1a\x03\x89\xda%ٶ\x92o*l\xbe\xe9\xc8\xceKA84\xc6%\x9d\x9fc\xfdrtxa\xe5\\!\xe8\x9ce\x8b\x81\xbc2\xd5\xec,|x\x9dK!\xf7X\x8be#k\x1d\x9a7\x0f\x1f\x1e\x7f\xdc\\l\x038o\x1dzRm)\xf3\xea\xf4eg\x17\xa0\xc4 \xbdr\x94\xba\xe6\xef\xe2\xe2\f\x80\x1dd-(\xb9A1\x00\xed\xb1\xad1\x96ML`w@{\x15\xc0\xa3\xf3\x18\xd0\xe4\x96\xe5ma\xc0n\xbf\xa2\xa4y\xcf\xf4\x06=\x9ba\xe4\xa3.\xb9\xaf\x0f\xe8\t╊\ff\xe5\xb2\"|?N\x00\xb7\xbe\x10\x06eJ\x9e\x96\xe6\xb2b'm3r\xfb\xa3)\xc1_\xbe_\xba\vM\xac\x87\xee\nx\xb2N\x89\x91}\x8f\x81\x94\x1c9x\xf5\xea\xbe\x0e`3\x1fJ\xa6\xa3\x9dB?\x91\xf1;\xe6m\xceq\u06007\x9c\x1c\xf8\xf1\x83\xa7\xe7\xd2K\xe6\xfe\xf1\xd2Dw\xe2\xf3F\x9a\xd9L3\x9d2\xb7#\x1dFL:[6\x915z\xa9\x0f\xef\x98\x1f\x9eU\xe5\xb1wu\x16\xe3dד\x19\xa3\x89\x9eH\xafj\xcfb{\x12\x14\xc3=|\x9f\x14\xdaj\xca\xe8}\xbaO\xf3.?\xa3^\xcc\xf8Z\x04\xea\x10\x1b?j'p\xff8\xd4h\x03cc@\xbc\xc1\xd0v\x8b7\x82k\x88R\"\x96\xc3+\x1e\x18\xdfZP~<\x17l\xefe\xcc1N\xfc\x18\x82\xa8\xa6\x92\xfc\x94\xa5\xf2\xeb\xa9Q\x01\xb1\xb5\x91\xae @\xfb\xb1\x1co\xa32\x11\xa9ۋ0\x15\xe7\x03ˌ\xf5E\uf0bd\x15\xc25J\xfb\x8c\xdfFv\xd7(\xca!-\x16\xf0\xd9\xd2\xf8\xd1MV\x93h\xba\xcd4I\xe4=y\xce\xfc\x02\x83\xc6\xe4\xa0\xff\x86Y+\xc2z\xf4\x8a\xbc>+yI[;\x8d\x84\xa7\xff\xbfq\xb1^諾\xd6\t\xb4|\xc0ϣ49W{\xa9-\xd9TbyM\x8fP^\x13\x83\x94\xd7\xcdW\x03\xdc\x1a\xaa\x91J\xdc;ZWK\x91\xe1~^9&3\xf0\x18\xa2\xa6g%\xb0N\xa2-~Y\xf1\xdc~ϋg|\xe6\xf2*`\xd3R\xe3U\x89\xf7B\xe9\xabǓ\xc9\x06\x12\x9e\xee\xeb\xdfͅ\xca\xe9߃w\xbb}\xfb\xbf\xec\xcf\x1b\xef\xc8\xf6Px/\x8e\xd3W\xf7`3\xf0op\xd9\t.\xe4\xe7Dw'nO\x7f\xf9K\xf8\xeb\x9fٿ\x01\x00\x00\xff\xff\x989~\x12\b\x14\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s#)\x92\xef\xfd+\b\xdd\xc3\xecnH\xf6v\xdcG\\\xf8\xad\xc7ݽ\xa3\x98\x99no\xdb\xe3}FU)\x891\x055@\xc9\xd6\xde\xdd\x7f\xbf \x81\xfaPQ*J\x96=\xbd\xbb\xcdK\xb7U\x90\x90\x1fd&I\x02\x8b\xc5\xe2\r-\xd9=(ͤ\xb8\"\xb4d\xf0d@ؿ\xf4\xc5\xc3\x7f\xeb\v&/wo\xdf<0\x91_\x91\xebJ\x1bY|\x01-+\x95\xc1{X3\xc1\f\x93\xe2M\x01\x86\xe6\xd4Ы7\x84P!\xa4\xa1\xf6gm\xff$$\x93\xc2(\xc99\xa8\xc5\x06\xc4\xc5C\xb5\x82U\xc5x\x0e\n\x81\x87\xaew\x7f\xbex\xfb_\x17\xff\xf9\x86\x10A\v\xb8\"+\x9a=T\xa5\xbe\xd8\x01\a%/\x98|\xa3K\xc8,ȍ\x92UyE\x9a\x0f\xae\x89\xef\xce\r\xf5{l\x8d?p\xa6͏\xad\x1f\x7fb\xda\xe0\x87\x92W\x8a\xf2\xba'\xfcMo\xa52\x9f\x1ah\v\xb2zp`4\x13\x9b\x8aS\x15\xea\xbf!Dg\xb2\x84+\x82\xd5K\x9aA\xfe\x86\x10\x8f\x0f6_xTvo\x1d\x84l\v\x05up\t\x91%\x88w7\xcb\xfb\x7f\xbf\xed\xfcLH\x0e:S\xac4H\x95\xff]Կ\x13?~\xc24\xa1\xe4\x1e\xb1'\xca3\x83\x98-5DA\xa9@\x830\x9a\x98-\x90\x8c\x96\xa6R@\xe4\x9a\xfcX\xad@\t0\xa0[\xf02^i\x03\x8ahC\r\x10j\b%\xa5d\xc2\x10&\x88a\x05\x90?\xbc\xbbY\x12\xb9\xfa\x152\xa3\t\x159\xa1ZˌQ\x039\xd9I^\x15\xe0\xda\xfe\xf1\xa2\x86Z*Y\x822,\x10Е\x96\x8c\xb5~=\x86\xab-\x96<\xae\x15ɭ\xb0\x81C˓\x18rOQ\x8b\x9f\xd92ݠ\x8f\xe2g\x7f\xa6\xc2\x0f\xff\xe2\x00\xf4-(\v\xc6\xf2\xbb⹕\xd1\x1d(K\xc0Ln\x04\xfb{\r[\x13#\xb1SN\rhK\x19\x03JPNv\x94W0\xb7D9\x80\\\xd0=Q`\xfb$\x95h\xc1\xc3\x06\xfap\x1c?K\x05\x84\x89\xb5\xbc\"[cJ}uy\xb9a&̼L\x16E%\x98\xd9_\xe2$b\xab\xcaH\xa5/s\xd8\x01\xbf\xd4l\xb3\xa0*\xdb2\x03\x99e\xf3%-\xd9\x02\x11\x118\xfb.\x8a\xfc߂x\xe8N\xb7fo\xc5V\x1b\xc5Ħ\xf5\x01g\xce\x04\xf6\xd8I\xe5\x84сr(6\\\xb0?Y\xd2}\xf9p{\xd7\x16T\xa6=SZ\xf2:\xc4\x1fKM&֠\\\xbb\xb5\x92\x05\xc2\x04\x91;QE9\xe7\f\x84!\xbaZ\x15\xccX1\xf8\xad\x02m\xe7\x80<\x04{\x8dډ\xac\x80Ten\xc5\xf8\xb0\xc2R\x90kZ\x00\xbf\xa6\x1a^\x99W\x96+za\x99\x90ĭ\xb6\xce=\xac\xec\xc8\xdb\xfa\x10T\xe7\x00k\x9db\xb9-!\xebL4ۊ\xadY\xe6\xa6\xd3Z\xaaF\xef8\x1dإP|\xeaےiv+h\xa9\xb7\xd2ܱ\x02de\x0ek\x8c\xc9\x1a2\xefvy\x00%\x8cЏ\x17uV\xa5!\xb7\x93\xf6\x912\x83c\xbe\xbe]\x92{TV\xa15*\xadJ\x13S)a\xa5$\xd2\xd7\x17\xa0\xf9\xfeN\xfe\xa2\x81\xe4\x15\nw\xa6\x00\xe90'+X[IP`\xdb\xdbO\xa0\x94\xa5\x8d\xc6\x01Ȫ\xa7ll\xb9ۂ\xa5-\xad\xb8\xf1\xf3\x84i\xf2\xf6Ϥ`\xa22=Q\x1b\xe4:R\x8a\x1aZ\xc8\x1d\xa8S\x88\xf8\x9e\x1a\xfa\xb3m|@;\v\x94 TK\xbc\x95\xa7\xe3j\x8f\x1fc\xdcve\xb9nAd\x9a\xccfD*2s\xb6y6w\xad+\xc6͂\x89v\x1f\x8f\x8c\xf3\xd0\xcb4\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa8\x9d\xf0\x9eD\x8b\x01X-\xd2\x91\x9ep\xeep\xeeAhKW\x8fH\x1fyQqNW\x1c\xae\x88Q\x15\f\xd0f%%\a*F\x88\xf3\x05\xb4a\xd99H\xe3 E\b\xa3\xfc\x87\x0e\x05\xd0h\xd2\a 4\x02\xda\xd3\xccZg\xce[\x84\xedR%:\xa6RAf\xb5\xf6\x95\xb7\x06\f8Z !\t\x97b\x03\xca\xf5n=\x95 `\n\xac\xc0\xe5\xc4*Z\x05\xdcZ\x13\xb2\xae\xac\x0e\xbe vv\x0f\xca\x00\x13\xda\x00\x8d\b\xe73\xf8\x03O\x19\xafrȯ\x9d\xe3uk\xfd\xc7<\xf8\xd3=\xad\x99§\x0fG!z\xeb\xccY\x86N\xa0\xf7\xf7\x16\xe8\xb7\xc6Ĵ1\xd2\xfb\x12\x9cSmY\xe9\x87\xddXߣ\xfa@\x83\xb1\x8df\x7f\x9a͑\xc3\xdd^\xbb}hB\x15\xd4dI֛P\x94f߯\xcd\f\x14\x11*\x1e\xd5'\x89\xfc\xa4J\xd1\xfd\x007k\xff\xff\x8c\xfc\x1c\x82y\xc0Q\x11\xaa\xbd2O\x0f\xfb\xfdg\xe6\xeay\xf8\xa8q\x1dL\x99\xb0\xfc\xb3K\xd2\x0e\xfb\xb4[\xbfY\xb2\ti\"\xf0\x98p\xf0piv\x84[\xbf\x13\xb1\xce\"\xf3CB^˖\x17\xde\x7fHJm\xa5|\x18\xa3\xce\x0f\xb6N\xb3(\"\x19\xc6[\xc8\n\xb6tǤ\xf2\xa87\xa6\x16\x9e \xabLt\xd6SCr\xb6^\x83\xb2p\xca-ՠ\xdd2y\x98 \xc3\xee;i\xa9\x91\xe8\xc7\x03<\x1aFZ6!\xe6CC\xb7~ġ\x95\f\xc5\x0eԺ\xd7h\x8cs\xb6cyE9\xdae*2\x87\x0f\xad\xc7\x15\xd32G\x98\xdc\x1bsT2]q\x0eA@\xca2\xa9\xb3R\x92\x02\xac\xcf[\xd85A\xbf\xea0\xe6+j}\x159\x84=Af\xa9\x8a\x83\xf6]\xe5\xe8F6:c\xde0\x05\x03\x11\x84\xd3\x15p\xa2\x81Cf\xa4\x8aSd\x8cϮ\xa4(\xc1\x01BF4_w\xa5\xd1 p\x04$\xc1%ܖe[\xe7\xeaY!B8$\x97`\x1d>ChY\xf2\x88\xb9h\xcaQ\xe6\xfbN\x8e\xcd\xf5\xa6\x8c\xcc\xfaCx\xb1\xf9ߔ\x04\x9dٔ(i\x9b\xf9եl-\x0e\xf15mS\xfe9\t\x1b4\xff\tB{d\xf6\x13\x8c\n%\xcb\xf4\xa0\xdcZ\xaa2\xd0\x17֝BOgN\x98\t\xbf\x8ë́\x8e\xcf\xd5\v\x96u\x88\xf0u\xf3f\xba\xd0'\xb2&eN\xbc\x10c\xea.\xfe\x01\xf9\x82&\xe3\xd6[\x8cd\x9e\xfc\xd4n5'l]\x13=\x9f\x935\xe3\x06\xd4\x01\xf5OR\xf5\x813\xe7 F\x8a\xd5#\x18\xbe7\xd9\xf6Óu\xc1t\xb3\x87\x95H\x97\xc3\xc6Α\r\xde~\xd7<\x8f\xc0%\x18\xc6f\n\n\f\x8f㊩\xfd\v\xbaV\xef>\xbd\x8f\xaf\xaf\xda%A\xf2z\x88\x8cL:W\xde\x1d`\xd4\x1e\x9fw\xe1\xc3\x17\xf4\x81\xea\x05\x90\xdb\n\x99\x13J\x1e`\xef\\\x17*\x88\xe5\x0f\r\x95\x13\xbaW\x80{2(g\x0f\xb0G0\xf1M\x96~I\x95\x06W\x1e`\x9fR퀆vLL\xfb\xcd#K'\xfb\x03\x12\x02c\xeb\xa9b\xe0\x8a\x9f\n\x91-\x8dxI\xd4%\xa1\x04ڟ\x80f\x92\xa8\xb4\xfbh\xefR\xa2\x04|\xa7\x1d/\xed\x8cٲ\x12\xd5*F\x1c\xe4:\x99\xa1\xae\xdcS\xce\xf2\xba#7G\x96bN>Ic\xff\xf9\xf0Ĵ\xdf\xc8|/A\x7f\x92\x06\x7fy\x11\x8a\xba\x81\xbf$=]\x0f8ф\xd3\xf2\x96`\xed\xad8gӬ\xb4մg\x9a,\x85]\xae8\x92$v\x85\xbb\xae\xae;\xd7QQi\xdcE\x13R,\\\xd8&֓\xa7\xb7T\x1dr?\xbbS\xdf\xe1\x9d5\x16\xee\x8b\xdb\xfb\xe54\x83\x86m\x92\x88\x96RG6\xf2\a\x862\"\xbc7R\x1b\x17/\xeb\xf8\xccр\x9a\fA4B\xd7.\x7fI\xaa\x90lb\x95\xf2X\xe8\xb7]\ueda0\xc1\xefW\xf8\xc0\x9c\x03jWv\xb3f~;m?s\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xecE$+\xa3\x8d{\x1ds\xa4n\x95\xe4R2\x8e\x87@CIwy-!&\xae\x17><\xb5\x02\xa2v\xeeۿ\xc7dl\xea\xb8\b&\x13\x16\x05=LSJ\x1a\xe2\xb5k\x19f\x83\a\xe4\x16\x1fjS\xa1&H\xb5\xe5\xb5\x00~\r\x8eB\xc1\xc4\x12; o_\xc0\xb1\xf0:4\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fn*\x97\x12\xb7\n\x14t\x98\u05cf\xaa\xa3\x1f*\xa4i\x05$&\xb8\x9b\xa5̿\xd3d͔6\xed!\xe8\x814\x95(\x98\x89\v/\xf1A\xa9\x93\xd6]\x9f]\xcbV\xb8k+\x1fCz\x96#L\"渿\x04\x84\xad\t3\x04D&+\x81\x01\x1c;\x8f\xb1\vG\\\xa7aY\xea$I\x9b\xfd\xb6\x80\xa8\x8a4\x02,PR\x988\x1a\xe9iW\xffH\x19\x7f\t\xb6\x99\xa1,\xb6X9mN\x84\x14\xb7vB^A\x9fXQ\x15\x84\x16\x96Gh\xccY\x01]\xa67\x89o\xb6\x05\x9a\t#\xed\x8c)9\x18\xf0\xc9k\x89cȤ\xd0,\x87ڸzA\x90\x82P\xb2\xa6\x8cW*Q\x03N\"\uf525\x88\xd7\x04\xe7[c\xa4u\xbe@R$Ds\x13}\xc5\xe3ڸT\xe9\x1eߘ\x9b\xa5`\xba\x97U*&1-\xf0̎\x96O\xa4\xa4b\xff\xcd\xd3J\x1d\xea7O\xebX\xf9\xe6i\x8d\x94o\x9e\xd67O+\xa5\xe67O뛧\xd5.\xff\x12\x9e\xd6؈\xdcy\xbe\x81\x8f\xa3\xa3Hت>6\xc4#\xf0}r\x85\xcf\x01\x7fV.\xe62\x0e*\x92\xf8?\x90\xd6\x1dSZ\x8d\xf1\xa8\x933\xed\xac\t2\xef\x8e\x17\x8d\xb8\x92\xcfȺ\x0f\x9d\x9e/\xeb~y\x14♲\xee\xfd\xb0\xc7}\xec\x93r\xee\x03Q\xa6eg\xcf}\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x19\xe9\xff\x95\x13s{Ycg\x94\x8f\x17\xcf\xe2O\x96\x91(Kg\x7f\x9a}}\xe4?\x0f\xc1\aIܧ\x9d?\xdf\x1c\x81jW\xa0\xed\xb4\xb0n\x16\xde\xd7)\xc6g\x91\xdb\xd4L\xfc\x9a\x88\x11X]\x91<\xa0\xe2ת\v\f\x14\x9fKo\x91\x9eqRu\x19\x81\x93tV\x95\xea\xbdȶJ\nYi\x1f\x95\xb0\xb0\xdee\xee@{\x00\x19\x13\xd6\xe8\f\xff\x0f\xb2\x95U$\x13\xfc\b\xf9F2\x02Ǒ\xef$\a\xfaMh0t\xf7\xf6\xa2\xfb\xc5H\x9f*H\x1e\x99\xd9F\x00=nA\xe0\x0e\xbbش\x0f\x00\x84\xfb\b\xfc\xc1\xfcC\x01\x8b\x00\x92\x8a\bƝ\xe4շ\x19\xb4\xe5\x8e|.]\xeci\xb2\xdfq<\xa6\x92\x96Lxr\na7Ep\xc0/\x9d\xba\xdb}\x96#\x13\xbfKj\xe0\xf4\x84\xc0\x94\x88\xd8H\xf2\xdf\t)\x7f\x89\xb9\xc5\xcfޞOI꛲b~\xb1\x04\xbe\xf3\xa7\xed%\xd1g\x7f\xa9\xa5\xf6\xe2\xc0ӧ\x9a<\x02\xe7\x84\xc6\xe6U\x0f\xf3\xcc\xddĔ\xc9\x05X{dg\xa7\xbf\x18\xc4_\xdf4w⎧kѪ\x15\xb1\x10\x13\x15÷\xc8\f\x1a\x8e\x14}\xd3\xf3`\x9d\x1f\x8e\xbf\xfdV\x81\xda\x13\xbcǦ\xf6s\x9aC`~bj\xbb\x10\v\xaa«\xad\xa1\xf8y\xcf\xe9o\xa62y'\x9c\xd5=\x1c\x0f\xb6\xb1:\xa2Y\xd4X\xc5g\xd7+\xd1>\x06\x9a\vY\xb7\x8e4\x1bs\x90SOK\xbd\xec\x12g\xfa\"gԫH\xf7\xfc~\xa7SP\xa7\x9c~JK\x00\x18=\xed\xf4RK\x9e\xb1EO\xb2\x9f\x97v\x9ai\xdaf\xe1\v\x9e^z\x89SK\x89\x94J9\xa54\x8dN\xafp*\xe9UO#\xbd\xd6)\xa4\xe4\xd3GI).ɻ\xc0\xa9)*'\x1e\xa7\x19\xdf\xe3=~\x9a(\xe1\x14Q\xc2\xee\xef8\x92'\xa0\x97pJh\xda\xe9\xa0\x04\x9e\xa5N\xc5W<\x05\xf4\x8a\xa7\x7f^\xfb\xd4ψd\x8d|\x9ev\xba\xe7\xe4-\v\xa9rPG\xb7}R\xa5\xf0\xa8\xfc\xa5\xacm\xba\x039\xd8\xef\b\xb7\xfe\xd9Z\x1d\x7f\x19̓\xbfh\x14\xaf\x94\x1dھ\xb4\x92\xd6\xf26:{Q\x8d\xfb\xd3u&\xfd=\xb3n\xbbJCI\x15\xde]\xbcڻt\x96\xa8i\xfe@\xb3\xed\x01\xf4-\xd5d-UA\r\x99\xd5\x1b\x80\x97\x0e\xb8\xfd{vA\xc8GY\xe7D\xb4\xef\xe5Ѭ(\xf9ޮPȬ\xdd\xe04\t\x88J[\xe8\xedFr\x96E|\xb7\xe8\xddL\xaer\xef\xb2\f\xbc1*k\xa7\f\x94\xb6b\xdcuC7\xaf{\x05\xe6Zr.\x1f'\xae\xfdi\xc9\xfe\x82wz?#:\xf4\xeef\x890\x82x\xe0%\xe1urV\x8d\xcd\n\xacYn\xf0\x1c\x9a\xfb\xcbu\ab7ϱ}9.\xe4\xee\x1e\xe4\xe0\x16xՙI\xab]n\x96n\x1cC\xbdX\x99\xa1bO$fԘ-S\xf9\xa2\xa4\xca\xec]\xa2Ƽ3\x86`K\x8fEw\x06\xadG\xffn\xe7(yÕθC\xb9/\xbb\x9b\xbe\x87\xb4;e\x1cç\x17G\xcf-\x9eq\x1c\xc3n\xc9\x02)\x15\xf99\x9a\xf9u\xb6\xa8\x99\xf67\x13\xff,w\xf0>\x1a=\xeb\x90\xe7\xf6\xa0z$=+@t\x97\xee\x0ef\xa9\xae\x00/\xe4\xed\x7fzF\xbeU\xe8\xdaߩzJ\xa0\xec\xb6\v\"\x82_\xb8a6t\x16\xd3Ox\x01\xfc\x9e\xdc\xdc\xe3\x1a\xadVm~\x8a\xfa5Z\b\x95\x85\xcd\xe0\b\x1c\xdf\xe0\xfb\xf3\xa7\xa6i#\x15\xdd\xc0O\xd2ݱ=\xc6\xf6n\xed\xce\xdd\xeb\xde\xeb\t\xf9\xa3a\xd2\xc4.\xe0\xf5\xb7}\x1f\x00kr\xbe{\x97\x1a\xdbQN\xbc\xa6\xd9\x18~\n\xdf\xef\xee~rX\x19V\xc0\xc5\xfbʥ;X\x9d\xa8\xc1\x928`\xeb \xad\xec\x7f\xb7\xf2\x11/\xff\x8d\xc71Û\t\r2\n0\xd9\x1cS\x10'\xa1T\x95\\\xd2\x1cԵ\x14k\xb6\x19\xc1\xee\x97N\xe5\x033\x9b\xe1\x8f\x1e\xb9\xdaF\x05\xf8g\xceA\xb0>\x0f\xe7\xc0?2\x0e\xda\r+A\x01\xdf\xf4[\xd5\xfa\xb8*V·[ۏu\a\x036Ρ\x85\xa1\xe8\x12\x94\xf5\xa2\\к\xd2AV\x87\x11o8\u0084\x81\r\xf4W\x81G4\xb0\xbbU\x1a\xcdgP'\xb8\x96\xf91\x16\xdf\xea \x7f?\xdc\U000804ed\x90W\xec\xc6=\xe7\x84\xdc\xdc_kR\x89\x1c\xc3\xc5\xf7\x7f\xb9\x9d$u\xbb\xce\xcd\xf5a\xb6\x8e)\xd5\xfbx\xab\x96s\xdc\xd2\x17\xce;\x96\xeb\b\x02CpZ\xef\x80<2\xe3/\xee:\xefM\xabCK\x9e\xa1\x17\x0e\xf0J\xff\xf17\x0e\xdc\xcd\xff\xfee\x14?\x1d+\x85פ\xfaW\x01\xf0Zѓ\x9e9X\xd5\t[u\xf2\x97~g\f\x14\xa5\x89\xf9\x1a\xe3\xea\xf0\xfbc\x00k?M\x1a\xca[\xb3\x92\x86\n1O[\xefEv,\xb1\xcck\xa3#\xdc<6\x1fc\x04\xb8\xf6\xe7!\xceF\x80\x1a\xe0\x10\x01t\x95e\xa0\xf5\xba\xe2|_\x1f\xc7\xf8J\xa8\xf1\x912~>R8h\x83\x82`\xd1;\ni\x14a\x9f\xee\r\"\x0f3=\x1cU\x9aF\n\xcf\x05\x9f\r\xa9\r-Nz\xb0\xe1\xba\x0f\x06\x9f\xecQy+\xa9\x92\xd6c\xa7\xbaa\x7f̸4\xe0\\K\\dYh\x90\x13\u0601 \xd6:;\x12\x87ר&B\xf1'\\\x9d\x85\v\xf6.\x84B\xa2\x0f\x13\x11\x1f\xed\xd0\xf8\x00\xcew\xba\x86\x89\xb9\xa2\xf8\x9eI\x9f\b}\xe7\xd7E+\xae\xac\xf7\x0f\v\v\xe24\xaf5\xaa\x9b3ͺv\xe1yJ\xee\xfav9\x04\xee\x14\x15\xd7\x7f\xee\xe5\x99Ӹ\x8f\xee\xb3TZ\x1f\xddI\n-\x02\xb1\x96\xf1\xf3\xe3\x8eS\xfd\xb4Kݱ\xa5s8\xb2p\x86\x8er\xee\x0f:\x16\xa05݄\xdb\xdc\x1f\xed\xd2c\x03\x02\\x\xcem\x9eD\x806\xa7\xe2\xbaw\x99\xbb)C3SQ\xdfAH\xf0m\xd5\xfaN\x13.cP\xf1A\x17\x16^\n\vk\xb2\x89\x84z*\x99JY\xc3}\xa8+Zڠ'\x8c\xdci\xdev\x03\xce6̮u,\xe76T\xad\xe8\x06\x16\x99\xe4\x1cP[\xf7\xc7\xf5\x92sݟ=\xfc\x02T\x8f\xa2\xf6\xb1]\xd7\xef\x00:n\xbb\x8do\xea\xd2\xdd\xf1\xf5.\xc3\x144\x0f\xe9\xf5\x06$\xb1\xe3I\x8e\xb2\xa3B\xf4\x95\xb9\xfeH\xdbuì\xf3j\xd9\xc7y\xfd#ss\x1f\x17\x88\xcbcA\x7f\x95jN\n&\xec?T\xe4n\x03/4\x9e4\xfe\xad\x94\x0f\xb7\x11'\xb67\xf8\x1f\xea\x8a\xcdV\a\x13n\xd8x`t%+\xbf\xfb^;\xb4\xf1m\x15\xbc\x99\xff\xcc\xcbM\x84y\xc4\x1e\xf4\xd0\x19\x8c\xe8\xfeЁ4j\n\\\xcf\x03\xb0n\xc3Kf\x9c\xef燐\x0f^Ml`\xb7^.\xf0n@s\x1f\xc1@GaG*\n\xa4\xbe\xf8\xa2\xad\xd0OY\xf5z2\x0f9\x93=\x1a\xff\xd0\xd4\x1e\xa2\xa3\x1bf\xcb\xdd\x1b@\xb0\xe3\x04\x9ew\xc1\x8e\xcfT\x8c\b\xff\x8d\xadS\xdf]\xd0Z\xb8\x85,\xb1\xc1(]\xfc\xec\xfb\x82|\x82\xfevł\xfc\xb5\x82*B\x83Ex\x18\xee\xd6P\xd5\x0f\xf9\xbac\xf0\x90cF\a\xce\xc6H\x95\xa5\xb8Qr\xa3@\xf7\x85uA\xfeF\x99ab\xf3Q\xaa\x1b^m\x98\xf8<|\xe4\xe7X\xe5\x1b\xaa\f\xb3\xc2\xee\xc6\x13\x1b(\x13\x94\xb3\xbf\xc7\xf4Z\xfb\xe38\xa0\xeb\xc1\x05ւ$\fc\xe8\xc3{\xb0>\xee`\\ \xaaBKO\xd7S\xfc\x95\xc0\x931\x9dZ\xfb\x12\x8d/\x12\xba\xbd \x9fdT1\xf8t(օi]2\xd0f\x01\xeb\xb5T\xc6\xedV/\x16\x84\xadC\xf0\xc1\xea\x1c\x8c\x9b\xb9\xb7*\t\x8bm3\u05c9&\x8d\xf9\u00a0\xb7B+\x8cW\xd9\x17t\xefv\xa6h\x96U\xd6úԆ\xf2\x88\x83\xf3,ŏQ\x1e;\xf9 \xff\xe5Y;y\xcb6\xa0~\xd0\x11\xfbq$\xc5\xcb4\x9c\xd7\xc7-\x8a ȣb\xc6X\x9fJ\x1eI%\xf0\xa42ַ\xe2\x9chKꓢ\x8fĩ\xd1\xe5pJN\x1a\xcaw5\x94!\xf5\xec\xb1Ɨ\x19WH\x1bb\xfd^\xcc>\xf2\xb5,\x9b\xb3-\x15\x9b\xc1\x1b\n\xb6JV\x9bm\x90\xe4\x01g\x9a\xe4\x15`\xb0\x16U\x8a\x0e\x0f\v\x9bJ\x89V*\xc1\x91c\xdf$\b\x03\x0e\x97f\x0f\xa4*\xe7\xfe\xe1^\xffb\xf3\xa5\x7f\x03e\xb1V\xb2X\xf8~1\x96:\xf7;\xf9\x8aI빘m\x94\xea\xc4y\xed\xfe\x99\x01\x94\x84\xb2\x04A\xa8\xf6='\xdc\x14u\xb2\x99\xfa͚\x86\x1b\xa9Y\x82\xb7\x1f\xe5\xf8_\xdb\x00\x02\xc3\xcb\xf0w\x97\x19~\x05\x83}\xc6\xf0\xf8\xec\x8f\xe0Î\n\xe3\x96\x13\xb5\x89\x9c9#6\x9b\xb4\x90\xd1ְ=+Hsہ0\x12\x9f\xc1\xee\xe2,\xba\xf5\xe9\x1a\xee\"\xb0k\xff\xfcj\rxN4\x13\xe1\xe1k\x97\xfa\xe1\xa4?\xba\x13(\xf0\xa1J\xa9\xe2٘\xc7\x03.]\x84^7ֲ\xab=\x89\x0f'/\xc5\xef\x0f`\x1c\x1c\xea\xc6wI\xeb*a\xf9\xfc\a\x16\xdb\x0f\xc04\xde̢\xf2\xc7\xdf\xfd\xb0\xf6.i\xa9\x17\xa7ȱ\x95\x1f.ꆗp\xddwHo8\xd8٦\x01\xba\x8b\xcaIsnw\xc6h\xda9Ci\xe1\x89\xf7\xf3Ēvg\f\xa2\xbdX\x04\xed\xbc(?R| \xfa\xa4Y\xfb7\xdf6\x12B\xf3`\xcf\x1dDk\xc5\xd0\xc2\xc0_5\x8a\x16\xb5\xb9\xbd\x1fQO\xe7-m\xe1{\xf2\xbf\xfc\x7f\x00\x00\x00\xff\xff!\xd0\x1d\xb3҂\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccZK\x93۸\x11\xbe\xebWt\xed\x1e\xf6\xb2\x94줒J\xe96\x96\x93*W\xc6\xf1\xd4hvr]\bhJ\xb0@\x80\x01@\xc9\xca㿧\x1a\x0f\x89\xe2C\x0f;q\u008b-\x12h\xf4\xf3\xeb\x0f\xc0\x14E1a\xb5|E\xeb\xa4\xd1s`\xb5\xc4/\x1e5\xfdr\xd3\xed\x1f\xdcT\x9a\xd9\xee\xedd+\xb5\x98âq\xdeT\xcf\xe8Lc9\xbe\xc7Rj\xe9\xa5ѓ\n=\x13̳\xf9\x04\x80im<\xa3\u05ce~\x02p\xa3\xbd5J\xa1-֨\xa7\xdbf\x85\xabF*\x816\b\xcfK\xef\xdeL\xdf\xfe~\xfa\xbb\t\x80f\x15\xcea\xc5\xf8\xb6\xa9\x9d7\x96\xadQ\x19\x1eENw\xa8К\xa94\x13W#\xa7\x15\xd6\xd64\xf5\x1cN\x1f\xa2\x84\xb4z\xd4\xfc]\x10\xb6\x8c\xc2\x1e\x93\xb0\xf0]I\xe7\xff<>\xe6Q:\x1f\xc6ժ\xb1L\x8d\xa9\x15\x86\xb8\x8d\xb1\xfe/\xa7\xa5\vX9\x15\xbfH\xbdn\x14\xb3#\xd3'\x00\x8e\x9b\x1a\xe7\x10f\u05cc\xa3\x98\x00$\xd7\x04i\x050!\x82\xb3\x99z\xb2R{\xb4\v\xa3\x9aJ\x1f\xd7\x12踕\xb5\x0fΌ\xb6@2\x06\xb25\xe0<\xf3\x8d\x03\xd7\xf0\r0\a\x0f;&\x15[)\x9c\xfd\xa2Y\xfe\x7f\x90\a\xf0\xd9\x19\xfd\xc4\xfcf\x0e\xd38kZo\x98\xcb_c\x8c\x9eZo\xfc\x81\fp\xdeJ\xbd\x1eR\xe9\x919\xffʔ\x14A\x93\x17Y!H\a~\x83\xa0\x98\xf3\xe0\xe9\x05\xfd\x8a\x1e\x02r\x11B\xf6\x10\xec\x99K\xeb\x00좔\xe0\xa3aMUo\xad3\xb5I\x15x\xedH\x89\xfaӛ\xa4}Kl\xce\xef)\xb7x\x14\xe9<\xab\xea3\xb9\x0fk\x1c\x13v\xe6\x8a\xf7X\xb2F\xf9\xb6\xa9\x14%\xd5\xce\xcbs\xb3j\xe4S\x11g\x9d\xad\xf8\xfe\xec]\\ue\x8cB\x16\xa5\xc4Q\xbb\xb71\v\xf9\x06+6O\x83M\x8d\xfa\xe1\xe9\xc3\xebo\x97g\xafa(\x91:EA\x81c\xad\xd8l\xd0\"\xbc\x86\xfa\x8bqsɴ\xa3L\x00\xb3\xfa\x8cܟ\x82X[S\xa3\xf52\x17K|ZX\xd4z\xdb\xd1\xe9\x9f\xc5\xd97\x002#\xce\x02A\xa0\x841\xafR\xfd\xa0H\x96\x83)\xc1o\xa4\x03\x8b\xb5E\x87:\xc2\x14\xbdf:)8\xed\x88^\xa2%1Tۍ\x12\x84e;\xb4\x1e,r\xb3\xd6\xf2\xefG\xd9\x0e\xbcI\xc9\xec\xd1y\b\x15\xaa\x99\xa2dm\xf0g`Zt$W\xec\x00\x16iMhtK^\x98\xe0\xbaz|\xa4j\x90\xba4s\xd8x_\xbb\xf9l\xb6\x96>#47U\xd5h\xe9\x0f\xb3\x00\xb6r\xd5xc\xddL\xe0\x0e\xd5\xcc\xc9u\xc1,\xdfH\x8f\xdc7\x16g\xac\x96E0DGH\xadď6a\xba;[\xb6W\xd2\xf1\t\x90zGx\b^c\xcaDQ\xd1\xc4S\x14\xe8\x15\xb9\xee\xf9\x8f\xcb\x17Ț\xc4HŠ\x9c\x86\xf6\xfc\x92\xe3Cޔ\xbaD\x1b\xe7\x95\xd6TA&jQ\x1b\xa9}\xf8\xc1\x95D\xed\xc15\xabJzJ\x83\xbf5\xe8<\x85\xae+v\x11\xba\x18\xac\x10\x9a:\x80Dw\xc0\a\r\vV\xa1Z0\x87\xdf9V\x14\x15WP\x10n\x8aV\xbb7w\aG\xf7\xb6>\xe4\x9e:\x12\xdaA4X\xd6\xc8\xcf\xeaN\xa0\x93\x96*\xc33\x8f\xa1\xba:\x0eJP1ޔ\xf33\f\x12\xf40\xceѹ\x8fF`\xf7KG\xe5\x87\xe3\xc03\x1dk\xb4\x95t\xa1\xbdBil\xb7\xf3\xb0#\x92\xb7\x9f\x8cx݀\x03\xa0n\xaa\xbe\"\x05<#\x13\x9f\xb4:\x8c|\xfa\xab\x95\xbe\xbf\xd0H \xe9\x89*.\x0f\x9a?\xa1\x95F\\1\xfe]g\xf8\xd1\x05\x1b\xb3\x872\xe4\xbf\xf6\xea@\xd8\xe5\x0e\x9a\xf7Q;?\x0fO\x1f2\x82\xc7\xdaJ\x85\x99|5\x85\x87TԦ\x847 \xa4#\"\xe1\x82о\xb3t\xa3\x02ј\x83\xb7\xcd]\xe6s\xa3K\xb9\xee\x1b\xdd\xe6Fc\x19sEt\xc7s\x8b\xb0\x12\xa1\x16eGm\xcdN\n\xb4\x05Շ,%O\x9a46v\x90R\xa2\x12=l\x1a\xad\xb2`\x8aEAE\xcdԕ\x18.\x8e\x03\x03\x93fR\xc7\f>\t\bXc\xabԚ\xb5G-\xb0\xdbm\x826&\x00\x9aC\x01{\xe97\x11)\xd5P\xdd\xc1\xc5ڣg\x8b\x87\xa1\xd7\x1d\xdd_6H#c\xe3Ep\xc8-\xfa\x90m\xa8(}(\x95\xa6\x00\x1f\x1b\x17\xb0\xb6\x8b\x13\xf9\t\x84/\xcf\xde\xe2\xa1\xefh\xb8\x16\xdcD\x85FT\x0e$j\x0e?\xfcpݤ^w\xcb\x0fQ\xf7l\xa8\xc5\x12-\xea\x1e\x9b\xc8\xcfK\xe8Q\x944\x94aX\x96Ƚܡ:\x84\x9eD\xe0\xf93\xac\x1a\x0f\xa2\xc1\x105Ʒ{f\x85\x03n\xaa\x9ay\xb9\x92J\xfa\x03H7\"\x9f)e\xf6(Rı\xaa\xfda\n\x1f\xb4\xf3LstG\x1eD\x1e\x8b\xa9\xc0t\x1c\x95\xaa8\x10:f\x8700\x8a\xaf\x8c\xf3\xc0\xd1R:\xaa\x03\xec\xad\xd1\xeb1c\a\xda!\xed\x01\xadF\x8f\xa1#\n\xc3\x1d5C\x8e\xb5w3\xb3C\xbb\x93\xb8\x9f\xed\x8d\xddJ\xbd.H\xc1\"\x81\xcf,\xec\xecf?\x86\x7f\xbe&\vL\x1dq\xe2\x86\xe4]\x86Z?\x10\xbd\xf5\x1b\x8c-b\x19s\xd0X \x02A\xa9]\xa5܍\xc8:TvC\xbc\xbc\xfd\xe4\x90\x0f\xf5\x8f-\xf6[\xc7\x05P\x01\xf8R\x9c|[T\xac.\xe2h\xe6M%\xf9\xa4km\xcc\xfb\xcb\xf8\x937+R\vɉܞ\xe3F\xdeĉ\xb3=̀\x1b\xba\xbb\x9c1\xb4\x1cvS47q\x85+\x1a\x7fj\x8f=m}#t\xa7\xfe\xef\xd0\x13\xeft\xa0\x91\xf8\x01\xb3}?\a\xc0\xe4FkB*o\x80\x1d\xdb\xc0O\xae\xdb\xff\xeeD\xcfU÷8\xe0\xf8\x9e)\xef\xc2\xc0\xec\xe38\x8dti\x1c\x86\xc6tM\r\xb8^\x11\x9c-\xd0ޢ\xcb(\xf2-\x1eH\u0091[0X<\xc0\xaa\xd1BaVu\xbfAM\xdb1Y\x1e\x88\xec\xbf<.\xb3c\x03\x01K[\xa7\xec\xde1 yO\xbb\x00JA1\x87_\x1c\xa6u\x9f\xb1\x04\xa9\x9dG\xd6#\xe9\xf1\x89\xbdq\x0e\xab\xc3\x00\u05fa\xd9A\xcfX~\xbb\x8f\x82\xae\xe4\xa1\xd4 8\xc6\xc4J\xb0\x92\xfa{\xde\x0f-\x1e\x02\xc4\x12\xdf \"}\xe6ґe\xeetth\xd0i\xf1\fdR\xc7\x02a\xd5\xd8\":\x1fR\x01\x8by9H䇃q\xb9.\xe0\x12\xb3\xe89\xfb>v1*\x13\x80\xdd\xc80\xe0z\xb2\xc0E\xa6\x017\xb0\x8d\x9e\x99\xa39\x05w\xb2\x0e\xf8\x0e\xcc\x03\xfe\xfb\xec\x03\xeef \xf0\xddY\bܖ)\x97\xd9\b|\x13#\xb9\xe0\x8bK\\\x05\xae\xf2\x15\xb8\xc8Y`\x94\xb7\xc05\xee\x02w\xf2\x17\bx\x82\xa5\xfcr\x032?\x85\x81\xb9\x93\xd6\xcco\xa8kH\x81\xc0\x06\xfaj<\xa1\x18q\xd0q\xd3\xfb)\x85\xef+\xfa\xee%\xd2\x17չ\x87\xf7e@\xbfB\x8c\x9eҰ\xa3\x17\xf2\xef\x04 \xe7\a c\x04mТ\xdd\xf1\xb4\xfdO\xf1X\x81\x0f\xa0\xf8\x992\xaf\xfd\x19\x17\x8e'\xf2\x99\xff\x10K\xa3Ͱ\xb1\x16]m\xb4\xa0\xb6w\xdb\xe1\xc4I\xe5\xff\xdc\x11\xc5pX\x8bs\xfa\xda\xf9\x96\xa3p\xd3\xf9\\\xb8߸\xfb\x84.\xde\xfa\xb4Ͽ\xccʡݵ\x0e\xe9:6~\x97\xb3\xb9\xc1\xce\xd6:\xb0#\xaa\xa4\xa1\xd1\xe1\xc8\"4\xad\xe9d`F\x9b\x17\xfa\xd0<\xa4\x03m\xf64\xb9%-v=\x13\xe9M8\xb4dZ\xa4\xe3b\xfa4 y/\x95\xa2\x1ef\xb12\xe4,\xd4^Zj\x96,\xb4\xb1\xddo\xa6o\xfewg\x81\x8a9\xbf\xc6Qq\xff\x99\xa6\x00[\x99\xc6\x0f\xf4\xfeV\xc2\x0f\xd6t\xb8e\xbfG\xc7\xf0\xb7\x03\xd7\xe8\t\x8d\xc9\x11፵\xe1\xb2._\"ݱ\xd1\x1cC\xe0\x87Ο8\xb4\xbf\xf5\xff\x00\xe2\x06\xbb\x06\xbbt\xefe촭\xb8&'\xb7\xdf4\xab\xe3\x15\xec\x1c\xfe\xf1\xafɿ\x03\x00\x00\xff\xff%\xff\\)\x99#\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVMo\x1b7\x10\xbd\xebW\f\xd0kwU\xa3hQ\xec\xadqr0\xda\x06\x82\x1d\xe4N\x91#-c.\xc9\xce\f\xe5\xba\x1f\xff\xbd \xb9+K\xab\x95\x93\\\xb27\x91Ù\xc7\xf7f\x1e\xd54\xcdJE\xfb\x11\x89m\xf0\x1d\xa8h\xf1/A\x9f\x7fq\xfb\xf8\v\xb76\xac\x0f7\xabG\xebM\a\xb7\x89%\f\xf7\xc8!\x91Ʒ\xb8\xb3ފ\r~5\xa0(\xa3Du+\x00\xe5}\x10\x95\x979\xff\x04\xd0\xc1\v\x05琚=\xfa\xf61mq\x9b\xac3H%\xf9T\xfa\xf0C{\xf3s\xfb\xd3\n\xc0\xab\x01;0\xe8Pp\xab\xf4c\x8a\x84\x7f&d\xe1\xf6\x80\x0e)\xb46\xac8\xa2\xce\xf9\xf7\x14R\xec\xe0e\xa3\x9e\x1fkW\xdcoK\xaa7%\xd5}MUv\x9de\xf9\xedZ\xc4\xefv\x8c\x8a.\x91rˀJ\x00[\xbfON\xd1b\xc8\n\x80u\x88\xd8\xc1\xfb\f+*\x8df\x050^\xbb\xc0l@\x19S\x88TnC\xd6\v\xd2mpi\x98\bl\xc0 k\xb2Q\nQ\x1fz,W\x84\xb0\x03\xe9\x11j9\x90\x00[\x1c\x11\x98r\x0e\xe0\x13\a\xbfQ\xd2w\xd0f\xbe\xda\x1a\x9a\x81\x8c\x01\x95\xea7\xf3ey\u0380Y\xc8\xfa\xfd5\b,J\x12O J]\x1b<\xd0\t\xbf\xe7\x00J|\x1b{\xc5\xe7\xd5\x1f\xcaƵ\xca5\xe6pS\x99\xd6=\x0e\xaa\x1bcCD\xff\xeb\xe6\xee\xe3\x8f\x0fg\xcbp\x8euAZ\xb0\fjB\x9a\x89\xab\xacA\xf0\b\x81`\b4\xb1\xca\xed1i\xa4\x10\x91\xc4N\xadU\xbf\x93\xe19Y\x9dA\xf8\xb79\xdb\x03Ȩ\xeb)0y\x8a\x90\v\x89cS\xa0\x19/Zɵ\f\x84\x91\x90\xd1\u05f9\xca\xcb\xcaC\xd8~B-\xed,\xf5\x03RN\x03܇\xe4L\x1e\xbe\x03\x92\x00\xa1\x0e{o\xff>\xe6\xe6|\xef\\\xd4))\x94\xe4\xb6\xf3\xca\xc1A\xb9\x84߃\xf2f\x96yP\xcf@\x98kB\xf2'\xf9\xca\x01\x9e\xe3\xf8#\x93h\xfd.tЋD\xee\xd6뽕\xc9Rt\x18\x86\xe4\xad<\xaf\x8b;\xd8m\x92@\xbc6x@\xb7f\xbbo\x14\xe9\xde\njI\x84k\x15mS.⋭\xb4\x83\xf9\x8eF\x13Ⳳ\x17\xddS\xbf\xe2\x02_!O\xf6\x84\xda#5U\xbd\xe2\x8b\ny)Sw\xff\xee\xe1\x03LH\xaaRU\x94\x97\xd0\v^&}2\x9b\xd6\xef\x90\xea\xb9\x1d\x85\xa1\xe4Dob\xb0^\xca\x0f\xed,z\x01N\xdb\xc1\nO\x1d\x9b\xa5\x9b\xa7\xbd-\xb6\x9b\x1d E\xa3\x04\xcd<\xe0\xceí\x1a\xd0\xdd*\xc6o\xacUV\x85\x9b,\xc2\x17\xa9u\xfa\x98̃+\xbd'\x1b\xd33pEڅ\xe1\x7f\x88\xa8\xb3\xb8\x99\xdf|\xda\ueb2ec\xb5\v\x04O\xbd\xd5\xfd4\xfc3\x9a\x8eFq\xce߲1\xe4\xef\xc5n\xe7;W/\x0fEdK8k\xd8\x06.\xbc\xfbu^\x8a\xa9~%3\xd5\xd1Gnt\"*\xcdw\xf4y\xb5t\xe8K\xb9@\xa2@\x17\xab3P\xefJP\xf9Ǡ\xacgP\xfey<\b\xd2+\x81'\xa4\r\x97\x95\x1ax\x8fO\v\xabw~CaO\xc8\xf3\x96ϛ\x9b\xca\x1e\xce߃WXZlʋE\xceVhNXd\t\xa4\xf6\xa7\xbcr\xda\x1e\x9d\xbe\x83\x7f\xfe[\xfd\x1f\x00\x00\xff\xff\xbeM\x1a\xea\xb1\n\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcWMo\xe36\x10\xbd\xfbW\f\xd0K\v\xac\xe4\x06E\x8b·\xd6\xd9C\xb0\xe96\x88\xb7\xb9S\xd4HbC\x91,9t6E\x7f|1\xa4\xe4\x0fYv\x9c\xcb\xea\xe6\xe1p\xf8\xe6\xcd\xcc#]\x14\xc5B8\xf5\x84>(kV \x9c¯\x84\x86\x7f\x85\xf2\xf9\xd7P*\xbb\xdc\xde,\x9e\x95\xa9W\xb0\x8e\x81l\xff\x88\xc1F/\xf1\x16\x1be\x14)k\x16=\x92\xa8\x05\x89\xd5\x02@\x18cI\xb09\xf0O\x00i\ry\xab5\xfa\xa2ES>\xc7\n\xab\xa8t\x8d>\x05\x1f\x8f\xde\xfeX\xde\xfcR\xfe\xbc\x000\xa2\xc7\x15\xd4\xf6\xc5h+j\x8f\xffD\f\x14\xca-j\xf4\xb6Tv\x11\x1cJ\x8e\xddz\x1b\xdd\n\xf6\vy\xefpn\xc6|;\x84y\xccaҊV\x81>ͭޫ\xc1\xc3\xe9\xe8\x85>\x05\x91\x16\x832m\xd4\u009f,/\x00\x82\xb4\x0eW\xf0\x99a8!\xb1^\x00\f)&XŐ\xdd\xf6&\x87\x92\x1d\xf6\"\xe3\x05\xb0\x0e\xcdo\x0fwO?m\x8e\xcc\x005\x06镣D\xd4\x7f\xc5\xce\x0e\xd3\x04@\x05\x100\xc0\x01\xb2;\x84 \f\bO\xaa\x11\x92\xa0\xf1\xb6\x87J\xc8\xe7\xe8\xc0V\x7f\xa3$\bd\xbdh\xf1\x03\x84(;\x10\x1c%;\x1c\x9c\xa5m\v\x8d\xd2X\xeel\xce[\x87\x9e\xd4Hy\xfe\x0e\x1a\xea\xc0z)\v\xfe8\xf1\xbc\vj\xee,\f@\x1d\x8e\xe4a=p\x05\xb6\x01\xeaT\x00\x8f\xcec@\x93{\x8d\xcd\xc2\fٔ\x93\xd0\x1b\xf4\x1c\x06Bg\xa3\xae\xb9!\xb7\xe8\t\x1aE\xaf\xcb41\xaa\x8ad}XָE\xbd\f\xaa-\x84\x97\x9d\"\x94\x14=.\x85SEJĤQ+\xfb\xfa;?\ff8:\x96^\xb9!\x03yeڃ\x854\x1d\xef(\x0f\xcfK\xee\xae\x1c*\xa7\xb8\xaf\x02\x9b\x98\xbaǏ\x9b/0\"ɕ\x1aZl\xe7z\xc2\xcbX\x1ffS\x99\x06}ޗڔc\xa2\xa9\x9dU\x86\xd2\x0f\xa9\x15\x1a\x82\x10\xab^Q\x18{\x9dK7\r\xbbNR\x04\x15Bt\xb5 \xac\xa7\x0ew\x06֢G\xbd\x16\x01\xbfq\xad\xb8*\xa1\xe0\"\\U\xadC\x81\x9d:gz\x0f\x16Fy&j^\x01\x128\xe1[\xa4\xa9u\x82\xe5Kr\xe2\xe3_:q,X\xdfcٖ\xac9a\x00\x92\xf5\xe8\x87i\xa1.a\x80\xd9F\x9fE2\xf67\xd3\xc0\xbc\xb2\xa0\xb0\xd8\x1db:=\x9a?4\xb1\x9f?\xa0\x80\xdf\x13\xe6{\xdb^\\_[C<\x17\x17\x9d\x9e\xac\x8e=n\x8cp\xa1\xb3o\xf8\xde\x11\xf6\x7f:\xf4\xf9\x1a\xbe\xe8:\xde滫\xef\x82c\xd4g\xcf}D\xbeA\xf0|\xa6\x83\xc3UQ\xae\xc04x^\x95\xe8zs\xf7\x1e\nϸ\xbf\xa3Hw\xa6\xb1o\xa4\xb8w\x9c\xf5;#\x03\xe3\x97\xde\x10o\xf74\xbfBƞ\xe6-\xf9\xeeD\xf8\x14+\xf4\x06\t\xc3^\xa9_\x14u\xb3\x11\x01^:%\xbb\xb41\r\x04_\x02!X\xa9\xe6$\xf5\n\xf8\xac#\xca\xe3\xccP\x16iXg\xcc\f\xfe\xc4|F\xfd\xce\x1dP\f\x8at\x95\x82\x92\xa0\x18ޡ\xa1\xc9\x7f\xa4ZF\xef\xd3\x15\x95\xad\xfc2\x99n\xb8VDG\xe5\xf9\xeb\xf1\xfe\r%\xbd\xdd{\xa6\x17\xb7P&\xa3q\x1e\x8b\xa0Z~A\xf1\x1akiҸS2\xf2w\xfc\xc2;&j\xb6\xa2\xf8թ<\x80o@\xfc\xb8ŝ\x8f&\xdf\xf3\xd37l\n\x88\x81\x9f[ \x85\x99\xc1X!Ԩ\x91\xb0\x86\xea5\xdf\\\xaf\x81\xb0?\xc5\xddX\xdf\vZ\x01\xdf\xff\x05\xa9\x9962QkQi\\\x01\xf9x\xae\xcbf\x13w\x9d\b3cx\x94\xf3\x03\xfb\xcc5\xc6n\x18/v\x06\x9c\xbd_\n\xf8\x8c/3\xd6\ao%\x86\x80\xa7ct6\x93\xd9!81\x06~\xa4\xd5\a,\r\x7f\x19\x06\xcb\xff\x01\x00\x00\xff\xffx\xae@\xbaJ\x0e\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4:Ks\x1b7\xd2w\xfd\x8a.吤\xca$\xe3|ߦ\xb6x\xb3\xe5͖v\x13\xafʔ}I\xe5\xd0\x1c49\x88f\x00,\x80\x11\xcd\xcd\xe6\xbfo5\x80\xe1\xbc@R\xa2\x93\x18\x17\x89x4\xfa\xfd\xc2\xccf\xb3+4\xf2\x03Y'\xb5Z\x02\x1aI\x1f=)\xfe\xe5\xe6\x0f\x7fus\xa9\x17\x8f/\xaf\x1e\xa4\x12K\xb8i\x9c\xd7\xf5;r\xba\xb1\x05\xbd\xa1\x8dT\xd2K\xad\xaej\xf2(\xd0\xe3\xf2\n\x00\x95\xd2\x1ey\xda\xf1O\x80B+ouU\x91\x9dmI\xcd\x1f\x9a5\xad\x1bY\t\xb2\x01x{\xf5\xe37\xf3\x97\xdf\xcd\xffr\x05\xa0\xb0\xa6%\x18-\x1eu\xd5Դ\xc6\xe2\xa11n\xfeH\x15Y=\x97\xfa\xca\x19*\x18\xf6\xd6\xea\xc6,\xa1[\x88gӽ\x11\xe7;->\x040\xaf\x03\x98\xb0RI\xe7\xff\x99[\xfdA:\x1fv\x98\xaa\xb1XM\x91\b\x8bN\xaamS\xa1\x9d,_\x01\xb8B\x1bZ\xc2[F\xc3`A\xe2\n \x91\x18К\x01\n\x11\x98\x86՝\x95ʓ\xbda\b-\xb3f \xc8\x15V\x1a\x1f\x982\xc2\x0f\x9cG\xdf8pMQ\x02:xK\xbbŭ\xba\xb3zk\xc9E\xe4\x00~qZݡ/\x970\x8f\xdb\xe7\xa6DGi52w\x15\x16Ҕ\xdf3\xca\xce[\xa9\xb69$\xeeeM \x1a\x1b\x84\xca\xd4\x17\x04\xbe\x94n\x82\xdd\x0e\x1dch} ;\x8fKXg\x88\xcecm\xc6H\xf5\x8eF\xac\x04z\xca\xe1t\xa3kS\x91'\x01뽧\x96\x92\x8d\xb65\xfa%H\xe5\xbf\xfb\xff\xe3\xecH\xfc\x9a\x87\xa3o\xb4\x1a\xf2\xe65\xcfBo:b²ڒ\xcd2H{\xac>\x05\x11\xcf\x00^\xf7\xceGL\"\xdc\xfe\xfcYTnUa\xa9&u\x19B\xb2;=Ŧ\x0f\xba\xbfj\xac\xd4V\xfa\xfd\x12^~\xf3T4\xd9>@o\xc0\x97\x04IyV^[\xdc\x12\xfc\xa0\x8b\xa8h\xbb\x92lR\xb4u\xd2\xfeR7\x95\x80u+\x18\x00\xe7\xb5\xcd*\x9b\xa1b\x1eO%\xb8-ؑ\xc6\r\xef\xfc#\f\xa2\xb0\x84Y\x83h\x9d\xe6<\xec\x90Z\xe5\xad\xe2Ֆ\x9ed\x11}\x96*-\xe8\xc0?\x9a\xa0%\x1d\x18\xab\vr\ue1212\x8c\x01\"o\xbb\x89\xb3\f*)\xeci\xf1iL\xa5Q\x90\x05\xaf\xa1D%*b2\x10\xbcE\xe56IE\xa6\x02l\x8f\xdd\xef\xcd\x10\x95\xf7i\xe1\x18:q\xd7\xe3\xcb讋\x92j\\\xa6\xbdڐzuw\xfb\xe1\xffV\x83iVcm\xc8zن\x8f8z\xc1\xb17\vCr\xff;\x1b\xac\x01\xf0\x05\xf1\x14\b\x8e\x92\xe4\x02\x1bR \x91p\x8a\xec\x91\x0e,\x19K\x8eM+h\x94\xde\x00*\xd0\xeb_\xa8\xf0\xf3\x11\xe8\x15Y\x06\xd3\xdaB\xa1\xd5#Y\x0f\x96\n\xbdU\xf2?\a؎y͗V\xe8\xc9\xf9`\x8cVa\x05\x8fX5\xf4\x02P\x89\x11\xe4\x1a\xf7`\x89\xef\x84F\xf5\xe0\x85\x03n\x8cǏ\xda\x12H\xb5\xd1K(\xbd7n\xb9Xl\xa5oS\x86B\xd7u\xa3\xa4\xdf/B\xf4\x97\xeb\xc6k\xeb\x16\x82\x1e\xa9Z8\xb9\x9d\xa1-J\xe9\xa9\xf0\x8d\xa5\x05\x1a9\v\x84\xa8\x906\xcck\xf1\x85MI\x86\x1b\\;\x11t\x1c!\xd2?C<\x1c\xfb\xd9\b0\x81\x8a$vR\xe0)fݻ\xbf\xad\xee\xa1\xc5$J*\n\xa5\xdb:\xe1K+\x1f\xe6\xa6T\x1b\xd6y>\xb7\xb1\xba\x0e0I\t\xa3\xa5\xf2\xe1GQIR\x1e\\\xb3\xae\xa5g5\xf8wCγ\xe8\xc6`oBZ\x05k\xb6%\xf6\x00b\xbc\xe1V\xc1\r\xd6Tݠ\xa3?YV,\x157c!\x89wg\xf9\xc3c#\xa9\x12!s8\x7fwVsy\xdcn\"\x12!\"x\r\bFRA\x83h\fR9O(\xd2$;AKi\xedE\xf4\xf4G\x91\xe4\xd1Em\x96\t G\x1e)\xe0\x1f\xab\x7f\xbd]\xfc]G:\x00\vN\xcdB\xad\x17\xf2\xed\x17\x87zO\x90\x93\x96\x04Wo4\xafQ\xc9\r9?O\xd0Ⱥ\x9f\xbe\xfd9\xcf?\x80\xef\xb5\x05\xfa\x88\\5\xbd\x00\x19y~\bf\xad\xdaH\x17\t?@\x84\x9d\xf4e@\xd4h\x91\b\xdc\x05\x12<>\xb0%G\x12\x1a\x82J>d\xec'\x8e\xeb\x90\xcduh\xfe\xca\xd6\xf3\xdb5|\x15\x9d\xd75\xff\xbc\x8eh\x1cҖ\xbe\x81u\xe8D+\xb3r\xbb\xa5.\xef\x9f(\v\x87Y\x0eP_\x83\xb6L\xab\xd2=\x10\x010\xcb)\xc6\a\x12\x13\xf4~\xfa\xf6\xe7k\xf8jȃ#WI%\xe8#|\xcb\xde'\xf0\xc6h\xf1\xf5\x1c\xee\x83\x1e\xec\x95Ǐ|SQjG\n\xb4\xaa\xf61\x01~$p\xba&\xd8QU\xcdb\x82(`\x87{Л#\xf7\xb4\"b\xd5D0h\xfd\xc9$1\xf1\xe1\xb4\xd1L\xb3\xa6v<\xcd^B\x16\xf5$\xeb\xfdl\x19\xc8\x139\x11ʅO\xe0D\xbf\xf4\xba\x80\x13\x0f͚\xac\"O\x81\x19B\x17\x8e\xf9P\x90\xf1n\xa1\x1f\xc9>J\xda-v\xda>H\xb5\x9d\xb12\u03a2\xd4\xdd\"t\xbb\x16_\x84?\x97\x12\x1e\xdaT\x9fJ}\x00\xf2\xf9X\xc0\xb7\xbb\xc5%\x1ch\xb3\xfb\xa7Ǯ\xa3|X\xa5\x84s\f\x93m~Wʢlk\xbd\x9e\xb7\xadQDw\x8cj\xff\x99l\x87\xf9\xdcX\xc6h?K\xad\xda\x19*\xc1\xff;\xe9<\xcf_\xc2\xd8F~\x92sy\x7f\xfb\xe6sZT#/\xf1$Gj\x988>\xce:\xacf5\x9aY܍^ײ\x18\xed\xe6\x1c\xfeV\xb0\x906\x92\xec\x99\xf4\xef\xdd`s\x9b\xa0f\xaa\x81Þg\xe5\x9f\x1e\xb7\x99\x84\xaf\xdf\xc5>\x95\x16\x9e\xe4\xd7yU\xb8ǭ\x03\xb4\x04\b5\x1aֈ\a\xda\xcfb\xc6aPr\xba\xc0\x19\xc1\xa11\bhL\xc51=f\x11\x19\x88)\xffM\xecA\x17\xe8;Ɛ\xac(ۮԊ\xbc\x97\xea32\xe7\xfd\b\x91ߗQ\x87\x9e]\xa1\xd5FnS\xb7s\xca)\xd5T\x15\xae+Z\x82\xb7ͱ\x9a\xeb$#\xefy\xcbi\xfa\xdf\xf7\xb6\xb6\x1a~\xa6\xc1\x98\xa7j\xd0v\x9c\x12C\xaa\xa9\xa7\xa8\xcc\xe0A\x1b\x89\x99yK\xceO\xac\x97\x17\xae\xaf\x9fccQ)/)\xb9c\x19\x9c\xabJ\x93\xa2\xa7\x04\xbe\xadL\xbd\ueabc\xacП\xe1\x1b\xb8\xba\xe7rd\x88\xf7,\xdf.\x19\xed\xe9u\x97\xdb)\xa3\xc5hf\xe8\x06G\x8b\x91\xbe'\xf5\x90BC\xfb\x19]\xa4\xf8Ȗx\x1a\x83\xa3o\x9f\xde8\xed\xbe\xb4\x8fą\x9d\xf1$\x0e\x8d\xfeK$\xfej\f$\xf4~\xadHF!k:\x94\xfeC_\x17\x8b\xbb5\x81\xb1d0\xdb\x15\x82йw\xa1\x85\xf9\xa5\x8b\xc0\xa4\x83Ƒ\b\x1d\xb4\xc9\xdd\x13\b\xed;\x93@O3>\x7f\x99\xbf\xc87\xa6\xe2\x9b_\xff\xa5\xe4\xa2.\xd5\x14̔\x85\xd8r-<ᴏ\x8d9\x8eu\xe0\x0e\xfc\x8a\xd0H\x84*\x94\x8b\xe4\rʊ\x04\xb4/\xd9τ\xb2\xa6\r\xa78\xd1ǵ}\x9c\x84\xde\xf1\xfa\xef\xb4$3L\x98&<\x7f\xa40\xc7O\x8dg$y;\xda\x0e\xa5\xae\x92\xbcTS\xafɲa\x86\aOP\xb4㺿(Qm\xb3N\xae}\xb0#\xa8\xd0yXw\x1f\x06\xe4\x88\uffd8\x8e)\xeb\xbfpv\xa3&\xe7p{Ν\xff\x18w\xc5\xce]:\x02\xb8֍\xcf\xdb\xef\x97.\xb9\xa0\xe7u\x0f\xb3M\xb1\xa1\xf7C_\xb6\xcen\xd3TU8ӏ\x1b\xdd\a\x1c\x01\xab5\xe53\xfe\x13\xad\xc3S\b\x96\xe8α\xea\x8e\xf7\xe4\xfc\xf1!؝t\xc8p\"\xb0\xbf\xa5]f\xb6\xf5s\x99\xa5\xbb\xe4<3K\x93/1\xfa\x8b\xb17\x9e\xe3\\\xbb\x96\x85y\xf8\xce!\xb3\xf6}\xf0*\xcfbv\xc2\xef\x12\xb7y\xe8\xadw\x96\x17>[\x98\xd8\xdf0\xff@%\xfab\xcb5!\xba\xf3\xad\x06EH\xa9\x91\x96\x9e\x04\x82\xeb\xf2\x1a\x84t\xa6\xc2\xfd\x81\x96P\xfa\xb1\xa9\xe6\xdfG:\x8bj=\xa6\xa1c\xa9\xec\xe9\x0e\xf7\xe1k\x91|]{\xda_\xc0\x19\x9f\x11\xd6\xf5qg\xf8{\xdcp\"\x15w\n\x8d+\xb5\xbf}sF5V\x87\x8d\xad=vee\b,\xe1\xe9-mJ\xaa\x90A\xb5\xf3n\xcfr\x16Ï\x87.\xd1\xe2\xd5\x00\u0099\xb8\x9f\xbee\xcaE\xd7\x15{\x01v@\xe1a\xf7f\xfc\x05NjC\x90A\x9f\x1a\xe41\x1e\xe5\xba\nZ\x85:B\xdb\xe9+;\x9c\r\xe4C\x82\xfe\xcc\x18\x9eU\xa7\xc9d\xc0\\\xf4`\xa77\xcd\xfeL\xb3><\xf7/\xe1\xd7߮\xfe\x17\x00\x00\xff\xfff=C\x19\x96(\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z͒\x1b\xb7\x11\xbe\xefSt\xad\x0f\xb6\xab4d\xa4$\xae\x14o\xd2*Nmbo\xb6DI\x17\x97\x0f\xe0\xa09\x03s\x06\x80\x01\f\xb9\xb4\xe3wO5\x80\x19\xce\x0fH.\xa9\x925\x17i\xf1\xd3\xf8\xf0u\xa3\xbb\xd1`\x96e7L\x8b\x8fh\xacPr\x01L\v|r(\xe9/;\xdb\xfc\xc3΄\x9ao_\xdel\x84\xe4\v\xb8k\xacS\xf5;\xb4\xaa19\xbeŵ\x90\xc2\t%ojt\x8c3\xc7\x167\x00LJ\xe5\x185[\xfa\x13 W\xd2\x19UUh\xb2\x02\xe5lӬpՈ\x8a\xa3\xf1\xc2ۥ\xb7\x7f\x99\xbd\xfcn\xf6\xf7\x1b\x00\xc9j\\\x80V|\xab\xaa\xa6F\x83\xd6)\x83v\xb6\xc5\n\x8d\x9a\tuc5\xe6$\xbc0\xaa\xd1\v8t\x84\xc9q\xe1\x00\xfaQ\xf1\x8f^λ \xc7wUº\xff$\xbb\x7f\x10\xd6\xf9!\xbaj\f\xab\x128|\xaf\x15\xb2h*f\xa6\xfd7\x006W\x1a\x17\xf0@P4ˑ\xdf\x00\xc4}zh\x190\xce=s\xacz4B:4w$\xa2e,\x03\x8e67B;\xcf\xcc\x18\"X\xc7\\c\xc16y\t\xcc\xc2\x03\xee\xe6\xf7\xf2Ѩ\u00a0\r\xf0\x00~\xb1J>2W.`\x16\x86\xcft\xc9,\xc6\xde@\xf1\xd2w\xc4&\xb7'\xcc\xd6\x19!\x8b\x14\x8a\xf7\xa2F\xe0\x8d\xf1\xaa\xa5\xfd\xe7\b\xae\x14v\no\xc7,A4\xceo<\r\xc6\xf7\x93H\xebX\xadǨzS\x03,\xce\x1c\xa6@ݩZW\xe8\x90\xc3j\xef\xb0\xdd\xcaZ\x99\x9a\xb9\x05\b\xe9\xbe\xfb\xdbq>\"a3?\xf5\xad\x92Cr\xdeP+\xf4\x9a\x03\x12\xd2V\x81&ɐr\xac\xfa\x14 \x8e\x04\xbc\xe9\xcd\x0fH\x82\xdc~\xfbY(dz\xa0\xd6\xe0J\x847,\xdf4\x1a\x96N\x19V \xfc\xa0\xf2\xa0\xc2]\x89\x06\xfd\x88U\x18A'\x18\x04\xe9N\x99\xa4\xea4\xe6\xb306\nke\x8d\xf47\\\xe8\xb3\xd8Wn\x90%\xed\xabuE3?B(\x996\xb2\xd7\x05>\xcb\xc0\xfaDJű\xc7\xda\x04\x97\xb0\xa0\x8d\xca\xd1\xda\x13\x86OB\x06H\x1e\x0e\rg)*яi\x015\xbaR\x8c\xa3\x01\xa7\xa0d\x92W\x18t\xe8\f\x93v\x1d-c\xaa\xc2v\xda\xfb\xbd\x1eB\xf9\xd0\xca\xeb\xf5L0\x85\xa1ۗ\xc1\r\xe6%\xd6l\x11\xc7*\x8d\xf2\xf5\xe3\xfdǿ.\a\xcd@\xb4h4N\xb4\x9e9|\xbd\xc0\xd3k\x85\xe1\x9e\xff\x97\r\xfa\x00h\x810\v8E \xb4\x9e\x8b\xe8_\x91GL\x81#a\xc1\xa06hQ\x86\x98D\xcdL\x82Z\xfd\x82\xb9\x9b\x8dD/ѐ\x18\xb0\xa5j*N\x81k\x8bƁ\xc1\\\x15R\xfc\xd6ɶD8-Z1\x87\xd6\xf9\x83h$\xab`˪\x06_\x00\x93|$\xb9f{0HkB#{\xf2\xfc\x04;\xc6\xf1\xa3\xb7&\xb9V\v(\x9d\xd3v1\x9f\x17µ\xe18Wu\xddH\xe1\xf6s\x1fYŪq\xca\xd89\xc7-Vs+\x8a\x8c\x99\xbc\x14\x0es\xd7\x18\x9c3-2\xbf\x11\xe9C\xf2\xac\xe6_\x99\x18\xc0\xed`ى\xa2\xc3\xe7\x83\xe8\x05ꡨJ'\x81EQa\x8b\a-P\x13Q\xf7\xee\x9f\xcb\xf7\xd0\"\t\x9a\nJ9\f\x9d\xf0\xd2\xea\x87\xd8\x14rM\x86O\xf3\xd6F\xd5^&J\xae\x95\x90\xce\xff\x91W\x02\xa5\x03۬j\xe1\xc8\f~m\xd0:R\xddX\xec\x9dOY`E\a\x8a\xfc\x00\x1f\x0f\xb8\x97p\xc7j\xac\xee\x98\xc5?YW\xa4\x15\x9b\x91\x12\x9e\xa5\xad~\"6\x1e\x1c\xe8\xedu\xb4Y\xd4\x11Վ\xfd\xdbRcN\x9a%ri\xaaX\x8b\x18I\xd6\xca\x00\x9b\x8c\x1f2\x95v\x01\xf4%#\xcax\xd09\xb3\xa3\xefMJP\x8bX\xf6\x1cy\x8cw6\x06\xaaj\x18\xa8\xfa\xdf$F\x1a\xd4\xca\n\xa7\xcc\xfe\x10)\xc7&qT;\xf4\xe5L\xe6X]\xb3\xbd;?\x13\x84\xe4\xc4;v&M\xce(H\xf5@\x95,\x14\x1d\xb2\x89:\xe0\xde\xd18\xb2s\x8b.\xbdYy4\xb2\t\t\x87\x1c\x13\xfa\xb9\xe4x\xdb+\xa5*dc6\xb5\xe2g6\xfd\xa8\xa2\xe30\xb8F\x83>\xfe\a7\xab\x95wƎ\tٺ\x8f\x90r\x83S\x89}\xac\xc8\xdd\x1cS\xcdq;\x84\x13!)\t\xf8\xf5\xe3}\x1bvZˊ\xd0'\x91\xa5\xcfO\xd2,\xe8[\v\xac\xb8\x0f\xd4\xe7\xd7NZ\b}\xf7\xeb\x00\xc2\xfb^\xa7\x80\x81\x16\x98\xe3 \ue050\xd6!㱑܍\xc1\xd8\xf7\"\xf8ԣ \xe9;\xc4GR\t0\xf2\xf1\x82ÿ\x97\xff}\x98\xffK\x85}\x00\xcb)\x13\xf2w\x15\xacQ\xba\x17\xdd}\x85\xa3\x15\x069\xdd>pV3)\xd6h\xdd,JCc\x7fz\xf5s\x9a?\x80\xef\x95\x01|b\x94\xf4\xbf\x00\x118\xef\xc2Fk5\u0086\x8dw\x12a'\\\xe9\x81j\xc5\xe3\x06w~\v\x8em\xe8Ą-4\b\x95\xd8`\x9a}\x80[\x9f<\x1d`\xfeN.\xe5\x8f[\xf8&8\x89[\xfa\xf36\xc0\xe8\x12\x84\xbe\xd79\xc0q%s\xe0\x8c(\n<$\xda\x13c\xa1\x80F\xa1\xe0[P\x86\xf6*UO\x84\x17Lz\n\x8e\x18\xf9\x04\xdeO\xaf~\xbe\x85o\x86\x1c\x1cYJH\x8eO\xf0\x8aθ\xe7F+\xfe\xed\f\xde{;\xd8KǞh\xa5\xbcT\x16%(Y\xedC\xbe\xb9E\xb0\xaaF\xd8aUe!\x15\xe3\xb0c{P\xeb#\xeb\xb4*\"\xd3d\xa0\x99q'ӱ\xc8\xc3\xe9C3\xcdO\xda\xefy\xe7\xc5\xe7+\xcf:\xbd_,\xd6?\x93\t\x9f\x98\x7f\x02\x13\xfd\xab\xce\x15Ll\x9a\x15\x1a\x89\x0e=\x19\\\xe5\x96x\xc8Q;;W[4[\x81\xbb\xf9N\x99\x8d\x90EFƘ\x05\xad۹/\xd9̿\xf2\xff\\\xbbq_g\xf9\xd4\xdd{!_\x8e\x02Z\xddίa\xa0ͣ\x9f\x1f\xbb\x8e\U000b0319\xddX&\x9d\xf9])\xf2\xb2\xbdU\xf5\xbcm\xcdxp\xc7L\xee\xbf\xd0\xd9!\x9e\x1bC\x88\xf6Y,8fLr\xfa\xbf\x15\xd6Q\xfb5\xc46ⓜˇ\xfb\xb7_\xf2D5\xe2\x1aOr\xe4\xb6\x10\xbe\xa7\xec\x80*\xab\x99\xce\xc2h\xe6T-\xf2\xd1hʕ\xef9)i-М\xc9\xfe\xde\r\x06\xb7Y{\"\xeb\xee\xc6\\\x94v[ɴ-\x95\xbb\x7f{\x06Dz\x1b\xd8b8\xe80&\x9d\xad,:\x12's\xcdg\xe0Y\x8a\xdf\x12n+\x89\x88\x86\xb6\x98*U\x88\x9cU`}\x9b\x8c\xc5\xca\b\xb3\x95=\x05\x94\xaaG\x8e\xe1\xf6\xab\x8a=\xbc\xde\x17<\x1c\xf7\xb4C\xc8\xc3\xd1-jeD!$\xab\x0e\x1e\xdb_\x1d%\xab\x99\xff+a\xab5\xd3Z\xc8\xe2\"n\xdb\xfa\xd6\x12\x9d\x13\xb2H$\xfa\xfd\xf2\xfb\xa9\xeb\xc0\xc9sr\xde\x05|\x18\x01\x01f\x10\x18\xed\x89T\xb5\xc1}\x16\xb2N\xcd\x04\xa5\x8c\x94\x15\xc6\xd4z\x85\xc0\xb4\xae(\xaf\v\x99d\xca7\xb5պ\\ɵ(b\xe5tʔl\xaa\x8a\xad*\\\x803ͱK[\xf2\xb8\xf7\v\x85g4\xfe\xa17\xb4U\xf7\x99RezW\x83\x02\xe6t3(\x9bz\n%\x83\x8d҂%\xda\xe9pN\x1c\x13u\xdc\xde^bR\xe1\xe4\x9f\xe1 ܙS\x05\x87\xe88\xe25$^\xb1\x83\xfbHG\xf3K\x1d\x8a\xc1_\x1b\xbaS\r\x11f\xe9\xda\xcah\x8cV\xfcfLZ\xdf\x17\x8f:\x0f\x9et\xdc1<\xf4\xa3\xde@\xc1\xb3\xcaR\xbeP~Ia*<\x87E\xdeC\x1a\xe0\xdaG2\xba`\\]\x9a\xa2;\xacvȻ7\x84k\xea6\xaf\xc7B|A\xd9\xf0xHD\x8d]\x91#ډ9\x94]B\x88\xd1\x065KZ\x04\xf8G\x01\xeb\v\xa3_\xdb MXh,r\xef['\x8b\x1f\x8d\t\x9c9\xcch\xfeu\x0e$]\xec\n\xcfs\xfdW\x98\xab*_S1S\x0eYG\x9b\x7f\x1fj\x1f\x06S\x94\x1d\xe4u\x84\x05q\xc8\xfd\x95\x1b\x94\x845\x13\x15r\xe8\x1e\x9f/f>\x01z\x9a\x8c}N\xf2k\xb4\x96\x15\xe7\x9c֏aT\xa8\xbc\xc5)\xc0V\xaaqG\xac\xf2k\x1b\x8f\xd6E1Y*~\x0eɃ\xe2\x1e\x86<\xfe\xe46E\x93PK\xff\x19\xee\"\x8c\xbe\xa8y\xaeHIcR\xae\xa6\x83|\xda\xd7\xc0\x89\x18\xf6\x80\xbbDk{\x82\x13]\x8f\xd1-$\xba&\xbf\a\xe8w\x86Jr*\xa7i\xfb\x922\xbb\xc7\xf6D\xdf\xf7\xfe\xb8\\\xc4v\xc4w\x8dC\xe8\xeaХ\xaaZ\x1f\xe0\x1f\xc9eS\xafА*V\xa9\x8c\x18\x98\xe4}ͥ\x8a\t\x9d\x846\f\aQ\xb1\x1e\x16\v\xe8\xfe\x94;\x05\\X]\xb1}\xb7\x19\x7f\x83\xa3#\x9d~N8\x9c\xab\xd6WQ\xe49\x92\xb7\x9d\xaeTw?ZH\xdfOOg\xfap&\xdb\xf7\xfdݏ\x11>\xcf\n'\xf2\xce\xe1\x8fC\xae1\x90\xe5@¹`\x11\x7f\xacr\xb9\x8f\x1f.\xf3g\xba\xf7${\x93F\x8f\x9c\xf7d\xc7'\xaf~K\xb3\xeaރ\x17\xf0\xfb\x1f7\xff\x0f\x00\x00\xff\xff;\xa8N\xc3\x13&\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ks\xdc8sw\xff\n\x94sؤJ#ǕG\xa5ts\xb4v\xac|\xdf\xca*\xc9\xf1\x9e1d\xcf\x10\x9f@\x80\v\x80\x1a\xcf&\xf9\xef)4\x1e|\fHbF\x1a\xednjqQ\x89$\x1a@\xbf\xbb\xd1\xc0\xacV\xab7\xb4a\xdf@i&\xc5\x15\xa1\r\x83\xef\x06\x84\xfdO_>\xfe\x9b\xbed\xf2\xdd\xd3\xfb7\x8fL\x94W\xe4\xba\xd5F\xd6\xf7\xa0e\xab\n\xf8\x116L0äxS\x83\xa1%5\xf4\xea\r!T\bi\xa8}\xac\xed\xbf\x84\x14R\x18%9\a\xb5ڂ\xb8|lװn\x19/A!\xf00\xf4\xd3?^\xbe\xff\xd7\xcb\x7fyC\x88\xa05\\\x11\x05\xdaH\x05\xfa\xf2\t8(y\xc9\xe4\x1b\xdd@aan\x95l\x9b+ҽp}\xfcxn\xae\xf7\xae;>\xe1L\x9b\xbf\xf4\x9f\xfe\x95i\x83o\x1a\xde*ʻ\xc1\xf0\xa1fb\xdbr\xaa\xe2\xe37\x84\xe8B6pEn\xed0\r-\xa0|C\x88\x9f:\x0e\xbb\xf2\xb3~z\xef@\x14\x15\xd4\xd4͇\x10ـ\xf8pw\xf3\xed\x9f\x1e\x06\x8f\t)A\x17\x8a5\x06\x11\xf0?\xab\xf8\x9c\x84\x89\x12\xa6\t%\xdfp\xa1v6\x88xb*j\x88\x82F\x81\x06a41\x15\x10\xda4\x9c\x15\x88w\"7=H\xa1\x97&\x1b%\xeb\x0eښ\x16\x8fmC\x8c$\x94\x18\xaa\xb6`\xc8_\xda5(\x01\x064)x\xab\r\xa8\xcb\b\xa8Q\xb2\x01eX\xc0\xb2k=\xde\xe9=\x9d[\x98m\x16\x17\xae\x17)-\x13\x81[\x82\xc7'\x94\x1e}Dn\x88\xa9\x98\xee\x96\x1a\x96G\xa8 r\xfd7(\xcc\xe5\b\xf4\x03(\v\x86\xe8J\xb6\xbc\xb4\xbc\xf7\x04\xca\"\xab\x90[\xc1~\x8d\xb0\xb5]\xb8\x1d\x94S\x03\xda\x10&\f(A9y\xa2\xbc\x85\vBE9\x82\\\xd3=Q`\xc7$\xad\xe8\xc1\xc3\x0ez<\x8f\x9f\x90xb#\xafHeL\xa3\xaf\u07bd\xdb2\x13$\xaa\x90u\xdd\nf\xf6\xefP8غ5R\xe9w%<\x01\x7f\xa7\xd9vEUQ1\x03\x85i\x15\xbc\xa3\r[\xe1B\x04J\xd5e]\xfe]$\xea`X\xb3\xb7<\xaa\x8dbb\xdb{\x81\x02q\x04y\xac\xa88\xc6s\xa0\xdc\x12;*\xd8G\x16u\xf7\x1f\x1f\xbe\xf6\x99\x92iO\x94\x1eoN\xd1\xc7b\x93\x89\r(\xd7\x0fY\xd3\xc2\x04Q6\x92\t\x83\xff\x14\x9c\x810D\xb7\xeb\x9a\x19\xcb\x06\xbf\xb4\xa0-\xbf\xcb1\xd8k\xd4:d\r\xa4mJj\xa0\x1c\x7fp#\xc85\xad\x81_S\r\xafL+K\x15\xbd\xb2DȢV_\x97\x8e?v\xe8\xed\xbd\b\x1aq\x82\xb4^\x8b<4P\f$\xcdvc\x9b\xa0.6R\r\x94\x8c\xed2\xc4QZ\xf8msZĪ\xc5\xf1\x9b%.\xb3\xed\xdfco\xcbovf\xad`\xbf\xb4\x80\xcaԉ?\x1c\xea+\xd5S\xed\xc3f\xd9hL\xddID\xdb\x06\xdf\vޖPF\xbd~\xb0\xc0\x9ce|<\x80\x82F\x8f2a\x85\xc8Z\x1f\xbb\x16ѽE\x05N\x15\x10!M\x02\x1e\x13\x0e\x1ea\x021\x90\xa4\t~h\xa0N\xccxvɄ\x88\x96s\xba\xe6pE\x8cj\x0f\xd1\xe8\xfaR\xa5\xe8~\x02[\xc1\x03x\x16\xb2\"\x10\xafj8+\x90\xe4Q\xa1 \xbe\xfe\xb8\xa8b\xda*ʰ\xca;\xc9Y\xb1_\xc0\xd7\xc7d\xa7 \xad^v\xfd\n\xc9\x1a*\xfaĤJ\x89\x81T\xf8iϞwjZZ-遌m\\悓Ȫ\xa4|\\b\x88\xcf\xf6\x9b\xce:\x90\x02\x1dʸ\x14Omo\xbb\xd7@\xe0;\x14\xadIL\x93\x90\xb2E\xd3$\x15i\xa46\xd3t\x9fV]\xa4\xef\x1c\xa5^\xce0\xcd\xc1ʒ\xac\xee\x9aW\u0081\xa8\x16\a\x03\x85,\x05\xd8eԖ\xa8ݷJ\xb6\xee\xdbI\xa4\x905\xd5P\x12)&GFvi9h?V\x89\x9c\xd1顋n\xfd\xe8\xf1\x10N\xd7\xc0\x89\x06\x0e\x85\x91\xea\x10\x999(u-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\xd7\x16\xc4j\f/\xa5Q\xba\x96\xa1\x86\xbb\x96Dm\xa7{\x0ft\x8b\x7fn\xe4\xec\xb2\xff\x7f\"6\x18\x93\x13\x98vF\xfe\t\xba\x9f\xd9<=ɷ\x18ၾ$7\x1b\x02uc\xf6\x17\x84\x99\xf0tI\x12(\xe7\xbd1\xfe\xc0\xb49\x9e\xe93I\x93#\x13g\"L\x1c\xe2\x0fH\x174\x19\x0f\xdebd\xd3\xe4\xaf\xfd^\x17\x84m\"\xd2\xcb\v\xb2a܀\x1aa\xff$U\x1f(\xf3\x12\xc8ȱz\x04\xf3\x04\xa6\xa8>~\xb7.\x8e\xee\x92`\x99x\x19wv\xbeq\x88 \x86\xe6y\x01.\xc1x\x99)\xa81\x0e'_\x11\x9b\xdd\x13t\xaa?\xdc\xfex\x18+\x8f[\x06\xe7\x1d,dA\xe8\\\xfb0ZQ\x7f~>*\bo\xd0\a\x8aA\x95˹\\\x10J\x1ea\xef\\\x17*\x88\xa5\x0f\r\x1fg\f\xaf\x00\x93?\xc8g\x8f\xb0G0\xe9l\xcea\xcb\xe5\x06\xd7\x1e!\xe1\xfa\xa7\xda\x00\x87vN>,vx\xb2\x0f\x10\x11\x18\xc3粁k^\x14\x12\xb9\x93t\xcb\xd4%\xa1\x05ܟ\xb0\xcc,V\xe9\x8f\xd1O}\"\a\xfc\xa0\x1d-\xad\xc4T\xcc\xe745\xa0\xcc\xe4\x12Եo\x94\xb32\x0e\xe4d\xe4F\\\x90[i\xec\x1f\f\xd042ʏ\x12\xf4\xad4\xf8\xe4,\x18u\x13?'>\xdd\b(h\xc2iy\x8b\xb0~\xce\xcf\xd94\xcbm\x11\xf7L\x93\x1ba\xe3\x15\x87\x92̡0\xbd\xeb\x86s\x03խ\xc6t\x9d\x90b\x85639\x92ǷT\x03t?{P?\xe0Wk,\xdc\x1b\x97d洀2D\x96\x98\xfd\xa4\x06\xb6\xac\xc8\x1c\xaf\x06\xb5\x05\xd2X\x15\x9e\xc7\x11\x99\x8aկ\xe68\xf6ɳ\xde\xfd\xf6}\xf5\x18\xf3\x05+krV\x1e\x82\x91u\x06\x0e\xbc\xee.\x97׳\xb22\x9b\xf1U\xe0\x84\xc5O'\x92\xa3ӟ\xe6 \xe5\x19\xe8@+\x8e.\xce\"uiY\xe2\x16\x1a\xe5wGX\x94#x\xe1X\xd5Л\xbb3\xc15m\xacZ\xf8okiQ\x9a\xfe\x974\x94)}I>\xe0N\x19\x87\xc1;\x9f\x87\xeb\x81\xc9\x18\xb2\xb1CY\xfey\xa2\xdc\xda~\xab\xc0\x05\x01\xee<\x01\xb99\xf0\x8b.Ȯ\x92ڙ\xed\r\x03\x8e\xfb\x15o\x1fa\xff\xf6\xc2\x0e\xbf8d_ɼ\xbd\x11o\x9d\x0fq\xa00\xa2\xc3!\x05ߓ\xb7\xf8\xee\xeds\\\xa9LN\xcd\xfcl\xc0\xa25m\xf28T$\x93\xf5]\x1bpL?7\xdf%当=\xb7\xda,\x16m\xa46\x9f\xd3yÉ\xf9܅\x1eC\xcf8\x91c[\x8c\x18|\x1e-\xea{\xebDn\f(\x9fKt6 \xc4\x1fό\xccR\xbb2\xfd\xc9\xc6d \x8d\xf9]\x8b\xe0\x05nr\x1b79S<\xc6a\xb5x9\xd2\xdb\xff\xf8\xbd\x97ϴ\x92k\xff\xef/\xe4\xa5\x1d\xeaB\xd65\x1d\xefjfM\xf5\xda\xf5\f<\xed\x019\xea\xabm\x8b\xf2\x9ck\x91;\x1e\xc2\xfd\xcb\x1d3\x15\x13\x84\x06\xb5\x01\xca3\x14%\x8dL\xe5\xb0S\xad\xa2\x9a\xac\x01DL\xd1\xff\x1e\\\x89\x9a\x89\x1b\x1c\x80\xbc?\x83\xeb\x11\xd1uNg\xf7:\xd2$R>>p&\xab\x91%\xd9U\xa0`\xc0\x18\x87yw\xf4T\x854\xbd\x94\xc5\x11\x0ei#\xcb\x1f4\xd90\xa5M\x7f\n\x9a\xb4:\x97\xd6G\x92\xcf\xce\xfb+\xabA\xb6\xe6\x9c\b\xfe\xd8\r3\xd8k\xae\xe9wV\xb75\xa1\xb5l\x9d17\xac\x8e\xbb\xba\x1e\xbd;\xcaLܶ\xc2\xfc\x8d\x91\x96\x04\r\a\x03d\r\x9b\xf4~o\xaa\x15RhV\x82\nU\n\x8elLZ\xc1\xdcP\xc6\xdb\xd4.Q\xaa\x1d\x1b\x01\x8b\x8fJ\x9d\x14\x00\x7fq={y\xc7J\xee\x86\b\xca\\;n\xa4\x01a\x1b\xc2\f\x01QX\x8c\x83r*\x19\x87\xf0\xc8@\u0530\\=\x97\xa7\xc0m\x03\xd1\xd6y\bX\xa1@21\x9br\xeb\x7f\xfe\x892~\x0e\xb2Y\xce\xfb$\xd5=\xd0\xf2\x94\x1c\xcdϽ\xee\x04\x84n\x15n\xfe;ݱc,q\x85o\x13\xc5\r\xc9\xd5\x1d\xef\tfQ6\xb4AЉyX\xf5\x04\xabV<\n\xb9\x13+\f\xc6\xf5\xd1:\xe4\xc4,\xd5s\x877'+\xa3e\xfd\x92\xaf\xa6\x97\xb4А_\xf3y*\xf8Og\xd02\xd9|sT\xc2c\x8e\v\x96\xf4\x9a+\xc0\x9ex\xb98\x8b\xb9\xf1g:\xfbM\xe9kW,\xfd\xac\xb2\xb8\x9b4\xa8\x9eS\xb8\xab\xc0T\xa0Bi\xf6\nK\xd2\xcb\xd9\x1d\xd2.x\x89ur\x96\xa9\x82\x8b\xec\xca?G\x95s\x18ݴ\x9c_Xަ-O\x86\xc3F\xa2\x88\x1drVV\xfdX\xdacȩ\xbe\xc8\xc6c\xbf\xd2bX_\x18\xab B\x81\xa1\f#{\x1a\xa7\u058b\x85\xa5\xbd\xfd\xfda9\x05\xe6\xff\xc2\xf4\x7f\xf3\xd2ÌJ\x89|4\xe6ViF$&`%\x18\xac\x87Ʈ\xbe\xc2\x7f\xe7\v}\x7f_85P\x7fi\xbc\xc4L\xba\xb0\x19hM\xc0\x19՛\xa05h\xb5s\x05\xa2\x1d\xf09C\xdb\xffC\xe1NA\x040)~\xfdZA\x10__\xbd\xcf4\xf9gR\xc96Q\xd57\x83\xb2\x85\xea\x8e\xe5\x05\x0f\n=\xfc\x86\x02\x18\xfa\xf4\xfer\xf8\xc6H_\xf6\x81Y\xb4\x04 \f\x8a\xba\xcc,\x13%{beKy\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\x8b\x17\xa9\x82\xfdMj=\x8e\xaf\xf0ȉ$\x16\xaa9N\xa8\xe1\xc8,\x16{\xf6~KN\x95\xc611\xf7\xd9*2^\xbe\x0e#\v?\xcb5\x17\xc7`\xe7\xec\xf5\x15\xafXU\xf1:\xb5\x14\x99\x15\x14/W\n\x99\x17}\x9eT\n\xb0\x1c\xb0LWA,\xd6><+\xa09iI\x8b5\r\xc7T2,R'O\xcc^\xadV\xe1\xd5*\x14^\xb7.a\x96\x8bf_\x1eSy\x10㤟h\xd30\xb1=d\x8a\\֙e\x9be\x96\xb9\x1dMd\xc03\xfdp\xa6\x8b\x0e'B_w\\:\x11I\x86\xb4%\x13F^\x92\x0fb\xef\xe1&\xe0\xf4\xc2G!\xcd\xc1A6;\xad\x1d\xe3\xbc\x7fZ\v\xc1\u0383\xf2g&5\xadݬ\xa6\xbc\xfd$]\xa5\x1a8\xe5'\x05\x8e_F0\xfa\xd9\xd1\xd7\xf4\xfc\xeb\x96\x1b\xd6p\xb0\x1e\xdd\x13+\x93g\xc8L\x05\xfb\x88\xe4\xbfI\xfb\xa5\x05\xb5'\xf2\tK\x18\xbc\xf7֝U\xf0\xeaF\xdb\x183(@\xaf\x8c\xa76\x15\x0eB\x99NA\x91\x0f\xc2\xf9\x12\xe3\xf9`\x1f\xab\xf9\xbaPͪs\x1b\x85%ǘ\xe8.d\xec\x9d\xe8\xb6\xe4\xf6\xe7\x16\xf5\x9f7p;>t[\xf4\x95\xf2\xfd\xd9ߨX\xff\x94\"\xfd\xbc\xed\xa0Ţ\xfcs\x05rK\xa1\\\xb6\xf7\x9aWt\x7f\xdc&\xea\x19\x8b\xec\xcfQ\\\x9f\x89\xa9\x9cb\xfa\xe3\xf0\xf4\n\xc5\xf3\xafZ4\xffZ\xc5\xf2\xd9E\xf2Y\xfb\x98ٛV\xb9ی'V}/\xef\xba\xcf\x17\xbdg\x14\xbbg\xec\xa4-/\xf2\x84\xe5e\x14\xb3\x1fWĞA\xb3\\Q|\xc5b\xf5W,R\x7f\xed\xe2\xf4\x05\xceZx}\\\x11\xfa\xc9;0a\xab\xffV\x96p'\x95Y\nN\xee\xc6\xdf'vR{\x01\x9b\xe4%\x11\xe1\xd3\xc4*1\xc4\xf0\xe1\xc5i\x8bJoz\x06w\xfa'Yڹ-\xed\xb1\u070f>?8\xab\xbc\x01\x05\xc2]\xf3\xf1\x9f\x0f_n#\xfc\x94\xcf\xeb=\xe3\xd1\xf5\x12\u0383)=r\xfc֜/fr\xd8B\x1f\xe0\x85\xf7Eh\xc3\xfe\x03ou{F:\xe8\xc3\xdd\r\xc2\b~\x1a^\x13\x17\xab(\xe2\x8e\xe5\x1a\xacŊ\xa8\x9a\x14\x8b\x9b\xcd\x00\xe2\xb0\xe2\xb7\x7f\x8d\x12\x94\xeeʬ`1Y\xa8\xf1\xb2\x82ww\xe3\xe615\xca'\xeb4\x8a=\x91\x8e#+\xa6\xcaUC\x95\xd9#\xdb\xe8\x8b\xc1\x1c\x82\x99\x99K\xe7L*\xd6\xc3k\xc0\x92\xe8\r\xb7\x7f\xe1^\xe4\xbe\x19\xee\xf6\x8eqw\xca<\xa6ϟ,\x9eg\x91\x0e\xc0`\x9d\xac\xa8z\x1e\xe4\x0e\x82\x8f\x19\x96\x8dҊݒ\x1a\x1c\xb8?i\xc5\xf8E/{\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\xa1\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(\xc3\xe7L\xc7\x19\x1fU\xfb\xd86\\\xd2\x12\x94s\xc9\x16\xd0\xfa_\x83\x8fG<[\xe0\xc3Vu\xd7\xed\xce^U\xfa,\xcd\xd5PE9\a\xfe\x89q\xd0?ʝ\xb0\xf3\xca\x10ȻT\xbf\xdeY٢U֬\xef\x89h\xeb5(\xa2\xc1\x98\xe9\x04\xdeF\xaa\xf9S+\x0e\xefL\x18\xd8B*\xe7\xb9S\xcc\xc0CC\x95\x06\x9cQ\xc6\n~\x1euq\x19\xc1\r\xa7[W\x9e\\\xb2\x82\x1a\x88\x06\x18G\x98\x9a>\xf6\xd7\b\x8b\xef\xb1ZTNlDd\v\xf5\xd41\xb9I\xb1\x9e\xba\xf29a\xaa\x93\x97>;\x8b\\\xd0\xc6\xe0\xa1D\xa4#\x12\xd1x\x18x\x91\xfa\xe8\xde\xe7\x01\xd8iN\xf3GK|\x11\xb36\xb4ND\t\xcbz\xe7\xfa\x10\f^ծ\xca^-t\xff\xd2\xdbX\xf4LvT\xc7\x03.I\u07fb\x83\xed\xc0\xa0\xabnACI\xe0\t\x04\xb1\xa2H\x19\x87r\x8eS\xbf\xe2\xe6\x9ez\x02\xf5\x83\x8ep\xb0:۲\xf8\x83\xa1\xcaĩ\x1f\xfa1.\x86\xbb\"%5\xb0\xb2\xbdOs\xdd\xd2WW+ub\x89\x06\x9e6\xf6\xe2Q\x84\xa3\x90\xd6\xfa\xb93\xc25hM\xb7!1\xb8\x03\x05d\v\xc2\xe2=\xee\xf7$=\xa6p\xcc\xda\x1b\x8bAb\x80\x16\xa6\xa5~\x00\xe7\xc2Ŋ\x96pg\x0f \xc5\xf0V\x1aʃ\x91\xb1|\x19?\xa8f.uy\b\x17\xdas\xbe\xbf\x18C\x1e\xfdRF\a\xbb\xea\xaeW\xf6\x9a\xa0\xbb\xd2cb\xa0\xb0\x13\x93\x04\x12of\xee|\x92\xa9{p\x97\xec\x1fB\xfd\x84\x93\xca\xc0\xf1\xe7\xee\xeb)<\xbai:\x87\x19D:\xd2$\x18|\x98*J\xc6\tS\x9f\xf1R\x9b\x8a\xea%\xf7\xf4\xce~\x13ݎ\x9e\xb9\x8aN\xe8\xfd\x84T\xa6\xef\x1eX\x91[\xd8%\x9e:daE\x02JU\xe2\x93\x1bq\xa7\xe4V\x81>d\xba\x15\x9e1gb\xfbI\xaa;\xden\x99\xf82}\x1ag\xee\xe3;\xaa\f\xb3L\xeb\xe6\x93\xe8{\x1dl\\\xe2\xddr\xef\xe9\x17LP\xce~M\xe9\xf2\xfe˥\x11f\xf4]\xe3\x91w\x8a\x85\n\x88_R\x80^C\xff\xa0{\xe6'\x8c{IneR\x8c}\xd1\x0e\x1b\x02e\x9a\xacA\x9b\x15l6R\x19\xb7\xa7\xbaZ\x11\xb6\t\x0e\x92\xd5\x10\x18'\xba_\x18!,\xb5\x19\x1a\xcb!\x82ò\xf1\xa9D\x85V\aCΚ\xee]F\x92\x16\x85\x8d\t\xe0\x9d64\x15\x9b\x10\x9cC\x1d^1\xe2\f:\x9f\xaa3\x18\xdc`D\xb4\xc5\xde)ʄ85v3\x1dv癚\xaf\x11ʔz\xf4\xeb\x1b\xfc8\x82/z\xf1\x1fY\xb2\x15\x15\x15\xdb\xc9Cƕ\x92\xed\xb6\n\xbc9\xe5\x10\x91\xb2\xc5ȹAU\xa0Ï9\x99V\x89^!\x85\xaf{\x9b\xd2\xd2q\xba\xd3>\xca3\x14\xb5\xea\x0e\x1bv\xaaj\xc6\xe6gg\t' .\xda\xfe\x04D\xaa\xf7\xa2\x98=\x16y\xb8Gu\x94k\x99DB\xd4\xc6/\x86\x84\bq\n\t}_\xa2\x8bx~7\x18\x99\xf2QNDǼ\x13\x83K\x9c\a\xb5\xbc\xe8\xbe\x134tw\x8eC\x87\x1e\x04\x7f'\xa5\xdd\x06\x10\x8e\x89|q\xect\xdc\xfb\xfb\x8dX\x9f\xa2\xb7\xf5\xf1\xe4\xd8\xf5\xdb\b\xc6\xe8X\xba\x8db\xbbaB\xbc\xf9\xf7l\x93\x92\x17\xf7\x8byk\x0e\xffp\xf0\xf6\x95\x8f\x97\xef\xa8\x12LlO\xc2\xc8Ͼo\"\x9e\xf7`\xcf\x19ч\x99\xbfXL\x9f4K\a\x0f\x91\xc1\xcb\x1e\x9e\xfdH\xfe\xc9\xff\x05\x00\x00\xff\xff\xbc\x9a$\xa6\xd7r\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\xd5\by_\x17\\\xb7\xdf\xcf\x183\x99\xaa\xe0\x92}Ʈ*\x9eA~Ƙǟ\xba^0\x9e\xe7D\x11^\\k!-\xe8+U\xd4e\xa0Ă\xe5`2-*K#\xbe\xb1\xdcֆ\xa9\r\xb3[\xe8\xf6\x83\xe5g\xa3\xe45\xb7\xdbK\xb64ToYm\xb9\t\xbf:\x129\x00\xfe\x93\xdd!n\xc6j!\xef\xc7z{Ǯ\xb4\x92\f\xbeU\x1a\f\xa2\xccrb\xa0\xbcgO[\x90\xcc*\xa6kI\xa8\xfc\x91g\x0fu5\x82H\x05\xd9r\x80\xa7Ǥ\xffq\n\x97\xdb-\xb0\x82\x1bˬ(\x81q\xdf!{\xe2\x86p\xd8(\xcd\xecV\x98i\x9a \x90\x1e\xb6\x0e\x9dO\xc3\xcf\x0e\xa1\x9c[\xf0\xe8t@\x05\xe1]f\x1aHnoE\t\xc6\xf2\xb2\x0f\xf3\xdd=$\x00#\x12U\xbc6$\x1cm\xeb\xeb\xee'\a`\xadT\x01\\\x9e\xb5\x95\x1e\xdf:\xd9˶P\xf2K_YU \xdf]\xaf\xee\xfe\xfd\xa6\xf7\x99\xf5)\xfa\x7f\x8b\xe6;k\xb8\xc1\x84a\x9c\xdd\xd1,a\xdaO[f\xb7\xdc2\r(\x06 -֨4,\x02\xa9s\xa6t\aT\x05Z\xa8\\d\x81E\xd4\xd8lU]\xe4l\rȭeS\xbbҪ\x02mE\x98\x87\xaet\xd4K\xe7\xeb!\xf4\xb1\xe0\x88]+'\xa6`H2\xfdl\x83\xdc\x13\xc9M\x1ea\xda\xf1\x10\a\xf13\x97L\xad\x7f\x86\xcc.\a\xa0o@#\x980\x8aL\xc9G\xd0H\x91L\xddK\xf1\xbf\rl\x83S\u0092\xa4Z0\x96\xd1|\x96\xbc`\x8f\xbc\xa8\xe1\x82q\x99\x0f \x97|\xc74`\x9f\xac\x96\x1dx\xd4\xc0\f\xf1\xf8\x8b\xd2\xc0\x84ܨK\xb6\xb5\xb62\x97o\xde\xdc\v\x1b\x94n\xa6ʲ\x96\xc2\xeeސ\xfe\x14\xeb\xda*m\xde\xe4\xf0\b\xc5\x1b#\xee\x17\\g[a!\xb3\xb5\x867\xbc\x12\v\x1a\x88$Ż,\xf3\x7f\v\xfc6\xbdn\xf7f\xa6+\xa42g\xb0\au\xa9\x93.\a\xca\r\xb1\xe5\x02~B\xd2}\xfdpsە\xa0\x10PP\x10#\x15+\x94\xbc\a\xed\xb0h\f<\x1a\x18@\x01\xcd\x19\xfa\xea\x1aͲ\x90lS\xa3\x1b\xbfd\xa8%\xa22\"\xa4\xb1\xc0#\xc2|\x02\xde\xc17\xb4\b\x90_\x15\xb5\xb1\xa0o2UA\x1eV\xe6F\xcdY*\x0f?\x1c\x84샾Bd\x80|\xc8\\\xa5\x05\xad\x8c\xc5D\xbb\x8d\xff\xd0<\xd2B\x1d\xb2\xda\x0f\xa1\r\xec&u\x8b\x01\x8b\r\xcf\x7f\x7f~A\x12\xd0\xef\xbdߏa\\CC\xa6Y\xba\x99ܤ\xf1\x16\xc2B\x19\xa1\ue90e\x9a\xc1w\xae5\xdf\x1d\xe0z\xb3\x02\xf9\x02|\x8f\xc1\x1ep^\x86j߉\xf7\xc3\xfe\xff\x15\xb9\x7fZ~\x1bZ\xa9\xe7B\"\x9f\val\x8f\xcd\xc6-\xfd!Y\xc7\xe2nO \xe9`\xa2\x9a\x9c\xe2\xea?\b1O:wb\x93\xa5\x91M?\x01\xfe\xa9(\xb9U\xea!\x85z\xff\x83\xf5\xdau?\x96\xd1n\x12[Ö?\n\xa5\xcdpm\x19\xbeAVۨf\xe1\x96\xe5b\xb3\x01\x8d\xb0ho\xa4\xd9J9D\xac\xc31\x1f먬h\x85\xc1\xb8Z\xa6#K\x89\x1a\xb1\xa1PT\x1f\x85\xea\x1c\x1c\f-ȁ\xc8ţ\xc8k^\x90/\xc1e\xe6\xc6\xc7\x1b\xfcbZmB \xf6\xf0\x8fJ\xb5+Ρ\t\x83D&\xf6\x96\n\x95\x04\xf4\xf1K\x8c\x8d\xf6\xab\xc6)\x11\xd6_\x0e\xf6\x8d\xcc\xd4u\x01\xc6w\x97\x93\x9b\xdcꤋ\x96Yna\xa6\xe0k(\x98\x81\x022\xabt\x9cB)r\xe0J\xaaҍ\x10wD\xcb\xf6\xa3\xadv0\x13`\x19\x85\xb8[\x91m\x9d\xfb\x8a\x82F\xb0X\xae\xc0\xd0R\x12\xaf\xaa\"b\xba\xda2)\x1c\xbe\xb3)\xbdі\x04\r2\x84\x1b\xd3%mI\xd4\xcfm\x19%{;7\xfbT\x1f\xdf\x1c\x19\xc5\xf7_\x89\xe8\xc1\xea\x1c)\xec\x13\x9a\x84\xd1&K\xf2|\x88\x92\x1e).\xc0,;K\x9a\u0086\xaf)\f\xed\xf9\x8f{\xfbO{D\xf9u\xf1\xee\xb8\t3\x83u\x93s\xeae\x19\xd7t\xf3O\xc272Y7\xdeb\xcd\xe2٧n\xcb\v\xdaJ\xf1\f\xc9/\xd8F\x14\x16ȩ\x9aB\x94\xcd\xe0\xdc)\t\x94j\x81\x19\xed\xac\xdbl\xfb\xa1\xd9pKh1\xa0\xd5\x10\x80s\xd0C\x94C\xf8DDE\x1b?\x1az\xcc\xdd\xdf\x13!\xefZ*\xdbYƙ\xe9DW*\xff\x8da\x1b\xa1\x8d\xed\xa2a\x0e$V\x8d\x82:\"\xf4\x94\x1f\xb4>:\xf2\xfc\xe2Z\x0fR']\xb6\xf9\x9cx;\x90t\xcb\x1f\xc1\xa7\xfb\x82\xccT-i)\f\xf5\x00v3\x03\xa2c\x8d\xb3\x02\x89\xf6\xae\xd38\x9a\x819V\x16$IBN\xae\x9bu\x9b|\xe4\"m݊\x1d\xc7V{(\x87s\xac\x1c?\x8fB\x82g\xf7\fBɿ\x89\xb2.\x19/\x91\x87\xe4v\x88\x12\x9ac\b\x8e\xddM\xda'\xb6 \xa3e\x15β\xaa\x00\v>ms\x06\x1e\x99\x92F\xe4И~/\x02J2\xce6\\\x14\xb5\x9e\xa1Ug\x93|n\x10\xe6\xb5\xc9\xe9#\xabtD\x16D\xa2\xc4u\xf6\x19^\xf0\xb4Ư\xf4c\n\x86\vZ\xe3\x97LJ\xf7\\\xf3gŝ_\x06\xb0PX\x82\x9b\xfa\x8aq@Y\x17VTE{\x89],\xe0\xdc®\xb9\xac\xe8gEG\xe4\xfdM]_\xbe6\x12\xbf\x1cD5ܰ'(\n\xc6css\x8f\n\x99\xbb<5S\v@ۈ\xb3\xdc_\xc6\xe4o\\\xbdpӅn\x03 \v[Ɩ\xfa\xb8<|\xd3\xd7A\x03\x96\xaa\xc7\xf6\x85\x98x\xfa01\xd3 m\xf0G\x0e;\xf1t\xe1\xfcS\x85\x89\xfc\x9d3\xa5_\xf9\xf4\xe0+\x9f\x1a\xfc\x1e\xa7\x05\x13$0\xa1\xca\xfcS\x81\xcfޒR:\a=\xb9\xed7Gj'\xe555\x96\xeb#6\xd8\xd7\n\xb7\xc9b\xad^\f@fɿ~@/]\x1c\xda\x06G\xc9\xecxD\xbd}\xc9\xd6]\xeb;\xc4\xfe\t\f\xb7ui\xa0\xe2h\x00(p\xa3Ԭ\xa8\xab\xf0\x81g\xdbA\x0f[n\xd8F\xe9\x92[v\xdel\x16\xbfq\x1d\xe0\xdf\xe7K\xc6>\xaa&W\xa7{_\x9a\x11eU\xec0\x12c\xe7\xdd\x06ϓ\x92\xa8t\x86\x9e\xafU!\xb2\x88\xcf9z\xaf\x9ek\xb0w\xd9\x10\xdd\xfc\x97u\xb2Eb\x81\x0f6\x17\xe1\xd6\xc5\xfe\x95\xcc\xee\x12\xfc#\xd7Jx%\xfeD\xefP\x9d`\xd5\xed\xdd\xf5\x8a`\x051\xa2\a\xae\x9a\x04ņ\xe5k@\x97\xa1\x1d\xfb!}\xb2\xda\xf4\xa0\xf6s\x84\xbb/|@\xee\x9es\tn\x8bW͙B\xadu\xbdr\xb8\x1c\xea\t\xe5\x8b\xcb\x1dS\xfe\xbd\x0e\xa1\xf3Eŵݹd\xa2\x8b\x1e\x1e\xc1\xaeO\xad\x9a\x1d\xb4V\xfb\xcf\xd5tK\x8f\xec\xe1\xa5\x1a\xda\xc9\xdeU\xfd\xe4\x81!=\x9f\x83\xd3\xe1SՓ\xe7\xa9_\x00\xa7\xc3.Ԃ\xa8\x18\xf9)\x9a\x01y\xf2\x15K\xe3o\xe8\xff\x8bz\x84\xf7ѕ\xcb\xfe\x935\x83&#\xa9\x89\x01*]2\x1f\xa1`\x9b\x8fHw|?O\xed\xc5s\r\x03*\xfe\x8e\xf0\xe7,N\xde\xf4A\x8d\xbf\xe2B7\xa8\x87Nc^\x15\xbd\x8f\xb5c\xd7w\x14\xb76\xaa\xd4O}\x1f\xb7\x86\xe5ɐ`\x10\x81%\xe4\xc1\x87mNEF\xab4\xbf\x87O\xca=H\x94\"&\xfd\x16\xbd窼\xe7\x16\xf2\xb5\xfd$\x8c)z?\xb6!\xc0\xf6|\xc6\xdeE\xff\x88\xed\x91O\x19X[\xb55\xc7\x06\xdc\f\x03\x87쟐{ՑХ\xfb\x13c\xb8\xc6:\xcd)W/G\xd40\\\xd6\x7f\x13c\xc2\xf8Q\xc8\x05\xfb\f\xfb\x11\xfb\x82}\x908\x88}\x02\xb8\xf3\x8e\x90\xd3\xd6\ni\xc79C|lZ\xd1a\xd3\x11\r9-\xb6w\x03\x18\x83Lvz\xf4\xa9\xa9\xe2N\x9b\x1a\xf6[1\xe6\x8dҎY\x86\x03\xfd\xddޯQ\r~P{\xc74\xf7\xa8\x1a\xd9\xfbH\xaf\a\xe6\x1d\xc9\xf1^z\xf7K\xbdn\x1fT`\x7f\xfb\xfb\xd9\xff\a\x00\x00\xff\xff\f/o%s|\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVK\x8f\xdb6\x10\xbe\xfbW\f\xd0k%wQ\xb4(tk69,\xda\x06\xc6n\x90;M\x8e-f)\x92\x9d\x19z\xeb>\xfe{AR\xf2C\xb67\xc9%\xba\x99\xf3\xe4\xf7\xcd|t\xd34\v\x15\xedG$\xb6\xc1w\xa0\xa2ſ\x04}\xfe\xc5\xed\xf3/\xdcڰ\xdc\xdd-\x9e\xad7\x1d\xdc'\x960<\"\x87D\x1a\xdf\xe2\xc6z+6\xf8ŀ\xa2\x8c\x12\xd5-\x00\x94\xf7AT>\xe6\xfc\x13@\a/\x14\x9cCj\xb6\xe8\xdb\xe7\xb4\xc6u\xb2\xce \x95\xe4S\xe9\xdd\x0f\xed\xdd\xcf\xedO\v\x00\xaf\x06\xec\xc0\xa0C\xc1\xb5\xd2\xcf)\x12\xfe\x99\x90\x85\xdb\x1d:\xa4\xd0ڰ\xe0\x88:\xe7\xdfRH\xb1\x83\xa3\xa1Ə\xb5k\xdfoK\xaa7%\xd5cMU\xacβ\xfcv\xcb\xe3w;zE\x97H\xb9\xeb\r\x15\a\xee\x03\xc9\xfbc\xd1\x06̚\xaa\xc5\xfamr\x8a\xae\x06/\x00X\x87\x88\x1d\x94ب4\x9a\x05\xc0\bH\xc9Հ2\xa6@\xac܊\xac\x17\xa4\xfb\xe0\xd2\xe0\x8f\x95\x905\xd9(\x05\xc2\x0f=\x96\xcbC\u0600\xf4\b\xb5\x1cH\x805\x8e\x1d\x98\x12\a\xf0\x89\x83_)\xe9;h3\x92mu͍\x8c\x0e\x95\x847\xf3c\xd9\xe7\x86Y\xc8\xfa\xed\xad\x16X\x94$\x9e\x9a(um\xf0@'ȟ7P\xfc\xdb\xd8+>\xaf\xfeT\f\xb7*W\x9f\xdd]EZ\xf78\xa8n\xf4\r\x11\xfd\xaf\xab\x87\x8f?>\x9d\x1d\xc3y\xafWH\aˠ\xa6N3p\x155\b\x1e!\x10\f\x81&T\xb9=$\x8d\x14\"\x92؉\xff\xfa\x9d\xac\xd5\xc9鬅\x7f\x9b3\x1b@\xee\xbaF\x81\xc9\xfb\x85\\@\x1c\x87\x02\xcdx\xd1\n\xaee \x8c\x84\x8c\xben\\>V\x1e\xc2\xfa\x13jig\xa9\x9f\x90r\x9a<\xaeə\xbc\x96;$\x01B\x1d\xb6\xde\xfe}\xc8\xcd\xf9\u07b9\xa8SR \xc9c畃\x9dr\t\xbf\a\xe5\xcd,\xf3\xa0\xf6@\x98kB\xf2'\xf9J\x00\xcf\xfb\xf8#\x83h\xfd&tЋD\xee\x96˭\x95Ilt\x18\x86\xe4\xad\xec\x97E7\xec:I ^\x1aܡ[\xb2\xdd6\x8ato\x05\xb5$¥\x8a\xb6)\x17\xf1Ep\xda\xc1|G\xa3<\xf1Yً\xe9\xa9_ч\xaf\xa0'\xabE\x9d\x91\x9a\xaa^\xf1\xc8B>\xca\xd0=\xbe{\xfa\x00S'\x95\xa9J\xca\xd1\xf5\x02\x97\x89\x9f\x8c\xa6\xf5\x1b\xa4\x1a\xb7\xa10\x94\x9c\xe8M\f\xd6K\xf9\xa1\x9dE/\xc0i=X\xe1ib3u\xf3\xb4\xf7E\x90\xb3\x02\xa4h\x94\xa0\x99;b\xa3\x13Q\x19\xbe\x83ΫkA_\x8a\x05\x12\x05\xba8\x9d5\xf5\xae8\x95\xff\x12\xcaz\x06\xe5\xf7c H\xaf\x04^\x90\xf2\x86萲Z\xa1\x01\x93.\xf0\x1ba9}\x93\"\x05\x8d|\xb1\x8a\x00Vp\xb8\xd2\xd3+\xec\xe4\xcf'\xe7\xd4\xdaa\aB\to0\xab\x88\xd4~f+o\xdfg Xe\x9fk\x1c\x1c\xde\xfbϒP\xe0\xf6i\xb8\xac\xd4\xc0{|\xb9r\xfa\xe0W\x14\xb6\x84<\x1f\xf9l\\U\xf4p\xfe\x1e\xbc\x82\xd2ա\xbc8\xe4,\x85\xe6\x04E\x96@j{\x8a+\xa7\xf5A\xe9;\xf8\xe7\xbf\xc5\xff\x01\x00\x00\xff\xffߙ6&\xcb\n\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcW\xcdn\xe36\x10\xbe\xfb)\x06\xe8\xa5\x05Vr\x83\xa2E\xe1[\xeb\xec!\xd8t\x1b\xc4\xdb\xdc)j$\xb1\xa1H\x96\x1c:\x9b\xa2\x0f_\f)\xf9G\x96\x1d粺y8\x9c\x9fof\xbe\xa1\x8b\xa2X\b\xa7\x9e\xd0\ae\xcd\n\x84S\xf8\x95\xd0\xf0\xafP>\xff\x1aJe\x97ۛų2\xf5\n\xd61\x90\xed\x1f1\xd8\xe8%\xdeb\xa3\x8c\"e͢G\x12\xb5 \xb1Z\x00\bc,\t\x16\a\xfe\t \xad!o\xb5F_\xb4h\xca\xe7Xa\x15\x95\xae\xd1'\xe3\xa3\xeb\xed\x8f\xe5\xcd/\xe5\xcf\v\x00#z\\Am_\x8c\xb6\xa2\xf6\xf8O\xc4@\xa1ܢFoKe\x17\xc1\xa1dۭ\xb7ѭ`\x7f\x90\xef\x0e~s̷\x83\x99\xc7l&\x9dh\x15\xe8\xd3\xdc\xe9\xbd\x1a4\x9c\x8e^\xe8\xd3 \xd2a謧\xcf{G\x05\xd4>\x1f(\xd3F-\xfcɽ\x05@\x90\xd6\xe1\n\xd25'$\xd6\v\x80!\xf7d\xa6\x18\xd2\xde\xdedS\xb2\xc3^d\xfb\x00֡\xf9\xed\xe1\xee\xe9\xa7͑\x18\xa0\xc6 \xbdr\x94\x10\xfc\xaf\xd8\xc9a\x9a\x19\xa8\x00\x02\x86p\x80\xec.B\x10\x06\x84'\xd5\bI\xd0x\xdbC%\xe4st`\xab\xbfQ\x12\x04\xb2^\xb4\xf8\x01B\x94\x1d\b\xb6\x92\x15\x0e|i\xdbB\xa34\x96;\x99\xf3֡'5B\x94\xbf\x83N;\x90^ʂ?N<߂\x9a[\x0e\x03P\x87#xX\x0fX\x81m\x80:\x15\xc0\xa3\xf3\x18\xd0\xe4&d\xb10C6\xe5\xc4\xf4\x06=\x9b\xe1\x8aF]s\xa7n\xd1\x13x\x94\xb65\xeaߝ\xed\xc0\x88\xb1S-(\x81i\b\xbd\x11\x1a\xb6BG\xfc\x00\xc2\xd4\x13˽x\x05\x8f\t\xc1h\x0e\xec\xa5\va\x1a\xc7\x1f\xd6#(\xd3\xd8\x15tD.\xac\x96\xcbV\xd18\x7f\xd2\xf6}4\x8a^\x97i\x94T\x15\xc9\xfa\xb0\xacq\x8bz\x19T[\b/;E()z\\\n\xa7\x8a\x94\x88I3X\xf6\xf5w~\x98\xd8p\xe4\x96^\xb9!\x03yeڃ\x8346\xef(\x0f\x0fR\xee\xael*\xa7\xb8\xaf\x02\x8b\x18\xbaǏ\x9b/0F\x92+5\xb4\xd8N\xf5\x04\x97\xb1>\x8c\xa62\r\xfa|/\xb5)\xdbDS;\xab\f\xa5\x1fR+4\x04!V\xbd\xa20\xf6:\x97njv\x9d8\n*\x84\xe8jAXO\x15\xee\f\xacE\x8fz-\x02~\xe3ZqUB\xc1E\xb8\xaaZ\x87\xcc;U\xce\xf0\x1e\x1c\x8c\xbcy\xa6\xb4\x13\xca\xd88\x94\\XƖo\xaaF\xc9\xef狪\xe3\x9a\xdf\xed\xc4\v\x8aQ\x9f\xf5\xfb\x88\xbcA\xf0|\xa6\x83\xc2UV\xae\x88iм*\xd1\xf5\xe6\xee=\x10\x9eQ\x7fG\x91\xeeLc\xdfHq\xaf8\xabw\x86\x06\xc6/\xbd!\xde\xeei~\x85\x8c=\xcdW\xf2\xeeD\xf8\x14+\xf4\x06\tÞ\xa9_\x14u\xb3\x16\x01^:%\xbbt1\r\x04/\x81\x10\xacTs\x94zE\xf8\xcc#\xca\xe3\xccP\x16iXg\xc4\x1c\xfc\x89\xf8\f\xfb\x9dsP\f\x8ct\x15\x83\x92\xa0\x18\xde\xc1\xa1I\x7f\x84ZF\xefӊ\xcaR~\x99L/\\K\xa2#\xf3\xfc\xf5x\xff\x06\x93\xde\xee5\xd3S\\(\x93\xa3q\x1e\x8b\xa0Z~A\xf1\x19si\xe2\xb8S0\xf2w\xfc\xc2;\x06j\xb6\xa2\xf8թ<\x80o\x84\xf8q\xa7\x98\t\x1fM\xde\xf3\xd37l2\x88\x81\x9f[ \x85\x99\x89\xb1B\xa8Q#a\r\xd5k\xde\\\xaf\x81\xb0?\x8d\xbb\xb1\xbe\x17\xb4\x02\xde\xff\x05\xa9\x9962QkQi\\\x01\xf9x\xae\xcbf\x13w\x9d\b3cx\x94\xf3\x03\xeb\xcc5\xc6n\x18/v\x06\x9c\xdd/\x05|Ɨ\x19郷\x12C\xc0\xd31:\x9b\xc9\xec\x10\x9c\b\x03?\xd2\xea\x03\x94\x86\xbf\f\x83\xe4\xff\x00\x00\x00\xff\xff\xe4\xb6\x15`c\x0e\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4:K\x93\x1b\xb7\xd1\xf7\xfd\x15]\xeb\x83\xed*\ri\xf9\xfb\xe2J\xf1&\xad\xe2\x14\x13[\xd9\x12W\xba\xb8|\xc0\f\x9a\x1cxg\x00\x04\xc0\x90b\x1c\xff\xf7T\x03\x987H\xeeR\xb6\x85\xcb.\xf1h\xf4\xfb\x85ɲ\xec\x86i\xf1\x01\x8d\x15J\xae\x80i\x81\x1f\x1dJ\xfae\x17\x8f\x7f\xb5\v\xa1\x96\xfb\x977\x8fB\xf2\x15\xdc5֩\xfa\x1dZ\u0558\x02\xdf\xe0VHᄒ75:ƙc\xab\x1b\x00&\xa5r\x8c\xa6-\xfd\x04(\x94tFU\x15\x9al\x87r\xf1\xd8\xe4\x987\xa2\xe2h<\xf0\xf6\xea\xfd7\x8b\x97\xdf-\xfer\x03 Y\x8d+Њ\xefU\xd5Ԙ\xb3\xe2\xb1\xd1v\xb1\xc7\n\x8dZ\buc5\x16\x04{gT\xa3W\xd0/\x84\xb3\xf1ހ\xf3\xbd\xe2\x1f<\x98\xd7\x1e\x8c_\xa9\x84u\xffL\xad\xfe \xac\xf3;t\xd5\x18V͑\xf0\x8b\xb6Tƽ\xed/\xca@\xef\xf3\xb0\"䮩\x98\x99\x1d\xbc\x01\xb0\x85Ҹ\x02\x7fN\xb3\x02\xf9\r@$\xde\xc3ɀq\xee\xd9ɪ{#\xa4CsG\x10dw\vG[\x18\xa1\x9dg\xd7\x04s\xb0\x8e\xb9Ƃm\x8a\x12\x98\x85\xb7xX\xae\xe5\xbdQ;\x836\xa0\r\xf0\x8bU\xf2\x9e\xb9r\x05\x8b\xb0}\xa1Kf1\xae\x06\xb6o\xfcB\x9crGB\xd9:#\xe4.\x85ă\xa8\x11xc\xbc\xb8\x89\xfa\x02\xc1\x95\xc2ΰ;0K\x18\x1a\xe7\xc9N\xe3\xe2\xd7\t\xa2u\xac\xd6S\xa4\x06G\x03V\x9c9L\xe1t\xa7j]\xa1C\x0e\xf9\xd1aK\xc9V\x99\x9a\xb9\x15\b\xe9\xbe\xfb\xff\xd3\xec\x88\xfcZ\xf8\xa3o\x94\x1c\xf3\xe65\xcd\xc2`:`B\xb2ڡI2H9V}\n\"\x8e\x00\xbc\x1e\x9c\x0f\x98\x04\xb8\xc3\xf9\x8b\xa8\xacea\xb0Fy\x1dB\xa2?=\xc7f\bz\xb8\xaa\x8dPF\xb8\xe3\n^~\xf3T4\xc9>@m\xc1\x95\bQy6N\x19\xb6C\xf8A\x15A\xd1\x0e%\x9a\xa8hy\xd4\xfeR5\x15\x87\xbc\x15\f\x80u\xca$\x95Mc\xb1\b\xa7\"\xdc\x16\xecD\xe3\xc6w\xfe\x11\x06Q\x18dI\x83h\xdd\xe9\xc2\xef\x10J\xa6\xad\xe2\xd5\x0e\x9fd\x11C\x96Jű\xe3\x1f\xce\xd0\x12\x16\xb4Q\x05Z{\xc6P\t\xc6\b\x91\xb7\xfd\xc4E\x06\x95\xe8\xf7\xb4\xf84\xbaR\x8c\xa3\x01\xa7\xa0d\x92WHd0p\x86I\xbb\x8d*2\x17`{\xec\xe1\xa8Ǩ\xbc\x8f\v\xa7\xd0\t\xbb\xf6/\x83\xbb.J\xac\xd9*\xeeU\x1a\xe5\xab\xfb\xf5\x87\xffی\xa6I\x8d\x95F\xe3D\xeb\xef\xc3\x18\x84\xcd\xc1,\x8c\xc9\xfdo6Z\x03\xa0\v\xc2)\xe0\x14?\xd1z6\xc4@\x80<\xe2\x14\xd8#,\x18\xd4\x06-\x99\x96\xd7(\xb5\x05&A\xe5\xbf`\xe1\x16\x13\xd0\x1b4\x04\xa6\xb5\x85B\xc9=\x1a\a\x06\v\xb5\x93\xe2?\x1dlK\xbc\xa6K+\xe6\xd0:o\x8cF\xb2\n\xf6\xacj\xf0\x050\xc9'\x90kv\x04\x83t'4r\x00\xcf\x1f\xb0S<~T\x06AȭZA霶\xab\xe5r'\\\x9bL\x14\xaa\xae\x1b)\xdcq\xe9\xf3\x02\x917N\x19\xbb\xe4\xb8\xc7ji\xc5.c\xa6(\x85\xc3\xc25\x06\x97L\x8b\xcc\x13\"}B\xb1\xa8\xf9\x17&\xa6\x1fvt\xedL\xd0a\xf8\x1c\xe0\x19⡬\x80\x8c\x80EP\x81\xc4^\n4E\xac{\xf7\xb7\xcd\x03\xb4\x98\x04I\x05\xa1\xf4[g|i\xe5C\xdc\x14rK:O\xe7\xb6F\xd5\x1e&J\xae\x95\x90\xce\xff(*\x81ҁm\xf2Z8R\x83\x7f7h\x1d\x89n\n\xf6\xce'\\\x90\x93-\x91\a\xe0\xd3\rk\tw\xac\xc6\xea\x8eY\xfc\x93eER\xb1\x19\t\xe1I\xd2\x1a\xa6\x91\xd3́\xbd\x83\x856\t\x13\x86a\xc6;%:W\xaaB6\xe5%\x85\xbb\v4S\x00L\t\xcbG[W2\xd7\xe2F\x9bL#圷4\x94|\x968\xb4\xe2\x17\xf0\x8a720\xb8E\x83>\x1b\t\xbe_+\x1f!\x1c\x13\xb2\xf5i\xa1^\x01\xa7\x12\x98\xe5A\x89\x90\xc3\xd46\xe0\xac}\xc0\x99@\x99\xc4\xf8\xd5\xfd\xba\r\x86-\x13#\xee\xb3xw\x91?4\xb6\x02+\xee3\x87\xcbw'5\x97\xc6z\x1b\x90\xf0\x11\xc1)`\xa0\x05\x168\x8a\xc6 \xa4u\xc8x\x9c$'h0\xae\xbd\b\x9e\xfe$\x92\x10\x8a\xca\x18\xb5I&\xc0(\xf2\b\x0e\xff\xd8\xfc\xeb\xed\xf2\xef*\xd0\x01\xac\xa0\xd4\xcc\xd7z>\xdf~\xd1\xd5{\x1c\xad0ȩz\xc3Eͤآu\x8b\b\r\x8d\xfd\xe9۟\xd3\xfc\x03\xf8^\x19\xc0\x8f\x8c\xaa\xa6\x17 \x02ϻ`֪\x8d\xb0\x81\xf0\x0e\"\x1c\x84+=\xa2Z\xf1H\xe0\xc1\x93\xe0\xd8#Yr \xa1A\xa8\xc4c\xc2~¸\xf5\xd9\\\x8f\xe6\xafd=\xbf\xdd\xc2W\xc1y\xdd\xd2\xcfۀF\x97\xb6\f\r\xacG'X\x99\x11\xbb\x1d\xf6y\xffLY(\xccR\x80\xfa\x1a\x94!Z\xa5\x1a\x80\xf0\x80IN!> \x9f\xa1\xf7ӷ?\xdf\xc2Wc\x1e\x9c\xb8JH\x8e\x1f\xe1[\xf2>\x9e7Z\xf1\xaf\x17\xf0\xe0\xf5\xe0(\x1d\xfbH7\x15\xa5\xb2(A\xc9\xea\x18\x12\xe0=\x82U5\xc2\x01\xab*\v\t\"\x87\x03;\x82ڞ\xb8\xa7\x15\x11\xa9&\x03͌;\x9b$F>\x9c7\x9ay\xd6Ԏ\xa7ًϢ\x9ed\xbd\x9f-\x03y\"'|\xb9\xf0\t\x9c\x18\x96^Wp\xe2\xb1\xc9\xd1Ht\xe8\x99\xc1Ua\x89\x0f\x05jg\x97j\x8ff/\xf0\xb0<(\xf3(\xe4.#ê\xd4\xed\xd2\xf7\xc1\x96_\xf8?\xd7\x12\xee\xdbT\x9fJ\xbd\a\xf2\xf9X@\xb7\xdb\xe55\x1ch\xb3\xfb\xa7Ǯ\x93|\xd8Ąs\n\x93l\xfeP\x8a\xa2lk\xbd\x81\xb7\xad\x19\x0f\xee\x98\xc9\xe3g\xb2\x1d\xe2sc\b\xa3c\x16\x9b\xb8\x19\x93\x9c\xfe\xb7\xc2:\x9a\xbf\x86\xb1\x8d\xf8$\xe7\xf2~\xfd\xe6sZT#\xae\xf1$'j\x980>f=VY\xcdt\x16v3\xa7jQLvS\x0e\xbf\xe6$\xa4\xad@s!\xfd{7\xda\xdc&\xa8\x89j\xa0\xdb\xf3\xac\xfcӱ]\"\xe1\x1bv\xb1ϥ\x85g\xf9uY\x15\x1e\xd8\xce\x023\b\fj\xa6I#\x1e\U0005814cC3A\xe9\x02e\x04]c\x10\x98\xd6\x15\xc5\xf4\x90E$ \xc6\xfc7\xb2\x87YO\xdf)\x86$E\xd9v\xa56蜐\x9f\x919\xef'\x88\xfc\xbe\x8c\xeazv\x85\x92[\xb1\x8b\xdd\xce9\xa7dSU,\xafp\x05\xce4\xa7j\xae\xb3\x8c|\xa0-\xe7\xe9\x7f?\xd8\xdaj\xf8\x85\x06c\x9a\xaaQ\xdbqN\fʦ\x9e\xa3\x92\xc1\xa3҂%\xe6\rZ7\xb3^Z\xb8\xbd}\x8e\x8d\x05\xa5\xbc\xa6\xe4\x0eep\xaa*\x8d\x8a\x1e\x13\xf8\xb62u\xaa\xaf\xf2\x92B\x7f\x86o\xa0\xea\x9eʑ1\xdeY\xba]2\xd93\xe8.\xb7SZ\xf1\xc9\xcc\xd8\rN\x16\x03}O\xea!\xf9\x86\xf63\xbaH\xe1\x91-\xf24\x04G\xd7>\xbdQ\xda}m\x1f\x89\n;\xed\x90w\x8d\xfek$\xfej\n\xc4\xf7~\r\x8fF!j\xecJ\xff\xb1\xaf\v\xc5]\x8e\xa0\rj\x96\xec\n\x81\xef\xdc[\xdf\xc2\xfc\xd2\x06`\xc2Bc\x91\xfb\x0e\xda\xec\xee\x19\x84\xf6\x9d\x893\x87\x19\x9d\xbf\xce_\xa4\x1bS\xe1\xcdo\xf8RrU\x97j\x0ef\xceB\xd6r\xcd?ᴏ\x8d)\x8e\xf5\xe0:~\x05h\xc8}\x15JE\xf2\x96\x89\n9\xb4o\xdcτ\x92\xe3\x96R\x9c\xe0\xe3\xda>ND\xeft\xfdw^\x92\t&\xcc\x13\x9e?R\x98ӧ\xc6\v\x92\\O\xb6C\xa9\xaa(/\xd9\xd49\x1a2L\xff\xe0\t\x12\x0fT\xf7\x17%\x93\xbb\xa4\x93k\x1f\xec\x10*f\x1d\xe4\xfd'\x03)\xe2\x87/\xa6Sʆ/\x9c\xfd\xa8\xd1Z\xb6\xbb\xe4\xce\x7f\f\xbbB\xe7.\x1e\x01\x96\xabƥ\xed\xf7K\x1b]\xd0\xf3\xba\x87ɦ\xd8\xd8\xfb1W\xb6\xcen\xdbT\x95?3\x8c\x1b\xfd\xa7\x1d\x1e\xab\x1c\xd3\x19\xff\x99\xd6\xe19\x04Kf/\xb1\xea\x9e\xf6\xa4\xfcq\x17\xec\xce:d8\x13\xd8\xdf\xe2!1\xdb\xfa\xb9\xc4\xd2}t\x9e\x89\xa5ٗ\x18\xc3\xc5\xd0\x1bOq\xae]K\xc2\xec\xbesH\xac}\xef\xbdʳ\x98\x1d\xf1\xbb\xc6mv\xbd\xf5\xde\xf2\xfcg\v3\xfb\x1b\xe7\x1fL\xf2\xa1\xd8RM\x88\xfe|\xabA\x01Rl\xa4\xc5'\x01ﺜ\x02.\xac\xaeر\xa3ŗ~d\xaa\xe9\xf7\x91ޢZ\x8f\xa9\xf1T*{\xbe\xc3\xdd}-\x92\xaek\xcf\xfb\v\xb8\xe03\xfc\xba:\xed\f\x7f\x8f\x1bΤ\xe2V2mK\xe5\xd6o.\xa8Ʀ\xdb\xd8\xdac_V\xfa\xc0\xe2\x9f\xde⦨\n\tT{\xef\xf6,g1\xfex\xe8\x1a-ތ \\\x88\xfb\xf1[\xa6Ttݐ\x17 \a\xe4\x1fv\xef\xa6_p\xbc\xe8\x82\fs\xb1A\x1e\xe2Q\xaa\xab\xa0\xa4\xaf#\x94\x99\xbf\xb2\xc3\xc5@>&\xe8ό\xe1Iu\x9aMz\xcc\xf9\x00v|\xd3\x1c\xce4y\xf7ܿ\x82_\x7f\xbb\xf9_\x00\x00\x00\xff\xff\xfc¬w\xb0(\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z͒\x1b\xb7\x11\xbe\xefSt\xad\x0f\xb6\xab4d\xa4$\xae\x14o\xd2*Nmbo\xb6DI\x17\x97\x0f\xe0\xa09\x03s\x06\x80\x01\f\xb9\xb4\xe3wO5\x80\x19\xce\x0fH.\xa9\x925\x17i\xf1\xd3\xf8\xf0u\xa3\xbb\xd1`\x96e7L\x8b\x8fh\xacPr\x01L\v|r(\xe9/;\xdb\xfc\xc3΄\x9ao_\xdel\x84\xe4\v\xb8k\xacS\xf5;\xb4\xaa19\xbeŵ\x90\xc2\t%ojt\x8c3\xc7\x167\x00LJ\xe5\x185[\xfa\x13 W\xd2\x19UUh\xb2\x02\xe5lӬpՈ\x8a\xa3\xf1\xc2ۥ\xb7\x7f\x99\xbd\xfcn\xf6\xf7\x1b\x00\xc9j\\\x80V|\xab\xaa\xa6F\x83\xd6)\x83v\xb6\xc5\n\x8d\x9a\tuc5\xe6$\xbc0\xaa\xd1\v8t\x84\xc9q\xe1\x00\xfaQ\xf1\x8f^λ \xc7wUº\xff$\xbb\x7f\x10\xd6\xf9!\xbaj\f\xab\x128|\xaf-\x95q\x0f\x87\xb52\xd0[\x13z\x84,\x9a\x8a\x99\xe9\xcc\x1b\x00\x9b+\x8d\v\xf0\x135ˑ\xdf\x00D\x06\xbc\xa0\f\x18\xe7\x9eSV=\x1a!\x1d\x9a;\x12!\xbbe8\xda\xdc\b\xed\xd3%\xb3\x18{\x03\xf9K\xdf\x11\x9bܞ0[g\x84,R(ދ\x1a\x817\xc6+\x9d\xf6\x9f#\xb8R\xd8)\xbc\x1d\xb3\x04\xd18\xbf\xf14\x18\xdfO\"\xadc\xb5\x1e\xa3\xeaM\r\xb08s\x98\x02u\xa7j]\xa1C\x0e\xab\xbd\xc3v+kej\xe6\x16 \xa4\xfb\xeeo\xc7\xf9\x88\x84\xcd\xfcԷJ\x0e\xc9yC\xad\xd0k\x0eHH[\x05\x9a$Cʱ\xeaS\x808\x12\xf0\xa67? \tr\xfb\xedg\xa1\x90\xe9\x81Z\x83+\x11ް|\xd3hX:eX\x81\xf0\x83ʃ\nw%\x1a\xf4#Va\x04\x9dm\x10\xa4;e\x92\xaaӘ\xcf\xc2\xd8(\xac\x955\xd2\xdfp\xa1\xcfb_\xb9A\x96\xb4\xaf\xd6I\xcd\xfc\b\xa1d\xda\xc8^\x17\xf8,\x03\xeb\x13)\x15\xc7\x1ek\x13\\\u00826*GkO\x18>\t\x19 y84\x9c\xa5\xa8D?\xa6\x05\xd4\xe8J1\x8e\x06\x9c\x82\x92I^aС3L\xdau\xb4\x8c\xa9\n\xdbi\xef\xf7z\b\xe5C+\xaf\xd73\xc1\x14\x86n_\x067\x98\x97X\xb3E\x1c\xab4\xca\u05cf\xf7\x1f\xff\xba\x1c4\x03Ѣ\xd18\xd1\xfa\xd1\xf0\xf5BR\xaf\x15\x86{\xfe_6\xe8\x03\xa0\x05\xc2,\xe0\x14\x9b\xd0z.\xa2\x7fE\x1e1\x05\x8e\x84\x05\x83ڠE\x19\xa2\x1553\tj\xf5\v\xe6n6\x12\xbdDCb\xc8\xed7\x15\xa7\x90\xb6E\xe3\xc0`\xae\n)~\xebd[\"\x9c\x16\xad\x98C\xeb\xfcA4\x92U\xb0eU\x83/\x80I>\x92\\\xb3=\x18\xa45\xa1\x91=y~\x82\x1d\xe3\xf8\xd1[\x93\\\xab\x05\x94\xcei\xbb\x98\xcf\v\xe1\xda@\x9d\xab\xban\xa4p\xfb\xb9\x8f\xb9b\xd58e\xec\x9c\xe3\x16\xab\xb9\x15E\xc6L^\n\x87\xb9k\fΙ\x16\x99߈\xf4\xc1zV\xf3\xafL\f\xedv\xb0\xecD\xd1\xe1\xf3\xe1\xf5\x02\xf5P\xbc\xa5\x93\xc0\xa2\xa8\xb0Ń\x16\xa8\x89\xa8{\xf7\xcf\xe5{h\x91\x04M\x05\xa5\x1c\x86Nxi\xf5Cl\n\xb9&çyk\xa3j/\x13%\xd7JH\xe7\xff\xc8+\x81ҁmV\xb5pd\x06\xbf6h\x1d\xa9n,\xf6\xce'3\xb0\xa2\x03E~\x80\x8f\a\xdcK\xb8c5Vw\xcc⟬+Ҋ\xcdH\t\xcf\xd2V?E\x1b\x0f\x0e\xf4\xf6:\xda\xfc\xea\x88j\xc7\xfem\xa91'\xcd\x12\xb94U\xacE\x8c$ke\x80M\xc6\x0f\x99J\xbb\x00\xfa\x92\x11e<\xe8\x9c\xd9\xd1\xf7&%\xa8E,{\x8e<\xc6;\x1b\x03U5\fT\xfdo\x12#\rje\x85Sf\x7f\x88\x94c\x938\xaa\x1d\xfar&s\xac\xae\xd9ޝ\x9f\tBr\xe2\x1d;\x93&g\x14\xa4z\xa0J\x16\x8a\x0e\xd9D\x1dp\xefh\x1cٹE\x97ެ<\x1aل\x84C\x8e\t\xfd\\r\xbc\xed\x95R\x15\xb21\x9bZ\xf13\x9b~T\xd1q\x18\\\xa3A\x1f\xff\x83\x9b\xd5\xca;cDŽl\xddGH\xb9\xc1\xa9\xc4>V\xe4n\x8e\xa9\xe6\xb8\x1d\u0089\x90\x94\x04\xfc\xfa\xf1\xbe\r;\xadeE\xe8\x93\xc8\xd2\xe7'i\x16\xf4\xad\x05V\xdc\a\xea\xf3k'-\x84\xbe\xfbu\x00\xe1}\xafS\xc0@\v\xccq\x10\xf7@H\xeb\x90\xf1\xd8H\xee\xc6`\xec{\x11|\xeaQ\x90\x10\xaeE1>\x92J\x80\x91\x8f\x17\x1c\xfe\xbd\xfc\xef\xc3\xfc_*\xec\x03XN\x99\x90\xbf\xab`\x8dҽ\xe8\xee+\x1c\xad0\xc8\xe9\xf6\x81\xb3\x9aI\xb1F\xebfQ\x1a\x1a\xfbӫ\x9f\xd3\xfc\x01|\xaf\f\xe0\x13\xa3\xa4\xff\x05\x88\xc0y\x176Z\xab\x116l\xbc\x93\b;\xe1J\x0fT+\x1e7\xb8\xf3[plC'&l\xa1A\xa8\xc4\x06\xd3\xec\x03\xdc\xfa\xe4\xe9\x00\xf3wr)\x7f\xdc\xc27\xc1I\xdcҟ\xb7\x01F\x97 \xf4\xbd\xce\x01\x8e+\x99\x03gDQ\xe0!ў\x18\v\x054\n\x05߂2\xb4W\xa9z\"\xbc`\xd2Sp\xc4\xc8'\xf0~z\xf5\xf3-|3\xe4\xe0\xc8RBr|\x82Wt\xc6=7Z\xf1og\xf0\xde\xdb\xc1^:\xf6D+奲(A\xc9j\x1f\xf2\xcd-\x82U5\xc2\x0e\xab*\v\xa9\x18\x87\x1dۃZ\x1fY\xa7U\x11\x99&\x03͌;\x99\x8eE\x1eN\x1f\x9ai~\xd2~\xcf;/>_y\xd6\xe9\xfdb\xb1\xfe\x99L\xf8\xc4\xfc\x13\x98\xe8_u\xae`bӬ\xd0Ht\xe8\xc9\xe0*\xb7\xc4C\x8e\xdaٹڢ\xd9\n\xdc\xcdw\xcal\x84,22\xc6,h\xdd\xce}1g\xfe\x95\xff\xe7ڍ\xfb:˧\xee\xde\v\xf9r\x14\xd0\xeav~\r\x03m\x1e\xfd\xfc\xd8u\x94\x87e\xcc\xec\xc62\xe9\xcc\xefJ\x91\x97\xed\xad\xaa\xe7mkƃ;fr\xff\x85\xce\x0e\xf1\xdc\x18B\xb4\xcfb)2c\x92\xd3\xff\xad\xb0\x8eگ!\xb6\x11\x9f\xe4\\>ܿ\xfd\x92'\xaa\x11\xd7x\x92#\xb7\x85\xf0=e\aTY\xcdt\x16F3\xa7j\x91\x8fFS\xae|\xcfIIk\x81\xe6L\xf6\xf7n0\xb8\xcd\xda\x13Yw7梴\xdbJ\xa6m\xa9\xdc\xfd\xdb38\x96\xdd\xc0\x16\xc3A\x871\xe9leё8\x99k>\x03\xcfR\xfc\x96p[ID4\xb4\xc5T\xa9B\xe4\xac\x02\xeb\xdbd,VF\x98\xad\xec)\xa0T=r\f\xb7_U\xec\xe1\xf5\xbe\xe0ḧ\x1dB\x1e\x8enQ+#\n!Yu\xf0\xd8\xfe\xea(Y\xcd\xfc_\t[\xad\x99\xd6B\x16\x17q\xdbַ\x96蜐E\"\xd1\xef\x97\xdfO]\aN\x9e\x93\xf3.\xe0\xc3\b\b0\x83\xc0hO\xa4\xaa\r\uecd0uj&(e\xa4\xac0\xa6\xd6+\x04\xa6uEy]\xc8$S\xbe\xa9\xad\xd6\xe5J\xaeE\x11+\xa7S\xa6dSUlU\xe1\x02\x9ci\x8e]ڒǽ_(<\xa3\xf1\x0f\xbd\xa1\xad\xbaϔ*ӻ\x1a\x140\xa7\x9bA\xd9\xd4S(\x19l\x94\x16,\xd1N\x87s☨\xe3\xf6\xf6\x12\x93\n'\xff\f\a\xe1Μ*8D\xc7\x11\xaf!\xf1\x8a\x1d\xdcG:\x9a_\xeaP\f\xfe\xdaНj\x880K\xd7VFc\xb4\xe27c\xd2\xfa\xbex\xd4y\xf0\xa4\xe3\x8e\xe1\xa1\x1f\xf5\x06\n\x9eU\x96\xf2\x85\xf2K\nS\xe19,\xf2\x1e\xd2\x00\xd7>\x92\xd1\x05\xe3\xea\xd2\x14\xdda\xb5C\u07bd!\\S\xb7y=\x16\xe2\vʆ\xc7C\"j\xec\x8a\x1c\xd1N̡\xec\x12B\x8c6\xa8Y\xd2\"\xc0?\nX_\x18\xfd\xda\x06i\xc2Bc\x91{\xdf:Y\xfchL\xe0\xccaF\xf3\xafs \xe9bWx\x9e\xeb\xbf\xc2\\U\xf9\x9a\x8a\x99r\xc8:\xda\xfc\xfbP\xfb0\x98\xa2\xec \xaf#,\x88C\xee\xafܠ$\xac\x99\xa8\x90C\xf7,}1\xf3\t\xd0\xd3d\xecs\x92_\xa3\xb5\xac8\xe7\xb4~\f\xa3B\xe5-N\x01\xb6R\x8d;b\x95_\xdbx\xb4.\x8a\xc9R\xf1sH\x1e\x14\xf70\xe4\xf1'\xb7)\x9a\x84Z\xfa\xcfp\x17a\xf4E\xcdsEJ\x1a\x93r5\x1d\xe4Ӿ\x06Nİ\a\xdc%Z\xdb\x13\x9c\xe8z\x8cn!\xd15\xf9=@\xbf3T\x92S9Mۗ\x94\xd9=\xb6'\xfa\xbe\xf7\xc7\xe5\"\xb6#\xbek\x1cBW\x87.U\xd5\xfa\x00\xffH.\x9bz\x85\x86T\xb1Je\xc4\xc0$\xefk.UL\xe8$\xb4a8\x88\x8a\xf5\xb0X@\xf7\xa7\xdc)\xe0\xc2\xea\x8a\xed\xbb\xcd\xf8\x1b\x1c\x1d\xe9\xf4s\xc2\xe1\\\xb5\xbe\x8a\"ϑ\xbc\xedt\xa5\xba\xfb\xd1B\xfa~z:Ӈ3پ\xef\xef~\x8c\xf0yV8\x91w\x0e\x7f\x1cr\x8d\x81,\a\x12\xce\x05\x8b\xf8c\x95\xcb}\xfcp\x99?ӽ'ٛ4z\xe4\xbc';>y\xf5[\x9aU\xf7\x1e\xbc\x80\xdf\xff\xb8\xf9\x7f\x00\x00\x00\xff\xffϡa\xa5-&\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ks\xdc8sw\xff\n\x94sؤJ#ǕG\xa5ts\xb4v\xac|\xdf\xca*\xc9\xf1\x9e1d\xcf\x10\x9f@\x80\v\x80\x1a\xcf&\xf9\xef)4\x1e|\fHbF\x1a\xednjyQ\x89\x04\x1a@\xbf\xbb\xd1\xc0\xacV\xab7\xb4a\xdf@i&\xc5\x15\xa1\r\x83\xef\x06\x84\xfdO_>\xfe\x9b\xbed\xf2\xdd\xd3\xfb7\x8fL\x94W\xe4\xba\xd5F\xd6\xf7\xa0e\xab\n\xf8\x116L0äxS\x83\xa1%5\xf4\xea\r!T\bi\xa8}\xad\xed\xbf\x84\x14R\x18%9\a\xb5ڂ\xb8|lװn\x19/A!\xf00\xf4\xd3?^\xbe\xff\xd7\xcb\x7fyC\x88\xa05\\\x11\x05\xdaH\x05\xfa\xf2\t8(y\xc9\xe4\x1b\xdd@aan\x95l\x9b+\xd2}p}\xfcxn\xae\xf7\xae;\xbe\xe1L\x9b\xbf\xf4\xdf\xfe\x95i\x83_\x1a\xde*ʻ\xc1𥮤2\xb7\x1d\xc0\x15Q\xbe\xb9fb\xdbr\xaab\x877\x84\xe8B6pE\xb0}C\v(\xdf\x10\xe2\x17\x85\xfdW~=O\xef\x1d\x88\xa2\x82\x9a:\xc0\x84\xc8\x06ć\xbb\x9bo\xff\xf40xMH\t\xbaP\xac1\x88\x9a\xffY\xc5\xf7$,\x810M(\xf9\x86(\xb0\xb3A\x92\x10SQC\x144\n4\b\xa3\x89\xa9\x80Ц\xe1\xac@\x8a\x10\xb9\xe9A\n\xbd4\xd9(Yw\xd0ִxl\x1bb$\xa1\xc4P\xb5\x05C\xfeҮA\t0\xa0I\xc1[m@]F@\x8d\x92\r(\xc3\x02\xba\xdc\xd3\xe3\xaa\xde۹\x85\xd9\xc7\xe2\xc2\xf5\"\xa5e/pK\xf0\xf8\x84ң\x8f\xc8\r1\x15\xd3\xddR\xc3\xf2\b\x15D\xae\xff\x06\x85\xb9\x1c\x81~\x00e\xc1X궼\xb4\\\xf9\x04\xca\"\xab\x90[\xc1~\x8d\xb0\xb5]\xb8\x1d\x94S\x03\xda\x10&\f(A9y\xa2\xbc\x85\vBE9\x82\\\xd3=Q`\xc7$\xad\xe8\xc1\xc3\x0ez<\x8f\x9f\x90xb#\xafHeL\xa3\xaf\u07bd\xdb2\x13d\xad\x90u\xdd\nf\xf6\xefPlغ5R\xe9w%<\x01\x7f\xa7\xd9vEUQ1\x03\x85i\x15\xbc\xa3\r[\xe1B\x04\xca\xdbe]\xfe]$\xea`X\xb3\xb7<\xaa\x8dbb\xdb\xfb\x80\xa2r\x04y\xac\x109\xc6s\xa0\xdc\x12;*\xd8W\x16u\xf7\x1f\x1f\xbe\xf6\x99\x92iO\x94\x1eoN\xd1\xc7b\x93\x89\r(\xd7\x0fY\xd3\xc2\x04Q6\x92\t\x83\xff\x14\x9c\x810D\xb7\xeb\x9a\x19\xcb\x06\xbf\xb4\xa0-\xbf\xcb1\xd8k\xd4Gd\r\xa4mJj\xa0\x1c7\xb8\x11\xe4\x9a\xd6\xc0\xaf\xa9\x86W\xa6\x95\xa5\x8a^Y\"dQ\xab\xafeǍ\x1dz{\x1f\x82\xae\x9c \xad\xd7\"\x0f\r\x14\x03I\xb3\xdd\xd8&\xa8\x8b\x8dT\x03%c\xbb\fq\x94\x16~\xfb8-b\xd5\xe2\xf8\xcb\x12\x97\xd9\xe7\xdfco\xcbovf\xad`\xbf\xb4\x80\xcaԉ?\x1c\xea+\xd5S\xfa\xc3Dzј\xba\x93\x88\xb6\x0f|/x[B\x19\xf5\xfa\xc1\x02s\x96\xf1\xf1\x00\n\x9aCʄ\x15\"k\x97\xecZD\xf7\x15\x158U@\x844\txL8x\x84\t\xc4@\x92&\xd8\xd0@\x9d\x98\xf1\xec\x92\t\x11-\xe7t\xcd\xe1\x8a\x18\xd5\x1e\xa2\xd1\xf5\xa5J\xd1\xfd\x04\xb6\x82o\xf0,dE ^\xd5pV ɣBA|\xfdqQŴU\x94a\x95w\x92\xb3b\xbf\x80\xaf\x8f\xc9NAZ\xbd\xec\xfa\x15\x925T\xf4\x89I\x95\x12\x03\xa9\xb0iϞwjZZ-遌m\\悓Ȫ\xa4|\\b\x88϶Mg\x1dH\x81\xaef\\\x8a\xa7\xb6\xb7\xddk \xf0\x1d\x8a\xd6$\xa6IH٢i\x92\x8a4R\x9bi\xbaO\xab.\xd2w\x8eR\x1fg\x98\xe6`eIVw\x8fW\u0081\xa8\x16\a\x03\x85,\x05\xd8eԖ\xa8][%[\xd7v\x12)dM5\x94D\x8aɑ\x91]Z\x0eڏU\"gtz\xe8\xa2[?z<\x84\xd35p\xa2\x81Ca\xa4:Df\x0eJݓ\xa3X'P\x99ЦC\t\xe8\x160\x03\x92XN\xdfU\xac\xa8\x9c\x87a\xd9\x13\xe1\x90R\x82\xb6\xda\x04]\xe6\xfd\xd4\"\xc9\x12\xf9\xfd sڣ{\x16\xc4j\f/\xa5Q\xba'C\rwO\x12\xb5\x9d\xee=\xd0-\xfe\xbd\x91\xb3\xcb\xfe\xff\x89\xd8`LN`\xda\x19\xf9'\xe8~f\xf3\xf4$\xdfb\x84\a\xfa\x92\xdcl\bԍ\xd9_\x10f\xc2\xdb%I\xa0\x9c\xf7\xc6\xf8\x03\xd3\xe6x\xa6\xcf$M\x8eL\x9c\x890q\x88? ]\xd0d{\x84=\x82Igs\x0e\x9f\\np\xcf#$\\\xff\xd43\xc0\xa1\x9d\x93\x0f\x8b\x1d\x9e\xec\vD\x04\xc6\xf0\xb9l\xe0\x1e/\n\x89\xdcI\xfa\xc9\xd4%\xe1\t\xb8?a\x99Y\xac\xd2\x1f\xa3\x9f\xfaD\x0e\xf8A;ZZ\x89\xa9\x98\xcfij@\x99\xc9%\xa8{\xbeQ\xce\xca8\x90\x93\x91\x1bqAn\xa5\xb1\x7f0@\xd3\xc8(?Jз\xd2\xe0\x9b\xb3`\xd4M\xfc\x9c\xf8t#\xa0\xa0\t\xa7\xe5-\xc2\xfa9?g\xd3,\xb7E\xdc3Mn\x84\x8dW\x1cJ2\x87\xc2\xf4\xae\x1b\xce\rT\xb7\x1a\xd3uB\x8a\x15\xda\xcc\xe4H\x1e\xdfR\r\xd0\xfd\xecA\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\xef\xabǘ/XY\x93\xb3\xf2\x10\x8c\xac3p\xe0uw\xb9\xbc\x9e\x95\x95ٌV\x81\x13\x16\x9bN$G\xa7\x9b\xe6 \xe5\x19\xe8@+\x8e.\xce\"uiY\xe2\xe6\x1a\xe5wGX\x94#x\xe1X\xd5Л\xbb3\xc15m\xacZ\xf8okiQ\x9a\xfe\x974\x94)}I>\xe0N\x19\x87\xc17\x9f\x87\xeb\x81\xc9\x18\xb2\xb1CY\xfey\xa2\xdc\xda~\xab\xc0\x05\x01\xee<\x01\xb99\xf0\x8b.Ȯ\x92ڙ\xed\r\x03\x8e\xfb\x15o\x1fa\xff\xf6\xc2\x0e\xbf8d_ɼ\xbd\x11o\x9d\x0fq\xa00\xa2\xc3!\x05ߓ\xb7\xf8\xed\xeds\\\xa9LN\xcdl6`њ6y\x1c*\x92\xc9\xfa\xee\x19pL?7\xdf%当=\xb7\xda,\x16m\xa46\x9f\xd3yÉ\xf9܅\x1eC\xcf8\x91c[\x8c\x18|\x1e-\xea{\xebDn\f(\x9fKt6 \xc4\x1fό\xccR\xbb2\xfd\xc9\xc6d \x8d\xf9]\x8b\xe0\x05nr\x1b79S<\xc6a\xb5x9\xd2\xdb\xff\xf8\xbd\x97ϴ\x92k\xff\xef/\xe4\xa5\x1d\xeaB\xd65\x1d\xefjfM\xf5\xda\xf5\f<\xed\x019\xea\xabm\x8b\xf2\x9ck\x91;\x1e\xc2\xfd\xcb\x1d3\x15\x13\x84\x06\xb5\x01\xca3\x14%\x8dL\xe5\xb0SOE5Y\x03\x88\x98\xa2\xff=\xb8\x125\x1378\x00y\x7f\x06\xd7#\xa2\xeb\x9c\xce\xeeu\xa4I\xa4||\xe1LV#K\xb2\xab@\xc1\x801\x0e\xf3\xee\xe8\xa9\niz)\x8b#\x1c\xd2F\x96?h\xb2aJ\x9b\xfe\x144iu.\xad\x8f$\x9f\x9d\xf7WV\x83l\xcd9\x11\xfc\xb1\x1bf\xb0\xd7\\\xd3\xef\xacnkBk\xd9:cnX\x1dwu=zw\x94\x99\xb8m\x85\xf9\x1b#-\t\x1a\x0e\x06\xc8\x1a6\xe9\xfd\xde\xd4SH\xa1Y\t*T)8\xb21i\x05sC\x19oS\xbbD\xa9\xe7\xd8\bX|T\xea\xa4\x00\xf8\x8b\xeb\xd9\xcb;Vr7DP\xe6\xdaq#\r\b\xdb\x10f\b\x88\xc2b\x1c\x94S\xc98\x84G\x06\xa2\x86\xe5\xea\xb9<\x05n\x1f\x10m\x9d\x87\x80\x15\n$\x13\xb3)\xb7~\xf3O\x94\xf1s\x90\xcdr\xde'\xa9\ue056\xa7\xe4h~\xeeu' t\xabp\xf3\xdf\xe9\x8e\x1d\xe3ys\xb6\x94#\x9c\xb6\xa2\xa8\x00\x95\x90\x18\xea\x06\a\x9e\tm\x80\xe6\xf2\x82\xf5\x8aZ!\x98\xd8\xe6\xd1.;\x11\xda=\x0e\xd5k)9\xd0\xe9]\xc8\uec78~\x05M\xf4s7\xcc35QG\x04\xb7m\x8etȦ\xa8UZ\x84\x1a\x03u\xe3DN\x12Պ\xbeu9\x83\":&\f\xf7\xb3x\xc9\xf8\x9a\t\x96A\xdb\x01]o\x043}\xe7т8\xab\xf3h\a\x88\xee\xc0)\x19\xb6\x9b\x01\x00+\xa0!\x0e\xc1\xb9G\xae9\u0091\\\x03\xa1e\t\xa5\xcb]ZWć%\xae\xf0m\xa2\xb8!\xb9\xba\xe3=\xc1,ʆg\x10tb\x1eV=\xc1\xaa\x15\x8fB\xee\xc4\n\x83q}\xb4\x0e91K\xf5\xdc\xe1\xcd\xc9\xcahY\xbf\xe4\xab\xe9%-4\xe4\xd7|\x9e\n\xfe\xd3\x19\xb4L6\xdf\x1c\x95\xf0\x98\xe3\x82%\xbd\xe6\n\xb0'>.\xcebn\xfc\x99\xce~S\xfa\xda\x15K?\xab,\xee&\r\xaa\xe7\x14\xee*0\x15\xa8P\x9a\xbd\u0092\xf4rv\x87\xb4\v^b\x9d\x9ce\xaa\xe0\"\xbb\xf2\xcfQ\xe5\x1cF7-\xe7\x17\x96\xb7i˓ᰑ(b\x87\x9c\x95U?\x96\xf6\x18r\xaa/\xb2\xf1د\xb4\x18\xd6\x17\xc6*\x88P`(\xc3ȞƩ\xf5baio\x7f\x7fXN\x81\xf9\xbf0\xfd\u07fc\xf40\xa3R\"\x1f\x8d\xb9U\x9a\x11\x89\tX\t\x06롱\xab\xaf\xf0\xed|\xa1\xef\xef\v\xa7\x06\xea/\x8d\x97\x98I\x176\x03\xad\t8\xa3z\x13\xb4\x06\xadv\xae@\xb4\x03>gh\xfb\x7f(\xdc)\x88\x00&ů_+\b\xe2\xeb\xab\xf7\x99&\xffL*\xd9&\xaa\xfafP\xb6Pݱ\xbc\xe0A\xa1\x87\xdfP\x00C\x9f\xde_\x0e\xbf\x18\xe9\xcb>0\x8b\x96\x00\x84AQ\x97\x99e\xa2dO\xacl)\x0fR\u06dd!p\f\xd4\xf1Y\x02\x9aTD0\xee\x180\xf4\x1f0\x1c\xf9Ҹm\x99\xa3Uܼ/\x9aW\x1drrMȰ\xe6c\xc2\x1a\x1e\xbb}\xf1\"U\xb0\xbfI\xad\xc7\xf1\x15\x1e9\x91\xc4B5\xc7\t5\x1c\x99\xc5b\xcf\xdeoɩ\xd28&\xe6>[E\xc6\xcb\xd7ad\xe1g\xb9\xe6\xe2\x18윽\xbe\xe2\x15\xab*^\xa7\x96\"\xb3\x82\xe2\xe5J!\xf3\xa2ϓJ\x01\x96\x03\x96\xe9*\x88\xc5ڇg\x054'-i\xb1\xa6\xe1\x98J\x86E\xea\xe4\x89٫\xd5*\xbcZ\x85\xc2\xeb\xd6%\xccr\xd1\xec\xc7c*\x0fb\x9c\xf4\x13m\x1a&\xb6\x87L\x91\xcb:\xb3l\xb3\xcc2\xb7\xa3\x89\fx\xa6\x1f\xcet\xd1\xe1D\xe8\xeb\x8eK'\"ɐ\xb6d\xc2\xc8K\xf2A\xec=\xdc\x04\x9c^\xf8(\xa498\xc8f\xa7\xb5c\x9c\xf7Ok!\xd8yP\xfe̤\xa6\xb5\x9bՔ\xb7\x9f\xa4\xabT\x03\xa7\xfc\xa4\xc0\xf1\xcb\bF?;\xfa\x9a\x9e\x7f\xddr\xc3\x1a\x0e֣{be\xf2\f\x99\xa9`\x1f\x91\xfc7\x89'\xa4\xd6{\x84\xf4\xe5>\xca\xe2\xe5(\x88\xa1\x9a\xec\x80sBS\xdcq\xb0\xfc\u009dL.\xe4\n\x8f\x04Z\xf2\x06&\xf1\xe7\x99/\x9c\x14\xe310\xa4^\x9d\x80[P\x81\xa7\x9bub!\x93\xe60G\x8b\x1e\xf8\xe5.\xba\xc0w\xbf\xb4\xa0\xf6D>a\t\x83\xf7\u07ba\xb3\n^\xddh\x1bc\x06\x05\xe8\x95\xf1Ԧ\xc2A(\xd3)(\xf2A8_b<\x1f\xecc5_\x17\xaaYun\xa3\xb0\xe4\x18\x13݅\x8c\xbd\x13ݖ\xdc\xfeܢ\xfe\xf3\x06nLJn\x8b\xbeR\xbe?\xfb\x1b\x15\xeb\x9fR\xa4\x9f\xb7\x1d\xb4X\x94\x7f\xae@n)\x94\xcb\xf6^\xf3\x8a\xee\x8f\xdbD=c\x91\xfd9\x8a\xeb31\x95SL\x7f\x1c\x9e^\xa1x\xfeU\x8b\xe6_\xabX>\xbbH>k\x1f3{\xd3*w\x9b\xf1Ī\xef\xe5]\xf7\xf9\xa2\xf7\x8cb\xf7\x8c\x9d\xb4\xe5E\x9e\xb0\xbc\x8cb\xf6\xe3\x8a\xd83h\x96+\x8a\xafX\xac\xfe\x8aE\xea\xaf]\x9c\xbe\xc0Y\v\x9f\x8f+B?y\a&l\xf5\xdf\xca\x12\xee\xa42K\xc1\xc9ݸ}b'\xb5\x17\xb0I^\x12\x11\x9a&V\x89!\x86\x0f/N[Tz\xd33\xb8\xd3?\xc9\xd2\xcemi\x8f\xe5~\xd4\xfc\xe0\xac\xf2\x06\x14\bw\xcd\xc7\x7f>|\xb9\x8d\xf0S>\xaf\xf7\x8cG\xd7K8\x0f\xa6\xf4\xc8\xf1[s\xbe\x98\xc9a\v}\x80\x17\xde\x17\xa1\r\xfb\x0f\xbc\xef\xed\x19\xe9\xa0\x0fw7\b#\xf8ix\x81\\\xac\xa2\x88;\x96k\xb0\x16+\xa2jR,n6\x03\x88Ê\xdf\xfe5JP\xba+\xb3\x82\xc5d\xa1\xc6\xcb\n\xdeݍ\x9b\xc7\xd4(\x9f\xac\xd3(\xf6D:\x8e\xac\x98*W\rUf\x8fl\xa3/\x06s\bff.\x9d3\xa9X\x0f\xaf\x01K\xa27\xdc\xfe\x85{\x91\xfbf\xb8\xdb;\xc6\xdd)\xf3\x98>\x7f\xb2x\xf2\xe4\x05\xe71m\xb1W\x88\xa9\xc4\xebd\x81ɋ\xa5\xc9\xd417\x05%e`\xe1ڠ\x9ej\xa0\xe4Z\x8a\r\xdb\xfeD\x9b`F\x1c>'\x95\x85O\xd14\x16\xb4\x05養u\xb5ihwzPi,a%t.eU~B\xc8w\x01\xb0\x06\xb7\xbd\xed\xb4R\\B\x03j\xd5\xe5ۺی\xf6\xcd\xf4l\xf5\xc5(d\xf5\xb7\xdc\fj\x17\xac\x1a4\xa0\x84\xff\x96\x9a\xab/\xb8y\xc0z\x9b\xdet\xf7q\xb2\x16\x1bv\x86\x96q\xfc\xe0x9\xd1fT\xac\x93\x00>ʧt\x18\xdcHUS\x13$\x00\x13z\xd4\xe1\xddݚ\xf6\xd0@q9$\xf9\x9f:\xf9O\x9d\xfc\xa7N~Y\x9dl\x95\xdbݷ\x93R\xe1\xf7\xb1\xf7\xbc\xefI9\x8f\xe9\xff\x04\x18\xdb\x1f\xddO-h\xa3\xab\xc45x\xcf\xf3?\xf1\x86HCM\xfb\x9cE:\x00\x83u\xb2\xa2\xeay\x90;\b>fX6J+vKjp\xe0\xfe\xa4\x15\xe3\x17\xbd\xec\xed\xeb\x94\xe9d^\xb1u\xf2\xe5Z\x0e=\x13\xea\aw$\xacj;\xc4\xd4\t\x05:\x8b\xe1v\xc6\xc1\x8f\xf9\xc4B\xe6\xd5Ly\x06\xe3\x84\xeb\x98\x10_\xb9\xb8\"\xc9[\x9a2ob\xfaM\x11=\xa3\xd5tQA\xd9r8\xf5\x1eև^\xff\xe5\x9bX\xc3h\x19w\xb1Zd\xf7\f\xb4\xf5\xb0\x86w\xbezJx\xc8}JN\x05ᘰqW>\x16\xeev\xe0\xa2\x00\xad7-\x0f\x95\xa3\x85\x02j\xa0\f͙\x8e3>\xaa\xf6\xb1m\xb8\xa4%(\xe7\x92-\xa0\xf5\xbf\x06\x8dG<[\xe0\xcbVu\xd7\xed\xce^U\xfa,\xcd\xd5PE9\a\xfe\x89q\xd0?ʝ\xb0\xf3\xca\x10ȻT\xbf\xdeY٢U֬\xef\x89h\xeb5(\xa2\xc1\x98\xe9\x04\xdeF\xaa\xf9S+\x0e\xefL\x18\xd8B*\xe7\xb9S\xcc\xc0CC\x95\x06\x9cQ\xc6\n~\x1euq\x19\xc1\r\xa7[W\x9e\\\xb2\x82\x1a\x88\x06\x18G\x98\x9a>\xf6\xd7\b\x8b\xef\xb1ZTNlDd\v\xf5\xd41\xb9I\xb1\x9e\xba\xf29a\xaa\x93\x97>;\x8b\\\xd0\xc6\xe0\xa1D\xa4#\x12\xd1x\x18x\x91\xfa\xe8\xde\xe7\x01\xd8iN\xf3GK|\x11\xb36\xb4ND\t\xcbz\xe7\xfa\x10\f^ծ\xca^-t\xff\xd2\xdbX\xf4LvT\xc7\x03.I\u07fb\x83\xed\xc0\xa0\xabnACI\xe0\t\x04\xb1\xa2H\x19\x87r\x8eS\xbf\xe2\xe6\x9ez\x02\xf5\x83\x8ep\xb0:۲\xf8\x83\xa1\xcaĩ\x1f\xfa1.\x86\xbb\"%5\xb0\xb2\xbdOs\xdd\xd2WW+ub\x89\x06\x9e6\xf6\xe2Q\x84\xa3\x90\xd6\xfa\xb93\xc25hM\xb7!1\xb8\x03\x05d\v\xc2\xe2=\xee\xf7$=\xa6p\xcc\xda\x1b\x8bAb\x80\x16\xa6\xa5~\x00\xe7\xc2Ŋ\x96pg\x10\x9cC\x1d^1\xe2\f:\x9f\xaa3\x18\xdc`D\xb4\xc5\xde)ʄ85v3\x1dv癚\xaf\x11ʔz\xf4\xeb\x1b\xfc8\x82/z\xf1\x8d,ي\x8a\x8a\xed\xe4!\xe3J\xc9v[\x05ޜr\x88H\xd9b\xe4ܠ*\xd0\xe1ǜL\xabD\xaf\x90\xc2\u05fdMi\xe98\xddi\x1f\xe5\x19\x8aZu\x87\r;U5c\U000f3cc4\x13\x10\x17m\x7f\x02\"\xd5{Q\xcc\x1e\x8b<ܣ:ʵL\"!j\xe3\x17CB\x848\x85\x84\xbe/\xd1E<\xbf\x1b\x8cL\xf9('\xa2cމ\xc1%\u0383Z^t\xdf\t\x1a\xba;ǡC\x0f\x82\xbf\x93\xd2n\x03\b\xc7D\xbe8v:\xee\xfd\xfdF\xacO\xd1\xdb\xfaxr\xec\xfam\x04ct,\xddF\xb1\xdd0!\xde\xfc{\xb6Iɋ\xfbż5\x87\x7f8\xf8\xfa\xca\xc7\xcbwT\t&\xb6'a\xe4g\xdf7\x11\xcf{\xb0\xe7\x8c\xe8\xc3\xcc_,\xa6O\x9a\xa5\x83\x97\xc8\xe0e\x0f\xcf~$\xff\xe6\xff\x02\x00\x00\xff\xffJ\xb7g~\xf1r\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfa\x15\x84\xeea?B\xdd^\xc7}ą\xde|\xb2gO\xb1\x1e[ai\xf4\xbctU\xb6\x9aQ\x15\xd4\x00\xd5r\xdf\xde\xfe\xf7\x8dL\xa0\xbe\xba\xe8\xa2Z-ygǼت\x86$\xc9L\xf2\x03\x12X,\x16g\xbc\x12\xf7\xa0\x8dP\xf2\x92\xf1J\xc0W\v\x12\xff2\xcb\xc7\xff6K\xa1\xdelߞ=\n\x99_\xb2\xab\xdaXU~\x01\xa3j\x9d\xc1{X\v)\xacP\xf2\xac\x04\xcbsn\xf9\xe5\x19c\\Je9~6\xf8'c\x99\x92V\xab\xa2\x00\xbdx\x00\xb9|\xacW\xb0\xaaE\x91\x83&\xe0\xa1\xebퟖo\xffk\xf9\x9fg\x8cI^\xc2%3\xd9\x06\xf2\xba\x00\xb3\xdcB\x01Z-\x85:3\x15d\b\xf4A\xab\xba\xbad\xed\x0f\xae\x91\xef\xd0!{\xeb\xdbӧB\x18\xfb\x97\xde\xe7\x8f\xc2X\xfa\xa9*j͋N\x7f\xf4\xd5l\x94\xb6\x9fZ\x98\v\xf7\xbb\xfbMȇ\xba\xe0\xbamsƘ\xc9T\x05\x97\x8c\x9aT<\xa3\xaa~l\x04b\xc1x\x9e\x13\xb5xq\xa3\x85\xb4\xa0\xafTQ\x97\xb2\xe9 \a\x93iQY\xa2ƭ\xe5\xb66L\xad\x99\xdd@\xb7\x1f,?\x1b%o\xb8\xdd\\\xb2\xa5\xa1z\xcbj\xc3M\xf8Ց\xcf\x01\xf0\x9f\xec\x0eq3V\v\xf90\xd6\xdb;v\xa5\x95d\xf0\xb5\xd2`\x10e\x96\x13s\xe5\x03{ڀdV1]KB\xe5\x7fx\xf6XW#\x88T\x90-\axzL\xfa\x1f\xa7p\xb9\xdb\x00+\xb8\xb1̊\x12\x18\xf7\x1d\xb2'n\b\x87\xb5\xd2\xccn\x84\x99\xa6\t\x02\xe9a\xeb\xd0\xf98\xfc\xec\x10ʹ\x05\x8fN\aT\x10\xece\xa6\x81d\xfaN\x94`,/\xfb0\xdf=@\x020\"Q\xc5k\xe3\xe5(\xb4\xbe\xe9~r\x00VJ\x15\xc0\xe5Y[i\xfb\xd6\xc9^\xb6\x81\x92_\xfaʪ\x02\xf9\xee\xe6\xfa\xfe\xdfo{\x9fY\x9f\xa2\xff\xbfh\xbe\xb3\x86\x1bL\x18\xc6\xd9=\xcd \xa6\xfd\x94fv\xc3-Ӏb\x00\xd2b\x8dJ\xc3\"\x90:gJw@U\xa0\x85\xcaE\x16XD\x8d\xcdF\xd5E\xceV\x80\xdcZ6\xb5+\xad*\xd0V\x84\xf9\xe4JG\xf5t\xbe\x1eB\x1f\v\x8eصrb\n\x86$\xd3\xcf6\xc8=\x91\xdc\xe4\x11\xa6\x1d\x0fq\x10?s\xc9\xd4\xeag\xc8\xecr\x00\xfa\x164\x82\t\xa3Ȕ܂F\x8ad\xeaA\x8a\xffk`\x1b\x9c\x12\x96$Ղ\xb1\x8c\xe6\xb3\xe4\x05\xdb\xf2\xa2\x86\v\xc6e>\x80\\\xf2\x1dӀ}\xb2Zv\xe0Q\x033\xc4\xe3G\xa5\x81\t\xb9V\x97lcme.\u07fcy\x106(\xe4L\x95e-\x85ݽ!\xdd*V\xb5Uڼ\xc9a\v\xc5\x1b#\x1e\x16\\g\x1ba!\xb3\xb5\x867\xbc\x12\v\x1a\x88$\xa5\xbc,\xf3\x7f\v\xfc6\xbdn\xf7f\xa6+\xa4Ng\xb0\a\xf5\xac\x93.\a\xca\r\xb1\xe5\x02~B\xd2}\xf9p{ו\xd5\xc7\xd7\xf9G\xf1\xfd-\x11=X\x9d#\x85}B\x930\xda/H\x9e\x0fQ\xd2#\xc5\x05\x98eguN\xd8\xf05\x85\xa1=\xffqo+e\x8f(\xbf.\xde\x1d7af\xb0nrN\xbd,\xe3\x9an\xfeE\xf8F&\xeb\xd6[\xacY<\xfb\xd8myA\xbb\x02\x9e!\xf9\x05[\x8b\xc2\x029US\x88\xb2\x19\x9c;%\x81R-0\xa3Mb\x9bm>4{G\t-\x06\xb4\x1a\x02p\x0ez\x88r\x88\a\t Y\xe3ZЦ\xa9\xd0P\xd2f,E\x92\xdd/\xe4\n\xbe\xfb\xf4>\x1e{vK\xa2\xa4\xee\r*aҺ\xf2n\xe0\x18uq\xf5\xa1J\xf8\x85\xfc\xb5&\x10t\x9b\xf0\x17\x8c\xb3G\xd89\x17\x8bK\x86|\xe3\xa1r\"\n\x1a(#\x804\xc5#\xec\b\xd4\xf8\x16\xffx\x99#-\xae<\xc2Ȯ_\xac\xf4\xe8\x8a\xf8\xf9\xbd\x14G7\xfc@\x84I\x99Mmi\x88\xea\xa7\xcf\xc8\x06{\xbc\xcc\xd0K\xa1\x04\xbe\x1c9\xecdq\xea\xf6\xd5O\x8ay\x84\xdd\xef\x8c\xe35β\x8d\xa0M'N\xab7j=\x8b\xe1\xae\xdc\xf3B\xe4Mgn^]\xcb\v\xf6IY\xfc\xe7\xc3Wa\xb0c\x99\xb3\xf7\n\xcc'e\xe9ˋR\xd9\r\xe25h\xecz\xa2\t*\x9d%A\"v\x93G\x9c-EAm\xf8!\f\xbb\x96\x18\x929\x12\xcd\xe8\x8er\x85\\\x97\xae\xb3\xb26\xb4\xd5*\x95\\\xb8e\xb1\xb1\xde<\x0f\x94\xee\xb1\xe0$\x1d\xfbN\xef\xd0\x18\xb9_\\\xd6R\xc13\xc8\xc3\x16\x1d\xa5\xd3p\v\x0f\"\x9b\xd1g\t\xfa\x01X\x85f!]Zf(j?\xb2\xf9\xe2\x95\xee9t\xcb\xd7\xc5c\xbd\x02-\xc1\x82Y\xa0Y[x(V\x95\x89t\xf16a$\xe7d\xac,p\xae'\xd6\fҒT=\x92\x91s\xb8z*\xb1\x9eI&\xf2\"\xc8\xedJ\x92\x82nb\xeb<\xeb5Sn\x8eQ1\x9d\xb18\x17\xa0䴵\xf67\xb4\xf44\x1b\xff\xce*.\xb4Y\xb2w\x94\xd9[@\xef7\xbf0\xd9\x01\x93\xd8mE\xab\xec\xbf\xd4b\xcb\v\xf4?\xd0@H\x06\x85\xf3F\xd4z\xcfW\xbb`O\x1be\x9c\xdb\xd0lڝ?\xc2\xce\xed('u\xdbUX\xe7\xd7\xf2\xdc\xf92{\x8a\xa7q|\x94,v\xec\x9c~;\x7f\xae{7C\xa2gT\xed\x89rɫtI\xa6\xbc\xd99\x81\x06\x06\xeb\xc1!\xc2\xc6M\x02)\x06\bS\x14H\x16\xe5J\x99H\xb2H\x04\xad\x04A\xbfQƺuȞ\xbf?\xbaP\xa9\xc2\xe2$\xe3k\v\x9a\x19\xabtH\xc9Dş\xb2\x14\xdf-w\x1b0\xe0\xf7\xa1\xfc\xa2\xa7\x03\x8cQ\xecy\xab\x1b\x9cU9w{a\xd4\x11\xcf\xc8{\xa2\xb6\x95V\x19\x98h^D[\x12mS\x8f\x82\xfbth\xd6u\xb9\x8b\xfe\xd6IZ;eQ:\x94y\x8e<\x92\xee\x88\xc8\xe8\xc3\xd7\xce\x125j\x17\xfc;EZ\x8f\xc1\x91\xd1Y\x8e\xb2\xe4\xc3t\xe0dt\xaf\\\xeb0\xc7<0\x17n釚t\xce\x1c\xaf\xa3\x11\xe5\x7f6צ\x14\xf2\x9a:bo_\xd0\x1d\xf2Z<\x96\x1e5V\x8ewүBg-\xf7\x9a\x0f>\xa7N\xd1Ə\x86\x1es\xf7\xf7DȻ\x96\xcav\x96qf:ѕ\xca\x7fg\xd8Zhc\xbbh\x98\x03\x89U\xa3\xa0\x8e\b=\xe5\a\xad\x8f\x8ems\x06\x1e\x99\x92F\xe4И~/\x02J2\xce\xd6\\\x14\xb5\x9e\xa1Ug\x93|n\x10\xe6\xb5\xc9\xe9#\xabtD\x16D\xa2\xc4u\xf6\x19^\xf0\xb4Ư\xf4c\n\x86\vZ\xe3\x89\xa8\x04\x1e\xb6R\\\xfaGl\x8c1IJ\xc0\xe3\x1b'\xbf\xefeL\xbe\x80,\xbdʉ\x9cY\xf24\xca\xfa\xf3?\x9e\xff:XtZ\xa6DٰO[\xa7\xc6c\xfa\x11c\xf9njd?K\xf5\xd73\x15N*\xfb\xa9'j\x1a\"G\xe0\xf5\xc5z@\xe5_\x93\xbe\xb1P~\xae\xbc\xb5<\xc1\t\xfb\xeb\x11xIg\xec\xb9\xd9\xc9l\xa3\x95T\xb5\xf1kB\b\xeb]\xe6\xee\x1d\b c\xc2>\xaaA\xfe\x83mT\x1d9\xb51Aڄ,\xda4\x82\xf4\x92j}b\x04X\xbe}\xbb\xec\xffb\x95O\xb1eO\xc2n\"\xc0\xe8>\n\x9e\xe7\x18\x17t\x0e\xf4x=\x10\xaeJ\x1a\ne\x04\x98\xd2L\x8a\xc2Il\x80ГW\xf6\xb9r\xab\x83G\xfbM\xd3kX鉸s\xd3o\x9bl\xc9i\xf7\xfd\x19I\xb7'=\x1a\xf5\xcd\xd2j\x8fK\xa6M]\xa1LH\x9cMO\x97Ma\xab+\xe9I\xb2\xc9\x11rjB\xec\xdc\x15\x88\x17M~}\x99\x94\xd7d\x9a\xa5\xa5\xb7Υث\xa4\xb2\xber\x02\xeb륭\xceHV=\xfd\xa9\x97\xf4\xb5\xf4\xa3\xb3+Ӗe\x0e'\x9c&\xa5\x99&-ݤ\f\xf8\xa8\xa1&\xa5\x8f\xceM\x1aM\xe2d\xfat}մ\xd0WM\x06}\xfd\x14\xd0Ii\x9b\xac07\xc9s\xfc\x92\xc3P\xa6\x1d\x80\xe2[\b\xe7sɤt\xcf5\x7fV\xdc\xf9y\x00\v\x85%\xb8\xa9\xaf\x18\a\x94uaEU\xb4\xf7\xb1\xc5\x02\xce\r\xec\x9aˊ~VtD\xde\xdf\xd4\xf5\xf9K#\xf1\xcbAT\xc3\r{\x82\xa2`<67\xf7\xa8\x90\xb9{@3\xb5\x00\xb4\x8d8\xcb\xfdeL\xfe\xf2\xd0\v7]\xe86\x00\xb2\xb0el\xa9\x8f\xcb\xc37}\x1d4`\xa9zl\xcf3w\xf1\x06}\xfb\xa5\x06\xbdct\xefX㛵\x87J\xfdD7\x18\x98\x06\xf5\xe3\xd5\xe1\xa1=\x93\xbd\x00\xa7U\x0f\xec\x9dt\x1e\xc1\x10'j\x83z\xa7\r\xe8P\xa9b\x9c\x16\xed'\x02B\xaa\x06B\xa4i\x8a\xf3?\xe7\x94\xe5K\x84w\xa7\b\xf0\x92<\xa0y\xde\xeb7<=y\xec\xa9\xc9\xf4d\x94\xa4S\x92/\x11\xee\xcd\t\xf8f\xf9\xab\xe9\xa7 \xe7o<\xbf\xf0\xa9Ǘ:\xed8\x83z\xa9\xa7\x1b\xe7\xd3\xee\x95N3\xbe\xfa)\xc6\xd7<\xbd8\xeb\xd4brz֬\x8c\x839\xa9U\xcf8n\x97\x96K0}\n1\xf1\xf4ab\xa6A\xda\xe0\x8f\x1cv\xe2\xe9\xc2\xf9\xa7\n\x13\xf9;gJ\xbf\xf2\xe9\xc1W>5\xf8-N\v&H`B\x95\xf9\xa7\x02\x9f\xbd%\xa5t\x0ezr\xdbo\x8e\xd4N\xcakj,\xd7Gl\xb0\xaf\x15n\x93\xc5Z\xbd\x18\x80̒\xbfȟ\x1em8\xb4\r\x8e\x92\xd9\xf1\x88z\xfb\x92\xad\xbb\xd6w\x88\xfdk\x0en\xeb\xd2@\xc5\xd1\x00P\xe0F\xa9YQW\xe1\x03\xcf6\x83\x1e6ܰ\xb5\xd2%\xb7\xec\xbc\xd9,~\xe3:\xc0\xbfϗ\x8c\xfd\xa0\x9a\\\x9d\xee}iF\x94U\xb1\xc3H\x8c\x9dw\x1b\xe7\xe8\xbdz\xae\xc1\xdeeCt\xf3_\xd6\xc9\x16\x89\x05>\xd8\\\x84[\x17\xfbW2\xbb\xfb\u070f\\+\xe1\x95\xf83=\xb7t\x82U\xb7w7\xd7\x04+\x88\x11\xbd\xe3\xd4$(6,_\x01\xba\f\xed\xd8\x0f\xe9\x93\xebu\x0fj?G\xb8\xfbX\x05\xe4\xeee\x92\xe0\xb6x՜)\xd4Z7\xd7\x0e\x97C=\xa1|q\xb9c\xca?=!t\xbe\xa8\xb8\xb6;\x97Lt\xd1\xc3#\xd8\xf5\xa9U\xb3\x83\xd6j\xff\xe5\x95n\xe9\x91=<\xbaB;ٻ\xaa\x9f<0\xa4\xe7sp:|\xaaz\xf2<\xf5\v\xe0t\u0605Z\x10\x15#?E3 O\xbebi\xfc\r\xfd?\xaa-\xbc\x8f\xae\\\xf6__\x194\x19IM\fP\xe9\x92\xf9\b\x05\xdb|D\xba\xe3\xfbyj/\x9ek\x18P\xf1w\x84?gq\xf2\xb6\x0fj\xfcA\x12\xbaA=t\x1a\xf3\xaa詧\x1d\xbb\xb9\xa7\xb8\xb5Q\xa5~\xea\xfb\xb85,O\x86\x04\x83\b,!\x0f\xbe\xd1r*2Z\xa5\xf9\x03|T\xeem\x9d\x141\xe9\xb7轼\xe4=\xb7\x90\xaf\xed'aL\xd1\xfb\xb1\r\x01\xb6\xe73\xf6.\xfaGl\x8f|\xca\xc0\xda\xe292rw\xf7э\x94\x9e4y\xef_'A}l\x00Y\x10(࠭\xf0\xbf\x1b\xf5D\x17\xe0\xc7ט\xc3\x03\"\x9d7̀\x0e\x8aP\n\xefQì\xabB\xf1\x1c\xf4\x15=\xa2\x920\xe2\x9fz\r\x06\xee@\xff)\x16o7#\xe3\t=\xbf`\x96\fztE\x01\xc5\x0f\xa2\x00\xe3\x10O4\r7\xfb-\x1bKQ\x97+穮\xf1Ǧ\x93\x03\x96\xd9\r\x956\x18*\xd0\xe8'\xba\xad\x88\xda\x04\xc9?L\f\xd6\xf0QH\v\x0f0\x1eCO\xd8\x04\xf7F\x039\x00A\x81Q\xc4\xf7\x97\xd8\xcac\x8f \xf7\xf1\xd6\x03\x19h\x16#cr\xac\xbc[us\x7feX-s\xda\x00\xb8\xff\xf3\xedQ\xf2\xbb\xed\xbd/\x13tB\x8az\xbf\x1fo\xd9\t\x11:ډ|\xfa\xb8\x12\x8f\xc1\xe2ƨLPT\xf1$\xac\xbf\xce\xf1\xe5\xee\x10?\x14 \x1e\x90\x8e\xda\xc0\xe7'\t\xfaK\xb0@\xe6Z\xc6\xdem\x99\xd6~?\xedA\x8b\xbe\xd7b\x15\xf6=\x02c\x00\x80\xa9\xb0\xcfe\xdcK@a{M\x98\xe6q\xb3}zN\xa8\x90\xb8\xa5\x1bw\xd8\x16\xe3o1-\x9a7\xa3\xce\x12\xc8\xed\xde?\xea\x03\x1e\x7f\xd2\xce=\x94\x94\xf1\xca\xd6:h\xd7Z\xd3-\xeb\b\x04\xdc%\xe4\xc7=j\u05feuv\f\x83\xdb\xc7\xc6\xda\xfd\x87\xc9\xe7PG\xe04\xcf\xd2E߸r\x11\xb5{\xaet\x81\xf0\x8f\xe3\xf1\xe8\x8cA\x9co\xdd\xdbe\x13D\xf8\xd8\xd6\x1c\x1bp3\f\x1c\xb2\x7f\r\xedUGB\x97\xeeO\x8c\xe1\x06\xeb4\xa7\\\xbd\x1cQ\xc3pY\xffm\x8c\t\xe3G!\x17\xec\x13\xecG\xec\v\xf6A\xe2 \xf6\t\xe0\xce;BN[+\xa4\x1d\xe7\fq۴\xa2æ#\x1arZl\xef\a0\x06\x99\xec\xf4\xe8SSŝ65\xec\xf7b\xcc\x1b\xa5\x1d\xb3\f\a\xfa\x87\xbd_\xa3\x1a\xfc\xa0\xf6\x8ei\xeeQ5\xb2\xf7\x91\x1e\xc2\xcb;\x92\xe3\xbd\xf4\xee\x97z\xd5>\xa8\xc0\xfe\xf6\xf7\xb3\x7f\x04\x00\x00\xff\xff)6\x10\xe1Z{\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"), } diff --git a/config/crd/v2alpha1/bases/velero.io_datadownloads.yaml b/config/crd/v2alpha1/bases/velero.io_datadownloads.yaml index 7a8b9441a..36ab864f9 100644 --- a/config/crd/v2alpha1/bases/velero.io_datadownloads.yaml +++ b/config/crd/v2alpha1/bases/velero.io_datadownloads.yaml @@ -11,6 +11,8 @@ spec: kind: DataDownload listKind: DataDownloadList plural: datadownloads + shortNames: + - dd singular: datadownload scope: Namespaced versions: diff --git a/config/crd/v2alpha1/bases/velero.io_datauploads.yaml b/config/crd/v2alpha1/bases/velero.io_datauploads.yaml index 556272aac..15682739b 100644 --- a/config/crd/v2alpha1/bases/velero.io_datauploads.yaml +++ b/config/crd/v2alpha1/bases/velero.io_datauploads.yaml @@ -11,6 +11,8 @@ spec: kind: DataUpload listKind: DataUploadList plural: datauploads + shortNames: + - du singular: dataupload scope: Namespaced versions: diff --git a/config/crd/v2alpha1/crds/crds.go b/config/crd/v2alpha1/crds/crds.go index 4c62c3c08..59af9e6f0 100644 --- a/config/crd/v2alpha1/crds/crds.go +++ b/config/crd/v2alpha1/crds/crds.go @@ -29,8 +29,8 @@ import ( ) var rawCRDs = [][]byte{ - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcYK\x93\xe3\xb6\x11\xbeϯ\xe8\xda\x1c\xf6\xb2\xd2d\xf3p\xa5t\xdb\xd1\xc4US\xf1Ϊ\xac\xc9\xdcA\xb2E\xc1\v\x02\b\x1e\x92\xe5$\xff\xdd\xd5\x00I\x81$4z\xd8^݄n4\xbe~\xa0\x1f\xe0l6\xbbc\x9a\xbf\xa2\xb1\\\xc9\x050\xcd\xf1g\x87\x92\xfe\xd9\xf9\xd7\x7f\xd89W\xf7\xbb\x8fw_\xb9\xac\x16\xb0\xf4֩\xe6G\xb4ʛ\x12\x1fq\xc3%w\\ɻ\x06\x1d\xab\x98c\x8b;\x00&\xa5r\x8c\x96-\xfd\x05(\x95tF\t\x81fV\xa3\x9c\x7f\xf5\x05\x16\x9e\x8b\nM\x10\xde\x1d\xbd\xfb\xf3\xfc\xe3w\xf3\xbf\xdf\x01H\xd6\xe0\x02H^\xa5\xf6R(V\xd9\xf9\x0e\x05\x1a5\xe7\xea\xcej,Ipm\x94\xd7\v8\x12\xe2\xc6\xf6\xd0\b\xf8\x919\xf6\xd8\xca\b˂[\xf7\xaf\t\xe9\an] k\xe1\r\x13\xa3\xb3\x03\xc5rY{\xc1̐v\a`K\xa5q\x01\xcft\xb4f%\xd2Z\xabS\x802\x03VU\xc1JL\xac\f\x97\x0e\xcdR\t\xdft֙A\x85\xb64\\\xbb`\x85\x14\x16Xǜ\xb7`}\xb9\x05f\xe1\x19\xf7\xf7OreTm\xd0FX\x00?Y%W\xccm\x170\x8f\xecs\xbde\x16[j4\xe5:\x10\xda%w \xbc\xd6\x19.\xeb\x1c\x82\x17\xde T\xde\x04\x17\x92\xde%\x82\xdbr;\x84\xb6g\x96\xe0\x19\x87\xd5I \x81N\xe2\xacc\x8d\x1e#J\xb6FH\x15s\x98\x03\xb4T\x8d\x16谂\xe2\xe0\xb0Sc\xa3L\xc3\xdc\x02\xb8t\xdf\xfd\xed\xb4-Zc\xcd\xc3\xd6G%\x87\x86y\xa0UH\x96#\x12\xf2R\x8d&k\x1d\xe5\x98\xf8-@\x1c\txH\xf6G$Qn\xba~\x16\n\x85\x1c\xa8\r\xb8-\xc2\x03+\xbfz\rk\xa7\f\xab\x11~Pet\xdf~\x8b\x06\x03G\x119(z\x81\x93\xef\x94ɺNc9\x8f\xbc\xad\xb0N\xd6\xc8\x7fÃ~\xf7\xd8*\r\xb2llu\xa9f\x1e8\xb8\x92\xf9\x00\xfbT\xe3E\xc1\x95\x1aQ\xaa\n\x13\x8b\r0q\vڨ\x12\xad}#\xe0I\xc0\x00\xc5\xf3qab\x9aȱ\xfb\v\x13z\xcb>\xc6$Sn\xb1a\x8bv\x87\xd2(?\xad\x9e^\xff\xba\x1e,\xc3\x1b\t\x83\x95\xceR\xa6 \xf8\xda(\xa7J%\xa0@\xb7G\x94\xd1\xf5\x8dڡ\xa1\x12\xbdFCb\xc0n\x95\x17\x15)\xbbC\xe3\xc0`\xa9j\xc9\x7f\xe9e[p*\x1c*\x98C\xeb\xc2e4\x92\t\xd81\xe1\xf1\x03\x19m$\xb9a\a0Hg\x82\x97\x89\xbc\xb0\xc1\x8eq|&+r\xb9Q\v\xd8:\xa7\xed\xe2\xfe\xbe\xe6\xae+\xbb\xa5j\x1a/\xb9;\xdc\ao\xf0\xc2;e\xec}\x85;\x14\xf7\x96\xd73f\xca-wX:o\xf0\x9ei>\v\x8a\xc8Pz\xe7M\xf5'\xd3\x16j;8v\x12\x88\xf1\x17\n\xe6\x15\xee\xa1*J\xb7\x82\xb5\xa2\xa2\x8aG/\xd0\x12\x99\xee\xc7\x7f\xae_\xa0C\x12=\x15\x9drd\x9dإ\xf3\x0fY\x93\xcb\r\x9a\xb8ocT\x13d\xa2\xac\xb4\xe2҅?\xa5\xe0(\x1dX_4\xdcQ\x18\xfcǣu亱\xd8ehM\xa0@\xf0\x9a\xf2A5fx\x92\xb0d\r\x8a%\xb3\xf8\x8d}E^\xb13r\xc2E\xdeJ\x1b\xae1s4oB\xe8:\xa6\x13\xaeM3\xc8ZcI^%\xc3\xd26\xbe\xe1m%\xa14\xc0\x06\xbcC\v\xe5\xaf>\xfd\xb2\xd5d\xcct.\xdc\xe8\xf7\x90\x13ԡ\x95I\"ok\x9dm\x8b\x94\x18\x16\xa9\xf47\xa9\x8f\x06\xb5\xb2\xdc)s8V\xc9q(\x9c\xf4\n\xfdJ&K\x14\xb7\xa8\xb7\f;\x81ˊl\x8e}(S\x12\x8aR\x03P%kE\x97k\xe0\nxr\xc4C\xb1m\xd1\xe5\x15\x95٪\xc6%\x1c{JH{DZ\xba\x85R\x02\xd9؊\x14\x85\x9f\xa9,,\x95\xdc\xf0z\xaax\xda\xfe\x9e\n\x9136\xcd\x04lr$iA\xd1IHf\xa1BͺХԾ\xe1\xb57\xa7\xfc\xbf\xe1(\xaaI\xfe9y\x93:\x85\xc3)\xb7\xf8\xb8\x87\xdeݮ\xb6\xaa%\xa5ש\x90\xa1l\xe8w\x93М\x82\x04x\xda$\x12\xb9\x85w\xef@\x19x\x17g\xa2w\x1f\xe2nυ\x9b\xf1A\xfd\xdfs!\xbaS\xae\x8an\xeap\xbe\xac\xcfh\xfe\x1c\x98\bϗ\xf5\xb5\xbd\xd5\x14\rJ\xdfL\x0f\x9c\x01\xf3Ne\x96\x05\x97\xfe\xe7\xcc\xfa\x9e\xcbJ\xed\xed5\xca\xf6\xfd\r\xb5\x98ʻ[\x1c\xfee$c\xe4wG\rq\xf0\xb5S\xb0g<\xe91\xfa\xd3퇌\xdc\x027T\x90\f:o$\xa5\x034\x862\xb4\r\"\x95\x9f\xf4\xe1]\xe6\xd2\xd9'\xbe\xb6\xb6n/\xbd\tY\xb0}\x83U\x9b\x1b\xa7\x1fV\x96\xa8\x1dV\x0f\aj\x8b.\xe8\x9c\b\x80|\xfbU\xea\xdf\xfa\xd87\xa1f\u05ce(\x1d\xa4\xfe\xe5얊\xf4i,$<\x9f\x98*\xe9k\xa6pco{\x1a4\xc0\v\xd5\xe00\xfe\xbf\x8f\xad\fm\v\r\x12\xb5\xf8\x93COVi\x9a\xefg\xb4\x7f\xc2!\xbd\x10\xac\x10\xb8\x00g\xfc\xa9Y'?\xdaŇ\xe8\xf4\xcd\xf1\xa69o*fj;ֿ\xb2\x85\xd7\xd0\xee\t\x89\x88A\x80\x83E\x8af\xf9\xef\xa9\a\x10\x14HBk:ᡫ\x8d\xe5\xe1m\xf8\xde\x02M&\x93;\xd6\xf07Ԇ+9\x03\xd6p\xfcŢ\xa4\xbf\xcc\xf4\xfd\xeff\xca\xd5\xc3\xf6\xe3\xdd;\x97\xe5\f\xe6\xceXU\x7fE\xa3\x9c.\xf0\t\xd7\\r˕\xbc\xabѲ\x92Y6\xbb\x03`R*\xcbh\xd8П\x00\x85\x92V+!PO6(\xa7\xefn\x85+\xc7E\x89\xda\x13\x8fGo\xbf\x9f~\xfca\xfa\xb7;\x00\xc9j\x9c\x01\xd1s\x8dP\xac4\xd3-\n\xd4j\xca՝i\xb0 \xb2\x1b\xad\\3\x83\xc3D\xd8\xd6\x1e\x19\xd8}b\x96\xfd\xcbS\xf0\x83\x82\x1b\xfb\xcf\xc1\xc4O\xdcX?\xd9\b\xa7\x99\xe8\x9d\xea\xc7\r\x97\x1b'\x98Ng\xee\x00L\xa1\x1a\x9c\xc13\x1dٰ\x02i\xac\x95ij0\x01V\x96^7L\xbch.-\xea\xb9\x12\xae\x8e:\x99@\x89\xa6м\xb1^\xf6\x03C`,\xb3\u0380qE\x05\xcc\xc03\xee\x1e\x16\xf2E\xab\x8dF\x13X\x02\xf8\xd9(\xf9\xc2l5\x83iX>m*f\xb0\x9d\r\xea[\xfa\x89v\xc8\xee\x89[c5\x97\x9b\xdc\xf9\xaf\xbcF(\x9d\xf6f#\x99\v\x04[q\x932\xb6c\x86\x98\xd3\x16ˣl\xf8y\"f,\xab\x9b!?\xc9\xd6\xc0P\xc9,\xe6ؙ\xab\xba\x11h\xb1\x84\xd5\xdeb\x14b\xadt\xcd\xec\f\xb8\xb4?\xfc\xf5\xb8&ZUM\xfd\xd6'%\xfbjy\xa4QH\x86\x03'd\xa1\r\xea\xacn\x94e\xe2\xb70b\x89\xc0c\xb2?p\x12\xe8\xa6\xe3gYY\xc8Bc\x8d\xf26\x86\xf8a\xf7\x98\x9b\x94t:\xdbh\xae4\xb7\xfb\x19|\xfc\xfeR6\xe9V\x80Z\x83\xad\x10\x1eY\xf1\xee\x1aXZ\xa5\xd9\x06\xe1'U\x04\x1f\xdbU\xa8[\x1f[\x85%\xa6RN\x94\xb0\x8a\x86\x010V鬳5XLî\x96n$;\xf0\xb8\xfe\x99\xdf\xf8.\x14\x1aY\xf6.D0\x9c\xfa\x15\\\xc9\xfc\x85\xf8\xb4\xc1\x8b.C\xaaM\xa9J\xecT\x87)G\xdc@\xa3U\x81Ɯ\xb8\x9e\xb4\xbd\xc7\xc3\xf3a`\xa4\x96\xb0b\xfbg&\x9a\x8a}\f`XTX\xb3Y\xbbC5(?\xbd,\xde\xfe\xb2\xec\r\xc3Qhc\x855\x84i\xc4z\xa3\x95U\x85\x12\xb0B\xbbC\x94\x1e^\xa1V[Ԅ\xc5\x1b.\r0Yv4!]p\x88(\xe4\xfa\x9e\x1e͆\xc9֝T\x83:5;\xb92\x8dY\x1e\x83D\xf8\x92藌\x0e\x84\xf8ߤ7\a@r\x87]PR\x18\xc4 U\x1b\x02\xb0lU\x15\xec\xc6\rhl4\x1a\xba^ޫ\xd4\x1a\x98\x04\xb5\xfa\x19\v;\x1d\x90^\xa2&2\xf1>\x14JnQ[\xd0X\xa8\x8d\xe4\xff\xe9h\x1b\xb0\xca\x1f*\x98Ec\xfd\x85Ԓ\t\xd82\xe1\xf0~\xa0=\xfaj\xb6\a\x8dt&8\x99\xd0\xf3\x1b̐\x8f\xcfJ#p\xb9V3\xa8\xacm\xcc\xec\xe1a\xc3m\xcc\t\nU\xd7Nr\xbb\x7f\xf0\xc6\xe0+g\x956\x0f%nQ<\x18\xbe\x990]T\xdcba\x9d\xc6\a\xd6\xf0\x89\x17D\xfa\xbc`Z\x97\x7f\xd2m\x16azǎ\xbc0|>\x9e_a\x1e\n\xf3t%XK*\x88x\xb0\x02\r\x91\xea\xbe\xfec\xf9\n\x91\x93`\xa9`\x94\xc3ґ^\xa2}H\x9b\\\xaeQ\x87}k\xadjO\x13e\xd9(.\xad\xff\xa3\x10\x1c\xa5\x05\xe3V5\xb7\xe4\x06\xffvh,\x99nHv\xee\xf3&X!\xb8\x86\xa0\xa0\x1c.XH\x98\xb3\x1aŜ\x19\xfc\x83mEV1\x132\xc2E\xd6J\xb3\xc1\xe1\xe2\xa0\xded\"&tGL{\x80\x8fe\x83\x05ٔ\xd4J\x9b\xf8\x9a\xb7\xb1\x840\x80%+\xfb\xda\xc9_{\xfa\xb2!d\xb8蜫\xd1\xf7\x98#\x14y\x95\t~\xc7P\xd7F&яL\xe9w\x00\xf9v\x8f\xc6F\x19n\x95\xde\x13\xe1\x10\x1a\x87np\xd4\"\xf4\x15L\x16(n\x11o\xeew\x02\x97%i\x1c;7&\x00\nT=\xa3Jn\x14]\xac\xc4\x10\xb0\xb0\xb4\x82\xbcڠ͋)3\xa1\x8cK8$\xbd\x90&\xb7CQWJ\tdC\r\x16\x86/%kL\xa5\xec\x19\x81\x17k\x88+_\xf7\r\xd2\xe1\xf3\xe5\xe2\x9e\xfe\x89\xe3\xe4A[^\xb6\x10O\xb7\x8c\xb2\xad\xbc\xd9Z;ϗ\v0\xed\xf6\xb1\x91\xa4\x13\x82\xad\x04\xce\xc0j7\x16\xec\xb8\xc3z\xee5ߢ\xce\xcd\fo\x8e_\x18\xbd0l\x03g|R\xed\x87ި \xc1(\xe5\\I\x8b2g\xa3\x93^E_\x94t.\x98\xc9\xf2<\xe0l\x99\xae\xcf]\x93H\x10\n\xbf\xc2V,\xcf\x17\x84\xa0\xeb\xe58l\xe2]n\x06;n\xab\x9b$\n\x17\xf4b\x81\x92\xe5Yy\xda\xfb\x1e\xc4Q\xeb\x13¼\xbcͽ\xbc\xe7$\xa3ps\x8bd۞\xd1/\x90\xad\xef%9\xe9\x06\\\x1e\x13N\x11\n\x10\x98a\t\xae\xb9\x9ew\x02\x1d\xae\xb1\x1c\xf3<\xe9\xd9+3\xdd\x17\xfa\b\x92\x8c\"\x13\xb4I\xe7gJ+\xe7J\xae\xf9f|vZ柺\xb6'E\x1bE\xbc\xe4H\xd28\x058\xe2d\xe23\xdcI\x8c~\x94\x1b\xae\xf9\xc6\xe9ch\xb4\xe6(\xcaQ\x02s\x16\x80\xce\xe8\xc33qK\x1c\xe9$\x8b\xf1\xbb\x85\xd4$\xb3\x0f^\x92\xa2T\b\x7fc\x19\x80\xa0\xfb@\x91\x1b\xf8\xf0\x01\x94\x86\x0f\xa1%\xf4\xe1>\xecv\\\xd8\t\xef\x95\x17;.D<\xe5\xaa\bڕ\x14T\xd0)w.\xb4du\xf0e@c\xa0\nKŧ\x17\xdf*\xd81\x9e\xa4\xf5\xdd\xe9\xe6>Cw\x85k\xca\x015Z\xa7%EaԚ\xd2\"\xe3I*\x97\tC'$5IH<#\xe50zz)\xe8\xffC,O\x01 #@\xceƧ8\xf4)\xfb\x8f\xcbK8L\x96F\x0e\xd7\\ \x98\xbd\xb1X\xf7\xb9\r\x95@\x00\x8c\x1b\x18\xea\x1a\x82\xb7\xf8ƲO\"\xf2\xaa4\xdfp\xf2\x00\xd9\xcd\x1c\xb2\xc3\x16|\xdb6\x8a\x87V\x1f\x1b\xb2\x17\xa6\x83oC\xf0} G\xf8\x12\x0e\xa7\xf0\xc3d\xe9\x13\x98n\xbel\xb1 \x83$g\x15\xf2\xf26\xbf\xc8Fy\x00\xcb\xde\xd1\x17\x03W\xb0\x99\x0f*\x93|i0X3\x84\x83\xc1tz\x87\x86S}Cgg_\xde\xe6\x17\x95O\xbe\xb3sY\x01\x15:˭\x96\v\xa7\xb5/MèZ\xdfTB\xb1\xa2\xc0\xc6b\xf9\xb8\x7fV\xe59\xa7\xff\xd4[L\x8c\xc8Kz[\x19S\xfbn\x176\xec\xda\x1a(\xb2\xdbu\xe4n\xb9\xa6\x9f\x86D|oF\x97\t\x82\x8f+\x9a\x80~Ǚ\x06x%\a\xf7\xbd\x85\xef\x02h\xd36\x1f\n\xe8z\x8e\x0e\x1dQ\x88M\xe0\x92Y\x9c\xd0\xfe\xdb\xc2~\xbev\f\r\xf9\xb4\x97yS!9&3\xd6\x1d\x8b\x15\xafo\xb2Ɨ\x80\x9c\xc6\x0e\xe4:}\x05jX\x02nQ\x82\x92\xb0f\\P2\xe1If\x00\xec4\x956\xaa\x86g\x9f\xd84\x8a\r\xc6l\xf7\xee\xbc%3J\x18\xa3\xd9\xefi\xcc.\xa7\xfd\x8aƉL\x16\xf3;\xe6\xb4\xe1\xc8о0ٜ\xf6t}\xcd\f0ЁH\x8b\x1b\xc7@\xebb%e\x13\xdd\xe1cɹ6\xc2`9TJ\xb4N-]\xbdBM\xdc\xfa'\x1b\x90\xb8\xa3<\xb5\xa8\x98\xdcd3\xa1\xf8\xe4\x80 \x98\xb1\xad\xbb\x1d\xf5\x90\xf4\xcdg(Y\xfaFs\xf8j4\x86m\u0381\xf5\xe7\xb0*tQ\xdb-\xc0V\x94\xb2\xf6\xb5\xfe\x9dic\xc8UH,χ\x8b\xab\x82D\xef\x01\xe4jN\xbe,/\xe0\xe5˒\x0e\xf9\xb2\xfc\xad\xbc\xa0tu\xae\x88eΪ̰\xe0\xd2\xfd\x92\x19\xdfqY\xaa\xdd\x18:N\x88\xda0[\x9d\x11\xf4\x85٪K\x92\x9d\x10~\xcf(\x97o\xb3\xce\x15\x12&~\xab\x94\u07b7\xf9αGkr)\f^\x02\a\xc74\xff\x8c\xbb\xcch\f\xb9\x99\xa9\x976\x8eg\xa6F\x8f\xf5\xe9d\xe8\xa4\xe6\xe02\xceeiv\xefᙹ\x1f}\x80\xbbJ\xcf-\x7f\xb7D\xf0\xae'{\xc07\xff\xbc=B\xb9~o\x88J\x8a\xc4b\x19\xc2\xc9\xfe\xae\x8e\xf1\x94\xa6\xf0Zq\x13\xbbȱ4.\xb9i\x04\xdbw\xb2\x9c\v\x1b\x1dn\r_\a\xc7Nr\xba\xfd\xda\xfd\xaa \xdf:;\x8d\xcap\x06\x99\xfd\xbc:\x1er\xbe\xc5\t'b^\xbcދ\xa7\vk\xfe\xc5S\xbc\x8a\xbcDi\xf9\x9a'/\xb2\x87b\xcdw\xf8s\xba\x1c\xbel\\W_\xf6~krS\xbdݣp&\x13m\x7f\xfa\x92\xcb\xf7\x96\x04\x06\x04A\xfe\rp>|\xf5\xbf\xef\":\xb3\xedCd\b\xfe\xb9\"VIJo|zt}j\xd9\x17\xe8\x8f\xcc*\xb3^5\x1a\xf4\x9c\x97\t\xed\xb6o\x9b\x8e\xb8U\xf72<\x83\xff\xfe\xff\xee\xd7\x00\x00\x00\xff\xff\xf1\x86o_\xa2&\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcYK\x93\xe3\xb6\x11\xbeϯ\xe8\xda\x1c\xf6\xb2\xd2d\xf3p\xa5t\xdb\xd1\xc4US\xf1Ϊ\xac\xc9\xdcA\xb2E\xc1\v\x02\b\x1e\x92\xe5$\xff\xdd\xd5\x00I\x81$4z\xd8^\xdd\x044>|\xe8n\xf4\x03\x9c\xcdfwL\xf3W4\x96+\xb9\x00\xa69\xfe\xecP\xd2?;\xff\xfa\x0f;\xe7\xea~\xf7\xf1\xee+\x97\xd5\x02\x96\xde:\xd5\xfc\x88VyS\xe2#n\xb8\xe4\x8e+yנc\x15slq\a\xc0\xa4T\x8eѰ\xa5\xbf\x00\xa5\x92\xce(!\xd0\xccj\x94\xf3\xaf\xbe\xc0\xc2sQ\xa1\t\xe0\xddֻ?\xcf?~7\xff\xfb\x1d\x80d\r.\x80\xf0*\xb5\x97B\xb1\xca\xcew(Ш9WwVcI\xc0\xb5Q^/\xe08\x11\x17\xb6\x9bF\u008f̱\xc7\x16#\f\vnݿ&S?p\xeb´\x16\xde01\xda;\xccح2\xee\xf9\x88?\x83*\"Z.k/\x98\x19.\xba\x03\xb0\xa5Ҹ\x80\xb0F\xb3\x12i\xac=l\xc0\x98\x01\xab\xaa\xa0>&V\x86K\x87f\xa9\x84o\xe4q\a\xb4\xa5\xe1\xda\x05\xf5\xa4|\xc1:\xe6\xbc\x05\xeb\xcb-0\vϸ\xbf\x7f\x92+\xa3j\x836\xf2\x05\xf8\xc9*\xb9bn\xbb\x80y\x14\x9f\xeb-\xb3\xd8\xceF\x1d\xaf\xc3D;\xe4\x0e\xc4\xd7:\xc3e\x9dc\xf0\xc2\x1b\x84ʛ`[:w\x89\xe0\xb6\xdc\x0e\xa9\xed\x99%z\xc6au\x92H\x98'8\xebX\xa3nj\x92\xa5\x91R\xc5\x1c\xe6\b-U\xa3\x05:\xac\xa088쎱Q\xa6an\x01\\\xba\xef\xfevZ\x17\xad\xb2\xe6a飒C\xc5<\xd0($Ñ\tY\xa9F\x93ՎrL\xfc\x16\"\x8e\x00\x1e\x92\xf5\x91I\xc4M\xc7\xcfR!\x97\x03\xb5\x01\xb7Ex`\xe5W\xafa\xed\x94a5\xc2\x0f\xaa\x8c\xe6\xdbo\xd1`\x90(\xa2\x04y/p\xb2\x9d2Y\xd3i,\xe7Q\xb6\x05\xeb\xb0F\xf6\x1bn\xf4\xbb\xfbVi\x90e}\xab\x8bA\xf3 \xc1\x95\xcc;ا\x1a/r\xaeT\x89RU\x98hl\xc0\x89[\xd0F\x95h\xed\x1b\x0eO\x00\x03\x16\xcfǁ\x89j\xa2\xc4\xee/L\xe8-\xfb\x18\x83L\xb9ņ-\xda\x15J\xa3\xfc\xb4zz\xfd\xebz0\fo\x04\fV:K\x91\x82\xe8k\xa3\x9c*\x95\x80\x02\xdd\x1eQF\xd37j\x87\x86\x02`ͥ\xed\x11)\x9cW\xa9\xc01\x98\x93\x7f\a<\x9a\x8d\x93\x06\x83\xf7\x10A\x93Z\x1fhO\x8d\xc6\xf1.|\xb6\xd8\xc7̓\x8c\x8e\xce\xf1\xbf\xd9`\x0e\x80\x8e\x1eWAE)\b\xe3\xb1\xda؊U\xab\xadhn\xa1\x94@6\xd6\"y\xe1gJ\vK%7\xbc\x9e\x1e<-\x7fO\xb9\xc8\x19\x9df\x1c6ْNA\xdeILf!C\xcd:ץо\xe1\xb57\xa7\xec\xbf\xe1(\xaaI\xfc9y\x93\xba\x03\x87]n\xb1qO\xbd\xbb]mVKR\xafS!B\xd9P\xef&\xae9%\t\xf0\xb4I\x10\xb9\x85w\xef@\x19x\x17\x9b\xa5w\x1f\xe2jυ\x9b\xf1A\xfe\xdfs!\xba]\xae\xf2n\xaap\xbe\xacϜ\xfc9\b\x11\x9f/\xebkk\xab)\x1b\x94\xbe\x99n8\x03\xe6\x9d\xca\f\v.\xfdϙ\xf1=\x97\x95\xda\xdbk\x0e\xdb\xd77Tb*\xefn1\xf8\x97\x11\xc6\xc8\xee\x8e\n\xe2`k\xa7`\xcfxRc\xf4\xbb\xdb\x0f\x19\xdc\x027\x94\x90\f:o$\x85\x034\x86\"\xb4\r\x90\xcaOj\x9e7Oj%\xd3v\xab\xdc\xd3\xe3\x993\xae{\xc1.\xee>=v&~\r^\xd7\a\xdfV\x122V\"\xfa]\x15Y\x85\xb4~\x13\xdb5\xff\x05/\xe4K\xa2\x1dc\xa1j^2\x016\x8cɶ\tl\x0f\xd1aO\t\xe5\xfa\xbc1ݴ[K\xf8\x86ڧ\x7f!\xb8ō\xd6C\x88\xee(\xca\U0001a4f3\xc8~\xe6x\xc7vJ\xf8&\x88\x92I\xb0\x02\xafO\xe8\x1a(}P\xb1U T|\xb3AC\x15U(\xb7\xe2ƫ\xd7\xe5{\x9bl\xc27\xe9\x1f\xcaT\r\xd3\x1a+\xea\xed\xc8\x19[\xdb^eU\xc7L\x8d\xee5\x90>\xa3\xa2\x97D\xb4S\x05\x95fd\xa0\xb6\xf6\x0f\x97+\x88\xc1\xeau\x99\xa9\xd4\xe9\xb7z\x9d2<]\xc7\xd0oc_\xe8\x04\x99\x99\x11\xc5\xef\xd7$ؑ\xdbp\x81`\x0f\xd6a\x13T0b\x18-\x95\xb3˙\xb4\bG3\\\xc0i\xe2>\xed\xf6=\xc6-\x04\xf4\ue09dW\xaf\xb92\xad\xb7\x0f\xb8-s$\xd1v\xfdP\x1c\xb2\x98\xd0Řֿn\xe3[^Dx\xf9&\xe3\xe5\x98\xf2\t\xbe\xc5\xe17S\xa6*\x90\x1b\xacr9\xf0\xb4\xe5f\xa0w\xd9\xc1\xf2\xf2Z'\xbf\xf3,_Џdƹs4}L8\xe3\x89a\xa0\x1bͦ1\xe2\xa2\xce'\xbc\xcb\\\xda\xfb\xc4\xd7\xd6\xd6\xec\xa57!\n\xb6o\xb0jsc\xf7\xc3\xca\x12\xb5\xc3\xea\xe1@e\xd1\x05\x95\x13\x11\x90o\xbfJ\xfd[\x1f\xeb&\xd4\xec\xda\x16\xa5\xa3Կ\x9cݒ\x91>\x8dA\xc2\U000c9a52\xbafJ7ֶ\xa7I\x03\xbcP\x0e\x0e\xed\xff\xfbX\xcaвP Q\x89?\xd9\xf4d\x96\xa6\xfe~F\xeb'\x12\xd2\v\xc1\n\x81\vpƟ\xeau\xf2\xad]|\x88N\xdf\x1co\xea\xf3\xa60Sݱ\xfe\x95-\xbc\x86vO\xe09\x95\x1d\xf1z\x85E8\xac\x00w(\x81\xbaw\xc6\x05V\x1df\xa6\xe19\xa7\xf9\f\xe9i-\xfdG*\xbfAkY}\xee\x02}\x8eR\xf1a\xaa]\x02\xac\xa0\xc2{\xdcv\xbc\xb7\xedݾ\xba\x01\xfa}.\xf1\x85\xed\xcf\x1b\\B\xb3~\x86̊dr1\xad\xa7v:\xa8\xc1\x1b\xdd\xd73\xee3\xa3\xdd\xfd\xccL\xad\xdaK\x9f\x99\x9a|\xd3J'\xe3\xabH.1vsY\xcc\xfe\xa3Qf\xee\xfbp\x19\xae\xd2t\xcb\xef\x96\xeb\u07bf\xadl\x95\xe8nx\xf8\xd8#}S\xa0!3\x14\xb9\x0e$<\xc9'V\xcb\x15\x7f=B\xdfL\x05\xa89\xbcl\xa94\x89\x0fB]{Yq\xab\x05;\xf4\x87IK\xe6\f\xf8\xf1\xd6L\xde\xfb\xaf\xad\x9a\xfb\x8fo\xf9\xca\xeb\xed\xce\n\xcetWa\xbe\xff\xa8\xf6\xc7\xec\xf0\xc6s\xd0\xf0#\xe7M\xbd\xdd\x00\xe1\\*h?\xba^\x1f\xc1\x87\xdb|\xcb\xe0\x9d\xd5\xded00\xaf\x12\xec\xf6\xf96\x1d\xf1E\xffMc\x01\xff\xfd\xffݯ\x01\x00\x00\xff\xff];\x85{\xd8 \x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcZIs\xe36\x16\xbe\xfbW\xbc\xea9\xe4b\xc9\xe9YRS\xba\xb5\xe5I\x95j\xd2nW\xcb\xe3;D>\x89\x88A\x80\x83E\x8af\xf9\xef\xa9\ap\x01IH\x94\x94Nx\xe8jcyx\x1b\xbe\xb7@\xb3\xd9\xec\x8eU\xfc\r\xb5\xe1J.\x80U\x1c\x7f\xb1(\xe9/3\x7f\xff\xbb\x99s\xf5\xb0\xffx\xf7\xcee\xbe\x80\xa53V\x95_\xd1(\xa73|\xc2-\x97\xdcr%\xefJ\xb4,g\x96-\xee\x00\x98\x94\xca2\x1a6\xf4'@\xa6\xa4\xd5J\bԳ\x1d\xca\xf9\xbb\xdb\xe0\xc6q\x91\xa3\xf6ě\xa3\xf7\xdf\xcf?\xfe0\xff\xdb\x1d\x80d%.\x80\xe8\xb9J(\x96\x9b\xf9\x1e\x05j5\xe7\xea\xceT\x98\x11ٝV\xaeZ@7\x11\xb6\xd5G\x06v\x9f\x98e\xff\xf2\x14\xfc\xa0\xe0\xc6\xfes0\xf1\x137\xd6OV\xc2i&z\xa7\xfaqS(m\x9f;\xca3\xc8]\x98\xe0r\xe7\x04\xd3\xf1\x96;\x00\x93\xa9\n\x17\xe0wT,C\x1a\xabE\xf4\x14f\xc0\xf2\xdc+\x8d\x89\x17ͥE\xbdT\u0095\xb2\xa3\x8f&Ӽ\xb2^)\x1d\xa7`,\xb3\u0380qY\x01\xcc\xc03\x1e\x1eV\xf2E\xab\x9dF\x13x\x05\xf8\xd9(\xf9\xc2l\xb1\x80yX>\xaf\nf\xb0\x9e\rz]\xfb\x89z\xc8\x1e\x89[c5\x97\xbb\xd4\xf9\xaf\xbcDȝ\xf6\xf6$\x993\x04[p\x133v`\x86\x98\xd3\x16\xf3\x93l\xf8y\"f,+\xab!?\xd1\xd6\xc0P\xce,\xa6\xd8Y\xaa\xb2\x12h1\x87\xcd\xd1b#\xc4V\xe9\x92\xd9\x05pi\x7f\xf8\xebiMԪ\x9a\xfb\xadOJ\xf6\xd5\xf2H\xa3\x10\r\aN\xc8B;\xd4I\xdd(\xcb\xc4oa\xc4\x12\x81\xc7h\x7f\xe0$Ѝ\xc7'YY\xc9Lc\x89\xf26\x86x\xb7{\xccML:\x9e\xad4W\x9a\xdb\xe3\x02>~\x7f)\x9bt+@m\xc1\x16\b\x8f,{w\x15\xac\xad\xd2l\x87\xf0\x93ʂ\x8f\x1d\nԵ\x8fm\xc2\x12S('r\xd84\x86\x010V餳U\x98\xcdî\x9anCv\xe0q\xfd3\xbf\xf1]\xc84\xb2\xe4]hPr\xeeWp%\xd3\x17\xe2\xd3\x0e/\xba\f\xb16\xa5ʱU\x1d\xc6\x1cq\x03\x95V\x19\x1as\xe6z\xd2\xf6\x1e\x0f\xcf\xdd\xc0H-a\xc5\xfe\xcfLT\x05\xfb\x18\xc00+\xb0d\x8bz\x87\xaaP~zY\xbd\xfde\xdd\x1b\x86\x93\xd0\xc62k\bӈ\xf5J+\xab2%`\x83\xf6\x80(=\xbcB\xa9\xf6\xa8\t\xa4w\\\x1a`2oiB\xbc\xa0\v5\xe4\xfa\x9e\x1e͆\xc9ڝT\x85:6;\xb92\x8dY\xde`|\xf8\xa2\xb0\x18\x8d\x0e\x84\xf8߬7\a@r\x87]\x90S|\xc4 U\x1d\x020\xafU\x15\xec\xc6\rh\xac4\x1a\xba^ޫ\xd4\x16\x98\x04\xb5\xf9\x193;\x1f\x90^\xa3&2\xcd}Ȕܣ\xb6\xa01S;\xc9\xff\xd3\xd26`\x95?T0\x8b\xc6\xfa\v\xa9%\x13\xb0g\xc2\xe1\xfd@{\xf4\x95\xec\b\x1a\xe9Lp2\xa2\xe77\x98!\x1f\x9f\x95F\xe0r\xab\x16PX[\x99\xc5\xc3Î\xdb&Y\xc8TY:\xc9\xed\xf1\xc1\x1b\x83o\x9cU\xda<\xe4\xb8G\xf1`\xf8n\xc6tVp\x8b\x99u\x1a\x1fX\xc5g^\x10\xe9\x13\x86y\x99\xffI\xd7\xe9\x85\xe9\x1d;\xf2\xc2\xf0\xf9@\x7f\x85y(\xfeӕ`5\xa9 bg\x05\x1a\"\xd5}\xfd\xc7\xfa\x15\x1aN\x82\xa5\x82Q\xba\xa5#\xbd4\xf6!mr\xb9E\x1d\xf6m\xb5*=M\x94y\xa5\xb8\xb4\xfe\x8fLp\x94\x16\x8c۔ܒ\x1b\xfcۡ\xb1d\xba!٥O\xa8`\x83\xe0*\x82\x82|\xb8`%a\xc9J\x14Kf\xf0\x0f\xb6\x15Y\xc5\xcc\xc8\b\x17Y+N\x13\x87\x8b\x83z\xa3\x89&\xd3;a\xda\x0e>\xd6\x15fdSR+m\xe2[^\xc7\x12\xc2\x00\x16\xad\xeck'}\xed\xe9K\x86\x90\xe1\xa2)W\xa3\xef1E\xa8\xe1UF\xf8݄\xba:2\x89~d\x8a\xbf\x0e\xe4\xeb=\x1a+e\xb8U\xfaH\x84Ch\x1c\xba\xc1I\x8bЗ1\x99\xa1\xb8E\xbc\xa5\xdf\t\\\xe6\xa4qlݘ\x00(P\xf5\x8c*\xb9St\xb1\"C\xc0\xca\xd2\n\xf2j\x836-\xa6L\x842.\xa1Kz!Nn\x87\xa2n\x94\x12Ȇ\x1a\xcc\f_KV\x99B\xd9\t\x81W[hV\xbe\x1e+\xa4×\xeb\xd5=\xfdӌ\x93\a\xedy^C<\xdd2ʶ\xd2f\xab\xed\xbc\\\xaf\xc0\xd4\xdb\xc7F\x92N\b\xb6\x11\xb8\x00\xab\xddX\xb0\xd3\x0e\xeb\xb9\xd7|\x8f:53\xbc9~a\xe3\x85a\x1b8\xe3\x93j?\xf4F\x05\t6R.\x95\xb4(S6:\xebU\xf45\x92.\x053I\x9e\a\x9c\xad\xe3\xf5\xa9k\xd2\x10\x84̯\xb0\x05K\xf3\x05!\xe8z9\xbaM\xbc\xcd\xcd\xe0\xc0mq\x93D\xe1\x82^,P\xb4<)O}߃8j{F\x98\x97\xb7\xa5\x97wJ2\n7\xb7H\xb6\xef\x19\xfd\x02\xd9\xfa^\x92\x92n\xc0\xe5)\xe1\x14\xa1\x00\x81\x19\xe6\xe0\xaa\xeby'\xd0\xe1\x1a\xf31ϳ\x9e\xbd\x12\xd3}\xa1O \xc9(2A\x9dt~\xa6\xb4r\xa9\xe4\x96\xef\xc6g\xc7e\xfe\xb9k{V\xb4Qċ\x8e$\x8dS\x80#Nf>Ý5яr\xc3-\xdf9}\n\x8d\xb6\x1cE>J`&\x01hB\x1f\x9e\x89[\xe2H+Y\x13\xbfkH\x8d2\xfb\xe0%1J\x85\xf07\x96\x01\b\xba;\x8a\xdc\xc0\x87\x0f\xa04|\b\xbd\xa2\x0f\xf7a\xb7\xe3\xc2\xcex\xaf\xbc8p!\x9aS\xae\x8a\xa0mIA\x05\x9drS\xa1%\xa9\x83/\x03\x1a\x03UX*>\xbd\xf8V\xc1\x81\xf1(\xadoO7\xf7\t\xba\x1b\xdcR\x0e\xa8\xd1:-)\n\xa3֔\x16\x19OR\xb9D\x18:#\xa9\x89B℔\xc3\xe8饠\xff\x0f\xb1<\x06\x80\x84\x00)\x1b\x9f\xe3Ч\xec?\xae/\xe10Z\xdap\xb8\xe5\x02\xc1\x1c\x8dŲ\xcfm\xa8\x04\x02`\xdc\xc0P\xdb\x10\xbc\xc57\xd6}\x12\r\xafJ\xf3\x1d'\x0f\x90\xedL\x97\x1d\xd6\xe0[\xb7Q<\xb4\xfaؐ\xbc0-|\x1b\x82\xef\x8e\x1c\xe1K8\x9c\xc2\x0f\x93\xb9O`\xda\xf9\xbcƂ\x04\x92L*\xe4\xe5my\x91y\xe8\xe0Dl\xa1\xe1C\xc1\xb3\xa2\xefK|\x8c\xf2\x00\x96\xbd\xa3/\x06\xae`3\x1dTf\xe9\xd2`\xb0f\b\a\x83\xe9\xf8\x0e\r\xa7\xfa\x86Nξ\xbc-/*\x9f|g\xe7\xb2\x02*t\x96k-gNk_\x9a\x86Q\xb5\xbd\xa9\x84bY\x86\x95\xc5\xfc\xf1\xf8\xac\xf2)\xa7\xff\xd4[L\x8c\xc8Kz[\tS\xfbn\x17V\xec\xda\x1a\xa8a\xb7\xed\xc8\xddrM?\r\x89\xf8ތ\xce#\x04\x1fW4\x01\xfdN3\r\xf0J\x0e\xee{\v\xdf\x05Цm>\x14\xd0\xf5\x1c\x1d:\xa2\xd04\x81sfqF\xfbo\v\xfb\xe9\xda14\xe4\xe3^\xe6M\x85\xe4\x98\xccXw\xac\xa9x}\x93\xb5y\tHi\xac#\xd7\xea+P\xc3\x1cp\x8f\x12\x94\x84-を\tO2\x01`\xe7\xa9\xd4Q5<\xfb4M\xa3\xa6\xc1\x98\xec\xdeM[2\xa1\x841\x9a\xfd\x9e\xc6lsگh\x9cHd1\xbfcN\x1b\x8e\f\xed\v\x93\xcci\xcf\xd7\xd7\xcc\x00\x03\x1d\x88Ըq\n\xb4.VR2\xd1\x1d>\x96L\xb5\x11\x06ˡP\xa2vj\xe9\xca\rj\xe2\xd6?ـ\xc4\x03\xe5\xa9Y\xc1\xe4.\x99\t5O\x0e\b\x82\x19[\xbb\xdbI\x0f\x89\xdf|\x86\x92\xc5o4\xddW\xa21l7\x05֟ê\xd0E\xad\xb7\x00\xdbP\xca\xda\xd7\xfaw\xa6\x8e!W!\xb1\x9c\x0e\x17W\x05\x89\xde\x03\xc8՜|Y_\xc0˗5\x1d\xf2e\xfd[yA\xe9\xcaT\x11˜U\x89a\xc1\xa5\xfb%1~\xe02W\x871t\x9c\x11\xb5b\xb6\x98\x10\xf4\x85٢M\x92\x9d\x10~\xcf(\x97\xaf\xb3\xce\r\x12&~\xab\x94\u07b7\xf9\xa6أ5\xa9\x14\x06/\x81\x83S\x9a\x7f\xc6Cb\xb4\t\xb9\x89\xa9\x97:\x8e'\xa6F\x8f\xf5\xf1d褦ಙK\xd2l\xdf\xc3\x13s?\xfa\x00w\x95\x9ek\xfen\x89\xe0mO\xb6\xc37\xff\xbc=B\xb9~o\x88J\x8a\xc8b\t\xc2\xd1\xfe\xb6\x8e\xf1\x94\xe6\xf0Zp\xd3t\x91\x9b\xd28\xe7\xa6\x12\xec\xd8\xca2\x156Z\xdc\x1a\xbe\x0e\x8e\x9d\xe4|\xfb\xb5\xfdUA\xbauv\x1e\x95a\x02\x99\xfd\xbc:\x1dr\xbe\xc5\tgb^s\xbdWO\x17\xd6\xfc\xab\xa7\xe6*\xf2\x1c\xa5\xe5[\x1e\xbd\xc8vŚ\xef\xf0\xa7t9|ٸ\xae\xbe\xec\xfd\xd6\xe4\xa6z\xbbGa\"\x13\xad\x7f\xfa\x92\xca\xf7\xd6\x04\x06\x04A\xfe\rp9|\xf5\xbfo#:\xb3\xf5Cd\b\xfe\xa9\"VIJo|zt}j\xd9\x17\xe8\x8f\xcc*\x93^5\x1a\xf4\x9c\xe7\x11\xed\xbao\x1b\x8f\xb8M\xfb2\xbc\x80\xff\xfe\xff\xee\xd7\x00\x00\x00\xff\xffʖ\x89F\xbb&\x00\x00"), } var CRDs = crds() diff --git a/pkg/apis/velero/v1/backup_repository_types.go b/pkg/apis/velero/v1/backup_repository_types.go index 8e2b3b715..5d56866ce 100644 --- a/pkg/apis/velero/v1/backup_repository_types.go +++ b/pkg/apis/velero/v1/backup_repository_types.go @@ -118,7 +118,7 @@ type BackupRepositoryMaintenanceStatus struct { // +kubebuilder:storageversion // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="Repository Type",type="string",JSONPath=".spec.repositoryType" -// +// +kubebuilder:resource:shortName=br type BackupRepository struct { metav1.TypeMeta `json:",inline"` diff --git a/pkg/apis/velero/v1/backup_types.go b/pkg/apis/velero/v1/backup_types.go index 435e88f30..f6b561ed8 100644 --- a/pkg/apis/velero/v1/backup_types.go +++ b/pkg/apis/velero/v1/backup_types.go @@ -516,6 +516,7 @@ type HookStatus struct { // +kubebuilder:storageversion // +kubebuilder:rbac:groups=velero.io,resources=backups,verbs=create;delete;get;list;patch;update;watch // +kubebuilder:rbac:groups=velero.io,resources=backups/status,verbs=get;update;patch +// +kubebuilder:resource:shortName=bkp // Backup is a Velero resource that represents the capture of Kubernetes // cluster state at a point in time (API objects and associated volume state). diff --git a/pkg/apis/velero/v1/delete_backup_request_types.go b/pkg/apis/velero/v1/delete_backup_request_types.go index 8c7b1fa09..256207f18 100644 --- a/pkg/apis/velero/v1/delete_backup_request_types.go +++ b/pkg/apis/velero/v1/delete_backup_request_types.go @@ -58,6 +58,7 @@ type DeleteBackupRequestStatus struct { // +kubebuilder:storageversion // +kubebuilder:printcolumn:name="BackupName",type="string",JSONPath=".spec.backupName",description="The name of the backup to be deleted" // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="The status of the deletion request" +// +kubebuilder:resource:shortName=dbr // DeleteBackupRequest is a request to delete one or more backups. type DeleteBackupRequest struct { diff --git a/pkg/apis/velero/v1/download_request_types.go b/pkg/apis/velero/v1/download_request_types.go index f23118fe5..37aaab88a 100644 --- a/pkg/apis/velero/v1/download_request_types.go +++ b/pkg/apis/velero/v1/download_request_types.go @@ -92,6 +92,7 @@ type DownloadRequestStatus struct { // +kubebuilder:object:root=true // +kubebuilder:object:generate=true // +kubebuilder:storageversion +// +kubebuilder:resource:shortName=dr // DownloadRequest is a request to download an artifact from backup object storage, such as a backup // log file. diff --git a/pkg/apis/velero/v1/pod_volume_backup_types.go b/pkg/apis/velero/v1/pod_volume_backup_types.go index b246906fb..5ad725df1 100644 --- a/pkg/apis/velero/v1/pod_volume_backup_types.go +++ b/pkg/apis/velero/v1/pod_volume_backup_types.go @@ -145,6 +145,7 @@ type PodVolumeBackupStatus struct { // +kubebuilder:printcolumn:name="Uploader",type="string",JSONPath=".spec.uploaderType",description="The type of the uploader to handle data transfer" // +kubebuilder:object:root=true // +kubebuilder:object:generate=true +// +kubebuilder:resource:shortName=pvb type PodVolumeBackup struct { metav1.TypeMeta `json:",inline"` diff --git a/pkg/apis/velero/v1/pod_volume_restore_type.go b/pkg/apis/velero/v1/pod_volume_restore_type.go index c1d75b71c..96c1a1e4b 100644 --- a/pkg/apis/velero/v1/pod_volume_restore_type.go +++ b/pkg/apis/velero/v1/pod_volume_restore_type.go @@ -133,6 +133,7 @@ type PodVolumeRestoreStatus struct { // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since this PodVolumeRestore was created" // +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.node",description="Name of the node where the PodVolumeRestore is processed" // +kubebuilder:printcolumn:name="Uploader Type",type="string",JSONPath=".spec.uploaderType",description="The type of the uploader to handle data transfer" +// +kubebuilder:resource:shortName=pvr type PodVolumeRestore struct { metav1.TypeMeta `json:",inline"` diff --git a/pkg/apis/velero/v1/restore_types.go b/pkg/apis/velero/v1/restore_types.go index f6e6bf9cf..c01686241 100644 --- a/pkg/apis/velero/v1/restore_types.go +++ b/pkg/apis/velero/v1/restore_types.go @@ -411,6 +411,7 @@ type RestoreProgress struct { // +kubebuilder:storageversion // +kubebuilder:rbac:groups=velero.io,resources=restores,verbs=create;delete;get;list;patch;update;watch // +kubebuilder:rbac:groups=velero.io,resources=restores/status,verbs=get;update;patch +// +kubebuilder:resource:shortName=rst // Restore is a Velero resource that represents the application of // resources from a Velero backup to a target Kubernetes cluster. diff --git a/pkg/apis/velero/v1/schedule_types.go b/pkg/apis/velero/v1/schedule_types.go index 6a5f885ab..c5248a861 100644 --- a/pkg/apis/velero/v1/schedule_types.go +++ b/pkg/apis/velero/v1/schedule_types.go @@ -104,6 +104,7 @@ type ScheduleStatus struct { // +kubebuilder:printcolumn:name="LastBackup",type="date",JSONPath=".status.lastBackup",description="The last time a Backup was run for this schedule" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="Paused",type="boolean",JSONPath=".spec.paused" +// +kubebuilder:resource:shortName=sched // Schedule is a Velero resource that represents a pre-scheduled or // periodic Backup that should be run. diff --git a/pkg/apis/velero/v2alpha1/data_download_types.go b/pkg/apis/velero/v2alpha1/data_download_types.go index 616876563..220bd382b 100644 --- a/pkg/apis/velero/v2alpha1/data_download_types.go +++ b/pkg/apis/velero/v2alpha1/data_download_types.go @@ -152,6 +152,7 @@ type DataDownloadStatus struct { // +kubebuilder:printcolumn:name="Storage Location",type="string",JSONPath=".spec.backupStorageLocation",description="Name of the Backup Storage Location where the backup data is stored" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since this DataDownload was created" // +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.node",description="Name of the node where the DataDownload is processed" +// +kubebuilder:resource:shortName=dd // DataDownload acts as the protocol between data mover plugins and data mover controller for the datamover restore operation type DataDownload struct { diff --git a/pkg/apis/velero/v2alpha1/data_upload_types.go b/pkg/apis/velero/v2alpha1/data_upload_types.go index 39ae349d6..ac57ad89d 100644 --- a/pkg/apis/velero/v2alpha1/data_upload_types.go +++ b/pkg/apis/velero/v2alpha1/data_upload_types.go @@ -197,6 +197,7 @@ type DataUploadStatus struct { // +kubebuilder:printcolumn:name="Storage Location",type="string",JSONPath=".spec.backupStorageLocation",description="Name of the Backup Storage Location where this backup should be stored" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since this DataUpload was created" // +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.node",description="Name of the node where the DataUpload is processed" +// +kubebuilder:resource:shortName=du // DataUpload acts as the protocol between data mover plugins and data mover controller for the datamover backup operation type DataUpload struct { From c24f403a2d5612e91221b0ddb5be6a5dc63e49df Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 15 Jul 2026 10:19:42 -0700 Subject: [PATCH 057/194] Add changelog for PR #10009 Signed-off-by: Shubham Pampattiwar --- changelogs/unreleased/10009-shubham-pampattiwar | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/10009-shubham-pampattiwar diff --git a/changelogs/unreleased/10009-shubham-pampattiwar b/changelogs/unreleased/10009-shubham-pampattiwar new file mode 100644 index 000000000..c67de415f --- /dev/null +++ b/changelogs/unreleased/10009-shubham-pampattiwar @@ -0,0 +1 @@ +Add CRD short names for all Velero custom resources From 251272ae090b2ae1184cfcb38c8667fea7785588 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Fri, 17 Jul 2026 11:44:40 -0700 Subject: [PATCH 058/194] Address review feedback: update short names for Backup, BackupRepository, and DownloadRequest - Backup: bkp -> bak (more common abbreviation) - BackupRepository: br -> repo (avoids conflict with "backup & restore") - DownloadRequest: dr -> dreq (avoids conflict with "disaster recovery") Signed-off-by: Shubham Pampattiwar --- config/crd/v1/bases/velero.io_backuprepositories.yaml | 2 +- config/crd/v1/bases/velero.io_backups.yaml | 2 +- config/crd/v1/bases/velero.io_downloadrequests.yaml | 2 +- config/crd/v1/crds/crds.go | 8 ++++---- pkg/apis/velero/v1/backup_repository_types.go | 2 +- pkg/apis/velero/v1/backup_types.go | 2 +- pkg/apis/velero/v1/download_request_types.go | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/crd/v1/bases/velero.io_backuprepositories.yaml b/config/crd/v1/bases/velero.io_backuprepositories.yaml index ccc553b86..6bf80f104 100644 --- a/config/crd/v1/bases/velero.io_backuprepositories.yaml +++ b/config/crd/v1/bases/velero.io_backuprepositories.yaml @@ -12,7 +12,7 @@ spec: listKind: BackupRepositoryList plural: backuprepositories shortNames: - - br + - repo singular: backuprepository scope: Namespaced versions: diff --git a/config/crd/v1/bases/velero.io_backups.yaml b/config/crd/v1/bases/velero.io_backups.yaml index cb20b5304..68ec68c68 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -12,7 +12,7 @@ spec: listKind: BackupList plural: backups shortNames: - - bkp + - bak singular: backup scope: Namespaced versions: diff --git a/config/crd/v1/bases/velero.io_downloadrequests.yaml b/config/crd/v1/bases/velero.io_downloadrequests.yaml index 500158e5b..9db2e9fb8 100644 --- a/config/crd/v1/bases/velero.io_downloadrequests.yaml +++ b/config/crd/v1/bases/velero.io_downloadrequests.yaml @@ -12,7 +12,7 @@ spec: listKind: DownloadRequestList plural: downloadrequests shortNames: - - dr + - dreq singular: downloadrequest scope: Namespaced versions: diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 43c054c50..5ecc27bcc 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -29,15 +29,15 @@ import ( ) var rawCRDs = [][]byte{ - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8adɡS\xf7\xe7\u074b!%[\x96dk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L8\xf5\x88>(k\x96 \x9c\xc2?\b\r\x7f\x85\xf9\xd3/a\xae\xec\xe2\xf0z\xf6\xa4L\xb9\x84U\fd\xeb5\x06\x1b\xbd\xc4w\xb8SF\x91\xb2fV#\x89R\x90X\xce\x00\x841\x96\x04o\a\xfe\x04\x90\u0590\xb7Z\xa3/*4\xf3\xa7\xb8\xc5mT\xbaD\x9f\x8c\xb7\xae\x0f?\xcc_\xff<\xffi\x06`D\x8dK\xd8\n\xf9\x14\x9dGg\x83\"\xeb\x15\x86\xf9\x015z;Wv\x16\x1cJ\xb6^y\x1b\xdd\x12\xce\aY\xbb\xf1\x9c\xa3~\x9b\f\xad[C\xc7t\xa4U\xa0\xdfF\x8f?\xaa@I\xc4\xe9\xe8\x85\x1e\v$\x1d\x87\xbd\xf5\xf4\xf9쬀\xad\xcf\a\xcaTQ\v?\xd0d\xcfAZ\x87KHzNH,g\x00M\t\x92\x9d\x02DY\xa6\xa2\n\xfd\xe0\x95!\xf4+\xabcmN^\xbe\x06k\x1e\x04\xed\x970o\xcb>\x97\x1eSſ\xa8\x1a\x03\x89\xda%ٶ\x92o*l\xbe\xe9\xc8\xceKA84\xc6%\x9d\x9fc\xfdrtxa\xe5\\!\xe8\x9ce\x8b\x81\xbc2\xd5\xec,|x\x9dK!\xf7X\x8be#k\x1d\x9a7\x0f\x1f\x1e\x7f\xdc\\l\x038o\x1dzRm)\xf3\xea\xf4eg\x17\xa0\xc4 \xbdr\x94\xba\xe6\xef\xe2\xe2\f\x80\x1dd-(\xb9A1\x00\xed\xb1\xad1\x96ML`w@{\x15\xc0\xa3\xf3\x18\xd0\xe4\x96\xe5ma\xc0n\xbf\xa2\xa4y\xcf\xf4\x06=\x9ba\xe4\xa3.\xb9\xaf\x0f\xe8\t╊\ff\xe5\xb2\"|?N\x00\xb7\xbe\x10\x06eJ\x9e\x96\xe6\xb2b'm3r\xfb\xa3)\xc1_\xbe_\xba\vM\xac\x87\xee\nx\xb2N\x89\x91}\x8f\x81\x94\x1c9x\xf5\xea\xbe\x0e`3\x1fJ\xa6\xa3\x9dB?\x91\xf1;\xe6m\xceq\u06007\x9c\x1c\xf8\xf1\x83\xa7\xe7\xd2K\xe6\xfe\xf1\xd2Dw\xe2\xf3F\x9a\xd9L3\x9d2\xb7#\x1dFL:[6\x915z\xa9\x0f\xef\x98\x1f\x9eU\xe5\xb1wu\x16\xe3dד\x19\xa3\x89\x9eH\xafj\xcfb{\x12\x14\xc3=|\x9f\x14\xdaj\xca\xe8}\xbaO\xf3.?\xa3^\xcc\xf8Z\x04\xea\x10\x1b?j'p\xff8\xd4h\x03cc@\xbc\xc1\xd0v\x8b7\x82k\x88R\"\x96\xc3+\x1e\x18\xdfZP~<\x17l\xefe\xcc1N\xfc\x18\x82\xa8\xa6\x92\xfc\x94\xa5\xf2\xeb\xa9Q\x01\xb1\xb5\x91\xae @\xfb\xb1\x1co\xa32\x11\xa9ۋ0\x15\xe7\x03ˌ\xf5E\uf0bd\x15\xc25J\xfb\x8c\xdfFv\xd7(\xca!-\x16\xf0\xd9\xd2\xf8\xd1MV\x93h\xba\xcd4I\xe4=y\xce\xfc\x02\x83\xc6\xe4\xa0\xff\x86Y+\xc2z\xf4\x8a\xbc>+yI[;\x8d\x84\xa7\xff\xbfq\xb1^諾\xd6\t\xb4|\xc0ϣ49W{\xa9-\xd9TbyM\x8fP^\x13\x83\x94\xd7\xcdW\x03\xdc\x1a\xaa\x91J\xdc;ZWK\x91\xe1~^9&3\xf0\x18\xa2\xa6g%\xb0N\xa2-~Y\xf1\xdc~ϋg|\xe6\xf2*`\xd3R\xe3U\x89\xf7B\xe9\xabǓ\xc9\x06\x12\x9e\xee\xeb\xdfͅ\xca\xe9߃w\xbb}\xfb\xbf\xec\xcf\x1b\xef\xc8\xf6Px/\x8e\xd3W\xf7`3\xf0op\xd9\t.\xe4\xe7Dw'nO\x7f\xf9K\xf8\xeb\x9fٿ\x01\x00\x00\xff\xff\x989~\x12\b\x14\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s#)\x92\xef\xfd+\b\xdd\xc3\xecnH\xf6v\xdcG\\\xf8\xad\xc7ݽ\xa3\x98\x99no\xdb\xe3}FU)\x891\x055@\xc9\xd6\xde\xdd\x7f\xbf \x81\xfaPQ*J\x96=\xbd\xbb\xcdK\xb7U\x90\x90\x1fd&I\x02\x8b\xc5\xe2\r-\xd9=(ͤ\xb8\"\xb4d\xf0d@ؿ\xf4\xc5\xc3\x7f\xeb\v&/wo\xdf<0\x91_\x91\xebJ\x1bY|\x01-+\x95\xc1{X3\xc1\f\x93\xe2M\x01\x86\xe6\xd4Ы7\x84P!\xa4\xa1\xf6gm\xff$$\x93\xc2(\xc99\xa8\xc5\x06\xc4\xc5C\xb5\x82U\xc5x\x0e\n\x81\x87\xaew\x7f\xbex\xfb_\x17\xff\xf9\x86\x10A\v\xb8\"+\x9a=T\xa5\xbe\xd8\x01\a%/\x98|\xa3K\xc8,ȍ\x92UyE\x9a\x0f\xae\x89\xef\xce\r\xf5{l\x8d?p\xa6͏\xad\x1f\x7fb\xda\xe0\x87\x92W\x8a\xf2\xba'\xfcMo\xa52\x9f\x1ah\v\xb2zp`4\x13\x9b\x8aS\x15\xea\xbf!Dg\xb2\x84+\x82\xd5K\x9aA\xfe\x86\x10\x8f\x0f6_xTvo\x1d\x84l\v\x05up\t\x91%\x88w7\xcb\xfb\x7f\xbf\xed\xfcLH\x0e:S\xac4H\x95\xff]Կ\x13?~\xc24\xa1\xe4\x1e\xb1'\xca3\x83\x98-5DA\xa9@\x830\x9a\x98-\x90\x8c\x96\xa6R@\xe4\x9a\xfcX\xad@\t0\xa0[\xf02^i\x03\x8ahC\r\x10j\b%\xa5d\xc2\x10&\x88a\x05\x90?\xbc\xbbY\x12\xb9\xfa\x152\xa3\t\x159\xa1ZˌQ\x039\xd9I^\x15\xe0\xda\xfe\xf1\xa2\x86Z*Y\x822,\x10Е\x96\x8c\xb5~=\x86\xab-\x96<\xae\x15ɭ\xb0\x81C˓\x18rOQ\x8b\x9f\xd92ݠ\x8f\xe2g\x7f\xa6\xc2\x0f\xff\xe2\x00\xf4-(\v\xc6\xf2\xbb⹕\xd1\x1d(K\xc0Ln\x04\xfb{\r[\x13#\xb1SN\rhK\x19\x03JPNv\x94W0\xb7D9\x80\\\xd0=Q`\xfb$\x95h\xc1\xc3\x06\xfap\x1c?K\x05\x84\x89\xb5\xbc\"[cJ}uy\xb9a&̼L\x16E%\x98\xd9_\xe2$b\xab\xcaH\xa5/s\xd8\x01\xbf\xd4l\xb3\xa0*\xdb2\x03\x99e\xf3%-\xd9\x02\x11\x118\xfb.\x8a\xfc߂x\xe8N\xb7fo\xc5V\x1b\xc5Ħ\xf5\x01g\xce\x04\xf6\xd8I\xe5\x84сr(6\\\xb0?Y\xd2}\xf9p{\xd7\x16T\xa6=SZ\xf2:\xc4\x1fKM&֠\\\xbb\xb5\x92\x05\xc2\x04\x91;QE9\xe7\f\x84!\xbaZ\x15\xccX1\xf8\xad\x02m\xe7\x80<\x04{\x8dډ\xac\x80Ten\xc5\xf8\xb0\xc2R\x90kZ\x00\xbf\xa6\x1a^\x99W\x96+za\x99\x90ĭ\xb6\xce=\xac\xec\xc8\xdb\xfa\x10T\xe7\x00k\x9db\xb9-!\xebL4ۊ\xadY\xe6\xa6\xd3Z\xaaF\xef8\x1dإP|\xeaےiv+h\xa9\xb7\xd2ܱ\x02de\x0ek\x8c\xc9\x1a2\xefvy\x00%\x8cЏ\x17uV\xa5!\xb7\x93\xf6\x912\x83c\xbe\xbe]\x92{TV\xa15*\xadJ\x13S)a\xa5$\xd2\xd7\x17\xa0\xf9\xfeN\xfe\xa2\x81\xe4\x15\nw\xa6\x00\xe90'+X[IP`\xdb\xdbO\xa0\x94\xa5\x8d\xc6\x01Ȫ\xa7ll\xb9ۂ\xa5-\xad\xb8\xf1\xf3\x84i\xf2\xf6Ϥ`\xa22=Q\x1b\xe4:R\x8a\x1aZ\xc8\x1d\xa8S\x88\xf8\x9e\x1a\xfa\xb3m|@;\v\x94 TK\xbc\x95\xa7\xe3j\x8f\x1fc\xdcve\xb9nAd\x9a\xccfD*2s\xb6y6w\xad+\xc6͂\x89v\x1f\x8f\x8c\xf3\xd0\xcb4\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa8\x9d\xf0\x9eD\x8b\x01X-\xd2\x91\x9ep\xeep\xeeAhKW\x8fH\x1fyQqNW\x1c\xae\x88Q\x15\f\xd0f%%\a*F\x88\xf3\x05\xb4a\xd99H\xe3 E\b\xa3\xfc\x87\x0e\x05\xd0h\xd2\a 4\x02\xda\xd3\xccZg\xce[\x84\xedR%:\xa6RAf\xb5\xf6\x95\xb7\x06\f8Z !\t\x97b\x03\xca\xf5n=\x95 `\n\xac\xc0\xe5\xc4*Z\x05\xdcZ\x13\xb2\xae\xac\x0e\xbe vv\x0f\xca\x00\x13\xda\x00\x8d\b\xe73\xf8\x03O\x19\xafrȯ\x9d\xe3uk\xfd\xc7<\xf8\xd3=\xad\x99§\x0fG!z\xeb\xccY\x86N\xa0\xf7\xf7\x16\xe8\xb7\xc6Ĵ1\xd2\xfb\x12\x9cSmY\xe9\x87\xddXߣ\xfa@\x83\xb1\x8df\x7f\x9a͑\xc3\xdd^\xbb}hB\x15\xd4dI֛P\x94f߯\xcd\f\x14\x11*\x1e\xd5'\x89\xfc\xa4J\xd1\xfd\x007k\xff\xff\x8c\xfc\x1c\x82y\xc0Q\x11\xaa\xbd2O\x0f\xfb\xfdg\xe6\xeay\xf8\xa8q\x1dL\x99\xb0\xfc\xb3K\xd2\x0e\xfb\xb4[\xbfY\xb2\ti\"\xf0\x98p\xf0piv\x84[\xbf\x13\xb1\xce\"\xf3CB^˖\x17\xde\x7fHJm\xa5|\x18\xa3\xce\x0f\xb6N\xb3(\"\x19\xc6[\xc8\n\xb6tǤ\xf2\xa87\xa6\x16\x9e \xabLt\xd6SCr\xb6^\x83\xb2p\xca-ՠ\xdd2y\x98 \xc3\xee;i\xa9\x91\xe8\xc7\x03<\x1aFZ6!\xe6CC\xb7~ġ\x95\f\xc5\x0eԺ\xd7h\x8cs\xb6cyE9\xdae*2\x87\x0f\xad\xc7\x15\xd32G\x98\xdc\x1bsT2]q\x0eA@\xca2\xa9\xb3R\x92\x02\xac\xcf[\xd85A\xbf\xea0\xe6+j}\x159\x84=Af\xa9\x8a\x83\xf6]\xe5\xe8F6:c\xde0\x05\x03\x11\x84\xd3\x15p\xa2\x81Cf\xa4\x8aSd\x8cϮ\xa4(\xc1\x01BF4_w\xa5\xd1 p\x04$\xc1%ܖe[\xe7\xeaY!B8$\x97`\x1d>ChY\xf2\x88\xb9h\xcaQ\xe6\xfbN\x8e\xcd\xf5\xa6\x8c\xcc\xfaCx\xb1\xf9ߔ\x04\x9dٔ(i\x9b\xf9եl-\x0e\xf15mS\xfe9\t\x1b4\xff\tB{d\xf6\x13\x8c\n%\xcb\xf4\xa0\xdcZ\xaa2\xd0\x17֝BOgN\x98\t\xbf\x8ë́\x8e\xcf\xd5\v\x96u\x88\xf0u\xf3f\xba\xd0'\xb2&eN\xbc\x10c\xea.\xfe\x01\xf9\x82&\xe3\xd6[\x8cd\x9e\xfc\xd4n5'l]\x13=\x9f\x935\xe3\x06\xd4\x01\xf5OR\xf5\x813\xe7 F\x8a\xd5#\x18\xbe7\xd9\xf6Óu\xc1t\xb3\x87\x95H\x97\xc3\xc6Α\r\xde~\xd7<\x8f\xc0%\x18\xc6f\n\n\f\x8f㊩\xfd\v\xbaV\xef>\xbd\x8f\xaf\xaf\xda%A\xf2z\x88\x8cL:W\xde\x1d`\xd4\x1e\x9fw\xe1\xc3\x17\xf4\x81\xea\x05\x90\xdb\n\x99\x13J\x1e`\xef\\\x17*\x88\xe5\x0f\r\x95\x13\xbaW\x80{2(g\x0f\xb0G0\xf1M\x96~I\x95\x06W\x1e`\x9fR퀆vLL\xfb\xcd#K'\xfb\x03\x12\x02c\xeb\xa9b\xe0\x8a\x9f\n\x91-\x8dxI\xd4%\xa1\x04ڟ\x80f\x92\xa8\xb4\xfbh\xefR\xa2\x04|\xa7\x1d/\xed\x8cٲ\x12\xd5*F\x1c\xe4:\x99\xa1\xae\xdcS\xce\xf2\xba#7G\x96bN>Ic\xff\xf9\xf0Ĵ\xdf\xc8|/A\x7f\x92\x06\x7fy\x11\x8a\xba\x81\xbf$=]\x0f8ф\xd3\xf2\x96`\xed\xad8gӬ\xb4մg\x9a,\x85]\xae8\x92$v\x85\xbb\xae\xae;\xd7QQi\xdcE\x13R,\\\xd8&֓\xa7\xb7T\x1dr?\xbbS\xdf\xe1\x9d5\x16\xee\x8b\xdb\xfb\xe54\x83\x86m\x92\x88\x96RG6\xf2\a\x862\"\xbc7R\x1b\x17/\xeb\xf8\xccр\x9a\fA4B\xd7.\x7fI\xaa\x90lb\x95\xf2X\xe8\xb7]\ueda0\xc1\xefW\xf8\xc0\x9c\x03jWv\xb3f~;m?s\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xecE$+\xa3\x8d{\x1ds\xa4n\x95\xe4R2\x8e\x87@CIwy-!&\xae\x17><\xb5\x02\xa2v\xeeۿ\xc7dl\xea\xb8\b&\x13\x16\x05=LSJ\x1a\xe2\xb5k\x19f\x83\a\xe4\x16\x1fjS\xa1&H\xb5\xe5\xb5\x00~\r\x8eB\xc1\xc4\x12; o_\xc0\xb1\xf0:4\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fn*\x97\x12\xb7\n\x14t\x98\u05cf\xaa\xa3\x1f*\xa4i\x05$&\xb8\x9b\xa5̿\xd3d͔6\xed!\xe8\x814\x95(\x98\x89\v/\xf1A\xa9\x93\xd6]\x9f]\xcbV\xb8k+\x1fCz\x96#L\"渿\x04\x84\xad\t3\x04D&+\x81\x01\x1c;\x8f\xb1\vG\\\xa7aY\xea$I\x9b\xfd\xb6\x80\xa8\x8a4\x02,PR\x988\x1a\xe9iW\xffH\x19\x7f\t\xb6\x99\xa1,\xb6X9mN\x84\x14\xb7vB^A\x9fXQ\x15\x84\x16\x96Gh\xccY\x01]\xa67\x89o\xb6\x05\x9a\t#\xed\x8c)9\x18\xf0\xc9k\x89cȤ\xd0,\x87ڸzA\x90\x82P\xb2\xa6\x8cW*Q\x03N\"\uf525\x88\xd7\x04\xe7[c\xa4u\xbe@R$Ds\x13}\xc5\xe3ڸT\xe9\x1eߘ\x9b\xa5`\xba\x97U*&1-\xf0̎\x96O\xa4\xa4b\xff\xcd\xd3J\x1d\xea7O\xebX\xf9\xe6i\x8d\x94o\x9e\xd67O+\xa5\xe67O뛧\xd5.\xff\x12\x9e\xd6؈\xdcy\xbe\x81\x8f\xa3\xa3Hت>6\xc4#\xf0}r\x85\xcf\x01\x7fV.\xe62\x0e*\x92\xf8?\x90\xd6\x1dSZ\x8d\xf1\xa8\x933\xed\xac\t2\xef\x8e\x17\x8d\xb8\x92\xcfȺ\x0f\x9d\x9e/\xeb~y\x14♲\xee\xfd\xb0\xc7}\xec\x93r\xee\x03Q\xa6eg\xcf}\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x19\xe9\xff\x95\x13s{Ycg\x94\x8f\x17\xcf\xe2O\x96\x91(Kg\x7f\x9a}}\xe4?\x0f\xc1\aIܧ\x9d?\xdf\x1c\x81jW\xa0\xed\xb4\xb0n\x16\xde\xd7)\xc6g\x91\xdb\xd4L\xfc\x9a\x88\x11X]\x91<\xa0\xe2ת\v\f\x14\x9fKo\x91\x9eqRu\x19\x81\x93tV\x95\xea\xbdȶJ\nYi\x1f\x95\xb0\xb0\xdee\xee@{\x00\x19\x13\xd6\xe8\f\xff\x0f\xb2\x95U$\x13\xfc\b\xf9F2\x02Ǒ\xef$\a\xfaMh0t\xf7\xf6\xa2\xfb\xc5H\x9f*H\x1e\x99\xd9F\x00=nA\xe0\x0e\xbbش\x0f\x00\x84\xfb\b\xfc\xc1\xfcC\x01\x8b\x00\x92\x8a\bƝ\xe4շ\x19\xb4\xe5\x8e|.]\xeci\xb2\xdfq<\xa6\x92\x96Lxr\na7Ep\xc0/\x9d\xba\xdb}\x96#\x13\xbfKj\xe0\xf4\x84\xc0\x94\x88\xd8H\xf2\xdf\t)\x7f\x89\xb9\xc5\xcfޞOI꛲b~\xb1\x04\xbe\xf3\xa7\xed%\xd1g\x7f\xa9\xa5\xf6\xe2\xc0ӧ\x9a<\x02\xe7\x84\xc6\xe6U\x0f\xf3\xcc\xddĔ\xc9\x05X{dg\xa7\xbf\x18\xc4_\xdf4w⎧kѪ\x15\xb1\x10\x13\x15÷\xc8\f\x1a\x8e\x14}\xd3\xf3`\x9d\x1f\x8e\xbf\xfdV\x81\xda\x13\xbcǦ\xf6s\x9aC`~bj\xbb\x10\v\xaa«\xad\xa1\xf8y\xcf\xe9o\xa62y'\x9c\xd5=\x1c\x0f\xb6\xb1:\xa2Y\xd4X\xc5g\xd7+\xd1>\x06\x9a\vY\xb7\x8e4\x1bs\x90SOK\xbd\xec\x12g\xfa\"gԫH\xf7\xfc~\xa7SP\xa7\x9c~JK\x00\x18=\xed\xf4RK\x9e\xb1EO\xb2\x9f\x97v\x9ai\xdaf\xe1\v\x9e^z\x89SK\x89\x94J9\xa54\x8dN\xafp*\xe9UO#\xbd\xd6)\xa4\xe4\xd3GI).ɻ\xc0\xa9)*'\x1e\xa7\x19\xdf\xe3=~\x9a(\xe1\x14Q\xc2\xee\xef8\x92'\xa0\x97pJh\xda\xe9\xa0\x04\x9e\xa5N\xc5W<\x05\xf4\x8a\xa7\x7f^\xfb\xd4ψd\x8d|\x9ev\xba\xe7\xe4-\v\xa9rPG\xb7}R\xa5\xf0\xa8\xfc\xa5\xacm\xba\x039\xd8\xef\b\xb7\xfe\xd9Z\x1d\x7f\x19̓\xbfh\x14\xaf\x94\x1dھ\xb4\x92\xd6\xf26:{Q\x8d\xfb\xd3u&\xfd=\xb3n\xbbJCI\x15\xde]\xbcڻt\x96\xa8i\xfe@\xb3\xed\x01\xf4-\xd5d-UA\r\x99\xd5\x1b\x80\x97\x0e\xb8\xfd{vA\xc8GY\xe7D\xb4\xef\xe5Ѭ(\xf9ޮPȬ\xdd\xe04\t\x88J[\xe8\xedFr\x96E|\xb7\xe8\xddL\xaer\xef\xb2\f\xbc1*k\xa7\f\x94\xb6b\xdcuC7\xaf{\x05\xe6Zr.\x1f'\xae\xfdi\xc9\xfe\x82wz?#:\xf4\xeef\x890\x82x\xe0%\xe1urV\x8d\xcd\n\xacYn\xf0\x1c\x9a\xfb\xcbu\ab7ϱ}9.\xe4\xee\x1e\xe4\xe0\x16xՙI\xab]n\x96n\x1cC\xbdX\x99\xa1bO$fԘ-S\xf9\xa2\xa4\xca\xec]\xa2Ƽ3\x86`K\x8fEw\x06\xadG\xffn\xe7(yÕθC\xb9/\xbb\x9b\xbe\x87\xb4;e\x1cç\x17G\xcf-\x9eq\x1c\xc3n\xc9\x02)\x15\xf99\x9a\xf9u\xb6\xa8\x99\xf67\x13\xff,w\xf0>\x1a=\xeb\x90\xe7\xf6\xa0z$=+@t\x97\xee\x0ef\xa9\xae\x00/\xe4\xed\x7fzF\xbeU\xe8\xdaߩzJ\xa0\xec\xb6\v\"\x82_\xb8a6t\x16\xd3Ox\x01\xfc\x9e\xdc\xdc\xe3\x1a\xadVm~\x8a\xfa5Z\b\x95\x85\xcd\xe0\b\x1c\xdf\xe0\xfb\xf3\xa7\xa6i#\x15\xdd\xc0O\xd2ݱ=\xc6\xf6n\xed\xce\xdd\xeb\xde\xeb\t\xf9\xa3a\xd2\xc4.\xe0\xf5\xb7}\x1f\x00kr\xbe{\x97\x1a\xdbQN\xbc\xa6\xd9\x18~\n\xdf\xef\xee~rX\x19V\xc0\xc5\xfbʥ;X\x9d\xa8\xc1\x928`\xeb \xad\xec\x7f\xb7\xf2\x11/\xff\x8d\xc71Û\t\r2\n0\xd9\x1cS\x10'\xa1T\x95\\\xd2\x1cԵ\x14k\xb6\x19\xc1\xee\x97N\xe5\x033\x9b\xe1\x8f\x1e\xb9\xdaF\x05\xf8g\xceA\xb0>\x0f\xe7\xc0?2\x0e\xda\r+A\x01\xdf\xf4[\xd5\xfa\xb8*V·[ۏu\a\x036Ρ\x85\xa1\xe8\x12\x94\xf5\xa2\\к\xd2AV\x87\x11o8\u0084\x81\r\xf4W\x81G4\xb0\xbbU\x1a\xcdgP'\xb8\x96\xf91\x16\xdf\xea \x7f?\xdc\U000804ed\x90W\xec\xc6=\xe7\x84\xdc\xdc_kR\x89\x1c\xc3\xc5\xf7\x7f\xb9\x9d$u\xbb\xce\xcd\xf5a\xb6\x8e)\xd5\xfbx\xab\x96s\xdc\xd2\x17\xce;\x96\xeb\b\x02CpZ\xef\x80<2\xe3/\xee:\xefM\xabCK\x9e\xa1\x17\x0e\xf0J\xff\xf17\x0e\xdc\xcd\xff\xfee\x14?\x1d+\x85פ\xfaW\x01\xf0Zѓ\x9e9X\xd5\t[u\xf2\x97~g\f\x14\xa5\x89\xf9\x1a\xe3\xea\xf0\xfbc\x00k?M\x1a\xca[\xb3\x92\x86\n1O[\xefEv,\xb1\xcck\xa3#\xdc<6\x1fc\x04\xb8\xf6\xe7!\xceF\x80\x1a\xe0\x10\x01t\x95e\xa0\xf5\xba\xe2|_\x1f\xc7\xf8J\xa8\xf1\x912~>R8h\x83\x82`\xd1;\ni\x14a\x9f\xee\r\"\x0f3=\x1cU\x9aF\n\xcf\x05\x9f\r\xa9\r-Nz\xb0\xe1\xba\x0f\x06\x9f\xecQy+\xa9\x92\xd6c\xa7\xbaa\x7f̸4\xe0\\K\\dYh\x90\x13\u0601 \xd6:;\x12\x87ר&B\xf1'\\\x9d\x85\v\xf6.\x84B\xa2\x0f\x13\x11\x1f\xed\xd0\xf8\x00\xcew\xba\x86\x89\xb9\xa2\xf8\x9eI\x9f\b}\xe7\xd7E+\xae\xac\xf7\x0f\v\v\xe24\xaf5\xaa\x9b3ͺv\xe1yJ\xee\xfav9\x04\xee\x14\x15\xd7\x7f\xee\xe5\x99Ӹ\x8f\xee\xb3TZ\x1f\xddI\n-\x02\xb1\x96\xf1\xf3\xe3\x8eS\xfd\xb4Kݱ\xa5s8\xb2p\x86\x8er\xee\x0f:\x16\xa05݄\xdb\xdc\x1f\xed\xd2c\x03\x02\\x\xcem\x9eD\x806\xa7\xe2\xbaw\x99\xbb)C3SQ\xdfAH\xf0m\xd5\xfaN\x13.cP\xf1A\x17\x16^\n\vk\xb2\x89\x84z*\x99JY\xc3}\xa8+Zڠ'\x8c\xdci\xdev\x03\xce6̮u,\xe76T\xad\xe8\x06\x16\x99\xe4\x1cP[\xf7\xc7\xf5\x92sݟ=\xfc\x02T\x8f\xa2\xf6\xb1]\xd7\xef\x00:n\xbb\x8do\xea\xd2\xdd\xf1\xf5.\xc3\x144\x0f\xe9\xf5\x06$\xb1\xe3I\x8e\xb2\xa3B\xf4\x95\xb9\xfeH\xdbuì\xf3j\xd9\xc7y\xfd#ss\x1f\x17\x88\xcbcA\x7f\x95jN\n&\xec?T\xe4n\x03/4\x9e4\xfe\xad\x94\x0f\xb7\x11'\xb67\xf8\x1f\xea\x8a\xcdV\a\x13n\xd8x`t%+\xbf\xfb^;\xb4\xf1m\x15\xbc\x99\xff\xcc\xcbM\x84y\xc4\x1e\xf4\xd0\x19\x8c\xe8\xfeЁ4j\n\\\xcf\x03\xb0n\xc3Kf\x9c\xef燐\x0f^Ml`\xb7^.\xf0n@s\x1f\xc1@GaG*\n\xa4\xbe\xf8\xa2\xad\xd0OY\xf5z2\x0f9\x93=\x1a\xff\xd0\xd4\x1e\xa2\xa3\x1bf\xcb\xdd\x1b@\xb0\xe3\x04\x9ew\xc1\x8e\xcfT\x8c\b\xff\x8d\xadS\xdf]\xd0Z\xb8\x85,\xb1\xc1(]\xfc\xec\xfb\x82|\x82\xfevł\xfc\xb5\x82*B\x83Ex\x18\xee\xd6P\xd5\x0f\xf9\xbac\xf0\x90cF\a\xce\xc6H\x95\xa5\xb8Qr\xa3@\xf7\x85uA\xfeF\x99ab\xf3Q\xaa\x1b^m\x98\xf8<|\xe4\xe7X\xe5\x1b\xaa\f\xb3\xc2\xee\xc6\x13\x1b(\x13\x94\xb3\xbf\xc7\xf4Z\xfb\xe38\xa0\xeb\xc1\x05ւ$\fc\xe8\xc3{\xb0>\xee`\\ \xaaBKO\xd7S\xfc\x95\xc0\x931\x9dZ\xfb\x12\x8d/\x12\xba\xbd \x9fdT1\xf8t(օi]2\xd0f\x01\xeb\xb5T\xc6\xedV/\x16\x84\xadC\xf0\xc1\xea\x1c\x8c\x9b\xb9\xb7*\t\x8bm3\u05c9&\x8d\xf9\u00a0\xb7B+\x8cW\xd9\x17t\xefv\xa6h\x96U\xd6úԆ\xf2\x88\x83\xf3,ŏQ\x1e;\xf9 \xff\xe5Y;y\xcb6\xa0~\xd0\x11\xfbq$\xc5\xcb4\x9c\xd7\xc7-\x8a ȣb\xc6X\x9fJ\x1eI%\xf0\xa42ַ\xe2\x9chKꓢ\x8fĩ\xd1\xe5pJN\x1a\xcaw5\x94!\xf5\xec\xb1Ɨ\x19WH\x1bb\xfd^\xcc>\xf2\xb5,\x9b\xb3-\x15\x9b\xc1\x1b\n\xb6JV\x9bm\x90\xe4\x01g\x9a\xe4\x15`\xb0\x16U\x8a\x0e\x0f\v\x9bJ\x89V*\xc1\x91c\xdf$\b\x03\x0e\x97f\x0f\xa4*\xe7\xfe\xe1^\xffb\xf3\xa5\x7f\x03e\xb1V\xb2X\xf8~1\x96:\xf7;\xf9\x8aI빘m\x94\xea\xc4y\xed\xfe\x99\x01\x94\x84\xb2\x04A\xa8\xf6='\xdc\x14u\xb2\x99\xfa͚\x86\x1b\xa9Y\x82\xb7\x1f\xe5\xf8_\xdb\x00\x02\xc3\xcb\xf0w\x97\x19~\x05\x83}\xc6\xf0\xf8\xec\x8f\xe0Î\n\xe3\x96\x13\xb5\x89\x9c9#6\x9b\xb4\x90\xd1ְ=+Hsہ0\x12\x9f\xc1\xee\xe2,\xba\xf5\xe9\x1a\xee\"\xb0k\xff\xfcj\rxN4\x13\xe1\xe1k\x97\xfa\xe1\xa4?\xba\x13(\xf0\xa1J\xa9\xe2٘\xc7\x03.]\x84^7ֲ\xab=\x89\x0f'/\xc5\xef\x0f`\x1c\x1c\xea\xc6wI\xeb*a\xf9\xfc\a\x16\xdb\x0f\xc04\xde̢\xf2\xc7\xdf\xfd\xb0\xf6.i\xa9\x17\xa7ȱ\x95\x1f.ꆗp\xddwHo8\xd8٦\x01\xba\x8b\xcaIsnw\xc6h\xda9Ci\xe1\x89\xf7\xf3Ēvg\f\xa2\xbdX\x04\xed\xbc(?R| \xfa\xa4Y\xfb7\xdf6\x12B\xf3`\xcf\x1dDk\xc5\xd0\xc2\xc0_5\x8a\x16\xb5\xb9\xbd\x1fQO\xe7-m\xe1{\xf2\xbf\xfc\x7f\x00\x00\x00\xff\xff!\xd0\x1d\xb3҂\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8aTɡS\xf7\xe7\u074b!%[\x96hk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L4\xea\x11\x9dW\xd6,A4\n\xff 4\xfc\xe5\xe7O\xbf\xf8\xb9\xb2\x8b\xc3\xebٓ2\xe5\x12V\xc1\x93\xad\xd7\xe8mp\x12\xdf\xe1N\x19EʚY\x8d$JAb9\x03\x10\xc6X\x12\xbc\xed\xf9\x13@ZC\xcej\x8d\xae\xa8\xd0̟\xc2\x16\xb7A\xe9\x12]4\u07b9>\xfc0\x7f\xfd\xf3\xfc\xa7\x19\x80\x115.a+\xe4Sh\x1c6\xd6+\xb2N\xa1\x9f\x1fP\xa3\xb3seg\xbeA\xc9\xd6+gC\xb3\x84\xf3A\xd2n=\xa7\xa8\xdfFC\xeb\xce\xd01\x1ei\xe5\xe9\xb7\xec\xf1G\xe5)\x8a4:8\xa1s\x81\xc4c\xbf\xb7\x8e>\x9f\x9d\x15\xc0\"\xe9H\x99*h\xe1F\xba\xec\xdbK\xdb\xe0\x12\xa2f#$\x963\x80\xb6\b\xd1R\x01\xa2,cY\x85~p\xca\x10\xba\x95ա6'?_\xbd5\x0f\x82\xf6K\x98w\x85\x9fK\x87\xb1\xe6_T\x8d\x9eD\xddDٮ\x96o*l\xbf\xe9\xc8\xceKA86\xc6E\x9d\x9fc\xfdrl\xf0\xc2ʹF\xd0;K\x16=9e\xaa\xd9Y\xf8\xf0:\x95B\xee\xb1\x16\xcbV\xd66h\xde<|x\xfcqs\xb1\r\xd08۠#\xd5\x153\xad^g\xf6v\x01J\xf4ҩ\x86b\xdf\xfc]\\\x9c\x01\xb0\x83\xa4\x05%\xb7(z\xa0=v5Ʋ\x8d\t\xec\x0eh\xaf<\xc3\xe6УIM\xcb\xdb\u0080\xdd~EI\xf3\x81\xe9\r:6\xc3\xd8\a]rg\x1f\xd0\x118\x94\xb62\xeaϓm\x0fd\xa3S-\b=AD\xd1\b\r\a\xa1\x03~\x0f\u0094\x03˵8\x82C\xf6\t\xc1\xf4\xecE\x05?\x8c\xe3\x93u\b\xca\xec\xec\x12\xf6D\x8d_.\x16\x95\xa2n^\xa5\xad\xeb`\x14\x1d\x17q\xf4\xd46\x90u~Q\xe2\x01\xf5«\xaa\x10N\xee\x15\xa1\xa4\xe0p!\x1aU\xc4DL\x9c\xd9y]~\xe7\xda\t\xf7\x17nG@\xa7\x15\x87\xec\x0exx\xea@y\x10\xad\xa9\x94\xe2\x19\x05\xde\xe2ҭ\x7f\xdd|\x81.\x92\x84T\x02\xe5,:\xaaK\x87\x0fWS\x99\x1d\xba\xa4\xb7s\xb6\x8e6є\x8dU\x86\xe2\x87\xd4\n\r\x81\x0f\xdbZ\x11\xb7\xc1\xef\x01=1tC\xb3\xab\xc8i\xb0E\b\r\x8fN9\x14\xf8``%j\xd4+\xe1\xf1?ƊQ\xf1\x05\x83\xf0,\xb4\xfaL=\x14N\xe5\xed\x1dt,{\x05\xda!sn\x1a\x94\x8c,\x17\x97U\xd5N\xc94S;\xeb@\x8c\xe4/+\x95\xa7\x00^\x89D7d\x9d\xa8\xf0\xa3M6\x87BSm\xc7\xebm\xceP\x171\xd3V\xe2\x04\xcc\vf\f\xd2^P\x8f\fH(s\xe2\x94l\x927\x90\x89\xe8\bf\n#\x8c\xc4\xf7\xb1\x1f\x8d\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xad+\xc6͒\x89v\x1f\x8f\x8c\xf3\xd0\xcb<\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa0\x9d2\x9dD\x8b\x01X-\xd2<\xee\xc0\xec@\x91R֮\xc0\x86q \xfa\xa0\r\x14\x9e0az\xf5\xf8DzB\xa3¹\a\xa1-]=\"}\xe4E\xc59]s\xb8\"FU0@\x9b\xb5\x94\x1c\xa8\x98 \xcegІe\xe7 \x8d\x83\x14!\x8c\xf2\x1f:\x14@o\x82>\x00\xa1\x11Оf\xd6m\xe1\xbcE\xd8.U\xa2c*\x15dv:\xbb\xf2\xd3$\x03\x8eS\xb3\x90\x84K\xb1\x05\xe5z\xb7V/\b\x98\x02+p9\xb13\x90\x02n\xa7Y\xb2\xa9\xec\xe4tI\xacv\x0f\xca\x00\x13\xda\x00\x8d\b\xe7\x13\xf8\x03_\xacu\x86\xfc\xday\xa4\xb7ֱ\xce\xc3B\xa37\x9d\xa4\xf0\xe9\xfd(D\xef\xb6p\x96\xa1w\xec\x1d\xe1%:\xf411m\xbc\x17;5\xe1jò\xd2\x0f\xbbqKF\xed\x81\x06c\x1b]\xfc\xe9b\x81\x1c\xee\xf6\xda\xedC\x13\xaa\xa0&K\xb2݄\xa24\x87~mf\xa0\x88PqԞ$\xf2\x93*E\x0f\x03ܬ\x17Fg\xe4\xe7\x10\xcc#\x8e\x8aP\xed\x85yz\xdc\xef?3W\xcf\xc3G\x8d\x01\x02ʄ\xe5\x9f]\xabwا\xdd\xc2֒MH\x13\x81\xe7\xfc:\xc8q\xcd:\u00ad߉Xg\x91\xf9!!\xafe\xcb\v\xef?$\xa5vR>LQ\xe7\a[\xa7Y-\x92\f\x03Qd\r;\xbagRyԛ\xa9\x16\xbe@V\x99\xa8\xd6SCr\xb6ـ\xb2p\xca\x1dՠ]\xfc`\x98 \xc3\xeb\x1a\xd22#яGx4\x8c\xb4lḂ\x86n\xfd\x88\xe3Y2\x14;P\xeb^\xe3d\x9c\xb3=\xcb+\xcaq^\xa6\"s\xf8\xd0z\\1+3\xc2\xe4ޘ\xa3\x92\xe9\x8as\b\x02R\x96I\x9d%\xa4\x14`}\xde®\t\xfaU\x871_S\xeb\xab\xc8!\xec\t2KU\x1c\xb4\xef*G7\xb2\xb1\x19\x8b\x86)\x18\xa1!\x9c\xae\x81\x13\r\x1c2#U\x9c\"S|v%\xc5\b\x0e\x102b\xf9\xba+\x8d\x06\x81\x11\x90\x04\x97p;\x96휫g\x85\b\xe1\x90\\\x82u\xf8\f\xa1e\xc9#\xd3ESF\x99\xef;\x19\xd3\xf5\xa6Lh\xfd1\xbc\x98\xfe7%\xc1f6%J\xdaF\xbf\xba\x94\xad\xc5!\xbe\xa6m\xca?'a\x83\xe5?AhG\xb4\x9f`\xb8,Y\xa6\a\xe5\xd6R\x95\x81\xbe\xb4\xee\x14z:\v\xc2L\xf8uJ\x13:>W/\x8a\xd8!\xc2\xd7͛\xf9B\x9fȚ\x14\x9dx&\xc6\xd4]\xfc\x03\xf2\x05\xa7\x8c[?c$\xf3\xe4\xa7v\xab\x05a\x9b\x9a\xe8\xf9\x82l\x187\xa0\x8e\xa8\x7f\x92\xa9\x0f\x9c9\a1Rf=\x82\xfb\x1a&۽\xffb]0\xddl\xee%\xd2帱sd\x83\xb7ߝ\x9e'\xe0\x12\x8c\xef3\x17m\u0557\xb8bj\xff\x82\xae\xd5ۏ\xef\xe2\xeb\xabvI\x90\xbc\x1e\"\x13J\xe7\xca\xdb#\x8c\xda\xe3\xf3.|\xf8\x82>P\xbd\x00r\xb1\xea\x05\xa1\xe4\x01\x0e\xceu\xa1\x82X\xfe\xd0P9\xa1{\x05\xb8Y\x85r\xf6\x00\a\x04\x13\xdf}\xea\x97Tip\xe5\x01\x0e)Վhh\xc7Ĵ\xdfU\xb3t\xb2? !p\xd3!U\f\\\xf1\xaa\x10\xd9뉗D[\x12J\xa0\xfd\th&\x89J\xbb\x8f\xf6\xf6-J\xc0w\xda\xf1\xd2j̎\x95hV1\xe2 7\xc9\fu\xe5\x9er\x96\xd7\x1d9\x1dY\x89\x05\xf9(\x8d\xfd\xe7\xfd\x17\xa6\xfd\x0e\xef;\t\xfa\xa34\xf8˳P\xd4\r\xfc9\xe9\x19v|,B\xce\xca[\x82\xb5\xf7(ݜf\xa5\xad\xa6=\xd3d%\xecrő$\xb1+\u070evݹ\x8e\x8aJ\xe3\xf6\xa2\x90b\xe9\xc26\xb1\x9e<\xbd\xa5\xea\x90\xfbɝ\xfa\x0e\xef\xecdᾸMqN3\xc8\xc3v\r\xee\xd6R\x03[\x96%\xf6W\x80\xda\x02)\xad\tO\x93\x88D\xc3걙'>i\xb3w\xbb|Y>\xd4\xc9\x0fK;\xe5,=\x04#\x8b\x04\x1ax\u06ddO㳴:\x9bP+H\xc2dՁ\xcd\xdc\xe1\xaa)Dy\x029p\x16G\x17g\x92\xbb4\xcf15\x88\xf2\x9b\x193\xca\fY\x98k\x1aZcwSpAq\xab\xe5\x7f\xecL\x8b\xda\xf4\x7f\xa4\xa4L\xe9K\xf2\x16s}8t\xbe\xf9\xa0Y\vLB\x97\x98\xabc\xe5gO\xb9\x9d\xfb\xad\x01\x17\x04\xb8\xf3\x04\xe4\xa6\xe7\x17-\xc8\xe3Nj7mכ8\x17\x0fpp;\x86\x93]\xb6\x8d\xcc\xc5J\\8\x1f\xa2g0j\x87C\n~ \x17\xf8\xed\xe2)\xaeT\xa2\xa4&V\xeb\x88hA\xcb4\t\xc5\\\xabTG\xdd.X\x83\x13b\x1b\xd69D\xd6\xc9\x1e\xc36IDK\xa9#\x1b\xf9\x03C\x99\x10\xde\x1b\xa9\x8d\x8b\x97u|\xe6h@M\x86 \x1a\xa1\x1b\x97\xd8%U\xc8±Fy*\xf4\xdb.w;\xd0\xe0\xf7+|`\xce\x01\xb5+\xbb\x8bF\xbf\x9d\xb5\xbfp\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xe6\x8bH\x96H\x1b\xf7:\xe6H\xdd*\xc9媌\x87@CIwy-!f\xae\x17\xde\x7fi\x05D\xad\xeeۿ\xa7dl\xee\xb8\bfY\x16\x05=\xce\xdfJ\x1a\xe2\xb5k\x19\xb4\xc1\x03r\x8b\x0f\xb5\xad\xd0\x12\xa4\xce\xe5\xb5\x00~\r\x8eB\xc1\xc4\n; o\x9e\xc1\xb1\xf064\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fN\x95K\x89[\x05\n:\xcc\xebG\xd5\xd1\x0f\x15Ҵ\x02\x123\xdc\xcdR\xe6\xdfi\xb2aJ\x9b\xf6\x10\xf4@\x9aJ\x14\xcc̅\x97x\xaf\xd4I\xeb\xaeO\xae\xe5Q\x02\x99\xcf[s\x84I\xc4\x1c\xf7\x97\x80\xb0\ra\x86\x80\xc8d%0\x80c\xf5\x18\xbbp\xc4u\x16\x96\xa5*I\x9a\xf6\x93\xc1\x1c\xb4XY\xa2\xa401\x1a\xe9iW\xff@Y?Q-Vf\xb2\xcd\fe\xb1\xc5\xcai:\x11R\xdcڙ\x8a\x05\xfd\u008a\xaa \xb4\xb0<\xc2ɜ\x15\xd0ez\x93\xf8f[\xe04a\xa4\u0558\x92\x83\x01\x9f\xbc\x968\x86L\n\xcdr\xa8'W/\bR\x10J6\x94\xf1J%Z\xc0Y䝳\x14\xf1\x96\xe0|k\x8c\xb4ΗH\x8a\x84hn\xa2\xaf8n\x8dK\x95\xee\xf1M\xb9Y\n\xe6{Y\xa5b\x12\xd3\x02\xcf\xech\xf9DJ*\x0e\xdf<\xadԡ~\xf3\xb4\xc6\xca7Ok\xa2|\xf3\xb4\xbeyZ)5\xbfyZ\xdf<\xadv\xf9\x97\xf0\xb4\xa6F\xe4\x0e:\x0e|\x9c\x1cE\xc2V\xf5\xd8\x10G\xe0\xfb\xe4\n\x9f\x03\xfe\xa4\\\xccU\x1cT$\xf1\x7f \xad;f\xb4\x9aɣNδZ\x13dޝ\xbb\x9ap%\x9f\x90u\x1f:=_\xd6\xfdj\x14♲\xee\xfd\xb0\xa7}\xec\x93r\xee\x03Q\xe6eg/|\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x99\xe8\xff\x85\x13s{Ycg\x94\x8fg\xcf\xe2O\x96\x91(K/\xfet\xf1\xf5\x91\xff<\x04\x1f$q\x9fv\xfe\xe0w\x04\xaa]\x81\xb6\xd3ºYx_\xa7\x18\x9fEnS3\xf1k\"F`uE\xf2\x88\x8a_\xab-0P|*\xfd\x8c\U001044ea\xab\b\x9c\xa4\xb3\xaaT\x1fD\xb6SR\xc8J\xfb\xa8\x84\x85\xf56s'\xfd\x03Ș\xb0F5\xfc?\xc8NV\x91L\xf0\x11\xf2Md\x04N#\xdfI\x0e\xf4\x9b\xd0`\xe8\xfe\xcde\xf7\x8b\x91>Up\xe8l\xf3\xe3\x0e\x04\uec0bm\xfb\x00@\xb8\xa8\xc1\xdfXp,`\x11@R\x11\xc1\xb8\x93\xbc\xfa\x9a\x87\xb6ܑO\xa5\x8b=\xcd\xf6;\xc6c*iɄ'\xa7\x10vS\x04\a\xfcҹ\xbb\xddg92\xf1\xbb\xa4\x06\xceO\bL\x89\x88M$\xff\x9d\x90\xf2\x97\x98[\xfc\xe4\xed\xf9\x94\xa4\xbe9+\xe6gK\xe0;\x7f\xda^\x12}\xa6S\xf4\xe6P\xe7\xd9\xd3\xf1^0\t\xefeR\xef\x12\x13\xeeΗ9\x9f\x16\x8f=)sl:t0\x9c47\x99*7\x19Z\x98Bl6J\x93)ps\x12\xdf&\xb9\x93\xa6f/\x96\xda\xf6b\tm/\x9b\xc66*E\xa3\x1f\xe7$\xaa\xc5\xef\xeb!\x93\x93-\x7f)a;\x95\fRu\xdcד\xd6W\x9f\x8e`X\xc6\a\xd7\xee\x85|\xe4\xa2↕\x1c7R\xf7,\x8f\x06\x1b\xcc\x0e\x0e\xf5\x05\x1a\xbfJ\xf53!Y\x13\x9f\xe7\x9d\xee9y\xcbB\xaa\x1c\xd4\xe8\xb6O\xaa\x14\x8e\xca_\xcaڦ;\x90\xa3\xfd\x8ep럭\xd5\xf1\x97qz\xf07\xb0\xe2]\xbbCۗV\xd2Z\xdeFg/\xaaq\x7f\xbaΤ\xbf\x80\xd7mWi(\xa9\xc2K\x9d\xd7\a\x97\xce\x12\x9d\x9a\xdf\xd3lw\x04}G5\xd9HUPC.\xea\r\xc0\xd7\x0e\xb8\xfd\xfb⒐\x0f\xb2Ήh\xdfˣYQ\xf2\x83]\xa1\x90\x8bv\x83\xd3$ *m\xa1\xb7\x1b\xc9Y\x16\xf1ݢw3\xb9ʽ\xcb2\xf0ƨ\xac\x9d2Pڊq\xd7\rݼ\xee\x15\x98\x1bɹ|\x9c\xb9\xf6\xa7%\xfb\v^v\xfe\x84\xe8\xd0ۛ\x15\xc2\b⁷\xa7\xd7\xc9Y56k\xb0\xd3r\x83\xe7\x90\xee\xaf6\x1d\x88\xdd<\xc7\xf6\xad\xc1\x90\xbb\v\xa2\x83[\xe0Mg&\xadu\xb9Y\xb9q\f\xf5be\x86\x8a\x03\x91\x98QcvL\xe5˒*sp\x89\x1a\x8b\xce\x18\xc2\\:\x16\xdd\x19\x9c=\xfa\x97^G\xc9\x1b\xee\xba\xc6\x1d\xcaC\xd9\xdd\xf4=\xa6\xdd)\xe3\x18>\xbd8yn\xf1\x8c\xe3\x18vK\x96H\xa9\xc8\xcf\xd1̯\xb3Eʹ\xbf\x99\xf8g\xb9\x87w\xd1\xe8Y\x87<\xb7G\xd5#\xe9Y\x01\xa2\xbbtw0Ku\rx!o\xff\xd3\x13\xf2\xadB\xd7\xfeN\xd5S\x02e\xb7]\x10\x11\xfc\xc2\r\xb3\xa1\xb3\x98}\u009b\xf1\x0f\xe4\xe6\x1e\xd7h\xb5i\xf3*\xea\xd7h!T\x166\x83#p|\x83\xefϟ\x9a\xa6\x8dTt\v?Iw\xf9\xf8\x14ۻ\xb5;\x97\xd2{\xaf'\xe4\x8f\x06\xa5\x89]\xc0\xeb\xafA?\x02\xd6\xe4|\xf7.5\xb6\xa3\x9cyM\xb31\xfc\x14\xbe\xdf\xdd\xfd\xe4\xb02\xac\x80\xcbw\x95Kw\xb06Q\x83%q\xc0\xd6AZ\xdb\xff\xee\xe4#^\xfe\x1b\x8fc\x86\xc7$\x1ad\x14`\xb29\xa6 \xceB\xa9*\xb9\xa49\xa8k)6l;\x81\xdd/\x9d\xcaG\xd3l\x86?z\xe4\xea9*\xc0?s\x0e\x82\xf5y8\a\xfe\x81q\xd0nX\t\x06\xf8\xa6ߪ\xb6\xc7U\xb1v>\xdc\xc6~\xac;\x18\x98\xe3\x1cZ\x18\x8a.AY/\xca\x05\xad+\x1ddu\x18\xf1\x86#L\x18\xd8B\x7f\x158b\x81ݭ\xd28}\x06s\x82k\x99\x1fc\xf1\xad\x0e\xf2\xf7\xc3-\x8f8\xd9\ny\xc5n\xdcsN\xc8\xcd\xfd\xb5&\x95\xc81\\|\xff\x97\xdbYR\xb7\xef\xdc\\\x1f\xb4uʨ\xde\xc7[\xb5\x9c㖽pޱ\xdcD\x10\x18\x82\xd3z \xe5\x91\x19\x7fq\xd7yoZ\x1dZ\xf2\f=\xfd\x80W\xfaO?\xfe\xe0n\xfe\xf7O\xc6xu\xac\x14^\x93\xea_\x05\xc0kE\x9f\xf0\xfeC'\xf9K\xbf5\x06\x8a\xd2\xc4|\x8dis\xf8\xfd\x18\xc0\xdaO\x93\x86\xf2\x96V\xd2P!\xe6i\xeb\x83\xc8\xc6\x12˼5\x1a\xe1\xe6\x98>\xc6\bp\xed\xcfC\x9c\x8d\x005\xc0!\x02\xe8*\xcb@\xebM\xc5\xf9\xa1>\x8e\xf1\x95P\xe3\x03e\xfc|\xa4p\xd0\x06\x05\xc1\xa27\ni\x12a\x9f\xee\r\"\x0f\x9a\x1e\x8e*\xcd#\x85\xe7\x82φԆ\x16'=\xd8p\xdd\a\x83o\x19\xa9\xbc\x95TI\xeb\xb1Sݰ?6\xb94\xe0\\K\\dYh\x90\x13\u0603 vvv$\x0e\xcfẗ́\xe2O\xb8\xba\x19.\xccw!\x14\x12}\xb1\x89\xf8h\x87Ɨ\x81\xbe\xd35L\xcc\x15\xc5\xf7L\xfaD\xe8;\xbf.Zqe\xbd\x7fXZ\x10\xa7y\xadC\xaf\xb9t照\x19\xb9\xeb\xdb\xd5\x10\xb8SL\\\xff\xb9\x97'\xaaq\x1f\xdd'\x99\xb4>\xba\xb3\fZ\x04b-\xe3\xe7\xc7\x1dU\xfd\xb4Kݱ\xa5s8\xb2p\x86\x8er\xee\x0f:\x16\xa05݆\xdb\xdc\x1f\xed\xd2c\v\x02\\x\xcem\x9eD\x806\xa7\xe2\xbaw\x99;\x95\xa1\x99\xa9\xa8\xef $\xf8\xb6j}\xa7\t\x971\xa8\xf8\xa0\v\vO\xa8\x855\xd9LB})\x99JYý\xaf+Zڠ'\x8c\xdci\x1e\xbd\x03ζ\xf8\xa4\x93\xe5ܖ\xaa5\xdd\xc22\x93\x9c\x03Z\xeb\xfe\xb8\x9eS\xd7\xfd\xd9\xc3\xcf@\xf5$j\x1f\xdau\xfd\x0e\xa0\xe3\xb6\xdb\xf8\xa6.\xdd\x1d\x9f53LA\xf3\xc2`o@\x12;\x9e\xe5(;*D\x9f\xdf돴]7h\x9d7\xcb>\xce\xeb_\xdf[4/jE\xc6Y\xd0_\xa5Z\x90\x82\t\xfb\x0f\x15\xb9\xdb\xc0\v\x8dg\x8d\x7f'\xe5\xc3mĉ\xed\r\xfe\x87\xbab\xb3\xd5\xc1\x84\x1b6\x1e\x18]\xcb\xca\xef\xbe\xd7\x0em|[\x05o\xe6?\xf3r\x13a\x8e\xcc\a=t\x06#\xba?t MN\x05\xae\xe7\x01X\xb7\xe1\x897\xce\x0f\x8bc\xc8G\xcfI6\xb0[/\x17x7\xa0\xb9\x8f`\xa0\xa3\xb0#\x15\x05R_|\xd16觬z=\x99\x87\x9c\xc9\x1e\x8d\x7fhj\x0f\xd1\xd1\r\xb3\xe5\xee\r \xd8q\x02ϻ`\xc7g*&\x84\xff\xc6֩\xef.h-\xdcB\x96\xd8`\x94n襻\x8f\xd0߮X\x92\xbfVPEh\xb0\f\x0f\xc3\xdd\x1a\xaa\xfa!_w\f\x1er\xcc\xe8@m\x8cTY\x89\x1b%\xb7\nt_X\x97\xe4o\x94\x19&\xb6\x1f\xa4\xba\xe1Ֆ\x89O\xc3G~\xc6*\xdfPe\x98\x15v7\x9e\xd8@\x99\xa0\x9c\xfd=f\xd7\xda\x1f\xa7\x01]\x0f.\xb0\x96$a\x18C\x1fށ\xf5q\a\xe3\x02Q\x13Zz\xba\x9e\xe2\xaf\x04\x9eL\xd9\xd4ڗh|\x91\xd0\xed%\xf9(\xa3\x86\xc1\xa7C\xb1.L뒁6K\xd8l\xa42n\xb7z\xb9$l\x13\x82\x0f\xd6\xe6`\xdc\xcc=\xe2IXl\x9b\xb9N4i\xa6/\fz+\x9c\x85\xf1*\xfb\x82\x1e\xdc\xce\x14Ͳ\xcazX\xaf\xb5\xa1<\xe2\xe0<\xc9\xf0c\x94\xe7{|\xb0\xf2\x97'\xed\xe4\xadڀ\xfaAG\xecǑ\x14/\xd3p^\x1f\xb7(\x82 \x8f\x8a\x19c}*9\x92J\xe0Ie\xaco\xc59і\xd4'E\x1f\x893\xa3\xabᔜ4\x94\xefj(C\xe6\xd9c\x8d/3֯\x82\xfa\xec#_˲9\xdbQ\xb1\x1d\xbc\xa1`\xa7d\xb5\xdd\x05I\x1ep\xa6I^\x01\x06kѤ\xe8\xf0ⲩ\x94h\xa5\x12\x8c\x1c\xfb&A\x18p\xb84{\xc0\xf7K\u074b\xc6\xfe)\xeb\xd7\xfe\r\x94\xe5F\xc9b\xe9\xfb\xc5X\xea\xc2\xef\xe4+&\xad\xe7bvQ\xaa\x13\xe7\xb5\xfbg\x06P\x12\xca\x12\x04\xa1\xda\xf7\x9cpS\xd4\xc9\xd3\xd4ovj\xb8\x91\x9a%x\xfbQ\x8e\xff\xb5\r 0\xbc\f\x7fw\x99\xe1W0\xd8g\f\x8fO\xfe\b>\xec\xa90n9QO\x91\x17n\x12\xbb\x98\xb5\x90\xd1vb{R\x90\xe6\xb6\x03a\">\x83\xdd\xc5Yt\xeb\xd35\xdcE`\xd7\xfe\xf9\xd5\x1a\xf0\x82h&\u008b\xe0.\xf5\xc3I\x7ft'P\xe0C\x95Rų1\xc7\x03.]\x84^6ֲ\xaf=\x89\xf7'/\xc5\xef\x8f`\x1c\x1d\xea\xc6wI\xeb*a\xf9\xfc\a\x16\xdb\x0f\xc04\xde̢\xf2\xc7\xdf\xfd\xb0\xf6>i\xa9\x17\xa7\xc8\xd8\xca\x0f\x17u\xc3K\xb8\xee;\xa47\x1c\xac\xb6i\x80\xee\xa2r\x96\xce\xed\xcf\x18M;g(-\xbc}\x7f\x9eX\xd2\xfe\x8cA\xb4g\x8b\xa0\x9d\x17\xe5G\x8a\x0fD\x9f\xa4\xb5\x7f\xf3m#!4\x0f\xf6\xdcA\xb4V\f-\f\xfcE\xa3h\xd19\xb7\xf7#\xda\xe9\xbce-|O\xfe\x97\xff\x0f\x00\x00\xff\xff9i\xfd\xfe\xeb\x83\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccZK\x93۸\x11\xbe\xebWt\xed\x1e\xf6\xb2\x94줒J\xe96\x96\x93*W\xc6\xf1\xd4hvr]\bhJ\xb0@\x80\x01@\xc9\xca㿧\x1a\x0f\x89\xe2C\x0f;q\u008b-\x12h\xf4\xf3\xeb\x0f\xc0\x14E1a\xb5|E\xeb\xa4\xd1s`\xb5\xc4/\x1e5\xfdr\xd3\xed\x1f\xdcT\x9a\xd9\xee\xedd+\xb5\x98âq\xdeT\xcf\xe8Lc9\xbe\xc7Rj\xe9\xa5ѓ\n=\x13̳\xf9\x04\x80im<\xa3\u05ce~\x02p\xa3\xbd5J\xa1-֨\xa7\xdbf\x85\xabF*\x816\b\xcfK\xef\xdeL\xdf\xfe~\xfa\xbb\t\x80f\x15\xcea\xc5\xf8\xb6\xa9\x9d7\x96\xadQ\x19\x1eENw\xa8К\xa94\x13W#\xa7\x15\xd6\xd64\xf5\x1cN\x1f\xa2\x84\xb4z\xd4\xfc]\x10\xb6\x8c\xc2\x1e\x93\xb0\xf0]I\xe7\xff<>\xe6Q:\x1f\xc6ժ\xb1L\x8d\xa9\x15\x86\xb8\x8d\xb1\xfe/\xa7\xa5\vX9\x15\xbfH\xbdn\x14\xb3#\xd3'\x00\x8e\x9b\x1a\xe7\x10f\u05cc\xa3\x98\x00$\xd7\x04i\x050!\x82\xb3\x99z\xb2R{\xb4\v\xa3\x9aJ\x1f\xd7\x12踕\xb5\x0fΌ\xb6@2\x06\xb25\xe0<\xf3\x8d\x03\xd7\xf0\r0\a\x0f;&\x15[)\x9c\xfd\xa2Y\xfe\x7f\x90\a\xf0\xd9\x19\xfd\xc4\xfcf\x0e\xd38kZo\x98\xcb_c\x8c\x9eZo\xfc\x81\fp\xdeJ\xbd\x1eR\xe9\x919\xffʔ\x14A\x93\x17Y!H\a~\x83\xa0\x98\xf3\xe0\xe9\x05\xfd\x8a\x1e\x02r\x11B\xf6\x10\xec\x99K\xeb\x00좔\xe0\xa3aMUo\xad3\xb5I\x15x\xedH\x89\xfaӛ\xa4}Kl\xce\xef)\xb7x\x14\xe9<\xab\xea3\xb9\x0fk\x1c\x13v\xe6\x8a\xf7X\xb2F\xf9\xb6\xa9\x14%\xd5\xce\xcbs\xb3j\xe4S\x11g\x9d\xad\xf8\xfe\xec]\\ue\x8cB\x16\xa5\xc4Q\xbb\xb71\v\xf9\x06+6O\x83M\x8d\xfa\xe1\xe9\xc3\xebo\x97g\xafa(\x91:EA\x81c\xad\xd8l\xd0\"\xbc\x86\xfa\x8bqsɴ\xa3L\x00\xb3\xfa\x8cܟ\x82X[S\xa3\xf52\x17K|ZX\xd4z\xdb\xd1\xe9\x9f\xc5\xd97\x002#\xce\x02A\xa0\x841\xafR\xfd\xa0H\x96\x83)\xc1o\xa4\x03\x8b\xb5E\x87:\xc2\x14\xbdf:)8\xed\x88^\xa2%1Tۍ\x12\x84e;\xb4\x1e,r\xb3\xd6\xf2\xefG\xd9\x0e\xbcI\xc9\xec\xd1y\b\x15\xaa\x99\xa2dm\xf0g`Zt$W\xec\x00\x16iMhtK^\x98\xe0\xbaz|\xa4j\x90\xba4s\xd8x_\xbb\xf9l\xb6\x96>#47U\xd5h\xe9\x0f\xb3\x00\xb6r\xd5xc\xddL\xe0\x0e\xd5\xcc\xc9u\xc1,\xdfH\x8f\xdc7\x16g\xac\x96E0DGH\xadď6a\xba;[\xb6W\xd2\xf1\t\x90zGx\b^c\xcaDQ\xd1\xc4S\x14\xe8\x15\xb9\xee\xf9\x8f\xcb\x17Ț\xc4HŠ\x9c\x86\xf6\xfc\x92\xe3Cޔ\xbaD\x1b\xe7\x95\xd6TA&jQ\x1b\xa9}\xf8\xc1\x95D\xed\xc15\xabJzJ\x83\xbf5\xe8<\x85\xae+v\x11\xba\x18\xac\x10\x9a:\x80Dw\xc0\a\r\vV\xa1Z0\x87\xdf9V\x14\x15WP\x10n\x8aV\xbb7w\aG\xf7\xb6>\xe4\x9e:\x12\xdaA4X\xd6\xc8\xcf\xeaN\xa0\x93\x96*\xc33\x8f\xa1\xba:\x0eJP1ޔ\xf33\f\x12\xf40\xceѹ\x8fF`\xf7KG\xe5\x87\xe3\xc03\x1dk\xb4\x95t\xa1\xbdBil\xb7\xf3\xb0#\x92\xb7\x9f\x8cx݀\x03\xa0n\xaa\xbe\"\x05<#\x13\x9f\xb4:\x8c|\xfa\xab\x95\xbe\xbf\xd0H \xe9\x89*.\x0f\x9a?\xa1\x95F\\1\xfe]g\xf8\xd1\x05\x1b\xb3\x872\xe4\xbf\xf6\xea@\xd8\xe5\x0e\x9a\xf7Q;?\x0fO\x1f2\x82\xc7\xdaJ\x85\x99|5\x85\x87TԦ\x847 \xa4#\"\xe1\x82о\xb3t\xa3\x02ј\x83\xb7\xcd]\xe6s\xa3K\xb9\xee\x1b\xdd\xe6Fc\x19sEt\xc7s\x8b\xb0\x12\xa1\x16eGm\xcdN\n\xb4\x05Շ,%O\x9a46v\x90R\xa2\x12=l\x1a\xad\xb2`\x8aEAE\xcdԕ\x18.\x8e\x03\x03\x93fR\xc7\f>\t\bXc\xabԚ\xb5G-\xb0\xdbm\x826&\x00\x9aC\x01{\xe97\x11)\xd5P\xdd\xc1\xc5ڣg\x8b\x87\xa1\xd7\x1d\xdd_6H#c\xe3Ep\xc8-\xfa\x90m\xa8(}(\x95\xa6\x00\x1f\x1b\x17\xb0\xb6\x8b\x13\xf9\t\x84/\xcf\xde\xe2\xa1\xefh\xb8\x16\xdcD\x85FT\x0e$j\x0e?\xfcpݤ^w\xcb\x0fQ\xf7l\xa8\xc5\x12-\xea\x1e\x9b\xc8\xcfK\xe8Q\x944\x94aX\x96Ƚܡ:\x84\x9eD\xe0\xf93\xac\x1a\x0f\xa2\xc1\x105Ʒ{f\x85\x03n\xaa\x9ay\xb9\x92J\xfa\x03H7\"\x9f)e\xf6(Rı\xaa\xfda\n\x1f\xb4\xf3LstG\x1eD\x1e\x8b\xa9\xc0t\x1c\x95\xaa8\x10:f\x8700\x8a\xaf\x8c\xf3\xc0\xd1R:\xaa\x03\xec\xad\xd1\xeb1c\a\xda!\xed\x01\xadF\x8f\xa1#\n\xc3\x1d5C\x8e\xb5w3\xb3C\xbb\x93\xb8\x9f\xed\x8d\xddJ\xbd.H\xc1\"\x81\xcf,\xec\xecf?\x86\x7f\xbe&\vL\x1dq\xe2\x86\xe4]\x86Z?\x10\xbd\xf5\x1b\x8c-b\x19s\xd0X \x02A\xa9]\xa5܍\xc8:TvC\xbc\xbc\xfd\xe4\x90\x0f\xf5\x8f-\xf6[\xc7\x05P\x01\xf8R\x9c|[T\xac.\xe2h\xe6M%\xf9\xa4km\xcc\xfb\xcb\xf8\x937+R\vɉܞ\xe3F\xdeĉ\xb3=̀\x1b\xba\xbb\x9c1\xb4\x1cvS47q\x85+\x1a\x7fj\x8f=m}#t\xa7\xfe\xef\xd0\x13\xeft\xa0\x91\xf8\x01\xb3}?\a\xc0\xe4FkB*o\x80\x1d\xdb\xc0O\xae\xdb\xff\xeeD\xcfU÷8\xe0\xf8\x9e)\xef\xc2\xc0\xec\xe38\x8dti\x1c\x86\xc6tM\r\xb8^\x11\x9c-\xd0ޢ\xcb(\xf2-\x1eH\u0091[0X<\xc0\xaa\xd1BaVu\xbfAM\xdb1Y\x1e\x88\xec\xbf<.\xb3c\x03\x01K[\xa7\xec\xde1 yO\xbb\x00JA1\x87_\x1c\xa6u\x9f\xb1\x04\xa9\x9dG\xd6#\xe9\xf1\x89\xbdq\x0e\xab\xc3\x00\u05fa\xd9A\xcfX~\xbb\x8f\x82\xae\xe4\xa1\xd4 8\xc6\xc4J\xb0\x92\xfa{\xde\x0f-\x1e\x02\xc4\x12\xdf \"}\xe6ґe\xeetth\xd0i\xf1\fdR\xc7\x02a\xd5\xd8\":\x1fR\x01\x8by9H䇃q\xb9.\xe0\x12\xb3\xe89\xfb>v1*\x13\x80\xdd\xc80\xe0z\xb2\xc0E\xa6\x017\xb0\x8d\x9e\x99\xa39\x05w\xb2\x0e\xf8\x0e\xcc\x03\xfe\xfb\xec\x03\xeef \xf0\xddY\bܖ)\x97\xd9\b|\x13#\xb9\xe0\x8bK\\\x05\xae\xf2\x15\xb8\xc8Y`\x94\xb7\xc05\xee\x02w\xf2\x17\bx\x82\xa5\xfcr\x032?\x85\x81\xb9\x93\xd6\xcco\xa8kH\x81\xc0\x06\xfaj<\xa1\x18q\xd0q\xd3\xfb)\x85\xef+\xfa\xee%\xd2\x17չ\x87\xf7e@\xbfB\x8c\x9eҰ\xa3\x17\xf2\xef\x04 \xe7\a c\x04mТ\xdd\xf1\xb4\xfdO\xf1X\x81\x0f\xa0\xf8\x992\xaf\xfd\x19\x17\x8e'\xf2\x99\xff\x10K\xa3Ͱ\xb1\x16]m\xb4\xa0\xb6w\xdb\xe1\xc4I\xe5\xff\xdc\x11\xc5pX\x8bs\xfa\xda\xf9\x96\xa3p\xd3\xf9\\\xb8߸\xfb\x84.\xde\xfa\xb4Ͽ\xccʡݵ\x0e\xe9:6~\x97\xb3\xb9\xc1\xce\xd6:\xb0#\xaa\xa4\xa1\xd1\xe1\xc8\"4\xad\xe9d`F\x9b\x17\xfa\xd0<\xa4\x03m\xf64\xb9%-v=\x13\xe9M8\xb4dZ\xa4\xe3b\xfa4 y/\x95\xa2\x1ef\xb12\xe4,\xd4^Zj\x96,\xb4\xb1\xddo\xa6o\xfewg\x81\x8a9\xbf\xc6Qq\xff\x99\xa6\x00[\x99\xc6\x0f\xf4\xfeV\xc2\x0f\xd6t\xb8e\xbfG\xc7\xf0\xb7\x03\xd7\xe8\t\x8d\xc9\x11፵\xe1\xb2._\"ݱ\xd1\x1cC\xe0\x87Ο8\xb4\xbf\xf5\xff\x00\xe2\x06\xbb\x06\xbbt\xefe촭\xb8&'\xb7\xdf4\xab\xe3\x15\xec\x1c\xfe\xf1\xafɿ\x03\x00\x00\xff\xff%\xff\\)\x99#\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVK\x8f\xdb6\x10\xbe\xfbW\f\xd0k%wQ\xb4(tk69,\xda\x06\xc6n\x90;M\x8e-f)\x92\x9d\x19z\xeb>\xfe{AR\xf2C\xb67\xc9%\xba\x99\xf3\xe4\xf7\xcd|t\xd34\v\x15\xedG$\xb6\xc1w\xa0\xa2ſ\x04}\xfe\xc5\xed\xf3/\xdcڰ\xdc\xdd-\x9e\xad7\x1d\xdc'\x960<\"\x87D\x1a\xdf\xe2\xc6z+6\xf8ŀ\xa2\x8c\x12\xd5-\x00\x94\xf7AT>\xe6\xfc\x13@\a/\x14\x9cCj\xb6\xe8\xdb\xe7\xb4\xc6u\xb2\xce \x95\xe4S\xe9\xdd\x0f\xed\xdd\xcf\xedO\v\x00\xaf\x06\xec\xc0\xa0C\xc1\xb5\xd2\xcf)\x12\xfe\x99\x90\x85\xdb\x1d:\xa4\xd0ڰ\xe0\x88:\xe7\xdfRH\xb1\x83\xa3\xa1Ə\xb5k\xdfoK\xaa7%\xd5cMU\xacβ\xfcv\xcb\xe3w;zE\x97H\xb9\xeb\r\x15\a\xee\x03\xc9\xfbc\xd1\x06̚\xaa\xc5\xfamr\x8a\xae\x06/\x00X\x87\x88\x1d\x94ب4\x9a\x05\xc0\bH\xc9Հ2\xa6@\xac܊\xac\x17\xa4\xfb\xe0\xd2\xe0\x8f\x95\x905\xd9(\x05\xc2\x0f=\x96\xcbC\u0600\xf4\b\xb5\x1cH\x805\x8e\x1d\x98\x12\a\xf0\x89\x83_)\xe9;h3\x92mu͍\x8c\x0e\x95\x847\xf3c\xd9\xe7\x86Y\xc8\xfa\xed\xad\x16X\x94$\x9e\x9a(um\xf0@'ȟ7P\xfc\xdb\xd8+>\xaf\xfeT\f\xb7*W\x9f\xdd]EZ\xf78\xa8n\xf4\r\x11\xfd\xaf\xab\x87\x8f?>\x9d\x1d\xc3y\xafWH\aˠ\xa6N3p\x155\b\x1e!\x10\f\x81&T\xb9=$\x8d\x14\"\x92؉\xff\xfa\x9d\xac\xd5\xc9鬅\x7f\x9b3\x1b@\xee\xbaF\x81\xc9\xfb\x85\\@\x1c\x87\x02\xcdx\xd1\n\xaee \x8c\x84\x8c\xben\\>V\x1e\xc2\xfa\x13jig\xa9\x9f\x90r\x9a<\xaeə\xbc\x96;$\x01B\x1d\xb6\xde\xfe}\xc8\xcd\xf9\u07b9\xa8SR \xc9c畃\x9dr\t\xbf\a\xe5\xcd,\xf3\xa0\xf6@\x98kB\xf2'\xf9J\x00\xcf\xfb\xf8#\x83h\xfd&tЋD\xee\x96˭\x95Ilt\x18\x86\xe4\xad\xec\x97E7\xec:I ^\x1aܡ[\xb2\xdd6\x8ato\x05\xb5$¥\x8a\xb6)\x17\xf1Ep\xda\xc1|G\xa3<\xf1Yً\xe9\xa9_ч\xaf\xa0'\xabE\x9d\x91\x9a\xaa^\xf1\xc8B>\xca\xd0=\xbe{\xfa\x00S'\x95\xa9J\xca\xd1\xf5\x02\x97\x89\x9f\x8c\xa6\xf5\x1b\xa4\x1a\xb7\xa10\x94\x9c\xe8M\f\xd6K\xf9\xa1\x9dE/\xc0i=X\xe1ib3u\xf3\xb4\xf7E\x90\xb3\x02\xa4h\x94\xa0\x99;b\xa3\x13Q\x19\xbe\x83ΫkA_\x8a\x05\x12\x05\xba8\x9d5\xf5\xae8\x95\xff\x12\xcaz\x06\xe5\xf7c H\xaf\x04^\x90\xf2\x86萲Z\xa1\x01\x93.\xf0\x1ba9}\x93\"\x05\x8d|\xb1\x8a\x00Vp\xb8\xd2\xd3+\xec\xe4\xcf'\xe7\xd4\xdaa\aB\to0\xab\x88\xd4~f+o\xdfg Xe\x9fk\x1c\x1c\xde\xfbϒP\xe0\xf6i\xb8\xac\xd4\xc0{|\xb9r\xfa\xe0W\x14\xb6\x84<\x1f\xf9l\\U\xf4p\xfe\x1e\xbc\x82\xd2ա\xbc8\xe4,\x85\xe6\x04E\x96@j{\x8a+\xa7\xf5A\xe9;\xf8\xe7\xbf\xc5\xff\x01\x00\x00\xff\xffߙ6&\xcb\n\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcW\xcdn\xe36\x10\xbe\xfb)\x06\xe8\xa5\x05Vr\x83\xa2E\xe1[\xeb\xec!\xd8t\x1b\xc4\xdb\xdc)j$\xb1\xa1H\x96\x1c:\x9b\xa2\x0f_\f)\xf9G\x96\x1d粺y8\x9c\x9fof\xbe\xa1\x8b\xa2X\b\xa7\x9e\xd0\ae\xcd\n\x84S\xf8\x95\xd0\xf0\xafP>\xff\x1aJe\x97ۛų2\xf5\n\xd61\x90\xed\x1f1\xd8\xe8%\xdeb\xa3\x8c\"e͢G\x12\xb5 \xb1Z\x00\bc,\t\x16\a\xfe\t \xad!o\xb5F_\xb4h\xca\xe7Xa\x15\x95\xae\xd1'\xe3\xa3\xeb\xed\x8f\xe5\xcd/\xe5\xcf\v\x00#z\\Am_\x8c\xb6\xa2\xf6\xf8O\xc4@\xa1ܢFoKe\x17\xc1\xa1dۭ\xb7ѭ`\x7f\x90\xef\x0e~s̷\x83\x99\xc7l&\x9dh\x15\xe8\xd3\xdc\xe9\xbd\x1a4\x9c\x8e^\xe8\xd3 \xd2a謧\xcf{G\x05\xd4>\x1f(\xd3F-\xfcɽ\x05@\x90\xd6\xe1\n\xd25'$\xd6\v\x80!\xf7d\xa6\x18\xd2\xde\xdedS\xb2\xc3^d\xfb\x00֡\xf9\xed\xe1\xee\xe9\xa7͑\x18\xa0\xc6 \xbdr\x94\x10\xfc\xaf\xd8\xc9a\x9a\x19\xa8\x00\x02\x86p\x80\xec.B\x10\x06\x84'\xd5\bI\xd0x\xdbC%\xe4st`\xab\xbfQ\x12\x04\xb2^\xb4\xf8\x01B\x94\x1d\b\xb6\x92\x15\x0e|i\xdbB\xa34\x96;\x99\xf3֡'5B\x94\xbf\x83N;\x90^ʂ?N<߂\x9a[\x0e\x03P\x87#xX\x0fX\x81m\x80:\x15\xc0\xa3\xf3\x18\xd0\xe4&d\xb10C6\xe5\xc4\xf4\x06=\x9b\xe1\x8aF]s\xa7n\xd1\x13x\x94\xb65\xeaߝ\xed\xc0\x88\xb1S-(\x81i\b\xbd\x11\x1a\xb6BG\xfc\x00\xc2\xd4\x13˽x\x05\x8f\t\xc1h\x0e\xec\xa5\va\x1a\xc7\x1f\xd6#(\xd3\xd8\x15tD.\xac\x96\xcbV\xd18\x7f\xd2\xf6}4\x8a^\x97i\x94T\x15\xc9\xfa\xb0\xacq\x8bz\x19T[\b/;E()z\\\n\xa7\x8a\x94\x88I3X\xf6\xf5w~\x98\xd8p\xe4\x96^\xb9!\x03yeڃ\x8346\xef(\x0f\x0fR\xee\xael*\xa7\xb8\xaf\x02\x8b\x18\xbaǏ\x9b/0F\x92+5\xb4\xd8N\xf5\x04\x97\xb1>\x8c\xa62\r\xfa|/\xb5)\xdbDS;\xab\f\xa5\x1fR+4\x04!V\xbd\xa20\xf6:\x97njv\x9d8\n*\x84\xe8jAXO\x15\xee\f\xacE\x8fz-\x02~\xe3ZqUB\xc1E\xb8\xaaZ\x87\xcc;U\xce\xf0\x1e\x1c\x8c\xbcy\xa6\xb4\x13\xca\xd88\x94\\XƖo\xaaF\xc9\xef狪\xe3\x9a\xdf\xed\xc4\v\x8aQ\x9f\xf5\xfb\x88\xbcA\xf0|\xa6\x83\xc2UV\xae\x88iм*\xd1\xf5\xe6\xee=\x10\x9eQ\x7fG\x91\xeeLc\xdfHq\xaf8\xabw\x86\x06\xc6/\xbd!\xde\xeei~\x85\x8c=\xcdW\xf2\xeeD\xf8\x14+\xf4\x06\tÞ\xa9_\x14u\xb3\x16\x01^:%\xbbt1\r\x04/\x81\x10\xacTs\x94zE\xf8\xcc#\xca\xe3\xccP\x16iXg\xc4\x1c\xfc\x89\xf8\f\xfb\x9dsP\f\x8ct\x15\x83\x92\xa0\x18\xde\xc1\xa1I\x7f\x84ZF\xefӊ\xcaR~\x99L/\\K\xa2#\xf3\xfc\xf5x\xff\x06\x93\xde\xee5\xd3S\\(\x93\xa3q\x1e\x8b\xa0Z~A\xf1\x19si\xe2\xb8S0\xf2w\xfc\xc2;\x06j\xb6\xa2\xf8թ<\x80o\x84\xf8q\xa7\x98\t\x1fM\xde\xf3\xd37l2\x88\x81\x9f[ \x85\x99\x89\xb1B\xa8Q#a\r\xd5k\xde\\\xaf\x81\xb0?\x8d\xbb\xb1\xbe\x17\xb4\x02\xde\xff\x05\xa9\x9962QkQi\\\x01\xf9x\xae\xcbf\x13w\x9d\b3cx\x94\xf3\x03\xeb\xcc5\xc6n\x18/v\x06\x9c\xdd/\x05|Ɨ\x19郷\x12C\xc0\xd31:\x9b\xc9\xec\x10\x9c\b\x03?\xd2\xea\x03\x94\x86\xbf\f\x83\xe4\xff\x00\x00\x00\xff\xff\xe4\xb6\x15`c\x0e\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcW\xcdn\xe36\x10\xbe\xfb)\x06\xe8\xa5\x05Vr\x83\xa2E\xe1[\xeb\xec!\xd8t\x1b\xc4\xdb\xdc)j$\xb1\xa1H\x96\x1c:\x9b\xa2\x0f_\f)\xf9G\x96\x1d粺y8\x9c\x9fof\xbe\xa1\x8b\xa2X\b\xa7\x9e\xd0\ae\xcd\n\x84S\xf8\x95\xd0\xf0\xafP>\xff\x1aJe\x97ۛų2\xf5\n\xd61\x90\xed\x1f1\xd8\xe8%\xdeb\xa3\x8c\"e͢G\x12\xb5 \xb1Z\x00\bc,\t\x16\a\xfe\t \xad!o\xb5F_\xb4h\xca\xe7Xa\x15\x95\xae\xd1'\xe3\xa3\xeb\xed\x8f\xe5\xcd/\xe5\xcf\v\x00#z\\Am_\x8c\xb6\xa2\xf6\xf8O\xc4@\xa1ܢFoKe\x17\xc1\xa1dۭ\xb7ѭ`\x7f\x90\xef\x0e~s̷\x83\x99\xc7l&\x9dh\x15\xe8\xd3\xdc\xe9\xbd\x1a4\x9c\x8e^\xe8\xd3 \xd2a謧\xcf{G\x05\xf0y>R\xa6\x8dZ\xf8\x93\x9b\v\x80 \xad\xc3\x15\xa4\x8bNH\xac\x17\x00C\xf6\xc9P1$\xbe\xbdɦd\x87\xbd\xc8\x1e\x00\xacC\xf3\xdb\xc3\xdd\xd3O\x9b#1@\x8dAz\xe5(a\xf8_\xb1\x93\xc347P\x01\x04\f\xe1\x00\xd9]\x84 \f\bO\xaa\x11\x92\xa0\xf1\xb6\x87J\xc8\xe7\xe8\xc0V\x7f\xa3$\bd\xbdh\xf1\x03\x84(;\x10l%+\x1c\xf8Ҷ\x85Fi,w2\xe7\xadCOj\x04)\x7f\a\xbdv \xbd\x94\x05\x7f\x9cx\xbe\x0557\x1d\x06\xa0\x0eG\xf0\xb0\x1e\xb0\x02\xdb\x00u*\x80G\xe71\xa0\xc9m\xc8ba\x86lʉ\xe9\rz6\xc35\x8d\xba\xe6^ݢ'\xf0(mkԿ;ہ\x11c\xa7ZP\x02\xd3\x10z#4l\x85\x8e\xf8\x01\x84\xa9'\x96{\xf1\n\x1e\x13\x82\xd1\x1c\xd8K\x17\xc24\x8e?\xacGP\xa6\xb1+\xe8\x88\\X-\x97\xad\xa2q\x02\xa5\xed\xfbh\x14\xbd.\xd30\xa9*\x92\xf5aY\xe3\x16\xf52\xa8\xb6\x10^v\x8aPR\xf4\xb8\x14N\x15)\x11\x93\xa6\xb0\xec\xeb\xef\xfc0\xb3\xe1\xc8-\xbdrC\x06\xf2ʴ\a\aip\xdeQ\x1e\x1e\xa5\xdc]\xd9TNq_\x05\x161t\x8f\x1f7_`\x8c$Wjh\xb1\x9d\xea\t.c}\x18Me\x1a\xf4\xf9^jS\xb6\x89\xa6vV\x19J?\xa4Vh\bB\xaczEa\xecu.\xdd\xd4\xec:\xb1\x14T\b\xd1Ղ\xb0\x9e*\xdc\x19X\x8b\x1e\xf5Z\x04\xfcƵ⪄\x82\x8bpU\xb5\x0e\xb9w\xaa\x9c\xe1=8\x18\x99\xf3Li'\x94\xb1q(\xb9\xb0\x8c-\xdfT\x8d\x92y\xa4\x1a\xebA\xec\x19d@\xfa\x18\xa8y\x06H\xc1\t\xdf\"M\xa5\x93X\xbe$%v\xff҉c\xc2\xfa\x1e˶d\xce\tC \x99\x8f~\x98\x16\xeaR\f0\xdb賑\x8c\xfd\xcd00\xaeL(Lv\x871\x9d\xba\xe6\x0fM\xec\xe7\x1d\x14\xf0{\x8a\xf9\u07b6\x17\xcf\xd7\xd6\x10\xcf\xc5E\xa5'\xabc\x8f\x1b#\\\xe8\xec\x1b\xbaw\x84\xfd\x9f\x0e}\xde\xd0\x17U\xc7E\xbfۊ\x17\x14\xa3>\xeb\xf7\x11y\x83\xe0\xf9L\a\x85\xab\xac\\\x11ӠyU\xa2\xeb\xcd\xdd{ <\xa3\xfe\x8e\"ݙƾ\x91\xe2^qV\xef\f\r\x8c_zC\xbc\xdd\xd3\xfc\n\x19{\x9a\xaf\xe4݉\xf0)V\xe8\r\x12\x86=S\xbf(\xeaf-\x02\xbctJv\xe9b\x1a\b^\x02!X\xa9\xe6(\xf5\x8a\xf0\x99G\x94Ǚ\xa1,ҰΈ9\xf8\x13\xf1\x19\xf6;\xe7\xa0\x18\x18\xe9*\x06%A1\xbc\x83C\x93\xfe\b\xb5\x8cާ\x15\x95\xa5\xfc2\x99^\xb8\x96DG\xe6\xf9\xeb\xf1\xfe\r&\xbd\xddk\xa6ǸP&G\xe3<\x16A\xb5\xfc\x82\xe23\xe6\xd2\xc4q\xa7`\xe4\xef\xf8\x85w\f\xd4lE\xf1\xabSy\x00\xdf\b\xf1\xe3N1\x13>\x9a\xbc\xe7\xa7o\xd8d\x10\x03?\xb7@\n3\x13c\x85P\xa3F\xc2\x1a\xaa\u05fc\xb9^\x03a\x7f\x1awc}/h\x05\xbc\xff\vR3md\xa2֢Ҹ\x02\xf2\xf1\\\x97\xcd&\xee:\x11f\xc6\xf0(\xe7\a֙k\x8c\xdd0^\xec\f8\xbb_\n\xf8\x8c/3\xd2\ao%\x86\x80\xa7ct6\x93\xd9!8\x11\x06~\xa4\xd5\a(\r\x7f\x19\x06\xc9\xff\x01\x00\x00\xff\xff\xe5\x0fY\x99e\x0e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4:K\x93\x1b\xb7\xd1\xf7\xfd\x15]\xeb\x83\xed*\ri\xf9\xfb\xe2J\xf1&\xad\xe2\x14\x13[\xd9\x12W\xba\xb8|\xc0\f\x9a\x1cxg\x00\x04\xc0\x90b\x1c\xff\xf7T\x03\x987H\xeeR\xb6\x85\xcb.\xf1h\xf4\xfb\x85ɲ\xec\x86i\xf1\x01\x8d\x15J\xae\x80i\x81\x1f\x1dJ\xfae\x17\x8f\x7f\xb5\v\xa1\x96\xfb\x977\x8fB\xf2\x15\xdc5֩\xfa\x1dZ\u0558\x02\xdf\xe0VHᄒ75:ƙc\xab\x1b\x00&\xa5r\x8c\xa6-\xfd\x04(\x94tFU\x15\x9al\x87r\xf1\xd8\xe4\x987\xa2\xe2h<\xf0\xf6\xea\xfd7\x8b\x97\xdf-\xfer\x03 Y\x8d+Њ\xefU\xd5Ԙ\xb3\xe2\xb1\xd1v\xb1\xc7\n\x8dZ\buc5\x16\x04{gT\xa3W\xd0/\x84\xb3\xf1ހ\xf3\xbd\xe2\x1f<\x98\xd7\x1e\x8c_\xa9\x84u\xffL\xad\xfe \xac\xf3;t\xd5\x18V͑\xf0\x8b\xb6Tƽ\xed/\xca@\xef\xf3\xb0\"䮩\x98\x99\x1d\xbc\x01\xb0\x85Ҹ\x02\x7fN\xb3\x02\xf9\r@$\xde\xc3ɀq\xee\xd9ɪ{#\xa4CsG\x10dw\vG[\x18\xa1\x9dg\xd7\x04s\xb0\x8e\xb9Ƃm\x8a\x12\x98\x85\xb7xX\xae\xe5\xbdQ;\x836\xa0\r\xf0\x8bU\xf2\x9e\xb9r\x05\x8b\xb0}\xa1Kf1\xae\x06\xb6o\xfcB\x9crGB\xd9:#\xe4.\x85ă\xa8\x11xc\xbc\xb8\x89\xfa\x02\xc1\x95\xc2ΰ;0K\x18\x1a\xe7\xc9N\xe3\xe2\xd7\t\xa2u\xac\xd6S\xa4\x06G\x03V\x9c9L\xe1t\xa7j]\xa1C\x0e\xf9\xd1aK\xc9V\x99\x9a\xb9\x15\b\xe9\xbe\xfb\xff\xd3\xec\x88\xfcZ\xf8\xa3o\x94\x1c\xf3\xe65\xcd\xc2`:`B\xb2ڡI2H9V}\n\"\x8e\x00\xbc\x1e\x9c\x0f\x98\x04\xb8\xc3\xf9\x8b\xa8\xacea\xb0Fy\x1dB\xa2?=\xc7f\bz\xb8\xaa\x8dPF\xb8\xe3\n^~\xf3T4\xc9>@m\xc1\x95\bQy6N\x19\xb6C\xf8A\x15A\xd1\x0e%\x9a\xa8hy\xd4\xfeR5\x15\x87\xbc\x15\f\x80u\xca$\x95Mc\xb1\b\xa7\"\xdc\x16\xecD\xe3\xc6w\xfe\x11\x06Q\x18dI\x83h\xdd\xe9\xc2\xef\x10J\xa6\xad\xe2\xd5\x0e\x9fd\x11C\x96Jű\xe3\x1f\xce\xd0\x12\x16\xb4Q\x05Z{\xc6P\t\xc6\b\x91\xb7\xfd\xc4E\x06\x95\xe8\xf7\xb4\xf84\xbaR\x8c\xa3\x01\xa7\xa0d\x92WHd0p\x86I\xbb\x8d*2\x17`{\xec\xe1\xa8Ǩ\xbc\x8f\v\xa7\xd0\t\xbb\xf6/\x83\xbb.J\xac\xd9*\xeeU\x1a\xe5\xab\xfb\xf5\x87\xffی\xa6I\x8d\x95F\xe3D\xeb\xef\xc3\x18\x84\xcd\xc1,\x8c\xc9\xfdo6Z\x03\xa0\v\xc2)\xe0\x14?\xd1z6\xc4@\x80<\xe2\x14\xd8#,\x18\xd4\x06-\x99\x96\xd7(\xb5\x05&A\xe5\xbf`\xe1\x16\x13\xd0\x1b4\x04\xa6\xb5\x85B\xc9=\x1a\a\x06\v\xb5\x93\xe2?\x1dlK\xbc\xa6K+\xe6\xd0:o\x8cF\xb2\n\xf6\xacj\xf0\x050\xc9'\x90kv\x04\x83t'4r\x00\xcf\x1f\xb0S<~T\x06AȭZA霶\xab\xe5r'\\\x9bL\x14\xaa\xae\x1b)\xdcq\xe9\xf3\x02\x917N\x19\xbb\xe4\xb8\xc7ji\xc5.c\xa6(\x85\xc3\xc25\x06\x97L\x8b\xcc\x13\"}B\xb1\xa8\xf9\x17&\xa6\x1fvt\xedL\xd0a\xf8\x1c\xe0\x19⡬\x80\x8c\x80EP\x81\xc4^\n4E\xac{\xf7\xb7\xcd\x03\xb4\x98\x04I\x05\xa1\xf4[g|i\xe5C\xdc\x14rK:O\xe7\xb6F\xd5\x1e&J\xae\x95\x90\xce\xff(*\x81ҁm\xf2Z8R\x83\x7f7h\x1d\x89n\n\xf6\xce'\\\x90\x93-\x91\a\xe0\xd3\rk\tw\xac\xc6\xea\x8eY\xfc\x93eER\xb1\x19\t\xe1I\xd2\x1a\xa6\x91\xd3́\xbd\x83\x856\t\x13\x86a\xc6;%:W\xaaB6\xe5%\x85\xbb\v4S\x00L\t\xcbG[W2\xd7\xe2F\x9bL#圷4\x94|\x968\xb4\xe2\x17\xf0\x8a720\xb8E\x83>\x1b\t\xbe_+\x1f!\x1c\x13\xb2\xf5i\xa1^\x01\xa7\x12\x98\xe5A\x89\x90\xc3\xd46\xe0\xac}\xc0\x99@\x99\xc4\xf8\xd5\xfd\xba\r\x86-\x13#\xee\xb3xw\x91?4\xb6\x02+\xee3\x87\xcbw'5\x97\xc6z\x1b\x90\xf0\x11\xc1)`\xa0\x05\x168\x8a\xc6 \xa4u\xc8x\x9c$'h0\xae\xbd\b\x9e\xfe$\x92\x10\x8a\xca\x18\xb5I&\xc0(\xf2\b\x0e\xff\xd8\xfc\xeb\xed\xf2\xef*\xd0\x01\xac\xa0\xd4\xcc\xd7z>\xdf~\xd1\xd5{\x1c\xad0ȩz\xc3Eͤآu\x8b\b\r\x8d\xfd\xe9۟\xd3\xfc\x03\xf8^\x19\xc0\x8f\x8c\xaa\xa6\x17 \x02ϻ`֪\x8d\xb0\x81\xf0\x0e\"\x1c\x84+=\xa2Z\xf1H\xe0\xc1\x93\xe0\xd8#Yr \xa1A\xa8\xc4c\xc2~¸\xf5\xd9\\\x8f\xe6\xafd=\xbf\xdd\xc2W\xc1y\xdd\xd2\xcfۀF\x97\xb6\f\r\xacG'X\x99\x11\xbb\x1d\xf6y\xffLY(\xccR\x80\xfa\x1a\x94!Z\xa5\x1a\x80\xf0\x80IN!> \x9f\xa1\xf7ӷ?\xdf\xc2Wc\x1e\x9c\xb8JH\x8e\x1f\xe1[\xf2>\x9e7Z\xf1\xaf\x17\xf0\xe0\xf5\xe0(\x1d\xfbH7\x15\xa5\xb2(A\xc9\xea\x18\x12\xe0=\x82U5\xc2\x01\xab*\v\t\"\x87\x03;\x82ڞ\xb8\xa7\x15\x11\xa9&\x03͌;\x9b$F>\x9c7\x9ay\xd6Ԏ\xa7ًϢ\x9ed\xbd\x9f-\x03y\"'|\xb9\xf0\t\x9c\x18\x96^Wp\xe2\xb1\xc9\xd1Ht\xe8\x99\xc1Ua\x89\x0f\x05jg\x97j\x8ff/\xf0\xb0<(\xf3(\xe4.#ê\xd4\xed\xd2\xf7\xc1\x96_\xf8?\xd7\x12\xee\xdbT\x9fJ\xbd\a\xf2\xf9X@\xb7\xdb\xe55\x1ch\xb3\xfb\xa7Ǯ\x93|\xd8Ąs\n\x93l\xfeP\x8a\xa2lk\xbd\x81\xb7\xad\x19\x0f\xee\x98\xc9\xe3g\xb2\x1d\xe2sc\b\xa3c\x16\x9b\xb8\x19\x93\x9c\xfe\xb7\xc2:\x9a\xbf\x86\xb1\x8d\xf8$\xe7\xf2~\xfd\xe6sZT#\xae\xf1$'j\x980>f=VY\xcdt\x16v3\xa7jQLvS\x0e\xbf\xe6$\xa4\xad@s!\xfd{7\xda\xdc&\xa8\x89j\xa0\xdb\xf3\xac\xfcӱ]\"\xe1\x1bv\xb1ϥ\x85g\xf9uY\x15\x1e\xd8\xce\x023\b\fj\xa6I#\x1e\U0005814cC3A\xe9\x02e\x04]c\x10\x98\xd6\x15\xc5\xf4\x90E$ \xc6\xfc7\xb2\x87YO\xdf)\x86$E\xd9v\xa56蜐\x9f\x919\xef'\x88\xfc\xbe\x8c\xeazv\x85\x92[\xb1\x8b\xdd\xce9\xa7dSU,\xafp\x05\xce4\xa7j\xae\xb3\x8c|\xa0-\xe7\xe9\x7f?\xd8\xdaj\xf8\x85\x06c\x9a\xaaQ\xdbqN\fʦ\x9e\xa3\x92\xc1\xa3҂%\xe6\rZ7\xb3^Z\xb8\xbd}\x8e\x8d\x05\xa5\xbc\xa6\xe4\x0eep\xaa*\x8d\x8a\x1e\x13\xf8\xb62u\xaa\xaf\xf2\x92B\x7f\x86o\xa0\xea\x9eʑ1\xdeY\xba]2\xd93\xe8.\xb7SZ\xf1\xc9\xcc\xd8\rN\x16\x03}O\xea!\xf9\x86\xf63\xbaH\xe1\x91-\xf24\x04G\xd7>\xbdQ\xda}m\x1f\x89\n;\xed\x90w\x8d\xfek$\xfej\n\xc4\xf7~\r\x8fF!j\xecJ\xff\xb1\xaf\v\xc5]\x8e\xa0\rj\x96\xec\n\x81\xef\xdc[\xdf\xc2\xfc\xd2\x06`\xc2Bc\x91\xfb\x0e\xda\xec\xee\x19\x84\xf6\x9d\x893\x87\x19\x9d\xbf\xce_\xa4\x1bS\xe1\xcdo\xf8RrU\x97j\x0ef\xceB\xd6r\xcd?ᴏ\x8d)\x8e\xf5\xe0:~\x05h\xc8}\x15JE\xf2\x96\x89\n9\xb4o\xdcτ\x92\xe3\x96R\x9c\xe0\xe3\xda>ND\xeft\xfdw^\x92\t&\xcc\x13\x9e?R\x98ӧ\xc6\v\x92\\O\xb6C\xa9\xaa(/\xd9\xd49\x1a2L\xff\xe0\t\x12\x0fT\xf7\x17%\x93\xbb\xa4\x93k\x1f\xec\x10*f\x1d\xe4\xfd'\x03)\xe2\x87/\xa6Sʆ/\x9c\xfd\xa8\xd1Z\xb6\xbb\xe4\xce\x7f\f\xbbB\xe7.\x1e\x01\x96\xabƥ\xed\xf7K\x1b]\xd0\xf3\xba\x87ɦ\xd8\xd8\xfb1W\xb6\xcen\xdbT\x95?3\x8c\x1b\xfd\xa7\x1d\x1e\xab\x1c\xd3\x19\xff\x99\xd6\xe19\x04Kf/\xb1\xea\x9e\xf6\xa4\xfcq\x17\xec\xce:d8\x13\xd8\xdf\xe2!1\xdb\xfa\xb9\xc4\xd2}t\x9e\x89\xa5ٗ\x18\xc3\xc5\xd0\x1bOq\xae]K\xc2\xec\xbesH\xac}\xef\xbdʳ\x98\x1d\xf1\xbb\xc6mv\xbd\xf5\xde\xf2\xfcg\v3\xfb\x1b\xe7\x1fL\xf2\xa1\xd8RM\x88\xfe|\xabA\x01Rl\xa4\xc5'\x01ﺜ\x02.\xac\xaeر\xa3ŗ~d\xaa\xe9\xf7\x91ޢZ\x8f\xa9\xf1T*{\xbe\xc3\xdd}-\x92\xaek\xcf\xfb\v\xb8\xe03\xfc\xba:\xed\f\x7f\x8f\x1bΤ\xe2V2mK\xe5\xd6o.\xa8Ʀ\xdb\xd8\xdac_V\xfa\xc0\xe2\x9f\xde⦨\n\tT{\xef\xf6,g1\xfex\xe8\x1a-ތ \\\x88\xfb\xf1[\xa6Ttݐ\x17 \a\xe4\x1fv\xef\xa6_p\xbc\xe8\x82\fs\xb1A\x1e\xe2Q\xaa\xab\xa0\xa4\xaf#\x94\x99\xbf\xb2\xc3\xc5@>&\xe8ό\xe1Iu\x9aMz\xcc\xf9\x00v|\xd3\x1c\xce4y\xf7ܿ\x82_\x7f\xbb\xf9_\x00\x00\x00\xff\xff\xfc¬w\xb0(\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z͒\x1b\xb7\x11\xbe\xefSt\xad\x0f\xb6\xab4d\xa4$\xae\x14o\xd2*Nmbo\xb6DI\x17\x97\x0f\xe0\xa09\x03s\x06\x80\x01\f\xb9\xb4\xe3wO5\x80\x19\xce\x0fH.\xa9\x925\x17i\xf1\xd3\xf8\xf0u\xa3\xbb\xd1`\x96e7L\x8b\x8fh\xacPr\x01L\v|r(\xe9/;\xdb\xfc\xc3΄\x9ao_\xdel\x84\xe4\v\xb8k\xacS\xf5;\xb4\xaa19\xbeŵ\x90\xc2\t%ojt\x8c3\xc7\x167\x00LJ\xe5\x185[\xfa\x13 W\xd2\x19UUh\xb2\x02\xe5lӬpՈ\x8a\xa3\xf1\xc2ۥ\xb7\x7f\x99\xbd\xfcn\xf6\xf7\x1b\x00\xc9j\\\x80V|\xab\xaa\xa6F\x83\xd6)\x83v\xb6\xc5\n\x8d\x9a\tuc5\xe6$\xbc0\xaa\xd1\v8t\x84\xc9q\xe1\x00\xfaQ\xf1\x8f^λ \xc7wUº\xff$\xbb\x7f\x10\xd6\xf9!\xbaj\f\xab\x128|\xaf-\x95q\x0f\x87\xb52\xd0[\x13z\x84,\x9a\x8a\x99\xe9\xcc\x1b\x00\x9b+\x8d\v\xf0\x135ˑ\xdf\x00D\x06\xbc\xa0\f\x18\xe7\x9eSV=\x1a!\x1d\x9a;\x12!\xbbe8\xda\xdc\b\xed\xd3%\xb3\x18{\x03\xf9K\xdf\x11\x9bܞ0[g\x84,R(ދ\x1a\x817\xc6+\x9d\xf6\x9f#\xb8R\xd8)\xbc\x1d\xb3\x04\xd18\xbf\xf14\x18\xdfO\"\xadc\xb5\x1e\xa3\xeaM\r\xb08s\x98\x02u\xa7j]\xa1C\x0e\xab\xbd\xc3v+kej\xe6\x16 \xa4\xfb\xeeo\xc7\xf9\x88\x84\xcd\xfcԷJ\x0e\xc9yC\xad\xd0k\x0eHH[\x05\x9a$Cʱ\xeaS\x808\x12\xf0\xa67? \tr\xfb\xedg\xa1\x90\xe9\x81Z\x83+\x11ް|\xd3hX:eX\x81\xf0\x83ʃ\nw%\x1a\xf4#Va\x04\x9dm\x10\xa4;e\x92\xaaӘ\xcf\xc2\xd8(\xac\x955\xd2\xdfp\xa1\xcfb_\xb9A\x96\xb4\xaf\xd6I\xcd\xfc\b\xa1d\xda\xc8^\x17\xf8,\x03\xeb\x13)\x15\xc7\x1ek\x13\\\u00826*GkO\x18>\t\x19 y84\x9c\xa5\xa8D?\xa6\x05\xd4\xe8J1\x8e\x06\x9c\x82\x92I^aС3L\xdau\xb4\x8c\xa9\n\xdbi\xef\xf7z\b\xe5C+\xaf\xd73\xc1\x14\x86n_\x067\x98\x97X\xb3E\x1c\xab4\xca\u05cf\xf7\x1f\xff\xba\x1c4\x03Ѣ\xd18\xd1\xfa\xd1\xf0\xf5BR\xaf\x15\x86{\xfe_6\xe8\x03\xa0\x05\xc2,\xe0\x14\x9b\xd0z.\xa2\x7fE\x1e1\x05\x8e\x84\x05\x83ڠE\x19\xa2\x1553\tj\xf5\v\xe6n6\x12\xbdDCb\xc8\xed7\x15\xa7\x90\xb6E\xe3\xc0`\xae\n)~\xebd[\"\x9c\x16\xad\x98C\xeb\xfcA4\x92U\xb0eU\x83/\x80I>\x92\\\xb3=\x18\xa45\xa1\x91=y~\x82\x1d\xe3\xf8\xd1[\x93\\\xab\x05\x94\xcei\xbb\x98\xcf\v\xe1\xda@\x9d\xab\xban\xa4p\xfb\xb9\x8f\xb9b\xd58e\xec\x9c\xe3\x16\xab\xb9\x15E\xc6L^\n\x87\xb9k\fΙ\x16\x99߈\xf4\xc1zV\xf3\xafL\f\xedv\xb0\xecD\xd1\xe1\xf3\xe1\xf5\x02\xf5P\xbc\xa5\x93\xc0\xa2\xa8\xb0Ń\x16\xa8\x89\xa8{\xf7\xcf\xe5{h\x91\x04M\x05\xa5\x1c\x86Nxi\xf5Cl\n\xb9&çyk\xa3j/\x13%\xd7JH\xe7\xff\xc8+\x81ҁmV\xb5pd\x06\xbf6h\x1d\xa9n,\xf6\xce'3\xb0\xa2\x03E~\x80\x8f\a\xdcK\xb8c5Vw\xcc⟬+Ҋ\xcdH\t\xcf\xd2V?E\x1b\x0f\x0e\xf4\xf6:\xda\xfc\xea\x88j\xc7\xfem\xa91'\xcd\x12\xb94U\xacE\x8c$ke\x80M\xc6\x0f\x99J\xbb\x00\xfa\x92\x11e<\xe8\x9c\xd9\xd1\xf7&%\xa8E,{\x8e<\xc6;\x1b\x03U5\fT\xfdo\x12#\rje\x85Sf\x7f\x88\x94c\x938\xaa\x1d\xfar&s\xac\xae\xd9ޝ\x9f\tBr\xe2\x1d;\x93&g\x14\xa4z\xa0J\x16\x8a\x0e\xd9D\x1dp\xefh\x1cٹE\x97ެ<\x1aل\x84C\x8e\t\xfd\\r\xbc\xed\x95R\x15\xb21\x9bZ\xf13\x9b~T\xd1q\x18\\\xa3A\x1f\xff\x83\x9b\xd5\xca;cDŽl\xddGH\xb9\xc1\xa9\xc4>V\xe4n\x8e\xa9\xe6\xb8\x1d\u0089\x90\x94\x04\xfc\xfa\xf1\xbe\r;\xadeE\xe8\x93\xc8\xd2\xe7'i\x16\xf4\xad\x05V\xdc\a\xea\xf3k'-\x84\xbe\xfbu\x00\xe1}\xafS\xc0@\v\xccq\x10\xf7@H\xeb\x90\xf1\xd8H\xee\xc6`\xec{\x11|\xeaQ\x90\x10\xaeE1>\x92J\x80\x91\x8f\x17\x1c\xfe\xbd\xfc\xef\xc3\xfc_*\xec\x03XN\x99\x90\xbf\xab`\x8dҽ\xe8\xee+\x1c\xad0\xc8\xe9\xf6\x81\xb3\x9aI\xb1F\xebfQ\x1a\x1a\xfbӫ\x9f\xd3\xfc\x01|\xaf\f\xe0\x13\xa3\xa4\xff\x05\x88\xc0y\x176Z\xab\x116l\xbc\x93\b;\xe1J\x0fT+\x1e7\xb8\xf3[plC'&l\xa1A\xa8\xc4\x06\xd3\xec\x03\xdc\xfa\xe4\xe9\x00\xf3wr)\x7f\xdc\xc27\xc1I\xdcҟ\xb7\x01F\x97 \xf4\xbd\xce\x01\x8e+\x99\x03gDQ\xe0!ў\x18\v\x054\n\x05߂2\xb4W\xa9z\"\xbc`\xd2Sp\xc4\xc8'\xf0~z\xf5\xf3-|3\xe4\xe0\xc8RBr|\x82Wt\xc6=7Z\xf1og\xf0\xde\xdb\xc1^:\xf6D+奲(A\xc9j\x1f\xf2\xcd-\x82U5\xc2\x0e\xab*\v\xa9\x18\x87\x1dۃZ\x1fY\xa7U\x11\x99&\x03͌;\x99\x8eE\x1eN\x1f\x9ai~\xd2~\xcf;/>_y\xd6\xe9\xfdb\xb1\xfe\x99L\xf8\xc4\xfc\x13\x98\xe8_u\xae`bӬ\xd0Ht\xe8\xc9\xe0*\xb7\xc4C\x8e\xdaٹڢ\xd9\n\xdc\xcdw\xcal\x84,22\xc6,h\xdd\xce}1g\xfe\x95\xff\xe7ڍ\xfb:˧\xee\xde\v\xf9r\x14\xd0\xeav~\r\x03m\x1e\xfd\xfc\xd8u\x94\x87e\xcc\xec\xc62\xe9\xcc\xefJ\x91\x97\xed\xad\xaa\xe7mkƃ;fr\xff\x85\xce\x0e\xf1\xdc\x18B\xb4\xcfb)2c\x92\xd3\xff\xad\xb0\x8eگ!\xb6\x11\x9f\xe4\\>ܿ\xfd\x92'\xaa\x11\xd7x\x92#\xb7\x85\xf0=e\aTY\xcdt\x16F3\xa7j\x91\x8fFS\xae|\xcfIIk\x81\xe6L\xf6\xf7n0\xb8\xcd\xda\x13Yw7梴\xdbJ\xa6m\xa9\xdc\xfd\xdb38\x96\xdd\xc0\x16\xc3A\x871\xe9leё8\x99k>\x03\xcfR\xfc\x96p[ID4\xb4\xc5T\xa9B\xe4\xac\x02\xeb\xdbd,VF\x98\xad\xec)\xa0T=r\f\xb7_U\xec\xe1\xf5\xbe\xe0ḧ\x1dB\x1e\x8enQ+#\n!Yu\xf0\xd8\xfe\xea(Y\xcd\xfc_\t[\xad\x99\xd6B\x16\x17q\xdbַ\x96蜐E\"\xd1\xef\x97\xdfO]\aN\x9e\x93\xf3.\xe0\xc3\b\b0\x83\xc0hO\xa4\xaa\r\uecd0uj&(e\xa4\xac0\xa6\xd6+\x04\xa6uEy]\xc8$S\xbe\xa9\xad\xd6\xe5J\xaeE\x11+\xa7S\xa6dSUlU\xe1\x02\x9ci\x8e]ڒǽ_(<\xa3\xf1\x0f\xbd\xa1\xad\xbaϔ*ӻ\x1a\x140\xa7\x9bA\xd9\xd4S(\x19l\x94\x16,\xd1N\x87s☨\xe3\xf6\xf6\x12\x93\n'\xff\f\a\xe1Μ*8D\xc7\x11\xaf!\xf1\x8a\x1d\xdcG:\x9a_\xeaP\f\xfe\xdaНj\x880K\xd7VFc\xb4\xe27c\xd2\xfa\xbex\xd4y\xf0\xa4\xe3\x8e\xe1\xa1\x1f\xf5\x06\n\x9eU\x96\xf2\x85\xf2K\nS\xe19,\xf2\x1e\xd2\x00\xd7>\x92\xd1\x05\xe3\xea\xd2\x14\xdda\xb5C\u07bd!\\S\xb7y=\x16\xe2\vʆ\xc7C\"j\xec\x8a\x1c\xd1N̡\xec\x12B\x8c6\xa8Y\xd2\"\xc0?\nX_\x18\xfd\xda\x06i\xc2Bc\x91{\xdf:Y\xfchL\xe0\xccaF\xf3\xafs \xe9bWx\x9e\xeb\xbf\xc2\\U\xf9\x9a\x8a\x99r\xc8:\xda\xfc\xfbP\xfb0\x98\xa2\xec \xaf#,\x88C\xee\xafܠ$\xac\x99\xa8\x90C\xf7,}1\xf3\t\xd0\xd3d\xecs\x92_\xa3\xb5\xac8\xe7\xb4~\f\xa3B\xe5-N\x01\xb6R\x8d;b\x95_\xdbx\xb4.\x8a\xc9R\xf1sH\x1e\x14\xf70\xe4\xf1'\xb7)\x9a\x84Z\xfa\xcfp\x17a\xf4E\xcdsEJ\x1a\x93r5\x1d\xe4Ӿ\x06Nİ\a\xdc%Z\xdb\x13\x9c\xe8z\x8cn!\xd15\xf9=@\xbf3T\x92S9Mۗ\x94\xd9=\xb6'\xfa\xbe\xf7\xc7\xe5\"\xb6#\xbek\x1cBW\x87.U\xd5\xfa\x00\xffH.\x9bz\x85\x86T\xb1Je\xc4\xc0$\xefk.UL\xe8$\xb4a8\x88\x8a\xf5\xb0X@\xf7\xa7\xdc)\xe0\xc2\xea\x8a\xed\xbb\xcd\xf8\x1b\x1c\x1d\xe9\xf4s\xc2\xe1\\\xb5\xbe\x8a\"ϑ\xbc\xedt\xa5\xba\xfb\xd1B\xfa~z:Ӈ3پ\xef\xef~\x8c\xf0yV8\x91w\x0e\x7f\x1cr\x8d\x81,\a\x12\xce\x05\x8b\xf8c\x95\xcb}\xfcp\x99?ӽ'ٛ4z\xe4\xbc';>y\xf5[\x9aU\xf7\x1e\xbc\x80\xdf\xff\xb8\xf9\x7f\x00\x00\x00\xff\xffϡa\xa5-&\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ks\xdc8sw\xff\n\x94sؤJ#ǕG\xa5ts\xb4v\xac|\xdf\xca*\xc9\xf1\x9e1d\xcf\x10\x9f@\x80\v\x80\x1a\xcf&\xf9\xef)4\x1e|\fHbF\x1a\xednjyQ\x89\x04\x1a@\xbf\xbb\xd1\xc0\xacV\xab7\xb4a\xdf@i&\xc5\x15\xa1\r\x83\xef\x06\x84\xfdO_>\xfe\x9b\xbed\xf2\xdd\xd3\xfb7\x8fL\x94W\xe4\xba\xd5F\xd6\xf7\xa0e\xab\n\xf8\x116L0äxS\x83\xa1%5\xf4\xea\r!T\bi\xa8}\xad\xed\xbf\x84\x14R\x18%9\a\xb5ڂ\xb8|lװn\x19/A!\xf00\xf4\xd3?^\xbe\xff\xd7\xcb\x7fyC\x88\xa05\\\x11\x05\xdaH\x05\xfa\xf2\t8(y\xc9\xe4\x1b\xdd@aan\x95l\x9b+\xd2}p}\xfcxn\xae\xf7\xae;\xbe\xe1L\x9b\xbf\xf4\xdf\xfe\x95i\x83_\x1a\xde*ʻ\xc1𥮤2\xb7\x1d\xc0\x15Q\xbe\xb9fb\xdbr\xaab\x877\x84\xe8B6pE\xb0}C\v(\xdf\x10\xe2\x17\x85\xfdW~=O\xef\x1d\x88\xa2\x82\x9a:\xc0\x84\xc8\x06ć\xbb\x9bo\xff\xf40xMH\t\xbaP\xac1\x88\x9a\xffY\xc5\xf7$,\x810M(\xf9\x86(\xb0\xb3A\x92\x10SQC\x144\n4\b\xa3\x89\xa9\x80Ц\xe1\xac@\x8a\x10\xb9\xe9A\n\xbd4\xd9(Yw\xd0ִxl\x1bb$\xa1\xc4P\xb5\x05C\xfeҮA\t0\xa0I\xc1[m@]F@\x8d\x92\r(\xc3\x02\xba\xdc\xd3\xe3\xaa\xde۹\x85\xd9\xc7\xe2\xc2\xf5\"\xa5e/pK\xf0\xf8\x84ң\x8f\xc8\r1\x15\xd3\xddR\xc3\xf2\b\x15D\xae\xff\x06\x85\xb9\x1c\x81~\x00e\xc1X궼\xb4\\\xf9\x04\xca\"\xab\x90[\xc1~\x8d\xb0\xb5]\xb8\x1d\x94S\x03\xda\x10&\f(A9y\xa2\xbc\x85\vBE9\x82\\\xd3=Q`\xc7$\xad\xe8\xc1\xc3\x0ez<\x8f\x9f\x90xb#\xafHeL\xa3\xaf\u07bd\xdb2\x13d\xad\x90u\xdd\nf\xf6\xefPlغ5R\xe9w%<\x01\x7f\xa7\xd9vEUQ1\x03\x85i\x15\xbc\xa3\r[\xe1B\x04\xca\xdbe]\xfe]$\xea`X\xb3\xb7<\xaa\x8dbb\xdb\xfb\x80\xa2r\x04y\xac\x109\xc6s\xa0\xdc\x12;*\xd8W\x16u\xf7\x1f\x1f\xbe\xf6\x99\x92iO\x94\x1eoN\xd1\xc7b\x93\x89\r(\xd7\x0fY\xd3\xc2\x04Q6\x92\t\x83\xff\x14\x9c\x810D\xb7\xeb\x9a\x19\xcb\x06\xbf\xb4\xa0-\xbf\xcb1\xd8k\xd4Gd\r\xa4mJj\xa0\x1c7\xb8\x11\xe4\x9a\xd6\xc0\xaf\xa9\x86W\xa6\x95\xa5\x8a^Y\"dQ\xab\xafeǍ\x1dz{\x1f\x82\xae\x9c \xad\xd7\"\x0f\r\x14\x03I\xb3\xdd\xd8&\xa8\x8b\x8dT\x03%c\xbb\fq\x94\x16~\xfb8-b\xd5\xe2\xf8\xcb\x12\x97\xd9\xe7\xdfco\xcbovf\xad`\xbf\xb4\x80\xcaԉ?\x1c\xea+\xd5S\xfa\xc3Dzј\xba\x93\x88\xb6\x0f|/x[B\x19\xf5\xfa\xc1\x02s\x96\xf1\xf1\x00\n\x9aCʄ\x15\"k\x97\xecZD\xf7\x15\x158U@\x844\txL8x\x84\t\xc4@\x92&\xd8\xd0@\x9d\x98\xf1\xec\x92\t\x11-\xe7t\xcd\xe1\x8a\x18\xd5\x1e\xa2\xd1\xf5\xa5J\xd1\xfd\x04\xb6\x82o\xf0,dE ^\xd5pV ɣBA|\xfdqQŴU\x94a\x95w\x92\xb3b\xbf\x80\xaf\x8f\xc9NAZ\xbd\xec\xfa\x15\x925T\xf4\x89I\x95\x12\x03\xa9\xb0iϞwjZZ-遌m\\悓Ȫ\xa4|\\b\x88϶Mg\x1dH\x81\xaef\\\x8a\xa7\xb6\xb7\xddk \xf0\x1d\x8a\xd6$\xa6IH٢i\x92\x8a4R\x9bi\xbaO\xab.\xd2w\x8eR\x1fg\x98\xe6`eIVw\x8fW\u0081\xa8\x16\a\x03\x85,\x05\xd8eԖ\xa8][%[\xd7v\x12)dM5\x94D\x8aɑ\x91]Z\x0eڏU\"gtz\xe8\xa2[?z<\x84\xd35p\xa2\x81Ca\xa4:Df\x0eJݓ\xa3X'P\x99ЦC\t\xe8\x160\x03\x92XN\xdfU\xac\xa8\x9c\x87a\xd9\x13\xe1\x90R\x82\xb6\xda\x04]\xe6\xfd\xd4\"\xc9\x12\xf9\xfd sڣ{\x16\xc4j\f/\xa5Q\xba'C\rwO\x12\xb5\x9d\xee=\xd0-\xfe\xbd\x91\xb3\xcb\xfe\xff\x89\xd8`LN`\xda\x19\xf9'\xe8~f\xf3\xf4$\xdfb\x84\a\xfa\x92\xdcl\bԍ\xd9_\x10f\xc2\xdb%I\xa0\x9c\xf7\xc6\xf8\x03\xd3\xe6x\xa6\xcf$M\x8eL\x9c\x890q\x88? ]\xd0d{\x84=\x82Igs\x0e\x9f\\np\xcf#$\\\xff\xd43\xc0\xa1\x9d\x93\x0f\x8b\x1d\x9e\xec\vD\x04\xc6\xf0\xb9l\xe0\x1e/\n\x89\xdcI\xfa\xc9\xd4%\xe1\t\xb8?a\x99Y\xac\xd2\x1f\xa3\x9f\xfaD\x0e\xf8A;ZZ\x89\xa9\x98\xcfij@\x99\xc9%\xa8{\xbeQ\xce\xca8\x90\x93\x91\x1bqAn\xa5\xb1\x7f0@\xd3\xc8(?Jз\xd2\xe0\x9b\xb3`\xd4M\xfc\x9c\xf8t#\xa0\xa0\t\xa7\xe5-\xc2\xfa9?g\xd3,\xb7E\xdc3Mn\x84\x8dW\x1cJ2\x87\xc2\xf4\xae\x1b\xce\rT\xb7\x1a\xd3uB\x8a\x15\xda\xcc\xe4H\x1e\xdfR\r\xd0\xfd\xecA\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\xef\xabǘ/XY\x93\xb3\xf2\x10\x8c\xac3p\xe0uw\xb9\xbc\x9e\x95\x95ٌV\x81\x13\x16\x9bN$G\xa7\x9b\xe6 \xe5\x19\xe8@+\x8e.\xce\"uiY\xe2\xe6\x1a\xe5wGX\x94#x\xe1X\xd5Л\xbb3\xc15m\xacZ\xf8okiQ\x9a\xfe\x974\x94)}I>\xe0N\x19\x87\xc17\x9f\x87\xeb\x81\xc9\x18\xb2\xb1CY\xfey\xa2\xdc\xda~\xab\xc0\x05\x01\xee<\x01\xb99\xf0\x8b.Ȯ\x92ڙ\xed\r\x03\x8e\xfb\x15o\x1fa\xff\xf6\xc2\x0e\xbf8d_ɼ\xbd\x11o\x9d\x0fq\xa00\xa2\xc3!\x05ߓ\xb7\xf8\xed\xeds\\\xa9LN\xcdl6`њ6y\x1c*\x92\xc9\xfa\xee\x19pL?7\xdf%当=\xb7\xda,\x16m\xa46\x9f\xd3yÉ\xf9܅\x1eC\xcf8\x91c[\x8c\x18|\x1e-\xea{\xebDn\f(\x9fKt6 \xc4\x1fό\xccR\xbb2\xfd\xc9\xc6d \x8d\xf9]\x8b\xe0\x05nr\x1b79S<\xc6a\xb5x9\xd2\xdb\xff\xf8\xbd\x97ϴ\x92k\xff\xef/\xe4\xa5\x1d\xeaB\xd65\x1d\xefjfM\xf5\xda\xf5\f<\xed\x019\xea\xabm\x8b\xf2\x9ck\x91;\x1e\xc2\xfd\xcb\x1d3\x15\x13\x84\x06\xb5\x01\xca3\x14%\x8dL\xe5\xb0SOE5Y\x03\x88\x98\xa2\xff=\xb8\x125\x1378\x00y\x7f\x06\xd7#\xa2\xeb\x9c\xce\xeeu\xa4I\xa4||\xe1LV#K\xb2\xab@\xc1\x801\x0e\xf3\xee\xe8\xa9\niz)\x8b#\x1c\xd2F\x96?h\xb2aJ\x9b\xfe\x144iu.\xad\x8f$\x9f\x9d\xf7WV\x83l\xcd9\x11\xfc\xb1\x1bf\xb0\xd7\\\xd3\xef\xacnkBk\xd9:cnX\x1dwu=zw\x94\x99\xb8m\x85\xf9\x1b#-\t\x1a\x0e\x06\xc8\x1a6\xe9\xfd\xde\xd4SH\xa1Y\t*T)8\xb21i\x05sC\x19oS\xbbD\xa9\xe7\xd8\bX|T\xea\xa4\x00\xf8\x8b\xeb\xd9\xcb;Vr7DP\xe6\xdaq#\r\b\xdb\x10f\b\x88\xc2b\x1c\x94S\xc98\x84G\x06\xa2\x86\xe5\xea\xb9<\x05n\x1f\x10m\x9d\x87\x80\x15\n$\x13\xb3)\xb7~\xf3O\x94\xf1s\x90\xcdr\xde'\xa9\ue056\xa7\xe4h~\xeeu' t\xabp\xf3\xdf\xe9\x8e\x1d\xe3ys\xb6\x94#\x9c\xb6\xa2\xa8\x00\x95\x90\x18\xea\x06\a\x9e\tm\x80\xe6\xf2\x82\xf5\x8aZ!\x98\xd8\xe6\xd1.;\x11\xda=\x0e\xd5k)9\xd0\xe9]\xc8\uec78~\x05M\xf4s7\xcc35QG\x04\xb7m\x8etȦ\xa8UZ\x84\x1a\x03u\xe3DN\x12Պ\xbeu9\x83\":&\f\xf7\xb3x\xc9\xf8\x9a\t\x96A\xdb\x01]o\x043}\xe7т8\xab\xf3h\a\x88\xee\xc0)\x19\xb6\x9b\x01\x00+\xa0!\x0e\xc1\xb9G\xae9\u0091\\\x03\xa1e\t\xa5\xcb]ZWć%\xae\xf0m\xa2\xb8!\xb9\xba\xe3=\xc1,ʆg\x10tb\x1eV=\xc1\xaa\x15\x8fB\xee\xc4\n\x83q}\xb4\x0e91K\xf5\xdc\xe1\xcd\xc9\xcahY\xbf\xe4\xab\xe9%-4\xe4\xd7|\x9e\n\xfe\xd3\x19\xb4L6\xdf\x1c\x95\xf0\x98\xe3\x82%\xbd\xe6\n\xb0'>.\xcebn\xfc\x99\xce~S\xfa\xda\x15K?\xab,\xee&\r\xaa\xe7\x14\xee*0\x15\xa8P\x9a\xbd\u0092\xf4rv\x87\xb4\v^b\x9d\x9ce\xaa\xe0\"\xbb\xf2\xcfQ\xe5\x1cF7-\xe7\x17\x96\xb7i˓ᰑ(b\x87\x9c\x95U?\x96\xf6\x18r\xaa/\xb2\xf1د\xb4\x18\xd6\x17\xc6*\x88P`(\xc3ȞƩ\xf5baio\x7f\x7fXN\x81\xf9\xbf0\xfd\u07fc\xf40\xa3R\"\x1f\x8d\xb9U\x9a\x11\x89\tX\t\x06롱\xab\xaf\xf0\xed|\xa1\xef\xef\v\xa7\x06\xea/\x8d\x97\x98I\x176\x03\xad\t8\xa3z\x13\xb4\x06\xadv\xae@\xb4\x03>gh\xfb\x7f(\xdc)\x88\x00&ů_+\b\xe2\xeb\xab\xf7\x99&\xffL*\xd9&\xaa\xfafP\xb6Pݱ\xbc\xe0A\xa1\x87\xdfP\x00C\x9f\xde_\x0e\xbf\x18\xe9\xcb>0\x8b\x96\x00\x84AQ\x97\x99e\xa2dO\xacl)\x0fR\u06dd!p\f\xd4\xf1Y\x02\x9aTD0\xee\x180\xf4\x1f0\x1c\xf9Ҹm\x99\xa3Uܼ/\x9aW\x1drrMȰ\xe6c\xc2\x1a\x1e\xbb}\xf1\"U\xb0\xbfI\xad\xc7\xf1\x15\x1e9\x91\xc4B5\xc7\t5\x1c\x99\xc5b\xcf\xdeoɩ\xd28&\xe6>[E\xc6\xcb\xd7ad\xe1g\xb9\xe6\xe2\x18윽\xbe\xe2\x15\xab*^\xa7\x96\"\xb3\x82\xe2\xe5J!\xf3\xa2ϓJ\x01\x96\x03\x96\xe9*\x88\xc5ڇg\x054'-i\xb1\xa6\xe1\x98J\x86E\xea\xe4\x89٫\xd5*\xbcZ\x85\xc2\xeb\xd6%\xccr\xd1\xec\xc7c*\x0fb\x9c\xf4\x13m\x1a&\xb6\x87L\x91\xcb:\xb3l\xb3\xcc2\xb7\xa3\x89\fx\xa6\x1f\xcet\xd1\xe1D\xe8\xeb\x8eK'\"ɐ\xb6d\xc2\xc8K\xf2A\xec=\xdc\x04\x9c^\xf8(\xa498\xc8f\xa7\xb5c\x9c\xf7Ok!\xd8yP\xfe̤\xa6\xb5\x9bՔ\xb7\x9f\xa4\xabT\x03\xa7\xfc\xa4\xc0\xf1\xcb\bF?;\xfa\x9a\x9e\x7f\xddr\xc3\x1a\x0e֣{be\xf2\f\x99\xa9`\x1f\x91\xfc7\x89'\xa4\xd6{\x84\xf4\xe5>\xca\xe2\xe5(\x88\xa1\x9a\xec\x80sBS\xdcq\xb0\xfc\u009dL.\xe4\n\x8f\x04Z\xf2\x06&\xf1\xe7\x99/\x9c\x14\xe310\xa4^\x9d\x80[P\x81\xa7\x9bub!\x93\xe60G\x8b\x1e\xf8\xe5.\xba\xc0w\xbf\xb4\xa0\xf6D>a\t\x83\xf7\u07ba\xb3\n^\xddh\x1bc\x06\x05\xe8\x95\xf1Ԧ\xc2A(\xd3)(\xf2A8_b<\x1f\xecc5_\x17\xaaYun\xa3\xb0\xe4\x18\x13݅\x8c\xbd\x13ݖ\xdc\xfeܢ\xfe\xf3\x06nLJn\x8b\xbeR\xbe?\xfb\x1b\x15\xeb\x9fR\xa4\x9f\xb7\x1d\xb4X\x94\x7f\xae@n)\x94\xcb\xf6^\xf3\x8a\xee\x8f\xdbD=c\x91\xfd9\x8a\xeb31\x95SL\x7f\x1c\x9e^\xa1x\xfeU\x8b\xe6_\xabX>\xbbH>k\x1f3{\xd3*w\x9b\xf1Ī\xef\xe5]\xf7\xf9\xa2\xf7\x8cb\xf7\x8c\x9d\xb4\xe5E\x9e\xb0\xbc\x8cb\xf6\xe3\x8a\xd83h\x96+\x8a\xafX\xac\xfe\x8aE\xea\xaf]\x9c\xbe\xc0Y\v\x9f\x8f+B?y\a&l\xf5\xdf\xca\x12\xee\xa42K\xc1\xc9ݸ}b'\xb5\x17\xb0I^\x12\x11\x9a&V\x89!\x86\x0f/N[Tz\xd33\xb8\xd3?\xc9\xd2\xcemi\x8f\xe5~\xd4\xfc\xe0\xac\xf2\x06\x14\bw\xcd\xc7\x7f>|\xb9\x8d\xf0S>\xaf\xf7\x8cG\xd7K8\x0f\xa6\xf4\xc8\xf1[s\xbe\x98\xc9a\v}\x80\x17\xde\x17\xa1\r\xfb\x0f\xbc\xef\xed\x19\xe9\xa0\x0fw7\b#\xf8ix\x81\\\xac\xa2\x88;\x96k\xb0\x16+\xa2jR,n6\x03\x88Ê\xdf\xfe5JP\xba+\xb3\x82\xc5d\xa1\xc6\xcb\n\xdeݍ\x9b\xc7\xd4(\x9f\xac\xd3(\xf6D:\x8e\xac\x98*W\rUf\x8fl\xa3/\x06s\bff.\x9d3\xa9X\x0f\xaf\x01K\xa27\xdc\xfe\x85{\x91\xfbf\xb8\xdb;\xc6\xdd)\xf3\x98>\x7f\xb2x\xf2\xe4\x05\xe71m\xb1W\x88\xa9\xc4\xebd\x81ɋ\xa5\xc9\xd417\x05%e`\xe1ڠ\x9ej\xa0\xe4Z\x8a\r\xdb\xfeD\x9b`F\x1c>'\x95\x85O\xd14\x16\xb4\x05養u\xb5ihwzPi,a%t.eU~B\xc8w\x01\xb0\x06\xb7\xbd\xed\xb4R\\B\x03j\xd5\xe5ۺی\xf6\xcd\xf4l\xf5\xc5(d\xf5\xb7\xdc\fj\x17\xac\x1a4\xa0\x84\xff\x96\x9a\xab/\xb8y\xc0z\x9b\xdet\xf7q\xb2\x16\x1bv\x86\x96q\xfc\xe0x9\xd1fT\xac\x93\x00>ʧt\x18\xdcHUS\x13$\x00\x13z\xd4\xe1\xddݚ\xf6\xd0@q9$\xf9\x9f:\xf9O\x9d\xfc\xa7N~Y\x9dl\x95\xdbݷ\x93R\xe1\xf7\xb1\xf7\xbc\xefI9\x8f\xe9\xff\x04\x18\xdb\x1f\xddO-h\xa3\xab\xc45x\xcf\xf3?\xf1\x86HCM\xfb\x9cE:\x00\x83u\xb2\xa2\xeay\x90;\b>fX6J+vKjp\xe0\xfe\xa4\x15\xe3\x17\xbd\xec\xed\xeb\x94\xe9d^\xb1u\xf2\xe5Z\x0e=\x13\xea\aw$\xacj;\xc4\xd4\t\x05:\x8b\xe1v\xc6\xc1\x8f\xf9\xc4B\xe6\xd5Ly\x06\xe3\x84\xeb\x98\x10_\xb9\xb8\"\xc9[\x9a2ob\xfaM\x11=\xa3\xd5tQA\xd9r8\xf5\x1eև^\xff\xe5\x9bX\xc3h\x19w\xb1Zd\xf7\f\xb4\xf5\xb0\x86w\xbezJx\xc8}JN\x05ᘰqW>\x16\xeev\xe0\xa2\x00\xad7-\x0f\x95\xa3\x85\x02j\xa0\f͙\x8e3>\xaa\xf6\xb1m\xb8\xa4%(\xe7\x92-\xa0\xf5\xbf\x06\x8dG<[\xe0\xcbVu\xd7\xed\xce^U\xfa,\xcd\xd5PE9\a\xfe\x89q\xd0?ʝ\xb0\xf3\xca\x10ȻT\xbf\xdeY٢U֬\xef\x89h\xeb5(\xa2\xc1\x98\xe9\x04\xdeF\xaa\xf9S+\x0e\xefL\x18\xd8B*\xe7\xb9S\xcc\xc0CC\x95\x06\x9cQ\xc6\n~\x1euq\x19\xc1\r\xa7[W\x9e\\\xb2\x82\x1a\x88\x06\x18G\x98\x9a>\xf6\xd7\b\x8b\xef\xb1ZTNlDd\v\xf5\xd41\xb9I\xb1\x9e\xba\xf29a\xaa\x93\x97>;\x8b\\\xd0\xc6\xe0\xa1D\xa4#\x12\xd1x\x18x\x91\xfa\xe8\xde\xe7\x01\xd8iN\xf3GK|\x11\xb36\xb4ND\t\xcbz\xe7\xfa\x10\f^ծ\xca^-t\xff\xd2\xdbX\xf4LvT\xc7\x03.I\u07fb\x83\xed\xc0\xa0\xabnACI\xe0\t\x04\xb1\xa2H\x19\x87r\x8eS\xbf\xe2\xe6\x9ez\x02\xf5\x83\x8ep\xb0:۲\xf8\x83\xa1\xcaĩ\x1f\xfa1.\x86\xbb\"%5\xb0\xb2\xbdOs\xdd\xd2WW+ub\x89\x06\x9e6\xf6\xe2Q\x84\xa3\x90\xd6\xfa\xb93\xc25hM\xb7!1\xb8\x03\x05d\v\xc2\xe2=\xee\xf7$=\xa6p\xcc\xda\x1b\x8bAb\x80\x16\xa6\xa5~\x00\xe7\xc2Ŋ\x96pg\x10\x9cC\x1d^1\xe2\f:\x9f\xaa3\x18\xdc`D\xb4\xc5\xde)ʄ85v3\x1dv癚\xaf\x11ʔz\xf4\xeb\x1b\xfc8\x82/z\xf1\x8d,ي\x8a\x8a\xed\xe4!\xe3J\xc9v[\x05ޜr\x88H\xd9b\xe4ܠ*\xd0\xe1ǜL\xabD\xaf\x90\xc2\u05fdMi\xe98\xddi\x1f\xe5\x19\x8aZu\x87\r;U5c\U000f3cc4\x13\x10\x17m\x7f\x02\"\xd5{Q\xcc\x1e\x8b<ܣ:ʵL\"!j\xe3\x17CB\x848\x85\x84\xbe/\xd1E<\xbf\x1b\x8cL\xf9('\xa2cމ\xc1%\u0383Z^t\xdf\t\x1a\xba;ǡC\x0f\x82\xbf\x93\xd2n\x03\b\xc7D\xbe8v:\xee\xfd\xfdF\xacO\xd1\xdb\xfaxr\xec\xfam\x04ct,\xddF\xb1\xdd0!\xde\xfc{\xb6Iɋ\xfbż5\x87\x7f8\xf8\xfa\xca\xc7\xcbwT\t&\xb6'a\xe4g\xdf7\x11\xcf{\xb0\xe7\x8c\xe8\xc3\xcc_,\xa6O\x9a\xa5\x83\x97\xc8\xe0e\x0f\xcf~$\xff\xe6\xff\x02\x00\x00\xff\xffJ\xb7g~\xf1r\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfa\x15\x84\xeea?B\xdd^\xc7}ą\xde|\xb2gO\xb1\x1e[ai\xf4\xbctU\xb6\x9aQ\x15\xd4\x00\xd5r\xdf\xde\xfe\xf7\x8dL\xa0\xbe\xba\xe8\xa2Z-ygǼت\x86$\xc9L\xf2\x03\x12X,\x16g\xbc\x12\xf7\xa0\x8dP\xf2\x92\xf1J\xc0W\v\x12\xff2\xcb\xc7\xff6K\xa1\xdelߞ=\n\x99_\xb2\xab\xdaXU~\x01\xa3j\x9d\xc1{X\v)\xacP\xf2\xac\x04\xcbsn\xf9\xe5\x19c\\Je9~6\xf8'c\x99\x92V\xab\xa2\x00\xbdx\x00\xb9|\xacW\xb0\xaaE\x91\x83&\xe0\xa1\xebퟖo\xffk\xf9\x9fg\x8cI^\xc2%3\xd9\x06\xf2\xba\x00\xb3\xdcB\x01Z-\x85:3\x15d\b\xf4A\xab\xba\xbad\xed\x0f\xae\x91\xef\xd0!{\xeb\xdbӧB\x18\xfb\x97\xde\xe7\x8f\xc2X\xfa\xa9*j͋N\x7f\xf4\xd5l\x94\xb6\x9fZ\x98\v\xf7\xbb\xfbMȇ\xba\xe0\xbamsƘ\xc9T\x05\x97\x8c\x9aT<\xa3\xaa~l\x04b\xc1x\x9e\x13\xb5xq\xa3\x85\xb4\xa0\xafTQ\x97\xb2\xe9 \a\x93iQY\xa2ƭ\xe5\xb66L\xad\x99\xdd@\xb7\x1f,?\x1b%o\xb8\xdd\\\xb2\xa5\xa1z\xcbj\xc3M\xf8Ց\xcf\x01\xf0\x9f\xec\x0eq3V\v\xf90\xd6\xdb;v\xa5\x95d\xf0\xb5\xd2`\x10e\x96\x13s\xe5\x03{ڀdV1]KB\xe5\x7fx\xf6XW#\x88T\x90-\axzL\xfa\x1f\xa7p\xb9\xdb\x00+\xb8\xb1̊\x12\x18\xf7\x1d\xb2'n\b\x87\xb5\xd2\xccn\x84\x99\xa6\t\x02\xe9a\xeb\xd0\xf98\xfc\xec\x10ʹ\x05\x8fN\aT\x10\xece\xa6\x81d\xfaN\x94`,/\xfb0\xdf=@\x020\"Q\xc5k\xe3\xe5(\xb4\xbe\xe9~r\x00VJ\x15\xc0\xe5Y[i\xfb\xd6\xc9^\xb6\x81\x92_\xfaʪ\x02\xf9\xee\xe6\xfa\xfe\xdfo{\x9fY\x9f\xa2\xff\xbfh\xbe\xb3\x86\x1bL\x18\xc6\xd9=\xcd \xa6\xfd\x94fv\xc3-Ӏb\x00\xd2b\x8dJ\xc3\"\x90:gJw@U\xa0\x85\xcaE\x16XD\x8d\xcdF\xd5E\xceV\x80\xdcZ6\xb5+\xad*\xd0V\x84\xf9\xe4JG\xf5t\xbe\x1eB\x1f\v\x8eصrb\n\x86$\xd3\xcf6\xc8=\x91\xdc\xe4\x11\xa6\x1d\x0fq\x10?s\xc9\xd4\xeag\xc8\xecr\x00\xfa\x164\x82\t\xa3Ȕ܂F\x8ad\xeaA\x8a\xffk`\x1b\x9c\x12\x96$Ղ\xb1\x8c\xe6\xb3\xe4\x05\xdb\xf2\xa2\x86\v\xc6e>\x80\\\xf2\x1dӀ}\xb2Zv\xe0Q\x033\xc4\xe3G\xa5\x81\t\xb9V\x97lcme.\u07fcy\x106(\xe4L\x95e-\x85ݽ!\xdd*V\xb5Uڼ\xc9a\v\xc5\x1b#\x1e\x16\\g\x1ba!\xb3\xb5\x867\xbc\x12\v\x1a\x88$\xa5\xbc,\xf3\x7f\v\xfc6\xbdn\xf7f\xa6+\xa4Ng\xb0\a\xf5\xac\x93.\a\xca\r\xb1\xe5\x02~B\xd2}\xf9p{ו\xd5\xc7\xd7\xf9G\xf1\xfd-\x11=X\x9d#\x85}B\x930\xda/H\x9e\x0fQ\xd2#\xc5\x05\x98eguN\xd8\xf05\x85\xa1=\xffqo+e\x8f(\xbf.\xde\x1d7af\xb0nrN\xbd,\xe3\x9an\xfeE\xf8F&\xeb\xd6[\xacY<\xfb\xd8myA\xbb\x02\x9e!\xf9\x05[\x8b\xc2\x029US\x88\xb2\x19\x9c;%\x81R-0\xa3Mb\x9bm>4{G\t-\x06\xb4\x1a\x02p\x0ez\x88r\x88\a\t Y\xe3ZЦ\xa9\xd0P\xd2f,E\x92\xdd/\xe4\n\xbe\xfb\xf4>\x1e{vK\xa2\xa4\xee\r*aҺ\xf2n\xe0\x18uq\xf5\xa1J\xf8\x85\xfc\xb5&\x10t\x9b\xf0\x17\x8c\xb3G\xd89\x17\x8bK\x86|\xe3\xa1r\"\n\x1a(#\x804\xc5#\xec\b\xd4\xf8\x16\xffx\x99#-\xae<\xc2Ȯ_\xac\xf4\xe8\x8a\xf8\xf9\xbd\x14G7\xfc@\x84I\x99Mmi\x88\xea\xa7\xcf\xc8\x06{\xbc\xcc\xd0K\xa1\x04\xbe\x1c9\xecdq\xea\xf6\xd5O\x8ay\x84\xdd\xef\x8c\xe35β\x8d\xa0M'N\xab7j=\x8b\xe1\xae\xdc\xf3B\xe4Mgn^]\xcb\v\xf6IY\xfc\xe7\xc3Wa\xb0c\x99\xb3\xf7\n\xcc'e\xe9ˋR\xd9\r\xe25h\xecz\xa2\t*\x9d%A\"v\x93G\x9c-EAm\xf8!\f\xbb\x96\x18\x929\x12\xcd\xe8\x8er\x85\\\x97\xae\xb3\xb26\xb4\xd5*\x95\\\xb8e\xb1\xb1\xde<\x0f\x94\xee\xb1\xe0$\x1d\xfbN\xef\xd0\x18\xb9_\\\xd6R\xc13\xc8\xc3\x16\x1d\xa5\xd3p\v\x0f\"\x9b\xd1g\t\xfa\x01X\x85f!]Zf(j?\xb2\xf9\xe2\x95\xee9t\xcb\xd7\xc5c\xbd\x02-\xc1\x82Y\xa0Y[x(V\x95\x89t\xf16a$\xe7d\xac,p\xae'\xd6\fҒT=\x92\x91s\xb8z*\xb1\x9eI&\xf2\"\xc8\xedJ\x92\x82nb\xeb<\xeb5Sn\x8eQ1\x9d\xb18\x17\xa0䴵\xf67\xb4\xf44\x1b\xff\xce*.\xb4Y\xb2w\x94\xd9[@\xef7\xbf0\xd9\x01\x93\xd8mE\xab\xec\xbf\xd4b\xcb\v\xf4?\xd0@H\x06\x85\xf3F\xd4z\xcfW\xbb`O\x1be\x9c\xdb\xd0lڝ?\xc2\xce\xed('u\xdbUX\xe7\xd7\xf2\xdc\xf92{\x8a\xa7q|\x94,v\xec\x9c~;\x7f\xae{7C\xa2gT\xed\x89rɫtI\xa6\xbc\xd99\x81\x06\x06\xeb\xc1!\xc2\xc6M\x02)\x06\bS\x14H\x16\xe5J\x99H\xb2H\x04\xad\x04A\xbfQƺuȞ\xbf?\xbaP\xa9\xc2\xe2$\xe3k\v\x9a\x19\xabtH\xc9Dş\xb2\x14\xdf-w\x1b0\xe0\xf7\xa1\xfc\xa2\xa7\x03\x8cQ\xecy\xab\x1b\x9cU9w{a\xd4\x11\xcf\xc8{\xa2\xb6\x95V\x19\x98h^D[\x12mS\x8f\x82\xfbth\xd6u\xb9\x8b\xfe\xd6IZ;eQ:\x94y\x8e<\x92\xee\x88\xc8\xe8\xc3\xd7\xce\x125j\x17\xfc;EZ\x8f\xc1\x91\xd1Y\x8e\xb2\xe4\xc3t\xe0dt\xaf\\\xeb0\xc7<0\x17n釚t\xce\x1c\xaf\xa3\x11\xe5\x7f6צ\x14\xf2\x9a:bo_\xd0\x1d\xf2Z<\x96\x1e5V\x8ewүBg-\xf7\x9a\x0f>\xa7N\xd1Ə\x86\x1es\xf7\xf7DȻ\x96\xcav\x96qf:ѕ\xca\x7fg\xd8Zhc\xbbh\x98\x03\x89U\xa3\xa0\x8e\b=\xe5\a\xad\x8f\x8ems\x06\x1e\x99\x92F\xe4И~/\x02J2\xce\xd6\\\x14\xb5\x9e\xa1Ug\x93|n\x10\xe6\xb5\xc9\xe9#\xabtD\x16D\xa2\xc4u\xf6\x19^\xf0\xb4Ư\xf4c\n\x86\vZ\xe3\x89\xa8\x04\x1e\xb6R\\\xfaGl\x8c1IJ\xc0\xe3\x1b'\xbf\xefeL\xbe\x80,\xbdʉ\x9cY\xf24\xca\xfa\xf3?\x9e\xff:XtZ\xa6DٰO[\xa7\xc6c\xfa\x11c\xf9njd?K\xf5\xd73\x15N*\xfb\xa9'j\x1a\"G\xe0\xf5\xc5z@\xe5_\x93\xbe\xb1P~\xae\xbc\xb5<\xc1\t\xfb\xeb\x11xIg\xec\xb9\xd9\xc9l\xa3\x95T\xb5\xf1kB\b\xeb]\xe6\xee\x1d\b c\xc2>\xaaA\xfe\x83mT\x1d9\xb51Aڄ,\xda4\x82\xf4\x92j}b\x04X\xbe}\xbb\xec\xffb\x95O\xb1eO\xc2n\"\xc0\xe8>\n\x9e\xe7\x18\x17t\x0e\xf4x=\x10\xaeJ\x1a\ne\x04\x98\xd2L\x8a\xc2Il\x80ГW\xf6\xb9r\xab\x83G\xfbM\xd3kX鉸s\xd3o\x9bl\xc9i\xf7\xfd\x19I\xb7'=\x1a\xf5\xcd\xd2j\x8fK\xa6M]\xa1LH\x9cMO\x97Ma\xab+\xe9I\xb2\xc9\x11rjB\xec\xdc\x15\x88\x17M~}\x99\x94\xd7d\x9a\xa5\xa5\xb7Υث\xa4\xb2\xber\x02\xeb륭\xceHV=\xfd\xa9\x97\xf4\xb5\xf4\xa3\xb3+Ӗe\x0e'\x9c&\xa5\x99&-ݤ\f\xf8\xa8\xa1&\xa5\x8f\xceM\x1aM\xe2d\xfat}մ\xd0WM\x06}\xfd\x14\xd0Ii\x9b\xac07\xc9s\xfc\x92\xc3P\xa6\x1d\x80\xe2[\b\xe7sɤt\xcf5\x7fV\xdc\xf9y\x00\v\x85%\xb8\xa9\xaf\x18\a\x94uaEU\xb4\xf7\xb1\xc5\x02\xce\r\xec\x9aˊ~VtD\xde\xdf\xd4\xf5\xf9K#\xf1\xcbAT\xc3\r{\x82\xa2`<67\xf7\xa8\x90\xb9{@3\xb5\x00\xb4\x8d8\xcb\xfdeL\xfe\xf2\xd0\v7]\xe86\x00\xb2\xb0el\xa9\x8f\xcb\xc37}\x1d4`\xa9zl\xcf3w\xf1\x06}\xfb\xa5\x06\xbdct\xefX㛵\x87J\xfdD7\x18\x98\x06\xf5\xe3\xd5\xe1\xa1=\x93\xbd\x00\xa7U\x0f\xec\x9dt\x1e\xc1\x10'j\x83z\xa7\r\xe8P\xa9b\x9c\x16\xed'\x02B\xaa\x06B\xa4i\x8a\xf3?\xe7\x94\xe5K\x84w\xa7\b\xf0\x92<\xa0y\xde\xeb7<=y\xec\xa9\xc9\xf4d\x94\xa4S\x92/\x11\xee\xcd\t\xf8f\xf9\xab\xe9\xa7 \xe7o<\xbf\xf0\xa9Ǘ:\xed8\x83z\xa9\xa7\x1b\xe7\xd3\xee\x95N3\xbe\xfa)\xc6\xd7<\xbd8\xeb\xd4brz֬\x8c\x839\xa9U\xcf8n\x97\x96K0}\n1\xf1\xf4ab\xa6A\xda\xe0\x8f\x1cv\xe2\xe9\xc2\xf9\xa7\n\x13\xf9;gJ\xbf\xf2\xe9\xc1W>5\xf8-N\v&H`B\x95\xf9\xa7\x02\x9f\xbd%\xa5t\x0ezr\xdbo\x8e\xd4N\xcakj,\xd7Gl\xb0\xaf\x15n\x93\xc5Z\xbd\x18\x80̒\xbfȟ\x1em8\xb4\r\x8e\x92\xd9\xf1\x88z\xfb\x92\xad\xbb\xd6w\x88\xfdk\x0en\xeb\xd2@\xc5\xd1\x00P\xe0F\xa9YQW\xe1\x03\xcf6\x83\x1e6ܰ\xb5\xd2%\xb7\xec\xbc\xd9,~\xe3:\xc0\xbfϗ\x8c\xfd\xa0\x9a\\\x9d\xee}iF\x94U\xb1\xc3H\x8c\x9dw\x1b\xe7\xe8\xbdz\xae\xc1\xdeeCt\xf3_\xd6\xc9\x16\x89\x05>\xd8\\\x84[\x17\xfbW2\xbb\xfb\u070f\\+\xe1\x95\xf83=\xb7t\x82U\xb7w7\xd7\x04+\x88\x11\xbd\xe3\xd4$(6,_\x01\xba\f\xed\xd8\x0f\xe9\x93\xebu\x0fj?G\xb8\xfbX\x05\xe4\xeee\x92\xe0\xb6x՜)\xd4Z7\xd7\x0e\x97C=\xa1|q\xb9c\xca?=!t\xbe\xa8\xb8\xb6;\x97Lt\xd1\xc3#\xd8\xf5\xa9U\xb3\x83\xd6j\xff\xe5\x95n\xe9\x91=<\xbaB;ٻ\xaa\x9f<0\xa4\xe7sp:|\xaaz\xf2<\xf5\v\xe0t\u0605Z\x10\x15#?E3 O\xbebi\xfc\r\xfd?\xaa-\xbc\x8f\xae\\\xf6__\x194\x19IM\fP\xe9\x92\xf9\b\x05\xdb|D\xba\xe3\xfbyj/\x9ek\x18P\xf1w\x84?gq\xf2\xb6\x0fj\xfcA\x12\xbaA=t\x1a\xf3\xaa詧\x1d\xbb\xb9\xa7\xb8\xb5Q\xa5~\xea\xfb\xb85,O\x86\x04\x83\b,!\x0f\xbe\xd1r*2Z\xa5\xf9\x03|T\xeem\x9d\x141\xe9\xb7轼\xe4=\xb7\x90\xaf\xed'aL\xd1\xfb\xb1\r\x01\xb6\xe73\xf6.\xfaGl\x8f|\xca\xc0\xda\xe292rw\xf7э\x94\x9e4y\xef_'A}l\x00Y\x10(࠭\xf0\xbf\x1b\xf5D\x17\xe0\xc7ט\xc3\x03\"\x9d7̀\x0e\x8aP\n\xefQì\xabB\xf1\x1c\xf4\x15=\xa2\x920\xe2\x9fz\r\x06\xee@\xff)\x16o7#\xe3\t=\xbf`\x96\fztE\x01\xc5\x0f\xa2\x00\xe3\x10O4\r7\xfb-\x1bKQ\x97+穮\xf1Ǧ\x93\x03\x96\xd9\r\x956\x18*\xd0\xe8'\xba\xad\x88\xda\x04\xc9?L\f\xd6\xf0QH\v\x0f0\x1eCO\xd8\x04\xf7F\x039\x00A\x81Q\xc4\xf7\x97\xd8\xcac\x8f \xf7\xf1\xd6\x03\x19h\x16#cr\xac\xbc[us\x7feX-s\xda\x00\xb8\xff\xf3\xedQ\xf2\xbb\xed\xbd/\x13tB\x8az\xbf\x1fo\xd9\t\x11:ډ|\xfa\xb8\x12\x8f\xc1\xe2ƨLPT\xf1$\xac\xbf\xce\xf1\xe5\xee\x10?\x14 \x1e\x90\x8e\xda\xc0\xe7'\t\xfaK\xb0@\xe6Z\xc6\xdem\x99\xd6~?\xedA\x8b\xbe\xd7b\x15\xf6=\x02c\x00\x80\xa9\xb0\xcfe\xdcK@a{M\x98\xe6q\xb3}zN\xa8\x90\xb8\xa5\x1bw\xd8\x16\xe3o1-\x9a7\xa3\xce\x12\xc8\xed\xde?\xea\x03\x1e\x7f\xd2\xce=\x94\x94\xf1\xca\xd6:h\xd7Z\xd3-\xeb\b\x04\xdc%\xe4\xc7=j\u05feuv\f\x83\xdb\xc7\xc6\xda\xfd\x87\xc9\xe7PG\xe04\xcf\xd2E߸r\x11\xb5{\xaet\x81\xf0\x8f\xe3\xf1\xe8\x8cA\x9co\xdd\xdbe\x13D\xf8\xd8\xd6\x1c\x1bp3\f\x1c\xb2\x7f\r\xedUGB\x97\xeeO\x8c\xe1\x06\xeb4\xa7\\\xbd\x1cQ\xc3pY\xffm\x8c\t\xe3G!\x17\xec\x13\xecG\xec\v\xf6A\xe2 \xf6\t\xe0\xce;BN[+\xa4\x1d\xe7\fq۴\xa2æ#\x1arZl\xef\a0\x06\x99\xec\xf4\xe8SSŝ65\xec\xf7b\xcc\x1b\xa5\x1d\xb3\f\a\xfa\x87\xbd_\xa3\x1a\xfc\xa0\xf6\x8ei\xeeQ5\xb2\xf7\x91\x1e\xc2\xcb;\x92\xe3\xbd\xf4\xee\x97z\xd5>\xa8\xc0\xfe\xf6\xf7\xb3\x7f\x04\x00\x00\xff\xff)6\x10\xe1Z{\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?\xbfp\x10jQ\u0605\x90\xdd~\x82>Ǟ\x8e#\x88\xa3\xafc\xba\xb9U\x1f\x8d\x9b\x90ϢO\x04\xe6\x88\xf1\xacT\x1e&\xfaF\x14\xc0\xcc\xceX(\x83\xaaoåN\f8,\xe4`\x15\x85\ac\x90\xde~P\xe3\x04\x91uQ\xf0u\x01\x97d\x19\x0f\xd0l\\I\x8f\x11\xed+\x18+\xb2S\x92\xccA\x1c!\x98\xf6?\xf4(C\xf1\x16\x7f\x00\xc6#\xe0==1\xb8+\x8a\x0e\xd1\xfbԊ\xe2Vi\xc8\xd0\xf1\xbf\xf4\x01\x85\x80\x82\x82\x18\xa9X\xa1\xe4=h\x87Ec\xe0\xd1\xc0\x00\nh\xce\xd0W\xd7h\x96\x85d\x9b\x1a\xdd\xf8%C-\x11\x95\x11!\x8d\x05\x1e\x11\xe6\x13\xf0\x0e\xbe\xa1E\x80\xfc\xaa\xa8\x8d\x05}\x93\xa9\n\xf2\xb0j7j\xceRy\xf8\xe1 d\x1f\xf4\x15\"\x03\xe4C\xe6*-he,&\xdam\xfc\x87\xe6\x91\x16\xf1\x90\xd5~\bm`7\xa9[\fXlx\xfe\xfb\xf3\v\x92\x80~\xef\xfd~\f\xe3\x1a\x1a2\xcd\xd2\xcd\xe4&\x8d\xb7\x10\x16\xca\bu'u\xd4\f\xbes\xad\xf9\xee\x00כ\x15\xc8\x17\xe0{\f\xf6\x80\xf32T\xfbN\xbc\x1f\xf6\xff\xaf\xc8\xfd\xd3\xf2\xdb\xd0*>\x17\x12\xf9\\\bc{l6n\xe9\x0f\xc9:\x16w{\x02I\a\x13\xd5\xe4\x14W\xffA\x88yҹ\x13\x9b,\x8dl\xfa\t\xf0OEɭR\x0f)\xd4\xfb\x1f\xac\u05ee\xfb\xb1\x8cv\x9a\xd8\x1a\xb6\xfcQ(m\x86k\xcb\xf0\r\xb2\xdaF5\v\xb7,\x17\x9b\rh\x84E{#\xcdV\xca!b\x1d\x8e\xf9XGeE+\f\xc6\xd52\x1dYJԈ\r\x85\xa2\xfa(T\xe7\xe0`hA\x0eD.\x1eE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7T\xa8$\xa0\x8f_bl\xb4_5N\x89\xb0\xfer\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccr\v3\x05_C\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fm\xb5\x83\x99\x00\xcb(\xc4݊l\xeb\xdcW\x144\x82\xc5r\x05\x86\x96\x92xU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8\xe6\xc8(\xbe\xffJD\x0fV\xe7Ha\x9f\xd0$\x8c6Y\x92\xe7C\x94\xf4Hq\x01f\xd9Y\xd2\x146|Mah\xcf\x7f\xdc\xdb\x7f\xda#ʯ\x8bw\xc7M\x98\x19\xac\x9b\x9cS/˸\xa6\x9b\x7f\x12\xbe\x91ɺ\xf1\x16k\x16\xcf>u[^\xd0V\x8agH~\xc16\xa2\xb0@N\xd5\x14\xa2l\x06\xe7NI\xa0T\v\xcchg\xddf\xdb\x0f͆[B\x8b\x01\xad\x86\x00\x9c\x83\x1e\xa2\x1c\xe2A\x02Hָ\x16\xb4\xd3,\xdcJ\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\xffp\xc18{\x80\x9ds\xb1\xb8d\xc87\x1e*'\xa2\xa0\x81\xd2(HS<\xc0\x8e@\x8d\xe7E\x8c\x979\xd2\xe2\xca\x03\x8cl\x95\xc6J\x8f\xae\x88\x9f߀rt\xc3\x0fD\x98\x94\xd9Ԗ\x86\xa8~\xfa\x8cd%\xc4\xcb\f\xbd\x14J\xe0ˑ\xc3N\x16\xa7n_\xfdL\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\xfb\x87\x03t\x96\x04\x89\xd8\u0378q\xb6\x14\x05\xb5\xe1\x870l%1$s$\x9a\xd1\x1d%X\xb9.]gemh\x7fZ*\xb9p\xcbbc\xbdy\x1e(\xddc\xc1I:\xf6\x9dޢ1r\xbf\xb8T\xaf\x82g\x90\x87-:\xcaA\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\xa2\xceXY\xe0\\O\xac\x19\xa4%\xa9z$\x8d\xe9p\xf5Tb=\x93L\xe4E\x90ە$\x05\xddl\xe0y\xd6k\xa6\xdc\x1c\xa3b:cq.@\xc9ik\xedoh\xe9i6\xfe\x9dU\\h\xb3d\xef(\x1d\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\xa3\x9c\xd4mWa\x9d\xaf\xe4\xb9\xf3e\xf6\x14O\xe3\xf8(Y\xec\xd89\xfdv\xfe\\\xf7n\x86DϨ\xda\x13\xe5\x92W\xe9\x92L\xc9\xc6s\x02\r\fփC\x84\x8d\x9b\xac[\f\x10\xa6(\x90,ʕ2\x91d\x91\bZ\t\x82~\xad\x8cu\xeb\x90=\x7f\x7ft\xa1R\x85\xc5I\xc67\x1643V\xe9\x90NJ\x8a?e)\xbe[n\xb7`\xc0\xefC\xf9EO\a\x18\xa3\xd8\xf3V78\xabr\xee\xf6¨#\x9e\x91\xf7Dm+\xad20Ѽ\x88\xb6$ڦ\x91\xac\xa5.\x1d\x9au]\ue8bfM\x92\xd6NY\x94\x0ee\x9e#\x8f\xa4;\"2\xfa\xf0\xad\xb3D\x8d\xda\x05\xffN\x91\xd6cpdt\x00\xa6,\xf90\x87:\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\x90:\xe9\xb2\xcd\xe7\xc4ہ\xa4[\xfe\b>\xdd\x17d\xa6jIKa\xa8\a\xb0\x9b\x19\x10\x1dk\x9c\x15H\xb4w\x9d\xc6\xd1\f̱\xb2 I\x12rrݬ\xdb\xe4#\x17i\xebV\xec8\xb6\xdaC9\x9cc\xe5\xf8y\x14\x12<\xbbg\x10J\xfeM\x94u\xc9x\x89<$\xb7C\x94\xd0\x1cCp\xecn\xd2>\xb1\x05\x19-\xabp\x96U\x05X\xf0i\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\xb4c.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;z\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*\x9fbK76D\x80\xd1%\x1e<\xa7\x9b\x1b:\az\xbc\x1e\b\xf7K\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~3d(\xd3\x0e@\xf1=\x84\xf3\xb9dR\xba\xe7\x9a?+\xee\xfc2\x80\x85\xc2\x12\xdc\xd4W\x8c\x03ʺ\xb0\xa2*\xdaK\xecb\x01\xe7\x16v\xcdeE?+:\"\xefo\xea\xfa\xf2\xb5\x91\xf8\xe5 \xaa\xe1\x86=AQ0\x1e\x9b\x9b{T\xc8\xdc婙Z\x00\xdaF\x9c\xe5\xfe2&\x7f\xe3ꅛ.t\x1b\x00Y\xd82\xb6\xd4\xc7\xe5\u16fe\x0e\x1a\xb0T=\xb6登x\x83\xbe\xfdR\x83\xde1\xbaw\xac\xf1\xcd\xdaC\xa5~\xa2\x1b\fL\x83\xfa\xf1\xea\xf0О\xc9^\x80Ӫ\a\xf6N:\x8f`\x88\x13\xb5A\xbd\xd3\x06t\xa8Te\xecr>\x16&\xe8>\b\xa9\x1a\b\x91\xa6)\xce\xff\x9cS\x96/\x11ޝ\"\xc0K\xf2\x80\xe6y\xaf\xdf\xf1\xf4䱧&ӓQ\x92NI\xbeD\xb87'\xe0\x9b實\x9f\x82\x9c\xbf\xf1\xfc§\x1e_\xea\xb4\xe3\f\ua95en\x9cO\xbbW:\xcd\xf8\xea\xa7\x18_\xf3\xf4\xe2\xacS\x8b\xc9\xe9Y\xb32\x0e\xe6\xa4V=\xe3\xb8]Z.\xc1\xf4)\xc4\xc4Ӈ\x89\x99\x06i\x83?r؉\xa7\v\xe7\x9f*L\xe4\xef\x9c)\xfdʧ\a_\xf9\xd4\xe0\xf78-\x98 \x81\tU\xe6\x9f\n|\xf6\x96\x94\xd29\xe8\xc9m\xbf9R;)\xaf\xa9\xb1\\\x1f\xb1\xc1\xbeV\xb8M\x16k\xf5b\x002K\xfe\xf5\x03z\xe9\xe2\xd068Jf\xc7#\xea\xedK\xb6\xeeZ\xdf!\xf6O`\xb8\xadK\x03\x15G\x03@\x81\x1b\xa5fE]\x85\x0f<\xdb\x0ez\xd8r\xc36J\x97ܲ\xf3f\xb3\xf8\x8d\xeb\x00\xff>_2\xf6Q5\xb9:\xdd\xfbҌ(\xabb\x87\x91\x18;\xef6x\x9e\x94D\xa53\xf4|\xad\n\x91E|\xce\xd1{\xf5\\\x83\xbdˆ\xe8濬\x93-\x12\v|\xb0\xb9\b\xb7.\xf6\xafdv\x97\xe0\x1f\xb9V\xc2+\xf1'z\xa3\xea\x04\xabn\xef\xaeW\x04+\x88\x11=~\xd5$(6,_\x03\xba\f\xed\xd8\x0f\xe9\x93զ\a\xb5\x9f#\xdc}\xe1\x03r\xf7\x9cKp[\xbcj\xce\x14j\xad\xeb\x95\xc3\xe5PO(_\\\xee\x98\xf2\xefu\b\x9d/*\xae\xed\xce%\x13]\xf4\xf0\bv}j\xd5젵\xda\x7f\xae\xa6[zd\x0f/\xd5\xd0N\xf6\xae\xea'\x0f\f\xe9\xf9\x1c\x9c\x0e\x9f\xaa\x9els*2Z\xa5\xf9=|R\xeeA\xa2\x141\xe9\xb7\xe8=W\xe5=\xb7\x90\xaf\xed'aL\xd1\xfb\xb1\r\x01\xb6\xe73\xf6.\xfaGl\x8f|\xca\xc0\xda\xe292r{\xfbɍ\x94ށy\xef\x9ftA}l\x00Y\x10(࠭\xf1\xbf[\xf5D\x17\xe0\xc7טë+\x9d\x87߀\x0e\x8aP\n\xefQì\xabB\xf1\x1c\xf4\x15\xbd<\x930\xe2\x9fz\r\x06\xee@\xff\xfd\x1ao7#\xe3\t=\xbf`\x96\fztE\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{\xa3\x81\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}\xec\xbd/\x13tB\x8az\xbf\x1bo\xd9\t\x11:ډ|\xfa\xb8\x12\x8f\xc1\xe2ƨLPTA\x0f\n\xd1I\xa4\x97\xbbC\xfcP\x80x@:j\x03_\x9e$\xe8\xaf\xc1\x02\x99\x95\x8c\xbd\xdb2\xad\xfd~ڃ\x16}\xaf\xc5*\xec{\x04\xc6\x00\x00Sa\x9f˸\x97\x80\xc2\xf6\x9a0͋p\xfb\xf4\x9cP!qK7\xee\xb0-\xc6\x1f\xb0Z4\x0fm\x9d%\x90۽\x7f\xd4\a<\xfe\x0e\xa0{()㕭uЮ\xb5\xa6[\xd6\x11\b\xb8Kȏ{\t\xb0} \xee\x18\x06\xb7/\xb4\xb5\xfb\x0f\x93oȎ\xc0i\xde\xf2\x8b>\f\xe6\"j\xf7\xc6\xeb\x02\xe1\x1f\xc7\xe3\xd1\x19\x838߸\a\xdf&\x88\xf0\xa9\xad96\xe0f\x188d\xff\x84ܫ\x8e\x84.ݟ\x18\xc35\xd6iN\xb9z9\xa2\x86\xe1\xb2\xfe\x9b\x18\x13ƏB.\xd8g؏\xd8\x17\xec\x83\xc4A\xec\x13\xc0\x9dw\x84\x9c\xb6VH;\xce\x19\xe2cӊ\x0e\x9b\x8eh\xc8i\xb1\xbd\x1b\xc0\x18d\xb2ӣOM\x15w\xda\u0530ߊ1o\x94v\xcc2\x1c\xe8\xef\xf6~\x8dj\xf0\x83\xda;\xa6\xb9G\xd5\xc8\xdeGz=0\xefH\x8e\xf7һ_\xeau\xfb\xa0\x02\xfb\xdb\xdf\xcf\xfe?\x00\x00\xff\xff0\xe5e\x05\x8f|\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"), } diff --git a/pkg/apis/velero/v1/backup_repository_types.go b/pkg/apis/velero/v1/backup_repository_types.go index 5d56866ce..7789ddd60 100644 --- a/pkg/apis/velero/v1/backup_repository_types.go +++ b/pkg/apis/velero/v1/backup_repository_types.go @@ -118,7 +118,7 @@ type BackupRepositoryMaintenanceStatus struct { // +kubebuilder:storageversion // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="Repository Type",type="string",JSONPath=".spec.repositoryType" -// +kubebuilder:resource:shortName=br +// +kubebuilder:resource:shortName=repo type BackupRepository struct { metav1.TypeMeta `json:",inline"` diff --git a/pkg/apis/velero/v1/backup_types.go b/pkg/apis/velero/v1/backup_types.go index f6b561ed8..e4e734279 100644 --- a/pkg/apis/velero/v1/backup_types.go +++ b/pkg/apis/velero/v1/backup_types.go @@ -516,7 +516,7 @@ type HookStatus struct { // +kubebuilder:storageversion // +kubebuilder:rbac:groups=velero.io,resources=backups,verbs=create;delete;get;list;patch;update;watch // +kubebuilder:rbac:groups=velero.io,resources=backups/status,verbs=get;update;patch -// +kubebuilder:resource:shortName=bkp +// +kubebuilder:resource:shortName=bak // Backup is a Velero resource that represents the capture of Kubernetes // cluster state at a point in time (API objects and associated volume state). diff --git a/pkg/apis/velero/v1/download_request_types.go b/pkg/apis/velero/v1/download_request_types.go index 37aaab88a..5e93862e6 100644 --- a/pkg/apis/velero/v1/download_request_types.go +++ b/pkg/apis/velero/v1/download_request_types.go @@ -92,7 +92,7 @@ type DownloadRequestStatus struct { // +kubebuilder:object:root=true // +kubebuilder:object:generate=true // +kubebuilder:storageversion -// +kubebuilder:resource:shortName=dr +// +kubebuilder:resource:shortName=dreq // DownloadRequest is a request to download an artifact from backup object storage, such as a backup // log file. From bd7b2ed690bbd725e48661e70deaff03597aa86a Mon Sep 17 00:00:00 2001 From: AmirHossein HajiMohammadi Date: Sat, 18 Jul 2026 17:13:54 +0330 Subject: [PATCH 059/194] Trim plugin image entries during install Signed-off-by: AmirHossein HajiMohammadi --- pkg/install/deployment.go | 5 ++++- pkg/install/deployment_test.go | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/install/deployment.go b/pkg/install/deployment.go index 4ce4b5a4f..e9474f1fe 100644 --- a/pkg/install/deployment.go +++ b/pkg/install/deployment.go @@ -139,7 +139,10 @@ func WithPodVolumeOperationTimeout(val time.Duration) podTemplateOption { func WithPlugins(plugins []string) podTemplateOption { return func(c *podTemplateConfig) { - c.plugins = plugins + c.plugins = make([]string, 0, len(plugins)) + for _, plugin := range plugins { + c.plugins = append(c.plugins, strings.TrimSpace(plugin)) + } } } diff --git a/pkg/install/deployment_test.go b/pkg/install/deployment_test.go index 53b696f72..0cfcb65dd 100644 --- a/pkg/install/deployment_test.go +++ b/pkg/install/deployment_test.go @@ -60,6 +60,15 @@ func TestDeployment(t *testing.T) { assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2) assert.Equal(t, "--features=EnableCSI,foo,bar,baz", deploy.Spec.Template.Spec.Containers[0].Args[1]) + deploy = Deployment("velero", WithPlugins([]string{ + "harbor-repo.vmware.com/harbor-ci/velero/velero-plugin-for-aws:v1.2.0", + " \n vsphereveleroplugin/velero-plugin-for-vsphere:v1.1.1 ", + })) + assert.Len(t, deploy.Spec.Template.Spec.InitContainers, 2) + assert.Equal(t, "harbor-repo.vmware.com/harbor-ci/velero/velero-plugin-for-aws:v1.2.0", deploy.Spec.Template.Spec.InitContainers[0].Image) + assert.Equal(t, "vsphereveleroplugin/velero-plugin-for-vsphere:v1.1.1", deploy.Spec.Template.Spec.InitContainers[1].Image) + assert.Equal(t, "vsphereveleroplugin-velero-plugin-for-vsphere", deploy.Spec.Template.Spec.InitContainers[1].Name) + deploy = Deployment("velero", WithUploaderType("kopia")) assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2) assert.Equal(t, "--uploader-type=kopia", deploy.Spec.Template.Spec.Containers[0].Args[1]) From 10c238ab5ab624820efec86587b95274ec6eaea6 Mon Sep 17 00:00:00 2001 From: AmirHossein HajiMohammadi Date: Sat, 18 Jul 2026 17:14:43 +0330 Subject: [PATCH 060/194] Add changelog for plugin install spacing Signed-off-by: AmirHossein HajiMohammadi --- changelogs/unreleased/10035-HajimohammadiNet | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/10035-HajimohammadiNet diff --git a/changelogs/unreleased/10035-HajimohammadiNet b/changelogs/unreleased/10035-HajimohammadiNet new file mode 100644 index 000000000..2905938ab --- /dev/null +++ b/changelogs/unreleased/10035-HajimohammadiNet @@ -0,0 +1 @@ +Trim whitespace around plugin image entries during install. From 26af4e0e9f119508611a62681203d999d20226bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:12:18 +0000 Subject: [PATCH 061/194] Bump aquasecurity/trivy-action from 0.35.0 to 0.36.0 Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.35.0 to 0.36.0. - [Release notes](https://github.com/aquasecurity/trivy-action/releases) - [Commits](https://github.com/aquasecurity/trivy-action/compare/57a97c7e7821a5776cebc9bb87c984fa69cba8f1...ed142fd0673e97e23eac54620cfb913e5ce36c25) --- updated-dependencies: - dependency-name: aquasecurity/trivy-action dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/nightly-trivy-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly-trivy-scan.yml b/.github/workflows/nightly-trivy-scan.yml index 85ce3cdc5..be0aa4dcf 100644 --- a/.github/workflows/nightly-trivy-scan.yml +++ b/.github/workflows/nightly-trivy-scan.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v6 - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 with: image-ref: 'docker.io/velero/${{ matrix.images }}:${{ matrix.versions }}' severity: 'CRITICAL,HIGH,MEDIUM' From 2b2aa061a8af3b9712c502ae0b6cb8890d6de94c Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 20 Jul 2026 14:05:26 +0800 Subject: [PATCH 062/194] optimize subobject description Signed-off-by: Lyndon-Li --- pkg/repository/udmrepo/kopialib/lib_repo.go | 42 +++++++++---------- .../udmrepo/kopialib/lib_repo_ex_test.go | 6 +-- pkg/uploader/block/uploader.go | 3 +- pkg/uploader/block/uploader_test.go | 3 +- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo.go b/pkg/repository/udmrepo/kopialib/lib_repo.go index 151bf1cb2..c7bb65a43 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo.go @@ -19,7 +19,6 @@ package kopialib import ( "context" "encoding/json" - "fmt" "io" "os" "strings" @@ -913,8 +912,7 @@ func (kow *kopiaObjectWriterEx) Write(p []byte) (int, error) { kow.entryLock.Unlock() buffOffset := curPos - offset - objName := fmt.Sprintf("%s-b%v", kow.description, entryID) - kow.writeObjectAsync(objName, entryID, p[buffOffset:buffOffset+kow.blockSize]) + kow.writeObjectAsync(entryID, p[buffOffset:buffOffset+kow.blockSize]) curPos += kow.blockSize } @@ -922,38 +920,38 @@ func (kow *kopiaObjectWriterEx) Write(p []byte) (int, error) { return length, nil } -func (kow *kopiaObjectWriterEx) writeObject(objName string, p []byte) (object.ID, error) { +func (kow *kopiaObjectWriterEx) writeObject(p []byte) (object.ID, error) { writer := kow.rawRepoWriter.NewObjectWriter(kopia.SetupKopiaLog(kow.ctx, kow.logger), object.WriterOptions{ - Description: objName, + Description: kow.description, Compressor: kow.compressor, Splitter: kow.splitter, }) if writer == nil { - return object.EmptyID, errors.Errorf("error opening writer for %s", objName) + return object.EmptyID, errors.New("error opening writer") } defer writer.Close() written, err := writer.Write(p) if err != nil { - return object.EmptyID, errors.Wrapf(err, "error writing for %s", objName) + return object.EmptyID, errors.Wrap(err, "error writing data") } if written != len(p) { - return object.EmptyID, errors.Errorf("short write for %s", objName) + return object.EmptyID, errors.New("short write") } objID, err := writer.Result() if err != nil { - return object.EmptyID, errors.Wrapf(err, "error flushing data for %s", objName) + return object.EmptyID, errors.Wrap(err, "error flushing data") } return objID, nil } -func (kow *kopiaObjectWriterEx) writeObjectSync(objName string, entry int, p []byte) error { - objID, err := kow.writeObject(objName, p) +func (kow *kopiaObjectWriterEx) writeObjectSync(entry int, p []byte) error { + objID, err := kow.writeObject(p) if err != nil { return err } @@ -965,10 +963,10 @@ func (kow *kopiaObjectWriterEx) writeObjectSync(objName string, entry int, p []b return nil } -func (kow *kopiaObjectWriterEx) writeObjectAsync(objName string, entryID int, p []byte) { +func (kow *kopiaObjectWriterEx) writeObjectAsync(entryID int, p []byte) { if kow.asyncWritesSem == nil { - if err := kow.writeObjectSync(objName, entryID, p); err != nil { - kow.saveWriteError(errors.Wrapf(err, "error writing object for %s", objName)) + if err := kow.writeObjectSync(entryID, p); err != nil { + kow.saveWriteError(errors.Wrapf(err, "error writing object for %s, entry %d", kow.description, entryID)) } } else { kow.asyncWritesSem <- struct{}{} @@ -977,8 +975,8 @@ func (kow *kopiaObjectWriterEx) writeObjectAsync(objName string, entryID int, p copy(buffer, p) kow.asyncWritesGroup.Go(func() { - if err := kow.writeObjectSync(objName, entryID, buffer); err != nil { - kow.saveWriteError(errors.Wrapf(err, "error writing object for %s", objName)) + if err := kow.writeObjectSync(entryID, buffer); err != nil { + kow.saveWriteError(errors.Wrapf(err, "error writing object for %s, entry %d", kow.description, entryID)) } kow.asyncBuffer.Return(buffer) @@ -987,10 +985,10 @@ func (kow *kopiaObjectWriterEx) writeObjectAsync(objName string, entryID int, p } } -func (kow *kopiaObjectWriterEx) writeZeroObject(objName string, entryID int) error { +func (kow *kopiaObjectWriterEx) writeZeroObject(entryID int) error { if kow.zeroObject == object.EmptyID { zeroBuffer := make([]byte, kow.blockSize) - objectID, err := kow.writeObject(objName, zeroBuffer) + objectID, err := kow.writeObject(zeroBuffer) if err != nil { return err } @@ -1071,9 +1069,8 @@ func (kow *kopiaObjectWriterEx) WriteAt(p []byte, offset int64) (int, error) { }) kow.entryLock.Unlock() - objName := fmt.Sprintf("%s-b%v", kow.description, entryID) - if err := kow.writeZeroObject(objName, entryID); err != nil { - return 0, errors.Wrapf(err, "error writing zero object for %s", objName) + if err := kow.writeZeroObject(entryID); err != nil { + return 0, errors.Wrapf(err, "error writing zero object for %s, entry %v", kow.description, entryID) } curPos += kow.blockSize @@ -1093,8 +1090,7 @@ func (kow *kopiaObjectWriterEx) WriteAt(p []byte, offset int64) (int, error) { kow.entryLock.Unlock() buffOffset := curPos - offset - objName := fmt.Sprintf("%s-b%v", kow.description, entryID) - kow.writeObjectAsync(objName, entryID, p[buffOffset:buffOffset+kow.blockSize]) + kow.writeObjectAsync(entryID, p[buffOffset:buffOffset+kow.blockSize]) curPos += kow.blockSize } diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go index 3294063a6..afeaaee60 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go @@ -291,7 +291,7 @@ func TestKopiaObjectWriterEx_Write(t *testing.T) { t.Helper() err := kow.getWriteError() require.Error(t, err) - assert.Contains(t, err.Error(), "error opening writer for -b0") + assert.Contains(t, err.Error(), "error writing object for , entry 0: error opening writer") }, }, { @@ -936,7 +936,7 @@ func TestKopiaObjectWriterEx_WriteAt(t *testing.T) { }, inputData: make([]byte, 1024), offset: 1024, - expectedErr: "error writing zero object for -b0: error writing for -b0: simulated zero object write error", + expectedErr: "error writing zero object for , entry 0: error writing data: simulated zero object write error", }, { name: "writeObject short write", @@ -964,7 +964,7 @@ func TestKopiaObjectWriterEx_WriteAt(t *testing.T) { t.Helper() err := kow.getWriteError() require.Error(t, err) - assert.Contains(t, err.Error(), "short write for -b0") + assert.Contains(t, err.Error(), "error writing object for , entry 0: short write") }, }, } diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 75e913cb7..3cb3f73ba 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -18,6 +18,7 @@ package block import ( "context" + "fmt" "io" "os" "runtime" @@ -84,7 +85,7 @@ func (blkup *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, b } destObj, err := blkup.repoWriter.NewObjectWriter(blkup.ctx, udmrepo.ObjectWriteOptions{ - Description: "BDEV:" + getObjectName(source.realSource), + Description: fmt.Sprintf("BDEV:%s-%s", getObjectName(source.realSource), snapStart.Format("2006-01-02-15-04-05")), DataType: udmrepo.ObjectDataTypeData, AccessMode: udmrepo.ObjectDataAccessModeBlock, ParentObject: parentObject, diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index 88fd4771e..2d06c5c80 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -21,6 +21,7 @@ import ( "context" "io" "os" + "strings" "testing" "time" @@ -357,7 +358,7 @@ func TestBlockUploaderBackup(t *testing.T) { } repoWriter.On("NewObjectWriter", mock.Anything, mock.MatchedBy(func(opt udmrepo.ObjectWriteOptions) bool { - return opt.Description == "BDEV:data-volume1" && opt.BackupMode == backupMode + return strings.HasPrefix(opt.Description, "BDEV:data-volume1-") && opt.BackupMode == backupMode })).Return(objWriter, tc.createObjErr) } From d59e7d4eb8f2136a327ceab627feb3efc0ea299d Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Mon, 20 Jul 2026 21:18:13 +0800 Subject: [PATCH 063/194] Prioritize exact namespace match in restore (#10033) * Prioritize exact namespace match in restore Align restore pipeline with backup pipeline by evaluating exact namespace matches before glob patterns in namespacedFilterPolicies. This ensures specific overrides always win regardless of list order. Signed-off-by: Adam Zhang * improve test cases add test cases for exact listed first, and excat listed last to ensure the behavior that the order does not matter for exact listed namespace, the rule will be always honored. Signed-off-by: Adam Zhang --------- Signed-off-by: Adam Zhang --- changelogs/unreleased/10033-adam-jian-zhang | 1 + .../fine-grained-restore-filters-design.md | 21 ++++++---- pkg/restore/restore.go | 16 +++++--- pkg/restore/restore_policies_test.go | 40 ++++++++++++++++++- 4 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 changelogs/unreleased/10033-adam-jian-zhang diff --git a/changelogs/unreleased/10033-adam-jian-zhang b/changelogs/unreleased/10033-adam-jian-zhang new file mode 100644 index 000000000..62a676ab9 --- /dev/null +++ b/changelogs/unreleased/10033-adam-jian-zhang @@ -0,0 +1 @@ +Fix issue #10032, prioritize exact namespace match in restore diff --git a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md index 0e4107171..913c056c0 100644 --- a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md +++ b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md @@ -307,6 +307,9 @@ The `getNamespaceFilter()` method on `restoreContext` takes the original namespa **Plugin Additional Items (Restore-Side):** Like the backup side — which is permissive at Stage 2 to allow CSI plugin-injected resources through — the restore side is permissive for AdditionalItems in `restoreItem()`. If a restore plugin requests an additional item, it is allowed to bypass the fine-grained `namespacedFilterPolicies` and `clusterScopedFilterPolicy` kind, name, and label selector checks. This allows plugins to successfully restore dependencies (like a PV needed by a PVC, or a specific Secret) without the user having to explicitly authorize every single dependent resource type in their configuration. Note that these additional items must still pass global resource/namespace exclusions. +**Exact Namespace Match Priority:** +If a namespace matches both an exact name pattern and a glob pattern across different `namespacedFilterPolicies` entries, the exact match always takes precedence, regardless of list order. This aligns with the backup pipeline behavior and ensures specific overrides are always honored. + **Multiple Glob Patterns Matching Same Namespace (Incorrect Order):** ```yaml namespacedFilterPolicies: @@ -665,7 +668,7 @@ data: ### Restore with Glob Namespace Patterns -Apply the same filter to all namespaces matching a pattern. **Critical: Order patterns from most specific to least specific:** +Apply the same filter to all namespaces matching a pattern. **Note on Precedence:** Exact namespace matches always take precedence regardless of where they are listed. However, if multiple glob patterns could match a namespace, they are evaluated in the order they appear. Always list specific globs before broad globs. ```yaml apiVersion: v1 @@ -677,19 +680,21 @@ data: policy: | version: v1 namespacedFilterPolicies: - # More specific patterns first + # Globs must be ordered specific-to-broad - namespaces: - - "team-frontend-prod" # Most specific (exact match) - resourceFilters: - - kinds: [Deployment, Service, ConfigMap, Secret, PersistentVolumeClaim] - - namespaces: - - "team-frontend-*" # Less specific (pattern match) + - "team-frontend-*" # specific pattern match resourceFilters: - kinds: [Deployment, Service, ConfigMap] - namespaces: - - "team-*" # Least specific (broad pattern) + - "team-*" # broad pattern resourceFilters: - kinds: [Deployment, Service] + + # Exact matches always win, even if placed at the bottom + - namespaces: + - "team-frontend-prod" # exact match + resourceFilters: + - kinds: [Deployment, Service, ConfigMap, Secret, PersistentVolumeClaim] ``` **Pattern Matching Results:** diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index a71fc4b23..8205accb9 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -478,7 +478,15 @@ func (ctx *restoreContext) getNamespaceFilter(namespace string) *resolvedNamespa return filter } - // 2. Walk patterns in definition order (first-match semantics) + // 2. Check for exact match first (O(1) map lookup) + // This ensures exact namespace matches take precedence over globs, + // regardless of where they are listed in the configuration. + if filter, ok := ctx.namespacedFilterMap[namespace]; ok { + ctx.namespaceFilterCache[namespace] = filter + return filter + } + + // 3. Walk patterns in definition order using pre-compiled globs // Note: namespaceFilterCache is mutated below without synchronization. This is safe // today because resource collection runs sequentially. If the restore loop is // parallelized in the future, these map writes will need a lock to prevent data races. @@ -489,14 +497,10 @@ func (ctx *restoreContext) getNamespaceFilter(namespace string) *resolvedNamespa ctx.namespaceFilterCache[namespace] = filter return filter } - } else if p.pattern == namespace { - filter := ctx.namespacedFilterMap[p.pattern] - ctx.namespaceFilterCache[namespace] = filter - return filter } } - // 3. Cache the miss so we don't re-evaluate failed matches + // 4. Cache the miss so we don't re-evaluate failed matches ctx.namespaceFilterCache[namespace] = nil return nil } diff --git a/pkg/restore/restore_policies_test.go b/pkg/restore/restore_policies_test.go index 42b8fb11f..a027f66aa 100644 --- a/pkg/restore/restore_policies_test.go +++ b/pkg/restore/restore_policies_test.go @@ -62,7 +62,7 @@ namespacedFilterPolicies: }, }, { - name: "namespaced filter policy with glob namespace match and first-match semantics", + name: "namespaced filter policy with exact match priority over glob (glob listed first)", restore: defaultRestore().Result(), backup: defaultBackup().Result(), policyYAML: `version: v1 @@ -94,7 +94,43 @@ namespacedFilterPolicies: test.Pods(), }, want: map[*test.APIResource][]string{ - test.Pods(): {"ns-1/pod-1", "ns-2/pod-1"}, + test.Pods(): {"ns-1/pod-2", "ns-2/pod-1"}, + }, + }, + { + name: "namespaced filter policy with exact match priority over glob (exact listed first)", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + policyYAML: `version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-1 + resourceFilters: + - kinds: + - pods + names: + - pod-2 + - namespaces: + - ns-* + resourceFilters: + - kinds: + - pods + names: + - pod-1 +`, + tarball: test.NewTarWriter(t). + AddItems("pods", + builder.ForPod("ns-1", "pod-1").Result(), + builder.ForPod("ns-1", "pod-2").Result(), + builder.ForPod("ns-2", "pod-1").Result(), + builder.ForPod("ns-2", "pod-2").Result(), + ). + Done(), + apiResources: []*test.APIResource{ + test.Pods(), + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-2", "ns-2/pod-1"}, }, }, { From 9ed3fc855a0b0de761ee6d25f0b6fc83ea34cea7 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Tue, 21 Jul 2026 03:55:54 +0800 Subject: [PATCH 064/194] add documentation for backup filters (#9967) * add documentation for backup filters Add user guide for fine grained backup filters with examples from easy to advanced. Signed-off-by: Adam Zhang * address review comments - enhanced example 3, explain how each item got excluded - enhanced example 8, explain the exact match rule, and how the ordering affecting namespace that has multiple match patterns - cross link to restore side design - fix the error msg to be consistent with implemenation Signed-off-by: Adam Zhang --------- Signed-off-by: Adam Zhang --- changelogs/unreleased/9967-adam-jian-zhang | 1 + .../docs/main/fine-grained-backup-filters.md | 787 ++++++++++++++++++ site/data/docs/main-toc.yml | 2 + 3 files changed, 790 insertions(+) create mode 100644 changelogs/unreleased/9967-adam-jian-zhang create mode 100644 site/content/docs/main/fine-grained-backup-filters.md diff --git a/changelogs/unreleased/9967-adam-jian-zhang b/changelogs/unreleased/9967-adam-jian-zhang new file mode 100644 index 000000000..3bed73061 --- /dev/null +++ b/changelogs/unreleased/9967-adam-jian-zhang @@ -0,0 +1 @@ +Fix issue #9820, user guide for backup fine-grained filters via resource policy diff --git a/site/content/docs/main/fine-grained-backup-filters.md b/site/content/docs/main/fine-grained-backup-filters.md new file mode 100644 index 000000000..c8e7da63b --- /dev/null +++ b/site/content/docs/main/fine-grained-backup-filters.md @@ -0,0 +1,787 @@ +--- +title: "Fine-Grained Backup Filters" +layout: docs +--- + +This guide explains how to use Velero's **fine-grained backup filters**: per-namespace, per-kind rules with independent label selectors and resource name patterns. Configuration lives in the same **ResourcePolicy ConfigMap** you may already use for volume policies. + +For architecture and pipeline details, see the [design document](https://github.com/velero-io/velero/blob/main/design/backup-filter-enhancement/fine-grained-backup-filters-design.md). + +--- + +## Introduction + +Velero's global backup filters apply the same namespace list, resource types, and label selector to every namespace in a backup. That works for many clusters, but common scenarios need more control: + +- **Different namespaces, different strategies** — back up everything in a database namespace, but only Deployments and ConfigMaps in a frontend namespace. +- **Filter by resource name** — back up `app-config` and `app-secret` without also capturing `monitoring-config`. +- **Different labels per kind** — Deployments labeled `app=workload-1` and StatefulSets labeled `app=workload-2` in the same namespace. + +Fine-grained filters add two optional sections to the ResourcePolicy ConfigMap: + +| Section | Scope | Behavior | +|---------|-------|----------| +| `namespacedFilterPolicies` | Namespaces you match (exact name or glob) | **Exclusive allowlist** — only resource kinds listed in `resourceFilters` (or covered by a catch-all) are backed up from those namespaces | +| `clusterScopedFilterPolicy` | Cluster-scoped resources globally | **Refinement overlay** — listed kinds get per-kind label and name rules; unlisted cluster-scoped kinds still use global BackupSpec filters | + +**No new BackupSpec CRD fields** are required. Reference the policy from `Backup.spec.resourcePolicy` or `velero backup create --resource-policies-configmap`. + +**Backward compatible:** if you omit both new sections, backups behave exactly as they do today. + +--- + +## Prerequisites and wiring + +### What you need + +- Velero installed with backup filters support (see your Velero release notes). +- A ResourcePolicy ConfigMap in the Velero namespace (`velero` by default). +- Permission to create Backups (or Schedules) that reference the ConfigMap. + +### End-to-end pattern + +Every example below follows the same three steps: + +1. **Create or update** a ConfigMap with `data.policy` containing `version: v1` and your filter rules. +2. **Create a Backup** (or Schedule) that includes the target namespaces and references the ConfigMap. +3. **Verify** with `velero backup describe` and inspect backup contents or logs. + +### Minimal skeleton + +Use this once; later examples show only the `policy:` body. + +**ResourcePolicy ConfigMap:** + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-backup-filter-policy + namespace: velero +data: + policy: | + version: v1 + namespacedFilterPolicies: + - namespaces: + - my-namespace + resourceFilters: + - kinds: [ConfigMap] + labelSelector: + app: my-app +``` + +**Backup:** + +```yaml +apiVersion: velero.io/v1 +kind: Backup +metadata: + name: my-backup + namespace: velero +spec: + includedNamespaces: + - my-namespace + resourcePolicy: + kind: configmap + name: my-backup-filter-policy + storageLocation: default +``` + +**CLI equivalent:** + +```bash +velero backup create my-backup \ + --include-namespaces my-namespace \ + --resource-policies-configmap my-backup-filter-policy +``` + +**Verify:** + +```bash +velero backup describe my-backup +velero backup describe my-backup -o json | jq '.namespacedFilterPolicies' +``` + +### Important: do not mix old-style BackupSpec resource filters + +When `namespacedFilterPolicies` or `clusterScopedFilterPolicy` is present in the ResourcePolicy, **do not** set these on the Backup: + +- `spec.includedResources` / `spec.excludedResources` +- `spec.includeClusterResources` + +Use `includeExcludePolicy` inside the ResourcePolicy ConfigMap for global resource-type include/exclude instead. Velero rejects backups that combine the new policy sections with old-style fields. + +Schedules follow the same rule: configure filters in the ResourcePolicy ConfigMap, not via deprecated resource filter fields on the Schedule template. + +--- + +## Examples + +Each example includes: **goal**, **policy YAML**, **backup notes**, **expected outcome**, and **how to verify**. + +--- + +### Example 0 — Baseline (no new filters) + +**Goal:** Confirm that namespaces without a `namespacedFilterPolicies` entry still use global BackupSpec filters. + +**Policy:** Omit `namespacedFilterPolicies` and `clusterScopedFilterPolicy` entirely (or use a ConfigMap with only `volumePolicies` / `includeExcludePolicy`). + +**Backup:** + +```yaml +spec: + includedNamespaces: + - ns-a + - ns-b + - production + # No resourcePolicy — global filters only +``` + +**Expected outcome:** All resources in included namespaces follow `includedNamespaces`, `labelSelector`, `includedResources`, and related global fields — same as before this feature. + +**Verify:** `velero backup describe` shows no namespace-scoped filter policies section. + +--- + +### Example 1 — Per-namespace kinds and labels + +**Goal:** In `ns-a`, back up only ConfigMaps, Secrets, Deployments, and Pods with `app=my-app`. In `ns-b`, use global filters (no policy entry for that namespace). + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [ConfigMap, Secret, Deployment, Pod] + labelSelector: + app: my-app +``` + +**Backup:** + +```yaml +spec: + includedNamespaces: + - ns-a + - ns-b + resourcePolicy: + kind: configmap + name: per-namespace-resource-filter-policy # or your ConfigMap name +``` + +**Expected outcome:** + +- **ns-a:** Only listed kinds with label `app=my-app` (e.g. `app-config`, `app-secret`, `app-deployment`). Resources like `monitoring-config` (different labels) are excluded. +- **ns-b:** Everything allowed by global filters (no namespace policy match). + +**Verify:** `velero backup describe` lists resolved filters for `ns-a`. + +--- + +### Example 2 — Exact resource names + +**Goal:** Back up only two ConfigMaps by exact name, optionally requiring a label. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - target-namespace + resourceFilters: + - kinds: [ConfigMap] + names: [vm-1, vm-2] + labelSelector: + resource-type: VirtualMachine +``` + +**Backup:** `includedNamespaces: [target-namespace]` plus `resourcePolicy` reference. + +**Expected outcome:** Only `vm-1` and `vm-2` ConfigMaps with `resource-type=VirtualMachine`. `vm-3` and other ConfigMaps are excluded. + +**Verify:** Backup archive contains exactly those two ConfigMaps in `target-namespace`. + +--- + +### Example 3 — Glob name patterns with exclusions + +**Goal:** Back up `app-*` ConfigMaps and Secrets in `production`, but exclude temporary and debug names. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - production + resourceFilters: + - kinds: [ConfigMap, Secret] + names: ["app-*"] + excludedNames: ["*-tmp-*", "*-debug-*", "*-tmp", "*-debug"] +``` + +**Expected outcome:** + +- **Included:** `app-config`, `app-cache-config`, `app-secret`, `app-db-secret` +- **Excluded:** `app-tmp-config`, `app-debug-config` (excluded by `excludedNames`), and `monitoring-tmp-secret` (excluded because it does not match the `names: ["app-*"]` allowlist) + +`excludedNames` takes precedence over `names` when both match. + +**Verify:** Inspect backup item list. + +--- + +### Example 4 — Per-kind label selectors + +**Goal:** Apply different label rules to different resource types in the same namespace. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - target-namespace + resourceFilters: + - kinds: [ConfigMap] + orLabelSelectors: + - app: production-workload-1 + component: vm-group + - app: production-workload-2 + component: vm-service +``` + +**Expected outcome:** ConfigMaps matching either label combination are backed up; other ConfigMaps in the namespace are not (for this kind). + +**Note:** Use `orLabelSelectors` when you need OR across label sets. `labelSelector` and `orLabelSelectors` cannot appear in the same `resourceFilters` entry. + +--- + +### Example 5 — OR label selectors across kinds + +**Goal:** Back up ConfigMaps, Secrets, or Deployments that match any of several label conditions. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [ConfigMap, Secret] + orLabelSelectors: + - app: my-app + - app: monitoring + - kinds: [Deployment] + orLabelSelectors: + - app: my-app + - app: monitoring + - component: backend +``` + +**Expected outcome:** Resources included if they match **any** map in `orLabelSelectors` for their kind (AND within each map, OR across maps). + +--- + +### Example 6 — Multiple criteria on one kind + +**Goal:** Combine exact names with OR label selectors for a single kind. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - target-namespace + resourceFilters: + - kinds: [ConfigMap] + names: [vm-1, vm-2] + orLabelSelectors: + - resource-type: VirtualMachine + - component: vm-group + - component: vm-service +``` + +**Expected outcome:** Only `vm-1` and `vm-2` that also satisfy one of the label OR branches. + +--- + +### Example 7 — One policy entry, multiple namespaces + +**Goal:** Apply the same rules to `ns-a`, `ns-b`, and `production` in a single policy block. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + - ns-b + - production + resourceFilters: + - kinds: [ConfigMap] + - kinds: [Deployment] + labelSelector: + tier: web +``` + +**Expected outcome:** + +- All ConfigMaps in those namespaces (no label filter on that entry). +- Deployments with `tier=web` only. + +--- + +### Example 8 — Namespace glob patterns and ordering + +**Goal:** Different backup breadth for `team-frontend-prod`, `team-frontend-dev`, and `team-backend-test` using glob patterns. + +**Policy (correct order — most specific first):** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - "team-frontend-*" + resourceFilters: + - kinds: [Deployment, Service, ConfigMap] + - namespaces: + - "team-*" + resourceFilters: + - kinds: [Deployment, Service] + - namespaces: + - team-frontend-prod # exact match + resourceFilters: + - kinds: [Deployment, Service, ConfigMap, Secret, PersistentVolumeClaim] +``` + +**Expected outcome:** + +| Namespace | Matched policy | Kinds backed up | +|-----------|----------------|-----------------| +| `team-frontend-prod` | First entry (exact) | 5 kinds | +| `team-frontend-dev` | `team-frontend-*` | 3 kinds | +| `team-backend-test` | `team-*` | 2 kinds | + +**Wrong order (avoid):** If `team-*` is listed **before** `team-frontend-*`, then `team-frontend-dev` matches the broader `team-*` rule first and only Deployments and Services are backed up — the more specific `team-frontend-*` rule is never reached. + +Velero evaluates namespaces by looking for an **exact match** first, and then evaluates glob patterns in **definition order** (first-match wins). Because `team-frontend-prod` is an exact match in this policy, its evaluation is unaffected by glob ordering. However, for namespaces relying on glob patterns like `team-frontend-dev`, the order of the glob patterns is critical. + +**Backup:** Include all relevant namespaces in `includedNamespaces` (they must still pass the global namespace filter). + +--- + +### Example 9 — Catch-all by label + +**Goal:** Back up any resource kind that has a given label, without listing every kind. Kind-specific entries override the catch-all. + +**Policy (recommended explicit form):** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: ["*"] # catch-all + labelSelector: + app: common-app + - kinds: [ConfigMap, Secret] # override for these kinds + labelSelector: + app: specialized-app +``` + +**Equivalent:** `kinds: []` (empty) also denotes a catch-all; `kinds: ["*"]` is preferred for readability. + +**Rules:** + +- At most **one** catch-all per namespace policy entry. +- Catch-all entries **cannot** use `names` or `excludedNames` — use kind-specific entries for name filtering. +- Catch-all does **not** inherit `BackupSpec.labelSelector`; set `labelSelector` or `orLabelSelectors` on the catch-all entry explicitly. + +**Expected outcome:** ConfigMaps and Secrets use `app=specialized-app`; all other kinds listed only via catch-all use `app=common-app`. + +--- + +### Example 10 — Catch-all with per-kind name overrides + +**Goal:** Pin critical Deployments and Secrets by exact name; back up everything else with a label convention. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [Deployment] + names: [api-server, worker] + - kinds: [Secret] + names: [db-credentials, tls-cert] + - kinds: ["*"] + labelSelector: + backup: "true" +``` + +**Expected outcome:** + +- Deployments: only `api-server` and `worker` +- Secrets: only `db-credentials` and `tls-cert` +- Other kinds (ConfigMap, Service, …): resources with `backup=true` only + +**Verify:** `other-deployment` and `no-backup-label-config` should be absent; `backup-labeled-config` and `catch-all-labeled-service` should be present. + +--- + +### Example 11 — Override-only catch-all (no label on catch-all) + +**Goal:** Apply a strict name filter to one kind while including all other kinds without listing them or adding labels. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [Secret] + names: [app-secret] + - kinds: ["*"] # no labelSelector — all other kinds included +``` + +**Expected outcome:** + +- Secrets: only `app-secret` +- Other kinds in `ns-a`: all instances included (subject to global filters and allowlist semantics for listed vs unlisted kinds via catch-all) + +Use this when you need a narrow exception for one type and broad inclusion for the rest of the namespace. + +--- + +### Example 12 — Cluster-scoped refinement + +**Goal:** Refine which cluster-scoped resources are backed up by name and label, without replacing global cluster-scoped inclusion. + +**Policy:** + +```yaml +version: v1 +clusterScopedFilterPolicy: + resourceFilters: + - kinds: [StorageClass] + names: ["my-app-*"] + - kinds: [ClusterRole, ClusterRoleBinding] + labelSelector: + app: my-app +``` + +**Backup (required):** You must still include cluster-scoped kinds on the Backup: + +```yaml +spec: + includedNamespaces: + - ns-a + includedClusterScopedResources: + - storageclasses + - clusterroles + - clusterrolebindings + resourcePolicy: + kind: configmap + name: cluster-scoped-filter-policy +``` + +**Expected outcome (full overlay):** + +- StorageClasses matching `my-app-*` only +- ClusterRoles and ClusterRoleBindings with `app=my-app` only +- Namespace-scoped resources in `ns-a`: global filters (no `namespacedFilterPolicies` in this example) + +**Partial overlay:** If `includedClusterScopedResources` lists only `clusterroles` and `clusterrolebindings`, StorageClasses are **not** backed up even if listed in `clusterScopedFilterPolicy` — global inclusion is evaluated first. + +**Differences from namespace policies:** + +- **Not** an allowlist — unlisted cluster-scoped kinds fall back to global filters. +- **No catch-all** — `kinds: []` or `kinds: ["*"]` is invalid and fails validation. + +--- + +### Example 13 — Global `includeExcludePolicy` and namespace filters + +**Goal:** Set a global resource-type baseline, then refine per namespace. Understand that global **exclusions** cannot be overridden per namespace. + +**Policy:** + +```yaml +version: v1 +includeExcludePolicy: + includedNamespaceScopedResources: + - configmaps + - secrets + - deployments + - services +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [ConfigMap, Secret] + labelSelector: + app: my-app + - namespaces: + - production + resourceFilters: + - kinds: [ConfigMap] + names: ["app-*"] +``` + +**Expected outcome:** + +- **ns-a:** ConfigMaps and Secrets with `app=my-app` (within global allowlist) +- **production:** ConfigMaps matching `app-*` pattern +- **Other included namespaces:** Only kinds allowed by `includeExcludePolicy` (no per-namespace override) + +**Global exclusion wins (important):** + +```yaml +includeExcludePolicy: + excludedNamespaceScopedResources: + - secrets +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [ConfigMap, Secret, Deployment] + labelSelector: + app: my-app +``` + +**Result:** No Secrets in the backup — the namespace policy cannot re-include a globally excluded kind. Velero logs a warning at backup start if you list an excluded kind in `namespacedFilterPolicies`. + +**Backup tip:** Do not set `includedResources` on the Backup; use `includeExcludePolicy` in the ConfigMap instead. + +--- + +### Example 14 — Volume policies and namespace filters together + +**Goal:** Use volume snapshot/fs-backup rules and namespace filters in one ConfigMap. + +**Policy:** + +```yaml +version: v1 +volumePolicies: + - conditions: + capacity: "0,10Gi" + storageClass: + - standard + action: + type: fs-backup + - conditions: + capacity: "10Gi,100Gi" + action: + type: snapshot +namespacedFilterPolicies: + - namespaces: + - production + resourceFilters: + - kinds: [ConfigMap] + names: ["app-*"] + excludedNames: ["*-tmp", "*-debug"] + - kinds: [Secret] + labelSelector: + workload: application +``` + +**Expected outcome:** Volume actions apply to PVCs per `volumePolicies`; resource inclusion follows `namespacedFilterPolicies`. The sections are independent. + +--- + +### Example 15 — `velero.io/exclude-from-backup=true` always wins + +**Goal:** Ensure explicitly excluded resources never appear in the backup, even when they match namespace filters or catch-all rules. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [ConfigMap, Secret] + labelSelector: + app: my-app + - kinds: ["*"] + labelSelector: + app: my-app +``` + +**On resources to exclude**, set: + +```yaml +metadata: + labels: + velero.io/exclude-from-backup: "true" +``` + +**Expected outcome:** Resources with `app=my-app` **and** `velero.io/exclude-from-backup=true` are excluded. Same rule applies to cluster-scoped resources refined by `clusterScopedFilterPolicy`. + +--- + +## Concepts reference + +### `resourceFilters` fields + +| Field | Description | +|-------|-------------| +| `kinds` | Resource type names (e.g. `ConfigMap`, `deployments`). Empty or `["*"]` = catch-all (namespace policies only). | +| `labelSelector` | Equality labels (`key: value`), AND across keys. No `in`, `exists`, etc. — use `orLabelSelectors` for OR. | +| `orLabelSelectors` | List of label maps; match if **any** map matches (AND within each map). Mutually exclusive with `labelSelector`. | +| `names` | Exact names or glob patterns to include. | +| `excludedNames` | Patterns to exclude; wins over `names` when both match. | + +Only kinds listed in `resourceFilters` (or covered by catch-all) are collected from namespaces matched by `namespacedFilterPolicies`. + +### Glob pattern syntax + +Name and namespace patterns use the same glob style as elsewhere in Velero (`gobwas/glob`): + +- Supported: `*`, `?`, `[abc]`, `[a-z]` +- Not supported: `**`, regex, `|`, `()`, `!`, `{}`, `,` + +Examples: `app-*`, `team-frontend-*`, `*-tmp`. + +### Precedence cheat sheet + +**Namespaces** + +1. `BackupSpec.excludedNamespaces` — excluded namespaces are never backed up; namespace policies cannot override this. +2. `namespacedFilterPolicies` — first matching pattern (exact match checked before globs in pattern order). +3. No match — use global BackupSpec + `includeExcludePolicy`. + +**Namespace-scoped resources (when a namespace policy matches)** + +1. Global `includeExcludePolicy` exclusions (e.g. `excludedNamespaceScopedResources`) apply first. +2. Only kinds in `resourceFilters` (or catch-all) are allowlisted for collection. +3. Per-kind `labelSelector` / `orLabelSelectors` for API list calls. +4. Per-kind `names` / `excludedNames` at backup write time. +5. Label `velero.io/exclude-from-backup=true` always excludes. + +**Cluster-scoped resources** + +1. Must be allowed by `includedClusterScopedResources` / global cluster settings. +2. If `clusterScopedFilterPolicy` lists the kind, apply its label and name rules. +3. If not listed in `clusterScopedFilterPolicy`, use global BackupSpec filters. +4. `velero.io/exclude-from-backup=true` always excludes. + +```mermaid +flowchart TD + nsGlobal[BackupSpec namespace include/exclude] + nsPolicy{namespacedFilterPolicies match?} + nsAllow[Allowlist kinds + per-kind filters] + nsGlobalFallback[Global BackupSpec + includeExcludePolicy] + + nsGlobal --> nsPolicy + nsPolicy -->|yes| nsAllow + nsPolicy -->|no| nsGlobalFallback + + csInclude[includedClusterScopedResources] + csPolicy{kind in clusterScopedFilterPolicy?} + csRefine[Per-kind label and name rules] + csGlobal[Global cluster filters] + + csInclude --> csPolicy + csPolicy -->|yes| csRefine + csPolicy -->|no| csGlobal +``` + +### Catch-all summary + +| Rule | Detail | +|------|--------| +| Syntax | `kinds: ["*"]` or `kinds: []` | +| Count | At most one catch-all per `namespacedFilterPolicies` entry | +| Names | `names` / `excludedNames` not allowed on catch-all | +| Override | Kind-specific entries take precedence over catch-all | +| Label inheritance | Does not use `BackupSpec.labelSelector` | +| Cluster-scoped | Catch-all **not** supported in `clusterScopedFilterPolicy` | + +--- + +## Troubleshooting and validation + +### Verify a backup + +```bash +velero backup describe BACKUP_NAME +velero backup logs BACKUP_NAME +velero backup describe BACKUP_NAME -o json | jq '.namespacedFilterPolicies' +velero backup describe BACKUP_NAME -o json | jq '.clusterScopedFilterPolicy' +``` + +Catch-all entries appear as ` (all other kinds)` in text output, or `"isCatchAll": true` in JSON. + +### Common misconfigurations + +| Symptom | Likely cause | Fix | +|---------|----------------|-----| +| Fewer resources than expected in `team-frontend-prod` | Broad namespace pattern listed before specific one | Reorder policies: most specific `namespaces` first | +| Namespace policy lists Secrets but none in backup | `includeExcludePolicy` excludes `secrets` globally | Remove global exclusion or accept no Secrets | +| `ClusterRole` in namespace policy has no effect | Cluster-scoped kind in `namespacedFilterPolicies` | Move rule to `clusterScopedFilterPolicy`; check logs for warning | +| Backup fails at creation with filter message | Old-style `includedResources` with new policies | Move resource types to `includeExcludePolicy` in ConfigMap | +| Catch-all does not use backup-wide label | By design | Set `labelSelector` on the catch-all entry | +| Cluster-scoped policy validation error on `kinds: ["*"]` | Catch-all not allowed for cluster policy | List each cluster-scoped kind explicitly | + +### Velero logs + +```bash +kubectl logs -n velero deployment/velero | grep -i "namespacedFilterPolicies\|clusterScopedFilterPolicy" +kubectl logs -n velero deployment/velero | grep "globally excluded by includeExcludePolicy" +kubectl logs -n velero deployment/velero | grep "cluster-scoped" +``` + +### Validation errors (policy ConfigMap) + +Velero validates the ResourcePolicy when a backup starts. Common errors: + +| Error (summary) | Cause | +|-----------------|--------| +| `at least one namespace must be specified` | Empty `namespaces: []` | +| `at least one resourceFilter must be specified` | Empty `resourceFilters: []` | +| `names or excludedNames cannot be specified for catch-all filters` | Name patterns on catch-all entry | +| `only one catch-all resource filter is allowed` | Multiple catch-alls in one policy entry | +| `kind "X" appears in both resourceFilters[...]` | Same kind in two entries | +| `labelSelector and orLabelSelectors cannot co-exist` | Both set in one entry | +| `duplicate namespace pattern` | Same namespace string in two policy entries | +| `invalid glob pattern` | Bad characters in namespace or name pattern | +| `clusterScopedFilterPolicy... kinds must be specified (catch-all is not supported)` | Empty or `["*"]` kinds in cluster policy | +| `include-resources, exclude-resources... cannot be used with namespace-scoped or cluster-scoped global filter policies` | Old-style BackupSpec filters with new policy | + +### Silent edge cases (no error) + +- Namespace pattern matches no existing namespace — policy loaded but never applied. +- Kind listed but no instances in namespace — empty result, backup still succeeds. +- `excludedNames` narrows `names` — e.g. `names: ["app-*"]` + `excludedNames: ["app-config"]` excludes `app-config` only. + +--- + +## Restore behavior + +Restore is unchanged: it restores whatever is in the backup archive. Resources excluded by fine-grained filters are simply absent. Use `Restore.spec.includedNamespaces` (and existing restore filters) to limit what you restore from a partial backup. + +Fine-grained resource filtering is also available on the restore path using `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. For details on the restore-side policies, see the [Fine-grained restore filters design](https://github.com/vmware-tanzu/velero/blob/main/design/restore-filter-enhancement/fine-grained-restore-filters-design.md). + +--- + +## Related links + +- [Fine-grained backup filters design](https://github.com/velero-io/velero/blob/main/design/backup-filter-enhancement/fine-grained-backup-filters-design.md) diff --git a/site/data/docs/main-toc.yml b/site/data/docs/main-toc.yml index 271705a1b..6008d5d66 100644 --- a/site/data/docs/main-toc.yml +++ b/site/data/docs/main-toc.yml @@ -33,6 +33,8 @@ toc: url: /enable-api-group-versions-feature - page: Resource filtering url: /resource-filtering + - page: Fine-Grained Backup Filters + url: /fine-grained-backup-filters - page: Namespace glob patterns url: /namespace-glob-patterns - page: Backup reference From e9a778b848fe697343fb3e4134dd3abadb30830e Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Tue, 21 Jul 2026 10:49:55 +0800 Subject: [PATCH 065/194] update backup filters example 14 update the excludeNames to match example 3 for better consistency. Signed-off-by: Adam Zhang --- site/content/docs/main/fine-grained-backup-filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/docs/main/fine-grained-backup-filters.md b/site/content/docs/main/fine-grained-backup-filters.md index c8e7da63b..01f5aef8a 100644 --- a/site/content/docs/main/fine-grained-backup-filters.md +++ b/site/content/docs/main/fine-grained-backup-filters.md @@ -595,7 +595,7 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap] names: ["app-*"] - excludedNames: ["*-tmp", "*-debug"] + excludedNames: ["*-tmp-*", "*-debug-*", "*-tmp", "*-debug"] - kinds: [Secret] labelSelector: workload: application From ac76402aa0957a536db44ddb6cad7ec0ec94b8c1 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Wed, 22 Jul 2026 13:34:11 +0800 Subject: [PATCH 066/194] design for RIA must-include-additional-items Design for `restore.velero.io/must-include-additional-items` annotation and its usage and interaction with existing filtering mechanism. Signed-off-by: Adam Zhang --- changelogs/unreleased/10056-adam-jian-zhang | 1 + ...ria-must-include-addtional-items-design.md | 357 ++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 changelogs/unreleased/10056-adam-jian-zhang create mode 100644 design/ria-must-include-addtional-items-design.md diff --git a/changelogs/unreleased/10056-adam-jian-zhang b/changelogs/unreleased/10056-adam-jian-zhang new file mode 100644 index 000000000..18bd93cc6 --- /dev/null +++ b/changelogs/unreleased/10056-adam-jian-zhang @@ -0,0 +1 @@ +RIA must include additional items design diff --git a/design/ria-must-include-addtional-items-design.md b/design/ria-must-include-addtional-items-design.md new file mode 100644 index 000000000..95f6863fd --- /dev/null +++ b/design/ria-must-include-addtional-items-design.md @@ -0,0 +1,357 @@ +# RestoreItemAction Must-Include Additional Items + +## Abstract + +Backup Item Actions (BIAs) can already mark additional items as must-include via `backup.velero.io/must-include-additional-items`, so Velero bypasses resource and namespace exclusion filters when backing those dependencies up. +This proposal adds the same plugin-controlled escape hatch on restore: `restore.velero.io/must-include-additional-items`, so Restore Item Actions (RIAs) can force-restore declared `AdditionalItems` even when they would otherwise be dropped by global restore filters. + +## Glossary & Abbreviation + +**Additional Item**: A resource identifier returned by a Backup/Restore Item Action's `Execute()` result that Velero should process as a dependency of the current item. +**BIA**: Backup Item Action plugin. +**RIA**: Restore Item Action plugin. +**Must-Include**: A plugin-set annotation on the action's `UpdatedItem` that tells Velero to bypass global include/exclude filters for that action's `AdditionalItems`. +**Global Restore Filter**: `RestoreSpec` filters applied uniformly — `IncludedNamespaces`/`ExcludedNamespaces`, `IncludedResources`/`ExcludedResources`, `IncludeClusterResources`, and label selectors. +**Fine-Grained Restore Filter**: Per-namespace / cluster-scoped policies from `RestoreSpec.ResourcePolicy` (`namespacedFilterPolicies`, `clusterScopedFilterPolicy`), as described in [Fine Grained Restore Filters via Resource Policies](https://github.com/velero-io/velero/blob/main/design/restore-filter-enhancement/fine-grained-restore-filters-design.md). +**`resourceMustHave`**: A small hardcoded server-side set of resource types that bypass resource and namespace I/E checks inside `restoreItem()` today (but not `IncludeClusterResources=false`). + +## Background + +### Backup-side precedent + +On backup, a BIA may set `backup.velero.io/must-include-additional-items: "true"` on the returned `UpdatedItem`. +Velero strips that annotation (it is an internal signal, not intended to land on the live object) and passes `mustInclude=true` into recursive `backupItem` calls for that action's `AdditionalItems`. +When `mustInclude` is true, `itemInclusionChecks` skips namespace/resource exclusion checks (and related exclusion labels / fine-grained name filters) so plugin-declared dependencies are not dropped by the user's backup filters. +In-tree CSI BIAs already rely on this for VolumeSnapshot / VolumeSnapshotContent / VolumeSnapshotClass style dependency chains. + +### Restore-side gap + +On restore, RIAs can return `AdditionalItems`, and Velero recursively calls `restoreItem()` for each of them. +That path already bypasses fine-grained restore filters and global label selectors, because those are evaluated earlier in `getOrderedResourceCollection` / `getSelectedRestoreableItems`. +However, `restoreItem()` still enforces global resource includes/excludes, namespace includes/excludes, and `IncludeClusterResources=false`. + +The fine-grained restore filters design explicitly documents this remaining floor: + +> Note that these additional items must still pass global resource/namespace exclusions. + +There is no restore-side equivalent of the BIA must-include annotation. +Plugins that need a hard dependency restored despite a selective restore configuration have no opt-in way to express that, short of relying on the server-side `resourceMustHave` list (which is global, not plugin-scoped, and does not bypass `IncludeClusterResources=false`). + +### Motivating scenario + +Consider a selective restore that includes only application namespaces and excludes storage/snapshot resource types, while a plugin knows that restoring a PVC correctly requires a related cluster-scoped or cross-namespace dependency that exists in the backup archive. +Today the RIA can request that dependency as an `AdditionalItem`, but Velero will skip it at the global exclusion checks inside `restoreItem()`. +With a restore must-include annotation, the plugin can declare the dependency as required and Velero will restore it (provided the object is present in the backup tarball). + +## Goals + +- Add `restore.velero.io/must-include-additional-items` with the same parent-annotation contract as the backup-side must-include annotation. +- When an RIA sets the annotation on `UpdatedItem`, bypass global resource I/E, namespace I/E, and `IncludeClusterResources=false` for that RIA's `AdditionalItems`. +- Keep the change opt-in and backward compatible: restores and plugins that do not set the annotation behave exactly as today. +- Document the trust model, precedence rules, and interaction with existing restore gates for plugin authors and operators. + +## Non-Goals + +- Changing the plugin protobuf / `RestoreItemAction` interface shape (no new RPC fields). +- Changing CRDs or adding CLI flags. +- Changing the `resourceMustHave` list (including any narrowing related to VolumeSnapshotContent). +- Updating in-tree RIAs (CSI or otherwise) to set the new annotation as part of this change. +- Per-additional-item granularity (the annotation applies blanket to all `AdditionalItems` from that RIA invocation, matching BIA). +- Materializing items that were never backed up. + +## High-Level Design + +Mirror the backup workflow: + +1. Introduce annotation constant `restore.velero.io/must-include-additional-items`. +2. After each RIA `Execute()`, if `UpdatedItem` carries the annotation with value `"true"`, strip it and set `mustIncludeAdditionalItems=true`. +3. Pass that boolean into recursive `restoreItem(..., mustInclude)` calls for the action's `AdditionalItems`. +4. When `mustInclude` is true, skip the global resource/namespace/`IncludeClusterResources` exclusion checks inside `restoreItem()`. +5. Keep all non-filter gates unchanged (tarball presence, already-restored, completed Jobs, API errors, wait-for-additional-items, etc.). + +Top-level items from the archive continue to be restored with `mustInclude=false`, so user filters still apply to the primary restore set. + +```mermaid +flowchart TD + startRestore[Start Restore] --> readTarball[Read Item from Backup Tarball] + readTarball --> topLevelRestoreItem["restoreItem(..., mustInclude=false)"] + + topLevelRestoreItem --> checkMustInclude{"mustInclude == true?"} + + checkMustInclude -- No --> checkFilters{"Pass Global Resource/Namespace Filters?"} + checkFilters -- No --> skipItem[Skip Restore] + checkFilters -- Yes --> nonFilterGates["Other gates: isCompleted, already-restored, ..."] + + checkMustInclude -- Yes --> nonFilterGates + + nonFilterGates --> executeRIA[Execute RestoreItemAction] + + executeRIA --> checkSkip{"SkipRestore?"} + checkSkip -- Yes --> skipItem + checkSkip -- No --> checkAnnotation{"Has must-include annotation?"} + + checkAnnotation -- Yes --> stripAnnotation[Strip Annotation] + stripAnnotation --> setFlagTrue["mustIncludeAdditionalItems = true"] + + checkAnnotation -- No --> setFlagFalse["mustIncludeAdditionalItems = false"] + + setFlagTrue --> loopAdditionalItems[Loop over AdditionalItems] + setFlagFalse --> loopAdditionalItems + + loopAdditionalItems --> existsInBackup{"Item file in tarball?"} + existsInBackup -- No --> warnSkip[Warn and skip] + existsInBackup -- Yes --> recursiveRestoreItem["restoreItem(..., mustInclude=mustIncludeAdditionalItems)"] + recursiveRestoreItem --> checkMustInclude +``` + +> The edge `recursiveRestoreItem --> checkMustInclude` is a recursive call (new `restoreItem` stack frame), not a same-frame loop. + +## Detailed Design + +### Annotation constant + +In `pkg/apis/velero/v1/labels_annotations.go`, next to the existing backup constant: + +```go +// Velero checks this annotation to determine whether to skip resource excluding check. +MustIncludeAdditionalItemAnnotation = "backup.velero.io/must-include-additional-items" + +// MustIncludeAdditionalItemRestoreAnnotation is set by RestoreItemActions on the UpdatedItem +// to tell Velero to bypass global resource/namespace exclusion checks (and IncludeClusterResources=false) +// for that action's AdditionalItems. Value must be "true". The annotation is stripped before +// the item is applied to the cluster. +// +// Notice: SkipRestore on the Execute output takes precedence. If SkipRestore is true, the +// annotation is never inspected and AdditionalItems are not processed. +MustIncludeAdditionalItemRestoreAnnotation = "restore.velero.io/must-include-additional-items" +``` + +Only the string value `"true"` enables the bypass (same as backup). + +### `restoreItem` signature + +```go +func (ctx *restoreContext) restoreItem( + obj *unstructured.Unstructured, + groupResource schema.GroupResource, + namespace string, + mustInclude bool, +) (results.Result, results.Result, bool) +``` + +Call sites: + +| Site | `mustInclude` value | +|---|---| +| Top-level restore loop | `false` | +| Recursive additional-item restore after an RIA | derived from that RIA's `UpdatedItem` annotation | + +### Bypass exclusion checks; keep namespace creation + +Today, namespace exclusion and `EnsureNamespaceExistsAndIsReady` share one `if namespace != ""` block in `restoreItem()`. +If must-include only skipped the exclusion check without refactoring, an additional item targeting an excluded namespace would fail because its target namespace was never ensured. + +Required structure: + +```go +if mustInclude { + restoreLogger.Info("Skipping the resource/namespace exclusion checks because the item is marked as must-include") +} else { + if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) && !ctx.resourceMustHave.Has(groupResource.String()) { + restoreLogger.Info("Not restoring item because resource is excluded") + return warnings, errs, itemExists + } + + if namespace != "" { + if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) { + restoreLogger.Info("Not restoring item because namespace is excluded") + return warnings, errs, itemExists + } + } else { + if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) { + restoreLogger.Info("Not restoring item because it's cluster-scoped") + return warnings, errs, itemExists + } + } +} + +// Namespace creation runs regardless of mustInclude. +if namespace != "" { + nsToEnsure := getNamespace(restoreLogger, archive.GetItemFilePath(ctx.restoreDir, "namespaces", "", obj.GetNamespace()), namespace) + _, nsCreated, err := kube.EnsureNamespaceExistsAndIsReady(nsToEnsure, ctx.namespaceClient, ctx.resourceTerminatingTimeout, ctx.resourceDeletionStatusTracker) + // ... existing error handling and restoredItems bookkeeping ... +} +``` + +Namespace remapping is unchanged: exclusion checks use the original namespace (`obj.GetNamespace()`); namespace creation uses the remapped target `namespace` parameter. + +### Process the annotation after each RIA + +Inside the applicable-actions loop in `restoreItem()`, after `SkipRestore` handling and type-asserting `UpdatedItem`: + +```go +obj = unstructuredObj + +mustIncludeAdditionalItems := false +if annotations := obj.GetAnnotations(); annotations != nil && + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] == "true" { + mustIncludeAdditionalItems = true + restoreLogger.Info("RestoreItemAction marked additional items as must-include; bypassing resource/namespace exclusion checks for them") + delete(annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation) + obj.SetAnnotations(annotations) +} + +for _, additionalItem := range executeOutput.AdditionalItems { + // existing tarball stat / unmarshal / namespace mapping ... + w, e, additionalItemExists := ctx.restoreItem( + additionalObj, + additionalItem.GroupResource, + additionalItemNamespace, + mustIncludeAdditionalItems, + ) + // existing merge / filteredAdditionalItems bookkeeping ... +} +``` + +### Filter bypass matrix + +| Gate | Plain AdditionalItem | `resourceMustHave` | RIA `mustInclude=true` | BIA `mustInclude=true` (parity target) | +|---|---|---|---|---| +| Fine-grained policies (kind/name/label) | Bypass (never enter selection Phase B filters) | N/A in `restoreItem` | Bypass (same) | Bypass | +| Global label selectors | Bypass (never re-enter selection) | N/A in `restoreItem` | Bypass (same) | Bypass | +| Global resource I/E | Honored | Bypass | Bypass | Bypass | +| Global namespace I/E | Honored | Bypass | Bypass | Bypass | +| `IncludeClusterResources=false` | Honored | Honored (not bypassed) | Bypass | Bypass | +| Item must exist in backup tarball | Required | Required | Required | N/A (fetched from cluster) | +| `isCompleted` / already-restored / API errors | Still apply | Still apply | Still apply | `DeletionTimestamp` still applies on backup | + +RIA must-include is intentionally a **stronger** override than `resourceMustHave` because it also bypasses `IncludeClusterResources=false`. +That matches BIA must-include semantics (plugin-trusted hard dependencies), rather than widening the hardcoded server list. + +### Interaction with fine-grained restore filters + +Per [Fine Grained Restore Filters via Resource Policies](../restore-filter-enhancement/fine-grained-restore-filters-design.md), plugin additional items already bypass `namespacedFilterPolicies` / `clusterScopedFilterPolicy` kind, name, and label checks. +Those filters live in the selection phases; additional items enter `restoreItem()` directly. + +This proposal only changes the remaining global gates inside `restoreItem()`. +With must-include set, an additional item effectively bypasses **all** restore filters (fine-grained and global). +Without the annotation, behavior is unchanged: fine-grained filters are still bypassed, global exclusions still apply. + +### Interaction with existing restore gates + +#### `SkipRestore` precedence + +If `Execute()` returns `SkipRestore: true`, `restoreItem()` returns before inspecting the annotation, and no `AdditionalItems` are processed. +This mirrors backup-side precedence where `velero.io/skip-from-backup` outranks must-include. + +#### Multi-RIA semantics + +Annotation handling is per RIA invocation inside the actions loop: + +1. RIA N executes → inspect/strip annotation on that `UpdatedItem` → restore that RIA's `AdditionalItems` with the derived flag. +2. RIA N+1 sees the already-stripped object unless it sets the annotation again. + +A later RIA does not inherit an earlier RIA's must-include decision. + +#### Transitive propagation + +The parent's `mustInclude` flag admits the child additional item through filters. +It does **not** automatically force-include grandchildren. +Each RIA level that needs the escape hatch must set the annotation on its own `UpdatedItem`, matching BIA behavior. + +#### Non-filter gates that still apply + +Even when `mustInclude=true`: + +- Missing archive file → warn and skip (existing behavior). +- `isCompleted` resources (e.g. completed Jobs) → skip. +- Already present in `ctx.restoredItems` → skip. +- Create/update API failures → errors as today. +- `WaitForAdditionalItems` / `AreAdditionalItemsReady` polling after the additional-item loop → unchanged. + +### Relationship to `resourceMustHave` + +| Mechanism | Who decides | Bypasses resource/ns I/E | Bypasses `IncludeClusterResources=false` | +|---|---|---|---| +| `resourceMustHave` | Velero server (hardcoded) | Yes | No | +| RIA must-include | Plugin author (annotation) | Yes | Yes | + +The two mechanisms coexist. +This proposal does not migrate in-tree CSI (or other) RIAs onto the annotation. +Doing so would be a separate behavior change: it could force-restore types users explicitly excluded, and would newly restore cluster-scoped dependencies even when `IncludeClusterResources=false`. + +### Plugin usage sketch + +```go +func (p *myRestoreAction) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: schema.GroupResource{Group: "example.io", Resource: "dependencies"}, Namespace: "dep-ns", Name: "dep-1"}, + }, + }, nil +} +``` + +Plugin authors must ensure the additional item was actually captured in the backup (typically via the corresponding BIA also using `backup.velero.io/must-include-additional-items`). + +### Tests + +Extend restore coverage (existing `TestRestoreActionAdditionalItems` patterns / focused cases) for: + +1. Resource exclusion bypass with annotation; still skipped without annotation. +2. Namespace exclusion bypass **and** target namespace creation. +3. `IncludeClusterResources=false` bypass for cluster-scoped additional items. +4. Annotation stripped from the object applied to the cluster. +5. `SkipRestore: true` prevents additional-item processing even if the annotation is set. +6. Missing tarball entry still warns and skips. +7. Transitive case: child RIA must re-set the annotation for grandchildren. +8. Top-level restore path still passes `mustInclude=false` and honors filters. + +### Documentation + +- Constant doc comment (including `SkipRestore` precedence). +- Plugin-author docs for Restore Item Actions: annotation key/value, blanket scope, filter-bypass matrix, namespace-creation side effect, tarball requirement. + +## Security Considerations + +Installing an RIA that sets this annotation grants that plugin authority to restore dependencies outside the operator's restore filters, including: + +- resources in namespaces the restore excluded (and creation of those target namespaces if needed); +- resource types the restore excluded; +- cluster-scoped resources even when `IncludeClusterResources=false`. + +This matches the existing BIA trust model: item-action plugins are already privileged components of the Velero deployment. +Operators should treat RIA installation as a trust decision. +The annotation is stripped before apply so it does not persist as attacker-controlled cluster state from the backup archive alone; a matching RIA must run and return `AdditionalItems` for the bypass to take effect. + +## Compatibility + +- No CRD or plugin interface changes. +- Existing restores unchanged when no RIA sets the annotation. +- Existing tests that assert additional items are dropped under namespace filters / `IncludeClusterResources=false` remain valid for the no-annotation path. +- Compatible with fine-grained restore filters: additional items already bypass those filters; this proposal only addresses the documented global-exclusion floor. + +## Alternatives Considered + +### Per-item must-include on each `ResourceIdentifier` + +Pros: selective control within one `AdditionalItems` list. +Cons: requires API changes to `ResourceIdentifier` or a parallel structure; diverges from BIA; plugins that need selectivity can already split across actions or omit non-required items. + +Rejected for this proposal; may be revisited later if plugin authors demonstrate a concrete need. + +### Widen `resourceMustHave` instead of a plugin annotation + +Pros: no plugin contract change. +Cons: server-forced, global, not scoped to a plugin call; does not give third-party plugins a general tool; does not match BIA; conflicts with efforts to keep hardcoded force-include lists narrow. + +Rejected — wrong trust model for a general plugin escape hatch. From 7bbd172684dc6bb92801a0d1f91b7047b85980d8 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Wed, 22 Jul 2026 17:52:01 +0800 Subject: [PATCH 067/194] persist source dev size Signed-off-by: Lyndon-Li --- pkg/uploader/block/snapshot.go | 12 +++++++++++- pkg/uploader/block/uploader.go | 10 ++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/uploader/block/snapshot.go b/pkg/uploader/block/snapshot.go index e30f5c1bb..b185f4e15 100644 --- a/pkg/uploader/block/snapshot.go +++ b/pkg/uploader/block/snapshot.go @@ -222,7 +222,17 @@ func Restore(ctx context.Context, blkUp Uploader, rep udmrepo.BackupRepo, snapsh defer destDev.Close() - size, err := blkUp.Restore(snapshot, destInfo{dev: destDev, path: destPath}, bitmap.Iterator(), uploaderCfg) + destSize, err := destDev.Seek(0, io.SeekEnd) + if err != nil { + return 0, errors.Wrapf(err, "error getting length of block device %s", dest) + } + + _, err = destDev.Seek(0, io.SeekStart) + if err != nil { + return 0, errors.Wrapf(err, "error reset pos of block device %s", dest) + } + + size, err := blkUp.Restore(snapshot, destInfo{dev: destDev, path: destPath, size: destSize}, bitmap.Iterator(), uploaderCfg) if err != nil { return 0, errors.Wrapf(err, "error restoring to block dev %s", destPath) } diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 75e913cb7..1e4982483 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -21,6 +21,7 @@ import ( "io" "os" "runtime" + "strconv" "strings" "github.com/cockroachdb/errors" @@ -35,8 +36,9 @@ import ( var ErrCanceled = errors.New("uploader is canceled") const ( - blockSize = (1 << 20) - bufferSize = 100 << 20 + blockSize = (1 << 20) + bufferSize = 100 << 20 + bdevSourceSizeTag = "bdev-source-size" ) type sourceInfo struct { @@ -48,6 +50,7 @@ type sourceInfo struct { type destInfo struct { dev *os.File path string + size int64 } type Uploader interface { @@ -134,6 +137,9 @@ func (blkup *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, b Type: udmrepo.ObjectDataTypeMetadata, Permissions: 0o777, }, + Tags: map[string]string{ + bdevSourceSizeTag: strconv.FormatInt(source.size, 10), + }, }, backupSize, nil } From bec292e738fce49f221ba16a2e3a6247acb37563 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Wed, 22 Jul 2026 17:58:02 +0800 Subject: [PATCH 068/194] block uploader restore data Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader.go | 63 ++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 1e4982483..0567edb8c 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -143,9 +143,46 @@ func (blkup *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, b }, backupSize, nil } -// TODO implement in following PRs func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bitmap cbt.Iterator, configs map[string]string) (int64, error) { - return 0, errors.New("not implemented") + if bitmap == nil { + return 0, errors.New("bitmap is not available") + } + + meta, err := blkup.repoWriter.ReadMetadata(blkup.ctx, snapshot.RootObject.ID) + if err != nil { + return 0, errors.Wrapf(err, "error readding snapshot metadata for %s", snapshot.Description) + } + + if len(meta.SubObjects) != 1 { + return 0, errors.Wrapf(err, "unexpected number of bdev object (%d) for snapshot %s", len(meta.SubObjects), snapshot.Description) + } + + sourceSize, err := getSourceSize(snapshot) + if err != nil { + sourceSize = meta.SubObjects[0].Size + blkup.log.Warnf("Failed to get source size from snapshot %s, use backup size %v", snapshot.Description, sourceSize) + } + + if sourceSize > meta.SubObjects[0].Size { + return 0, errors.Wrapf(err, "unexpected size (%v vs. %v) for bdev object %s", meta.SubObjects[0].Size, sourceSize, meta.SubObjects[0].Name) + } + + if sourceSize > dest.size { + return 0, errors.Wrapf(err, "dest dev(%s) size is too small (%v vs. %v)", dest.path, dest.size, sourceSize) + } + + reader, err := blkup.repoWriter.OpenObject(blkup.ctx, meta.SubObjects[0].ID) + if err != nil { + return 0, errors.Wrapf(err, "error opening bdev object %v", meta.SubObjects[0].Name) + } + defer reader.Close() + + size, err := blkup.restoreData(reader, dest.dev, bitmap, sourceSize, dest.path) + if err != nil { + return 0, errors.Wrapf(err, "error restoring bdev object %s to volume %s", meta.SubObjects[0].Name, dest.path) + } + + return size, nil } func (blkup *blockUploader) backupObject(dev *os.File, dest udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (udmrepo.ID, int64, int64, error) { @@ -324,6 +361,28 @@ func getObjectName(source string) string { return strings.Trim(s, "-") } +func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bitmap cbt.Iterator, totalLength int64, destPath string) (int64, error) { + return 0, nil +} + +func getSourceSize(snapshot udmrepo.Snapshot) (int64, error) { + if snapshot.Tags == nil { + return 0, errors.New("source size tag is empty") + } + + s, found := snapshot.Tags[bdevSourceSizeTag] + if !found { + return 0, errors.New("source size tag is missing") + } + + size, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0, errors.Wrapf(err, "error parsing size from %s", s) + } + + return size, nil +} + func loadObjectFromSnapshot(ctx context.Context, rep udmrepo.BackupRepo, snapshot *udmrepo.Snapshot) (udmrepo.ID, error) { if snapshot == nil { return "", errors.New("snapshot is empty") From 9c1d8bee71a529b52f1e0a3d3c710b5b18836464 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Wed, 22 Jul 2026 18:00:13 +0800 Subject: [PATCH 069/194] block uploader restore data Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader.go | 154 ++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 0567edb8c..30908b396 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -17,6 +17,7 @@ limitations under the License. package block import ( + "bytes" "context" "io" "os" @@ -362,7 +363,158 @@ func getObjectName(source string) string { } func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bitmap cbt.Iterator, totalLength int64, destPath string) (int64, error) { - return 0, nil + list := freelist.New(bufferSize, blockSize) + resultChan := make(chan readResult, list.Capacity()) + zeroBlock := make([]byte, blockSize) + totalCount := bitmap.Count() + + quit := make(chan struct{}) + defer close(quit) + + go func() { + defer close(resultChan) + + offset, valid := bitmap.Next() + var buffer []byte + var nextPos uint64 = uint64(0) + for valid { + select { + case <-blkup.ctx.Done(): + return + case <-quit: + return + case buffer = <-list.Chunks(): + } + + var err error + + if nextPos != offset { + _, err = reader.Seek(int64(offset), io.SeekStart) + } + + if err == nil { + var length int + length, err = io.ReadFull(reader, buffer) + if err == nil && length <= 0 { + err = io.ErrUnexpectedEOF + } + } + + r := readResult{ + buffer: buffer, + offset: int64(offset), + err: err, + } + + if r.err != nil { + r.resetBuffer(list) + } + + resultChan <- r + + if r.err != nil { + return + } + + nextPos = offset + uint64(blockSize) + offset, valid = bitmap.Next() + } + }() + + var written int64 + var result readResult + var writeErr error + var readerRunning bool + var zeroStart int64 = -1 + var zeroLength int64 + var curCount int64 + + for curCount < int64(totalCount) { + select { + case <-blkup.ctx.Done(): + writeErr = ErrCanceled + case result, readerRunning = <-resultChan: + if !readerRunning { + if blkup.ctx.Err() != nil { + writeErr = ErrCanceled + } else { + writeErr = io.ErrUnexpectedEOF + } + } + } + + if writeErr != nil { + break + } + + if result.err != nil { + writeErr = result.err + break + } + + length := min(int64(blockSize), totalLength-result.offset) + if bytes.Equal(result.buffer, zeroBlock) { + if zeroStart == -1 { + zeroStart = result.offset + zeroLength = length + } else if result.offset == zeroStart+zeroLength { + zeroLength += length + } else { + if err := blkup.flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath); err != nil { + writeErr = errors.Wrapf(err, "error flushing zero blocks from %v, length %v", zeroStart, zeroLength) + break + } + zeroStart = result.offset + zeroLength = length + } + } else { + if zeroStart != -1 { + if err := blkup.flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath); err != nil { + writeErr = errors.Wrapf(err, "error flushing zero blocks from %v, length %v", zeroStart, zeroLength) + break + } + + zeroStart = -1 + zeroLength = 0 + } + + n, err := dest.WriteAt(result.buffer[:length], result.offset) + if err != nil { + writeErr = err + break + } + + if length != int64(n) { + writeErr = io.ErrShortWrite + break + } + } + + written += length + curCount++ + + result.resetBuffer(list) + + blkup.progress.UpdateProgress(&uploader.Progress{BytesDone: written, TotalBytes: totalLength}) + } + + result.resetBuffer(list) + + if writeErr != nil { + return written, writeErr + } + + if zeroStart != -1 { + if err := blkup.flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath); err != nil { + return written, errors.Wrapf(err, "error flushing zero blocks from %v, length %v", zeroStart, zeroLength) + } + } + + return written, nil +} + +func (bu *blockUploader) flushZeroBlocks(dest *os.File, start int64, length int64, zeroBlock []byte, destPath string) error { + return nil } func getSourceSize(snapshot udmrepo.Snapshot) (int64, error) { From 97978ed9b767a0e14b1ed0b8dc50c1d406cc1ef5 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Wed, 22 Jul 2026 18:01:57 +0800 Subject: [PATCH 070/194] block uploader flush zero blocks Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 30908b396..7717a7e7e 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -514,6 +514,29 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit } func (bu *blockUploader) flushZeroBlocks(dest *os.File, start int64, length int64, zeroBlock []byte, destPath string) error { + err := blkZeroOut(dest, start, length) + if err == nil { + return nil + } + + bu.log.WithError(err).Warnf("Failed to call zero out from dev %s, start %v, length %v. Fallback to conservative way", destPath, start, length) + + var written int64 + for written < length { + writeSize := min(len(zeroBlock), int(length-written)) + + n, err := dest.WriteAt(zeroBlock[:writeSize], start+written) + if err != nil { + return errors.Wrapf(err, "error writing zero buffer at %v, length %v", start+written, writeSize) + } + + if writeSize != n { + return errors.Wrapf(err, "short write zero buffer at %v, length %v", start+written, writeSize) + } + + written += int64(writeSize) + } + return nil } From 7ecf06d190fc6c6a1d07bc72d770d4dd30997359 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Wed, 22 Jul 2026 10:14:18 -0400 Subject: [PATCH 071/194] Fix CI: make Bitnami MinIO Dockerfile SHA lookup resilient (#10049) curl piped straight into jq with no error check; a non-JSON or failed HTTP response (rate limit, transient API error) broke jq with an opaque parse error. Add --fail-with-body, retries, and validate the parsed SHA before continuing. Fixes #10048 Signed-off-by: Tiger Kaovilai --- .github/workflows/e2e-test-kind.yaml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index fc77cb4d3..42dcaa707 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -62,8 +62,28 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - DOCKERFILE_SHA=$(curl -s -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/repos/bitnami/containers/commits?path=bitnami/minio/2026/debian-12/Dockerfile\&per_page=1 | jq -r '.[0].sha') - echo "dockerfile_sha=${DOCKERFILE_SHA}" >> $GITHUB_OUTPUT + set -euo pipefail + + url="https://api.github.com/repos/bitnami/containers/commits?path=bitnami/minio/2026/debian-12/Dockerfile&per_page=1" + + response="$(curl --fail-with-body -sS \ + --retry 5 \ + --retry-delay 2 \ + --retry-all-errors \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$url")" + + DOCKERFILE_SHA="$(echo "$response" | jq -r '.[0].sha // empty')" + + if [ -z "$DOCKERFILE_SHA" ]; then + echo "Failed to resolve Bitnami MinIO Dockerfile SHA from GitHub API response" + echo "$response" + exit 1 + fi + + echo "dockerfile_sha=${DOCKERFILE_SHA}" >> "$GITHUB_OUTPUT" - name: Cache MinIO Image uses: actions/cache@v4 id: minio-cache From 1968bf44ee90ec9fa6909e65ed5d7121308a8058 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 09:42:55 -0700 Subject: [PATCH 072/194] Add ResetBackupLastSuccessfulTimestamp to ServerMetrics Add a method to reset all backupLastSuccessfulTimestamp gauge values. This will be used by the backup controller's periodic resync to prune stale metrics for deleted schedules. Fixes #9239 Signed-off-by: Shubham Pampattiwar --- pkg/metrics/metrics.go | 7 +++++++ pkg/metrics/metrics_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 86d78028c..d54eb02b5 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -758,6 +758,13 @@ func (m *ServerMetrics) RegisterPodVolumeOpLatencyGauge(node, pvbName, opName, b } } +// ResetBackupLastSuccessfulTimestamp removes all schedule-level backupLastSuccessfulTimestamp values. +func (m *ServerMetrics) ResetBackupLastSuccessfulTimestamp() { + if g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec); ok { + g.Reset() + } +} + // SetBackupTarballSizeBytesGauge records the size, in bytes, of a backup tarball. func (m *ServerMetrics) SetBackupTarballSizeBytesGauge(backupSchedule string, size int64) { if g, ok := m.metrics[backupTarballSizeBytesGauge].(*prometheus.GaugeVec); ok { diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index a24f2bf33..07004f172 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -457,6 +457,38 @@ func getHistogramCount(t *testing.T, vec *prometheus.HistogramVec, scheduleLabel return 0 } +// TestResetBackupLastSuccessfulTimestamp verifies that ResetBackupLastSuccessfulTimestamp +// removes all schedule-level values from the backupLastSuccessfulTimestamp gauge. +func TestResetBackupLastSuccessfulTimestamp(t *testing.T) { + m := NewServerMetrics() + + now := time.Now() + m.SetBackupLastSuccessfulTimestamp("schedule-1", now) + m.SetBackupLastSuccessfulTimestamp("schedule-2", now.Add(-time.Hour)) + m.SetBackupLastSuccessfulTimestamp("", now.Add(-2*time.Hour)) + + // Verify all three entries exist + g := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec) + assert.Equal(t, 3, collectGaugeCount(t, g)) + + // Reset should remove all entries + m.ResetBackupLastSuccessfulTimestamp() + assert.Equal(t, 0, collectGaugeCount(t, g)) +} + +// collectGaugeCount returns the number of time series in a GaugeVec. +func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { + t.Helper() + ch := make(chan prometheus.Metric, 10) + g.Collect(ch) + close(ch) + count := 0 + for range ch { + count++ + } + return count +} + // TestRepoMaintenanceMetrics verifies that repo maintenance metrics are properly recorded. func TestRepoMaintenanceMetrics(t *testing.T) { tests := []struct { From 8cf03998ddfc643830195128d0c72785ed24cfd0 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 09:47:39 -0700 Subject: [PATCH 073/194] Reset backupLastSuccessfulTimestamp during periodic resync The periodic backup metrics resync in updateTotalBackupMetric only set backupLastSuccessfulTimestamp values but never removed stale entries. When a schedule was deleted and its backups removed, the gauge persisted until the Velero pod was restarted. Reset the gauge before re-setting current values so that deleted schedules are pruned automatically each resync cycle. Fixes #9239 Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller.go | 4 ++- pkg/controller/backup_controller_test.go | 32 ++++++++++++++++++++++++ pkg/metrics/metrics.go | 17 +++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 74b857fd2..7a58424eb 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -215,7 +215,9 @@ func (b *backupReconciler) updateTotalBackupMetric() { } // recompute backup_last_successful_timestamp metric for each - // schedule (including the empty schedule, i.e. ad-hoc backups) + // schedule (including the empty schedule, i.e. ad-hoc backups). + // Reset first to prune stale entries for deleted schedules. + b.metrics.ResetBackupLastSuccessfulTimestamp() for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) { b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) } diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index bab98efb6..3a1903e0f 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -2041,6 +2041,38 @@ func Test_getLastSuccessBySchedule(t *testing.T) { } } +// Test_updateTotalBackupMetric_prunesStaleTimestamps verifies that the periodic +// resync removes backupLastSuccessfulTimestamp entries for schedules that no longer +// have any completed backups (e.g. after the schedule and its backups are deleted). +func Test_updateTotalBackupMetric_prunesStaleTimestamps(t *testing.T) { + baseTime, err := time.Parse(time.RFC1123, time.RFC1123) + require.NoError(t, err) + + m := metrics.NewServerMetrics() + + // Simulate a previous resync that set the metric for "deleted-schedule" + m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) + require.Equal(t, 1, m.BackupLastSuccessfulTimestampCount()) + + // Current backups only contain entries for "active-schedule" + backups := []velerov1api.Backup{ + *builder.ForBackup("velero", "b1"). + ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")). + Phase(velerov1api.BackupPhaseCompleted). + CompletionTimestamp(baseTime). + Result(), + } + + // Replicate the resync logic: reset then set + m.ResetBackupLastSuccessfulTimestamp() + for schedule, timestamp := range getLastSuccessBySchedule(backups) { + m.SetBackupLastSuccessfulTimestamp(schedule, timestamp) + } + + // Only "active-schedule" should remain; "deleted-schedule" should be pruned + assert.Equal(t, 1, m.BackupLastSuccessfulTimestampCount()) +} + // Unit tests to make sure that the backup's status is updated correctly during reconcile. // To clear up confusion whether status can be updated with Patch alone without status writer and not kbClient.Status().Patch() func TestPatchResourceWorksWithStatus(t *testing.T) { diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d54eb02b5..d95867fd1 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -765,6 +765,23 @@ func (m *ServerMetrics) ResetBackupLastSuccessfulTimestamp() { } } +// BackupLastSuccessfulTimestampCount returns the number of active time series +// in the backupLastSuccessfulTimestamp gauge. +func (m *ServerMetrics) BackupLastSuccessfulTimestampCount() int { + g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec) + if !ok { + return 0 + } + ch := make(chan prometheus.Metric, 100) + g.Collect(ch) + close(ch) + count := 0 + for range ch { + count++ + } + return count +} + // SetBackupTarballSizeBytesGauge records the size, in bytes, of a backup tarball. func (m *ServerMetrics) SetBackupTarballSizeBytesGauge(backupSchedule string, size int64) { if g, ok := m.metrics[backupTarballSizeBytesGauge].(*prometheus.GaugeVec); ok { From 4357ad89767ca0f968156658617d6f4e81eb6e65 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 09:48:49 -0700 Subject: [PATCH 074/194] Add changelog for PR #10000 Signed-off-by: Shubham Pampattiwar --- changelogs/unreleased/10000-shubham-pampattiwar | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/10000-shubham-pampattiwar diff --git a/changelogs/unreleased/10000-shubham-pampattiwar b/changelogs/unreleased/10000-shubham-pampattiwar new file mode 100644 index 000000000..4134b77e2 --- /dev/null +++ b/changelogs/unreleased/10000-shubham-pampattiwar @@ -0,0 +1 @@ +Fix stale backupLastSuccessfulTimestamp metric after schedule deletion From 6ea95548d69e41744467ed69c634ba7ba8f30ad5 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 20 Jul 2026 06:10:53 -0700 Subject: [PATCH 075/194] Remove BackupLastSuccessfulTimestampCount, use Metrics() in test Remove the exported method that was only used in tests. Use the existing Metrics() getter to access the gauge directly in the backup controller test instead. Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller_test.go | 18 ++++++++++++++++-- pkg/metrics/metrics.go | 17 ----------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 3a1903e0f..3aafa2c71 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -31,6 +31,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -2049,10 +2050,11 @@ func Test_updateTotalBackupMetric_prunesStaleTimestamps(t *testing.T) { require.NoError(t, err) m := metrics.NewServerMetrics() + gauge := m.Metrics()["backup_last_successful_timestamp"].(*prometheus.GaugeVec) // Simulate a previous resync that set the metric for "deleted-schedule" m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) - require.Equal(t, 1, m.BackupLastSuccessfulTimestampCount()) + require.Equal(t, 1, collectGaugeCount(t, gauge)) // Current backups only contain entries for "active-schedule" backups := []velerov1api.Backup{ @@ -2070,7 +2072,19 @@ func Test_updateTotalBackupMetric_prunesStaleTimestamps(t *testing.T) { } // Only "active-schedule" should remain; "deleted-schedule" should be pruned - assert.Equal(t, 1, m.BackupLastSuccessfulTimestampCount()) + assert.Equal(t, 1, collectGaugeCount(t, gauge)) +} + +func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { + t.Helper() + ch := make(chan prometheus.Metric, 10) + g.Collect(ch) + close(ch) + count := 0 + for range ch { + count++ + } + return count } // Unit tests to make sure that the backup's status is updated correctly during reconcile. diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d95867fd1..d54eb02b5 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -765,23 +765,6 @@ func (m *ServerMetrics) ResetBackupLastSuccessfulTimestamp() { } } -// BackupLastSuccessfulTimestampCount returns the number of active time series -// in the backupLastSuccessfulTimestamp gauge. -func (m *ServerMetrics) BackupLastSuccessfulTimestampCount() int { - g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec) - if !ok { - return 0 - } - ch := make(chan prometheus.Metric, 100) - g.Collect(ch) - close(ch) - count := 0 - for range ch { - count++ - } - return count -} - // SetBackupTarballSizeBytesGauge records the size, in bytes, of a backup tarball. func (m *ServerMetrics) SetBackupTarballSizeBytesGauge(backupSchedule string, size int64) { if g, ok := m.metrics[backupTarballSizeBytesGauge].(*prometheus.GaugeVec); ok { From bfeccba0a84958943aae709766c663f73950160d Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 20 Jul 2026 12:32:02 -0700 Subject: [PATCH 076/194] Move metric reset inside List success block Avoid clearing backupLastSuccessfulTimestamp on transient API errors. The reset and re-set now only run when the backup List call succeeds, so existing metric values remain stable across temporary failures. Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 7a58424eb..0e2fb1384 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -212,14 +212,14 @@ func (b *backupReconciler) updateTotalBackupMetric() { b.logger.Error(err, "Error computing backup_total metric") } else { b.metrics.SetBackupTotal(int64(len(backups.Items))) - } - // recompute backup_last_successful_timestamp metric for each - // schedule (including the empty schedule, i.e. ad-hoc backups). - // Reset first to prune stale entries for deleted schedules. - b.metrics.ResetBackupLastSuccessfulTimestamp() - for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) { - b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) + // recompute backup_last_successful_timestamp metric for each + // schedule (including the empty schedule, i.e. ad-hoc backups). + // Reset first to prune stale entries for deleted schedules. + b.metrics.ResetBackupLastSuccessfulTimestamp() + for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) { + b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) + } } }, backupResyncPeriod, From b9d9dcfc385583d98fec0f7c1722344168c25327 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 21 Jul 2026 10:13:12 -0700 Subject: [PATCH 077/194] Add integration test for updateTotalBackupMetric resync Add a test that exercises the actual updateTotalBackupMetric goroutine with a fake client to verify stale backupLastSuccessfulTimestamp entries are pruned during a real resync cycle. Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller_test.go | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 3aafa2c71..9f6bf1a28 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -18,6 +18,7 @@ package controller import ( "bytes" + "context" "fmt" "io" "reflect" @@ -2087,6 +2088,44 @@ func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { return count } +// Test_updateTotalBackupMetric_prunesStaleTimestamps_integration tests the actual +// updateTotalBackupMetric goroutine with a fake client to verify stale metrics are +// pruned during a real resync cycle. +func Test_updateTotalBackupMetric_prunesStaleTimestamps_integration(t *testing.T) { + baseTime, err := time.Parse(time.RFC1123, time.RFC1123) + require.NoError(t, err) + + m := metrics.NewServerMetrics() + gauge := m.Metrics()["backup_last_successful_timestamp"].(*prometheus.GaugeVec) + + activeBackup := builder.ForBackup("velero", "b1"). + ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")). + Phase(velerov1api.BackupPhaseCompleted). + CompletionTimestamp(baseTime). + Result() + + fakeClient := velerotest.NewFakeControllerRuntimeClient(t, activeBackup) + + m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) + require.Equal(t, 1, collectGaugeCount(t, gauge)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + c := &backupReconciler{ + ctx: ctx, + kbClient: fakeClient, + logger: logrus.StandardLogger(), + metrics: m, + } + + c.updateTotalBackupMetric() + time.Sleep(7 * time.Second) + cancel() + + assert.Equal(t, 1, collectGaugeCount(t, gauge)) +} + // Unit tests to make sure that the backup's status is updated correctly during reconcile. // To clear up confusion whether status can be updated with Patch alone without status writer and not kbClient.Status().Patch() func TestPatchResourceWorksWithStatus(t *testing.T) { From 4ea38216f5d1e09a400c30e3e5977ba0dfb76346 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 22 Jul 2026 09:16:24 -0700 Subject: [PATCH 078/194] Address review feedback: use DeleteLabelValues, extract resync method Replace blanket Reset() with targeted DeleteLabelValues to avoid briefly wiping metrics for schedules that still exist. Track known schedules in a set and only delete stale entries on each resync. Extract the wait.Until closure into resyncBackupMetrics() so tests can call it directly without goroutine timing. Replace hand-rolled collectGaugeCount helper with testutil.CollectAndCount. Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller.go | 58 +++++++++------- pkg/controller/backup_controller_test.go | 84 ++++++------------------ pkg/metrics/metrics.go | 7 +- pkg/metrics/metrics_test.go | 21 +++--- 4 files changed, 74 insertions(+), 96 deletions(-) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 0e2fb1384..01a660dad 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -107,10 +107,11 @@ type backupReconciler struct { credentialFileStore credentials.FileStore maxConcurrentK8SConnections int defaultSnapshotMoveData bool - globalCRClient kbclient.Client - itemBlockWorkerCount int - concurrentBackups int - globalVolumePoliciesConfigMap string + globalCRClient kbclient.Client + itemBlockWorkerCount int + concurrentBackups int + globalVolumePoliciesConfigMap string + knownSchedulesWithSuccessfulBackup sets.Set[string] } func NewBackupReconciler( @@ -204,30 +205,43 @@ func (b *backupReconciler) updateTotalBackupMetric() { time.Sleep(5 * time.Second) wait.Until( - func() { - // recompute backup_total metric - backups := &velerov1api.BackupList{} - err := b.kbClient.List(context.Background(), backups, &kbclient.ListOptions{LabelSelector: labels.Everything()}) - if err != nil { - b.logger.Error(err, "Error computing backup_total metric") - } else { - b.metrics.SetBackupTotal(int64(len(backups.Items))) - - // recompute backup_last_successful_timestamp metric for each - // schedule (including the empty schedule, i.e. ad-hoc backups). - // Reset first to prune stale entries for deleted schedules. - b.metrics.ResetBackupLastSuccessfulTimestamp() - for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) { - b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) - } - } - }, + b.resyncBackupMetrics, backupResyncPeriod, b.ctx.Done(), ) }() } +func (b *backupReconciler) resyncBackupMetrics() { + backups := &velerov1api.BackupList{} + err := b.kbClient.List(context.Background(), backups, &kbclient.ListOptions{LabelSelector: labels.Everything()}) + if err != nil { + b.logger.Error(err, "Error computing backup_total metric") + return + } + + b.metrics.SetBackupTotal(int64(len(backups.Items))) + + currentSchedules := getLastSuccessBySchedule(backups.Items) + for schedule, timestamp := range currentSchedules { + b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) + } + + // Remove metrics for schedules that no longer have successful backups + if b.knownSchedulesWithSuccessfulBackup != nil { + for schedule := range b.knownSchedulesWithSuccessfulBackup { + if _, exists := currentSchedules[schedule]; !exists { + b.metrics.DeleteBackupLastSuccessfulTimestamp(schedule) + } + } + } + + b.knownSchedulesWithSuccessfulBackup = sets.New[string]() + for schedule := range currentSchedules { + b.knownSchedulesWithSuccessfulBackup.Insert(schedule) + } +} + // getLastSuccessBySchedule finds the most recent completed backup for each schedule // and returns a map of schedule name -> completion time of the most recent completed // backup. This map includes an entry for ad-hoc/non-scheduled backups, where the key diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 9f6bf1a28..b86434796 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -18,7 +18,6 @@ package controller import ( "bytes" - "context" "fmt" "io" "reflect" @@ -32,7 +31,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" - "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -2043,60 +2042,15 @@ func Test_getLastSuccessBySchedule(t *testing.T) { } } -// Test_updateTotalBackupMetric_prunesStaleTimestamps verifies that the periodic -// resync removes backupLastSuccessfulTimestamp entries for schedules that no longer -// have any completed backups (e.g. after the schedule and its backups are deleted). -func Test_updateTotalBackupMetric_prunesStaleTimestamps(t *testing.T) { +// Test_resyncBackupMetrics_prunesStaleTimestamps verifies that resyncBackupMetrics +// removes backupLastSuccessfulTimestamp entries for schedules that no longer have +// any completed backups (e.g. after the schedule and its backups are deleted). +func Test_resyncBackupMetrics_prunesStaleTimestamps(t *testing.T) { baseTime, err := time.Parse(time.RFC1123, time.RFC1123) require.NoError(t, err) m := metrics.NewServerMetrics() - gauge := m.Metrics()["backup_last_successful_timestamp"].(*prometheus.GaugeVec) - - // Simulate a previous resync that set the metric for "deleted-schedule" - m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) - require.Equal(t, 1, collectGaugeCount(t, gauge)) - - // Current backups only contain entries for "active-schedule" - backups := []velerov1api.Backup{ - *builder.ForBackup("velero", "b1"). - ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")). - Phase(velerov1api.BackupPhaseCompleted). - CompletionTimestamp(baseTime). - Result(), - } - - // Replicate the resync logic: reset then set - m.ResetBackupLastSuccessfulTimestamp() - for schedule, timestamp := range getLastSuccessBySchedule(backups) { - m.SetBackupLastSuccessfulTimestamp(schedule, timestamp) - } - - // Only "active-schedule" should remain; "deleted-schedule" should be pruned - assert.Equal(t, 1, collectGaugeCount(t, gauge)) -} - -func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { - t.Helper() - ch := make(chan prometheus.Metric, 10) - g.Collect(ch) - close(ch) - count := 0 - for range ch { - count++ - } - return count -} - -// Test_updateTotalBackupMetric_prunesStaleTimestamps_integration tests the actual -// updateTotalBackupMetric goroutine with a fake client to verify stale metrics are -// pruned during a real resync cycle. -func Test_updateTotalBackupMetric_prunesStaleTimestamps_integration(t *testing.T) { - baseTime, err := time.Parse(time.RFC1123, time.RFC1123) - require.NoError(t, err) - - m := metrics.NewServerMetrics() - gauge := m.Metrics()["backup_last_successful_timestamp"].(*prometheus.GaugeVec) + gauge := m.Metrics()["backup_last_successful_timestamp"] activeBackup := builder.ForBackup("velero", "b1"). ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")). @@ -2104,26 +2058,30 @@ func Test_updateTotalBackupMetric_prunesStaleTimestamps_integration(t *testing.T CompletionTimestamp(baseTime). Result() - fakeClient := velerotest.NewFakeControllerRuntimeClient(t, activeBackup) + deletedBackup := builder.ForBackup("velero", "b2"). + ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "deleted-schedule")). + Phase(velerov1api.BackupPhaseCompleted). + CompletionTimestamp(baseTime). + Result() - m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) - require.Equal(t, 1, collectGaugeCount(t, gauge)) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + fakeClient := velerotest.NewFakeControllerRuntimeClient(t, activeBackup, deletedBackup) c := &backupReconciler{ - ctx: ctx, kbClient: fakeClient, logger: logrus.StandardLogger(), metrics: m, } - c.updateTotalBackupMetric() - time.Sleep(7 * time.Second) - cancel() + // First resync: sets metrics for both schedules + c.resyncBackupMetrics() + assert.Equal(t, 2, testutil.CollectAndCount(gauge)) - assert.Equal(t, 1, collectGaugeCount(t, gauge)) + // Simulate schedule deletion: remove the backup for "deleted-schedule" + require.NoError(t, fakeClient.Delete(t.Context(), deletedBackup)) + + // Second resync: prunes "deleted-schedule" metric, keeps "active-schedule" + c.resyncBackupMetrics() + assert.Equal(t, 1, testutil.CollectAndCount(gauge)) } // Unit tests to make sure that the backup's status is updated correctly during reconcile. diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d54eb02b5..4661eaec8 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -758,10 +758,11 @@ func (m *ServerMetrics) RegisterPodVolumeOpLatencyGauge(node, pvbName, opName, b } } -// ResetBackupLastSuccessfulTimestamp removes all schedule-level backupLastSuccessfulTimestamp values. -func (m *ServerMetrics) ResetBackupLastSuccessfulTimestamp() { +// DeleteBackupLastSuccessfulTimestamp removes the backupLastSuccessfulTimestamp +// metric for a single schedule. +func (m *ServerMetrics) DeleteBackupLastSuccessfulTimestamp(scheduleName string) { if g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec); ok { - g.Reset() + g.DeleteLabelValues(scheduleName) } } diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 07004f172..2f2135ad1 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -21,6 +21,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -457,9 +458,9 @@ func getHistogramCount(t *testing.T, vec *prometheus.HistogramVec, scheduleLabel return 0 } -// TestResetBackupLastSuccessfulTimestamp verifies that ResetBackupLastSuccessfulTimestamp -// removes all schedule-level values from the backupLastSuccessfulTimestamp gauge. -func TestResetBackupLastSuccessfulTimestamp(t *testing.T) { +// TestDeleteBackupLastSuccessfulTimestamp verifies that DeleteBackupLastSuccessfulTimestamp +// removes only the specified schedule's metric. +func TestDeleteBackupLastSuccessfulTimestamp(t *testing.T) { m := NewServerMetrics() now := time.Now() @@ -467,13 +468,17 @@ func TestResetBackupLastSuccessfulTimestamp(t *testing.T) { m.SetBackupLastSuccessfulTimestamp("schedule-2", now.Add(-time.Hour)) m.SetBackupLastSuccessfulTimestamp("", now.Add(-2*time.Hour)) - // Verify all three entries exist g := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec) - assert.Equal(t, 3, collectGaugeCount(t, g)) + assert.Equal(t, 3, testutil.CollectAndCount(g)) - // Reset should remove all entries - m.ResetBackupLastSuccessfulTimestamp() - assert.Equal(t, 0, collectGaugeCount(t, g)) + m.DeleteBackupLastSuccessfulTimestamp("schedule-1") + assert.Equal(t, 2, testutil.CollectAndCount(g)) + + m.DeleteBackupLastSuccessfulTimestamp("schedule-2") + assert.Equal(t, 1, testutil.CollectAndCount(g)) + + m.DeleteBackupLastSuccessfulTimestamp("") + assert.Equal(t, 0, testutil.CollectAndCount(g)) } // collectGaugeCount returns the number of time series in a GaugeVec. From 911cc9eb9e8ed5aadbb7a85c6acfd8ca2ff38bd9 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 22 Jul 2026 09:57:28 -0700 Subject: [PATCH 079/194] Fix gofmt alignment in backupReconciler struct Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller.go | 56 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 01a660dad..167fb7eaf 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -84,34 +84,34 @@ var autoExcludeClusterScopedResources = []string{ } type backupReconciler struct { - ctx context.Context - logger logrus.FieldLogger - discoveryHelper discovery.Helper - backupper pkgbackup.Backupper - kbClient kbclient.Client - clock clock.WithTickerAndDelayedExecution - backupLogLevel logrus.Level - newPluginManager func(logrus.FieldLogger) clientmgmt.Manager - backupTracker BackupTracker - defaultBackupLocation string - defaultVolumesToFsBackup bool - defaultBackupTTL time.Duration - defaultVGSLabelKey string - defaultCSISnapshotTimeout time.Duration - resourceTimeout time.Duration - defaultItemOperationTimeout time.Duration - defaultSnapshotLocations map[string]string - metrics *metrics.ServerMetrics - backupStoreGetter persistence.ObjectBackupStoreGetter - formatFlag logging.Format - credentialFileStore credentials.FileStore - maxConcurrentK8SConnections int - defaultSnapshotMoveData bool - globalCRClient kbclient.Client - itemBlockWorkerCount int - concurrentBackups int - globalVolumePoliciesConfigMap string - knownSchedulesWithSuccessfulBackup sets.Set[string] + ctx context.Context + logger logrus.FieldLogger + discoveryHelper discovery.Helper + backupper pkgbackup.Backupper + kbClient kbclient.Client + clock clock.WithTickerAndDelayedExecution + backupLogLevel logrus.Level + newPluginManager func(logrus.FieldLogger) clientmgmt.Manager + backupTracker BackupTracker + defaultBackupLocation string + defaultVolumesToFsBackup bool + defaultBackupTTL time.Duration + defaultVGSLabelKey string + defaultCSISnapshotTimeout time.Duration + resourceTimeout time.Duration + defaultItemOperationTimeout time.Duration + defaultSnapshotLocations map[string]string + metrics *metrics.ServerMetrics + backupStoreGetter persistence.ObjectBackupStoreGetter + formatFlag logging.Format + credentialFileStore credentials.FileStore + maxConcurrentK8SConnections int + defaultSnapshotMoveData bool + globalCRClient kbclient.Client + itemBlockWorkerCount int + concurrentBackups int + globalVolumePoliciesConfigMap string + knownSchedulesWithSuccessfulBackup sets.Set[string] } func NewBackupReconciler( From 0f01b534c314dbdd2378df5dfff93c7e3888f8d2 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 22 Jul 2026 10:43:13 -0700 Subject: [PATCH 080/194] Remove unused collectGaugeCount, assert surviving label values Remove the unused collectGaugeCount helper and assert specific label values survive after each deletion using testutil.ToFloat64. Signed-off-by: Shubham Pampattiwar --- pkg/metrics/metrics_test.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 2f2135ad1..d7f070298 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -473,27 +473,17 @@ func TestDeleteBackupLastSuccessfulTimestamp(t *testing.T) { m.DeleteBackupLastSuccessfulTimestamp("schedule-1") assert.Equal(t, 2, testutil.CollectAndCount(g)) + assert.Equal(t, float64(now.Add(-time.Hour).Unix()), testutil.ToFloat64(g.WithLabelValues("schedule-2"))) + assert.Equal(t, float64(now.Add(-2*time.Hour).Unix()), testutil.ToFloat64(g.WithLabelValues(""))) m.DeleteBackupLastSuccessfulTimestamp("schedule-2") assert.Equal(t, 1, testutil.CollectAndCount(g)) + assert.Equal(t, float64(now.Add(-2*time.Hour).Unix()), testutil.ToFloat64(g.WithLabelValues(""))) m.DeleteBackupLastSuccessfulTimestamp("") assert.Equal(t, 0, testutil.CollectAndCount(g)) } -// collectGaugeCount returns the number of time series in a GaugeVec. -func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { - t.Helper() - ch := make(chan prometheus.Metric, 10) - g.Collect(ch) - close(ch) - count := 0 - for range ch { - count++ - } - return count -} - // TestRepoMaintenanceMetrics verifies that repo maintenance metrics are properly recorded. func TestRepoMaintenanceMetrics(t *testing.T) { tests := []struct { From e2249c26d5edda46ae1bfae77e4895126ab3a8c0 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 22 Jul 2026 10:56:39 -0700 Subject: [PATCH 081/194] Update docs and governance links from vmware-tanzu to velero-io The Velero repositories have moved to the velero-io GitHub organization. Update references in docs, GOVERNANCE.md, and SECURITY.md for repos that have migrated (velero, plugin-for-aws, plugin-for-gcp, plugin-for-microsoft-azure, plugin-for-example). References to repos that have not moved (helm-charts, plugin-for-vsphere, plugin-for-csi) are left unchanged. Signed-off-by: Shubham Pampattiwar --- GOVERNANCE.md | 20 +++++++++---------- SECURITY.md | 6 +++--- .../docs/main/csi-snapshot-data-movement.md | 2 +- site/content/docs/main/csi.md | 2 +- .../docs/main/fine-grained-backup-filters.md | 2 +- .../docs/main/plugin-release-instructions.md | 4 ++-- site/content/docs/main/support-process.md | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 687baeb00..73d5a7069 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -8,16 +8,16 @@ This document defines the project governance for Velero. ## Code Repositories -The following code repositories are governed by Velero community and maintained under the `vmware-tanzu\Velero` organization. +The following code repositories are governed by Velero community and maintained under the `velero-io` organization. -* **[Velero](https://github.com/vmware-tanzu/velero):** Main Velero codebase +* **[Velero](https://github.com/velero-io/velero):** Main Velero codebase * **[Helm Chart](https://github.com/vmware-tanzu/helm-charts/tree/main/charts/velero):** The Helm chart for the Velero server component * **[Velero CSI Plugin](https://github.com/vmware-tanzu/velero-plugin-for-csi):** This repository contains Velero plugins for snapshotting CSI backed PVCs using the CSI beta snapshot APIs * **[Velero Plugin for vSphere](https://github.com/vmware-tanzu/velero-plugin-for-vsphere):** This repository contains the Velero Plugin for vSphere. This plugin is a volume snapshotter plugin that provides crash-consistent snapshots of vSphere block volumes and backup of volume data into S3 compatible storage. -* **[Velero Plugin for AWS](https://github.com/vmware-tanzu/velero-plugin-for-aws):** This repository contains the plugins to support running Velero on AWS, including the object store plugin and the volume snapshotter plugin -* **[Velero Plugin for GCP](https://github.com/vmware-tanzu/velero-plugin-for-gcp):** This repository contains the plugins to support running Velero on GCP, including the object store plugin and the volume snapshotter plugin -* **[Velero Plugin for Azure](https://github.com/vmware-tanzu/velero-plugin-for-microsoft-azure):** This repository contains the plugins to support running Velero on Azure, including the object store plugin and the volume snapshotter plugin -* **[Velero Plugin Example](https://github.com/vmware-tanzu/velero-plugin-example):** This repository contains example plugins for Velero +* **[Velero Plugin for AWS](https://github.com/velero-io/velero-plugin-for-aws):** This repository contains the plugins to support running Velero on AWS, including the object store plugin and the volume snapshotter plugin +* **[Velero Plugin for GCP](https://github.com/velero-io/velero-plugin-for-gcp):** This repository contains the plugins to support running Velero on GCP, including the object store plugin and the volume snapshotter plugin +* **[Velero Plugin for Azure](https://github.com/velero-io/velero-plugin-for-microsoft-azure):** This repository contains the plugins to support running Velero on Azure, including the object store plugin and the volume snapshotter plugin +* **[Velero Plugin Example](https://github.com/velero-io/velero-plugin-example):** This repository contains example plugins for Velero ## Community Roles @@ -67,12 +67,12 @@ interested in implementing the proposal should be either deeply engaged in the proposal process or be an author of the proposal. The proposal should be documented as a separated markdown file pushed to the root of the -`design` folder in the [Velero](https://github.com/vmware-tanzu/velero/tree/main/design) +`design` folder in the [Velero](https://github.com/velero-io/velero/tree/main/design) repository via PR. The name of the file should follow the name pattern `_design.md`, e.g: `restore-hooks-design.md`. -Use the [Proposal Template](https://github.com/vmware-tanzu/velero/blob/main/design/_template.md) as a starting point. +Use the [Proposal Template](https://github.com/velero-io/velero/blob/main/design/_template.md) as a starting point. ### Proposal Lifecycle @@ -88,7 +88,7 @@ To maintain velocity in a project as busy as Velero, the concept of [Lazy Consensus](http://en.osswiki.info/concepts/lazy_consensus) is practiced. Ideas and / or proposals should be shared by maintainers via GitHub with the appropriate maintainer groups (e.g., -`@vmware-tanzu/velero-maintainers`) tagged. Out of respect for other contributors, +`@velero-io/velero-maintainers`) tagged. Out of respect for other contributors, major changes should also be accompanied by a ping on Slack or a note on the Velero mailing list as appropriate. Author(s) of proposal, Pull Requests, issues, etc. will give a time period of no less than five (5) working days for @@ -111,7 +111,7 @@ Lazy consensus does _not_ apply to the process of: ### Deprecation Process -Any contributor may introduce a request to deprecate a feature or an option of a feature by opening a feature request issue in the vmware-tanzu/velero GitHub project. The issue should describe why the feature is no longer needed or has become detrimental to Velero, as well as whether and how it has been superseded. The submitter should give as much detail as possible. +Any contributor may introduce a request to deprecate a feature or an option of a feature by opening a feature request issue in the velero-io/velero GitHub project. The issue should describe why the feature is no longer needed or has become detrimental to Velero, as well as whether and how it has been superseded. The submitter should give as much detail as possible. Once the issue is filed, a one-month discussion period begins. Discussions take place within the issue itself as well as in the community meetings. The person who opens the issue, or a maintainer, should add the date and time marking the end of the discussion period in a comment on the issue as soon as possible after it is opened. A decision on the issue needs to be made within this one-month period. diff --git a/SECURITY.md b/SECURITY.md index 84e6f45dc..219426f6f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ Velero is an open source tool with a growing community devoted to safe backup an ## Supported Versions -The Velero project maintains the following [governance document](https://github.com/vmware-tanzu/velero/blob/main/GOVERNANCE.md), [release document](https://github.com/vmware-tanzu/velero/blob/f42c63af1b9af445e38f78a7256b1c48ef79c10e/site/docs/main/release-instructions.md), and [support document](https://velero.io/docs/main/support-process/). Please refer to these for release and related details. Only the most recent version of Velero is supported. Each [release](https://github.com/vmware-tanzu/velero/releases) includes information about upgrading to the latest version. +The Velero project maintains the following [governance document](https://github.com/velero-io/velero/blob/main/GOVERNANCE.md), [release document](https://github.com/velero-io/velero/blob/f42c63af1b9af445e38f78a7256b1c48ef79c10e/site/docs/main/release-instructions.md), and [support document](https://velero.io/docs/main/support-process/). Please refer to these for release and related details. Only the most recent version of Velero is supported. Each [release](https://github.com/velero-io/velero/releases) includes information about upgrading to the latest version. ## Reporting a Vulnerability - Private Disclosure Process @@ -18,7 +18,7 @@ If you know of a publicly disclosed security vulnerability for Velero, please ** **IMPORTANT: Do not file public issues on GitHub for security vulnerabilities** -To report a vulnerability or a security-related issue, please contact the email address with the details of the vulnerability. The email will be fielded by the Security Team and then shared with the Velero maintainers who have committer and release permissions. Emails will be addressed within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/vmware-tanzu/velero/issues/new/choose) instead. +To report a vulnerability or a security-related issue, please contact the email address with the details of the vulnerability. The email will be fielded by the Security Team and then shared with the Velero maintainers who have committer and release permissions. Emails will be addressed within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/velero-io/velero/issues/new/choose) instead. ## Proposed Email Content @@ -68,7 +68,7 @@ The Security Team will respond to vulnerability reports as follows: ## Public Disclosure Process -The Security Team publishes a [public advisory](https://github.com/vmware-tanzu/velero/security/advisories) to the Velero community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog and other channels will assist in educating Velero users and rolling out the patched release to affected users. +The Security Team publishes a [public advisory](https://github.com/velero-io/velero/security/advisories) to the Velero community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog and other channels will assist in educating Velero users and rolling out the patched release to affected users. The Security Team will also publish any mitigating steps users can take until the fix can be applied to their Velero instances. Velero distributors will handle creating and publishing their own security advisories. diff --git a/site/content/docs/main/csi-snapshot-data-movement.md b/site/content/docs/main/csi-snapshot-data-movement.md index 154abb198..378f99055 100644 --- a/site/content/docs/main/csi-snapshot-data-movement.md +++ b/site/content/docs/main/csi-snapshot-data-movement.md @@ -67,7 +67,7 @@ On source cluster, Velero needs to manipulate CSI snapshots through the CSI volu To integrate Velero with the CSI volume snapshot APIs, you must enable the `EnableCSI` feature flag. -From release-1.14, the `github.com/vmware-tanzu/velero-plugin-for-csi` repository, which is the Velero CSI plugin, is merged into the `github.com/velero-io/velero` repository. +From release-1.14, the `github.com/velero-io/velero-plugin-for-csi` repository, which is the Velero CSI plugin, is merged into the `github.com/velero-io/velero` repository. The reasons to merge the CSI plugin are: * The VolumeSnapshot data mover depends on the CSI plugin, it's reasonabe to integrate them. * This change reduces the Velero deploying complexity. diff --git a/site/content/docs/main/csi.md b/site/content/docs/main/csi.md index fddc5f258..11973f50a 100644 --- a/site/content/docs/main/csi.md +++ b/site/content/docs/main/csi.md @@ -8,7 +8,7 @@ Integrating Container Storage Interface (CSI) snapshot support into Velero enabl By supporting CSI snapshot APIs, Velero can support any volume provider that has a CSI driver, without requiring a Velero-specific plugin to be available. This page gives an overview of how to add support for CSI snapshots to Velero. ## Notice -From release-1.14, the `github.com/vmware-tanzu/velero-plugin-for-csi` repository, which is the Velero CSI plugin, is merged into the `github.com/velero-io/velero` repository. +From release-1.14, the `github.com/velero-io/velero-plugin-for-csi` repository, which is the Velero CSI plugin, is merged into the `github.com/velero-io/velero` repository. The reasons to merge the CSI plugin are: * The VolumeSnapshot data mover depends on the CSI plugin, it's reasonabe to integrate them. * This change reduces the Velero deploying complexity. diff --git a/site/content/docs/main/fine-grained-backup-filters.md b/site/content/docs/main/fine-grained-backup-filters.md index 01f5aef8a..d9f90debd 100644 --- a/site/content/docs/main/fine-grained-backup-filters.md +++ b/site/content/docs/main/fine-grained-backup-filters.md @@ -778,7 +778,7 @@ Velero validates the ResourcePolicy when a backup starts. Common errors: Restore is unchanged: it restores whatever is in the backup archive. Resources excluded by fine-grained filters are simply absent. Use `Restore.spec.includedNamespaces` (and existing restore filters) to limit what you restore from a partial backup. -Fine-grained resource filtering is also available on the restore path using `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. For details on the restore-side policies, see the [Fine-grained restore filters design](https://github.com/vmware-tanzu/velero/blob/main/design/restore-filter-enhancement/fine-grained-restore-filters-design.md). +Fine-grained resource filtering is also available on the restore path using `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. For details on the restore-side policies, see the [Fine-grained restore filters design](https://github.com/velero-io/velero/blob/main/design/restore-filter-enhancement/fine-grained-restore-filters-design.md). --- diff --git a/site/content/docs/main/plugin-release-instructions.md b/site/content/docs/main/plugin-release-instructions.md index 46494cac9..02ca6940d 100644 --- a/site/content/docs/main/plugin-release-instructions.md +++ b/site/content/docs/main/plugin-release-instructions.md @@ -19,11 +19,11 @@ Plugins the Velero core team is responsible include all those listed in [the Vel 1. Once the PR is merged, checkout the upstream `main` branch. Your local upstream might be named `upstream` or `origin`, so use this command: `git checkout /main`. 1. Tag the git version - `git tag v`. 1. Push the git tag - `git push --tags ` to trigger the image build. -2. Wait for the container images to build. You may check the progress of the GH action that triggers the image build at `https://github.com/vmware-tanzu//actions` +2. Wait for the container images to build. You may check the progress of the GH action that triggers the image build at `https://github.com/velero-io//actions` 3. Verify that an image with the new tag is available at `https://hub.docker.com/repository/docker/velero//`. 4. Run the Velero [e2e tests][2] using the new image. Until it is made configurable, you will have to edit the [plugin version][1] in the test. ### Release -1. If all e2e tests pass, go to the GitHub release page of the plugin (`https://github.com/vmware-tanzu//releases`) and manually create a release for the new tag. +1. If all e2e tests pass, go to the GitHub release page of the plugin (`https://github.com/velero-io//releases`) and manually create a release for the new tag. 1. Copy and paste the content of the new changelog file into the release description field. [1]: https://github.com/velero-io/velero/blob/c8dfd648bbe85db0184ea53296de4220895497e6/test/e2e/velero_utils.go#L27 diff --git a/site/content/docs/main/support-process.md b/site/content/docs/main/support-process.md index d142329f8..5c1363e7a 100644 --- a/site/content/docs/main/support-process.md +++ b/site/content/docs/main/support-process.md @@ -40,4 +40,4 @@ Generally speaking, new GitHub issues will fall into one of several categories. - If the issue ends up being a feature request or a bug, update the title and follow the appropriate process for it - If the reporter becomes unresponsive after multiple pings, close out the issue due to inactivity and comment that the user can always reach out again as needed -[0]: https://github.com/vmware-tanzu?q=velero&type=&language= +[0]: https://github.com/velero-io?q=velero&type=&language= From d69f6abe5cc31fbc5c0543db9ebce9f3e26a6c4d 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: Thu, 23 Jul 2026 10:31:42 +0800 Subject: [PATCH 082/194] Add param to StartRestore to facilitates future expansion (#10057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add param to StartRestore to facilitates future expansion Signed-off-by: Wenkai Yin(尹文开) --- pkg/controller/data_download_controller.go | 4 ++-- pkg/controller/data_download_controller_test.go | 4 ++-- pkg/controller/data_upload_controller_test.go | 2 +- pkg/controller/pod_volume_restore_controller.go | 4 ++-- pkg/controller/pod_volume_restore_controller_test.go | 4 ++-- pkg/datamover/restore_micro_service.go | 2 +- pkg/datamover/restore_micro_service_test.go | 4 ++-- pkg/datapath/data_path.go | 6 +++++- pkg/datapath/data_path_test.go | 2 +- pkg/datapath/micro_service_watcher.go | 2 +- pkg/datapath/mocks/asyncBR.go | 10 +++++----- pkg/datapath/types.go | 2 +- pkg/podvolume/restore_micro_service.go | 2 +- pkg/podvolume/restore_micro_service_test.go | 4 ++-- 14 files changed, 28 insertions(+), 24 deletions(-) diff --git a/pkg/controller/data_download_controller.go b/pkg/controller/data_download_controller.go index fc7cb1a53..7e06c459d 100644 --- a/pkg/controller/data_download_controller.go +++ b/pkg/controller/data_download_controller.go @@ -454,7 +454,7 @@ func (r *DataDownloadReconciler) startCancelableDataPath(asyncBR datapath.AsyncB if err := asyncBR.StartRestore(dd.Spec.SnapshotID, datapath.AccessPoint{ ByPath: res.ByPod.VolumeName, - }, dd.Spec.DataMoverConfig); err != nil { + }, dd.Spec.DataMoverConfig, nil); err != nil { return errors.Wrapf(err, "error starting async restore for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName) } @@ -1096,7 +1096,7 @@ func (r *DataDownloadReconciler) resumeCancellableDataPath(ctx context.Context, if err := asyncBR.StartRestore(dd.Spec.SnapshotID, datapath.AccessPoint{ ByPath: res.ByPod.VolumeName, - }, nil); err != nil { + }, nil, nil); err != nil { return errors.Wrapf(err, "error to resume asyncBR watcher for dd %s", dd.Name) } diff --git a/pkg/controller/data_download_controller_test.go b/pkg/controller/data_download_controller_test.go index 518788635..a605fcaaa 100644 --- a/pkg/controller/data_download_controller_test.go +++ b/pkg/controller/data_download_controller_test.go @@ -529,7 +529,7 @@ func TestDataDownloadReconcile(t *testing.T) { } if test.mockStart { - asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr) + asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr) } if test.mockCancel { @@ -1288,7 +1288,7 @@ func TestResumeCancellableRestore(t *testing.T) { } if test.mockStart { - mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr) + mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr) } if test.mockClose { diff --git a/pkg/controller/data_upload_controller_test.go b/pkg/controller/data_upload_controller_test.go index 9703abe92..ec819f8eb 100644 --- a/pkg/controller/data_upload_controller_test.go +++ b/pkg/controller/data_upload_controller_test.go @@ -348,7 +348,7 @@ func (f *fakeFSBR) StartBackup(source datapath.AccessPoint, uploaderConfigs map[ return f.startErr } -func (f *fakeFSBR) StartRestore(snapshotID string, target datapath.AccessPoint, uploaderConfigs map[string]string) error { +func (f *fakeFSBR) StartRestore(snapshotID string, target datapath.AccessPoint, uploaderConfigs map[string]string, param any) error { return nil } diff --git a/pkg/controller/pod_volume_restore_controller.go b/pkg/controller/pod_volume_restore_controller.go index 12ba49d10..ca25b4f95 100644 --- a/pkg/controller/pod_volume_restore_controller.go +++ b/pkg/controller/pod_volume_restore_controller.go @@ -528,7 +528,7 @@ func (r *PodVolumeRestoreReconciler) startCancelableDataPath(asyncBR datapath.As if err := asyncBR.StartRestore(pvr.Spec.SnapshotID, datapath.AccessPoint{ ByPath: res.ByPod.VolumeName, - }, pvr.Spec.UploaderSettings); err != nil { + }, pvr.Spec.UploaderSettings, nil); err != nil { return errors.Wrapf(err, "error starting async restore for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName) } @@ -1146,7 +1146,7 @@ func (r *PodVolumeRestoreReconciler) resumeCancellableDataPath(ctx context.Conte if err := asyncBR.StartRestore(pvr.Spec.SnapshotID, datapath.AccessPoint{ ByPath: res.ByPod.VolumeName, - }, pvr.Spec.UploaderSettings); err != nil { + }, pvr.Spec.UploaderSettings, nil); err != nil { return errors.Wrapf(err, "error to resume asyncBR watcher for PVR %s", pvr.Name) } diff --git a/pkg/controller/pod_volume_restore_controller_test.go b/pkg/controller/pod_volume_restore_controller_test.go index 61d34fae3..abd2df206 100644 --- a/pkg/controller/pod_volume_restore_controller_test.go +++ b/pkg/controller/pod_volume_restore_controller_test.go @@ -1099,7 +1099,7 @@ func TestPodVolumeRestoreReconcile(t *testing.T) { } if test.mockStart { - asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr) + asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr) } if test.mockCancel { @@ -1901,7 +1901,7 @@ func TestResumeCancellablePodVolumeRestore(t *testing.T) { } if test.mockStart { - mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr) + mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr) } if test.mockClose { diff --git a/pkg/datamover/restore_micro_service.go b/pkg/datamover/restore_micro_service.go index d918667f9..5880dfc91 100644 --- a/pkg/datamover/restore_micro_service.go +++ b/pkg/datamover/restore_micro_service.go @@ -180,7 +180,7 @@ func (r *RestoreMicroService) RunCancelableDataPath(ctx context.Context) (string } log.Info("fs init") - if err := dp.StartRestore(dd.Spec.SnapshotID, r.sourceTargetPath, dd.Spec.DataMoverConfig); err != nil { + if err := dp.StartRestore(dd.Spec.SnapshotID, r.sourceTargetPath, dd.Spec.DataMoverConfig, &datapath.RestoreStartParam{}); err != nil { return "", errors.Wrap(err, "error starting data path restore") } diff --git a/pkg/datamover/restore_micro_service_test.go b/pkg/datamover/restore_micro_service_test.go index 33e22eab3..39e055572 100644 --- a/pkg/datamover/restore_micro_service_test.go +++ b/pkg/datamover/restore_micro_service_test.go @@ -355,12 +355,12 @@ func TestRunCancelableRestore(t *testing.T) { if test.startErr != nil { fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) - fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startErr) + fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startErr) } if test.dataPathStarted { fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) - fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(nil) + fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) } return fsBR diff --git a/pkg/datapath/data_path.go b/pkg/datapath/data_path.go index 6cef1af26..6e36ce6af 100644 --- a/pkg/datapath/data_path.go +++ b/pkg/datapath/data_path.go @@ -59,6 +59,10 @@ type BackupStartParam struct { SnapshotID string } +// RestoreStartParam define the input param for restore start +type RestoreStartParam struct { +} + type generalDataPath struct { ctx context.Context cancel context.CancelFunc @@ -221,7 +225,7 @@ func (dp *generalDataPath) StartBackup(source AccessPoint, uploaderConfig map[st return nil } -func (dp *generalDataPath) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string) error { +func (dp *generalDataPath) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string, param any) error { if !dp.initialized { return errors.New("data path is not initialized") } diff --git a/pkg/datapath/data_path_test.go b/pkg/datapath/data_path_test.go index 65d7f9b65..58df5d4e8 100644 --- a/pkg/datapath/data_path_test.go +++ b/pkg/datapath/data_path_test.go @@ -190,7 +190,7 @@ func TestAsyncRestore(t *testing.T) { dp.initialized = true dp.callbacks = test.callbacks - err := dp.StartRestore(test.snapshot, AccessPoint{ByPath: test.path}, map[string]string{}) + err := dp.StartRestore(test.snapshot, AccessPoint{ByPath: test.path}, map[string]string{}, &RestoreStartParam{}) require.NoError(t, err) <-finish diff --git a/pkg/datapath/micro_service_watcher.go b/pkg/datapath/micro_service_watcher.go index 3e8ace651..67ec4c29d 100644 --- a/pkg/datapath/micro_service_watcher.go +++ b/pkg/datapath/micro_service_watcher.go @@ -221,7 +221,7 @@ func (ms *microServiceBRWatcher) StartBackup(source AccessPoint, uploaderConfig return nil } -func (ms *microServiceBRWatcher) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string) error { +func (ms *microServiceBRWatcher) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string, param any) error { ms.log.Infof("Start watching restore ms to target %s, from snapshot %s", target.ByPath, snapshotID) ms.startWatch() diff --git a/pkg/datapath/mocks/asyncBR.go b/pkg/datapath/mocks/asyncBR.go index ef87fde83..deec61dae 100644 --- a/pkg/datapath/mocks/asyncBR.go +++ b/pkg/datapath/mocks/asyncBR.go @@ -60,17 +60,17 @@ func (_m *AsyncBR) StartBackup(source datapath.AccessPoint, dataMoverConfig map[ return r0 } -// StartRestore provides a mock function with given fields: snapshotID, target, dataMoverConfig -func (_m *AsyncBR) StartRestore(snapshotID string, target datapath.AccessPoint, dataMoverConfig map[string]string) error { - ret := _m.Called(snapshotID, target, dataMoverConfig) +// StartRestore provides a mock function with given fields: snapshotID, target, dataMoverConfig, param +func (_m *AsyncBR) StartRestore(snapshotID string, target datapath.AccessPoint, dataMoverConfig map[string]string, param interface{}) error { + ret := _m.Called(snapshotID, target, dataMoverConfig, param) if len(ret) == 0 { panic("no return value specified for StartRestore") } var r0 error - if rf, ok := ret.Get(0).(func(string, datapath.AccessPoint, map[string]string) error); ok { - r0 = rf(snapshotID, target, dataMoverConfig) + if rf, ok := ret.Get(0).(func(string, datapath.AccessPoint, map[string]string, interface{}) error); ok { + r0 = rf(snapshotID, target, dataMoverConfig, param) } else { r0 = ret.Error(0) } diff --git a/pkg/datapath/types.go b/pkg/datapath/types.go index a9c2331a6..65a6be58f 100644 --- a/pkg/datapath/types.go +++ b/pkg/datapath/types.go @@ -66,7 +66,7 @@ type AsyncBR interface { StartBackup(source AccessPoint, dataMoverConfig map[string]string, param any) error // StartRestore starts an asynchronous data path instance for restore - StartRestore(snapshotID string, target AccessPoint, dataMoverConfig map[string]string) error + StartRestore(snapshotID string, target AccessPoint, dataMoverConfig map[string]string, param any) error // Cancel cancels an asynchronous data path instance Cancel() diff --git a/pkg/podvolume/restore_micro_service.go b/pkg/podvolume/restore_micro_service.go index 24f001147..b9dbd8d64 100644 --- a/pkg/podvolume/restore_micro_service.go +++ b/pkg/podvolume/restore_micro_service.go @@ -184,7 +184,7 @@ func (r *RestoreMicroService) RunCancelableDataPath(ctx context.Context) (string log.Info("Async fs br init") - if err := fsRestore.StartRestore(pvr.Spec.SnapshotID, r.sourceTargetPath, pvr.Spec.UploaderSettings); err != nil { + if err := fsRestore.StartRestore(pvr.Spec.SnapshotID, r.sourceTargetPath, pvr.Spec.UploaderSettings, &datapath.RestoreStartParam{}); err != nil { return "", errors.Wrap(err, "error starting data path restore") } diff --git a/pkg/podvolume/restore_micro_service_test.go b/pkg/podvolume/restore_micro_service_test.go index 007060160..1964d5035 100644 --- a/pkg/podvolume/restore_micro_service_test.go +++ b/pkg/podvolume/restore_micro_service_test.go @@ -436,12 +436,12 @@ func TestRunCancelableDataPathRestore(t *testing.T) { if test.startErr != nil { fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) - fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startErr) + fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startErr) } if test.dataPathStarted { fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) - fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(nil) + fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) } return fsBR From e5654fa7eda520408896a2f1b09c806ab8c07012 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Thu, 23 Jul 2026 15:42:36 -0400 Subject: [PATCH 083/194] Fix flaky TestKopiaObjectWriterEx_ConcurrentAsyncErrors (#10030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test assumed all ten Write calls succeed before any async goroutine stores its error, but with a mock that fails instantly a goroutine can poison the writer mid-loop, making a later Write correctly fail fast — a timing-dependent test failure. Rewrite the test to assert the real-world contract instead of one schedule: a failed async block write either fails a subsequent Write fast or surfaces at Result, and is never lost. Add a separate deterministic case pinning the late-error schedule, holding async writes until all writes are queued so Result alone must report the error. Verified with -race -count=100. Fixes #10029 Signed-off-by: Tiger Kaovilai Co-authored-by: Claude Fable 5 --- .../udmrepo/kopialib/lib_repo_ex_test.go | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go index 3294063a6..6d698ec7b 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go @@ -1208,6 +1208,10 @@ func TestKopiaObjectWriterEx_MixedWriteAndWriteAt(t *testing.T) { assert.Equal(t, int64(3072), kow.entries[3].Start) } +// TestKopiaObjectWriterEx_ConcurrentAsyncErrors verifies the async error contract +// under real scheduling: once an async block write fails, the error either fails a +// subsequent Write call fast or surfaces at Result — it is never lost. Which of the +// two happens first depends on goroutine scheduling, and both are correct. func TestKopiaObjectWriterEx_ConcurrentAsyncErrors(t *testing.T) { mockRepoWriter := repomocks.NewMockRepositoryWriter(t) mockWriter := repomocks.NewWriter(t) @@ -1231,14 +1235,65 @@ func TestKopiaObjectWriterEx_ConcurrentAsyncErrors(t *testing.T) { data := make([]byte, 1024) - // Issue multiple writes so they all spawn async goroutines - // First few writes shouldn't fail immediately until getWriteError catches the asynchronous fault + // Issue multiple writes so they all spawn async goroutines. A later Write may + // observe the stored async error and fail fast — that is correct behavior. + for i := 0; i < 10; i++ { + l, err := kow.Write(data) + if err != nil { + assert.Contains(t, err.Error(), "simulated async error") + break + } + assert.Equal(t, 1024, l) + } + + // Regardless of whether a Write observed the error first, Result must report it. + id, err := kow.Result() + + require.Error(t, err) + assert.Contains(t, err.Error(), "simulated async error") + assert.Equal(t, udmrepo.ID(""), id) +} + +// TestKopiaObjectWriterEx_AsyncErrorSurfacesAtResult pins the late-error schedule: +// async writes are held until all writes have been queued, so no Write call observes +// the failure and Result alone must report it. +func TestKopiaObjectWriterEx_AsyncErrorSurfacesAtResult(t *testing.T) { + mockRepoWriter := repomocks.NewMockRepositoryWriter(t) + mockWriter := repomocks.NewWriter(t) + + releaseWrites := make(chan struct{}) + mockWriter.On("Write", mock.Anything).Run(func(mock.Arguments) { + <-releaseWrites + }).Return(0, errors.New("simulated async error")) + mockWriter.On("Close").Return(nil) + + mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter) + + sem := make(chan struct{}, 10) + buf := freelist.New(10*1024, 1024) + + kow := &kopiaObjectWriterEx{ + ctx: context.Background(), + rawRepoWriter: mockRepoWriter, + blockSize: 1024, + asyncWritesSem: sem, + asyncBuffer: buf, + logger: velerotest.NewLogger(), + } + + data := make([]byte, 1024) + + // All async writes block on releaseWrites, so no error can be stored yet and + // every Write must succeed. for i := 0; i < 10; i++ { l, err := kow.Write(data) require.NoError(t, err) assert.Equal(t, 1024, l) } + close(releaseWrites) + + // Result waits for the async writers to finish and must report their error. id, err := kow.Result() require.Error(t, err) From 5ecf38b5d7fdcd081aa7b64f4777eab74e350de1 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Thu, 23 Jul 2026 15:43:47 -0400 Subject: [PATCH 084/194] Derive dev-tool CLI versions from go.mod (ginkgo, protoc-gen-go, goimports) (#10024) * Derive Ginkgo CLI version from go.mod in test/Makefile Hardcoded @v2.22.0 pin drifted from go.mod's v2.28.3, causing Ginkgo CLI/package version mismatch warnings. Fixes #10023 Signed-off-by: Tiger Kaovilai * Derive protoc-gen-go and goimports versions from go.mod in build-image Same drift issue as #10023: Dockerfile hardcoded @v1.33.0 and @v0.33.0 while go.mod had moved on. Build context is hack/build-image, which doesn't include go.mod, so versions are computed in the Makefile (which does have go.mod) and passed through as build-args, same as GOPROXY. protoc-gen-go-grpc and controller-gen/setup-envtest/golangci-lint are left as-is: no matching go.mod entry, or independently versioned from the module they live alongside. Fixes #10023 Signed-off-by: Tiger Kaovilai --------- Signed-off-by: Tiger Kaovilai --- Makefile | 9 +++++++-- hack/build-image/Dockerfile | 10 ++++++---- test/Makefile | 3 ++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 515abf88d..bb766c7c9 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,11 @@ GOARCH = $(word 2, $(platform_temp)) GOPROXY ?= https://proxy.golang.org GOBIN=$$(pwd)/.go/bin +# Keep these build-image tool versions in sync with go.mod so the CLI/library +# pair doesn't drift (see https://github.com/velero-io/velero/issues/10023). +PROTOC_GEN_GO_VERSION := $(shell go list -m -f '{{.Version}}' google.golang.org/protobuf) +GOIMPORTS_VERSION := $(shell go list -m -f '{{.Version}}' golang.org/x/tools) + # If you want to build all binaries, see the 'all-build' rule. # If you want to build all containers, see the 'all-containers' rule. all: @@ -395,9 +400,9 @@ ifeq ($(BUILDX_ENABLED), true) ifneq ($(CONTAINER_TOOL),docker) $(error $(DOCKER_ONLY_ERROR)) endif - @cd hack/build-image && $(CONTAINER_TOOL) buildx build --build-arg=GOPROXY=$(GOPROXY) --output=type=docker --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) . + @cd hack/build-image && $(CONTAINER_TOOL) buildx build --build-arg=GOPROXY=$(GOPROXY) --build-arg=PROTOC_GEN_GO_VERSION=$(PROTOC_GEN_GO_VERSION) --build-arg=GOIMPORTS_VERSION=$(GOIMPORTS_VERSION) --output=type=docker --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) . else - @cd hack/build-image && $(CONTAINER_TOOL) build --build-arg=GOPROXY=$(GOPROXY) --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) . + @cd hack/build-image && $(CONTAINER_TOOL) build --build-arg=GOPROXY=$(GOPROXY) --build-arg=PROTOC_GEN_GO_VERSION=$(PROTOC_GEN_GO_VERSION) --build-arg=GOIMPORTS_VERSION=$(GOIMPORTS_VERSION) --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) . endif $(eval new_id=$(shell $(CONTAINER_TOOL) image inspect --format '{{ .ID }}' ${BUILDER_IMAGE} 2>/dev/null)) @if [ "$(old_id)" != "" ] && [ "$(old_id)" != "$(new_id)" ]; then \ diff --git a/hack/build-image/Dockerfile b/hack/build-image/Dockerfile index 88dedde95..aa725da03 100644 --- a/hack/build-image/Dockerfile +++ b/hack/build-image/Dockerfile @@ -15,6 +15,8 @@ FROM --platform=$TARGETPLATFORM golang:1.26-trixie ARG GOPROXY +ARG PROTOC_GEN_GO_VERSION +ARG GOIMPORTS_VERSION ENV GO111MODULE=on # Use a proxy for go modules to reduce the likelihood of various hosts being down and breaking the build @@ -34,9 +36,9 @@ RUN wget --quiet https://github.com/kubernetes-sigs/kubebuilder/releases/downloa # get controller-tools RUN go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.5 -# get goimports (the revision is pinned so we don't indiscriminately update, but the particular commit -# is not important) -RUN go install golang.org/x/tools/cmd/goimports@v0.33.0 +# get goimports, version derived from go.mod's golang.org/x/tools requirement +# (see https://github.com/velero-io/velero/issues/10023) +RUN go install golang.org/x/tools/cmd/goimports@${GOIMPORTS_VERSION} # get protoc compiler and golang plugin WORKDIR /root @@ -71,7 +73,7 @@ RUN ARCH=$(go env GOARCH) && \ 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@v1.33.0 \ +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 # get goreleaser diff --git a/test/Makefile b/test/Makefile index ae58e2c95..4f051ae00 100644 --- a/test/Makefile +++ b/test/Makefile @@ -48,6 +48,7 @@ GOBIN := $(REPO_ROOT)/.go/bin TOOLS_BIN_DIR := $(TOOLS_DIR)/$(BIN_DIR) GINKGO := $(GOBIN)/ginkgo +GINKGO_VERSION := $(shell go list -m -f '{{.Version}}' github.com/onsi/ginkgo/v2 2>/dev/null) KUSTOMIZE := $(TOOLS_BIN_DIR)/kustomize @@ -186,7 +187,7 @@ ginkgo: ${GOBIN}/ginkgo # This target does not run if ginkgo is already in $GOBIN ${GOBIN}/ginkgo: - GOBIN=${GOBIN} go install github.com/onsi/ginkgo/v2/ginkgo@v2.22.0 + GOBIN=${GOBIN} go install github.com/onsi/ginkgo/v2/ginkgo@${GINKGO_VERSION} .PHONY: run-e2e run-e2e: ginkgo From 92f636ca528e98e70fcf8986a544b3c598f0678c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:52:44 -0400 Subject: [PATCH 085/194] Bump google.golang.org/grpc from 1.81.1 to 1.82.1 (#10058) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.1 to 1.82.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.81.1...v1.82.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.82.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index a2c41faf6..3aa6ea020 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( golang.org/x/sys v0.46.0 golang.org/x/text v0.37.0 google.golang.org/api v0.283.0 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af k8s.io/api v0.36.0 k8s.io/apiextensions-apiserver v0.36.0 @@ -76,7 +76,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect @@ -189,7 +189,7 @@ require ( github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect diff --git a/go.sum b/go.sum index ed0070272..63cf28c46 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMs github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= @@ -466,8 +466,8 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= -go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= @@ -564,8 +564,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 236c45b4436229c0012d28fdccbafdc720578a71 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Fri, 24 Jul 2026 13:37:40 +0800 Subject: [PATCH 086/194] block uploader restore UT Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader_test.go | 228 ++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index 88fd4771e..69b5efcb5 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -459,3 +459,231 @@ func TestLoadObjectFromSnapshot(t *testing.T) { }) } } + +func TestGetSourceSize(t *testing.T) { + testCases := []struct { + name string + snapshot udmrepo.Snapshot + expectErr bool + expected int64 + }{ + { + name: "nil tags", + snapshot: udmrepo.Snapshot{}, + expectErr: true, + }, + { + name: "missing tag", + snapshot: udmrepo.Snapshot{ + Tags: map[string]string{}, + }, + expectErr: true, + }, + { + name: "invalid tag value", + snapshot: udmrepo.Snapshot{ + Tags: map[string]string{ + "bdev-source-size": "abc", + }, + }, + expectErr: true, + }, + { + name: "valid tag value", + snapshot: udmrepo.Snapshot{ + Tags: map[string]string{ + "bdev-source-size": "1048576", + }, + }, + expectErr: false, + expected: 1048576, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + size, err := getSourceSize(tc.snapshot) + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, size) + } + }) + } +} + +func TestFlushZeroBlocks(t *testing.T) { + t.Run("success via write fallback", func(t *testing.T) { + f, err := os.CreateTemp("", "zerotest-*") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + + require.NoError(t, f.Truncate(2048)) + + bu := &blockUploader{ + log: logrus.New(), + } + bu.log.(*logrus.Logger).Out = io.Discard + + zeroBlock := make([]byte, 1024) + err = bu.flushZeroBlocks(f, 0, 2048, zeroBlock, f.Name()) + + assert.NoError(t, err) + + data, err := os.ReadFile(f.Name()) + require.NoError(t, err) + assert.Equal(t, make([]byte, 2048), data) + }) +} + +type errReader struct { + err error +} + +func (r *errReader) Read(p []byte) (n int, err error) { + return 0, r.err +} + +func (r *errReader) Seek(offset int64, whence int) (int64, error) { + return 0, nil +} + +func TestRestoreData(t *testing.T) { + t.Run("success", func(t *testing.T) { + ctx := context.Background() + progress := &mockProgressUpdater{} + progress.On("UpdateProgress", mock.Anything).Return() + bu := &blockUploader{ + ctx: ctx, + progress: progress, + log: logrus.New(), + } + + f, err := os.CreateTemp("", "restoretest-*") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + + data := make([]byte, 1048576) + for i := range data { + data[i] = 1 + } + reader := bytes.NewReader(data) + + iterMock := cbtmocks.NewIterator(t) + iterMock.On("Count").Return(uint64(1)) + iterMock.On("Next").Return(uint64(0), true).Once() + iterMock.On("Next").Return(uint64(0), false) + + written, err := bu.restoreData(reader, f, iterMock, 1048576, f.Name()) + assert.NoError(t, err) + assert.Equal(t, int64(1048576), written) + + f.Seek(0, 0) + writtenData, err := io.ReadAll(f) + require.NoError(t, err) + assert.Equal(t, data, writtenData) + }) + + t.Run("read err", func(t *testing.T) { + ctx := context.Background() + bu := &blockUploader{ + ctx: ctx, + log: logrus.New(), + } + + f, err := os.CreateTemp("", "restoretest-*") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + + reader := &errReader{err: errors.New("read error")} + + iterMock := cbtmocks.NewIterator(t) + iterMock.On("Count").Return(uint64(1)) + iterMock.On("Next").Return(uint64(0), true).Once() + iterMock.On("Next").Return(uint64(0), false) + + _, err = bu.restoreData(reader, f, iterMock, 1048576, f.Name()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "read error") + }) +} + +func TestBlockUploaderRestore(t *testing.T) { + t.Run("missing metadata", func(t *testing.T) { + ctx := context.Background() + repoWriter := udmrepomocks.NewBackupRepo(t) + bu := NewUploader(ctx, repoWriter, nil, logrus.New()) + + repoWriter.On("ReadMetadata", mock.Anything, udmrepo.ID("root-id")).Return(nil, errors.New("meta not found")) + + iterMock := cbtmocks.NewIterator(t) + _, err := bu.Restore(udmrepo.Snapshot{RootObject: udmrepo.ObjectMetadata{ID: "root-id"}}, destInfo{}, iterMock, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "meta not found") + }) + + t.Run("success", func(t *testing.T) { + ctx := context.Background() + repoWriter := udmrepomocks.NewBackupRepo(t) + progress := &mockProgressUpdater{} + progress.On("UpdateProgress", mock.Anything).Return() + + bu := NewUploader(ctx, repoWriter, progress, logrus.New()) + + f, err := os.CreateTemp("", "restoretest-*") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + + meta := &udmrepo.Metadata{ + SubObjects: []udmrepo.ObjectMetadata{ + { + ID: "data-id", + Name: "bdev", + Size: 1048576, + }, + }, + } + + repoWriter.On("ReadMetadata", mock.Anything, udmrepo.ID("root-id")).Return(meta, nil) + + objReader := udmrepomocks.NewObjectReader(t) + objReader.On("Read", mock.Anything).Run(func(args mock.Arguments) { + p := args.Get(0).([]byte) + for i := range p { + p[i] = 1 + } + }).Return(1048576, io.EOF).Once() + objReader.On("Read", mock.Anything).Return(0, io.EOF) + objReader.On("Close").Return(nil) + + repoWriter.On("OpenObject", mock.Anything, udmrepo.ID("data-id")).Return(objReader, nil) + + snap := udmrepo.Snapshot{ + Description: "test snapshot", + RootObject: udmrepo.ObjectMetadata{ID: "root-id"}, + Tags: map[string]string{ + "bdev-source-size": "1048576", + }, + } + + dest := destInfo{ + dev: f, + size: 2048576, + path: f.Name(), + } + + iterMock := cbtmocks.NewIterator(t) + iterMock.On("Count").Return(uint64(1)) + iterMock.On("Next").Return(uint64(0), true).Once() + iterMock.On("Next").Return(uint64(0), false) + + written, err := bu.Restore(snap, dest, iterMock, nil) + assert.NoError(t, err) + assert.Equal(t, int64(1048576), written) + }) +} From 00f1626f7aaf318f4b5fb436e70889abbd05363b Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Fri, 24 Jul 2026 13:47:39 +0800 Subject: [PATCH 087/194] block uploader restore implementation Signed-off-by: Lyndon-Li --- changelogs/unreleased/10071-Lyndon-Li | 1 + pkg/uploader/block/uploader.go | 8 ++--- pkg/uploader/block/uploader_test.go | 42 +++++++++++++-------------- 3 files changed, 26 insertions(+), 25 deletions(-) create mode 100644 changelogs/unreleased/10071-Lyndon-Li diff --git a/changelogs/unreleased/10071-Lyndon-Li b/changelogs/unreleased/10071-Lyndon-Li new file mode 100644 index 000000000..dd3454a4d --- /dev/null +++ b/changelogs/unreleased/10071-Lyndon-Li @@ -0,0 +1 @@ +Fix issue #9828, add implementation for block uploader restore \ No newline at end of file diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 7717a7e7e..0378f4f5b 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -151,7 +151,7 @@ func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bi meta, err := blkup.repoWriter.ReadMetadata(blkup.ctx, snapshot.RootObject.ID) if err != nil { - return 0, errors.Wrapf(err, "error readding snapshot metadata for %s", snapshot.Description) + return 0, errors.Wrapf(err, "error reading snapshot metadata for %s", snapshot.Description) } if len(meta.SubObjects) != 1 { @@ -376,7 +376,7 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit offset, valid := bitmap.Next() var buffer []byte - var nextPos uint64 = uint64(0) + var nextPos = uint64(0) for valid { select { case <-blkup.ctx.Done(): @@ -513,13 +513,13 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit return written, nil } -func (bu *blockUploader) flushZeroBlocks(dest *os.File, start int64, length int64, zeroBlock []byte, destPath string) error { +func (blkup *blockUploader) flushZeroBlocks(dest *os.File, start int64, length int64, zeroBlock []byte, destPath string) error { err := blkZeroOut(dest, start, length) if err == nil { return nil } - bu.log.WithError(err).Warnf("Failed to call zero out from dev %s, start %v, length %v. Fallback to conservative way", destPath, start, length) + blkup.log.WithError(err).Warnf("Failed to call zero out from dev %s, start %v, length %v. Fallback to conservative way", destPath, start, length) var written int64 for written < length { diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index 69b5efcb5..bb7c79c5a 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -506,7 +506,7 @@ func TestGetSourceSize(t *testing.T) { if tc.expectErr { assert.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, tc.expected, size) } }) @@ -515,22 +515,22 @@ func TestGetSourceSize(t *testing.T) { func TestFlushZeroBlocks(t *testing.T) { t.Run("success via write fallback", func(t *testing.T) { - f, err := os.CreateTemp("", "zerotest-*") + f, err := os.CreateTemp(t.TempDir(), "zerotest-*") require.NoError(t, err) defer os.Remove(f.Name()) defer f.Close() require.NoError(t, f.Truncate(2048)) - bu := &blockUploader{ + blkup := &blockUploader{ log: logrus.New(), } - bu.log.(*logrus.Logger).Out = io.Discard + blkup.log.(*logrus.Logger).Out = io.Discard zeroBlock := make([]byte, 1024) - err = bu.flushZeroBlocks(f, 0, 2048, zeroBlock, f.Name()) + err = blkup.flushZeroBlocks(f, 0, 2048, zeroBlock, f.Name()) - assert.NoError(t, err) + require.NoError(t, err) data, err := os.ReadFile(f.Name()) require.NoError(t, err) @@ -555,13 +555,13 @@ func TestRestoreData(t *testing.T) { ctx := context.Background() progress := &mockProgressUpdater{} progress.On("UpdateProgress", mock.Anything).Return() - bu := &blockUploader{ + blkup := &blockUploader{ ctx: ctx, progress: progress, log: logrus.New(), } - f, err := os.CreateTemp("", "restoretest-*") + f, err := os.CreateTemp(t.TempDir(), "restoretest-*") require.NoError(t, err) defer os.Remove(f.Name()) defer f.Close() @@ -577,8 +577,8 @@ func TestRestoreData(t *testing.T) { iterMock.On("Next").Return(uint64(0), true).Once() iterMock.On("Next").Return(uint64(0), false) - written, err := bu.restoreData(reader, f, iterMock, 1048576, f.Name()) - assert.NoError(t, err) + written, err := blkup.restoreData(reader, f, iterMock, 1048576, f.Name()) + require.NoError(t, err) assert.Equal(t, int64(1048576), written) f.Seek(0, 0) @@ -589,12 +589,12 @@ func TestRestoreData(t *testing.T) { t.Run("read err", func(t *testing.T) { ctx := context.Background() - bu := &blockUploader{ + blkup := &blockUploader{ ctx: ctx, log: logrus.New(), } - f, err := os.CreateTemp("", "restoretest-*") + f, err := os.CreateTemp(t.TempDir(), "restoretest-*") require.NoError(t, err) defer os.Remove(f.Name()) defer f.Close() @@ -606,8 +606,8 @@ func TestRestoreData(t *testing.T) { iterMock.On("Next").Return(uint64(0), true).Once() iterMock.On("Next").Return(uint64(0), false) - _, err = bu.restoreData(reader, f, iterMock, 1048576, f.Name()) - assert.Error(t, err) + _, err = blkup.restoreData(reader, f, iterMock, 1048576, f.Name()) + require.Error(t, err) assert.Contains(t, err.Error(), "read error") }) } @@ -616,13 +616,13 @@ func TestBlockUploaderRestore(t *testing.T) { t.Run("missing metadata", func(t *testing.T) { ctx := context.Background() repoWriter := udmrepomocks.NewBackupRepo(t) - bu := NewUploader(ctx, repoWriter, nil, logrus.New()) + blkup := NewUploader(ctx, repoWriter, nil, logrus.New()) repoWriter.On("ReadMetadata", mock.Anything, udmrepo.ID("root-id")).Return(nil, errors.New("meta not found")) iterMock := cbtmocks.NewIterator(t) - _, err := bu.Restore(udmrepo.Snapshot{RootObject: udmrepo.ObjectMetadata{ID: "root-id"}}, destInfo{}, iterMock, nil) - assert.Error(t, err) + _, err := blkup.Restore(udmrepo.Snapshot{RootObject: udmrepo.ObjectMetadata{ID: "root-id"}}, destInfo{}, iterMock, nil) + require.Error(t, err) assert.Contains(t, err.Error(), "meta not found") }) @@ -632,9 +632,9 @@ func TestBlockUploaderRestore(t *testing.T) { progress := &mockProgressUpdater{} progress.On("UpdateProgress", mock.Anything).Return() - bu := NewUploader(ctx, repoWriter, progress, logrus.New()) + blkup := NewUploader(ctx, repoWriter, progress, logrus.New()) - f, err := os.CreateTemp("", "restoretest-*") + f, err := os.CreateTemp(t.TempDir(), "restoretest-*") require.NoError(t, err) defer os.Remove(f.Name()) defer f.Close() @@ -682,8 +682,8 @@ func TestBlockUploaderRestore(t *testing.T) { iterMock.On("Next").Return(uint64(0), true).Once() iterMock.On("Next").Return(uint64(0), false) - written, err := bu.Restore(snap, dest, iterMock, nil) - assert.NoError(t, err) + written, err := blkup.Restore(snap, dest, iterMock, nil) + require.NoError(t, err) assert.Equal(t, int64(1048576), written) }) } From d44a185115096a180990979043ee36529f5ba6be Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 22 Jul 2026 12:45:30 -0700 Subject: [PATCH 088/194] Remove community health files now provided by org-level .github repo These files are now maintained in the velero-io/.github repo and automatically apply as org-wide defaults across all velero-io repos. See https://github.com/velero-io/.github Fixes #10042 Signed-off-by: Shubham Pampattiwar --- CODE_OF_CONDUCT.md | 148 --------------------------------------------- CONTRIBUTING.md | 3 - GOVERNANCE.md | 135 ----------------------------------------- SECURITY.md | 128 --------------------------------------- SUPPORT.md | 7 --- 5 files changed, 421 deletions(-) delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.md delete mode 100644 GOVERNANCE.md delete mode 100644 SECURITY.md delete mode 100644 SUPPORT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index fe6ec8c6f..000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,148 +0,0 @@ -# Velero Code of Conduct - -Velero is a [Cloud Native Computing Foundation](https://www.cncf.io/) sandbox -project. As a CNCF project, the Velero community follows the -[**CNCF Code of Conduct**](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). - -The text below is the project's adopted Code of Conduct, based on the -[Contributor Covenant](https://www.contributor-covenant.org/), and is -substantively aligned with the CNCF Code of Conduct. Where any conflict exists, -the CNCF Code of Conduct prevails. - -Instances of unacceptable behavior may be reported to the CNCF Code of -Conduct Committee at [conduct@cncf.io](mailto:conduct@cncf.io). For more -detailed instructions on how to submit a report, including how to submit a -report anonymously, please see the CNCF -[Incident Resolution Procedures](https://github.com/cncf/foundation/blob/main/code-of-conduct/coc-incident-resolution-procedures.md). -You can expect a response within three business days. - ---- - -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in the Velero project and our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socioeconomic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the CNCF Code of Conduct Committee at -[conduct@cncf.io](mailto:conduct@cncf.io). -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 24d7f4dbd..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contributing - -Authors are expected to follow some guidelines when submitting PRs. Please see [our documentation](https://velero.io/docs/main/code-standards/) for details. diff --git a/GOVERNANCE.md b/GOVERNANCE.md deleted file mode 100644 index 73d5a7069..000000000 --- a/GOVERNANCE.md +++ /dev/null @@ -1,135 +0,0 @@ -# Velero Governance - -This document defines the project governance for Velero. - -## Overview - -**Velero**, an open source project, is committed to building an open, inclusive, productive and self-governing open source community focused on building a high quality tool that enables users to safely backup and restore, perform disaster recovery, and migrate Kubernetes cluster resources and persistent volumes. The community is governed by this document with the goal of defining how community should work together to achieve this goal. - -## Code Repositories - -The following code repositories are governed by Velero community and maintained under the `velero-io` organization. - -* **[Velero](https://github.com/velero-io/velero):** Main Velero codebase -* **[Helm Chart](https://github.com/vmware-tanzu/helm-charts/tree/main/charts/velero):** The Helm chart for the Velero server component -* **[Velero CSI Plugin](https://github.com/vmware-tanzu/velero-plugin-for-csi):** This repository contains Velero plugins for snapshotting CSI backed PVCs using the CSI beta snapshot APIs -* **[Velero Plugin for vSphere](https://github.com/vmware-tanzu/velero-plugin-for-vsphere):** This repository contains the Velero Plugin for vSphere. This plugin is a volume snapshotter plugin that provides crash-consistent snapshots of vSphere block volumes and backup of volume data into S3 compatible storage. -* **[Velero Plugin for AWS](https://github.com/velero-io/velero-plugin-for-aws):** This repository contains the plugins to support running Velero on AWS, including the object store plugin and the volume snapshotter plugin -* **[Velero Plugin for GCP](https://github.com/velero-io/velero-plugin-for-gcp):** This repository contains the plugins to support running Velero on GCP, including the object store plugin and the volume snapshotter plugin -* **[Velero Plugin for Azure](https://github.com/velero-io/velero-plugin-for-microsoft-azure):** This repository contains the plugins to support running Velero on Azure, including the object store plugin and the volume snapshotter plugin -* **[Velero Plugin Example](https://github.com/velero-io/velero-plugin-example):** This repository contains example plugins for Velero - - -## Community Roles - -* **Users:** Members that engage with the Velero community via any medium (Slack, GitHub, mailing lists, etc.). -* **Contributors:** Regular contributions to projects (documentation, code reviews, responding to issues, participation in proposal discussions, contributing code, etc.). -* **Maintainers**: The Velero project leaders. They are responsible for the overall health and direction of the project; final reviewers of PRs and responsible for releases. Some Maintainers are responsible for one or more components within a project, acting as technical leads for that component. Maintainers are expected to contribute code and documentation, review PRs including ensuring quality of code, triage issues, proactively fix bugs, and perform maintenance tasks for these components. - -### Maintainers - -New maintainers must be nominated by an existing maintainer and must be elected by a supermajority of existing maintainers. Likewise, maintainers can be removed by a supermajority of the existing maintainers or can resign by notifying one of the maintainers. - -### Supermajority - -A supermajority is defined as two-thirds of members in the group. -A supermajority of [Maintainers](#maintainers) is required for certain -decisions as outlined above. A supermajority vote is equivalent to the number of votes in favor being at least twice the number of votes against. For example, if you have 5 maintainers, a supermajority vote is 4 votes. Voting on decisions can happen on the mailing list, GitHub, Slack, email, or via a voting service, when appropriate. Maintainers can either vote "agree, yes, +1", "disagree, no, -1", or "abstain". A vote passes when supermajority is met. An abstain vote equals not voting at all. - -### Decision Making - -Ideally, all project decisions are resolved by consensus. If impossible, any -maintainer may call a vote. Unless otherwise specified in this document, any -vote will be decided by a supermajority of maintainers. - -Votes by maintainers belonging to the same company -will count as one vote; e.g., 4 maintainers employed by fictional company **Valerium** will -only have **one** combined vote. If voting members from a given company do not -agree, the company's vote is determined by a supermajority of voters from that -company. If no supermajority is achieved, the company is considered to have -abstained. - -## Proposal Process - -One of the most important aspects in any open source community is the concept -of proposals. Large changes to the codebase and / or new features should be -preceded by a proposal in our community repo. This process allows for all -members of the community to weigh in on the concept (including the technical -details), share their comments and ideas, and offer to help. It also ensures -that members are not duplicating work or inadvertently stepping on toes by -making large conflicting changes. - -The project roadmap is defined by accepted proposals. - -Proposals should cover the high-level objectives, use cases, and technical -recommendations on how to implement. In general, the community member(s) -interested in implementing the proposal should be either deeply engaged in the -proposal process or be an author of the proposal. - -The proposal should be documented as a separated markdown file pushed to the root of the -`design` folder in the [Velero](https://github.com/velero-io/velero/tree/main/design) -repository via PR. The name of the file should follow the name pattern `_design.md`, e.g: -`restore-hooks-design.md`. - -Use the [Proposal Template](https://github.com/velero-io/velero/blob/main/design/_template.md) as a starting point. - -### Proposal Lifecycle - -The proposal PR can follow the GitHub lifecycle of the PR to indicate its status: - -* **Open**: Proposal is created and under review and discussion. -* **Merged**: Proposal has been reviewed and is accepted (either by consensus or through a vote). -* **Closed**: Proposal has been reviewed and was rejected (either by consensus or through a vote). - -## Lazy Consensus - -To maintain velocity in a project as busy as Velero, the concept of [Lazy -Consensus](http://en.osswiki.info/concepts/lazy_consensus) is practiced. Ideas -and / or proposals should be shared by maintainers via -GitHub with the appropriate maintainer groups (e.g., -`@velero-io/velero-maintainers`) tagged. Out of respect for other contributors, -major changes should also be accompanied by a ping on Slack or a note on the -Velero mailing list as appropriate. Author(s) of proposal, Pull Requests, -issues, etc. will give a time period of no less than five (5) working days for -comment and remain cognizant of popular observed world holidays. - -Other maintainers may chime in and request additional time for review, but -should remain cognizant of blocking progress and abstain from delaying -progress unless absolutely needed. The expectation is that blocking progress -is accompanied by a guarantee to review and respond to the relevant action(s) -(proposals, PRs, issues, etc.) in short order. - -Lazy Consensus is practiced for all projects in the `Velero` org, including -the main project repository and the additional repositories. - -Lazy consensus does _not_ apply to the process of: - -* Removal of maintainers from Velero - -## Deprecation Policy - -### Deprecation Process - -Any contributor may introduce a request to deprecate a feature or an option of a feature by opening a feature request issue in the velero-io/velero GitHub project. The issue should describe why the feature is no longer needed or has become detrimental to Velero, as well as whether and how it has been superseded. The submitter should give as much detail as possible. - -Once the issue is filed, a one-month discussion period begins. Discussions take place within the issue itself as well as in the community meetings. The person who opens the issue, or a maintainer, should add the date and time marking the end of the discussion period in a comment on the issue as soon as possible after it is opened. A decision on the issue needs to be made within this one-month period. - -The feature will be deprecated by a supermajority vote of 50% plus one of the project maintainers at the time of the vote tallying, which is 72 hours after the end of the community meeting that is the end of the comment period. (Maintainers are permitted to vote in advance of the deadline, but should hold their votes until as close as possible to hear all possible discussion.) Votes will be tallied in comments on the issue. - -Non-maintainers may add non-binding votes in comments to the issue as well; these are opinions to be taken into consideration by maintainers, but they do not count as votes. - -If the vote passes, the deprecation window takes effect in the subsequent release, and the removal follows the schedule. - -### Schedule -If depreciation proposal passes by supermajority votes, the feature is deprecated in the next minor release and the feature can be removed completely after two minor version or equivalent major version e.g., if feature gets deprecated in Nth minor version, then feature can be removed after N+2 minor version or its equivalent if the major version number changes. - -### Deprecation Window - -The deprecation window is the period from the release in which the deprecation takes effect through the release in which the feature is removed. During this period, only critical security vulnerabilities and catastrophic bugs should be fixed. - -**Note:** If a backup relies on a deprecated feature, then backups made with the last Velero release before this feature is removed must still be restorable in version `n+2`. For instance, something like restic feature support, that might mean that restic is removed from the list of supported uploader types in version `n` but the underlying implementation required to restore from a restic backup won't be removed until release `n+2`. - -## Updating Governance - -All substantive changes in Governance require a supermajority agreement by all maintainers. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 219426f6f..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,128 +0,0 @@ -# Security Release Process - -Velero is an open source tool with a growing community devoted to safe backup and restore, disaster recovery, and data migration of Kubernetes resources and persistent volumes. The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues. - - -## Supported Versions - -The Velero project maintains the following [governance document](https://github.com/velero-io/velero/blob/main/GOVERNANCE.md), [release document](https://github.com/velero-io/velero/blob/f42c63af1b9af445e38f78a7256b1c48ef79c10e/site/docs/main/release-instructions.md), and [support document](https://velero.io/docs/main/support-process/). Please refer to these for release and related details. Only the most recent version of Velero is supported. Each [release](https://github.com/velero-io/velero/releases) includes information about upgrading to the latest version. - - -## Reporting a Vulnerability - Private Disclosure Process - -Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to Velero privately, to minimize attacks against current users of Velero before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project. - -If you know of a publicly disclosed security vulnerability for Velero, please **IMMEDIATELY** contact the Security Team (velero-security.pdl@broadcom.com). - - - -**IMPORTANT: Do not file public issues on GitHub for security vulnerabilities** - -To report a vulnerability or a security-related issue, please contact the email address with the details of the vulnerability. The email will be fielded by the Security Team and then shared with the Velero maintainers who have committer and release permissions. Emails will be addressed within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/velero-io/velero/issues/new/choose) instead. - - -## Proposed Email Content - -Provide a descriptive subject line and in the body of the email include the following information: - - - -* Basic identity information, such as your name and your affiliation or company. -* Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and logs are all helpful to us). -* Description of the effects of the vulnerability on Velero and the related hardware and software configurations, so that the Security Team can reproduce it. -* How the vulnerability affects Velero usage and an estimation of the attack surface, if there is one. -* List other projects or dependencies that were used in conjunction with Velero to produce the vulnerability. - - - - -## When to report a vulnerability - - - -* When you think Velero has a potential security vulnerability. -* When you suspect a potential vulnerability but you are unsure that it impacts Velero. -* When you know of or suspect a potential vulnerability on another project that is used by Velero. - - - - -## Patch, Release, and Disclosure - -The Security Team will respond to vulnerability reports as follows: - - - - - -1. The Security Team will investigate the vulnerability and determine its effects and criticality. -2. If the issue is not deemed to be a vulnerability, the Security Team will follow up with a detailed reason for rejection. -3. The Security Team will initiate a conversation with the reporter within 3 business days. -4. If a vulnerability is acknowledged and the timeline for a fix is determined, the Security Team will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out. -5. The Security Team will also create a [CVSS](https://www.first.org/cvss/specification-document) using the [CVSS Calculator](https://www.first.org/cvss/calculator/3.0). The Security Team makes the final call on the calculated CVSS; it is better to move quickly than making the CVSS perfect. Issues may also be reported to [Mitre](https://cve.mitre.org/) using this [scoring calculator](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The CVE will initially be set to private. -6. The Security Team will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix. -7. The Security Team will provide early disclosure of the vulnerability by emailing the [Velero Distributors](https://groups.google.com/u/1/g/projectvelero-distributors) mailing list. Distributors can initially plan for the vulnerability patch ahead of the fix, and later can test the fix and provide feedback to the Velero team. See the section **Early Disclosure to Velero Distributors List** for details about how to join this mailing list. -8. A public disclosure date is negotiated by the SecurityTeam, the bug submitter, and the distributors list. We prefer to fully disclose the bug as soon as possible once a user mitigation or patch is available. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for distributor coordination. The timeframe for disclosure is from immediate (especially if it’s already publicly known) to a few weeks. For a critical vulnerability with a straightforward mitigation, we expect the report date for the public disclosure date to be on the order of 14 business days. The Security Team holds the final say when setting a public disclosure date. -9. Once the fix is confirmed, the Security Team will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. Upon release of the patched version of Velero, we will follow the **Public Disclosure Process**. - - -## Public Disclosure Process - -The Security Team publishes a [public advisory](https://github.com/velero-io/velero/security/advisories) to the Velero community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog and other channels will assist in educating Velero users and rolling out the patched release to affected users. - -The Security Team will also publish any mitigating steps users can take until the fix can be applied to their Velero instances. Velero distributors will handle creating and publishing their own security advisories. - - - - -## Mailing lists - - - -* Use velero-security.pdl@broadcom.com to report security concerns to the Security Team, who uses the list to privately discuss security issues and fixes prior to disclosure. -* Join the [Velero Distributors](https://groups.google.com/u/1/g/projectvelero-distributors) mailing list for early private information and vulnerability disclosure. Early disclosure may include mitigating steps and additional information on security patch releases. See below for information on how Velero distributors or vendors can apply to join this list. - - -## Early Disclosure to Velero Distributors List - -The private list is intended to be used primarily to provide actionable information to multiple distributor projects at once. This list is not intended to inform individuals about security issues. - - -## Membership Criteria - -To be eligible to join the [Velero Distributors](https://groups.google.com/u/1/g/projectvelero-distributors) mailing list, you should: - - - -1. Be an active distributor of Velero. -2. Have a user base that is not limited to your own organization. -3. Have a publicly verifiable track record up to the present day of fixing security issues. -4. Not be a downstream or rebuild of another distributor. -5. Be a participant and active contributor in the Velero community. -6. Accept the Embargo Policy that is outlined below. -7. Have someone who is already on the list vouch for the person requesting membership on behalf of your distribution. - -**The terms and conditions of the Embargo Policy apply to all members of this mailing list. A request for membership represents your acceptance to the terms and conditions of the Embargo Policy.** - - -## Embargo Policy - -The information that members receive on the Velero Distributors mailing list must not be made public, shared, or even hinted at anywhere beyond those who need to know within your specific team, unless you receive explicit approval to do so from the Security Team. This remains true until the public disclosure date/time agreed upon by the list. Members of the list and others cannot use the information for any reason other than to get the issue fixed for your respective distribution's users. - -Before you share any information from the list with members of your team who are required to fix the issue, these team members must agree to the same terms, and only be provided with information on a need-to-know basis. - -In the unfortunate event that you share information beyond what is permitted by this policy, you must urgently inform the Security Team (velero-security.pdl@broadcom.com) of exactly what information was leaked and to whom. If you continue to leak information and break the policy outlined here, you will be permanently removed from the list. - - - - -## Requesting to Join - -Send new membership requests to projectvelero-distributors@googlegroups.com. In the body of your request please specify how you qualify for membership and fulfill each criterion listed in the Membership Criteria section above. - - -## Confidentiality, integrity and availability - -We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns. Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern. The Security Team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner. - -Note that we do not currently consider the default settings for Velero to be secure-by-default. It is necessary for operators to explicitly configure settings, role based access control, and other resource related features in Velero to provide a hardened Velero environment. We will not act on any security disclosure that relates to a lack of safe defaults. Over time, we will work towards improved safe-by-default configuration, taking into account backwards compatibility. diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index 62c461036..000000000 --- a/SUPPORT.md +++ /dev/null @@ -1,7 +0,0 @@ -# Velero Support - -Thanks for trying out Velero! We welcome all feedback, find all the ways to connect with us on our Community page: - -- [Velero Community](https://velero.io/community/) - -You can find details on the Velero maintainers' support process [here](https://velero.io/docs/main/support-process/). From a12b373e4cd379d2ab619c0cfbbd988f1f2684f1 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Sat, 25 Jul 2026 04:03:51 +0800 Subject: [PATCH 089/194] Support set-based filter label selectors (#10064) * Support set-based filter label selectors Use matchLabels/matchExpressions in fine-grained filters. Signed-off-by: Adam Zhang * omit the details of resource policy for cli The reason to not resolve and display CLI is because it may go out of sync, we want to avoid display it to mislead users. We may consider to cpature those information and display it in later release. Signed-off-by: Adam Zhang --------- Signed-off-by: Adam Zhang Co-authored-by: Scott Seago --- changelogs/unreleased/10064-adam-jian-zhang | 1 + .../fine-grained-backup-filters-design.md | 137 ++++++------ .../fine-grained-restore-filters-design.md | 64 ++++-- .../resourcepolicies/resource_policies.go | 88 +++++++- .../resource_policies_test.go | 197 ++++++++++++++++-- pkg/backup/backup.go | 16 +- pkg/backup/backup_test.go | 92 ++++++-- pkg/cmd/util/output/backup_describer.go | 118 ----------- pkg/cmd/util/output/backup_describer_test.go | 85 -------- .../output/backup_structured_describer.go | 85 -------- .../backup_structured_describer_test.go | 96 --------- pkg/restore/restore.go | 16 +- pkg/restore/restore_policies_test.go | 3 +- .../docs/main/fine-grained-backup-filters.md | 127 ++++++++--- 14 files changed, 575 insertions(+), 550 deletions(-) create mode 100644 changelogs/unreleased/10064-adam-jian-zhang diff --git a/changelogs/unreleased/10064-adam-jian-zhang b/changelogs/unreleased/10064-adam-jian-zhang new file mode 100644 index 000000000..9d45481db --- /dev/null +++ b/changelogs/unreleased/10064-adam-jian-zhang @@ -0,0 +1 @@ +Add set based label selectors for fine-grained filters diff --git a/design/backup-filter-enhancement/fine-grained-backup-filters-design.md b/design/backup-filter-enhancement/fine-grained-backup-filters-design.md index 0bb52ffd5..3fef82974 100644 --- a/design/backup-filter-enhancement/fine-grained-backup-filters-design.md +++ b/design/backup-filter-enhancement/fine-grained-backup-filters-design.md @@ -41,7 +41,7 @@ This creates three critical gaps for common backup scenarios: - Maintain full backward compatibility — existing backups with no `namespacedFilterPolicies` behave exactly as they do today - Define clear precedence rules for how per-namespace filters interact with global filters - Add corresponding validation within the Resource Policies validation pipeline using existing Velero wildcard validation functions -- Update `velero backup describe` output to display per-namespace filter information when present +- Update `velero backup describe` output to display the referenced ResourcePolicy ConfigMap name when configured - Ensure the restore process works correctly with backups produced by namespace-scoped filters, without requiring restore-side code changes in the initial phase ## Non-Goals @@ -77,7 +77,8 @@ clusterScopedFilterPolicy: names: ["my-app-*"] - kinds: [CustomResourceDefinition] labelSelector: - app: my-app + matchLabels: + app: my-app namespacedFilterPolicies: # NEW: per-namespace filter overrides - namespaces: @@ -85,7 +86,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret, Deployment] labelSelector: - app: my-app + matchLabels: + app: my-app - namespaces: - ns-b resourceFilters: @@ -93,7 +95,8 @@ namespacedFilterPolicies: names: [app-1, app-2] - kinds: [ConfigMap] labelSelector: - app: my-service + matchLabels: + app: my-service ``` All four sections coexist in the same ConfigMap. They are independent — `volumePolicies` handles volume backup strategy, `includeExcludePolicy` handles global resource type filtering, `clusterScopedFilterPolicy` handles cluster-scoped resource filtering by kind/name/label, and `namespacedFilterPolicies` handles per-namespace, per-kind overrides. @@ -107,7 +110,9 @@ namespacedFilterPolicies: - namespaces: [ns-a] resourceFilters: - kinds: [ConfigMap, Secret] # these kinds share a selector - labelSelector: {app: my-app} + labelSelector: + matchLabels: + app: my-app names: ["app-*"] - kinds: [Deployment] # this kind has its own selector names: [workload-1, workload-2] @@ -116,6 +121,24 @@ namespacedFilterPolicies: This model has one way to express filters — there is no ambiguity about how to structure the configuration. Only resource kinds listed in `resourceFilters` entries are included in the backup for the matched namespaces; unlisted kinds are implicitly excluded. +#### Label selectors (`matchLabels` / `matchExpressions`) + +`labelSelector` and each entry of `orLabelSelectors` use the standard Kubernetes selector shape (same as `BackupSpec.labelSelector`): + +```yaml +labelSelector: + matchLabels: + app: my-app + matchExpressions: + - key: environment + operator: In + values: [prod, staging] + - key: do-not-backup + operator: DoesNotExist +``` + +Supported `matchExpressions` operators: `In`, `NotIn`, `Exists`, `DoesNotExist`. Prefer `In` for value-OR on one key; use `orLabelSelectors` for OR across independent multi-key groups. `labelSelector` and `orLabelSelectors` cannot co-exist in the same `resourceFilters` entry. + #### Catch-All Resource Filter (Empty `kinds` or `["*"]`) A `ResourceFilter` entry with an empty (or omitted) `kinds` field, or a field explicitly set to `["*"]`, acts as a **catch-all**. Its `labelSelector` or `orLabelSelectors` (if provided) is applied to **all resource types in the namespace that are not already matched by a kind-specific filter entry**. If no selectors are provided, all unlisted resources are included. Using `["*"]` is highly recommended as it makes the catch-all intention explicit and self-documenting. @@ -319,9 +342,10 @@ resourceFilters: resourceFilters: - kinds: ["Pod"] labelSelector: - "invalid label key!": "value" # invalid key syntax + matchLabels: + "invalid label key!": "value" # invalid key syntax ``` -**Behavior:** Validation error during backup creation when `labels.SelectorFromSet()` fails: +**Behavior:** Validation error during backup creation when `metav1.LabelSelectorAsSelector()` fails: ``` namespacedFilterPolicies[0].resourceFilters[0]: invalid label selector: "invalid label key!" is not a valid label key ``` @@ -340,7 +364,33 @@ This is consistent with how other discovery-dependent features handle this error ## ResourceFilter Field Notes -**`labelSelector`** supports equality-based selectors only (`key=value`). Set-based requirements (e.g., `environment in (prod, staging)`) are not supported. To match resources with any of several label combinations, use `orLabelSelectors` with multiple maps — each map is AND-evaluated internally, and the maps are OR-evaluated across the list. `labelSelector` and `orLabelSelectors` cannot co-exist in the same entry. +**`labelSelector`** uses the standard Kubernetes shape: `matchLabels` (equality) and `matchExpressions` (set-based: `In`, `NotIn`, `Exists`, `DoesNotExist`). All requirements within one selector are AND-ed. Example: + +```yaml +labelSelector: + matchLabels: + app: my-app + matchExpressions: + - key: environment + operator: In + values: [prod, staging] + - key: do-not-backup + operator: DoesNotExist +``` + +**`orLabelSelectors`** is a list of the same selector shape. Match if **any** entry matches (AND within each entry, OR across the list). Prefer `In` for value-OR on one key; use `orLabelSelectors` for OR of independent multi-key groups. `labelSelector` and `orLabelSelectors` cannot co-exist in the same entry. + +```yaml +orLabelSelectors: + - matchLabels: + tier: frontend + matchExpressions: + - key: track + operator: In + values: [canary] + - matchLabels: + tier: backend +``` **`names` / `excludedNames`** accept exact resource names or glob patterns. If `names` is empty, all resource names are included (subject to label filters). `excludedNames` takes precedence over `names` when a name matches both. @@ -420,7 +470,8 @@ data: resourceFilters: - kinds: [ConfigMap, Secret, Deployment] labelSelector: - app: my-app + matchLabels: + app: my-app # ns-b has no filter policy entry, so global filters apply (include everything) ``` @@ -462,10 +513,12 @@ data: resourceFilters: - kinds: [Deployment] labelSelector: - app: production-workload-1 + matchLabels: + app: production-workload-1 - kinds: [StatefulSet] labelSelector: - app: production-workload-2 + matchLabels: + app: production-workload-2 ``` ### Per-Kind Exact Names @@ -561,7 +614,8 @@ data: resourceFilters: - kinds: ["*"] # catch-all: applies to every kind not listed below labelSelector: - backup: "true" # back up any resource carrying this label + matchLabels: + backup: "true" # back up any resource carrying this label ``` **Result:** Every resource type in `production` that has the label `backup=true` is backed up. Resources without that label are excluded. No kind enumeration is required. @@ -589,7 +643,8 @@ data: names: [db-credentials, tls-cert] # these exact Secrets by name - kinds: ["*"] # catch-all for all other kinds labelSelector: - backup: "true" # back up by label + matchLabels: + backup: "true" # back up by label ``` **Result:** @@ -666,7 +721,8 @@ data: names: [workload-1, workload-2] - kinds: [StatefulSet] labelSelector: - app: my-app + matchLabels: + app: my-app - kinds: [ConfigMap, Secret] names: ["app-*"] excludedNames: ["*-tmp", "*-debug"] @@ -697,7 +753,7 @@ spec: ### `velero backup describe` -The output is extended to display namespace-scoped filter policies when present in the ResourcePolicy ConfigMap: +The output displays the referenced ResourcePolicy ConfigMap name when configured on the backup. It intentionally avoids resolving and displaying the live ConfigMap contents, because the ConfigMap content in the cluster may be modified or deleted after the backup execution, which could lead to displaying out-of-sync or inaccurate information: ``` Name: selective-backup @@ -721,46 +777,9 @@ Resources: Label selector: -Resource Policy: backup-filter-policy - -Namespace-Scoped Filter Policies: - ns-a: - Resource Filters: - ConfigMap, Secret, Deployment: - Label selector: app=my-app - Included names: - Excluded names: - target-namespace: - Resource Filters: - Deployment: - Label selector: app=production-workload-1 - Included names: - Excluded names: - StatefulSet: - Label selector: app=production-workload-2 - Included names: - Excluded names: - production: - Resource Filters: - Deployment: - Label selector: - Included names: [api-server, worker] - Excluded names: - (all other kinds): - Label selector: backup=true - Included names: - Excluded names: - -Fine-Grained Global Filter Policy: - Resource Filters: - ClusterRole, ClusterRoleBinding: - Label selector: - Included names: [my-app-*] - Excluded names: - CustomResourceDefinition: - Label selector: app=my-app - Included names: - Excluded names: +Resource policies: + Type: configmap + Name: backup-filter-policy Storage Location: default @@ -795,7 +814,7 @@ Notes: - Global filters (--include-resources, --selector, etc.) apply to all included namespaces - Namespace-scoped filters defined in --resource-policies-configmap override global filters for matching namespaces - Fine-grained global filter policies defined in --resource-policies-configmap override global filters for cluster-scoped resources -- Use 'velero backup describe' to view resolved filter policies after backup creation +- Use 'velero backup describe' to view the referenced ResourcePolicy ConfigMap name after backup creation ``` ### CLI Integration Points @@ -808,12 +827,12 @@ Notes: **Help and Discovery:** - `velero backup create --help` includes updated filtering documentation -- `velero backup describe` shows resolved filter policies for troubleshooting +- `velero backup describe` shows the referenced ResourcePolicy ConfigMap name - Validation errors include ConfigMap field references for easy debugging **Configuration Discovery:** - `velero backup create --help` includes namespace-scoped filtering documentation -- `velero backup describe` shows resolved filter policies for verification +- `velero backup describe` shows the referenced ResourcePolicy ConfigMap name for verification ## User Perspective @@ -823,7 +842,7 @@ This design provides fine-grained, per-namespace, per-kind control over backup f - **For users adopting namespace-scoped filter policies**: Create a ConfigMap with the `namespacedFilterPolicies` section and reference it via `BackupSpec.ResourcePolicy` (or the existing `--resource-policies-configmap` flag). The backup will selectively include/exclude resources per namespace based on the filter rules. - **For users already using ResourcePolicy for volume policies**: Add the `namespacedFilterPolicies` section to the same ConfigMap. Both volume policies and namespace-scoped filters coexist. - **For restore from a namespace-filtered backup**: No changes to restore workflow. Restore processes whatever is in the archive. Users can use existing `RestoreSpec.IncludedNamespaces` for additional filtering at restore time. -- **`velero backup describe` output**: Extended to show per-namespace, per-kind filter details when the ResourcePolicy ConfigMap contains `namespacedFilterPolicies`. +- **`velero backup describe` output**: Displays the referenced ResourcePolicy ConfigMap name when configured on the backup. - **Validation errors**: Reported at backup start when the ResourcePolicy ConfigMap contains invalid `namespacedFilterPolicies` configurations. Consistent with how volume policy validation errors are reported today. ## Alternatives Considered diff --git a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md index 913c056c0..fd3069b68 100644 --- a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md +++ b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md @@ -108,14 +108,16 @@ clusterScopedFilterPolicy: names: ["my-app-*"] - kinds: [CustomResourceDefinition] labelSelector: - app: my-app + matchLabels: + app: my-app namespacedFilterPolicies: - namespaces: - ns-a resourceFilters: - kinds: [ConfigMap, Secret, Deployment] labelSelector: - app: my-app + matchLabels: + app: my-app - namespaces: - ns-b resourceFilters: @@ -123,7 +125,8 @@ namespacedFilterPolicies: names: [app-1, app-2] - kinds: [ConfigMap] labelSelector: - app: my-service + matchLabels: + app: my-service ``` The restore-side ConfigMap does **not** require `volumePolicies` or `includeExcludePolicy` sections. Those are backup-specific. The YAML parser will ignore unknown fields gracefully, so a user can technically point to the same ConfigMap used for backup — the restore pipeline will only read `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. @@ -137,7 +140,9 @@ namespacedFilterPolicies: - namespaces: [ns-a] resourceFilters: - kinds: [ConfigMap, Secret] # these kinds share a selector - labelSelector: {app: my-app} + labelSelector: + matchLabels: + app: my-app names: ["app-*"] - kinds: [Deployment] # this kind has its own selector names: [workload-1, workload-2] @@ -146,6 +151,36 @@ namespacedFilterPolicies: Only resource kinds listed in `resourceFilters` entries are restored for the matched namespaces; unlisted kinds are implicitly excluded (globally excluded kinds cannot be re-included — see precedence model). +#### Label selectors (`matchLabels` / `matchExpressions`) + +`labelSelector` and each entry of `orLabelSelectors` use the standard Kubernetes selector shape (same as `RestoreSpec.labelSelector`): + +```yaml +labelSelector: + matchLabels: + app: my-app + matchExpressions: + - key: environment + operator: In + values: [prod, staging] + - key: do-not-restore + operator: DoesNotExist +``` + +Supported `matchExpressions` operators: `In`, `NotIn`, `Exists`, `DoesNotExist`. Prefer `In` for value-OR on one key; use `orLabelSelectors` for OR across independent multi-key groups. `labelSelector` and `orLabelSelectors` cannot co-exist in the same `resourceFilters` entry. + +```yaml +orLabelSelectors: + - matchLabels: + tier: frontend + matchExpressions: + - key: track + operator: In + values: [canary] + - matchLabels: + tier: backend +``` + #### Peek-and-Map Fallback for Unresolved Kinds The `kinds` field accepts both plural resource names (e.g., `configmaps`, `mycustomkinds.mygroup.io`) and singular `Kind` names (e.g., `ConfigMap`, `MyCustomKind`). @@ -382,9 +417,10 @@ resourceFilters: resourceFilters: - kinds: ["Deployment"] labelSelector: - "invalid label key!": "value" # invalid key syntax + matchLabels: + "invalid label key!": "value" # invalid key syntax ``` -**Behavior:** Validation error during restore creation when `labels.ValidatedSelectorFromSet()` fails: +**Behavior:** Validation error during restore creation when `metav1.LabelSelectorAsSelector()` fails: ``` namespacedFilterPolicies[0].resourceFilters[0]: invalid label selector: "invalid label key!" is not a valid label key ``` @@ -420,7 +456,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret] # Secret listed here is ineffective — globally excluded labelSelector: - app: my-app + matchLabels: + app: my-app - kinds: [Deployment] ``` @@ -461,8 +498,8 @@ After existing filter setup, the filter policies are resolved into the runtime m The `resolveRestoreNamespacedFilterPolicies` function: - For each `NamespacedFilterPolicy`, iterates its `ResourceFilters` entries - Resolves kind names to fully-qualified group-resource strings using the discovery helper -- Converts `labelSelector` maps into `labels.Selector` objects using `labels.ValidatedSelectorFromSet()` -- Converts `orLabelSelectors` maps into `[]labels.Selector` +- Converts `labelSelector` into a `labels.Selector` via `ToMetaV1LabelSelector` + `metav1.LabelSelectorAsSelector()` +- Converts `orLabelSelectors` into `[]labels.Selector` the same way - Creates `IncludesExcludes` instances for `names`/`excludedNames` patterns - Identifies catch-all entries (empty or `["*"]` kinds) and stores them in `catchAllFilter` - Builds a `resourceFilterMap` keyed by the resolved group-resource string @@ -537,7 +574,8 @@ data: resourceFilters: - kinds: [Deployment, ConfigMap] labelSelector: - app: my-app + matchLabels: + app: my-app # ns-b has no filter policy entry, so global filters apply (restore everything) ``` @@ -631,7 +669,8 @@ data: names: [db-credentials, tls-cert] # these exact Secrets by name - kinds: ["*"] # catch-all for all other kinds labelSelector: - backup: "true" # restore by label + matchLabels: + backup: "true" # restore by label ``` **Result:** @@ -658,7 +697,8 @@ data: names: ["my-app-*"] - kinds: [CustomResourceDefinition] labelSelector: - app: my-app + matchLabels: + app: my-app namespacedFilterPolicies: - namespaces: - production diff --git a/internal/resourcepolicies/resource_policies.go b/internal/resourcepolicies/resource_policies.go index 235f48ed5..39504d6ff 100644 --- a/internal/resourcepolicies/resource_policies.go +++ b/internal/resourcepolicies/resource_policies.go @@ -21,12 +21,13 @@ import ( "fmt" "strings" - "k8s.io/apimachinery/pkg/util/sets" - "github.com/cockroachdb/errors" "github.com/gobwas/glob" "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/sets" crclient "sigs.k8s.io/controller-runtime/pkg/client" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -100,13 +101,66 @@ func (a *Action) GetDataMover() (string, error) { return dataMover, nil } +// PolicyLabelSelector mirrors metav1.LabelSelector with yaml tags for ConfigMap decode. +// metav1.LabelSelector only has json tags, which do not populate under go.yaml.in/yaml/v3. +type PolicyLabelSelector struct { + MatchLabels map[string]string `yaml:"matchLabels,omitempty"` + MatchExpressions []PolicyLabelSelectorRequirement `yaml:"matchExpressions,omitempty"` +} + +// PolicyLabelSelectorRequirement mirrors metav1.LabelSelectorRequirement with yaml tags. +type PolicyLabelSelectorRequirement struct { + Key string `yaml:"key"` + Operator string `yaml:"operator"` + Values []string `yaml:"values,omitempty"` +} + +// IsPresentLabelSelector reports whether s defines any label constraints. +// Empty {} (nil MatchLabels and empty MatchExpressions) is treated as absent. +func IsPresentLabelSelector(s *PolicyLabelSelector) bool { + return s != nil && (len(s.MatchLabels) > 0 || len(s.MatchExpressions) > 0) +} + +// ToMetaV1LabelSelector converts the YAML mirror type to metav1.LabelSelector. +// Conversion itself is infallible; call LabelSelectorAsSelector (or +// SelectorFromPolicyLabelSelector) to validate operators and values. +func ToMetaV1LabelSelector(s *PolicyLabelSelector) *metav1.LabelSelector { + if s == nil { + return nil + } + ls := &metav1.LabelSelector{MatchLabels: s.MatchLabels} + for _, expr := range s.MatchExpressions { + ls.MatchExpressions = append(ls.MatchExpressions, metav1.LabelSelectorRequirement{ + Key: expr.Key, + Operator: metav1.LabelSelectorOperator(expr.Operator), + Values: expr.Values, + }) + } + return ls +} + +// SelectorFromPolicyLabelSelector converts a present policy label selector to a +// runtime labels.Selector. Returns (nil, nil) when s defines no constraints. +func SelectorFromPolicyLabelSelector(s *PolicyLabelSelector) (labels.Selector, error) { + if !IsPresentLabelSelector(s) { + return nil, nil + } + return metav1.LabelSelectorAsSelector(ToMetaV1LabelSelector(s)) +} + +// validatePolicyLabelSelector converts and validates a policy label selector. +func validatePolicyLabelSelector(s *PolicyLabelSelector) error { + _, err := SelectorFromPolicyLabelSelector(s) + return err +} + // ResourceFilter defines a filter for specific resource kinds. type ResourceFilter struct { - Kinds []string `yaml:"kinds"` - LabelSelector map[string]string `yaml:"labelSelector,omitempty"` - OrLabelSelectors []map[string]string `yaml:"orLabelSelectors,omitempty"` - Names []string `yaml:"names,omitempty"` - ExcludedNames []string `yaml:"excludedNames,omitempty"` + Kinds []string `yaml:"kinds"` + LabelSelector *PolicyLabelSelector `yaml:"labelSelector,omitempty"` + OrLabelSelectors []*PolicyLabelSelector `yaml:"orLabelSelectors,omitempty"` + Names []string `yaml:"names,omitempty"` + ExcludedNames []string `yaml:"excludedNames,omitempty"` } // IsCatchAll returns true if the filter is a catch-all entry (empty kinds or ["*"]) @@ -605,9 +659,17 @@ func (p *Policies) validateNamespacedFilterPolicies() error { seenKinds[kind] = j } - if len(rf.LabelSelector) > 0 && len(rf.OrLabelSelectors) > 0 { + if IsPresentLabelSelector(rf.LabelSelector) && len(rf.OrLabelSelectors) > 0 { return fmt.Errorf("namespacedFilterPolicies[%d].resourceFilters[%d]: labelSelector and orLabelSelectors cannot co-exist", i, j) } + if err := validatePolicyLabelSelector(rf.LabelSelector); err != nil { + return fmt.Errorf("namespacedFilterPolicies[%d].resourceFilters[%d]: invalid label selector: %w", i, j, err) + } + for k, ols := range rf.OrLabelSelectors { + if err := validatePolicyLabelSelector(ols); err != nil { + return fmt.Errorf("namespacedFilterPolicies[%d].resourceFilters[%d].orLabelSelectors[%d]: invalid label selector: %w", i, j, k, err) + } + } // Validate glob patterns for names and excludedNames using gobwas/glob for k, pattern := range rf.Names { @@ -657,9 +719,17 @@ func (p *Policies) validateClusterScopedFilterPolicy() error { seenKinds[kind] = j } - if len(rf.LabelSelector) > 0 && len(rf.OrLabelSelectors) > 0 { + if IsPresentLabelSelector(rf.LabelSelector) && len(rf.OrLabelSelectors) > 0 { return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d]: labelSelector and orLabelSelectors cannot co-exist", j) } + if err := validatePolicyLabelSelector(rf.LabelSelector); err != nil { + return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d]: invalid label selector: %w", j, err) + } + for k, ols := range rf.OrLabelSelectors { + if err := validatePolicyLabelSelector(ols); err != nil { + return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d].orLabelSelectors[%d]: invalid label selector: %w", j, k, err) + } + } for k, pattern := range rf.Names { if _, err := glob.Compile(pattern); err != nil { diff --git a/internal/resourcepolicies/resource_policies_test.go b/internal/resourcepolicies/resource_policies_test.go index 445b479f0..7a7da6d3d 100644 --- a/internal/resourcepolicies/resource_policies_test.go +++ b/internal/resourcepolicies/resource_policies_test.go @@ -25,6 +25,7 @@ import ( corev1api "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -2027,7 +2028,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: ["Pod", "ConfigMap"] labelSelector: - app: web + matchLabels: + app: web names: ["app-*"] - kinds: ["Secret"] excludedNames: ["temp-*"]`, @@ -2041,8 +2043,10 @@ namespacedFilterPolicies: resourceFilters: - kinds: ["Pod"] orLabelSelectors: - - env: prod - - env: staging`, + - matchLabels: + env: prod + - matchLabels: + env: staging`, wantErr: false, }, { @@ -2084,7 +2088,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: ["*"] labelSelector: - app: web`, + matchLabels: + app: web`, wantErr: false, }, { @@ -2095,10 +2100,12 @@ namespacedFilterPolicies: resourceFilters: - kinds: ["*"] labelSelector: - app: web + matchLabels: + app: web - kinds: ["*"] labelSelector: - app: db`, + matchLabels: + app: db`, wantErr: true, errMsg: "only one catch-all resource filter is allowed", }, @@ -2110,10 +2117,12 @@ namespacedFilterPolicies: resourceFilters: - kinds: [] labelSelector: - app: web + matchLabels: + app: web - kinds: ["*"] labelSelector: - app: db`, + matchLabels: + app: db`, wantErr: true, errMsg: "only one catch-all resource filter is allowed", }, @@ -2125,10 +2134,12 @@ namespacedFilterPolicies: resourceFilters: - kinds: [] labelSelector: - app: web + matchLabels: + app: web - kinds: [] labelSelector: - app: db`, + matchLabels: + app: db`, wantErr: true, errMsg: "only one catch-all resource filter is allowed", }, @@ -2141,7 +2152,8 @@ namespacedFilterPolicies: - kinds: [] names: ["app-*"] labelSelector: - app: web`, + matchLabels: + app: web`, wantErr: true, errMsg: "names or excludedNames cannot be specified for catch-all filters", }, @@ -2154,7 +2166,8 @@ namespacedFilterPolicies: - kinds: [] excludedNames: ["app-*"] labelSelector: - app: web`, + matchLabels: + app: web`, wantErr: true, errMsg: "names or excludedNames cannot be specified for catch-all filters", }, @@ -2186,9 +2199,11 @@ namespacedFilterPolicies: resourceFilters: - kinds: ["Pod"] labelSelector: - app: web + matchLabels: + app: web orLabelSelectors: - - env: prod`, + - matchLabels: + env: prod`, wantErr: true, errMsg: "labelSelector and orLabelSelectors cannot co-exist", }, @@ -2272,7 +2287,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: ["Pod"] labelSelector: - app: web` + matchLabels: + app: web` resPolicies, err := unmarshalResourcePolicies(&yamlData) require.NoError(t, err) @@ -2290,7 +2306,135 @@ namespacedFilterPolicies: rf := policy.ResourceFilters[0] assert.Equal(t, []string{"Pod"}, rf.Kinds) - assert.Equal(t, map[string]string{"app": "web"}, rf.LabelSelector) + assert.Equal(t, &PolicyLabelSelector{MatchLabels: map[string]string{"app": "web"}}, rf.LabelSelector) +} + +func TestPolicyLabelSelectorSetBased(t *testing.T) { + t.Run("yaml decode matchLabels and matchExpressions", func(t *testing.T) { + yamlData := `version: v1 +namespacedFilterPolicies: +- namespaces: ["ns1"] + resourceFilters: + - kinds: ["Pod"] + labelSelector: + matchLabels: + app: web + matchExpressions: + - key: environment + operator: In + values: [prod, staging] + - key: do-not-backup + operator: DoesNotExist` + + resPolicies, err := unmarshalResourcePolicies(&yamlData) + require.NoError(t, err) + + policies := &Policies{} + require.NoError(t, policies.BuildPolicy(resPolicies)) + require.NoError(t, policies.Validate()) + + rf := policies.GetNamespacedFilterPolicies()[0].ResourceFilters[0] + require.NotNil(t, rf.LabelSelector) + assert.Equal(t, map[string]string{"app": "web"}, rf.LabelSelector.MatchLabels) + require.Len(t, rf.LabelSelector.MatchExpressions, 2) + assert.Equal(t, "environment", rf.LabelSelector.MatchExpressions[0].Key) + assert.Equal(t, "In", rf.LabelSelector.MatchExpressions[0].Operator) + assert.Equal(t, []string{"prod", "staging"}, rf.LabelSelector.MatchExpressions[0].Values) + assert.Equal(t, "do-not-backup", rf.LabelSelector.MatchExpressions[1].Key) + assert.Equal(t, "DoesNotExist", rf.LabelSelector.MatchExpressions[1].Operator) + }) + + t.Run("empty labelSelector is no filter", func(t *testing.T) { + yamlData := `version: v1 +namespacedFilterPolicies: +- namespaces: ["ns1"] + resourceFilters: + - kinds: ["Pod"] + labelSelector: {}` + + resPolicies, err := unmarshalResourcePolicies(&yamlData) + require.NoError(t, err) + + policies := &Policies{} + require.NoError(t, policies.BuildPolicy(resPolicies)) + require.NoError(t, policies.Validate()) + + rf := policies.GetNamespacedFilterPolicies()[0].ResourceFilters[0] + assert.False(t, IsPresentLabelSelector(rf.LabelSelector)) + }) + + t.Run("invalid operator rejected", func(t *testing.T) { + yamlData := `version: v1 +namespacedFilterPolicies: +- namespaces: ["ns1"] + resourceFilters: + - kinds: ["Pod"] + labelSelector: + matchExpressions: + - key: environment + operator: Equals + values: [prod]` + + resPolicies, err := unmarshalResourcePolicies(&yamlData) + require.NoError(t, err) + + policies := &Policies{} + require.NoError(t, policies.BuildPolicy(resPolicies)) + err = policies.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid label selector") + }) + + t.Run("NotIn Exists operators validate", func(t *testing.T) { + yamlData := `version: v1 +clusterScopedFilterPolicy: + resourceFilters: + - kinds: ["ClusterRole"] + labelSelector: + matchExpressions: + - key: tier + operator: NotIn + values: [debug] + - key: managed-by + operator: Exists` + + resPolicies, err := unmarshalResourcePolicies(&yamlData) + require.NoError(t, err) + + policies := &Policies{} + require.NoError(t, policies.BuildPolicy(resPolicies)) + require.NoError(t, policies.Validate()) + }) + + t.Run("ToMetaV1LabelSelector and IsPresentLabelSelector", func(t *testing.T) { + assert.False(t, IsPresentLabelSelector(nil)) + assert.False(t, IsPresentLabelSelector(&PolicyLabelSelector{})) + assert.True(t, IsPresentLabelSelector(&PolicyLabelSelector{MatchLabels: map[string]string{"a": "b"}})) + + ls := ToMetaV1LabelSelector(&PolicyLabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + MatchExpressions: []PolicyLabelSelectorRequirement{ + {Key: "env", Operator: "In", Values: []string{"prod"}}, + }, + }) + require.NotNil(t, ls) + assert.Equal(t, map[string]string{"app": "web"}, ls.MatchLabels) + require.Len(t, ls.MatchExpressions, 1) + assert.Equal(t, metav1.LabelSelectorOpIn, ls.MatchExpressions[0].Operator) + + assert.Nil(t, ToMetaV1LabelSelector(nil)) + + sel, err := SelectorFromPolicyLabelSelector(&PolicyLabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }) + require.NoError(t, err) + require.NotNil(t, sel) + assert.True(t, sel.Matches(labels.Set{"app": "web"})) + + emptySel, err := SelectorFromPolicyLabelSelector(&PolicyLabelSelector{}) + require.NoError(t, err) + assert.Nil(t, emptySel) + }) } func TestClusterScopedFilterPoliciesAccessor(t *testing.T) { @@ -2394,7 +2538,8 @@ clusterScopedFilterPolicy: resourceFilters: - kinds: ["ClusterRole", "ClusterRoleBinding"] labelSelector: - app: my-app`, + matchLabels: + app: my-app`, wantErr: false, }, { @@ -2404,8 +2549,10 @@ clusterScopedFilterPolicy: resourceFilters: - kinds: ["CustomResourceDefinition"] orLabelSelectors: - - app: my-app - - app: other-app`, + - matchLabels: + app: my-app + - matchLabels: + app: other-app`, wantErr: false, }, { @@ -2443,7 +2590,8 @@ clusterScopedFilterPolicy: resourceFilters: - kinds: ["*"] labelSelector: - app: my-app`, + matchLabels: + app: my-app`, wantErr: true, errMsg: "kinds must be specified", }, @@ -2456,7 +2604,8 @@ clusterScopedFilterPolicy: names: ["my-app-*"] - kinds: ["ClusterRole"] labelSelector: - app: other`, + matchLabels: + app: other`, wantErr: true, errMsg: `kind "ClusterRole" appears in both`, }, @@ -2467,9 +2616,11 @@ clusterScopedFilterPolicy: resourceFilters: - kinds: ["ClusterRole"] labelSelector: - app: my-app + matchLabels: + app: my-app orLabelSelectors: - - app: other`, + - matchLabels: + app: other`, wantErr: true, errMsg: "labelSelector and orLabelSelectors cannot co-exist", }, diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index dc60bba8c..30eb26a36 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -1428,22 +1428,20 @@ func resolveClusterScopedFilterPolicy( } func resolveResourceFilter(rf resourcepolicies.ResourceFilter) (*ResolvedResourceFilter, error) { - var selector labels.Selector - if len(rf.LabelSelector) > 0 { - var err error - selector, err = labels.ValidatedSelectorFromSet(labels.Set(rf.LabelSelector)) - if err != nil { - return nil, fmt.Errorf("invalid label selector in resource filter: %w", err) - } + selector, err := resourcepolicies.SelectorFromPolicyLabelSelector(rf.LabelSelector) + if err != nil { + return nil, fmt.Errorf("invalid label selector in resource filter: %w", err) } var orSelectors []labels.Selector for _, ols := range rf.OrLabelSelectors { - s, err := labels.ValidatedSelectorFromSet(labels.Set(ols)) + s, err := resourcepolicies.SelectorFromPolicyLabelSelector(ols) if err != nil { return nil, fmt.Errorf("invalid OR label selector in resource filter: %w", err) } - orSelectors = append(orSelectors, s) + if s != nil { + orSelectors = append(orSelectors, s) + } } var nameIE *collections.IncludesExcludes diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index 56f4aaf33..3baae0131 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -5741,7 +5741,7 @@ func TestResolveResourceFilter(t *testing.T) { { name: "valid label selector", rf: resourcepolicies.ResourceFilter{ - LabelSelector: map[string]string{"app": "foo"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}}, }, expectErr: false, checkResult: func(t *testing.T, r *ResolvedResourceFilter) { @@ -5754,16 +5754,16 @@ func TestResolveResourceFilter(t *testing.T) { { name: "invalid label selector", rf: resourcepolicies.ResourceFilter{ - LabelSelector: map[string]string{"invalid/label/key": "value"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}}, }, expectErr: true, }, { name: "valid or label selectors", rf: resourcepolicies.ResourceFilter{ - OrLabelSelectors: []map[string]string{ - {"app": "foo"}, - {"app": "bar"}, + OrLabelSelectors: []*resourcepolicies.PolicyLabelSelector{ + {MatchLabels: map[string]string{"app": "foo"}}, + {MatchLabels: map[string]string{"app": "bar"}}, }, }, expectErr: false, @@ -5776,8 +5776,8 @@ func TestResolveResourceFilter(t *testing.T) { { name: "invalid or label selectors", rf: resourcepolicies.ResourceFilter{ - OrLabelSelectors: []map[string]string{ - {"invalid/label/key": "value"}, + OrLabelSelectors: []*resourcepolicies.PolicyLabelSelector{ + {MatchLabels: map[string]string{"invalid/label/key": "value"}}, }, }, expectErr: true, @@ -5797,6 +5797,68 @@ func TestResolveResourceFilter(t *testing.T) { assert.False(t, r.NameIE.ShouldInclude("exc1")) }, }, + { + name: "empty labelSelector is no filter", + rf: resourcepolicies.ResourceFilter{ + LabelSelector: &resourcepolicies.PolicyLabelSelector{}, + }, + expectErr: false, + checkResult: func(t *testing.T, r *ResolvedResourceFilter) { + t.Helper() + require.NotNil(t, r) + assert.Nil(t, r.LabelSelector) + }, + }, + { + name: "set-based In and DoesNotExist", + rf: resourcepolicies.ResourceFilter{ + LabelSelector: &resourcepolicies.PolicyLabelSelector{ + MatchExpressions: []resourcepolicies.PolicyLabelSelectorRequirement{ + {Key: "environment", Operator: "In", Values: []string{"prod", "staging"}}, + {Key: "do-not-backup", Operator: "DoesNotExist"}, + }, + }, + }, + expectErr: false, + checkResult: func(t *testing.T, r *ResolvedResourceFilter) { + t.Helper() + require.NotNil(t, r.LabelSelector) + assert.True(t, r.LabelSelector.Matches(labels.Set{"environment": "prod"})) + assert.True(t, r.LabelSelector.Matches(labels.Set{"environment": "staging"})) + assert.False(t, r.LabelSelector.Matches(labels.Set{"environment": "dev"})) + assert.False(t, r.LabelSelector.Matches(labels.Set{"environment": "prod", "do-not-backup": "true"})) + }, + }, + { + name: "set-based NotIn and Exists", + rf: resourcepolicies.ResourceFilter{ + LabelSelector: &resourcepolicies.PolicyLabelSelector{ + MatchExpressions: []resourcepolicies.PolicyLabelSelectorRequirement{ + {Key: "tier", Operator: "NotIn", Values: []string{"debug"}}, + {Key: "app", Operator: "Exists"}, + }, + }, + }, + expectErr: false, + checkResult: func(t *testing.T, r *ResolvedResourceFilter) { + t.Helper() + require.NotNil(t, r.LabelSelector) + assert.True(t, r.LabelSelector.Matches(labels.Set{"app": "web", "tier": "frontend"})) + assert.False(t, r.LabelSelector.Matches(labels.Set{"app": "web", "tier": "debug"})) + assert.False(t, r.LabelSelector.Matches(labels.Set{"tier": "frontend"})) + }, + }, + { + name: "invalid operator", + rf: resourcepolicies.ResourceFilter{ + LabelSelector: &resourcepolicies.PolicyLabelSelector{ + MatchExpressions: []resourcepolicies.PolicyLabelSelectorRequirement{ + {Key: "env", Operator: "Equals", Values: []string{"prod"}}, + }, + }, + }, + expectErr: true, + }, } for _, tc := range tests { @@ -5834,11 +5896,11 @@ func TestResolveClusterScopedFilterPolicy(t *testing.T) { ResourceFilters: []resourcepolicies.ResourceFilter{ { Kinds: []string{"pods", "secrets"}, - LabelSelector: map[string]string{"app": "foo"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}}, }, { Kinds: []string{"invalid-kind"}, - LabelSelector: map[string]string{"invalid/label/key": "value"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}}, }, }, } @@ -5852,7 +5914,7 @@ func TestResolveClusterScopedFilterPolicy(t *testing.T) { ResourceFilters: []resourcepolicies.ResourceFilter{ { Kinds: []string{"pods", "secrets"}, - LabelSelector: map[string]string{"app": "foo"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}}, }, }, } @@ -5900,11 +5962,11 @@ func TestResolveNamespacedFilterPolicies(t *testing.T) { ResourceFilters: []resourcepolicies.ResourceFilter{ { Kinds: []string{"pods"}, - LabelSelector: map[string]string{"app": "foo"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}}, }, { Kinds: []string{"*"}, - LabelSelector: map[string]string{"catch": "all"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"catch": "all"}}, }, }, }, @@ -5932,7 +5994,7 @@ func TestResolveNamespacedFilterPolicies(t *testing.T) { ResourceFilters: []resourcepolicies.ResourceFilter{ { Kinds: []string{"pods"}, - LabelSelector: map[string]string{"invalid/label/key": "value"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}}, }, }, }, @@ -6016,7 +6078,7 @@ func TestBackupWithResPoliciesLogs(t *testing.T) { ResourceFilters: []resourcepolicies.ResourceFilter{ { Kinds: []string{"pods"}, - LabelSelector: map[string]string{"invalid/label/key": "value"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}}, }, }, } @@ -6035,7 +6097,7 @@ func TestBackupWithResPoliciesLogs(t *testing.T) { ResourceFilters: []resourcepolicies.ResourceFilter{ { Kinds: []string{"pods"}, - LabelSelector: map[string]string{"invalid/label/key": "value"}, + LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}}, }, }, }, diff --git a/pkg/cmd/util/output/backup_describer.go b/pkg/cmd/util/output/backup_describer.go index 4c8222f81..445ce3df5 100644 --- a/pkg/cmd/util/output/backup_describer.go +++ b/pkg/cmd/util/output/backup_describer.go @@ -21,7 +21,6 @@ import ( "context" "encoding/json" "fmt" - "io" "sort" "strconv" "strings" @@ -31,7 +30,6 @@ import ( "github.com/cockroachdb/errors" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" - "github.com/sirupsen/logrus" "github.com/fatih/color" kbclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -94,9 +92,6 @@ func DescribeBackup( if backup.Spec.ResourcePolicy != nil { d.Println() DescribeResourcePolicies(d, backup.Spec.ResourcePolicy) - - // Display fine-grained filter policies if they exist - DescribeFineGrainedFilterPolicies(ctx, kbClient, d, backup) } DescribeGlobalVolumePolicy(d, backup) @@ -151,119 +146,6 @@ func DescribeGlobalVolumePolicy(d *Describer, backup *velerov1api.Backup) { d.Printf("\tName:\t%s\n", name) } -// DescribeFineGrainedFilterPolicies describes cluster-scoped and namespace-scoped filter policies if present -func DescribeFineGrainedFilterPolicies(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup) { - if backup.Spec.ResourcePolicy == nil { - return - } - - // Create a discard logger for the resource policies function since this is CLI output context - discardLogger := logrus.New() - discardLogger.Out = io.Discard - - resourcePolicies, err := resourcepolicies.GetResourcePoliciesFromBackup(*backup, kbClient, discardLogger) - if err != nil { - // Don't fail the describe if we can't read policies, just skip - return - } - - if resourcePolicies == nil { - return - } - - clusterScopedFilterPolicy := resourcePolicies.GetClusterScopedFilterPolicy() - if clusterScopedFilterPolicy != nil { - d.Printf("\nCluster Scoped Filter Policy:\n") - d.Printf(" Resource Filters:\n") - for _, rf := range clusterScopedFilterPolicy.ResourceFilters { - kindsStr := strings.Join(rf.Kinds, ", ") - d.Printf(" %s:\n", kindsStr) - - // Label selector - if len(rf.LabelSelector) > 0 { - selectorStr := formatLabelMap(rf.LabelSelector) - d.Printf(" Label selector: %s\n", selectorStr) - } else if len(rf.OrLabelSelectors) > 0 { - var orStrs []string - for _, ols := range rf.OrLabelSelectors { - orStrs = append(orStrs, formatLabelMap(ols)) - } - d.Printf(" OR label selectors: [%s]\n", strings.Join(orStrs, ", ")) - } else { - d.Printf(" Label selector: \n") - } - - // Name patterns - if len(rf.Names) > 0 { - d.Printf(" Included names: [%s]\n", strings.Join(rf.Names, ", ")) - } else { - d.Printf(" Included names: \n") - } - - if len(rf.ExcludedNames) > 0 { - d.Printf(" Excluded names: [%s]\n", strings.Join(rf.ExcludedNames, ", ")) - } else { - d.Printf(" Excluded names: \n") - } - } - } - - nfPolicies := resourcePolicies.GetNamespacedFilterPolicies() - if len(nfPolicies) > 0 { - d.Printf("\nNamespace-Scoped Filter Policies:\n") - for _, policy := range nfPolicies { - for _, ns := range policy.Namespaces { - d.Printf(" %s:\n", ns) - d.Printf(" Resource Filters:\n") - for _, rf := range policy.ResourceFilters { - var kindsStr string - if rf.IsCatchAll() { - kindsStr = " (all other kinds)" - } else { - kindsStr = strings.Join(rf.Kinds, ", ") - } - d.Printf(" %s:\n", kindsStr) - - // Label selector - if len(rf.LabelSelector) > 0 { - selectorStr := formatLabelMap(rf.LabelSelector) - d.Printf(" Label selector: %s\n", selectorStr) - } else if len(rf.OrLabelSelectors) > 0 { - var orStrs []string - for _, ols := range rf.OrLabelSelectors { - orStrs = append(orStrs, formatLabelMap(ols)) - } - d.Printf(" OR label selectors: [%s]\n", strings.Join(orStrs, ", ")) - } else { - d.Printf(" Label selector: \n") - } - - // Name patterns - if len(rf.Names) > 0 { - d.Printf(" Included names: [%s]\n", strings.Join(rf.Names, ", ")) - } else { - d.Printf(" Included names: \n") - } - - if len(rf.ExcludedNames) > 0 { - d.Printf(" Excluded names: [%s]\n", strings.Join(rf.ExcludedNames, ", ")) - } else { - d.Printf(" Excluded names: \n") - } - } - } - } - } -} - -func formatLabelMap(labelMap map[string]string) string { - var pairs []string - for k, v := range labelMap { - pairs = append(pairs, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(pairs, ",") -} - // DescribeUploaderConfigForBackup describes uploader config in human-readable format func DescribeUploaderConfigForBackup(d *Describer, spec velerov1api.BackupSpec) { d.Printf("Uploader config:\n") diff --git a/pkg/cmd/util/output/backup_describer_test.go b/pkg/cmd/util/output/backup_describer_test.go index 248b0a45b..da28f6c87 100644 --- a/pkg/cmd/util/output/backup_describer_test.go +++ b/pkg/cmd/util/output/backup_describer_test.go @@ -18,7 +18,6 @@ package output import ( "bytes" - "context" "testing" "text/tabwriter" "time" @@ -26,8 +25,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "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" @@ -897,85 +894,3 @@ func TestDescribeBackupItemOperation(t *testing.T) { d.out.Flush() assert.Equal(t, expected, d.buf.String()) } - -func TestDescribeFineGrainedFilterPolicies(t *testing.T) { - yamlData := ` -version: v1 -clusterScopedFilterPolicy: - resourceFilters: - - kinds: ["StorageClass"] - labelSelector: {"app": "velero"} - - kinds: ["ClusterRole"] - orLabelSelectors: - - {"app": "velero"} - - {"app": "test"} - names: ["role1"] - excludedNames: ["role2"] -namespacedFilterPolicies: -- namespaces: ["ns1", "ns2"] - resourceFilters: - - kinds: ["Pod", "ConfigMap"] - labelSelector: {"app": "velero"} - - kinds: ["*"] -` - cm := &corev1api.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-policy", - Namespace: "velero", - }, - Data: map[string]string{ - "policy.yaml": yamlData, - }, - } - - client := fake.NewClientBuilder().WithRuntimeObjects(cm).Build() - - backup := builder.ForBackup("velero", "test-backup"). - ResourcePolicies("test-policy").Result() - - d := &Describer{ - Prefix: "", - out: &tabwriter.Writer{}, - buf: &bytes.Buffer{}, - } - d.out.Init(d.buf, 0, 8, 2, ' ', 0) - - DescribeFineGrainedFilterPolicies(context.Background(), client, d, backup) - d.out.Flush() - - expected := ` -Cluster Scoped Filter Policy: - Resource Filters: - StorageClass: - Label selector: app=velero - Included names: - Excluded names: - ClusterRole: - OR label selectors: [app=velero, app=test] - Included names: [role1] - Excluded names: [role2] - -Namespace-Scoped Filter Policies: - ns1: - Resource Filters: - Pod, ConfigMap: - Label selector: app=velero - Included names: - Excluded names: - (all other kinds): - Label selector: - Included names: - Excluded names: - ns2: - Resource Filters: - Pod, ConfigMap: - Label selector: app=velero - Included names: - Excluded names: - (all other kinds): - Label selector: - Included names: - Excluded names: -` - assert.Equal(t, expected, d.buf.String()) -} diff --git a/pkg/cmd/util/output/backup_structured_describer.go b/pkg/cmd/util/output/backup_structured_describer.go index dfffcda06..b2541df4b 100644 --- a/pkg/cmd/util/output/backup_structured_describer.go +++ b/pkg/cmd/util/output/backup_structured_describer.go @@ -21,10 +21,8 @@ import ( "context" "encoding/json" "fmt" - "io" "strings" - "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -57,7 +55,6 @@ func DescribeBackupInSF( if backup.Spec.ResourcePolicy != nil { DescribeResourcePoliciesInSF(d, backup.Spec.ResourcePolicy) - DescribeFineGrainedFilterPoliciesInSF(ctx, kbClient, d, backup) } DescribeGlobalVolumePolicyInSF(d, backup) @@ -228,88 +225,6 @@ func DescribeBackupSpecInSF(d *StructuredDescriber, spec velerov1api.BackupSpec) d.Describe("spec", backupSpecInfo) } -// DescribeFineGrainedFilterPoliciesInSF adds the clusterScopedFilterPolicy -// and namespacedFilterPolicies sections to the structured describer output when present -// in the ResourcePolicy ConfigMap referenced by the backup. -func DescribeFineGrainedFilterPoliciesInSF(ctx context.Context, kbClient kbclient.Client, d *StructuredDescriber, backup *velerov1api.Backup) { - if backup.Spec.ResourcePolicy == nil { - return - } - - discardLogger := logrus.New() - discardLogger.Out = io.Discard - - resPolicies, err := resourcepolicies.GetResourcePoliciesFromBackup(*backup, kbClient, discardLogger) - if err != nil || resPolicies == nil { - return - } - - clusterScopedFilterPolicy := resPolicies.GetClusterScopedFilterPolicy() - if clusterScopedFilterPolicy != nil { - var clusterScopedFilters []map[string]any - for _, rf := range clusterScopedFilterPolicy.ResourceFilters { - entry := map[string]any{ - "kinds": rf.Kinds, - } - if len(rf.LabelSelector) > 0 { - entry["labelSelector"] = rf.LabelSelector - } - if len(rf.OrLabelSelectors) > 0 { - entry["orLabelSelectors"] = rf.OrLabelSelectors - } - if len(rf.Names) > 0 { - entry["names"] = rf.Names - } - if len(rf.ExcludedNames) > 0 { - entry["excludedNames"] = rf.ExcludedNames - } - clusterScopedFilters = append(clusterScopedFilters, entry) - } - d.Describe("clusterScopedFilterPolicy", map[string]any{ - "resourceFilters": clusterScopedFilters, - }) - } - - nfPolicies := resPolicies.GetNamespacedFilterPolicies() - if len(nfPolicies) == 0 { - return - } - - var structuredPolicies []map[string]any - for _, policy := range nfPolicies { - for _, ns := range policy.Namespaces { - var rfEntries []map[string]any - for _, rf := range policy.ResourceFilters { - entry := map[string]any{} - if rf.IsCatchAll() { - entry["kinds"] = []string{} - entry["isCatchAll"] = true - } else { - entry["kinds"] = rf.Kinds - } - if len(rf.LabelSelector) > 0 { - entry["labelSelector"] = rf.LabelSelector - } - if len(rf.OrLabelSelectors) > 0 { - entry["orLabelSelectors"] = rf.OrLabelSelectors - } - if len(rf.Names) > 0 { - entry["names"] = rf.Names - } - if len(rf.ExcludedNames) > 0 { - entry["excludedNames"] = rf.ExcludedNames - } - rfEntries = append(rfEntries, entry) - } - structuredPolicies = append(structuredPolicies, map[string]any{ - "namespace": ns, - "resourceFilters": rfEntries, - }) - } - } - d.Describe("namespacedFilterPolicies", structuredPolicies) -} - // DescribeBackupStatusInSF describes a backup status in structured format. func DescribeBackupStatusInSF(ctx context.Context, kbClient kbclient.Client, d *StructuredDescriber, backup *velerov1api.Backup, details bool, insecureSkipTLSVerify bool, caCertPath string, podVolumeBackups []velerov1api.PodVolumeBackup) { diff --git a/pkg/cmd/util/output/backup_structured_describer_test.go b/pkg/cmd/util/output/backup_structured_describer_test.go index cb46a4676..88af0f95f 100644 --- a/pkg/cmd/util/output/backup_structured_describer_test.go +++ b/pkg/cmd/util/output/backup_structured_describer_test.go @@ -17,7 +17,6 @@ limitations under the License. package output import ( - "context" "reflect" "testing" "time" @@ -25,8 +24,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "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" @@ -731,96 +728,3 @@ func TestDescribeDeleteBackupRequestsInSF(t *testing.T) { }) } } - -func TestDescribeFineGrainedFilterPoliciesInSF(t *testing.T) { - yamlData := ` -version: v1 -clusterScopedFilterPolicy: - resourceFilters: - - kinds: ["StorageClass"] - labelSelector: {"app": "velero"} - - kinds: ["ClusterRole"] - orLabelSelectors: - - {"app": "velero"} - - {"app": "test"} - names: ["role1"] - excludedNames: ["role2"] -namespacedFilterPolicies: -- namespaces: ["ns1", "ns2"] - resourceFilters: - - kinds: ["Pod", "ConfigMap"] - labelSelector: {"app": "velero"} - - kinds: ["*"] -` - cm := &corev1api.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-policy", - Namespace: "velero", - }, - Data: map[string]string{ - "policy.yaml": yamlData, - }, - } - - client := fake.NewClientBuilder().WithRuntimeObjects(cm).Build() - - backup := builder.ForBackup("velero", "test-backup"). - ResourcePolicies("test-policy").Result() - - sd := &StructuredDescriber{ - output: make(map[string]any), - format: "", - } - - DescribeFineGrainedFilterPoliciesInSF(context.Background(), client, sd, backup) - - expect := map[string]any{ - "clusterScopedFilterPolicy": map[string]any{ - "resourceFilters": []map[string]any{ - { - "kinds": []string{"StorageClass"}, - "labelSelector": map[string]string{"app": "velero"}, - }, - { - "kinds": []string{"ClusterRole"}, - "orLabelSelectors": []map[string]string{ - {"app": "velero"}, - {"app": "test"}, - }, - "names": []string{"role1"}, - "excludedNames": []string{"role2"}, - }, - }, - }, - "namespacedFilterPolicies": []map[string]any{ - { - "namespace": "ns1", - "resourceFilters": []map[string]any{ - { - "kinds": []string{"Pod", "ConfigMap"}, - "labelSelector": map[string]string{"app": "velero"}, - }, - { - "kinds": []string{}, - "isCatchAll": true, - }, - }, - }, - { - "namespace": "ns2", - "resourceFilters": []map[string]any{ - { - "kinds": []string{"Pod", "ConfigMap"}, - "labelSelector": map[string]string{"app": "velero"}, - }, - { - "kinds": []string{}, - "isCatchAll": true, - }, - }, - }, - }, - } - - assert.True(t, reflect.DeepEqual(sd.output, expect)) -} diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 8205accb9..bc452b49c 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -638,21 +638,19 @@ func resolveRestoreNamespacedFilterPolicies( func resolveResourceFilter( rf resourcepolicies.ResourceFilter, ) (*resolvedResourceFilter, error) { - var selector labels.Selector - if len(rf.LabelSelector) > 0 { - var err error - selector, err = labels.ValidatedSelectorFromSet(labels.Set(rf.LabelSelector)) - if err != nil { - return nil, fmt.Errorf("invalid label selector in resource filter: %w", err) - } + selector, err := resourcepolicies.SelectorFromPolicyLabelSelector(rf.LabelSelector) + if err != nil { + return nil, fmt.Errorf("invalid label selector in resource filter: %w", err) } var orSelectors []labels.Selector for _, ols := range rf.OrLabelSelectors { - s, err := labels.ValidatedSelectorFromSet(labels.Set(ols)) + s, err := resourcepolicies.SelectorFromPolicyLabelSelector(ols) if err != nil { return nil, fmt.Errorf("invalid OR label selector in resource filter: %w", err) } - orSelectors = append(orSelectors, s) + if s != nil { + orSelectors = append(orSelectors, s) + } } var nameIE *collections.IncludesExcludes if len(rf.Names) > 0 || len(rf.ExcludedNames) > 0 { diff --git a/pkg/restore/restore_policies_test.go b/pkg/restore/restore_policies_test.go index a027f66aa..569d8923d 100644 --- a/pkg/restore/restore_policies_test.go +++ b/pkg/restore/restore_policies_test.go @@ -170,7 +170,8 @@ namespacedFilterPolicies: - kinds: - '*' labelSelector: - app: test + matchLabels: + app: test `, tarball: test.NewTarWriter(t). AddItems("pods", diff --git a/site/content/docs/main/fine-grained-backup-filters.md b/site/content/docs/main/fine-grained-backup-filters.md index d9f90debd..d49cf6c93 100644 --- a/site/content/docs/main/fine-grained-backup-filters.md +++ b/site/content/docs/main/fine-grained-backup-filters.md @@ -67,7 +67,8 @@ data: resourceFilters: - kinds: [ConfigMap] labelSelector: - app: my-app + matchLabels: + app: my-app ``` **Backup:** @@ -158,7 +159,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret, Deployment, Pod] labelSelector: - app: my-app + matchLabels: + app: my-app ``` **Backup:** @@ -197,7 +199,8 @@ namespacedFilterPolicies: - kinds: [ConfigMap] names: [vm-1, vm-2] labelSelector: - resource-type: VirtualMachine + matchLabels: + resource-type: VirtualMachine ``` **Backup:** `includedNamespaces: [target-namespace]` plus `resourcePolicy` reference. @@ -250,15 +253,62 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap] orLabelSelectors: - - app: production-workload-1 - component: vm-group - - app: production-workload-2 - component: vm-service + - matchLabels: + app: production-workload-1 + component: vm-group + - matchLabels: + app: production-workload-2 + component: vm-service ``` **Expected outcome:** ConfigMaps matching either label combination are backed up; other ConfigMaps in the namespace are not (for this kind). -**Note:** Use `orLabelSelectors` when you need OR across label sets. `labelSelector` and `orLabelSelectors` cannot appear in the same `resourceFilters` entry. +**Note:** Prefer `matchExpressions` with `In` for value-OR on a single key (see next example). Use `orLabelSelectors` when you need OR across **independent multi-key groups**. `labelSelector` and `orLabelSelectors` cannot appear in the same `resourceFilters` entry. + +--- + +### Example 4b — Set-based label selectors (`matchExpressions`) + +**Goal:** Back up Deployments and Pods that are in `prod` or `staging`, belong to `app=my-app`, and do **not** carry a skip label. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - production + resourceFilters: + - kinds: [Deployment, Pod] + labelSelector: + matchLabels: + app: my-app + matchExpressions: + - key: environment + operator: In + values: [prod, staging] + - key: do-not-backup + operator: DoesNotExist +``` + +**Supported operators:** `In`, `NotIn`, `Exists`, `DoesNotExist` (same as Kubernetes / Velero global `--selector`). + +**Other useful patterns:** + +```yaml +# Exclude environments +matchExpressions: + - key: environment + operator: NotIn + values: [dev, test] + +# Require a label key to be present (any value) +matchExpressions: + - key: tier + operator: Exists +``` + +**Expected outcome:** Only Deployments/Pods with `app=my-app`, `environment` in `{prod, staging}`, and without `do-not-backup` are backed up. --- @@ -276,16 +326,21 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret] orLabelSelectors: - - app: my-app - - app: monitoring + - matchLabels: + app: my-app + - matchLabels: + app: monitoring - kinds: [Deployment] orLabelSelectors: - - app: my-app - - app: monitoring - - component: backend + - matchLabels: + app: my-app + - matchLabels: + app: monitoring + - matchLabels: + component: backend ``` -**Expected outcome:** Resources included if they match **any** map in `orLabelSelectors` for their kind (AND within each map, OR across maps). +**Expected outcome:** Resources included if they match **any** selector in `orLabelSelectors` for their kind (AND within each selector, OR across the list). --- @@ -304,9 +359,12 @@ namespacedFilterPolicies: - kinds: [ConfigMap] names: [vm-1, vm-2] orLabelSelectors: - - resource-type: VirtualMachine - - component: vm-group - - component: vm-service + - matchLabels: + resource-type: VirtualMachine + - matchLabels: + component: vm-group + - matchLabels: + component: vm-service ``` **Expected outcome:** Only `vm-1` and `vm-2` that also satisfy one of the label OR branches. @@ -330,7 +388,8 @@ namespacedFilterPolicies: - kinds: [ConfigMap] - kinds: [Deployment] labelSelector: - tier: web + matchLabels: + tier: web ``` **Expected outcome:** @@ -393,10 +452,12 @@ namespacedFilterPolicies: resourceFilters: - kinds: ["*"] # catch-all labelSelector: - app: common-app + matchLabels: + app: common-app - kinds: [ConfigMap, Secret] # override for these kinds labelSelector: - app: specialized-app + matchLabels: + app: specialized-app ``` **Equivalent:** `kinds: []` (empty) also denotes a catch-all; `kinds: ["*"]` is preferred for readability. @@ -429,7 +490,8 @@ namespacedFilterPolicies: names: [db-credentials, tls-cert] - kinds: ["*"] labelSelector: - backup: "true" + matchLabels: + backup: "true" ``` **Expected outcome:** @@ -482,7 +544,8 @@ clusterScopedFilterPolicy: names: ["my-app-*"] - kinds: [ClusterRole, ClusterRoleBinding] labelSelector: - app: my-app + matchLabels: + app: my-app ``` **Backup (required):** You must still include cluster-scoped kinds on the Backup: @@ -535,7 +598,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret] labelSelector: - app: my-app + matchLabels: + app: my-app - namespaces: - production resourceFilters: @@ -561,7 +625,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret, Deployment] labelSelector: - app: my-app + matchLabels: + app: my-app ``` **Result:** No Secrets in the backup — the namespace policy cannot re-include a globally excluded kind. Velero logs a warning at backup start if you list an excluded kind in `namespacedFilterPolicies`. @@ -598,7 +663,8 @@ namespacedFilterPolicies: excludedNames: ["*-tmp-*", "*-debug-*", "*-tmp", "*-debug"] - kinds: [Secret] labelSelector: - workload: application + matchLabels: + workload: application ``` **Expected outcome:** Volume actions apply to PVCs per `volumePolicies`; resource inclusion follows `namespacedFilterPolicies`. The sections are independent. @@ -619,10 +685,12 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret] labelSelector: - app: my-app + matchLabels: + app: my-app - kinds: ["*"] labelSelector: - app: my-app + matchLabels: + app: my-app ``` **On resources to exclude**, set: @@ -644,8 +712,8 @@ metadata: | Field | Description | |-------|-------------| | `kinds` | Resource type names (e.g. `ConfigMap`, `deployments`). Empty or `["*"]` = catch-all (namespace policies only). | -| `labelSelector` | Equality labels (`key: value`), AND across keys. No `in`, `exists`, etc. — use `orLabelSelectors` for OR. | -| `orLabelSelectors` | List of label maps; match if **any** map matches (AND within each map). Mutually exclusive with `labelSelector`. | +| `labelSelector` | Kubernetes-style selector with `matchLabels` and/or `matchExpressions` (`In`, `NotIn`, `Exists`, `DoesNotExist`). All requirements are AND-ed. | +| `orLabelSelectors` | List of selectors; match if **any** entry matches (AND within each, OR across the list). Use for OR of multi-key groups; prefer `In` for value-OR on one key. Mutually exclusive with `labelSelector`. | | `names` | Exact names or glob patterns to include. | | `excludedNames` | Patterns to exclude; wins over `names` when both match. | @@ -761,6 +829,7 @@ Velero validates the ResourcePolicy when a backup starts. Common errors: | `only one catch-all resource filter is allowed` | Multiple catch-alls in one policy entry | | `kind "X" appears in both resourceFilters[...]` | Same kind in two entries | | `labelSelector and orLabelSelectors cannot co-exist` | Both set in one entry | +| `invalid label selector` | Bad operator, values, or label key/value syntax | | `duplicate namespace pattern` | Same namespace string in two policy entries | | `invalid glob pattern` | Bad characters in namespace or name pattern | | `clusterScopedFilterPolicy... kinds must be specified (catch-all is not supported)` | Empty or `["*"]` kinds in cluster policy | From 5fa1cc3bf5d12c6634bc66d347112c67f98b77e4 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 23 Jul 2026 12:08:39 -0700 Subject: [PATCH 090/194] Add SnapshotClassParameter constant and GetSnapshotClass getter Add a new snapshotClass action parameter to volume policies, allowing users to specify which VolumeSnapshotClass to use for CSI snapshots. This follows the existing dataMover parameter pattern with a typed constant and getter method on the Action struct. Ref: #8807 Signed-off-by: Shubham Pampattiwar --- .../resourcepolicies/resource_policies.go | 29 +++++++++- .../resource_policies_test.go | 57 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/internal/resourcepolicies/resource_policies.go b/internal/resourcepolicies/resource_policies.go index 39504d6ff..22830356a 100644 --- a/internal/resourcepolicies/resource_policies.go +++ b/internal/resourcepolicies/resource_policies.go @@ -54,6 +54,10 @@ const ( // DataMoverParameter is the key of the action parameter that selects the data // mover to be used for the matched volumes when the action type is snapshot. DataMoverParameter = "dataMover" + + // SnapshotClassParameter is the key of the action parameter that selects the + // VolumeSnapshotClass to use for CSI snapshots when the action type is snapshot. + SnapshotClassParameter = "snapshotClass" ) // validDataMovers is the set of data mover values accepted in the snapshot @@ -101,6 +105,30 @@ func (a *Action) GetDataMover() (string, error) { return dataMover, nil } +// GetSnapshotClass returns the VolumeSnapshotClass name configured in the +// snapshot action's snapshotClass parameter. The snapshotClass parameter is +// only meaningful for the snapshot action, so it returns an error when the +// action is nil or its type is not snapshot. When the parameter is absent, +// it returns an empty string, meaning the caller should fall back to the +// existing VolumeSnapshotClass selection logic. +func (a *Action) GetSnapshotClass() (string, error) { + if a == nil || a.Type != Snapshot { + return "", fmt.Errorf("the %q parameter is only supported for the %q action", SnapshotClassParameter, Snapshot) + } + if len(a.Parameters) == 0 { + return "", nil + } + raw, ok := a.Parameters[SnapshotClassParameter] + if !ok { + return "", nil + } + snapshotClass, ok := raw.(string) + if !ok { + return "", fmt.Errorf("parameter %q must be a string, got %T", SnapshotClassParameter, raw) + } + return snapshotClass, nil +} + // PolicyLabelSelector mirrors metav1.LabelSelector with yaml tags for ConfigMap decode. // metav1.LabelSelector only has json tags, which do not populate under go.yaml.in/yaml/v3. type PolicyLabelSelector struct { @@ -153,7 +181,6 @@ func validatePolicyLabelSelector(s *PolicyLabelSelector) error { _, err := SelectorFromPolicyLabelSelector(s) return err } - // ResourceFilter defines a filter for specific resource kinds. type ResourceFilter struct { Kinds []string `yaml:"kinds"` diff --git a/internal/resourcepolicies/resource_policies_test.go b/internal/resourcepolicies/resource_policies_test.go index 7a7da6d3d..1c9e4635f 100644 --- a/internal/resourcepolicies/resource_policies_test.go +++ b/internal/resourcepolicies/resource_policies_test.go @@ -3063,3 +3063,60 @@ func TestActionGetDataMover(t *testing.T) { }) } } + +func TestActionGetSnapshotClass(t *testing.T) { + testCases := []struct { + name string + action *Action + expectedClass string + expectErr bool + }{ + { + name: "nil action", + action: nil, + expectErr: true, + }, + { + name: "snapshot action without parameters", + action: &Action{Type: Snapshot}, + expectedClass: "", + }, + { + name: "snapshot action without snapshotClass parameter", + action: &Action{Type: Snapshot, Parameters: map[string]any{"other": "value"}}, + expectedClass: "", + }, + { + name: "snapshot action with snapshotClass", + action: &Action{Type: Snapshot, Parameters: map[string]any{"snapshotClass": "my-vsc"}}, + expectedClass: "my-vsc", + }, + { + name: "non-snapshot action returns error", + action: &Action{Type: FSBackup, Parameters: map[string]any{"snapshotClass": "my-vsc"}}, + expectErr: true, + }, + { + name: "snapshot action with non-string snapshotClass returns error", + action: &Action{Type: Snapshot, Parameters: map[string]any{"snapshotClass": 123}}, + expectErr: true, + }, + { + name: "snapshot action with both snapshotClass and dataMover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"snapshotClass": "my-vsc", "dataMover": "velero-fs"}}, + expectedClass: "my-vsc", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + snapshotClass, err := tc.action.GetSnapshotClass() + if tc.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectedClass, snapshotClass) + }) + } +} From 436c82b977738964e3af85451095d2aea2105284 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 23 Jul 2026 12:08:53 -0700 Subject: [PATCH 091/194] Add snapshotClass parameter validation Validate the snapshotClass parameter in Action.validate(): it must only appear on snapshot actions, must be a string, and must not be empty. Follows the same validation pattern as the dataMover parameter. Ref: #8807 Signed-off-by: Shubham Pampattiwar --- .../volume_resources_validator.go | 14 ++++ .../volume_resources_validator_test.go | 80 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/internal/resourcepolicies/volume_resources_validator.go b/internal/resourcepolicies/volume_resources_validator.go index 332f98d2e..e1e55182a 100644 --- a/internal/resourcepolicies/volume_resources_validator.go +++ b/internal/resourcepolicies/volume_resources_validator.go @@ -118,5 +118,19 @@ func (a *Action) validate() error { } } + if raw, ok := a.Parameters[SnapshotClassParameter]; ok { + if a.Type != Snapshot { + return fmt.Errorf("parameter %q is only supported for the %q action, but the action type is %q", + SnapshotClassParameter, Snapshot, a.Type) + } + snapshotClass, ok := raw.(string) + if !ok { + return fmt.Errorf("parameter %q must be a string, got %T", SnapshotClassParameter, raw) + } + if snapshotClass == "" { + return fmt.Errorf("parameter %q must not be empty", SnapshotClassParameter) + } + } + return nil } diff --git a/internal/resourcepolicies/volume_resources_validator_test.go b/internal/resourcepolicies/volume_resources_validator_test.go index 489e9c653..6f55f8832 100644 --- a/internal/resourcepolicies/volume_resources_validator_test.go +++ b/internal/resourcepolicies/volume_resources_validator_test.go @@ -658,6 +658,86 @@ func TestValidate(t *testing.T) { }, wantErr: false, }, + { + name: "snapshot action with valid snapshotClass", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"snapshotClass": "my-vsc"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: false, + }, + { + name: "snapshot action with both snapshotClass and dataMover", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"snapshotClass": "my-vsc", "dataMover": "velero-fs"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: false, + }, + { + name: "snapshotClass parameter on non-snapshot action is rejected", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: FSBackup, + Parameters: map[string]any{"snapshotClass": "my-vsc"}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: true, + }, + { + name: "snapshot action with non-string snapshotClass is rejected", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"snapshotClass": 123}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: true, + }, + { + name: "snapshot action with empty snapshotClass is rejected", + res: &ResourcePolicies{ + Version: "v1", + VolumePolicies: []VolumePolicy{ + { + Action: Action{ + Type: Snapshot, + Parameters: map[string]any{"snapshotClass": ""}, + }, + Conditions: map[string]any{"storageClass": []string{"gp2"}}, + }, + }, + }, + wantErr: true, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { From 9eaefe79088baa716c095bda099e25ecf038795b Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 23 Jul 2026 12:09:08 -0700 Subject: [PATCH 092/194] Add volume policy tier to VolumeSnapshotClass selection Add GetVolumeSnapshotClassFromVolumePolicy helper and extend GetVolumeSnapshotClass with a policySnapshotClass parameter. The new tier sits between PVC annotation and backup annotation in the priority chain: PVC annotation > volume policy > backup annotation > VSC label. Ref: #8807 Signed-off-by: Shubham Pampattiwar --- pkg/util/csi/volume_snapshot.go | 39 ++++++++++++ pkg/util/csi/volume_snapshot_test.go | 89 +++++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/pkg/util/csi/volume_snapshot.go b/pkg/util/csi/volume_snapshot.go index b78455bc8..e8fe9bead 100644 --- a/pkg/util/csi/volume_snapshot.go +++ b/pkg/util/csi/volume_snapshot.go @@ -314,6 +314,7 @@ func GetVolumeSnapshotClass( pvc *corev1api.PersistentVolumeClaim, log logrus.FieldLogger, crClient crclient.Client, + policySnapshotClass string, ) (*snapshotv1api.VolumeSnapshotClass, error) { snapshotClasses := new(snapshotv1api.VolumeSnapshotClassList) err := crClient.List(context.TODO(), snapshotClasses) @@ -331,6 +332,16 @@ func GetVolumeSnapshotClass( return snapshotClass, nil } + // If a snapshot class is specified by volume policy, use that + snapshotClass, err = GetVolumeSnapshotClassFromVolumePolicy( + policySnapshotClass, provisioner, snapshotClasses) + if err != nil { + log.Debugf("Didn't find VolumeSnapshotClass from volume policy: %v", err) + } + if snapshotClass != nil { + return snapshotClass, nil + } + // If there is no annotation in PVC, attempt to fetch it from backup annotations snapshotClass, err = GetVolumeSnapshotClassFromBackupAnnotationsForDriver( backup, provisioner, snapshotClasses) @@ -412,6 +423,34 @@ func GetVolumeSnapshotClassFromBackupAnnotationsForDriver( ) } +// GetVolumeSnapshotClassFromVolumePolicy returns a VolumeSnapshotClass +// specified by a volume policy's snapshotClass parameter. If +// policySnapshotClass is empty, it returns nil (no match). +func GetVolumeSnapshotClassFromVolumePolicy( + policySnapshotClass string, + provisioner string, + snapshotClasses *snapshotv1api.VolumeSnapshotClassList, +) (*snapshotv1api.VolumeSnapshotClass, error) { + if policySnapshotClass == "" { + return nil, nil + } + for _, sc := range snapshotClasses.Items { + if strings.EqualFold(policySnapshotClass, sc.ObjectMeta.Name) { + if !strings.EqualFold(sc.Driver, provisioner) { + return nil, errors.Errorf( + "VolumeSnapshotClass %s specified by volume policy is not for driver %s", + sc.ObjectMeta.Name, provisioner, + ) + } + return &sc, nil + } + } + return nil, errors.Errorf( + "No CSI VolumeSnapshotClass found with name %s specified by volume policy for driver %s", + policySnapshotClass, provisioner, + ) +} + // GetVolumeSnapshotClassForStorageClass returns a VolumeSnapshotClass // for the supplied volume provisioner/ driver name. func GetVolumeSnapshotClassForStorageClass( diff --git a/pkg/util/csi/volume_snapshot_test.go b/pkg/util/csi/volume_snapshot_test.go index 67a07d135..335cff6ee 100644 --- a/pkg/util/csi/volume_snapshot_test.go +++ b/pkg/util/csi/volume_snapshot_test.go @@ -1032,7 +1032,7 @@ func TestGetVolumeSnapshotClass(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualSnapshotClass, actualError := GetVolumeSnapshotClass( - tc.driverName, tc.backup, tc.pvc, logrus.New(), fakeClient) + tc.driverName, tc.backup, tc.pvc, logrus.New(), fakeClient, "") if tc.expectError { require.Error(t, actualError) assert.Nil(t, actualSnapshotClass) @@ -1043,6 +1043,93 @@ func TestGetVolumeSnapshotClass(t *testing.T) { } } +func TestGetVolumeSnapshotClassFromVolumePolicy(t *testing.T) { + vscArray1 := &snapshotv1api.VolumeSnapshotClass{ + ObjectMeta: metav1.ObjectMeta{Name: "vsc-array-1"}, + Driver: "infinibox-csi-driver", + } + vscArray2 := &snapshotv1api.VolumeSnapshotClass{ + ObjectMeta: metav1.ObjectMeta{Name: "vsc-array-2"}, + Driver: "infinibox-csi-driver", + } + vscOther := &snapshotv1api.VolumeSnapshotClass{ + ObjectMeta: metav1.ObjectMeta{Name: "vsc-other"}, + Driver: "other-csi-driver", + } + + snapshotClasses := &snapshotv1api.VolumeSnapshotClassList{ + Items: []snapshotv1api.VolumeSnapshotClass{*vscArray1, *vscArray2, *vscOther}, + } + + testCases := []struct { + name string + policySnapshotClass string + provisioner string + expectedVSC *snapshotv1api.VolumeSnapshotClass + expectError bool + }{ + { + name: "empty policy returns nil", + policySnapshotClass: "", + provisioner: "infinibox-csi-driver", + expectedVSC: nil, + expectError: false, + }, + { + name: "matching VSC with correct driver", + policySnapshotClass: "vsc-array-1", + provisioner: "infinibox-csi-driver", + expectedVSC: vscArray1, + expectError: false, + }, + { + name: "matching VSC with correct driver second array", + policySnapshotClass: "vsc-array-2", + provisioner: "infinibox-csi-driver", + expectedVSC: vscArray2, + expectError: false, + }, + { + name: "VSC exists but wrong driver", + policySnapshotClass: "vsc-other", + provisioner: "infinibox-csi-driver", + expectError: true, + }, + { + name: "VSC does not exist", + policySnapshotClass: "non-existent", + provisioner: "infinibox-csi-driver", + expectError: true, + }, + { + name: "case-insensitive name matching", + policySnapshotClass: "VSC-ARRAY-1", + provisioner: "infinibox-csi-driver", + expectedVSC: vscArray1, + expectError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualVSC, actualError := GetVolumeSnapshotClassFromVolumePolicy( + tc.policySnapshotClass, tc.provisioner, snapshotClasses) + if tc.expectError { + require.Error(t, actualError) + assert.Nil(t, actualVSC) + return + } + if tc.expectedVSC == nil { + assert.Nil(t, actualVSC) + } else { + require.NotNil(t, actualVSC) + assert.Equal(t, tc.expectedVSC.Name, actualVSC.Name) + assert.Equal(t, tc.expectedVSC.Driver, actualVSC.Driver) + } + }) + } +} + func TestGetVolumeSnapshotClassForStorageClass(t *testing.T) { hostpathClass := &snapshotv1api.VolumeSnapshotClass{ ObjectMeta: metav1.ObjectMeta{ From 1e8555f14294f00f5896c21cd06c316bad5023aa Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 23 Jul 2026 12:09:21 -0700 Subject: [PATCH 093/194] Wire snapshotClass from volume policy through CSI plugin In pvcBackupItemAction.Execute, call GetActionParameters to extract the snapshotClass from the matched volume policy and pass it through getVolumeSnapshotReference and createVolumeSnapshot to GetVolumeSnapshotClass. This connects the volume policy parameter to the CSI snapshot creation path. Fixes #8807 Signed-off-by: Shubham Pampattiwar --- pkg/backup/actions/csi/pvc_action.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index 66c14b820..06c65075d 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -43,6 +43,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/internal/resourcepolicies" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" veleroclient "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/kuberesource" @@ -211,6 +212,7 @@ func (p *pvcBackupItemAction) validatePVCAndPV( func (p *pvcBackupItemAction) createVolumeSnapshot( pvc corev1api.PersistentVolumeClaim, backup *velerov1api.Backup, + policySnapshotClass string, ) ( vs *snapshotv1api.VolumeSnapshot, err error, @@ -231,6 +233,7 @@ func (p *pvcBackupItemAction) createVolumeSnapshot( &pvc, p.log, p.crClient, + policySnapshotClass, ) if err != nil { return nil, errors.Wrapf( @@ -337,7 +340,20 @@ func (p *pvcBackupItemAction) Execute( return nil, nil, "", nil, err } - vs, err := p.getVolumeSnapshotReference(context.TODO(), pvc, backup) + policySnapshotClass := "" + matched, actionType, params, paramsErr := vh.GetActionParameters(item, kuberesource.PersistentVolumeClaims) + if paramsErr != nil { + p.log.WithError(paramsErr).Warn("failed to get action parameters from volume policy, proceeding without policy snapshotClass") + } else if matched && actionType == string(resourcepolicies.Snapshot) && params != nil { + if sc, ok := params[resourcepolicies.SnapshotClassParameter]; ok { + if scStr, ok := sc.(string); ok && scStr != "" { + policySnapshotClass = scStr + p.log.Infof("Volume policy specifies snapshotClass=%s for PVC %s/%s", scStr, pvc.Namespace, pvc.Name) + } + } + } + + vs, err := p.getVolumeSnapshotReference(context.TODO(), pvc, backup, policySnapshotClass) if err != nil { return nil, nil, "", nil, err } @@ -670,6 +686,7 @@ func (p *pvcBackupItemAction) getVolumeSnapshotReference( ctx context.Context, pvc corev1api.PersistentVolumeClaim, backup *velerov1api.Backup, + policySnapshotClass string, ) (*snapshotv1api.VolumeSnapshot, error) { vgsLabelKey := backup.Spec.VolumeGroupSnapshotLabelKey group, hasLabel := pvc.Labels[vgsLabelKey] @@ -800,7 +817,7 @@ func (p *pvcBackupItemAction) getVolumeSnapshotReference( } // Legacy fallback: create individual VS - return p.createVolumeSnapshot(pvc, backup) + return p.createVolumeSnapshot(pvc, backup, policySnapshotClass) } func (p *pvcBackupItemAction) findExistingVSForBackup( From 7582f899fe7318bd3dd3864ea2e3e2b4024ea492 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 23 Jul 2026 12:10:23 -0700 Subject: [PATCH 094/194] Add changelog for PR #10070 Signed-off-by: Shubham Pampattiwar --- changelogs/unreleased/10070-shubham-pampattiwar | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/10070-shubham-pampattiwar diff --git a/changelogs/unreleased/10070-shubham-pampattiwar b/changelogs/unreleased/10070-shubham-pampattiwar new file mode 100644 index 000000000..02f87194a --- /dev/null +++ b/changelogs/unreleased/10070-shubham-pampattiwar @@ -0,0 +1 @@ +Add snapshotClass parameter to volume policy snapshot action From 43a41adbf0a5eb1882a27884646ea874cc1e937c Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 23 Jul 2026 12:13:47 -0700 Subject: [PATCH 095/194] Document snapshotClass volume policy parameter Add documentation for the new snapshotClass parameter in the volume policy snapshot action. Update the CSI docs to include volume policy as a tier in the VolumeSnapshotClass selection priority, and add Example 6 to resource-filtering.md showing multi-array usage. Ref: #8807 Signed-off-by: Shubham Pampattiwar --- site/content/docs/main/csi.md | 19 ++++++++++++++-- site/content/docs/main/resource-filtering.md | 24 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/site/content/docs/main/csi.md b/site/content/docs/main/csi.md index 11973f50a..68d2c5f67 100644 --- a/site/content/docs/main/csi.md +++ b/site/content/docs/main/csi.md @@ -86,8 +86,23 @@ This section documents some of the choices made during implementing the CSI snap ``` Note: Please ensure all your annotations are in lowercase. And follow the following format: `velero.io/csi-volumesnapshot-class_ = ` - 3. **Choosing VolumeSnapshotClass for a particular PVC:** - If you want to use a particular VolumeSnapshotClass for a particular PVC, you can add a annotation to the PVC to indicate which VolumeSnapshotClass to use. This overrides any annotation added to backup or schedule. For example, if you want to use the VolumeSnapshotClass `test-snapclass` for a particular PVC, you can create a PVC like this: + 3. **Choosing VolumeSnapshotClass via Volume Policy:** + If you want to use a particular VolumeSnapshotClass based on conditions like StorageClass, you can specify the `snapshotClass` parameter in a volume policy's `snapshot` action. This is useful when multiple storage arrays share the same CSI driver but require different VolumeSnapshotClasses. For example: + ```yaml + version: v1 + volumePolicies: + - conditions: + storageClass: + - nutanix-files + action: + type: snapshot + parameters: + snapshotClass: nutanix-files-snapclass + ``` + This overrides backup/schedule annotations and VolumeSnapshotClass labels, but is overridden by PVC-level annotations. See the [resource filtering documentation](resource-filtering.md) for more volume policy examples. + + 4. **Choosing VolumeSnapshotClass for a particular PVC:** + If you want to use a particular VolumeSnapshotClass for a particular PVC, you can add a annotation to the PVC to indicate which VolumeSnapshotClass to use. This overrides any other method of selecting a VolumeSnapshotClass. For example, if you want to use the VolumeSnapshotClass `test-snapclass` for a particular PVC, you can create a PVC like this: ```yaml apiVersion: v1 kind: PersistentVolumeClaim diff --git a/site/content/docs/main/resource-filtering.md b/site/content/docs/main/resource-filtering.md index 88584b362..8f8f800ef 100644 --- a/site/content/docs/main/resource-filtering.md +++ b/site/content/docs/main/resource-filtering.md @@ -617,6 +617,7 @@ a volume policy but for a particular volume included in the backup there are no in such a scenario the legacy approach will be used for backing up the particular volume. Considering everything, the recommendation would be to use only one of the approaches to backup volumes - volume policy approach or the opt-in/opt-out legacy approach, and not mix them for clarity. - Snapshot action can either be a native snapshot or a csi snapshot or csi snapshot datamover, as is the case with the current flow where velero itself makes the decision based on the backup CR's existing options. +- The `snapshot` action supports an optional `snapshotClass` parameter that specifies which VolumeSnapshotClass to use for CSI snapshots. This is useful when multiple storage arrays share the same CSI driver but require different VolumeSnapshotClasses. When specified, this takes priority over backup annotations and VolumeSnapshotClass labels, but is overridden by PVC-level annotations. See the [CSI documentation](csi.md) for the full VolumeSnapshotClass selection priority order. - The `snapshot` action via Volume Policy has higher priority if there is a `snapshot` action matching for a particular volume, this volume would be backed up via snapshot irrespective of the value of `backup.Spec.SnapshotVolumes`. - If for a particular volume there is no `snapshot` matching action then the volume will be backed up via snapshot given that `backup.Spec.SnapshotVolumes` is not explicitly set to false. - Let's see some examples on how to use the volume policy feature for `fs-backup` and `snapshot` action purposes: @@ -705,6 +706,29 @@ volumePolicies: - `fs-backup` on `Volume 1` because `Volume 1` satisfies the criteria for `fs-backup` action. - Also, for Volume 2 as no matching action was found so legacy approach will be used as a fallback option for this volume (`fs-backup` operation will be done as `defaultVolumesToFSBackup: true` is specified by the user). +***Example 6: User has two storage arrays using the same CSI driver and needs different VolumeSnapshotClasses for each*** +1. User specifies the volume policy as follows: +```yaml +version: v1 +volumePolicies: +- conditions: + storageClass: + - array-1-sc + action: + type: snapshot + parameters: + snapshotClass: vsc-array-1 +- conditions: + storageClass: + - array-2-sc + action: + type: snapshot + parameters: + snapshotClass: vsc-array-2 +``` +2. User creates a backup using this volume policy +3. The outcome would be that velero would use `vsc-array-1` VolumeSnapshotClass for volumes on storage class `array-1-sc` and `vsc-array-2` VolumeSnapshotClass for volumes on storage class `array-2-sc`, even though both storage classes use the same CSI driver. + ### Global backup volume policies Resource policies (volume policies) are normally opt-in per backup via `--resource-policies-configmap`. An administrator can instead configure a cluster-wide baseline that applies to **every** backup by starting the Velero server with the `--global-backup-volume-policies-configmap` flag, pointing at a ConfigMap in the Velero install namespace: From 0f45175bf81c1107084fbb85b2d5dca3e8e6d137 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 23 Jul 2026 12:26:30 -0700 Subject: [PATCH 096/194] Fix import ordering in pvc_action.go Signed-off-by: Shubham Pampattiwar --- pkg/backup/actions/csi/pvc_action.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index 06c65075d..c112b9c59 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -42,8 +42,8 @@ import ( crclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/internal/resourcepolicies" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" veleroclient "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/kuberesource" From 928310d0204401c8c47536a0d0acfc9a8197f71e Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 23 Jul 2026 13:32:27 -0700 Subject: [PATCH 097/194] Add end-to-end test for snapshotClass volume policy parameter Verify that when a volume policy specifies snapshotClass, the CSI plugin creates a VolumeSnapshot using that VolumeSnapshotClass. The test uses a VSC without the velero label to confirm selection comes from the volume policy parameter, not the label-based fallback. Ref: #8807 Signed-off-by: Shubham Pampattiwar --- pkg/backup/actions/csi/pvc_action_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pkg/backup/actions/csi/pvc_action_test.go b/pkg/backup/actions/csi/pvc_action_test.go index e7320cd1a..804a451e4 100644 --- a/pkg/backup/actions/csi/pvc_action_test.go +++ b/pkg/backup/actions/csi/pvc_action_test.go @@ -96,6 +96,7 @@ func TestExecute(t *testing.T) { resourcePolicy *corev1api.ConfigMap failVSCreate bool skipVSReadyUpdate bool // New flag to control VS readiness + expectedVSClassName string }{ { name: "Skip PVC BIA when backup is in finalizing phase", @@ -188,6 +189,16 @@ func TestExecute(t *testing.T) { sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(), }, + { + name: "Volume policy with snapshotClass selects correct VolumeSnapshotClass", + backup: builder.ForBackup("velero", "test").ResourcePolicies("resourcePolicy").CSISnapshotTimeout(time.Duration(3600) * time.Second).Result(), + resourcePolicy: builder.ForConfigMap("velero", "resourcePolicy").Data("policy", `{"version":"v1","volumePolicies":[{"conditions":{"csi":{}},"action":{"type":"snapshot","parameters":{"snapshotClass":"policy-selected-vsclass"}}}]}`).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), + pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(), + sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), + vsClass: builder.ForVolumeSnapshotClass("policy-selected-vsclass").Driver("hostpath").Result(), + expectedVSClassName: "policy-selected-vsclass", + }, } for _, tc := range tests { @@ -301,6 +312,15 @@ func TestExecute(t *testing.T) { runtime.DefaultUnstructuredConverter.FromUnstructured(resultUnstructed.UnstructuredContent(), resultPVC) require.True(t, cmp.Equal(tc.expectedPVC, resultPVC, cmpopts.IgnoreFields(corev1api.PersistentVolumeClaim{}, "ResourceVersion", "Annotations", "Labels"))) } + + if tc.expectedVSClassName != "" { + vsList := new(snapshotv1api.VolumeSnapshotList) + require.NoError(t, crClient.List(t.Context(), vsList, &crclient.ListOptions{Namespace: tc.pvc.Namespace})) + require.NotEmpty(t, vsList.Items, "expected VolumeSnapshot to be created") + require.NotNil(t, vsList.Items[0].Spec.VolumeSnapshotClassName) + assert.Equal(t, tc.expectedVSClassName, *vsList.Items[0].Spec.VolumeSnapshotClassName, + "VolumeSnapshot should use the VolumeSnapshotClass specified by volume policy") + } }) } } From 6527b1e301abdf469a2ae57f2ec203f43641fdf8 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 23 Jul 2026 14:48:07 -0700 Subject: [PATCH 098/194] Fix gofmt struct field alignment in pvc_action_test.go Signed-off-by: Shubham Pampattiwar --- pkg/backup/actions/csi/pvc_action_test.go | 44 +++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/backup/actions/csi/pvc_action_test.go b/pkg/backup/actions/csi/pvc_action_test.go index 804a451e4..73108c14b 100644 --- a/pkg/backup/actions/csi/pvc_action_test.go +++ b/pkg/backup/actions/csi/pvc_action_test.go @@ -81,21 +81,21 @@ func (c *errorInjectingClient) Create(ctx context.Context, obj crclient.Object, func TestExecute(t *testing.T) { boolTrue := true tests := []struct { - name string - backup *velerov1api.Backup - pvc *corev1api.PersistentVolumeClaim - pv *corev1api.PersistentVolume - sc *storagev1api.StorageClass - vsClass *snapshotv1api.VolumeSnapshotClass - operationID string - expectedErr error - expectErr bool // Use bool for cases where we just need to check for any error - expectedBackup *velerov1api.Backup - expectedDataUpload *velerov2alpha1.DataUpload - expectedPVC *corev1api.PersistentVolumeClaim - resourcePolicy *corev1api.ConfigMap - failVSCreate bool - skipVSReadyUpdate bool // New flag to control VS readiness + name string + backup *velerov1api.Backup + pvc *corev1api.PersistentVolumeClaim + pv *corev1api.PersistentVolume + sc *storagev1api.StorageClass + vsClass *snapshotv1api.VolumeSnapshotClass + operationID string + expectedErr error + expectErr bool // Use bool for cases where we just need to check for any error + expectedBackup *velerov1api.Backup + expectedDataUpload *velerov2alpha1.DataUpload + expectedPVC *corev1api.PersistentVolumeClaim + resourcePolicy *corev1api.ConfigMap + failVSCreate bool + skipVSReadyUpdate bool // New flag to control VS readiness expectedVSClassName string }{ { @@ -190,13 +190,13 @@ func TestExecute(t *testing.T) { vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(), }, { - name: "Volume policy with snapshotClass selects correct VolumeSnapshotClass", - backup: builder.ForBackup("velero", "test").ResourcePolicies("resourcePolicy").CSISnapshotTimeout(time.Duration(3600) * time.Second).Result(), - resourcePolicy: builder.ForConfigMap("velero", "resourcePolicy").Data("policy", `{"version":"v1","volumePolicies":[{"conditions":{"csi":{}},"action":{"type":"snapshot","parameters":{"snapshotClass":"policy-selected-vsclass"}}}]}`).Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), - pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(), - sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), - vsClass: builder.ForVolumeSnapshotClass("policy-selected-vsclass").Driver("hostpath").Result(), + name: "Volume policy with snapshotClass selects correct VolumeSnapshotClass", + backup: builder.ForBackup("velero", "test").ResourcePolicies("resourcePolicy").CSISnapshotTimeout(time.Duration(3600) * time.Second).Result(), + resourcePolicy: builder.ForConfigMap("velero", "resourcePolicy").Data("policy", `{"version":"v1","volumePolicies":[{"conditions":{"csi":{}},"action":{"type":"snapshot","parameters":{"snapshotClass":"policy-selected-vsclass"}}}]}`).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), + pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(), + sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), + vsClass: builder.ForVolumeSnapshotClass("policy-selected-vsclass").Driver("hostpath").Result(), expectedVSClassName: "policy-selected-vsclass", }, } From cc91b74846aba29bcffc4d6916cd8df5b047f4f7 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Fri, 24 Jul 2026 12:04:47 -0700 Subject: [PATCH 099/194] Add GetSnapshotClass to VolumeHelper interface Add a GetSnapshotClass method to VolumeHelper that encapsulates the extraction of the snapshotClass parameter from volume policy actions. This avoids requiring callers to parse raw parameters from GetActionParameters. Simplify the CSI plugin to use the new method. Ref: #8807 Signed-off-by: Shubham Pampattiwar --- internal/volumehelper/volume_policy_helper.go | 15 +++++++++++++++ pkg/backup/actions/csi/pvc_action.go | 17 +++++------------ pkg/util/volumehelper/volume_policy_helper.go | 1 + 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/internal/volumehelper/volume_policy_helper.go b/internal/volumehelper/volume_policy_helper.go index 6931697c9..3259bdb43 100644 --- a/internal/volumehelper/volume_policy_helper.go +++ b/internal/volumehelper/volume_policy_helper.go @@ -430,6 +430,21 @@ func (v *volumeHelperImpl) GetActionParameters(obj runtime.Unstructured, groupRe return false, "", nil, nil } +func (v *volumeHelperImpl) GetSnapshotClass(obj runtime.Unstructured, groupResource schema.GroupResource) (string, error) { + matched, actionType, params, err := v.GetActionParameters(obj, groupResource) + if err != nil { + return "", err + } + if !matched { + return "", nil + } + action := &resourcepolicies.Action{ + Type: resourcepolicies.VolumeActionType(actionType), + Parameters: params, + } + return action.GetSnapshotClass() +} + func (v *volumeHelperImpl) shouldIncludeVolumeInBackup(vol corev1api.Volume) bool { includeVolumeInBackup := true // cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index c112b9c59..c4d3007aa 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -42,7 +42,6 @@ import ( crclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "github.com/vmware-tanzu/velero/internal/resourcepolicies" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" veleroclient "github.com/vmware-tanzu/velero/pkg/client" @@ -340,17 +339,11 @@ func (p *pvcBackupItemAction) Execute( return nil, nil, "", nil, err } - policySnapshotClass := "" - matched, actionType, params, paramsErr := vh.GetActionParameters(item, kuberesource.PersistentVolumeClaims) - if paramsErr != nil { - p.log.WithError(paramsErr).Warn("failed to get action parameters from volume policy, proceeding without policy snapshotClass") - } else if matched && actionType == string(resourcepolicies.Snapshot) && params != nil { - if sc, ok := params[resourcepolicies.SnapshotClassParameter]; ok { - if scStr, ok := sc.(string); ok && scStr != "" { - policySnapshotClass = scStr - p.log.Infof("Volume policy specifies snapshotClass=%s for PVC %s/%s", scStr, pvc.Namespace, pvc.Name) - } - } + policySnapshotClass, scErr := vh.GetSnapshotClass(item, kuberesource.PersistentVolumeClaims) + if scErr != nil { + p.log.WithError(scErr).Warn("failed to get snapshotClass from volume policy, proceeding without it") + } else if policySnapshotClass != "" { + p.log.Infof("Volume policy specifies snapshotClass=%s for PVC %s/%s", policySnapshotClass, pvc.Namespace, pvc.Name) } vs, err := p.getVolumeSnapshotReference(context.TODO(), pvc, backup, policySnapshotClass) diff --git a/pkg/util/volumehelper/volume_policy_helper.go b/pkg/util/volumehelper/volume_policy_helper.go index 95f104994..6abdc73f8 100644 --- a/pkg/util/volumehelper/volume_policy_helper.go +++ b/pkg/util/volumehelper/volume_policy_helper.go @@ -27,4 +27,5 @@ type VolumeHelper interface { ShouldPerformFSBackup(volume corev1api.Volume, pod corev1api.Pod) (bool, error) ShouldPerformCustomAction(obj runtime.Unstructured, groupResource schema.GroupResource, matchParams map[string]any) (bool, error) GetActionParameters(obj runtime.Unstructured, groupResource schema.GroupResource) (bool, string, map[string]any, error) + GetSnapshotClass(obj runtime.Unstructured, groupResource schema.GroupResource) (string, error) } From 931232caba3225b77997f96af1c057dbef39ba58 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Fri, 24 Jul 2026 15:47:13 -0700 Subject: [PATCH 100/194] Fix gofmt formatting in resource_policies.go Signed-off-by: Shubham Pampattiwar --- internal/resourcepolicies/resource_policies.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/resourcepolicies/resource_policies.go b/internal/resourcepolicies/resource_policies.go index 22830356a..c1ba0ffc8 100644 --- a/internal/resourcepolicies/resource_policies.go +++ b/internal/resourcepolicies/resource_policies.go @@ -181,6 +181,7 @@ func validatePolicyLabelSelector(s *PolicyLabelSelector) error { _, err := SelectorFromPolicyLabelSelector(s) return err } + // ResourceFilter defines a filter for specific resource kinds. type ResourceFilter struct { Kinds []string `yaml:"kinds"` From ff7273548b5d2d1283120db5c7d0b1bc9de5fe9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:12:25 +0000 Subject: [PATCH 101/194] Bump codecov/codecov-action from 6 to 7 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6 to 7. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v6...v7) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pr-ci-check.yml | 2 +- .github/workflows/push.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-ci-check.yml b/.github/workflows/pr-ci-check.yml index ba55e6ab0..b189a622a 100644 --- a/.github/workflows/pr-ci-check.yml +++ b/.github/workflows/pr-ci-check.yml @@ -24,7 +24,7 @@ jobs: - name: Make ci run: make ci - name: Upload test coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.out diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index b45af38d9..528776e54 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -45,7 +45,7 @@ jobs: - name: Test run: make test - name: Upload test coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.out From 91e089d3e6d1797099bd76ebc165b82978b9aec6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:12:46 +0000 Subject: [PATCH 102/194] Bump actions/labeler from 5 to 7 Bumps [actions/labeler](https://github.com/actions/labeler) from 5 to 7. - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/v5...v7) --- updated-dependencies: - dependency-name: actions/labeler dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/auto_label_prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto_label_prs.yml b/.github/workflows/auto_label_prs.yml index 21540d8cb..cc61473db 100644 --- a/.github/workflows/auto_label_prs.yml +++ b/.github/workflows/auto_label_prs.yml @@ -18,6 +18,6 @@ jobs: if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v7 with: configuration-path: .github/labeler.yml From bc596da38666c8e813953b91db7f2fe94ebfe0f9 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 27 Jul 2026 14:19:22 +0800 Subject: [PATCH 103/194] block uploader restore implementation Signed-off-by: Lyndon-Li --- pkg/uploader/block/snapshot.go | 5 ++++- pkg/uploader/block/uploader.go | 2 +- pkg/uploader/block/uploader_test.go | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/uploader/block/snapshot.go b/pkg/uploader/block/snapshot.go index b185f4e15..53e7e7f14 100644 --- a/pkg/uploader/block/snapshot.go +++ b/pkg/uploader/block/snapshot.go @@ -121,7 +121,10 @@ func snapshotSource( return "", 0, errors.Wrapf(err, "Failed to run uploader backup for si %v", source) } - snap.Tags = make(map[string]string) + if snap.Tags == nil { + snap.Tags = make(map[string]string) + } + snap.Tags[uploader.CBTChangeIDTag] = cbtSource.ChangeID snap.Tags[uploader.CBTVolumeIDTag] = cbtSource.VolumeID if snapshotTags != nil { diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 0378f4f5b..8aa58bf96 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -155,7 +155,7 @@ func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bi } if len(meta.SubObjects) != 1 { - return 0, errors.Wrapf(err, "unexpected number of bdev object (%d) for snapshot %s", len(meta.SubObjects), snapshot.Description) + return 0, errors.Errorf("unexpected number of bdev object (%d) for snapshot %s", len(meta.SubObjects), snapshot.Description) } sourceSize, err := getSourceSize(snapshot) diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index bb7c79c5a..79c7be954 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -483,7 +483,7 @@ func TestGetSourceSize(t *testing.T) { name: "invalid tag value", snapshot: udmrepo.Snapshot{ Tags: map[string]string{ - "bdev-source-size": "abc", + bdevSourceSizeTag: "abc", }, }, expectErr: true, @@ -492,7 +492,7 @@ func TestGetSourceSize(t *testing.T) { name: "valid tag value", snapshot: udmrepo.Snapshot{ Tags: map[string]string{ - "bdev-source-size": "1048576", + bdevSourceSizeTag: "1048576", }, }, expectErr: false, @@ -667,7 +667,7 @@ func TestBlockUploaderRestore(t *testing.T) { Description: "test snapshot", RootObject: udmrepo.ObjectMetadata{ID: "root-id"}, Tags: map[string]string{ - "bdev-source-size": "1048576", + bdevSourceSizeTag: "1048576", }, } From 77e119c274640951d030b104f749c1b068609a8f Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 27 Jul 2026 00:05:44 -0700 Subject: [PATCH 104/194] Fix stale 'Latest Release Information' link on velero.io (#10081) Update the landing page CTA link from the outdated Velero 1.11 blog post to the GitHub releases/latest URL, which always resolves to the most recent release and will not go stale. Fixes #10080 Signed-off-by: Shubham Pampattiwar --- site/content/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/_index.md b/site/content/_index.md index 5d34a41a3..79426ecc8 100644 --- a/site/content/_index.md +++ b/site/content/_index.md @@ -10,7 +10,7 @@ hero: content: Velero is an open source tool to safely backup and restore, perform disaster recovery, and migrate Kubernetes cluster resources and persistent volumes. cta_link1: text: Latest Release Information - url: /blog/Velero-1.11/ + url: https://github.com/velero-io/velero/releases/latest cta_link2: text: Download Velero url: https://github.com/velero-io/velero/releases/latest From 3905ccb0eaa5f472606e509d1702396d9a073e8f Mon Sep 17 00:00:00 2001 From: Xun Jiang/Bruce Jiang <59276555+blackpiglet@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:36:44 +0800 Subject: [PATCH 105/194] Backup workflow for block data mover. (#10067) Signed-off-by: Xun Jiang --- changelogs/unreleased/10067-blackpiglet | 1 + .../v2alpha1/bases/velero.io_datauploads.yaml | 7 + config/crd/v2alpha1/crds/crds.go | 2 +- pkg/apis/velero/shared/constants.go | 22 +++ pkg/apis/velero/v2alpha1/data_upload_types.go | 6 + pkg/backup/actions/csi/pvc_action.go | 12 ++ pkg/backup/actions/csi/pvc_action_test.go | 152 ++++++++++++++++-- pkg/datamover/backup_micro_service.go | 2 +- pkg/datamover/util.go | 7 +- pkg/datamover/util_test.go | 10 ++ pkg/util/datamover/datamover.go | 13 +- pkg/util/datamover/datamover_test.go | 68 ++++++++ 12 files changed, 285 insertions(+), 17 deletions(-) create mode 100644 changelogs/unreleased/10067-blackpiglet create mode 100644 pkg/apis/velero/shared/constants.go diff --git a/changelogs/unreleased/10067-blackpiglet b/changelogs/unreleased/10067-blackpiglet new file mode 100644 index 000000000..3a4b67c31 --- /dev/null +++ b/changelogs/unreleased/10067-blackpiglet @@ -0,0 +1 @@ +Backup workflow for block data mover. \ No newline at end of file diff --git a/config/crd/v2alpha1/bases/velero.io_datauploads.yaml b/config/crd/v2alpha1/bases/velero.io_datauploads.yaml index 15682739b..6aed785d3 100644 --- a/config/crd/v2alpha1/bases/velero.io_datauploads.yaml +++ b/config/crd/v2alpha1/bases/velero.io_datauploads.yaml @@ -131,6 +131,13 @@ spec: OperationTimeout specifies the time used to wait internal operations, before returning error as timeout. type: string + parentSnapshot: + description: |- + ParentSnapshot specifies the parent snapshot that current backup is based on. + If its value is "" or "auto", the data mover finds the recent backup of the same volume as parent. + If its value is "none", the data mover will do a full backup + If its value is a specific snapshotID, the data mover finds the specific snapshot as parent. + type: string snapshotType: description: SnapshotType is the type of the snapshot to be backed up. diff --git a/config/crd/v2alpha1/crds/crds.go b/config/crd/v2alpha1/crds/crds.go index 59af9e6f0..485fafa80 100644 --- a/config/crd/v2alpha1/crds/crds.go +++ b/config/crd/v2alpha1/crds/crds.go @@ -30,7 +30,7 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcYK\x93\xe3\xb6\x11\xbeϯ\xe8\xda\x1c\xf6\xb2\xd2d\xf3p\xa5t\xdb\xd1\xc4US\xf1Ϊ\xac\xc9\xdcA\xb2E\xc1\v\x02\b\x1e\x92\xe5$\xff\xdd\xd5\x00I\x81$4z\xd8^\xdd\x044>|\xe8n\xf4\x03\x9c\xcdfwL\xf3W4\x96+\xb9\x00\xa69\xfe\xecP\xd2?;\xff\xfa\x0f;\xe7\xea~\xf7\xf1\xee+\x97\xd5\x02\x96\xde:\xd5\xfc\x88VyS\xe2#n\xb8\xe4\x8e+yנc\x15slq\a\xc0\xa4T\x8eѰ\xa5\xbf\x00\xa5\x92\xce(!\xd0\xccj\x94\xf3\xaf\xbe\xc0\xc2sQ\xa1\t\xe0\xddֻ?\xcf?~7\xff\xfb\x1d\x80d\r.\x80\xf0*\xb5\x97B\xb1\xca\xcew(Ш9WwVcI\xc0\xb5Q^/\xe08\x11\x17\xb6\x9bF\u008f̱\xc7\x16#\f\vnݿ&S?p\xeb´\x16\xde01\xda;\xccح2\xee\xf9\x88?\x83*\"Z.k/\x98\x19.\xba\x03\xb0\xa5Ҹ\x80\xb0F\xb3\x12i\xac=l\xc0\x98\x01\xab\xaa\xa0>&V\x86K\x87f\xa9\x84o\xe4q\a\xb4\xa5\xe1\xda\x05\xf5\xa4|\xc1:\xe6\xbc\x05\xeb\xcb-0\vϸ\xbf\x7f\x92+\xa3j\x836\xf2\x05\xf8\xc9*\xb9bn\xbb\x80y\x14\x9f\xeb-\xb3\xd8\xceF\x1d\xaf\xc3D;\xe4\x0e\xc4\xd7:\xc3e\x9dc\xf0\xc2\x1b\x84ʛ`[:w\x89\xe0\xb6\xdc\x0e\xa9\xed\x99%z\xc6au\x92H\x98'8\xebX\xa3nj\x92\xa5\x91R\xc5\x1c\xe6\b-U\xa3\x05:\xac\xa088쎱Q\xa6an\x01\\\xba\xef\xfevZ\x17\xad\xb2\xe6a飒C\xc5<\xd0($Ñ\tY\xa9F\x93ՎrL\xfc\x16\"\x8e\x00\x1e\x92\xf5\x91I\xc4M\xc7\xcfR!\x97\x03\xb5\x01\xb7Ex`\xe5W\xafa\xed\x94a5\xc2\x0f\xaa\x8c\xe6\xdbo\xd1`\x90(\xa2\x04y/p\xb2\x9d2Y\xd3i,\xe7Q\xb6\x05\xeb\xb0F\xf6\x1bn\xf4\xbb\xfbVi\x90e}\xab\x8bA\xf3 \xc1\x95\xcc;ا\x1a/r\xaeT\x89RU\x98hl\xc0\x89[\xd0F\x95h\xed\x1b\x0eO\x00\x03\x16\xcfǁ\x89j\xa2\xc4\xee/L\xe8-\xfb\x18\x83L\xb9ņ-\xda\x15J\xa3\xfc\xb4zz\xfd\xebz0\fo\x04\fV:K\x91\x82\xe8k\xa3\x9c*\x95\x80\x02\xdd\x1eQF\xd37j\x87\x86\x02`ͥ\xed\x11)\x9cW\xa9\xc01\x98\x93\x7f\a<\x9a\x8d\x93\x06\x83\xf7\x10A\x93Z\x1fhO\x8d\xc6\xf1.|\xb6\xd8\xc7̓\x8c\x8e\xce\xf1\xbf\xd9`\x0e\x80\x8e\x1eWAE)\b\xe3\xb1\xda؊U\xab\xadhn\xa1\x94@6\xd6\"y\xe1gJ\vK%7\xbc\x9e\x1e<-\x7fO\xb9\xc8\x19\x9df\x1c6ْNA\xdeILf!C\xcd:ץо\xe1\xb57\xa7\xec\xbf\xe1(\xaaI\xfc9y\x93\xba\x03\x87]n\xb1qO\xbd\xbb]mVKR\xafS!B\xd9P\xef&\xae9%\t\xf0\xb4I\x10\xb9\x85w\xef@\x19x\x17\x9b\xa5w\x1f\xe2jυ\x9b\xf1A\xfe\xdfs!\xba]\xae\xf2n\xaap\xbe\xacϜ\xfc9\b\x11\x9f/\xebkk\xab)\x1b\x94\xbe\x99n8\x03\xe6\x9d\xca\f\v.\xfdϙ\xf1=\x97\x95\xda\xdbk\x0e\xdb\xd77Tb*\xefn1\xf8\x97\x11\xc6\xc8\xee\x8e\n\xe2`k\xa7`\xcfxRc\xf4\xbb\xdb\x0f\x19\xdc\x027\x94\x90\f:o$\x85\x034\x86\"\xb4\r\x90\xcaOj\x9e7Oj%\xd3v\xab\xdc\xd3\xe3\x993\xae{\xc1.\xee>=v&~\r^\xd7\a\xdfV\x122V\"\xfa]\x15Y\x85\xb4~\x13\xdb5\xff\x05/\xe4K\xa2\x1dc\xa1j^2\x016\x8cɶ\tl\x0f\xd1aO\t\xe5\xfa\xbc1ݴ[K\xf8\x86ڧ\x7f!\xb8ō\xd6C\x88\xee(\xca\U0001a4f3\xc8~\xe6x\xc7vJ\xf8&\x88\x92I\xb0\x02\xafO\xe8\x1a(}P\xb1U T|\xb3AC\x15U(\xb7\xe2ƫ\xd7\xe5{\x9bl\xc27\xe9\x1f\xcaT\r\xd3\x1a+\xea\xed\xc8\x19[\xdb^eU\xc7L\x8d\xee5\x90>\xa3\xa2\x97D\xb4S\x05\x95fd\xa0\xb6\xf6\x0f\x97+\x88\xc1\xeau\x99\xa9\xd4\xe9\xb7z\x9d2<]\xc7\xd0oc_\xe8\x04\x99\x99\x11\xc5\xef\xd7$ؑ\xdbp\x81`\x0f\xd6a\x13T0b\x18-\x95\xb3˙\xb4\bG3\\\xc0i\xe2>\xed\xf6=\xc6-\x04\xf4\ue09dW\xaf\xb92\xad\xb7\x0f\xb8-s$\xd1v\xfdP\x1c\xb2\x98\xd0Řֿn\xe3[^Dx\xf9&\xe3\xe5\x98\xf2\t\xbe\xc5\xe17S\xa6*\x90\x1b\xacr9\xf0\xb4\xe5f\xa0w\xd9\xc1\xf2\xf2Z'\xbf\xf3,_Џdƹs4}L8\xe3\x89a\xa0\x1bͦ1\xe2\xa2\xce'\xbc\xcb\\\xda\xfb\xc4\xd7\xd6\xd6\xec\xa57!\n\xb6o\xb0jsc\xf7\xc3\xca\x12\xb5\xc3\xea\xe1@e\xd1\x05\x95\x13\x11\x90o\xbfJ\xfd[\x1f\xeb&\xd4\xec\xda\x16\xa5\xa3Կ\x9cݒ\x91>\x8dA\xc2\U000c9a52\xbafJ7ֶ\xa7I\x03\xbcP\x0e\x0e\xed\xff\xfbX\xcaвP Q\x89?\xd9\xf4d\x96\xa6\xfe~F\xeb'\x12\xd2\v\xc1\n\x81\vpƟ\xeau\xf2\xad]|\x88N\xdf\x1co\xea\xf3\xa60Sݱ\xfe\x95-\xbc\x86vO\xe09\x95\x1d\xf1z\x85E8\xac\x00w(\x81\xbaw\xc6\x05V\x1df\xa6\xe19\xa7\xf9\f\xe9i-\xfdG*\xbfAkY}\xee\x02}\x8eR\xf1a\xaa]\x02\xac\xa0\xc2{\xdcv\xbc\xb7\xedݾ\xba\x01\xfa}.\xf1\x85\xed\xcf\x1b\\B\xb3~\x86̊dr1\xad\xa7v:\xa8\xc1\x1b\xdd\xd73\xee3\xa3\xdd\xfd\xccL\xad\xdaK\x9f\x99\x9a|\xd3J'\xe3\xabH.1vsY\xcc\xfe\xa3Qf\xee\xfbp\x19\xae\xd2t\xcb\xef\x96\xeb\u07bf\xadl\x95\xe8nx\xf8\xd8#}S\xa0!3\x14\xb9\x0e$<\xc9'V\xcb\x15\x7f=B\xdfL\x05\xa89\xbcl\xa94\x89\x0fB]{Yq\xab\x05;\xf4\x87IK\xe6\f\xf8\xf1\xd6L\xde\xfb\xaf\xad\x9a\xfb\x8fo\xf9\xca\xeb\xed\xce\n\xcetWa\xbe\xff\xa8\xf6\xc7\xec\xf0\xc6s\xd0\xf0#\xe7M\xbd\xdd\x00\xe1\\*h?\xba^\x1f\xc1\x87\xdb|\xcb\xe0\x9d\xd5\xded00\xaf\x12\xec\xf6\xf96\x1d\xf1E\xffMc\x01\xff\xfd\xffݯ\x01\x00\x00\xff\xff];\x85{\xd8 \x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcZIs\xe36\x16\xbe\xfbW\xbc\xea9\xe4b\xc9\xe9YRS\xba\xb5\xe5I\x95j\xd2nW\xcb\xe3;D>\x89\x88A\x80\x83E\x8af\xf9\xef\xa9\ap\x01IH\x94\x94Nx\xe8jcyx\x1b\xbe\xb7@\xb3\xd9\xec\x8eU\xfc\r\xb5\xe1J.\x80U\x1c\x7f\xb1(\xe9/3\x7f\xff\xbb\x99s\xf5\xb0\xffx\xf7\xcee\xbe\x80\xa53V\x95_\xd1(\xa73|\xc2-\x97\xdcr%\xefJ\xb4,g\x96-\xee\x00\x98\x94\xca2\x1a6\xf4'@\xa6\xa4\xd5J\bԳ\x1d\xca\xf9\xbb\xdb\xe0\xc6q\x91\xa3\xf6ě\xa3\xf7\xdf\xcf?\xfe0\xff\xdb\x1d\x80d%.\x80\xe8\xb9J(\x96\x9b\xf9\x1e\x05j5\xe7\xea\xceT\x98\x11ٝV\xaeZ@7\x11\xb6\xd5G\x06v\x9f\x98e\xff\xf2\x14\xfc\xa0\xe0\xc6\xfes0\xf1\x137\xd6OV\xc2i&z\xa7\xfaqS(m\x9f;\xca3\xc8]\x98\xe0r\xe7\x04\xd3\xf1\x96;\x00\x93\xa9\n\x17\xe0wT,C\x1a\xabE\xf4\x14f\xc0\xf2\xdc+\x8d\x89\x17ͥE\xbdT\u0095\xb2\xa3\x8f&Ӽ\xb2^)\x1d\xa7`,\xb3\u0380qY\x01\xcc\xc03\x1e\x1eV\xf2E\xab\x9dF\x13x\x05\xf8\xd9(\xf9\xc2l\xb1\x80yX>\xaf\nf\xb0\x9e\rz]\xfb\x89z\xc8\x1e\x89[c5\x97\xbb\xd4\xf9\xaf\xbcDȝ\xf6\xf6$\x993\x04[p\x133v`\x86\x98\xd3\x16\xf3\x93l\xf8y\"f,+\xab!?\xd1\xd6\xc0P\xce,\xa6\xd8Y\xaa\xb2\x12h1\x87\xcd\xd1b#\xc4V\xe9\x92\xd9\x05pi\x7f\xf8\xebiMԪ\x9a\xfb\xadOJ\xf6\xd5\xf2H\xa3\x10\r\aN\xc8B;\xd4I\xdd(\xcb\xc4oa\xc4\x12\x81\xc7h\x7f\xe0$Ѝ\xc7'YY\xc9Lc\x89\xf26\x86x\xb7{\xccML:\x9e\xad4W\x9a\xdb\xe3\x02>~\x7f)\x9bt+@m\xc1\x16\b\x8f,{w\x15\xac\xad\xd2l\x87\xf0\x93ʂ\x8f\x1d\nԵ\x8fm\xc2\x12S('r\xd84\x86\x010V餳U\x98\xcdî\x9anCv\xe0q\xfd3\xbf\xf1]\xc84\xb2\xe4]hPr\xeeWp%\xd3\x17\xe2\xd3\x0e/\xba\f\xb16\xa5ʱU\x1d\xc6\x1cq\x03\x95V\x19\x1as\xe6z\xd2\xf6\x1e\x0f\xcf\xdd\xc0H-a\xc5\xfe\xcfLT\x05\xfb\x18\xc00+\xb0d\x8bz\x87\xaaP~zY\xbd\xfde\xdd\x1b\x86\x93\xd0\xc62k\bӈ\xf5J+\xab2%`\x83\xf6\x80(=\xbcB\xa9\xf6\xa8\t\xa4w\\\x1a`2oiB\xbc\xa0\v5\xe4\xfa\x9e\x1e͆\xc9ڝT\x85:6;\xb92\x8dY\xde`|\xf8\xa2\xb0\x18\x8d\x0e\x84\xf8߬7\a@r\x87]\x90S|\xc4 U\x1d\x020\xafU\x15\xec\xc6\rh\xac4\x1a\xba^ޫ\xd4\x16\x98\x04\xb5\xf9\x193;\x1f\x90^\xa3&2\xcd}Ȕܣ\xb6\xa01S;\xc9\xff\xd3\xd26`\x95?T0\x8b\xc6\xfa\v\xa9%\x13\xb0g\xc2\xe1\xfd@{\xf4\x95\xec\b\x1a\xe9Lp2\xa2\xe77\x98!\x1f\x9f\x95F\xe0r\xab\x16PX[\x99\xc5\xc3Î\xdb&Y\xc8TY:\xc9\xed\xf1\xc1\x1b\x83o\x9cU\xda<\xe4\xb8G\xf1`\xf8n\xc6tVp\x8b\x99u\x1a\x1fX\xc5g^\x10\xe9\x13\x86y\x99\xffI\xd7\xe9\x85\xe9\x1d;\xf2\xc2\xf0\xf9@\x7f\x85y(\xfeӕ`5\xa9 bg\x05\x1a\"\xd5}\xfd\xc7\xfa\x15\x1aN\x82\xa5\x82Q\xba\xa5#\xbd4\xf6!mr\xb9E\x1d\xf6m\xb5*=M\x94y\xa5\xb8\xb4\xfe\x8fLp\x94\x16\x8c۔ܒ\x1b\xfcۡ\xb1d\xba!٥O\xa8`\x83\xe0*\x82\x82|\xb8`%a\xc9J\x14Kf\xf0\x0f\xb6\x15Y\xc5\xcc\xc8\b\x17Y+N\x13\x87\x8b\x83z\xa3\x89&\xd3;a\xda\x0e>\xd6\x15fdSR+m\xe2[^\xc7\x12\xc2\x00\x16\xad\xeck'}\xed\xe9K\x86\x90\xe1\xa2)W\xa3\xef1E\xa8\xe1UF\xf8݄\xba:2\x89~d\x8a\xbf\x0e\xe4\xeb=\x1a+e\xb8U\xfaH\x84Ch\x1c\xba\xc1I\x8bЗ1\x99\xa1\xb8E\xbc\xa5\xdf\t\\\xe6\xa4qlݘ\x00(P\xf5\x8c*\xb9St\xb1\"C\xc0\xca\xd2\n\xf2j\x836-\xa6L\x842.\xa1Kz!Nn\x87\xa2n\x94\x12Ȇ\x1a\xcc\f_KV\x99B\xd9\t\x81W[hV\xbe\x1e+\xa4×\xeb\xd5=\xfdӌ\x93\a\xedy^C<\xdd2ʶ\xd2f\xab\xed\xbc\\\xaf\xc0\xd4\xdb\xc7F\x92N\b\xb6\x11\xb8\x00\xab\xddX\xb0\xd3\x0e\xeb\xb9\xd7|\x8f:53\xbc9~a\xe3\x85a\x1b8\xe3\x93j?\xf4F\x05\t6R.\x95\xb4(S6:\xebU\xf45\x92.\x053I\x9e\a\x9c\xad\xe3\xf5\xa9k\xd2\x10\x84̯\xb0\x05K\xf3\x05!\xe8z9\xbaM\xbc\xcd\xcd\xe0\xc0mq\x93D\xe1\x82^,P\xb4<)O}߃8j{F\x98\x97\xb7\xa5\x97wJ2\n7\xb7H\xb6\xef\x19\xfd\x02\xd9\xfa^\x92\x92n\xc0\xe5)\xe1\x14\xa1\x00\x81\x19\xe6\xe0\xaa\xeby'\xd0\xe1\x1a\xf31ϳ\x9e\xbd\x12\xd3}\xa1O \xc9(2A\x9dt~\xa6\xb4r\xa9\xe4\x96\xef\xc6g\xc7e\xfe\xb9k{V\xb4Qċ\x8e$\x8dS\x80#Nf>Ý5яr\xc3-\xdf9}\n\x8d\xb6\x1cE>J`&\x01hB\x1f\x9e\x89[\xe2H+Y\x13\xbfkH\x8d2\xfb\xe0%1J\x85\xf07\x96\x01\b\xba;\x8a\xdc\xc0\x87\x0f\xa04|\b\xbd\xa2\x0f\xf7a\xb7\xe3\xc2\xcex\xaf\xbc8p!\x9aS\xae\x8a\xa0mIA\x05\x9drS\xa1%\xa9\x83/\x03\x1a\x03UX*>\xbd\xf8V\xc1\x81\xf1(\xadoO7\xf7\t\xba\x1b\xdcR\x0e\xa8\xd1:-)\n\xa3֔\x16\x19OR\xb9D\x18:#\xa9\x89B℔\xc3\xe8饠\xff\x0f\xb1<\x06\x80\x84\x00)\x1b\x9f\xe3Ч\xec?\xae/\xe10Z\xdap\xb8\xe5\x02\xc1\x1c\x8dŲ\xcfm\xa8\x04\x02`\xdc\xc0P\xdb\x10\xbc\xc57\xd6}\x12\r\xafJ\xf3\x1d'\x0f\x90\xedL\x97\x1d\xd6\xe0[\xb7Q<\xb4\xfaؐ\xbc0-|\x1b\x82\xef\x8e\x1c\xe1K8\x9c\xc2\x0f\x93\xb9O`\xda\xf9\xbcƂ\x04\x92L*\xe4\xe5my\x91y\xe8\xe0Dl\xa1\xe1C\xc1\xb3\xa2\xefK|\x8c\xf2\x00\x96\xbd\xa3/\x06\xae`3\x1dTf\xe9\xd2`\xb0f\b\a\x83\xe9\xf8\x0e\r\xa7\xfa\x86Nξ\xbc-/*\x9f|g\xe7\xb2\x02*t\x96k-gNk_\x9a\x86Q\xb5\xbd\xa9\x84bY\x86\x95\xc5\xfc\xf1\xf8\xac\xf2)\xa7\xff\xd4[L\x8c\xc8Kz[\tS\xfbn\x17V\xec\xda\x1a\xa8a\xb7\xed\xc8\xddrM?\r\x89\xf8ތ\xce#\x04\x1fW4\x01\xfdN3\r\xf0J\x0e\xee{\v\xdf\x05Цm>\x14\xd0\xf5\x1c\x1d:\xa2\xd04\x81sfqF\xfbo\v\xfb\xe9\xda14\xe4\xe3^\xe6M\x85\xe4\x98\xccXw\xac\xa9x}\x93\xb5y\tHi\xac#\xd7\xea+P\xc3\x1cp\x8f\x12\x94\x84-を\tO2\x01`\xe7\xa9\xd4Q5<\xfb4M\xa3\xa6\xc1\x98\xec\xdeM[2\xa1\x841\x9a\xfd\x9e\xc6lsگh\x9cHd1\xbfcN\x1b\x8e\f\xed\v\x93\xcci\xcf\xd7\xd7\xcc\x00\x03\x1d\x88Ըq\n\xb4.VR2\xd1\x1d>\x96L\xb5\x11\x06ˡP\xa2vj\xe9\xca\rj\xe2\xd6?ـ\xc4\x03\xe5\xa9Y\xc1\xe4.\x99\t5O\x0e\b\x82\x19[\xbb\xdbI\x0f\x89\xdf|\x86\x92\xc5o4\xddW\xa21l7\x05֟ê\xd0E\xad\xb7\x00\xdbP\xca\xda\xd7\xfaw\xa6\x8e!W!\xb1\x9c\x0e\x17W\x05\x89\xde\x03\xc8՜|Y_\xc0˗5\x1d\xf2e\xfd[yA\xe9\xcaT\x11˜U\x89a\xc1\xa5\xfb%1~\xe02W\x871t\x9c\x11\xb5b\xb6\x98\x10\xf4\x85٢M\x92\x9d\x10~\xcf(\x97\xaf\xb3\xce\r\x12&~\xab\x94\u07b7\xf9\xa6أ5\xa9\x14\x06/\x81\x83S\x9a\x7f\xc6Cb\xb4\t\xb9\x89\xa9\x97:\x8e'\xa6F\x8f\xf5\xf1d褦ಙK\xd2l\xdf\xc3\x13s?\xfa\x00w\x95\x9ek\xfen\x89\xe0mO\xb6\xc37\xff\xbc=B\xb9~o\x88J\x8a\xc8b\t\xc2\xd1\xfe\xb6\x8e\xf1\x94\xe6\xf0Zp\xd3t\x91\x9b\xd28\xe7\xa6\x12\xec\xd8\xca2\x156Z\xdc\x1a\xbe\x0e\x8e\x9d\xe4|\xfb\xb5\xfdUA\xbauv\x1e\x95a\x02\x99\xfd\xbc:\x1dr\xbe\xc5\tgb^s\xbdWO\x17\xd6\xfc\xab\xa7\xe6*\xf2\x1c\xa5\xe5[\x1e\xbd\xc8vŚ\xef\xf0\xa7t9|ٸ\xae\xbe\xec\xfd\xd6\xe4\xa6z\xbbGa\"\x13\xad\x7f\xfa\x92\xca\xf7\xd6\x04\x06\x04A\xfe\rp9|\xf5\xbfo#:\xb3\xf5Cd\b\xfe\xa9\"VIJo|zt}j\xd9\x17\xe8\x8f\xcc*\x93^5\x1a\xf4\x9c\xe7\x11\xed\xbao\x1b\x8f\xb8M\xfb2\xbc\x80\xff\xfe\xff\xee\xd7\x00\x00\x00\xff\xffʖ\x89F\xbb&\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcZI\xb3\xdb6\x12\xbe\xbf_\xd1\xe59\xe4b\xe9ų\xa4\xa6t\xb3\xe5Iի\x89\xedW\x96\xe7\xdd!\xb2)\"&\x01\x0e\x16)\x9a忧\x1a\v\t\x92\xd0\x1a'<\xb8\xfc\xb04zC\xf7\xd7\r-\x16\x8b\a\xd6\xf1\x17T\x9aK\xb1\x02\xd6q\xfcŠ\xa0\xbf\xf4\xf2\xeb\xdf\xf5\x92\xcb\xc7\xfd\x9b\x87\xaf\\\x94+X[md\xfb\x19\xb5\xb4\xaa\xc0\xf7Xq\xc1\r\x97\xe2\xa1E\xc3Jf\xd8\xea\x01\x80\t!\r\xa3aM\x7f\x02\x14R\x18%\x9b\x06\xd5b\x87b\xf9\xd5nqkyS\xa2r\xc4\xe3\xd1\xfb\xef\x97o~X\xfe\xed\x01@\xb0\x16W@\xf4l\xd7HV\xea\xe5\x1e\x1bTr\xc9\xe5\x83\xee\xb0 \xb2;%m\xb7\x82a\xc2o\vGzv\xdf3\xc3\xfe\xe5(\xb8\xc1\x86k\xf3\xcf\xc9\xc4O\\\x1b7\xd95V\xb1ft\xaa\x1b\u05f5T\xe6\xe3@y\x01\xa5\xf5\x13\\\xecl\xc3T\xba\xe5\x01@\x17\xb2\xc3\x15\xb8\x1d\x1d+\x90Ƃ\x88\x8e\xc2\x02XY:\xa5\xb1\xe6YqaP\xadec[1\xd0G](\xde\x19\xa7\x94\x81SІ\x19\xabAۢ\x06\xa6\xe1#\x1e\x1e\x9fij\x92;\x85\xda\xf3\n\xf0\xb3\x96♙z\x05K\xbf|\xd9\xd5Lc\x98\xf5zݸ\x890d\x8eĭ6\x8a\x8b]\xee\xfc/\xbcE(\xadr\xf6$\x99\v\x04Ss\x9d2v`\x9a\x98S\x06˓l\xb8y\"\xa6\rk\xbb)?\xc9V\xcfP\xc9\f\xe6\xd8Y˶k\xd0`\tۣ\xc1(D%U\xcb\xcc\n\xb80?\xfc\xf5\xb4&\x82\xaa\x96n\xeb{)\xc6jyG\xa3\x90\f{N\xc8B;TY\xddHÚ\xdf\u0088!\x02\xef\x92\xfd\x9e\x13O7\x1d\xbf\xc8ʓ(\x14\xb6(\xeec\x88\x0f\xbb\xe7ܤ\xa4\xd3\xd9Nq\xa9\xb89\xae\xe0\xcd\xf7ײI\xb7\x02d\x05\xa6FxNJ\xaf\xb6\x83\x8d\x91\x8a\xed\x10~\x92\x85\xf7\xb1C\x8d*\xf8\xd8\xd6/ѵ\xb4M\t\xdbh\x18\x00m\xa4\xca:[\x87\xc5\xd2\xef\nt#ىǍ\xcf\xfc\xc6w\xa1PȲw!Fɥ[\xc1\xa5\xc8_\x88\xb7;\xbc\xea2\xa4\xda\x14\xb2\xc4^u\x98r\xc45tJ\x16\xa8\xf5\x99\xebI\xdbG<|\x1c\x06fj\xf1+\xf6\x7ffMW\xb37>\x18\x165\xb6l\x15v\xc8\x0e\xc5\xdb秗\xbflF\xc3p2\xb4\xb1\xc2h\x8ai\xc4z\xa7\xa4\x91\x85l`\x8b\xe6\x80(\\x\x85V\xeeQQ\x90\xdeq\xa1\x81\x89\xb2\xa7\t\xe9\x82!Ր\xeb;z4\xeb'\x83;\xc9\x0eUjvre\x1a3<\xc6x\xff%i1\x19\x9d\b\xf1\xbf\xc5h\x0e\x80\xe4\xf6\xbb\xa0\xa4\xfc\x88^\xaa\x90\x02\xb0\f\xaa\xf2v\xe3\x1a\x14v\n5]/\xe7U\xb2\x02&@n\x7f\xc6\xc2,'\xa47\xa8\x88L\xbc\x0f\x85\x14{T\x06\x14\x16r'\xf8\x7fz\xda\x1a\x8ct\x876̠6\xeeB*\xc1\x1aس\xc6\xe2\xeb\x89\xf6\xe8k\xd9\x11\x14ҙ`EB\xcfm\xd0S>>H\x85\xc0E%WP\x1b\xd3\xe9\xd5\xe3㎛\b\x16\nٶVps|t\xc6\xe0[k\xa4ҏ%\xee\xb1y\xd4|\xb7`\xaa\xa8\xb9\xc1\xc2X\x85\x8f\xac\xe3\v'\x88p\x80aٖ\x7fR\x01^\xe8ѱ3/\xf4\x9fK\xf47\x98\x87\xf2?]\t\x16Hy\x11\a+\xd0\x10\xa9\xee\xf3?6_ r\xe2-\xe5\x8d2,\x9d\xe9%ڇ\xb4\xc9E\x85\xca䀹l\x1dM\x14e'\xb90\ue3e2\xe1(\fh\xbbm\xb9!7\xf8\xb7Em\xc8tS\xb2k\a\xa8`\x8b`;\n\x05\xe5t\xc1\x93\x805k\xb1Y3\x8d\x7f\xb0\xad\xc8*zAF\xb8\xcaZ)L\x9c.\xf6\xeaM&\"\xd2;a\xda!|l:,Ȧ\xa4V\xda\xc4+\x1er\t\xc5\x00\x96\xac\x1ck'\x7f\xed\xe9˦\x90\xe9\xa2K\xaeF\u07fb\x1c\xa1ȫH\xe2wLu!35\xe3̔~C\x90\x0f{\x14vRs#Ց\b\xfb\xd48u\x83\x93\x16\xa1\xaf`\xa2\xc0\xe6\x1e\xf1\xd6n'pQ\x92Ʊwc\n@\x9e\xaacT\x8a\x9d\xa4\x8b\x95\x18\x02\x9e\f\xad \xaf\xd6h\xf2b\x8aL*\xe3\x02\x06\xd0\v)\xb8\x9d\x8a\xba\x95\xb2A6\xd5`\xa1\xf9F\xb0N\xd7\xd2\\\x10\xf8\xa9\x82\xb8\xf2˱C:|\xbdyzM\xff\xc4q\xf2\xa0=/C\x88\xa7[Fh+o\xb6`\xe7\xf5\xe6\tt\xd8>7\x92\xb0Mö\r\xae\xc0(;\x17\xec\xb4\xc3:\xee\x15ߣ\xca\xcdLo\x8e[\x18\xbd\xd0o\x03\xab\x1d\xa8vC/T\x90`\x94r-\x85A\x91\xb3\xd1Y\xaf\xa2/J\xban\x98\xce\xf2<\xe1l\x93\xae\xcf]\x93H\x10\n\xb7\xc2\xd4,\xcf\x17\xf8\xa4\xeb\xe4\x186\xf1\x1e\x9b\xc1\x81\x9b\xfa.\x89\xfc\x05\xbdZ\xa0dyV\x9ep߽8\xb2:#\xcc\xf3\xcb\xda\xc9{I2J7\xf7H\xb6\x1f\x19\xfd\n\xd9\xc6^\x92\x93n\xc2\xe5)\xe1$E\x01\nfX\x82\xedn睂\x0eWX\xcey^\x8c앙\x1e\v}\"\x92\xcc2\x13\x04\xd0\xf9\x81`\xe5Z\x8a\x8a\xef\xe6g\xa7e\xfe\xb9k{V\xb4Y\xc6K\x8e$\x8dS\x82#N\x16\x0e\xe1.b\xf6#lX\xf1\x9dU\xa7\xa2Qű)g\x00\xe6b\x00\xba\xa0\x0f\xc7\xc4=y\xa4\x97,\xe6\xef\x10R\x13d\xef\xbd$\x8dR>\xfd\xcde\x00\n\xdd\x03E\xae\xe1\xd5+\x90\n^\xf9^ѫ\xd7~\xb7\xe5\x8dY\xf0Qyq\xe0M\x13O\xb9)\x83\xf6%\x05\x15t\xd2^J-Y\x1d|\x9aИ\xa8\xc2P\xf1\xe9\xc47\x12\x0e\x8c'\xb0\xbe?]\xbf\xce\xd0\xddbE\x18P\xa1\xb1JP\x16F\xa5\b\x16iGR\xdaL\x1a:#i\xc7\x14\nse\n\xcd\xca\xf9<\xa20\x91ғ\x1f\xe2\x9a\vx\x85Un4\xe0\x1d\xd7\x18 EHq\xc2\xf8\x04\xa8=\xae\x1f\x8cϬ\x89\xa6O,^\x11ru\x83\n\x8b\xe4\x8c\x18\x9e)\x98\x85(\xc6t\xe0\xee\xaaC\x85\x148?\xce9X)\x81Ae\xc9\xd5\xdcaW\x90c=\xae\xedU\xf3\xf4\xfe\x8c0\xb3\xd5\xe7\xb8?cm\x9d\x00\xa0\v\xb6\x9eb%\xe7\xb3\xf4\xffi\xe6N\xc3}F\xf4܍>ǡ+\xd0~\xdc\\\xc3a\xb24rX\xf1\x06A\x1f\xb5\xc1v̭\xaf\xfb\xbc\xe9\xef`\xa8o\xff\xdesC6c\x12\x91W\xa9\xf8\x8e\xd3}\x17\xfd\xccP\v\x04'\rM3\x97H\x1d\x12\xc8:k\x9f\xac\x9d\x7f\x0f\xe4(\x9b\xf8\xc3\tl0Q:\xb8\xdaϗ!\xf2g\xf2\xc6E\x85<\xbf\xac\xaf2\x0f\x1d\x9cA\x124|\xa8yQ\x8f}\x89\xcfs:\x80a_ѕ~7\xb0\x99\x87\x10\x8b|!8Y3\r\xfe\x93\xe9\xf4\x0eM\xa7Ɔ\xce\xce>\xbf\xac\xaf*\x96]\x1f\xef\xbarٿ#\x04-\xc7\xe0\x1a^\x17duW\xc1̊\x02;\x83\xe5\xbb\xe3GY^r\xfa\xb7\xa3\xc5Ĉ\xb8\xa6\x93\x991\xb5\xebm\"\x05\xb6\xdb\xf2ud\xb7\xef\xbf\xdesM\xdfN\x89\xb8N\x9c*\x93|=\xaf_}\xf4;\xcd4\xc0\x17rp\xd7I\xfaΧh\xda\xe6\x12?]\xcf١3\n\xb1\xe5_2\x83\v\xda\x7f\x1f\xc8\xcbw\n\xfc\xf3Kڹ\xbe\xabm0'3\xd7\x1d\x8b\xb9ص\xd4\xe3\xbbONc\x03\xb9^_\x9e\x1a\x96\x80{\x14 \x05T\x8c7\x04\x1d\x1d\xc9L\x00;O%`(\xff\xc8\x17[\x84\x11*d{\xb5\x97-\x99Q\xc2<\x9a\xfd\x9e\xc6\xec+\x98Ϩm\x93\xc1r\xbfc\x05\xe3\x8f\xf4\xcd*\x9d\xad`\xcewS\x18a\"剄\xb8q*h]\xad\xa4lY3}\x1a\xbb\xd44\x9a,\x87Z6\xc1\xa9\x85m\xb7\xa8\x88[\xf7@\a\x02\x0f\x04L\x8b\x9a\x89]\x16\t\xc5\a&\x84\x86is\n,\xe6^\xf8\xa6\x92\xa5/r\xc3ע\xd6lw)X\x7f\xf0\xab<\n\r[\x80m\xa9@\x19k\xfd;\x1dr\xc8M\x91X\\N\x177%\x89\xd1s\xd7͜|\xda\\\xc1˧\r\x1d\xf2i\xf3[yAa\xdb\\˂*\x95\xccpÅ\xfd%3~\u0894\x87y\xe88[ę\xfa\x82\xa0\xcf\xcc\xd4=H\xa6Z\x85\xf6̰|@\x9d[\xa4\x98\xf8\xad \xbdk\xea^b\x8f\xd6\xe4 \f^\x13\x0eNi\xfe#\x1e2\xa31\xe5f\xa6\x9eC\x1e\xcfL\xcd~\x9a\x91N\xfa\xbey.\\ƹ,\xcd\xfe\xd7\x0f\x99\xb9\x1f]\x82\xbbIρ\xbf\xbb\x8a\xf8\u0601\x1f\xe2\x9b\xfb1\xc3,ʍ;\x81TR$\x16\xcb\x10N\xf6\xf7u\x8c\xa3\xb4\x84/5\xd7\xf1\xcd 6BJ\xae\xbb\x86\x1d{Y.\xa5\x8d>nM߂\xe7Nr\xbe\xd9\xde\xff\x86$\xdf(=\x1f\x95\xe1Bdv\xf3\xf2t\xca\xf9\x16'\x9c\xc9yC\x8b\xe1ʚ\xff\xe9}\xbc\x8a\xbcDaxœ\xf7\xf7\xa1Xs\xef99]N߱n\xab/G\xbf,\xba\xab\xde\x1eQ\xb8\x80D\xc3\x0f\x9drxoC\xc1\x80B\x90{\xf1]O\x7f\xe3\xf1\xba\xcf\xe8̄֎O\xfe\xb9\"V\n\x827\x0e\x1e\xdd\x0e-\xc7\x02\xfd\x91\xa82\xebU\xb3A\xc7y\x99\xd0\x0e]\xfat\xc4n\xfb\xdf\x01\xac\xe0\xbf\xff\x7f\xf85\x00\x00\xff\xff\x02\xf2+ܩ(\x00\x00"), } var CRDs = crds() diff --git a/pkg/apis/velero/shared/constants.go b/pkg/apis/velero/shared/constants.go new file mode 100644 index 000000000..12f8b51ee --- /dev/null +++ b/pkg/apis/velero/shared/constants.go @@ -0,0 +1,22 @@ +/* +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 shared + +const ( + DataUploadParentSnapshotNone = "none" + DataUploadParentSnapshotAuto = "auto" +) diff --git a/pkg/apis/velero/v2alpha1/data_upload_types.go b/pkg/apis/velero/v2alpha1/data_upload_types.go index ac57ad89d..56225f387 100644 --- a/pkg/apis/velero/v2alpha1/data_upload_types.go +++ b/pkg/apis/velero/v2alpha1/data_upload_types.go @@ -64,6 +64,12 @@ type DataUploadSpec struct { // SourceFSType is the file system type of the source volume. // +optional SourceFSType string `json:"sourceFSType,omitempty"` + + // ParentSnapshot specifies the parent snapshot that current backup is based on. + // If its value is "" or "auto", the data mover finds the recent backup of the same volume as parent. + // If its value is "none", the data mover will do a full backup + // If its value is a specific snapshotID, the data mover finds the specific snapshot as parent. + ParentSnapshot string `json:"parentSnapshot,omitempty"` } type SnapshotType string diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index 66c14b820..259ec5783 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -42,6 +42,7 @@ import ( crclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + veleroshared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" veleroclient "github.com/vmware-tanzu/velero/pkg/client" @@ -535,6 +536,16 @@ func newDataUpload( vsc *snapshotv1api.VolumeSnapshotContent, fsType string, ) *velerov2alpha1.DataUpload { + var parentSnapshot string + switch backup.Spec.BackupType { + case velerov1api.BackupTypeFull: + parentSnapshot = veleroshared.DataUploadParentSnapshotNone + case velerov1api.BackupTypeIncremental: + parentSnapshot = veleroshared.DataUploadParentSnapshotAuto + default: + parentSnapshot = veleroshared.DataUploadParentSnapshotAuto + } + dataUpload := &velerov2alpha1.DataUpload{ TypeMeta: metav1.TypeMeta{ APIVersion: velerov2alpha1.SchemeGroupVersion.String(), @@ -572,6 +583,7 @@ func newDataUpload( SourceNamespace: pvc.Namespace, OperationTimeout: backup.Spec.CSISnapshotTimeout, SourceFSType: fsType, + ParentSnapshot: parentSnapshot, }, } diff --git a/pkg/backup/actions/csi/pvc_action_test.go b/pkg/backup/actions/csi/pvc_action_test.go index e7320cd1a..9c8405efc 100644 --- a/pkg/backup/actions/csi/pvc_action_test.go +++ b/pkg/backup/actions/csi/pvc_action_test.go @@ -23,40 +23,39 @@ import ( "testing" "time" - "github.com/vmware-tanzu/velero/pkg/kuberesource" - - volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" - "github.com/stretchr/testify/assert" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" - - "github.com/vmware-tanzu/velero/pkg/label" - + "github.com/cockroachdb/errors" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" - - "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" storagev1api "k8s.io/api/storage/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/utils/ptr" crclient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" + veleroshared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/builder" factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks" + "github.com/vmware-tanzu/velero/pkg/kuberesource" + "github.com/vmware-tanzu/velero/pkg/label" "github.com/vmware-tanzu/velero/pkg/plugin/velero" velerotest "github.com/vmware-tanzu/velero/pkg/test" + uploaderUtil "github.com/vmware-tanzu/velero/pkg/uploader/util" + "github.com/vmware-tanzu/velero/pkg/util/boolptr" ) const testDriver = "csi.example.com" @@ -163,6 +162,7 @@ func TestExecute(t *testing.T) { SourcePVC: "testPVC", SourceNamespace: "velero", OperationTimeout: metav1.Duration{Duration: 1 * time.Minute}, + ParentSnapshot: veleroshared.DataUploadParentSnapshotAuto, }, }, }, @@ -2176,3 +2176,131 @@ func TestGetOrCreateVolumeHelper(t *testing.T) { // The pvcPodCache should be the same instance require.Same(t, cache1, action.pvcPodCache, "Expected same pvcPodCache instance on repeated calls") } + +func TestNewDataUpload(t *testing.T) { + tests := []struct { + name string + backupType velerov1api.BackupType + vsClassName *string + uploaderConfig *velerov1api.UploaderConfigForBackup + expectedParentSnap string + expectedDataMoverCfg map[string]string + }{ + { + name: "Full backup type, no uploader config, no vs class name", + backupType: velerov1api.BackupTypeFull, + vsClassName: nil, + uploaderConfig: nil, + expectedParentSnap: "none", + expectedDataMoverCfg: nil, + }, + { + name: "Incremental backup type, with uploader config, with vs class name", + backupType: velerov1api.BackupTypeIncremental, + vsClassName: ptr.To("test-vs-class"), + uploaderConfig: &velerov1api.UploaderConfigForBackup{ParallelFilesUpload: 10}, + expectedParentSnap: "auto", + expectedDataMoverCfg: map[string]string{ + uploaderUtil.ParallelFilesUpload: "10", + }, + }, + { + name: "Default backup type, uploader config with 0 parallel files", + backupType: "", + vsClassName: ptr.To("test-vs-class"), + uploaderConfig: &velerov1api.UploaderConfigForBackup{ParallelFilesUpload: 0}, + expectedParentSnap: "auto", + expectedDataMoverCfg: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + backup := &velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-backup", + Namespace: "velero", + UID: types.UID("backup-uid"), + }, + Spec: velerov1api.BackupSpec{ + BackupType: tc.backupType, + DataMover: "velero", + StorageLocation: "default", + CSISnapshotTimeout: metav1.Duration{Duration: 10 * time.Minute}, + UploaderConfig: tc.uploaderConfig, + }, + } + + vs := &snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-vs", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: tc.vsClassName, + }, + } + + pvc := &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + UID: types.UID("pvc-uid"), + }, + Spec: corev1api.PersistentVolumeClaimSpec{ + StorageClassName: ptr.To("test-storage-class"), + }, + } + + vsc := &snapshotv1api.VolumeSnapshotContent{ + Spec: snapshotv1api.VolumeSnapshotContentSpec{ + Driver: "test-driver", + }, + } + + operationID := "test-op-id" + fsType := "ext4" + + du := newDataUpload(backup, vs, pvc, operationID, vsc, fsType) + + require.NotNil(t, du) + assert.Equal(t, velerov2alpha1.SchemeGroupVersion.String(), du.APIVersion) + assert.Equal(t, "DataUpload", du.Kind) + assert.Equal(t, backup.Namespace, du.Namespace) + assert.Equal(t, backup.Name+"-", du.GenerateName) + + require.Len(t, du.OwnerReferences, 1) + assert.Equal(t, velerov1api.SchemeGroupVersion.String(), du.OwnerReferences[0].APIVersion) + assert.Equal(t, "Backup", du.OwnerReferences[0].Kind) + assert.Equal(t, backup.Name, du.OwnerReferences[0].Name) + assert.Equal(t, backup.UID, du.OwnerReferences[0].UID) + assert.Equal(t, boolptr.True(), du.OwnerReferences[0].Controller) + + expectedLabels := map[string]string{ + velerov1api.BackupNameLabel: label.GetValidName(backup.Name), + velerov1api.BackupUIDLabel: string(backup.UID), + velerov1api.PVCUIDLabel: string(pvc.UID), + velerov1api.AsyncOperationIDLabel: operationID, + } + assert.Equal(t, expectedLabels, du.Labels) + + assert.Equal(t, velerov2alpha1.SnapshotTypeCSI, du.Spec.SnapshotType) + assert.Equal(t, vs.Name, du.Spec.CSISnapshot.VolumeSnapshot) + assert.Equal(t, *pvc.Spec.StorageClassName, du.Spec.CSISnapshot.StorageClass) + assert.Equal(t, vsc.Spec.Driver, du.Spec.CSISnapshot.Driver) + if tc.vsClassName != nil { + assert.Equal(t, *tc.vsClassName, du.Spec.CSISnapshot.SnapshotClass) + } else { + assert.Empty(t, du.Spec.CSISnapshot.SnapshotClass) + } + + assert.Equal(t, pvc.Name, du.Spec.SourcePVC) + assert.Equal(t, backup.Spec.DataMover, du.Spec.DataMover) + assert.Equal(t, backup.Spec.StorageLocation, du.Spec.BackupStorageLocation) + assert.Equal(t, pvc.Namespace, du.Spec.SourceNamespace) + assert.Equal(t, backup.Spec.CSISnapshotTimeout, du.Spec.OperationTimeout) + assert.Equal(t, fsType, du.Spec.SourceFSType) + assert.Equal(t, tc.expectedParentSnap, du.Spec.ParentSnapshot) + assert.Equal(t, tc.expectedDataMoverCfg, du.Spec.DataMoverConfig) + }) + } +} diff --git a/pkg/datamover/backup_micro_service.go b/pkg/datamover/backup_micro_service.go index 08a005217..cb5aeb3fe 100644 --- a/pkg/datamover/backup_micro_service.go +++ b/pkg/datamover/backup_micro_service.go @@ -204,7 +204,7 @@ func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string, if err := dp.StartBackup(r.sourceTargetPath, du.Spec.DataMoverConfig, &datapath.BackupStartParam{ RealSource: GetRealSource(du.Spec.SourceNamespace, du.Spec.SourcePVC), - ParentSnapshot: "", + ParentSnapshot: du.Spec.ParentSnapshot, ForceFull: false, Tags: tags, VolumeID: r.volumeID, diff --git a/pkg/datamover/util.go b/pkg/datamover/util.go index ed66d497a..c82184f31 100644 --- a/pkg/datamover/util.go +++ b/pkg/datamover/util.go @@ -19,12 +19,15 @@ package datamover import ( "fmt" + "github.com/vmware-tanzu/velero/pkg/uploader" datamoverutil "github.com/vmware-tanzu/velero/pkg/util/datamover" ) func GetUploaderType(dataMover string) string { - if datamoverutil.IsBuiltInDataMover(dataMover) { - return "kopia" + if datamoverutil.IsVeleroFSDataMover(dataMover) { + return uploader.KopiaType + } else if datamoverutil.IsVeleroBlockDataMover(dataMover) { + return uploader.BlockType } else { return dataMover } diff --git a/pkg/datamover/util_test.go b/pkg/datamover/util_test.go index d44f3c307..d29b3de12 100644 --- a/pkg/datamover/util_test.go +++ b/pkg/datamover/util_test.go @@ -22,6 +22,16 @@ func TestGetUploaderType(t *testing.T) { input: "velero", want: "kopia", }, + { + name: "velero-fs dataMover is kopia", + input: "velero-fs", + want: "kopia", + }, + { + name: "velero-block dataMover is velero-block", + input: "velero-block", + want: "velero-block", + }, { name: "kopia dataMover is kopia", input: "kopia", diff --git a/pkg/util/datamover/datamover.go b/pkg/util/datamover/datamover.go index 59dd1499b..b6d965d60 100644 --- a/pkg/util/datamover/datamover.go +++ b/pkg/util/datamover/datamover.go @@ -32,7 +32,18 @@ const ( // IsBuiltInDataMover reports whether the given data mover value refers to a // Velero built-in data mover (an empty value or the default "velero" alias). func IsBuiltInDataMover(dataMover string) bool { - return dataMover == "" || dataMover == DataMoverTypeVelero + return IsVeleroBlockDataMover(dataMover) || IsVeleroFSDataMover(dataMover) +} + +func IsVeleroFSDataMover(dataMover string) bool { + if dataMover == "" || dataMover == DataMoverTypeVelero { + dataMover = DataMoverTypeVeleroFs + } + return dataMover == DataMoverTypeVeleroFs +} + +func IsVeleroBlockDataMover(dataMover string) bool { + return dataMover == DataMoverTypeVeleroBlock } // GetDefaultBuiltInDataMover returns the data mover used when the default diff --git a/pkg/util/datamover/datamover_test.go b/pkg/util/datamover/datamover_test.go index 8576aed0e..94585e8f9 100644 --- a/pkg/util/datamover/datamover_test.go +++ b/pkg/util/datamover/datamover_test.go @@ -38,6 +38,16 @@ func TestIsBuiltInDataMover(t *testing.T) { dataMover: "velero", want: true, }, + { + name: "velero-fs dataMover is builtin", + dataMover: "velero-fs", + want: true, + }, + { + name: "velero-block dataMover is builtin", + dataMover: "velero-block", + want: true, + }, { name: "kopia dataMover is not builtin", dataMover: "kopia", @@ -54,3 +64,61 @@ func TestIsBuiltInDataMover(t *testing.T) { func TestGetDefaultBuiltInDataMover(t *testing.T) { assert.Equal(t, DataMoverTypeVeleroFs, GetDefaultBuiltInDataMover()) } + +func TestIsFSDataMover(t *testing.T) { + testcases := []struct { + name string + dataMover string + want bool + }{ + { + name: "empty dataMover is fs", + dataMover: "", + want: true, + }, + { + name: "velero dataMover is fs", + dataMover: "velero", + want: true, + }, + { + name: "velero-fs dataMover is fs", + dataMover: "velero-fs", + want: true, + }, + { + name: "velero-block dataMover is not fs", + dataMover: "velero-block", + want: false, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + assert.Equal(tt, tc.want, IsVeleroFSDataMover(tc.dataMover)) + }) + } +} + +func TestIsBlockDataMover(t *testing.T) { + testcases := []struct { + name string + dataMover string + want bool + }{ + { + name: "velero-block dataMover is block", + dataMover: "velero-block", + want: true, + }, + { + name: "velero-fs dataMover is not block", + dataMover: "velero-fs", + want: false, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + assert.Equal(tt, tc.want, IsVeleroBlockDataMover(tc.dataMover)) + }) + } +} From a43a1bce6a5e92d942bbb389823a18cad3a45bd9 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Tue, 28 Jul 2026 04:36:55 +0800 Subject: [PATCH 106/194] Add RIA must-include additional items (#10082) Let RestoreItemActions opt in via annotation to bypass global restore filters for AdditionalItems, mirroring the backup-side must-include behavior. Signed-off-by: Adam Zhang --- changelogs/unreleased/10082-adam-jian-zhang | 1 + pkg/apis/velero/v1/labels_annotations.go | 8 + pkg/restore/restore.go | 71 ++-- pkg/restore/restore_test.go | 414 ++++++++++++++++++++ site/content/docs/main/custom-plugins.md | 26 ++ 5 files changed, 497 insertions(+), 23 deletions(-) create mode 100644 changelogs/unreleased/10082-adam-jian-zhang diff --git a/changelogs/unreleased/10082-adam-jian-zhang b/changelogs/unreleased/10082-adam-jian-zhang new file mode 100644 index 000000000..e704ed6a7 --- /dev/null +++ b/changelogs/unreleased/10082-adam-jian-zhang @@ -0,0 +1 @@ +Add restore.velero.io/must-include-additional-items so RestoreItemActions can opt in to bypassing global restore filters for AdditionalItems (mirrors the backup-side must-include annotation; no default behavior change for existing restores/plugins) diff --git a/pkg/apis/velero/v1/labels_annotations.go b/pkg/apis/velero/v1/labels_annotations.go index 13da279d8..b34f05ed9 100644 --- a/pkg/apis/velero/v1/labels_annotations.go +++ b/pkg/apis/velero/v1/labels_annotations.go @@ -166,6 +166,14 @@ const ( // Velero checks this annotation to determine whether to skip resource excluding check. MustIncludeAdditionalItemAnnotation = "backup.velero.io/must-include-additional-items" + // MustIncludeAdditionalItemRestoreAnnotation is set by RestoreItemActions on the UpdatedItem + // to tell Velero to bypass global resource/namespace exclusion checks (and IncludeClusterResources=false) + // for that action's AdditionalItems. Value must be "true" to enable the bypass. The annotation is + // always stripped before the item is applied to the cluster when present, including non-"true" values. + // + // Notice: SkipRestore on the Execute output takes precedence. If SkipRestore is true, the + // annotation is never inspected and AdditionalItems are not processed. + MustIncludeAdditionalItemRestoreAnnotation = "restore.velero.io/must-include-additional-items" // SkippedNoCSIPVAnnotation - Velero checks this annotation on processed PVC to // find out if the snapshot was skipped b/c the PV is not provisioned via CSI SkippedNoCSIPVAnnotation = "backup.velero.io/skipped-no-csi-pv" diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index bc452b49c..7ba9ae6fd 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -1060,7 +1060,7 @@ func (ctx *restoreContext) processSelectedResource( continue } - w, e, _ := ctx.restoreItem(obj, groupResource, targetNS) + w, e, _ := ctx.restoreItem(obj, groupResource, targetNS, false) warnings.Merge(&w) errs.Merge(&e) processedItems++ @@ -1386,7 +1386,7 @@ func (ctx *restoreContext) getResource(groupResource schema.GroupResource, obj * return u, nil } -func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupResource schema.GroupResource, namespace string) (results.Result, results.Result, bool) { +func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupResource schema.GroupResource, namespace string, mustInclude bool) (results.Result, results.Result, bool) { warnings, errs := results.Result{}, results.Result{} // itemExists bool is used to determine whether to include this item in the "wait for additional items" list itemExists := false @@ -1403,27 +1403,41 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso // Check if group/resource should be restored. We need to do this here since // this method may be getting called for an additional item which is a group/resource // that's excluded. - if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) && !ctx.resourceMustHave.Has(groupResource.String()) { - restoreLogger.Info("Not restoring item because resource is excluded") - return warnings, errs, itemExists - } - - // Check if namespace/cluster-scoped resource should be restored. We need - // to do this here since this method may be getting called for an additional - // item which is in a namespace that's excluded, or which is cluster-scoped - // and should be excluded. Note that we're checking the object's namespace ( - // via obj.GetNamespace()) instead of the namespace parameter, because we want - // to check the *original* namespace, not the remapped one if it's been remapped. // // Note: Additional items intentionally bypass fine-grained resource filter policies // (like per-namespace label/name selectors) to avoid breaking semantic dependencies, - // but they must still pass the global exclusions enforced below. - if namespace != "" { - if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) { - restoreLogger.Info("Not restoring item because namespace is excluded") + // but they must still pass the global exclusions enforced below unless mustInclude is set. + if mustInclude { + restoreLogger.Info("Skipping the resource/namespace exclusion checks because the item is marked as must-include") + } else { + if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) && !ctx.resourceMustHave.Has(groupResource.String()) { + restoreLogger.Info("Not restoring item because resource is excluded") return warnings, errs, itemExists } + // Check if namespace/cluster-scoped resource should be restored. We need + // to do this here since this method may be getting called for an additional + // item which is in a namespace that's excluded, or which is cluster-scoped + // and should be excluded. Note that we're checking the object's namespace ( + // via obj.GetNamespace()) instead of the namespace parameter, because we want + // to check the *original* namespace, not the remapped one if it's been remapped. + if namespace != "" { + if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) { + restoreLogger.Info("Not restoring item because namespace is excluded") + return warnings, errs, itemExists + } + } else { + if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) { + restoreLogger.Info("Not restoring item because it's cluster-scoped") + return warnings, errs, itemExists + } + } + } + + // Namespace creation runs unconditionally when namespace != "", regardless of + // mustInclude. This ensures target namespaces exist for additional items that + // bypass the namespace-exclusion check above. + if namespace != "" { // If the namespace scoped resource should be restored, ensure that the // namespace into which the resource is being restored into exists. // This is the *remapped* namespace that we are ensuring exists. @@ -1442,11 +1456,6 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } ctx.restoredItems[itemKey] = restoredItemStatus{action: ItemRestoreResultCreated, itemExists: true, createdName: nsToEnsure.Name} } - } else { - if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) { - restoreLogger.Info("Not restoring item because it's cluster-scoped") - return warnings, errs, itemExists - } } // Make a copy of object retrieved from backup to make it available unchanged @@ -1668,6 +1677,21 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso obj = unstructuredObj + mustIncludeAdditionalItems := false + if annotations := obj.GetAnnotations(); annotations != nil { + if _, present := annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation]; present { + // Only the string value "true" enables the bypass. + if annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] == "true" { + mustIncludeAdditionalItems = true + restoreLogger.Info("RestoreItemAction marked additional items as must-include; bypassing resource/namespace exclusion checks for them") + } + // Always strip the annotation so it never lands on the cluster, + // regardless of whether the value enabled the bypass. + delete(annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation) + obj.SetAnnotations(annotations) + } + } + var filteredAdditionalItems []velero.ResourceIdentifier for _, additionalItem := range executeOutput.AdditionalItems { itemPath := archive.GetItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name) @@ -1687,6 +1711,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso additionalObj, err := archive.Unmarshal(ctx.fileSystem, itemPath) if err != nil { errs.Add(namespace, errors.Wrapf(err, "error restoring additional item %s", additionalResourceID)) + continue } additionalItemNamespace := additionalItem.Namespace @@ -1696,7 +1721,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } } - w, e, additionalItemExists := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace) + w, e, additionalItemExists := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace, mustIncludeAdditionalItems) if additionalItemExists { filteredAdditionalItems = append(filteredAdditionalItems, additionalItem) } diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index fc4051387..9d46c3e53 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -2150,6 +2150,102 @@ func TestRestoreActionAdditionalItems(t *testing.T) { test.PVs(): nil, }, }, + { + name: "must-include annotation bypasses resource exclusion for additional items", + restore: defaultRestore().IncludedResources("pods").Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). + Done(), + apiResources: []*test.APIResource{test.Pods(), test.PVs()}, + actions: []riav2.RestoreItemAction{ + &pluggableAction{ + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"}, + }, + }, nil + }, + }, + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1"}, + test.PVs(): {"/pv-1"}, + }, + }, + { + name: "must-include annotation bypasses namespace exclusion for additional items", + restore: defaultRestore().IncludedNamespaces("ns-1").Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()).Done(), + apiResources: []*test.APIResource{test.Pods()}, + actions: []riav2.RestoreItemAction{ + &pluggableAction{ + selector: velero.ResourceSelector{IncludedNamespaces: []string{"ns-1"}}, + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.Pods, Namespace: "ns-2", Name: "pod-2"}, + }, + }, nil + }, + }, + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1", "ns-2/pod-2"}, + }, + }, + { + name: "must-include annotation bypasses IncludeClusterResources=false for additional items", + restore: defaultRestore().IncludeClusterResources(false).Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). + Done(), + apiResources: []*test.APIResource{test.Pods(), test.PVs()}, + actions: []riav2.RestoreItemAction{ + &pluggableAction{ + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"}, + }, + }, nil + }, + }, + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1"}, + test.PVs(): {"/pv-1"}, + }, + }, } for _, tc := range tests { @@ -2180,6 +2276,324 @@ func TestRestoreActionAdditionalItems(t *testing.T) { } } +// TestRestoreMustIncludeAdditionalItems covers restore must-include edge cases beyond the +// basic filter-bypass cases in TestRestoreActionAdditionalItems. +func TestRestoreMustIncludeAdditionalItems(t *testing.T) { + t.Run("must-include annotation is stripped from the restored item", func(t *testing.T) { + h := newHarness(t) + h.AddItems(t, test.Pods()) + + data := &Request{ + Log: h.log, + Restore: defaultRestore().Result(), + Backup: defaultBackup().Result(), + BackupReader: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + Done(), + } + warnings, errs := h.restorer.Restore( + data, + []riav2.RestoreItemAction{ + &pluggableAction{ + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + annotations["keep-me"] = "yes" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{UpdatedItem: item}, nil + }, + }, + }, + nil, + ) + + assertEmptyResults(t, warnings, errs) + + got, err := h.DynamicClient.Resource(test.Pods().GVR()).Namespace("ns-1").Get(t.Context(), "pod-1", metav1.GetOptions{}) + require.NoError(t, err) + annotations := got.GetAnnotations() + assert.NotContains(t, annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation) + assert.Equal(t, "yes", annotations["keep-me"]) + }) + + t.Run("non-true must-include annotation is stripped without bypassing filters", func(t *testing.T) { + h := newHarness(t) + h.AddItems(t, test.Pods()) + h.AddItems(t, test.PVs()) + + data := &Request{ + Log: h.log, + Restore: defaultRestore().IncludedResources("pods").Result(), + Backup: defaultBackup().Result(), + BackupReader: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). + Done(), + } + warnings, errs := h.restorer.Restore( + data, + []riav2.RestoreItemAction{ + &pluggableAction{ + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "True" + annotations["keep-me"] = "yes" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"}, + }, + }, nil + }, + }, + }, + nil, + ) + + assertEmptyResults(t, warnings, errs) + assertAPIContents(t, h, map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1"}, + test.PVs(): nil, + }) + + got, err := h.DynamicClient.Resource(test.Pods().GVR()).Namespace("ns-1").Get(t.Context(), "pod-1", metav1.GetOptions{}) + require.NoError(t, err) + annotations := got.GetAnnotations() + assert.NotContains(t, annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation) + assert.Equal(t, "yes", annotations["keep-me"]) + }) + + t.Run("SkipRestore supersedes must-include annotation and skips additional items", func(t *testing.T) { + h := newHarness(t) + h.AddItems(t, test.Pods()) + h.AddItems(t, test.PVs()) + + data := &Request{ + Log: h.log, + Restore: defaultRestore().IncludedResources("pods").Result(), + Backup: defaultBackup().Result(), + BackupReader: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). + Done(), + } + warnings, errs := h.restorer.Restore( + data, + []riav2.RestoreItemAction{ + &pluggableAction{ + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + SkipRestore: true, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"}, + }, + }, nil + }, + }, + }, + nil, + ) + + assertEmptyResults(t, warnings, errs) + assertAPIContents(t, h, map[*test.APIResource][]string{ + test.Pods(): nil, + test.PVs(): nil, + }) + }) + + t.Run("must-include does not restore additional items missing from the backup tarball", func(t *testing.T) { + h := newHarness(t) + h.AddItems(t, test.Pods()) + h.AddItems(t, test.PVs()) + + data := &Request{ + Log: h.log, + Restore: defaultRestore().IncludedResources("pods").Result(), + Backup: defaultBackup().Result(), + BackupReader: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + Done(), + } + warnings, errs := h.restorer.Restore( + data, + []riav2.RestoreItemAction{ + &pluggableAction{ + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "pv-missing"}, + }, + }, nil + }, + }, + }, + nil, + ) + + assertEmptyResults(t, errs) + assertNonEmptyResults(t, "warning", warnings) + assertAPIContents(t, h, map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1"}, + test.PVs(): nil, + }) + }) + + t.Run("transitive must-include requires each RIA level to re-set the annotation", func(t *testing.T) { + h := newHarness(t) + h.AddItems(t, test.Pods()) + h.AddItems(t, test.PVs()) + h.AddItems(t, test.PVCs()) + + data := &Request{ + Log: h.log, + Restore: defaultRestore().IncludedResources("pods").Result(), + Backup: defaultBackup().Result(), + BackupReader: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-1").Result()). + Done(), + } + warnings, errs := h.restorer.Restore( + data, + []riav2.RestoreItemAction{ + // Parent pod RIA force-includes the excluded PV. + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"pods"}}, + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"}, + }, + }, nil + }, + }, + // Child PV RIA also re-sets the annotation to force-include an excluded PVC. + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumes"}}, + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns-2", Name: "pvc-1"}, + }, + }, nil + }, + }, + }, + nil, + ) + + assertEmptyResults(t, warnings, errs) + assertAPIContents(t, h, map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1"}, + test.PVs(): {"/pv-1"}, + test.PVCs(): {"ns-2/pvc-1"}, + }) + }) + + t.Run("without re-annotating, transitive additional items still respect filters", func(t *testing.T) { + h := newHarness(t) + h.AddItems(t, test.Pods()) + h.AddItems(t, test.PVs()) + h.AddItems(t, test.PVCs()) + + data := &Request{ + Log: h.log, + Restore: defaultRestore().IncludedResources("pods").Result(), + Backup: defaultBackup().Result(), + BackupReader: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-1").Result()). + Done(), + } + warnings, errs := h.restorer.Restore( + data, + []riav2.RestoreItemAction{ + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"pods"}}, + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"}, + }, + }, nil + }, + }, + // Child PV RIA returns an additional PVC but does NOT set must-include. + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumes"}}, + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: input.Item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns-2", Name: "pvc-1"}, + }, + }, nil + }, + }, + }, + nil, + ) + + assertEmptyResults(t, warnings, errs) + assertAPIContents(t, h, map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1"}, + test.PVs(): {"/pv-1"}, + test.PVCs(): nil, + }) + }) +} + // TestShouldRestore runs the ShouldRestore function for various permutations of // existing/nonexisting/being-deleted PVs, PVCs, and namespaces, and verifies the // result/error matches expectations. diff --git a/site/content/docs/main/custom-plugins.md b/site/content/docs/main/custom-plugins.md index b0881d579..106ebfd0f 100644 --- a/site/content/docs/main/custom-plugins.md +++ b/site/content/docs/main/custom-plugins.md @@ -65,6 +65,32 @@ order in which item action plugins are invoked. However, if a single binary impl they may be invoked in the order in which they are registered but it is best to not depend on this implementation. This is not guaranteed officially and the implementation can change at any time. +### Must-include additional items (Restore Item Actions) + +Restore Item Actions may return `AdditionalItems` that Velero restores as dependencies of the current item. +By default those additional items must still pass the restore's global resource and namespace include/exclude +filters (and `IncludeClusterResources=false` for cluster-scoped resources). + +To force-restore hard dependencies despite those filters, set the following annotation on the `UpdatedItem` +returned from `Execute()`: + +``` +restore.velero.io/must-include-additional-items: "true" +``` + +Behavior: +- Only the string value `"true"` enables the bypass. +- The annotation applies blanket to all `AdditionalItems` from that RIA invocation (not per-item). +- Velero strips the annotation before applying the item to the cluster. +- `SkipRestore: true` takes precedence: if set, the annotation is never inspected and `AdditionalItems` are not processed. +- Must-include only bypasses filters; the additional item must still exist in the backup tarball. +- When an additional item targets an excluded namespace, Velero may still create that target namespace so the item can be restored. +- Cluster-scoped additional items are restored even when `IncludeClusterResources=false`. +- Transitive force-include requires each RIA level to re-set the annotation on its own `UpdatedItem`. + +This mirrors the backup-side annotation `backup.velero.io/must-include-additional-items` used by Backup Item Actions. +Installing an RIA that sets this annotation is a trust decision: the plugin can restore resources outside the operator's restore filters. + ## Plugin Logging Velero provides a [logger][2] that can be used by plugins to log structured information to the main Velero server log or From c95597720a0e5705b4581e5af6c14fb41e909fde Mon Sep 17 00:00:00 2001 From: Chlins Zhang Date: Tue, 28 Jul 2026 07:34:38 +0800 Subject: [PATCH 107/194] build(image): remove kubectl installation from build image (#10065) Signed-off-by: chlins --- hack/build-image/Dockerfile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hack/build-image/Dockerfile b/hack/build-image/Dockerfile index aa725da03..4f34ba470 100644 --- a/hack/build-image/Dockerfile +++ b/hack/build-image/Dockerfile @@ -102,10 +102,5 @@ RUN ARCH=$(go env GOARCH) && \ # release API/CDN, which has been returning intermittent/persistent HTTP 504s. RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 -# install kubectl -RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/$(go env GOARCH)/kubectl -RUN chmod +x ./kubectl -RUN mv ./kubectl /usr/local/bin - # Fix the "dubious ownership" issue from git when running goreleaser.sh RUN echo "[safe] \n\t directory = *" > /.gitconfig From b635d3f8ed6acabc73fe973eaaccd85312598f26 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 28 Jul 2026 10:51:58 +0800 Subject: [PATCH 108/194] refactor block uploader thread module Signed-off-by: Lyndon-Li --- changelogs/unreleased/10091-Lyndon-Li | 1 + pkg/uploader/block/uploader.go | 385 +++++++++++++++----------- pkg/uploader/block/uploader_test.go | 13 +- 3 files changed, 228 insertions(+), 171 deletions(-) create mode 100644 changelogs/unreleased/10091-Lyndon-Li diff --git a/changelogs/unreleased/10091-Lyndon-Li b/changelogs/unreleased/10091-Lyndon-Li new file mode 100644 index 000000000..b1e5deb03 --- /dev/null +++ b/changelogs/unreleased/10091-Lyndon-Li @@ -0,0 +1 @@ +Refactor block uploader thread module for better thread safety and code reading \ No newline at end of file diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 8aa58bf96..af31c71e7 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -24,6 +24,7 @@ import ( "runtime" "strconv" "strings" + "sync" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" @@ -213,110 +214,30 @@ func (blkup *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.Object blockSize := bitmap.BlockSize() list := freelist.New(bufferSize, int(blockSize)) resultChan := make(chan readResult, list.Capacity()) - totalCount := bitmap.Count() - aligned := (totalLength + int64(blockSize) - 1) / int64(blockSize) * int64(blockSize) - quit := make(chan struct{}) - defer close(quit) + aligned := (totalLength + int64(blockSize) - 1) / int64(blockSize) * int64(blockSize) + wg := &sync.WaitGroup{} + var writeErr error + var written int64 + var lastPos int64 + + wg.Add(2) go func() { - defer close(resultChan) - - offset, valid := bitmap.Next() - var buffer []byte - for valid { - select { - case <-blkup.ctx.Done(): - return - case <-quit: - return - case buffer = <-list.Chunks(): - } - - length := blockSize - if offset+uint64(length) > uint64(totalLength) { - length = uint(uint64(totalLength) - offset) - clear(buffer) - } - - readBytes, err := reader.ReadAt(buffer[:length], int64(offset)) - if err == nil && readBytes <= 0 { - err = io.ErrUnexpectedEOF - } - - r := readResult{ - buffer: buffer, - offset: int64(offset), - err: err, - } - - if r.err != nil { - r.resetBuffer(list) - } - - resultChan <- r - - if r.err != nil { - return - } - - offset, valid = bitmap.Next() - } + defer wg.Done() + backupReadProc(blkup.ctx, reader, resultChan, quit, bitmap, list, totalLength) }() - var lastPos int64 - var result readResult - var written int64 - var curCount int64 - var writeErr error - var readerRunning bool + go func() { + defer wg.Done() + defer close(quit) + written, lastPos, writeErr = backupWriteProc(blkup.ctx, writer, resultChan, list, aligned, int64(bitmap.Count()), int(blockSize), blkup.progress) + }() - for curCount < int64(totalCount) { - select { - case <-blkup.ctx.Done(): - writeErr = ErrCanceled - case result, readerRunning = <-resultChan: - if !readerRunning { - if blkup.ctx.Err() != nil { - writeErr = ErrCanceled - } else { - writeErr = io.ErrUnexpectedEOF - } - } - } - - if writeErr != nil { - break - } - - if result.err != nil { - writeErr = result.err - break - } - - n, err := writer.WriteAt(result.buffer, result.offset) - if err != nil { - writeErr = err - break - } - - if blockSize != uint(n) { - writeErr = io.ErrShortWrite - break - } - - written += int64(blockSize) - lastPos = result.offset + int64(blockSize) - result.resetBuffer(list) - curCount++ - - blkup.progress.UpdateProgress(&uploader.Progress{BytesDone: lastPos, TotalBytes: aligned}) - } - - result.resetBuffer(list) + wg.Wait() if writeErr != nil { - return written, aligned, writeErr + return written, aligned, errors.Wrap(writeErr, "error writing data") } if lastPos < aligned { @@ -333,6 +254,112 @@ func (blkup *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.Object return written, aligned, nil } +func backupReadProc(ctx context.Context, reader io.ReaderAt, resultChan chan readResult, quit chan struct{}, bitmap cbt.Iterator, list *freelist.FreeList, totalLength int64) { + defer close(resultChan) + + blockSize := bitmap.BlockSize() + offset, valid := bitmap.Next() + var buffer []byte + for valid { + select { + case <-ctx.Done(): + return + case <-quit: + return + case buffer = <-list.Chunks(): + } + + length := blockSize + if offset+uint64(length) > uint64(totalLength) { + length = uint(uint64(totalLength) - offset) + clear(buffer) + } + + readBytes, err := reader.ReadAt(buffer[:length], int64(offset)) + if err == nil && readBytes <= 0 { + err = io.ErrUnexpectedEOF + } + + r := readResult{ + buffer: buffer, + offset: int64(offset), + err: err, + } + + if r.err != nil { + r.resetBuffer(list) + } + + resultChan <- r + + if r.err != nil { + return + } + + offset, valid = bitmap.Next() + } +} + +func backupWriteProc(ctx context.Context, writer udmrepo.ObjectWriter, resultChan chan readResult, list *freelist.FreeList, totalLength int64, + totalCount int64, blockSize int, progress uploader.ProgressUpdater) (int64, int64, error) { + var lastPos int64 + var result readResult + var written int64 + var curCount int64 + var writeErr error + + for { + select { + case <-ctx.Done(): + writeErr = ErrCanceled + case result = <-resultChan: + } + + if writeErr != nil { + break + } + + if result.err != nil { + writeErr = result.err + break + } + + if result.buffer == nil { + break + } + + n, err := writer.WriteAt(result.buffer, result.offset) + if err != nil { + writeErr = err + break + } + + if blockSize != n { + writeErr = io.ErrShortWrite + break + } + + written += int64(blockSize) + lastPos = result.offset + int64(blockSize) + result.resetBuffer(list) + curCount++ + + progress.UpdateProgress(&uploader.Progress{BytesDone: lastPos, TotalBytes: totalLength}) + } + + result.resetBuffer(list) + + if writeErr != nil { + return written, lastPos, writeErr + } + + if curCount < totalCount { + return written, lastPos, io.ErrUnexpectedEOF + } + + return written, lastPos, nil +} + func copyTailData(source io.ReaderAt, writer udmrepo.ObjectWriter, totalLength int64, blockSize int64) (int64, error) { roundUp := (totalLength + blockSize - 1) / blockSize * blockSize roundDown := totalLength / blockSize * blockSize @@ -363,84 +390,104 @@ func getObjectName(source string) string { } func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bitmap cbt.Iterator, totalLength int64, destPath string) (int64, error) { - list := freelist.New(bufferSize, blockSize) + blockSize := bitmap.BlockSize() + list := freelist.New(bufferSize, int(blockSize)) resultChan := make(chan readResult, list.Capacity()) - zeroBlock := make([]byte, blockSize) - totalCount := bitmap.Count() - quit := make(chan struct{}) - defer close(quit) + var writeErr error + var written int64 + + wg := &sync.WaitGroup{} + + wg.Add(2) go func() { - defer close(resultChan) - - offset, valid := bitmap.Next() - var buffer []byte - var nextPos = uint64(0) - for valid { - select { - case <-blkup.ctx.Done(): - return - case <-quit: - return - case buffer = <-list.Chunks(): - } - - var err error - - if nextPos != offset { - _, err = reader.Seek(int64(offset), io.SeekStart) - } - - if err == nil { - var length int - length, err = io.ReadFull(reader, buffer) - if err == nil && length <= 0 { - err = io.ErrUnexpectedEOF - } - } - - r := readResult{ - buffer: buffer, - offset: int64(offset), - err: err, - } - - if r.err != nil { - r.resetBuffer(list) - } - - resultChan <- r - - if r.err != nil { - return - } - - nextPos = offset + uint64(blockSize) - offset, valid = bitmap.Next() - } + defer wg.Done() + restoreReadProc(blkup.ctx, reader, resultChan, quit, bitmap, list) }() + go func() { + defer wg.Done() + defer close(quit) + written, writeErr = restoreWriteProc(blkup.ctx, dest, resultChan, list, totalLength, int64(bitmap.Count()), int(blockSize), destPath, blkup.progress, blkup.log) + }() + + wg.Wait() + + if writeErr != nil { + return written, errors.Wrap(writeErr, "error writing data") + } + + return written, nil +} + +func restoreReadProc(ctx context.Context, reader io.ReadSeeker, resultChan chan readResult, quit chan struct{}, bitmap cbt.Iterator, list *freelist.FreeList) { + defer close(resultChan) + + blockSize := bitmap.BlockSize() + offset, valid := bitmap.Next() + var buffer []byte + var nextPos = uint64(0) + for valid { + select { + case <-ctx.Done(): + return + case <-quit: + return + case buffer = <-list.Chunks(): + } + + var err error + + if nextPos != offset { + _, err = reader.Seek(int64(offset), io.SeekStart) + } + + if err == nil { + var length int + length, err = io.ReadFull(reader, buffer) + if err == nil && length <= 0 { + err = io.ErrUnexpectedEOF + } + } + + r := readResult{ + buffer: buffer, + offset: int64(offset), + err: err, + } + + if r.err != nil { + r.resetBuffer(list) + } + + resultChan <- r + + if r.err != nil { + return + } + + nextPos = offset + uint64(blockSize) + offset, valid = bitmap.Next() + } +} + +func restoreWriteProc(ctx context.Context, dest *os.File, resultChan chan readResult, list *freelist.FreeList, totalLength int64, totalCount int64, + blockSize int, destPath string, progress uploader.ProgressUpdater, log logrus.FieldLogger) (int64, error) { + zeroBlock := make([]byte, blockSize) + var written int64 var result readResult var writeErr error - var readerRunning bool var zeroStart int64 = -1 var zeroLength int64 var curCount int64 - for curCount < int64(totalCount) { + for { select { - case <-blkup.ctx.Done(): + case <-ctx.Done(): writeErr = ErrCanceled - case result, readerRunning = <-resultChan: - if !readerRunning { - if blkup.ctx.Err() != nil { - writeErr = ErrCanceled - } else { - writeErr = io.ErrUnexpectedEOF - } - } + case result = <-resultChan: } if writeErr != nil { @@ -452,6 +499,10 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit break } + if result.buffer == nil { + break + } + length := min(int64(blockSize), totalLength-result.offset) if bytes.Equal(result.buffer, zeroBlock) { if zeroStart == -1 { @@ -460,7 +511,7 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit } else if result.offset == zeroStart+zeroLength { zeroLength += length } else { - if err := blkup.flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath); err != nil { + if err := flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath, log); err != nil { writeErr = errors.Wrapf(err, "error flushing zero blocks from %v, length %v", zeroStart, zeroLength) break } @@ -469,7 +520,7 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit } } else { if zeroStart != -1 { - if err := blkup.flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath); err != nil { + if err := flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath, log); err != nil { writeErr = errors.Wrapf(err, "error flushing zero blocks from %v, length %v", zeroStart, zeroLength) break } @@ -495,7 +546,7 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit result.resetBuffer(list) - blkup.progress.UpdateProgress(&uploader.Progress{BytesDone: written, TotalBytes: totalLength}) + progress.UpdateProgress(&uploader.Progress{BytesDone: written, TotalBytes: totalLength}) } result.resetBuffer(list) @@ -504,8 +555,12 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit return written, writeErr } + if curCount < totalCount { + return written, io.ErrUnexpectedEOF + } + if zeroStart != -1 { - if err := blkup.flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath); err != nil { + if err := flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath, log); err != nil { return written, errors.Wrapf(err, "error flushing zero blocks from %v, length %v", zeroStart, zeroLength) } } @@ -513,13 +568,13 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit return written, nil } -func (blkup *blockUploader) flushZeroBlocks(dest *os.File, start int64, length int64, zeroBlock []byte, destPath string) error { +func flushZeroBlocks(dest *os.File, start int64, length int64, zeroBlock []byte, destPath string, log logrus.FieldLogger) error { err := blkZeroOut(dest, start, length) if err == nil { return nil } - blkup.log.WithError(err).Warnf("Failed to call zero out from dev %s, start %v, length %v. Fallback to conservative way", destPath, start, length) + log.WithError(err).Warnf("Failed to call zero out from dev %s, start %v, length %v. Fallback to conservative way", destPath, start, length) var written int64 for written < length { diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index 79c7be954..8a4708e35 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -196,7 +196,7 @@ func TestBlockUploaderBackup(t *testing.T) { name: "canceled in progress", cancelInProgress: true, expectErr: true, - expectErrStr: "error backing up bdev /data/volume1: uploader is canceled", + expectErrStr: "error backing up bdev /data/volume1: error writing data: uploader is canceled", }, { name: "create object writer err", @@ -522,13 +522,11 @@ func TestFlushZeroBlocks(t *testing.T) { require.NoError(t, f.Truncate(2048)) - blkup := &blockUploader{ - log: logrus.New(), - } - blkup.log.(*logrus.Logger).Out = io.Discard + log := logrus.New() + log.Out = io.Discard zeroBlock := make([]byte, 1024) - err = blkup.flushZeroBlocks(f, 0, 2048, zeroBlock, f.Name()) + err = flushZeroBlocks(f, 0, 2048, zeroBlock, f.Name(), log) require.NoError(t, err) @@ -576,6 +574,7 @@ func TestRestoreData(t *testing.T) { iterMock.On("Count").Return(uint64(1)) iterMock.On("Next").Return(uint64(0), true).Once() iterMock.On("Next").Return(uint64(0), false) + iterMock.On("BlockSize").Return(uint(1048576)) written, err := blkup.restoreData(reader, f, iterMock, 1048576, f.Name()) require.NoError(t, err) @@ -605,6 +604,7 @@ func TestRestoreData(t *testing.T) { iterMock.On("Count").Return(uint64(1)) iterMock.On("Next").Return(uint64(0), true).Once() iterMock.On("Next").Return(uint64(0), false) + iterMock.On("BlockSize").Return(uint(1048576)) _, err = blkup.restoreData(reader, f, iterMock, 1048576, f.Name()) require.Error(t, err) @@ -681,6 +681,7 @@ func TestBlockUploaderRestore(t *testing.T) { iterMock.On("Count").Return(uint64(1)) iterMock.On("Next").Return(uint64(0), true).Once() iterMock.On("Next").Return(uint64(0), false) + iterMock.On("BlockSize").Return(uint(1048576)) written, err := blkup.Restore(snap, dest, iterMock, nil) require.NoError(t, err) From ef100da89b27842bd2faf0ed1ed87671987602c0 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Tue, 28 Jul 2026 08:53:18 +0800 Subject: [PATCH 109/194] remove VolumeSnapshotContents from resourceMustHave list Stop force-including VolumeSnapshotContents via resourceMustHave on every restore; CSI VolumeSnapshot/PVC RestoreItemActions now set `restore.velero.io/must-include-additional-items` so bound snapshot dependencies are restored only when their parent is restored. Fixes: #9957 Signed-off-by: Adam Zhang --- changelogs/unreleased/10087-adam-jian-zhang | 1 + pkg/restore/actions/csi/pvc_action.go | 9 +++ pkg/restore/actions/csi/pvc_action_test.go | 27 ++++++-- .../actions/csi/volumesnapshot_action.go | 21 ++++-- .../actions/csi/volumesnapshot_action_test.go | 4 ++ pkg/restore/restore.go | 1 - pkg/restore/restore_test.go | 69 +++++++++++++++++++ pkg/test/api_server.go | 3 + pkg/test/resources.go | 34 +++++++++ 9 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/10087-adam-jian-zhang diff --git a/changelogs/unreleased/10087-adam-jian-zhang b/changelogs/unreleased/10087-adam-jian-zhang new file mode 100644 index 000000000..7edaa117f --- /dev/null +++ b/changelogs/unreleased/10087-adam-jian-zhang @@ -0,0 +1 @@ +Stop force-including VolumeSnapshotContents via resourceMustHave on every restore; CSI VolumeSnapshot/PVC RestoreItemActions now set restore.velero.io/must-include-additional-items so bound snapshot dependencies are restored only when their parent is restored (fixes #9957) diff --git a/pkg/restore/actions/csi/pvc_action.go b/pkg/restore/actions/csi/pvc_action.go index 2203682be..6026f5378 100644 --- a/pkg/restore/actions/csi/pvc_action.go +++ b/pkg/restore/actions/csi/pvc_action.go @@ -175,6 +175,15 @@ func (p *pvcRestoreItemAction) Execute( Name: vsName, Namespace: pvc.Namespace, }) + + // Force-restore the VolumeSnapshot even when restore resource filters + // would otherwise exclude it (mirrors backup-side must-include). + annotations := pvc.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + pvc.SetAnnotations(annotations) } } diff --git a/pkg/restore/actions/csi/pvc_action_test.go b/pkg/restore/actions/csi/pvc_action_test.go index ea712c027..4ad8cd636 100644 --- a/pkg/restore/actions/csi/pvc_action_test.go +++ b/pkg/restore/actions/csi/pvc_action_test.go @@ -402,15 +402,22 @@ func TestExecute(t *testing.T) { vs: builder.ForVolumeSnapshot("velero", vsName).ObjectMeta( builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi"), ).Result(), - expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName")).Result(), + expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations( + velerov1api.VolumeSnapshotLabel, "vsName", + velerov1api.MustIncludeAdditionalItemRestoreAnnotation, "true", + )).Result(), }, { - name: "Restore from VolumeSnapshot without volume-snapshot-name annotation", - backup: builder.ForBackup("velero", "testBackup").Result(), - restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", AnnSelectedNode, "node1")).Result(), - vs: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi")).Result(), - expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", AnnSelectedNode, "node1")).Result(), + name: "Restore from VolumeSnapshot without volume-snapshot-name annotation", + backup: builder.ForBackup("velero", "testBackup").Result(), + restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", AnnSelectedNode, "node1")).Result(), + vs: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi")).Result(), + expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations( + velerov1api.VolumeSnapshotLabel, "vsName", + AnnSelectedNode, "node1", + velerov1api.MustIncludeAdditionalItemRestoreAnnotation, "true", + )).Result(), }, { name: "DataUploadResult cannot be found", @@ -508,6 +515,12 @@ func TestExecute(t *testing.T) { err := runtime.DefaultUnstructuredConverter.FromUnstructured(output.UpdatedItem.UnstructuredContent(), pvc) require.NoError(t, err) require.Equal(t, tc.expectedPVC.GetObjectMeta(), pvc.GetObjectMeta()) + if tc.name == "Restore from VolumeSnapshot" { + require.Equal(t, "true", pvc.GetAnnotations()[velerov1api.MustIncludeAdditionalItemRestoreAnnotation]) + require.Len(t, output.AdditionalItems, 1) + require.Equal(t, "volumesnapshots.snapshot.storage.k8s.io", output.AdditionalItems[0].GroupResource.String()) + require.Equal(t, "vsName", output.AdditionalItems[0].Name) + } if pvc.Spec.Selector != nil && pvc.Spec.Selector.MatchLabels != nil { // This is used for long name and namespace case. if len(tc.pvc.Namespace+"."+tc.pvc.Name) >= validation.DNS1035LabelMaxLength { diff --git a/pkg/restore/actions/csi/volumesnapshot_action.go b/pkg/restore/actions/csi/volumesnapshot_action.go index da5d4d281..ec0f1912b 100644 --- a/pkg/restore/actions/csi/volumesnapshot_action.go +++ b/pkg/restore/actions/csi/volumesnapshot_action.go @@ -282,12 +282,6 @@ func (p *volumeSnapshotRestoreItemAction) Execute( vs.Namespace, vs.Name) } - vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&vs) - if err != nil { - p.log.Errorf("Fail to convert VS %s to unstructured", vs.Namespace+"/"+vs.Name) - return nil, errors.WithStack(err) - } - if vsFromBackup.Status == nil || vsFromBackup.Status.BoundVolumeSnapshotContentName == nil { p.log.Errorf("VS %s doesn't have bound VSC", vsFromBackup.Name) @@ -299,6 +293,21 @@ func (p *volumeSnapshotRestoreItemAction) Execute( Name: *vsFromBackup.Status.BoundVolumeSnapshotContentName, } + // Force-restore the bound VSC even when restore resource filters would + // otherwise exclude it (mirrors backup-side must-include for CSI deps). + annotations := vs.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + vs.SetAnnotations(annotations) + + vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&vs) + if err != nil { + p.log.Errorf("Fail to convert VS %s to unstructured", vs.Namespace+"/"+vs.Name) + return nil, errors.WithStack(err) + } + p.log.Infof(`Returning from VolumeSnapshotRestoreItemAction with VolumeSnapshotContent in additionalItems`) diff --git a/pkg/restore/actions/csi/volumesnapshot_action_test.go b/pkg/restore/actions/csi/volumesnapshot_action_test.go index de3e592c0..d1b42b91c 100644 --- a/pkg/restore/actions/csi/volumesnapshot_action_test.go +++ b/pkg/restore/actions/csi/volumesnapshot_action_test.go @@ -184,6 +184,10 @@ func TestVSExecute(t *testing.T) { require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured( result.UpdatedItem.UnstructuredContent(), &vs)) require.Equal(t, test.expectedVS.Spec, vs.Spec) + require.Equal(t, "true", vs.GetAnnotations()[velerov1api.MustIncludeAdditionalItemRestoreAnnotation]) + require.Len(t, result.AdditionalItems, 1) + require.Equal(t, "volumesnapshotcontents.snapshot.storage.k8s.io", result.AdditionalItems[0].GroupResource.String()) + require.Equal(t, "vscName", result.AdditionalItems[0].Name) } }) } diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 7ba9ae6fd..e7a284fb1 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -87,7 +87,6 @@ const ObjectStatusRestoreAnnotationKey = "velero.io/restore-status" var resourceMustHave = []string{ "datauploads.velero.io", - "volumesnapshotcontents.snapshot.storage.k8s.io", } type VolumeSnapshotterGetter interface { diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 9d46c3e53..935586e63 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -754,6 +754,29 @@ func TestRestoreResourceFiltering(t *testing.T) { apiResources: []*test.APIResource{test.ServiceAccounts()}, want: map[*test.APIResource][]string{test.ServiceAccounts(): {"ns-1/sa-1"}}, }, + { + // Regression for #9957: VSC must not be force-included via resourceMustHave + // when the restore only selects unrelated resource types. + name: "volumesnapshotcontents are not force-included for selective resource restores", + restore: defaultRestore().IncludedResources("storageclasses").IncludeClusterResources(true).Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t). + AddItems("storageclasses.storage.k8s.io", + builder.ForStorageClass("sc-1").Result(), + ). + AddItems("volumesnapshotcontents.snapshot.storage.k8s.io", + builder.ForVolumeSnapshotContent("vsc-1").Result(), + ). + Done(), + apiResources: []*test.APIResource{ + test.StorageClasses(), + test.VolumeSnapshotContents(), + }, + want: map[*test.APIResource][]string{ + test.StorageClasses(): {"/sc-1"}, + test.VolumeSnapshotContents(): nil, + }, + }, } for _, tc := range tests { @@ -2592,6 +2615,52 @@ func TestRestoreMustIncludeAdditionalItems(t *testing.T) { test.PVCs(): nil, }) }) + + t.Run("VS must-include restores excluded VolumeSnapshotContent additional item", func(t *testing.T) { + h := newHarness(t) + h.AddItems(t, test.VolumeSnapshots()) + h.AddItems(t, test.VolumeSnapshotContents()) + + data := &Request{ + Log: h.log, + Restore: defaultRestore().IncludedResources("volumesnapshots.snapshot.storage.k8s.io").IncludeClusterResources(true).Result(), + Backup: defaultBackup().Result(), + BackupReader: test.NewTarWriter(t). + AddItems("volumesnapshots.snapshot.storage.k8s.io", builder.ForVolumeSnapshot("ns-1", "vs-1").Result()). + AddItems("volumesnapshotcontents.snapshot.storage.k8s.io", builder.ForVolumeSnapshotContent("vsc-1").Result()). + Done(), + } + warnings, errs := h.restorer.Restore( + data, + []riav2.RestoreItemAction{ + &pluggableAction{ + selector: velero.ResourceSelector{IncludedResources: []string{"volumesnapshots.snapshot.storage.k8s.io"}}, + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + item := input.Item.(*unstructured.Unstructured) + annotations := item.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true" + item.SetAnnotations(annotations) + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.VolumeSnapshotContents, Name: "vsc-1"}, + }, + }, nil + }, + }, + }, + nil, + ) + + assertEmptyResults(t, warnings, errs) + assertAPIContents(t, h, map[*test.APIResource][]string{ + test.VolumeSnapshots(): {"ns-1/vs-1"}, + test.VolumeSnapshotContents(): {"/vsc-1"}, + }) + }) } // TestShouldRestore runs the ShouldRestore function for various permutations of diff --git a/pkg/test/api_server.go b/pkg/test/api_server.go index 63975014a..c69dc5926 100644 --- a/pkg/test/api_server.go +++ b/pkg/test/api_server.go @@ -58,6 +58,9 @@ func NewAPIServer(t *testing.T) *APIServer { {Group: "velero.io", Version: "v2alpha1", Resource: "datauploads"}: "DataUploadsList", {Group: "mygroup.io", Version: "v1", Resource: "mycustomkinds"}: "MyCustomKindList", {Group: "mygroup.io", Version: "v1", Resource: "myclustercustomkinds"}: "MyClusterCustomKindList", + {Group: "storage.k8s.io", Version: "v1", Resource: "storageclasses"}: "StorageClassList", + {Group: "snapshot.storage.k8s.io", Version: "v1", Resource: "volumesnapshots"}: "VolumeSnapshotList", + {Group: "snapshot.storage.k8s.io", Version: "v1", Resource: "volumesnapshotcontents"}: "VolumeSnapshotContentList", }) discoveryClient = &DiscoveryClient{FakeDiscovery: kubeClient.Discovery().(*discoveryfake.FakeDiscovery)} ) diff --git a/pkg/test/resources.go b/pkg/test/resources.go index fe2ad6352..975359d47 100644 --- a/pkg/test/resources.go +++ b/pkg/test/resources.go @@ -220,3 +220,37 @@ func DataUploads(items ...metav1.Object) *APIResource { Items: items, } } + +func StorageClasses(items ...metav1.Object) *APIResource { + return &APIResource{ + Group: "storage.k8s.io", + Version: "v1", + Name: "storageclasses", + ShortName: "sc", + Kind: "StorageClass", + Namespaced: false, + Items: items, + } +} + +func VolumeSnapshotContents(items ...metav1.Object) *APIResource { + return &APIResource{ + Group: "snapshot.storage.k8s.io", + Version: "v1", + Name: "volumesnapshotcontents", + Kind: "VolumeSnapshotContent", + Namespaced: false, + Items: items, + } +} + +func VolumeSnapshots(items ...metav1.Object) *APIResource { + return &APIResource{ + Group: "snapshot.storage.k8s.io", + Version: "v1", + Name: "volumesnapshots", + Kind: "VolumeSnapshot", + Namespaced: true, + Items: items, + } +} From d685b818ad41e7a75b2b1853e2102598fca79d0b Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 28 Jul 2026 15:19:43 +0800 Subject: [PATCH 110/194] refactor block uploader thread module Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index af31c71e7..0b937c82e 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -212,6 +212,7 @@ func (r *readResult) resetBuffer(list *freelist.FreeList) { func (blkup *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (int64, int64, error) { blockSize := bitmap.BlockSize() + totalCount := int64(bitmap.Count()) list := freelist.New(bufferSize, int(blockSize)) resultChan := make(chan readResult, list.Capacity()) quit := make(chan struct{}) @@ -231,7 +232,7 @@ func (blkup *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.Object go func() { defer wg.Done() defer close(quit) - written, lastPos, writeErr = backupWriteProc(blkup.ctx, writer, resultChan, list, aligned, int64(bitmap.Count()), int(blockSize), blkup.progress) + written, lastPos, writeErr = backupWriteProc(blkup.ctx, writer, resultChan, list, aligned, totalCount, int(blockSize), blkup.progress) }() wg.Wait() @@ -312,7 +313,14 @@ func backupWriteProc(ctx context.Context, writer udmrepo.ObjectWriter, resultCha select { case <-ctx.Done(): writeErr = ErrCanceled - case result = <-resultChan: + case r, ok := <-resultChan: + if !ok { + if ctx.Err() != nil { + writeErr = ErrCanceled + } + } else { + result = r + } } if writeErr != nil { @@ -391,6 +399,7 @@ func getObjectName(source string) string { func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bitmap cbt.Iterator, totalLength int64, destPath string) (int64, error) { blockSize := bitmap.BlockSize() + totalCount := int64(bitmap.Count()) list := freelist.New(bufferSize, int(blockSize)) resultChan := make(chan readResult, list.Capacity()) quit := make(chan struct{}) @@ -409,7 +418,7 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit go func() { defer wg.Done() defer close(quit) - written, writeErr = restoreWriteProc(blkup.ctx, dest, resultChan, list, totalLength, int64(bitmap.Count()), int(blockSize), destPath, blkup.progress, blkup.log) + written, writeErr = restoreWriteProc(blkup.ctx, dest, resultChan, list, totalLength, totalCount, int(blockSize), destPath, blkup.progress, blkup.log) }() wg.Wait() @@ -487,7 +496,14 @@ func restoreWriteProc(ctx context.Context, dest *os.File, resultChan chan readRe select { case <-ctx.Done(): writeErr = ErrCanceled - case result = <-resultChan: + case r, ok := <-resultChan: + if !ok { + if ctx.Err() != nil { + writeErr = ErrCanceled + } + } else { + result = r + } } if writeErr != nil { From 63cfddd18de1204c801febab72c8fa24cf3ab845 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Tue, 28 Jul 2026 16:06:04 +0800 Subject: [PATCH 111/194] add tests to cover pvc and vsc ria Signed-off-by: Adam Zhang --- pkg/restore/actions/csi/pvc_action_test.go | 27 +++++++++++++++- .../actions/csi/volumesnapshot_action.go | 3 ++ .../actions/csi/volumesnapshot_action_test.go | 32 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/pkg/restore/actions/csi/pvc_action_test.go b/pkg/restore/actions/csi/pvc_action_test.go index 4ad8cd636..0e10144f6 100644 --- a/pkg/restore/actions/csi/pvc_action_test.go +++ b/pkg/restore/actions/csi/pvc_action_test.go @@ -371,6 +371,7 @@ func TestExecute(t *testing.T) { backup *velerov1api.Backup restore *velerov1api.Restore pvc *corev1api.PersistentVolumeClaim + pvcFromBackup *corev1api.PersistentVolumeClaim vs *snapshotv1api.VolumeSnapshot dataUploadResult *corev1api.ConfigMap expectedErr string @@ -407,6 +408,24 @@ func TestExecute(t *testing.T) { velerov1api.MustIncludeAdditionalItemRestoreAnnotation, "true", )).Result(), }, + { + name: "Restore from VolumeSnapshot with nil PVC annotations", + backup: builder.ForBackup("velero", "testBackup").Result(), + restore: builder.ForRestore("velero", "testRestore").ObjectMeta(builder.WithUID("restoreUID")).Backup("testBackup").Result(), + pvc: &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPVC", + Namespace: "velero", + }, + }, + pvcFromBackup: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName")).Result(), + vs: builder.ForVolumeSnapshot("velero", vsName).ObjectMeta( + builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi"), + ).Result(), + expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations( + velerov1api.MustIncludeAdditionalItemRestoreAnnotation, "true", + )).Result(), + }, { name: "Restore from VolumeSnapshot without volume-snapshot-name annotation", backup: builder.ForBackup("velero", "testBackup").Result(), @@ -487,7 +506,13 @@ func TestExecute(t *testing.T) { require.NoError(t, err) input.Item = &unstructured.Unstructured{Object: pvcMap} - input.ItemFromBackup = &unstructured.Unstructured{Object: pvcMap} + if tc.pvcFromBackup != nil { + pvcFromBackupMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pvcFromBackup) + require.NoError(t, err) + input.ItemFromBackup = &unstructured.Unstructured{Object: pvcFromBackupMap} + } else { + input.ItemFromBackup = &unstructured.Unstructured{Object: pvcMap} + } input.Restore = tc.restore } if tc.preCreatePVC { diff --git a/pkg/restore/actions/csi/volumesnapshot_action.go b/pkg/restore/actions/csi/volumesnapshot_action.go index ec0f1912b..13b7cb246 100644 --- a/pkg/restore/actions/csi/volumesnapshot_action.go +++ b/pkg/restore/actions/csi/volumesnapshot_action.go @@ -66,6 +66,9 @@ func resetVolumeSnapshotSpecForRestore(vs *snapshotv1api.VolumeSnapshot, vscName } func resetVolumeSnapshotAnnotation(vs *snapshotv1api.VolumeSnapshot) { + if vs.ObjectMeta.Annotations == nil { + vs.ObjectMeta.Annotations = make(map[string]string) + } vs.ObjectMeta.Annotations[velerov1api.VSCDeletionPolicyAnnotation] = string(snapshotv1api.VolumeSnapshotContentRetain) } diff --git a/pkg/restore/actions/csi/volumesnapshot_action_test.go b/pkg/restore/actions/csi/volumesnapshot_action_test.go index d1b42b91c..9d72971d0 100644 --- a/pkg/restore/actions/csi/volumesnapshot_action_test.go +++ b/pkg/restore/actions/csi/volumesnapshot_action_test.go @@ -103,6 +103,26 @@ func TestResetVolumeSnapshotSpecForRestore(t *testing.T) { } } +func TestResetVolumeSnapshotAnnotation(t *testing.T) { + t.Run("should set deletion policy annotation when annotations is nil", func(t *testing.T) { + vs := snapshotv1api.VolumeSnapshot{} + resetVolumeSnapshotAnnotation(&vs) + assert.NotNil(t, vs.ObjectMeta.Annotations) + assert.Equal(t, string(snapshotv1api.VolumeSnapshotContentRetain), vs.ObjectMeta.Annotations[velerov1api.VSCDeletionPolicyAnnotation]) + }) + + t.Run("should preserve existing annotations and set deletion policy annotation", func(t *testing.T) { + vs := snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"foo": "bar"}, + }, + } + resetVolumeSnapshotAnnotation(&vs) + assert.Equal(t, "bar", vs.ObjectMeta.Annotations["foo"]) + assert.Equal(t, string(snapshotv1api.VolumeSnapshotContentRetain), vs.ObjectMeta.Annotations[velerov1api.VSCDeletionPolicyAnnotation]) + }) +} + func TestVSExecute(t *testing.T) { newVscName := util.GenerateSha256FromRestoreUIDAndVsName("restoreUID", "vsName") tests := []struct { @@ -145,6 +165,18 @@ func TestVSExecute(t *testing.T) { expectErr: false, expectedVS: builder.ForVolumeSnapshot("ns", "test").SourceVolumeSnapshotContentName(newVscName).Result(), }, + { + name: "Normal case with nil VS annotations, VSC should be created", + vs: builder.ForVolumeSnapshot("ns", "vsName"). + SourceVolumeSnapshotContentName(newVscName). + VolumeSnapshotClass("vscClass"). + Status(). + BoundVolumeSnapshotContentName("vscName"). + Result(), + restore: builder.ForRestore("velero", "restore").ObjectMeta(builder.WithUID("restoreUID")).Result(), + expectErr: false, + expectedVS: builder.ForVolumeSnapshot("ns", "test").SourceVolumeSnapshotContentName(newVscName).Result(), + }, } for _, test := range tests { From 4745c45fafc9dda87b3b7a29ceb37112c9dd9203 Mon Sep 17 00:00:00 2001 From: chlins Date: Tue, 28 Jul 2026 14:19:47 +0800 Subject: [PATCH 112/194] Pin prow GitHub action to commit SHA Signed-off-by: chlins --- .github/workflows/prow-action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prow-action.yml b/.github/workflows/prow-action.yml index 871f69f8f..8a9190180 100644 --- a/.github/workflows/prow-action.yml +++ b/.github/workflows/prow-action.yml @@ -14,7 +14,7 @@ jobs: if: github.repository == 'velero-io/velero' runs-on: ubuntu-latest steps: - - uses: jpmcb/prow-github-actions@v1.1.3 + - uses: jpmcb/prow-github-actions@f4d01dd4b13f289014c23fe5a19878a2479cb35b # v1.1.3 with: # TODO: before allowing the /lgtm command, see if we can block merging if changelog labels are missing. prow-commands: | From 5ca38aa075817a0b1d4ce010b0ee20407eace847 Mon Sep 17 00:00:00 2001 From: Chlins Zhang Date: Wed, 29 Jul 2026 15:41:57 +0800 Subject: [PATCH 113/194] ci(push): pin action versions to commit SHAs and restrict permissions (#10083) Signed-off-by: chlins Co-authored-by: Daniel Jiang --- .github/workflows/push.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 528776e54..b010aa76d 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -8,6 +8,9 @@ on: tags: - '*' +permissions: + contents: read + jobs: get-go-version: uses: ./.github/workflows/get-go-version.yaml @@ -20,21 +23,21 @@ jobs: needs: get-go-version steps: - name: Check out the code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Set up Go version - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: ${{ needs.get-go-version.outputs.version }} - name: Set up QEMU id: qemu - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 with: platforms: all - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 with: version: latest - name: Build @@ -45,7 +48,7 @@ jobs: - name: Test run: make test - name: Upload test coverage - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.out From 5691f7f29d35a43b05b657826a58edcb1cbc3187 Mon Sep 17 00:00:00 2001 From: Lubron Date: Wed, 29 Jul 2026 00:55:51 -0700 Subject: [PATCH 114/194] Support overriding Schedule annotations via template.metadata.annotations (#10045) * Support overriding Schedule annotations via template.metadata.annotations Adds an Annotations field to BackupSpec.Metadata, mirroring the existing Labels override. When Schedule.Spec.Template.Metadata.Annotations is set, it is used for the resulting Backup's annotations instead of copying Schedule.Annotations directly, allowing users to opt out of unwanted annotations (e.g. ArgoCD tracking annotations) being propagated from Schedule to Backup. Fixes #5836 Signed-off-by: Lubron Zhan * Rename changelog fragment to match PR number 10045 Signed-off-by: Lubron Zhan --------- Signed-off-by: Lubron Zhan Co-authored-by: Daniel Jiang --- changelogs/unreleased/10045-lubronzhan | 1 + config/crd/v1/bases/velero.io_backups.yaml | 5 ++ config/crd/v1/bases/velero.io_schedules.yaml | 5 ++ config/crd/v1/crds/crds.go | 4 +- pkg/apis/velero/v1/backup_types.go | 3 + pkg/apis/velero/v1/zz_generated.deepcopy.go | 7 ++ pkg/builder/backup_builder.go | 19 ++++- pkg/builder/backup_builder_test.go | 84 ++++++++++++++++++++ site/content/docs/main/api-types/schedule.md | 6 +- 9 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/10045-lubronzhan create mode 100644 pkg/builder/backup_builder_test.go diff --git a/changelogs/unreleased/10045-lubronzhan b/changelogs/unreleased/10045-lubronzhan new file mode 100644 index 000000000..d8974e9a3 --- /dev/null +++ b/changelogs/unreleased/10045-lubronzhan @@ -0,0 +1 @@ +Fix issue #5836, respect schedule.spec.template.metadata.annotations to override annotations copied from the Schedule to Backup objects, matching the existing behavior for labels diff --git a/config/crd/v1/bases/velero.io_backups.yaml b/config/crd/v1/bases/velero.io_backups.yaml index 68ec68c68..96c425caa 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -393,6 +393,11 @@ spec: x-kubernetes-map-type: atomic metadata: properties: + annotations: + additionalProperties: + type: string + nullable: true + type: object labels: additionalProperties: type: string diff --git a/config/crd/v1/bases/velero.io_schedules.yaml b/config/crd/v1/bases/velero.io_schedules.yaml index 7ec1b6025..0b32b298b 100644 --- a/config/crd/v1/bases/velero.io_schedules.yaml +++ b/config/crd/v1/bases/velero.io_schedules.yaml @@ -434,6 +434,11 @@ spec: x-kubernetes-map-type: atomic metadata: properties: + annotations: + additionalProperties: + type: string + nullable: true + type: object labels: additionalProperties: type: string diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 5ecc27bcc..f309e5d4d 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -30,14 +30,14 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8aTɡS\xf7\xe7\u074b!%[\x96hk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L4\xea\x11\x9dW\xd6,A4\n\xff 4\xfc\xe5\xe7O\xbf\xf8\xb9\xb2\x8b\xc3\xebٓ2\xe5\x12V\xc1\x93\xad\xd7\xe8mp\x12\xdf\xe1N\x19EʚY\x8d$JAb9\x03\x10\xc6X\x12\xbc\xed\xf9\x13@ZC\xcej\x8d\xae\xa8\xd0̟\xc2\x16\xb7A\xe9\x12]4\u07b9>\xfc0\x7f\xfd\xf3\xfc\xa7\x19\x80\x115.a+\xe4Sh\x1c6\xd6+\xb2N\xa1\x9f\x1fP\xa3\xb3seg\xbeA\xc9\xd6+gC\xb3\x84\xf3A\xd2n=\xa7\xa8\xdfFC\xeb\xce\xd01\x1ei\xe5\xe9\xb7\xec\xf1G\xe5)\x8a4:8\xa1s\x81\xc4c\xbf\xb7\x8e>\x9f\x9d\x15\xc0\"\xe9H\x99*h\xe1F\xba\xec\xdbK\xdb\xe0\x12\xa2f#$\x963\x80\xb6\b\xd1R\x01\xa2,cY\x85~p\xca\x10\xba\x95ա6'?_\xbd5\x0f\x82\xf6K\x98w\x85\x9fK\x87\xb1\xe6_T\x8d\x9eD\xddDٮ\x96o*l\xbf\xe9\xc8\xceKA86\xc6E\x9d\x9fc\xfdrl\xf0\xc2ʹF\xd0;K\x16=9e\xaa\xd9Y\xf8\xf0:\x95B\xee\xb1\x16\xcbV\xd66h\xde<|x\xfcqs\xb1\r\xd08۠#\xd5\x153\xad^g\xf6v\x01J\xf4ҩ\x86b\xdf\xfc]\\\x9c\x01\xb0\x83\xa4\x05%\xb7(z\xa0=v5Ʋ\x8d\t\xec\x0eh\xaf<\xc3\xe6УIM\xcb\xdb\u0080\xdd~EI\xf3\x81\xe9\r:6\xc3\xd8\a]rg\x1f\xd0\x118\x94\xb62\xeaϓm\x0fd\xa3S-\b=AD\xd1\b\r\a\xa1\x03~\x0f\u0094\x03˵8\x82C\xf6\t\xc1\xf4\xecE\x05?\x8c\xe3\x93u\b\xca\xec\xec\x12\xf6D\x8d_.\x16\x95\xa2n^\xa5\xad\xeb`\x14\x1d\x17q\xf4\xd46\x90u~Q\xe2\x01\xf5«\xaa\x10N\xee\x15\xa1\xa4\xe0p!\x1aU\xc4DL\x9c\xd9y]~\xe7\xda\t\xf7\x17nG@\xa7\x15\x87\xec\x0exx\xea@y\x10\xad\xa9\x94\xe2\x19\x05\xde\xe2ҭ\x7f\xdd|\x81.\x92\x84T\x02\xe5,:\xaaK\x87\x0fWS\x99\x1d\xba\xa4\xb7s\xb6\x8e6є\x8dU\x86\xe2\x87\xd4\n\r\x81\x0f\xdbZ\x11\xb7\xc1\xef\x01=1tC\xb3\xab\xc8i\xb0E\b\r\x8fN9\x14\xf8``%j\xd4+\xe1\xf1?ƊQ\xf1\x05\x83\xf0,\xb4\xfaL=\x14N\xe5\xed\x1dt,{\x05\xda!sn\x1a\x94\x8c,\x17\x97U\xd5N\xc94S;\xeb@\x8c\xe4/+\x95\xa7\x00^\x89D7d\x9d\xa8\xf0\xa3M6\x87BSm\xc7\xebm\xceP\x171\xd3V\xe2\x04\xcc\vf\f\xd2^P\x8f\fH(s\xe2\x94l\x927\x90\x89\xe8\bf\n#\x8c\xc4\xf7\xb1\x1f\x8d\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xad+\xc6͒\x89v\x1f\x8f\x8c\xf3\xd0\xcb<\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa0\x9d2\x9dD\x8b\x01X-\xd2<\xee\xc0\xec@\x91R֮\xc0\x86q \xfa\xa0\r\x14\x9e0az\xf5\xf8DzB\xa3¹\a\xa1-]=\"}\xe4E\xc59]s\xb8\"FU0@\x9b\xb5\x94\x1c\xa8\x98 \xcegІe\xe7 \x8d\x83\x14!\x8c\xf2\x1f:\x14@o\x82>\x00\xa1\x11Оf\xd6m\xe1\xbcE\xd8.U\xa2c*\x15dv:\xbb\xf2\xd3$\x03\x8eS\xb3\x90\x84K\xb1\x05\xe5z\xb7V/\b\x98\x02+p9\xb13\x90\x02n\xa7Y\xb2\xa9\xec\xe4tI\xacv\x0f\xca\x00\x13\xda\x00\x8d\b\xe7\x13\xf8\x03_\xacu\x86\xfc\xday\xa4\xb7ֱ\xce\xc3B\xa37\x9d\xa4\xf0\xe9\xfd(D\xef\xb6p\x96\xa1w\xec\x1d\xe1%:\xf411m\xbc\x17;5\xe1jò\xd2\x0f\xbbqKF\xed\x81\x06c\x1b]\xfc\xe9b\x81\x1c\xee\xf6\xda\xedC\x13\xaa\xa0&K\xb2݄\xa24\x87~mf\xa0\x88PqԞ$\xf2\x93*E\x0f\x03ܬ\x17Fg\xe4\xe7\x10\xcc#\x8e\x8aP\xed\x85yz\xdc\xef?3W\xcf\xc3G\x8d\x01\x02ʄ\xe5\x9f]\xabwا\xdd\xc2֒MH\x13\x81\xe7\xfc:\xc8q\xcd:\u00ad߉Xg\x91\xf9!!\xafe\xcb\v\xef?$\xa5vR>LQ\xe7\a[\xa7Y-\x92\f\x03Qd\r;\xbagRyԛ\xa9\x16\xbe@V\x99\xa8\xd6SCr\xb6ـ\xb2p\xca\x1dՠ]\xfc`\x98 \xc3\xeb\x1a\xd22#яGx4\x8c\xb4lḂ\x86n\xfd\x88\xe3Y2\x14;P\xeb^\xe3d\x9c\xb3=\xcb+\xcaq^\xa6\"s\xf8\xd0z\\1+3\xc2\xe4ޘ\xa3\x92\xe9\x8as\b\x02R\x96I\x9d%\xa4\x14`}\xde®\t\xfaU\x871_S\xeb\xab\xc8!\xec\t2KU\x1c\xb4\xef*G7\xb2\xb1\x19\x8b\x86)\x18\xa1!\x9c\xae\x81\x13\r\x1c2#U\x9c\"S|v%\xc5\b\x0e\x102b\xf9\xba+\x8d\x06\x81\x11\x90\x04\x97p;\x96휫g\x85\b\xe1\x90\\\x82u\xf8\f\xa1e\xc9#\xd3ESF\x99\xef;\x19\xd3\xf5\xa6Lh\xfd1\xbc\x98\xfe7%\xc1f6%J\xdaF\xbf\xba\x94\xad\xc5!\xbe\xa6m\xca?'a\x83\xe5?AhG\xb4\x9f`\xb8,Y\xa6\a\xe5\xd6R\x95\x81\xbe\xb4\xee\x14z:\v\xc2L\xf8uJ\x13:>W/\x8a\xd8!\xc2\xd7͛\xf9B\x9fȚ\x14\x9dx&\xc6\xd4]\xfc\x03\xf2\x05\xa7\x8c[?c$\xf3\xe4\xa7v\xab\x05a\x9b\x9a\xe8\xf9\x82l\x187\xa0\x8e\xa8\x7f\x92\xa9\x0f\x9c9\a1Rf=\x82\xfb\x1a&۽\xffb]0\xddl\xee%\xd2帱sd\x83\xb7ߝ\x9e'\xe0\x12\x8c\xef3\x17m\u0557\xb8bj\xff\x82\xae\xd5ۏ\xef\xe2\xeb\xabvI\x90\xbc\x1e\"\x13J\xe7\xca\xdb#\x8c\xda\xe3\xf3.|\xf8\x82>P\xbd\x00r\xb1\xea\x05\xa1\xe4\x01\x0e\xceu\xa1\x82X\xfe\xd0P9\xa1{\x05\xb8Y\x85r\xf6\x00\a\x04\x13\xdf}\xea\x97Tip\xe5\x01\x0e)Վhh\xc7Ĵ\xdfU\xb3t\xb2? !p\xd3!U\f\\\xf1\xaa\x10\xd9뉗D[\x12J\xa0\xfd\th&\x89J\xbb\x8f\xf6\xf6-J\xc0w\xda\xf1\xd2j̎\x95hV1\xe2 7\xc9\fu\xe5\x9er\x96\xd7\x1d9\x1dY\x89\x05\xf9(\x8d\xfd\xe7\xfd\x17\xa6\xfd\x0e\xef;\t\xfa\xa34\xf8˳P\xd4\r\xfc9\xe9\x19v|,B\xce\xca[\x82\xb5\xf7(ݜf\xa5\xad\xa6=\xd3d%\xecrő$\xb1+\u070evݹ\x8e\x8aJ\xe3\xf6\xa2\x90b\xe9\xc26\xb1\x9e<\xbd\xa5\xea\x90\xfbɝ\xfa\x0e\xef\xecdᾸMqN3\xc8\xc3v\r\xee\xd6R\x03[\x96%\xf6W\x80\xda\x02)\xad\tO\x93\x88D\xc3걙'>i\xb3w\xbb|Y>\xd4\xc9\x0fK;\xe5,=\x04#\x8b\x04\x1ax\u06ddO㳴:\x9bP+H\xc2dՁ\xcd\xdc\xe1\xaa)Dy\x029p\x16G\x17g\x92\xbb4\xcf15\x88\xf2\x9b\x193\xca\fY\x98k\x1aZcwSpAq\xab\xe5\x7f\xecL\x8b\xda\xf4\x7f\xa4\xa4L\xe9K\xf2\x16s}8t\xbe\xf9\xa0Y\vLB\x97\x98\xabc\xe5gO\xb9\x9d\xfb\xad\x01\x17\x04\xb8\xf3\x04\xe4\xa6\xe7\x17-\xc8\xe3Nj7mכ8\x17\x0fpp;\x86\x93]\xb6\x8d\xcc\xc5J\\8\x1f\xa2g0j\x87C\n~ \x17\xf8\xed\xe2)\xaeT\xa2\xa4&V\xeb\x88hA\xcb4\t\xc5\\\xabTG\xdd.X\x83\x13b\x1b\xd69D\xd6\xc9\x1e\xc36IDK\xa9#\x1b\xf9\x03C\x99\x10\xde\x1b\xa9\x8d\x8b\x97u|\xe6h@M\x86 \x1a\xa1\x1b\x97\xd8%U\xc8±Fy*\xf4\xdb.w;\xd0\xe0\xf7+|`\xce\x01\xb5+\xbb\x8bF\xbf\x9d\xb5\xbfp\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xe6\x8bH\x96H\x1b\xf7:\xe6H\xdd*\xc9媌\x87@CIwy-!f\xae\x17\xde\x7fi\x05D\xad\xeeۿ\xa7dl\xee\xb8\bfY\x16\x05=\xce\xdfJ\x1a\xe2\xb5k\x19\xb4\xc1\x03r\x8b\x0f\xb5\xad\xd0\x12\xa4\xce\xe5\xb5\x00~\r\x8eB\xc1\xc4\n; o\x9e\xc1\xb1\xf064\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fN\x95K\x89[\x05\n:\xcc\xebG\xd5\xd1\x0f\x15Ҵ\x02\x123\xdc\xcdR\xe6\xdfi\xb2aJ\x9b\xf6\x10\xf4@\x9aJ\x14\xcc̅\x97x\xaf\xd4I\xeb\xaeO\xae\xe5Q\x02\x99\xcf[s\x84I\xc4\x1c\xf7\x97\x80\xb0\ra\x86\x80\xc8d%0\x80c\xf5\x18\xbbp\xc4u\x16\x96\xa5*I\x9a\xf6\x93\xc1\x1c\xb4XY\xa2\xa401\x1a\xe9iW\xff@Y?Q-Vf\xb2\xcd\fe\xb1\xc5\xcai:\x11R\xdcڙ\x8a\x05\xfd\u008a\xaa \xb4\xb0<\xc2ɜ\x15\xd0ez\x93\xf8f[\xe04a\xa4\u0558\x92\x83\x01\x9f\xbc\x968\x86L\n\xcdr\xa8'W/\bR\x10J6\x94\xf1J%Z\xc0Y䝳\x14\xf1\x96\xe0|k\x8c\xb4ΗH\x8a\x84hn\xa2\xaf8n\x8dK\x95\xee\xf1M\xb9Y\n\xe6{Y\xa5b\x12\xd3\x02\xcf\xech\xf9DJ*\x0e\xdf<\xadԡ~\xf3\xb4\xc6\xca7Ok\xa2|\xf3\xb4\xbeyZ)5\xbfyZ\xdf<\xadv\xf9\x97\xf0\xb4\xa6F\xe4\x0e:\x0e|\x9c\x1cE\xc2V\xf5\xd8\x10G\xe0\xfb\xe4\n\x9f\x03\xfe\xa4\\\xccU\x1cT$\xf1\x7f \xad;f\xb4\x9aɣNδZ\x13dޝ\xbb\x9ap%\x9f\x90u\x1f:=_\xd6\xfdj\x14♲\xee\xfd\xb0\xa7}\xec\x93r\xee\x03Q\xe6eg/|\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x99\xe8\xff\x85\x13s{Ycg\x94\x8fg\xcf\xe2O\x96\x91(K/\xfet\xf1\xf5\x91\xff<\x04\x1f$q\x9fv\xfe\xe0w\x04\xaa]\x81\xb6\xd3ºYx_\xa7\x18\x9fEnS3\xf1k\"F`uE\xf2\x88\x8a_\xab-0P|*\xfd\x8c\U001044ea\xab\b\x9c\xa4\xb3\xaaT\x1fD\xb6SR\xc8J\xfb\xa8\x84\x85\xf56s'\xfd\x03Ș\xb0F5\xfc?\xc8NV\x91L\xf0\x11\xf2Md\x04N#\xdfI\x0e\xf4\x9b\xd0`\xe8\xfe\xcde\xf7\x8b\x91>Up\xe8l\xf3\xe3\x0e\x04\uec0bm\xfb\x00@\xb8\xa8\xc1\xdfXp,`\x11@R\x11\xc1\xb8\x93\xbc\xfa\x9a\x87\xb6ܑO\xa5\x8b=\xcd\xf6;\xc6c*iɄ'\xa7\x10vS\x04\a\xfcҹ\xbb\xddg92\xf1\xbb\xa4\x06\xceO\bL\x89\x88M$\xff\x9d\x90\xf2\x97\x98[\xfc\xe4\xed\xf9\x94\xa4\xbe9+\xe6gK\xe0;\x7f\xda^\x12}\xa6S\xf4\xe6P\xe7\xd9\xd3\xf1^0\t\xefeR\xef\x12\x13\xeeΗ9\x9f\x16\x8f=)sl:t0\x9c47\x99*7\x19Z\x98Bl6J\x93)ps\x12\xdf&\xb9\x93\xa6f/\x96\xda\xf6b\tm/\x9b\xc66*E\xa3\x1f\xe7$\xaa\xc5\xef\xeb!\x93\x93-\x7f)a;\x95\fRu\xdcד\xd6W\x9f\x8e`X\xc6\a\xd7\xee\x85|\xe4\xa2↕\x1c7R\xf7,\x8f\x06\x1b\xcc\x0e\x0e\xf5\x05\x1a\xbfJ\xf53!Y\x13\x9f\xe7\x9d\xee9y\xcbB\xaa\x1c\xd4\xe8\xb6O\xaa\x14\x8e\xca_\xcaڦ;\x90\xa3\xfd\x8ep럭\xd5\xf1\x97qz\xf07\xb0\xe2]\xbbCۗV\xd2Z\xdeFg/\xaaq\x7f\xbaΤ\xbf\x80\xd7mWi(\xa9\xc2K\x9d\xd7\a\x97\xce\x12\x9d\x9a\xdf\xd3lw\x04}G5\xd9HUPC.\xea\r\xc0\xd7\x0e\xb8\xfd\xfb⒐\x0f\xb2Ήh\xdfˣYQ\xf2\x83]\xa1\x90\x8bv\x83\xd3$ *m\xa1\xb7\x1b\xc9Y\x16\xf1ݢw3\xb9ʽ\xcb2\xf0ƨ\xac\x9d2Pڊq\xd7\rݼ\xee\x15\x98\x1bɹ|\x9c\xb9\xf6\xa7%\xfb\v^v\xfe\x84\xe8\xd0ۛ\x15\xc2\b⁷\xa7\xd7\xc9Y56k\xb0\xd3r\x83\xe7\x90\xee\xaf6\x1d\x88\xdd<\xc7\xf6\xad\xc1\x90\xbb\v\xa2\x83[\xe0Mg&\xadu\xb9Y\xb9q\f\xf5be\x86\x8a\x03\x91\x98QcvL\xe5˒*sp\x89\x1a\x8b\xce\x18\xc2\\:\x16\xdd\x19\x9c=\xfa\x97^G\xc9\x1b\xee\xba\xc6\x1d\xcaC\xd9\xdd\xf4=\xa6\xdd)\xe3\x18>\xbd8yn\xf1\x8c\xe3\x18vK\x96H\xa9\xc8\xcf\xd1̯\xb3Eʹ\xbf\x99\xf8g\xb9\x87w\xd1\xe8Y\x87<\xb7G\xd5#\xe9Y\x01\xa2\xbbtw0Ku\rx!o\xff\xd3\x13\xf2\xadB\xd7\xfeN\xd5S\x02e\xb7]\x10\x11\xfc\xc2\r\xb3\xa1\xb3\x98}\u009b\xf1\x0f\xe4\xe6\x1e\xd7h\xb5i\xf3*\xea\xd7h!T\x166\x83#p|\x83\xefϟ\x9a\xa6\x8dTt\v?Iw\xf9\xf8\x14ۻ\xb5;\x97\xd2{\xaf'\xe4\x8f\x06\xa5\x89]\xc0\xeb\xafA?\x02\xd6\xe4|\xf7.5\xb6\xa3\x9cyM\xb31\xfc\x14\xbe\xdf\xdd\xfd\xe4\xb02\xac\x80\xcbw\x95Kw\xb06Q\x83%q\xc0\xd6AZ\xdb\xff\xee\xe4#^\xfe\x1b\x8fc\x86\xc7$\x1ad\x14`\xb29\xa6 \xceB\xa9*\xb9\xa49\xa8k)6l;\x81\xdd/\x9d\xcaG\xd3l\x86?z\xe4\xea9*\xc0?s\x0e\x82\xf5y8\a\xfe\x81q\xd0nX\t\x06\xf8\xa6ߪ\xb6\xc7U\xb1v>\xdc\xc6~\xac;\x18\x98\xe3\x1cZ\x18\x8a.AY/\xca\x05\xad+\x1ddu\x18\xf1\x86#L\x18\xd8B\x7f\x158b\x81ݭ\xd28}\x06s\x82k\x99\x1fc\xf1\xad\x0e\xf2\xf7\xc3-\x8f8\xd9\ny\xc5n\xdcsN\xc8\xcd\xfd\xb5&\x95\xc81\\|\xff\x97\xdbYR\xb7\xef\xdc\\\x1f\xb4uʨ\xde\xc7[\xb5\x9c㖽pޱ\xdcD\x10\x18\x82\xd3z \xe5\x91\x19\x7fq\xd7yoZ\x1dZ\xf2\f=\xfd\x80W\xfaO?\xfe\xe0n\xfe\xf7O\xc6xu\xac\x14^\x93\xea_\x05\xc0kE\x9f\xf0\xfeC'\xf9K\xbf5\x06\x8a\xd2\xc4|\x8dis\xf8\xfd\x18\xc0\xdaO\x93\x86\xf2\x96V\xd2P!\xe6i\xeb\x83\xc8\xc6\x12˼5\x1a\xe1\xe6\x98>\xc6\bp\xed\xcfC\x9c\x8d\x005\xc0!\x02\xe8*\xcb@\xebM\xc5\xf9\xa1>\x8e\xf1\x95P\xe3\x03e\xfc|\xa4p\xd0\x06\x05\xc1\xa27\ni\x12a\x9f\xee\r\"\x0f\x9a\x1e\x8e*\xcd#\x85\xe7\x82φԆ\x16'=\xd8p\xdd\a\x83o\x19\xa9\xbc\x95TI\xeb\xb1Sݰ?6\xb94\xe0\\K\\dYh\x90\x13\u0603 vvv$\x0e\xcfẗ́\xe2O\xb8\xba\x19.\xccw!\x14\x12}\xb1\x89\xf8h\x87Ɨ\x81\xbe\xd35L\xcc\x15\xc5\xf7L\xfaD\xe8;\xbf.Zqe\xbd\x7fXZ\x10\xa7y\xadC\xaf\xb9t照\x19\xb9\xeb\xdb\xd5\x10\xb8SL\\\xff\xb9\x97'\xaaq\x1f\xdd'\x99\xb4>\xba\xb3\fZ\x04b-\xe3\xe7\xc7\x1dU\xfd\xb4Kݱ\xa5s8\xb2p\x86\x8er\xee\x0f:\x16\xa05݆\xdb\xdc\x1f\xed\xd2c\v\x02\\x\xcem\x9eD\x806\xa7\xe2\xbaw\x99;\x95\xa1\x99\xa9\xa8\xef $\xf8\xb6j}\xa7\t\x971\xa8\xf8\xa0\v\vO\xa8\x855\xd9LB})\x99JYý\xaf+Zڠ'\x8c\xdci\x1e\xbd\x03ζ\xf8\xa4\x93\xe5ܖ\xaa5\xdd\xc22\x93\x9c\x03Z\xeb\xfe\xb8\x9eS\xd7\xfd\xd9\xc3\xcf@\xf5$j\x1f\xdau\xfd\x0e\xa0\xe3\xb6\xdb\xf8\xa6.\xdd\x1d\x9f53LA\xf3\xc2`o@\x12;\x9e\xe5(;*D\x9f\xdf돴]7h\x9d7\xcb>\xce\xeb_\xdf[4/jE\xc6Y\xd0_\xa5Z\x90\x82\t\xfb\x0f\x15\xb9\xdb\xc0\v\x8dg\x8d\x7f'\xe5\xc3mĉ\xed\r\xfe\x87\xbab\xb3\xd5\xc1\x84\x1b6\x1e\x18]\xcb\xca\xef\xbe\xd7\x0em|[\x05o\xe6?\xf3r\x13a\x8e\xcc\a=t\x06#\xba?t MN\x05\xae\xe7\x01X\xb7\xe1\x897\xce\x0f\x8bc\xc8G\xcfI6\xb0[/\x17x7\xa0\xb9\x8f`\xa0\xa3\xb0#\x15\x05R_|\xd16觬z=\x99\x87\x9c\xc9\x1e\x8d\x7fhj\x0f\xd1\xd1\r\xb3\xe5\xee\r \xd8q\x02ϻ`\xc7g*&\x84\xff\xc6֩\xef.h-\xdcB\x96\xd8`\x94n襻\x8f\xd0߮X\x92\xbfVPEh\xb0\f\x0f\xc3\xdd\x1a\xaa\xfa!_w\f\x1er\xcc\xe8@m\x8cTY\x89\x1b%\xb7\nt_X\x97\xe4o\x94\x19&\xb6\x1f\xa4\xba\xe1Ֆ\x89O\xc3G~\xc6*\xdfPe\x98\x15v7\x9e\xd8@\x99\xa0\x9c\xfd=f\xd7\xda\x1f\xa7\x01]\x0f.\xb0\x96$a\x18C\x1fށ\xf5q\a\xe3\x02Q\x13Zz\xba\x9e\xe2\xaf\x04\x9eL\xd9\xd4ڗh|\x91\xd0\xed%\xf9(\xa3\x86\xc1\xa7C\xb1.L뒁6K\xd8l\xa42n\xb7z\xb9$l\x13\x82\x0f\xd6\xe6`\xdc\xcc=\xe2IXl\x9b\xb9N4i\xa6/\fz+\x9c\x85\xf1*\xfb\x82\x1e\xdc\xce\x14Ͳ\xcazX\xaf\xb5\xa1<\xe2\xe0<\xc9\xf0c\x94\xe7{|\xb0\xf2\x97'\xed\xe4\xadڀ\xfaAG\xecǑ\x14/\xd3p^\x1f\xb7(\x82 \x8f\x8a\x19c}*9\x92J\xe0Ie\xaco\xc59і\xd4'E\x1f\x893\xa3\xabᔜ4\x94\xefj(C\xe6\xd9c\x8d/3֯\x82\xfa\xec#_˲9\xdbQ\xb1\x1d\xbc\xa1`\xa7d\xb5\xdd\x05I\x1ep\xa6I^\x01\x06kѤ\xe8\xf0ⲩ\x94h\xa5\x12\x8c\x1c\xfb&A\x18p\xb84{\xc0\xf7K\u074b\xc6\xfe)\xeb\xd7\xfe\r\x94\xe5F\xc9b\xe9\xfb\xc5X\xea\xc2\xef\xe4+&\xad\xe7bvQ\xaa\x13\xe7\xb5\xfbg\x06P\x12\xca\x12\x04\xa1\xda\xf7\x9cpS\xd4\xc9\xd3\xd4ovj\xb8\x91\x9a%x\xfbQ\x8e\xff\xb5\r 0\xbc\f\x7fw\x99\xe1W0\xd8g\f\x8fO\xfe\b>\xec\xa90n9QO\x91\x17n\x12\xbb\x98\xb5\x90\xd1vb{R\x90\xe6\xb6\x03a\">\x83\xdd\xc5Yt\xeb\xd35\xdcE`\xd7\xfe\xf9\xd5\x1a\xf0\x82h&\u008b\xe0.\xf5\xc3I\x7ft'P\xe0C\x95Rų1\xc7\x03.]\x84^6ֲ\xaf=\x89\xf7'/\xc5\xef\x8f`\x1c\x1d\xea\xc6wI\xeb*a\xf9\xfc\a\x16\xdb\x0f\xc04\xde̢\xf2\xc7\xdf\xfd\xb0\xf6>i\xa9\x17\xa7\xc8\xd8\xca\x0f\x17u\xc3K\xb8\xee;\xa47\x1c\xac\xb6i\x80\xee\xa2r\x96\xce\xed\xcf\x18M;g(-\xbc}\x7f\x9eX\xd2\xfe\x8cA\xb4g\x8b\xa0\x9d\x17\xe5G\x8a\x0fD\x9f\xa4\xb5\x7f\xf3m#!4\x0f\xf6\xdcA\xb4V\f-\f\xfcE\xa3h\xd19\xb7\xf7#\xda\xe9\xbce-|O\xfe\x97\xff\x0f\x00\x00\xff\xff9i\xfd\xfe\xeb\x83\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfe\x15\x84\xeeav7\xba\xe5u\xdcG\\\xe8\xcd#\xdb;\x1d3ck-\x8d\xf6\x99\xae\xca\xeefDA\rP-\xf7\xde\xdd\x7f\xbf \x81\xfa袪\xa8VK\xe3\xdd5/\xb6\xba !?I\x92\x04\x96\xcb\xe5+Z\xb2{P\x9aIqEh\xc9\xe0\x8b\x01a\xffҗ\x0f\xff\xad/\x99|\xbd\x7f\xf3ꁉ\xfc\x8a\\W\xda\xc8\xe23hY\xa9\f\xde\xc1\x86\tf\x98\x14\xaf\n04\xa7\x86^\xbd\"\x84\n!\r\xb5?k\xfb'!\x99\x14FI\xceA-\xb7 .\x1f\xaa5\xac+\xc6sP\b\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xad+\xc6͒\x89v\x1f\x8f\x8c\xf3\xd0\xcb<\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa0\x9d2\x9dD\x8b\x01X-\xd2<\xee\xc0\xec@\x91R֮\xc0\x86q \xfa\xa0\r\x14\x9e0az\xf5\xf8DzB\xa3¹\a\xa1-]=\"}\xe4E\xc59]s\xb8\"FU0@\x9b\xb5\x94\x1c\xa8\x98 \xcegІe\xe7 \x8d\x83\x14!\x8c\xf2\x1f:\x14@o\x82>\x00\xa1\x11Оf\xd6m\xe1\xbcE\xd8.U\xa2c*\x15dv:\xbb\xf2\xd3$\x03\x8eS\xb3\x90\x84K\xb1\x05\xe5z\xb7V/\b\x98\x02+p9\xb13\x90\x02n\xa7Y\xb2\xa9\xec\xe4tI\xacv\x0f\xca\x00\x13\xda\x00\x8d\b\xe7\x13\xf8\x03_\xacu\x86\xfc\xday\xa4\xb7ֱ\xce\xc3B\xa37\x9d\xa4\xf0\xe9\xfd(D\xef\xb6p\x96\xa1w\xec\x1d\xe1%:\xf411m\xbc\x17;5\xe1jò\xd2\x0f\xbbqKF\xed\x81\x06c\x1b]\xfc\xe9b\x81\x1c\xee\xf6\xda\xedC\x13\xaa\xa0&K\xb2݄\xa24\x87~mf\xa0\x88PqԞ$\xf2\x93*E\x0f\x03ܬ\x17Fg\xe4\xe7\x10\xcc#\x8e\x8aP\xed\x85yz\xdc\xef?3W\xcf\xc3G\x8d\x01\x02ʄ\xe5\x9f]\xabwا\xdd\xc2֒MH\x13\x81\xe7\xfc:\xc8q\xcd:\u00ad߉Xg\x91\xf9!!\xafe\xcb\v\xef?$\xa5vR>LQ\xe7\a[\xa7Y-\x92\f\x03Qd\r;\xbagRyԛ\xa9\x16\xbe@V\x99\xa8\xd6SCr\xb6ـ\xb2p\xca\x1dՠ]\xfc`\x98 \xc3\xeb\x1a\xd22#яGx4\x8c\xb4lḂ\x86n\xfd\x88\xe3Y2\x14;P\xeb^\xe3d\x9c\xb3=\xcb+\xcaq^\xa6\"s\xf8\xd0z\\1+3\xc2\xe4ޘ\xa3\x92\xe9\x8as\b\x02R\x96I\x9d%\xa4\x14`}\xde®\t\xfaU\x871_S\xeb\xab\xc8!\xec\t2KU\x1c\xb4\xef*G7\xb2\xb1\x19\x8b\x86)\x18\xa1!\x9c\xae\x81\x13\r\x1c2#U\x9c\"S|v%\xc5\b\x0e\x102b\xf9\xba+\x8d\x06\x81\x11\x90\x04\x97p;\x96휫g\x85\b\xe1\x90\\\x82u\xf8\f\xa1e\xc9#\xd3ESF\x99\xef;\x19\xd3\xf5\xa6Lh\xfd1\xbc\x98\xfe7%\xc1f6%J\xdaF\xbf\xba\x94\xad\xc5!\xbe\xa6m\xca?'a\x83\xe5?AhG\xb4\x9f`\xb8,Y\xa6\a\xe5\xd6R\x95\x81\xbe\xb4\xee\x14z:\v\xc2L\xf8uJ\x13:>W/\x8a\xd8!\xc2\xd7͛\xf9B\x9fȚ\x14\x9dx&\xc6\xd4]\xfc\x03\xf2\x05\xa7\x8c[?c$\xf3\xe4\xa7v\xab\x05a\x9b\x9a\xe8\xf9\x82l\x187\xa0\x8e\xa8\x7f\x92\xa9\x0f\x9c9\a1Rf=\x82\xfb\x1a&۽\xffb]0\xddl\xee%\xd2帱sd\x83\xb7ߝ\x9e'\xe0\x12\x8c\xef3\x17m\u0557\xb8bj\xff\x82\xae\xd5ۏ\xef\xe2\xeb\xabvI\x90\xbc\x1e\"\x13J\xe7\xca\xdb#\x8c\xda\xe3\xf3.|\xf8\x82>P\xbd\x00r\xb1\xea\x05\xa1\xe4\x01\x0e\xceu\xa1\x82X\xfe\xd0P9\xa1{\x05\xb8Y\x85r\xf6\x00\a\x04\x13\xdf}\xea\x97Tip\xe5\x01\x0e)Վhh\xc7Ĵ\xdfU\xb3t\xb2? !p\xd3!U\f\\\xf1\xaa\x10\xd9뉗D[\x12J\xa0\xfd\th&\x89J\xbb\x8f\xf6\xf6-J\xc0w\xda\xf1\xd2j̎\x95hV1\xe2 7\xc9\fu\xe5\x9er\x96\xd7\x1d9\x1dY\x89\x05\xf9(\x8d\xfd\xe7\xfd\x17\xa6\xfd\x0e\xef;\t\xfa\xa34\xf8˳P\xd4\r\xfc9\xe9\x19v|,B\xce\xca[\x82\xb5\xf7(ݜf\xa5\xad\xa6=\xd3d%\xecrő$\xb1+\u070evݹ\x8e\x8aJ\xe3\xf6\xa2\x90b\xe9\xc26\xb1\x9e<\xbd\xa5\xea\x90\xfbɝ\xfa\x0e\xef\xecdᾸMqN3\xc8\xc3v\r\xee\xd6R\x03[\x96%\xf6W\x80\xda\x02)\xad\tO\x93\x88D\xc3걙'>i\xb3w\xbb|Y>\xd4\xc9\x0fK;\xe5,=\x04#\x8b\x04\x1ax\u06ddO㳴:\x9bP+H\xc2dՁ\xcd\xdc\xe1\xaa)Dy\x029p\x16G\x17g\x92\xbb4\xcf15\x88\xf2\x9b\x193\xca\fY\x98k\x1aZcwSpAq\xab\xe5\x7f\xecL\x8b\xda\xf4\x7f\xa4\xa4L\xe9K\xf2\x16s}8t\xbe\xf9\xa0Y\vLB\x97\x98\xabc\xe5gO\xb9\x9d\xfb\xad\x01\x17\x04\xb8\xf3\x04\xe4\xa6\xe7\x17-\xc8\xe3Nj7mכ8\x17\x0fpp;\x86\x93]\xb6\x8d\xcc\xc5J\\8\x1f\xa2g0j\x87C\n~ \x17\xf8\xed\xe2)\xaeT\xa2\xa4&V\xeb\x88hA\xcb4\t\xc5\\\xabTG\xdd.X\x83\x13b\x1b\xd69D\xd6\xc9\x1e\xc36IDK\xa9#\x1b\xf9\x03C\x99\x10\xde\x1b\xa9\x8d\x8b\x97u|\xe6h@M\x86 \x1a\xa1\x1b\x97\xd8%U\xc8±Fy*\xf4\xdb.w;\xd0\xe0\xf7+|`\xce\x01\xb5+\xbb\x8bF\xbf\x9d\xb5\xbfp\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xe6\x8bH\x96H\x1b\xf7:\xe6H\xdd*\xc9媌\x87@CIwy-!f\xae\x17\xde\x7fi\x05D\xad\xeeۿ\xa7dl\xee\xb8\bfY\x16\x05=\xce\xdfJ\x1a\xe2\xb5k\x19\xb4\xc1\x03r\x8b\x0f\xb5\xad\xd0\x12\xa4\xce\xe5\xb5\x00~\r\x8eB\xc1\xc4\n; o\x9e\xc1\xb1\xf064\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fN\x95K\x89[\x05\n:\xcc\xebG\xd5\xd1\x0f\x15Ҵ\x02\x123\xdc\xcdR\xe6\xdfi\xb2aJ\x9b\xf6\x10\xf4@\x9aJ\x14\xcc̅\x97x\xaf\xd4I\xeb\xaeO\xae\xe5Q\x02\x99\xcf[s\x84I\xc4\x1c\xf7\x97\x80\xb0\ra\x86\x80\xc8d%0\x80c\xf5\x18\xbbp\xc4u\x16\x96\xa5*I\x9a\xf6\x93\xc1\x1c\xb4XY\xa2\xa401\x1a\xe9iW\xff@Y?Q-Vf\xb2\xcd\fe\xb1\xc5\xcai:\x11R\xdcڙ\x8a\x05\xfd\u008a\xaa \xb4\xb0<\xc2ɜ\x15\xd0ez\x93\xf8f[\xe04a\xa4\u0558\x92\x83\x01\x9f\xbc\x968\x86L\n\xcdr\xa8'W/\bR\x10J6\x94\xf1J%Z\xc0Y䝳\x14\xf1\x96\xe0|k\x8c\xb4ΗH\x8a\x84hn\xa2\xaf8n\x8dK\x95\xee\xf1M\xb9Y\n\xe6{Y\xa5b\x12\xd3\x02\xcf\xech\xf9DJ*\x0e\xdf<\xadԡ~\xf3\xb4\xc6\xca7Ok\xa2|\xf3\xb4\xbeyZ)5\xbfyZ\xdf<\xadv\xf9\x97\xf0\xb4\xa6F\xe4\x0e:\x0e|\x9c\x1cE\xc2V\xf5\xd8\x10G\xe0\xfb\xe4\n\x9f\x03\xfe\xa4\\\xccU\x1cT$\xf1\x7f \xad;f\xb4\x9aɣNδZ\x13dޝ\xbb\x9ap%\x9f\x90u\x1f:=_\xd6\xfdj\x14♲\xee\xfd\xb0\xa7}\xec\x93r\xee\x03Q\xe6eg/|\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x99\xe8\xff\x85\x13s{Ycg\x94\x8fg\xcf\xe2O\x96\x91(K/\xfet\xf1\xf5\x91\xff<\x04\x1f$q\x9fv\xfe\xe0w\x04\xaa]\x81\xb6\xd3ºYx_\xa7\x18\x9fEnS3\xf1k\"F`uE\xf2\x88\x8a_\xab-0P|*\xfd\x8c\U001044ea\xab\b\x9c\xa4\xb3\xaaT\x1fD\xb6SR\xc8J\xfb\xa8\x84\x85\xf56s'\xfd\x03Ș\xb0F5\xfc?\xc8NV\x91L\xf0\x11\xf2Md\x04N#\xdfI\x0e\xf4\x9b\xd0`\xe8\xfe\xcde\xf7\x8b\x91>Up\xe8l\xf3\xe3\x0e\x04\uec0bm\xfb\x00@\xb8\xa8\xc1\xdfXp,`\x11@R\x11\xc1\xb8\x93\xbc\xfa\x9a\x87\xb6ܑO\xa5\x8b=\xcd\xf6;\xc6c*iɄ'\xa7\x10vS\x04\a\xfcҹ\xbb\xddg92\xf1\xbb\xa4\x06\xceO\bL\x89\x88M$\xff\x9d\x90\xf2\x97\x98[\xfc\xe4\xed\xf9\x94\xa4\xbe9+\xe6gK\xe0;\x7f\xda^\x12}\xa6S\xf4\xe6P\xe7\xd9\xd3\xf1^0\t\xefeR\xef\x12\x13\xeeΗ9\x9f\x16\x8f=)sl:t0\x9c47\x99*7\x19Z\x98Bl6J\x93)ps\x12\xdf&\xb9\x93\xa6f/\x96\xda\xf6b\tm/\x9b\xc66*E\xa3\x1f\xe7$\xaa\xc5\xef\xeb!\x93\x93m\xef>\xb5^\x85sJ\\r,nP\xe9\xf8K)ǩl\x93\xaa\xe3n\x9f\xb4\x1e\xfct\x04\xc3\njpE_ȧ/*nX\xc9q\xe3w\xcf\xf2hp\xc4\xec\xe0P_\xf8\xf1\xabģ\xb2\xfe\xe6\x9aO\x9fk-\xbb\xb8\xf4\x9b\xe8\xd4\xfc\x9ef\xbb#\xe8;\xaa\xc9F\xaa\x82\x1arQoX\xbev\xc0\xed\xdf\x17\x97\x84|\x90u\x0eG\xfb\x1e!͊\x92\x1f\xec\n\x85\\\xb4\x1b\x9c&\x01Qi\v\xbd\xddHβ\x88\xef\x16\xbdK\xcaU\xee]\xee\x817\\e\xed\x14\x87\xd2V\x8c\xbbn\xe8\xe6u\xaf\xec\xdcH\xce\xe5\xe3\xdcXE\xc9\xfe\x82\x97\xb3?!\x9a\xf5\xf6f\x850\x82x\xe0m\xefu2Y\x8d\xcd\x1a\xec\xb4\xdc\xe09\xa4\xfb\xabM\ab7/\xb3}\xcb1\xe4\xeeB\xeb\xe0\x16xәIk]nVn\x1cC\xbdX\x99\xa1\xe2@$f\x00\x99\x1dS\xf9\xb2\xa4\xca\x1c\\bɢ3\x860\x97\x8eE\xa3\x06g\x8f\xfe%\xddQ\U00086ef9qG\xf5Pv7\xa9\x8fiw\xca8\x86O[N\x9e\xb3<\xe38\x86ݒ%R*\xf2s4S\xedlQ>\xedoR\xfeY\xee\xe1]4\xda\xd7!\xcf\xedQ\xf5H:Y\x80\xe8.\t\x1e̪]\x03^ \xdc\xff\xf4\x84\xfc\xb0е\xbf\x03\xf6\x94@\xd9m\x17D\x04\xbfp#n\xe8,f\x9f\xf0&\xff\x03\xb9\xb9\xc75Zmڼ\x8a\xfa5Z\b\x95\x85\xcd\xeb\b\x1c\xdf\xe0\xfb\xf3\xa7\xd2i#\x15\xdd\xc2O\xd2]\x96>\xc5\xf6n\xed\xce%\xfa\xde\xeb\t\xf9\xaeAib\x17\x06\xfbkۏ\x8059\xea\xbdK\x98\xed(g^+m\f?\x85\xefww?9\xac\f+\xe0\xf2]\xe5\xd23\xacM\xd4`I\x1c\xb0u\x90\xd6\xf6\xbf;\xf9\x88\x97\x15\xc7\xe3\x98\xe1\xf1\x8b\x06\x19\x05\x98\x1c\x8f)\x93\xb3P\xaaJ.i\x0e\xeaZ\x8a\r\xdbN`\xf7K\xa7\xf2\xd14\x9b\xe1\x8f\x1e\xb9z\x8e\n\xf0Ϝ3a}\x1e\u0381\x7f`\x1c\xb4\x1bV\x82\x01\xbe鷪\xedqU\xac\x9d\x0f\xb7\xb1\x1f\xeb\x0e\x06\xe68\x87\x16\x86\xa2KP\u058brA\xebJ\aY\x1dF\xbc\xe1\b\x13\x06\xb6\xd0_\x05\x8eX`w\v6N\x9f\xc1\x9c\xe0Z\xe6\xc7X|\xab\x83\xfc\xfdp\xcb#N\xb6B^\xb1\x1b\x02\x9d\x13rs\x7f\xadI%r\f\x17\xdf\xff\xe5v\x96\xd4\xed;7\xed\am\x9d2\xaa\xf7\xf1V-\xe7\xb8e/\x9cw,7\x11\x04\x86\xe0\xb4\x1etyd\xc6_4vޛa\x87\x96ڡ\xf1%\xa3\xeft\r\x13s[\xf1\xfd\x95>\x11\xfaί\x8bV\\Y\xef\x1f\x96\x16\xc4i^\xeb\xd0\xeb3\xddy\xe1iF\xee\xfav5\x04\xee\x14\x13\xd7\x7f\x9e\xe6\x89j\xdcG\xf7I&\xad\x8f\xee,\x83\x16\x81X\xcb\xf8\xf9qGU?\xed\x12zl\xe9\x1c\x8e,\x9c\xf9\xa3\x9c\xfb\x83\x99\x05hM\xb7\xe1\xf6\xf9G\xbb\xf4\u0602\x00\x17\x9es\x9b'\x11\xa0\xcd)\xbe\xee\xdd\xebNehf*\xea;\b\tɭZ\xdfi\xc2e\f*>@\xc3\u0093oaM6\x93P_J\xa6R\xd6p\xef늖6\xe8\t#w\x9aG\xfa\x80\xb3->Ae9\xb7\xa5jM\xb7\xb0\xcc$\xe7\x80ֺ?\xae\xe7\xd4u\x7fV\xf23P=\x89ڇv]\xbf\x03\xe8\xb8\xed6\xbe\xa9K\xcf\xc7g\xd8\fSм\x88\xd8\x1b\x90Ďg9ʎ\n\xd1\xe7\x02\xfb#m\xd7\rZ\xe7Ͳ\x8f\xf3\xfa\xd7\x02\x17\xcd\v`\x91q\x16\xf4W\xa9\x16\xa4`\xc2\xfeCE\xee6\xf0B\xe3Y\xe3\xdfI\xf9p\x1bqb{\x83\xff\xa1\xae\xd8lu0ᆍ\a\\ײ\xf2\xbb\xef\xb5C\x1b\xdfV\xc1\x97\x04μ\xdcD\x98#\xf3A\x0f\x9d\xc1\x88\xee\x0f\x1dH\x93S\x81\xeby\x00\xd6mx\x92\x8e\xf3\xc3\xe2\x18\xf2\xd1\xf3\x97\r\xec\xd6K\v\xde\rh\xeeO\x18\xe8(\xecHE\x81\xd4\x17u\xb4\r\xfa)\xab^O\xe6!g\xb2G\xe3\x1f\x9a\xdaCtt\xc3l\xb9{\x03\bv\x9c\xc0\xf3.\xd8\xf1Y\x8d\t\u1ff1u\xea\xbb\x16Z\v\xb7\x90%6\x18\xa5\x1bz\x99\xef#\xf4\xb7+\x96\xe4\xaf\x15T\x11\x1a,\xc3Cv\xb7\x86\xaa~\xc8\xd7\x1dۇ\x1c3:P\x1b#UV\xe2Fɭ\x02\xdd\x17\xd6%\xf9\x1be\x86\x89\xed\a\xa9nx\xb5e\xe2\xd3\xf0\x11\xa5\xb1\xca7T\x19f\x85ݍ'6P&(g\x7f\x8fٵ\xf6\xc7i@׃\v\xac%I\x18\xc6Їw`}\xdc\xc1\xb8@Ԅ\x96\x9e\xae\xa7\xf8+\x81'S6\xb5\xf6%\x1a_$t{I>ʨa\xf0\xe9P\xac\vӺd\xa0\xcd\x126\x1b\xa9\x8cۭ^.\tۄ\xe0\x83\xb59\x187s\x8f\x8e\x12\x16\xdbf\xae\x13M\x9a\xe9\v\x83\xde\nga\xbcz\xbf\xa0\a\xb73E\xb3\xac\xb2\x1e\xd6km(\x8f88O2\xfc\x18\xe5\xf9\x1e\x1f\xd8\xfc\xe5I;y\xab6\xa0~\xd0\x11\xfbq$\xc5\xcb?\x9c\xd7\xc7-\x8a ȣb\xc6X\x9fJ\x8e\xa4\x12xR\x19\xeb[qN\xb4%\xf5I\xd1G\xe2\xcc\xe8j8%'\r\xe5\xbb\x1aʐy\xf6X\xe3K\x92\xf5+\xa6>\xfb\xc8ײl\xcevTl\aoT\xd8)YmwA\x92\a\x9ci\x92W\x80\xc1Z4):\xbc\x10m*%Z\xa9\x04#\xc7\xd4I\x10\x06\x1c.\xcd\x1e\xf0\xbdU\xf7\x02\xb3\x7fz\xfb\xb5\x7f\xb3e\xb9Q\xb2X\xfa~1\x96\xba\xf0;\xf9\x8aI빘]\x94\xea\xc4y\xed\xfeY\x04\x94\x84\xb2\x04A\xa8\xf6='\xdclu\xf24\xf5\x9b\x9d\x1an\xa4f\t\xde~\x94\xe3\x7fm\x03\b\f/\xc3\xdf]f\xf8\x15\f\xf6\x19\xc3㓿2\x00\xf6T\x18\xb7\x9c\xa8\xa7\xc8\v7\x89]\xccZ\xc8h;\xb1=)Hsہ0\x11\x9f\xc1\xee\xe2,\xba\xf5\xe9\x1a\xee\xe2\xb2k\xff\\l\rxA4\x13\xe1\x05s\x97\xfa\xe1\xa4?\xba\x13(\xf0aM\xa9\xe2٘\xe3\x01\x97.B/\x1bk\xd9מ\xc4\xfb\x93\x97\xe2\xf7G0\x8e\x0e\xa1\xe3;\xaau\x95\xb0|\xfe\x03\x8b\xed\a`\x1aofQ\xf9\xe3\xef~\xb8|\x9f\xb4ԋSdl凋\xba\xe1%\\\xf7\xdd\xd4\x1b\x0eV\xdb4@wQ9K\xe7\xf6g\x8c\xa6\x9d3\x94\x16\xde\xea?O,i\x7f\xc6 ڳE\xd0\u038b\xf2#\xc5\a\xadO\xd2ڿ\xf9\xb6\x91\x10\x9a\a{\xee Z+\x86\x16\x06\xfe\xa2Q\xb4\xe8\x9c\xdb\xfb\x11\xedt\u07b2\x16\xbe'\xff\xcb\xff\a\x00\x00\xff\xff\x11\r8\xff\x9b\x84\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccZK\x93۸\x11\xbe\xebWt\xed\x1e\xf6\xb2\x94줒J\xe96\x96\x93*W\xc6\xf1\xd4hvr]\bhJ\xb0@\x80\x01@\xc9\xca㿧\x1a\x0f\x89\xe2C\x0f;q\u008b-\x12h\xf4\xf3\xeb\x0f\xc0\x14E1a\xb5|E\xeb\xa4\xd1s`\xb5\xc4/\x1e5\xfdr\xd3\xed\x1f\xdcT\x9a\xd9\xee\xedd+\xb5\x98âq\xdeT\xcf\xe8Lc9\xbe\xc7Rj\xe9\xa5ѓ\n=\x13̳\xf9\x04\x80im<\xa3\u05ce~\x02p\xa3\xbd5J\xa1-֨\xa7\xdbf\x85\xabF*\x816\b\xcfK\xef\xdeL\xdf\xfe~\xfa\xbb\t\x80f\x15\xcea\xc5\xf8\xb6\xa9\x9d7\x96\xadQ\x19\x1eENw\xa8К\xa94\x13W#\xa7\x15\xd6\xd64\xf5\x1cN\x1f\xa2\x84\xb4z\xd4\xfc]\x10\xb6\x8c\xc2\x1e\x93\xb0\xf0]I\xe7\xff<>\xe6Q:\x1f\xc6ժ\xb1L\x8d\xa9\x15\x86\xb8\x8d\xb1\xfe/\xa7\xa5\vX9\x15\xbfH\xbdn\x14\xb3#\xd3'\x00\x8e\x9b\x1a\xe7\x10f\u05cc\xa3\x98\x00$\xd7\x04i\x050!\x82\xb3\x99z\xb2R{\xb4\v\xa3\x9aJ\x1f\xd7\x12踕\xb5\x0fΌ\xb6@2\x06\xb25\xe0<\xf3\x8d\x03\xd7\xf0\r0\a\x0f;&\x15[)\x9c\xfd\xa2Y\xfe\x7f\x90\a\xf0\xd9\x19\xfd\xc4\xfcf\x0e\xd38kZo\x98\xcb_c\x8c\x9eZo\xfc\x81\fp\xdeJ\xbd\x1eR\xe9\x919\xffʔ\x14A\x93\x17Y!H\a~\x83\xa0\x98\xf3\xe0\xe9\x05\xfd\x8a\x1e\x02r\x11B\xf6\x10\xec\x99K\xeb\x00좔\xe0\xa3aMUo\xad3\xb5I\x15x\xedH\x89\xfaӛ\xa4}Kl\xce\xef)\xb7x\x14\xe9<\xab\xea3\xb9\x0fk\x1c\x13v\xe6\x8a\xf7X\xb2F\xf9\xb6\xa9\x14%\xd5\xce\xcbs\xb3j\xe4S\x11g\x9d\xad\xf8\xfe\xec]\\ue\x8cB\x16\xa5\xc4Q\xbb\xb71\v\xf9\x06+6O\x83M\x8d\xfa\xe1\xe9\xc3\xebo\x97g\xafa(\x91:EA\x81c\xad\xd8l\xd0\"\xbc\x86\xfa\x8bqsɴ\xa3L\x00\xb3\xfa\x8cܟ\x82X[S\xa3\xf52\x17K|ZX\xd4z\xdb\xd1\xe9\x9f\xc5\xd97\x002#\xce\x02A\xa0\x841\xafR\xfd\xa0H\x96\x83)\xc1o\xa4\x03\x8b\xb5E\x87:\xc2\x14\xbdf:)8\xed\x88^\xa2%1Tۍ\x12\x84e;\xb4\x1e,r\xb3\xd6\xf2\xefG\xd9\x0e\xbcI\xc9\xec\xd1y\b\x15\xaa\x99\xa2dm\xf0g`Zt$W\xec\x00\x16iMhtK^\x98\xe0\xbaz|\xa4j\x90\xba4s\xd8x_\xbb\xf9l\xb6\x96>#47U\xd5h\xe9\x0f\xb3\x00\xb6r\xd5xc\xddL\xe0\x0e\xd5\xcc\xc9u\xc1,\xdfH\x8f\xdc7\x16g\xac\x96E0DGH\xadď6a\xba;[\xb6W\xd2\xf1\t\x90zGx\b^c\xcaDQ\xd1\xc4S\x14\xe8\x15\xb9\xee\xf9\x8f\xcb\x17Ț\xc4HŠ\x9c\x86\xf6\xfc\x92\xe3Cޔ\xbaD\x1b\xe7\x95\xd6TA&jQ\x1b\xa9}\xf8\xc1\x95D\xed\xc15\xabJzJ\x83\xbf5\xe8<\x85\xae+v\x11\xba\x18\xac\x10\x9a:\x80Dw\xc0\a\r\vV\xa1Z0\x87\xdf9V\x14\x15WP\x10n\x8aV\xbb7w\aG\xf7\xb6>\xe4\x9e:\x12\xdaA4X\xd6\xc8\xcf\xeaN\xa0\x93\x96*\xc33\x8f\xa1\xba:\x0eJP1ޔ\xf33\f\x12\xf40\xceѹ\x8fF`\xf7KG\xe5\x87\xe3\xc03\x1dk\xb4\x95t\xa1\xbdBil\xb7\xf3\xb0#\x92\xb7\x9f\x8cx݀\x03\xa0n\xaa\xbe\"\x05<#\x13\x9f\xb4:\x8c|\xfa\xab\x95\xbe\xbf\xd0H \xe9\x89*.\x0f\x9a?\xa1\x95F\\1\xfe]g\xf8\xd1\x05\x1b\xb3\x872\xe4\xbf\xf6\xea@\xd8\xe5\x0e\x9a\xf7Q;?\x0fO\x1f2\x82\xc7\xdaJ\x85\x99|5\x85\x87TԦ\x847 \xa4#\"\xe1\x82о\xb3t\xa3\x02ј\x83\xb7\xcd]\xe6s\xa3K\xb9\xee\x1b\xdd\xe6Fc\x19sEt\xc7s\x8b\xb0\x12\xa1\x16eGm\xcdN\n\xb4\x05Շ,%O\x9a46v\x90R\xa2\x12=l\x1a\xad\xb2`\x8aEAE\xcdԕ\x18.\x8e\x03\x03\x93fR\xc7\f>\t\bXc\xabԚ\xb5G-\xb0\xdbm\x826&\x00\x9aC\x01{\xe97\x11)\xd5P\xdd\xc1\xc5ڣg\x8b\x87\xa1\xd7\x1d\xdd_6H#c\xe3Ep\xc8-\xfa\x90m\xa8(}(\x95\xa6\x00\x1f\x1b\x17\xb0\xb6\x8b\x13\xf9\t\x84/\xcf\xde\xe2\xa1\xefh\xb8\x16\xdcD\x85FT\x0e$j\x0e?\xfcpݤ^w\xcb\x0fQ\xf7l\xa8\xc5\x12-\xea\x1e\x9b\xc8\xcfK\xe8Q\x944\x94aX\x96Ƚܡ:\x84\x9eD\xe0\xf93\xac\x1a\x0f\xa2\xc1\x105Ʒ{f\x85\x03n\xaa\x9ay\xb9\x92J\xfa\x03H7\"\x9f)e\xf6(Rı\xaa\xfda\n\x1f\xb4\xf3LstG\x1eD\x1e\x8b\xa9\xc0t\x1c\x95\xaa8\x10:f\x8700\x8a\xaf\x8c\xf3\xc0\xd1R:\xaa\x03\xec\xad\xd1\xeb1c\a\xda!\xed\x01\xadF\x8f\xa1#\n\xc3\x1d5C\x8e\xb5w3\xb3C\xbb\x93\xb8\x9f\xed\x8d\xddJ\xbd.H\xc1\"\x81\xcf,\xec\xecf?\x86\x7f\xbe&\vL\x1dq\xe2\x86\xe4]\x86Z?\x10\xbd\xf5\x1b\x8c-b\x19s\xd0X \x02A\xa9]\xa5܍\xc8:TvC\xbc\xbc\xfd\xe4\x90\x0f\xf5\x8f-\xf6[\xc7\x05P\x01\xf8R\x9c|[T\xac.\xe2h\xe6M%\xf9\xa4km\xcc\xfb\xcb\xf8\x937+R\vɉܞ\xe3F\xdeĉ\xb3=̀\x1b\xba\xbb\x9c1\xb4\x1cvS47q\x85+\x1a\x7fj\x8f=m}#t\xa7\xfe\xef\xd0\x13\xeft\xa0\x91\xf8\x01\xb3}?\a\xc0\xe4FkB*o\x80\x1d\xdb\xc0O\xae\xdb\xff\xeeD\xcfU÷8\xe0\xf8\x9e)\xef\xc2\xc0\xec\xe38\x8dti\x1c\x86\xc6tM\r\xb8^\x11\x9c-\xd0ޢ\xcb(\xf2-\x1eH\u0091[0X<\xc0\xaa\xd1BaVu\xbfAM\xdb1Y\x1e\x88\xec\xbf<.\xb3c\x03\x01K[\xa7\xec\xde1 yO\xbb\x00JA1\x87_\x1c\xa6u\x9f\xb1\x04\xa9\x9dG\xd6#\xe9\xf1\x89\xbdq\x0e\xab\xc3\x00\u05fa\xd9A\xcfX~\xbb\x8f\x82\xae\xe4\xa1\xd4 8\xc6\xc4J\xb0\x92\xfa{\xde\x0f-\x1e\x02\xc4\x12\xdf \"}\xe6ґe\xeetth\xd0i\xf1\fdR\xc7\x02a\xd5\xd8\":\x1fR\x01\x8by9H䇃q\xb9.\xe0\x12\xb3\xe89\xfb>v1*\x13\x80\xdd\xc80\xe0z\xb2\xc0E\xa6\x017\xb0\x8d\x9e\x99\xa39\x05w\xb2\x0e\xf8\x0e\xcc\x03\xfe\xfb\xec\x03\xeef \xf0\xddY\bܖ)\x97\xd9\b|\x13#\xb9\xe0\x8bK\\\x05\xae\xf2\x15\xb8\xc8Y`\x94\xb7\xc05\xee\x02w\xf2\x17\bx\x82\xa5\xfcr\x032?\x85\x81\xb9\x93\xd6\xcco\xa8kH\x81\xc0\x06\xfaj<\xa1\x18q\xd0q\xd3\xfb)\x85\xef+\xfa\xee%\xd2\x17չ\x87\xf7e@\xbfB\x8c\x9eҰ\xa3\x17\xf2\xef\x04 \xe7\a c\x04mТ\xdd\xf1\xb4\xfdO\xf1X\x81\x0f\xa0\xf8\x992\xaf\xfd\x19\x17\x8e'\xf2\x99\xff\x10K\xa3Ͱ\xb1\x16]m\xb4\xa0\xb6w\xdb\xe1\xc4I\xe5\xff\xdc\x11\xc5pX\x8bs\xfa\xda\xf9\x96\xa3p\xd3\xf9\\\xb8߸\xfb\x84.\xde\xfa\xb4Ͽ\xccʡݵ\x0e\xe9:6~\x97\xb3\xb9\xc1\xce\xd6:\xb0#\xaa\xa4\xa1\xd1\xe1\xc8\"4\xad\xe9d`F\x9b\x17\xfa\xd0<\xa4\x03m\xf64\xb9%-v=\x13\xe9M8\xb4dZ\xa4\xe3b\xfa4 y/\x95\xa2\x1ef\xb12\xe4,\xd4^Zj\x96,\xb4\xb1\xddo\xa6o\xfewg\x81\x8a9\xbf\xc6Qq\xff\x99\xa6\x00[\x99\xc6\x0f\xf4\xfeV\xc2\x0f\xd6t\xb8e\xbfG\xc7\xf0\xb7\x03\xd7\xe8\t\x8d\xc9\x11፵\xe1\xb2._\"ݱ\xd1\x1cC\xe0\x87Ο8\xb4\xbf\xf5\xff\x00\xe2\x06\xbb\x06\xbbt\xefe촭\xb8&'\xb7\xdf4\xab\xe3\x15\xec\x1c\xfe\xf1\xafɿ\x03\x00\x00\xff\xff%\xff\\)\x99#\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVK\x8f\xdb6\x10\xbe\xfbW\f\xd0k%wQ\xb4(tk69,\xda\x06\xc6n\x90;M\x8e-f)\x92\x9d\x19z\xeb>\xfe{AR\xf2C\xb67\xc9%\xba\x99\xf3\xe4\xf7\xcd|t\xd34\v\x15\xedG$\xb6\xc1w\xa0\xa2ſ\x04}\xfe\xc5\xed\xf3/\xdcڰ\xdc\xdd-\x9e\xad7\x1d\xdc'\x960<\"\x87D\x1a\xdf\xe2\xc6z+6\xf8ŀ\xa2\x8c\x12\xd5-\x00\x94\xf7AT>\xe6\xfc\x13@\a/\x14\x9cCj\xb6\xe8\xdb\xe7\xb4\xc6u\xb2\xce \x95\xe4S\xe9\xdd\x0f\xed\xdd\xcf\xedO\v\x00\xaf\x06\xec\xc0\xa0C\xc1\xb5\xd2\xcf)\x12\xfe\x99\x90\x85\xdb\x1d:\xa4\xd0ڰ\xe0\x88:\xe7\xdfRH\xb1\x83\xa3\xa1Ə\xb5k\xdfoK\xaa7%\xd5cMU\xacβ\xfcv\xcb\xe3w;zE\x97H\xb9\xeb\r\x15\a\xee\x03\xc9\xfbc\xd1\x06̚\xaa\xc5\xfamr\x8a\xae\x06/\x00X\x87\x88\x1d\x94ب4\x9a\x05\xc0\bH\xc9Հ2\xa6@\xac܊\xac\x17\xa4\xfb\xe0\xd2\xe0\x8f\x95\x905\xd9(\x05\xc2\x0f=\x96\xcbC\u0600\xf4\b\xb5\x1cH\x805\x8e\x1d\x98\x12\a\xf0\x89\x83_)\xe9;h3\x92mu͍\x8c\x0e\x95\x847\xf3c\xd9\xe7\x86Y\xc8\xfa\xed\xad\x16X\x94$\x9e\x9a(um\xf0@'ȟ7P\xfc\xdb\xd8+>\xaf\xfeT\f\xb7*W\x9f\xdd]EZ\xf78\xa8n\xf4\r\x11\xfd\xaf\xab\x87\x8f?>\x9d\x1d\xc3y\xafWH\aˠ\xa6N3p\x155\b\x1e!\x10\f\x81&T\xb9=$\x8d\x14\"\x92؉\xff\xfa\x9d\xac\xd5\xc9鬅\x7f\x9b3\x1b@\xee\xbaF\x81\xc9\xfb\x85\\@\x1c\x87\x02\xcdx\xd1\n\xaee \x8c\x84\x8c\xben\\>V\x1e\xc2\xfa\x13jig\xa9\x9f\x90r\x9a<\xaeə\xbc\x96;$\x01B\x1d\xb6\xde\xfe}\xc8\xcd\xf9\u07b9\xa8SR \xc9c畃\x9dr\t\xbf\a\xe5\xcd,\xf3\xa0\xf6@\x98kB\xf2'\xf9J\x00\xcf\xfb\xf8#\x83h\xfd&tЋD\xee\x96˭\x95Ilt\x18\x86\xe4\xad\xec\x97E7\xec:I ^\x1aܡ[\xb2\xdd6\x8ato\x05\xb5$¥\x8a\xb6)\x17\xf1Ep\xda\xc1|G\xa3<\xf1Yً\xe9\xa9_ч\xaf\xa0'\xabE\x9d\x91\x9a\xaa^\xf1\xc8B>\xca\xd0=\xbe{\xfa\x00S'\x95\xa9J\xca\xd1\xf5\x02\x97\x89\x9f\x8c\xa6\xf5\x1b\xa4\x1a\xb7\xa10\x94\x9c\xe8M\f\xd6K\xf9\xa1\x9dE/\xc0i=X\xe1ib3u\xf3\xb4\xf7E\x90\xb3\x02\xa4h\x94\xa0\x99;b\xa3\x13Q\x19\xbe\x83ΫkA_\x8a\x05\x12\x05\xba8\x9d5\xf5\xae8\x95\xff\x12\xcaz\x06\xe5\xf7c H\xaf\x04^\x90\xf2\x86萲Z\xa1\x01\x93.\xf0\x1ba9}\x93\"\x05\x8d|\xb1\x8a\x00Vp\xb8\xd2\xd3+\xec\xe4\xcf'\xe7\xd4\xdaa\aB\to0\xab\x88\xd4~f+o\xdfg Xe\x9fk\x1c\x1c\xde\xfbϒP\xe0\xf6i\xb8\xac\xd4\xc0{|\xb9r\xfa\xe0W\x14\xb6\x84<\x1f\xf9l\\U\xf4p\xfe\x1e\xbc\x82\xd2ա\xbc8\xe4,\x85\xe6\x04E\x96@j{\x8a+\xa7\xf5A\xe9;\xf8\xe7\xbf\xc5\xff\x01\x00\x00\xff\xffߙ6&\xcb\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcW\xcdn\xe36\x10\xbe\xfb)\x06\xe8\xa5\x05Vr\x83\xa2E\xe1[\xeb\xec!\xd8t\x1b\xc4\xdb\xdc)j$\xb1\xa1H\x96\x1c:\x9b\xa2\x0f_\f)\xf9G\x96\x1d粺y8\x9c\x9fof\xbe\xa1\x8b\xa2X\b\xa7\x9e\xd0\ae\xcd\n\x84S\xf8\x95\xd0\xf0\xafP>\xff\x1aJe\x97ۛų2\xf5\n\xd61\x90\xed\x1f1\xd8\xe8%\xdeb\xa3\x8c\"e͢G\x12\xb5 \xb1Z\x00\bc,\t\x16\a\xfe\t \xad!o\xb5F_\xb4h\xca\xe7Xa\x15\x95\xae\xd1'\xe3\xa3\xeb\xed\x8f\xe5\xcd/\xe5\xcf\v\x00#z\\Am_\x8c\xb6\xa2\xf6\xf8O\xc4@\xa1ܢFoKe\x17\xc1\xa1dۭ\xb7ѭ`\x7f\x90\xef\x0e~s̷\x83\x99\xc7l&\x9dh\x15\xe8\xd3\xdc\xe9\xbd\x1a4\x9c\x8e^\xe8\xd3 \xd2a謧\xcf{G\x05\xf0y>R\xa6\x8dZ\xf8\x93\x9b\v\x80 \xad\xc3\x15\xa4\x8bNH\xac\x17\x00C\xf6\xc9P1$\xbe\xbdɦd\x87\xbd\xc8\x1e\x00\xacC\xf3\xdb\xc3\xdd\xd3O\x9b#1@\x8dAz\xe5(a\xf8_\xb1\x93\xc347P\x01\x04\f\xe1\x00\xd9]\x84 \f\bO\xaa\x11\x92\xa0\xf1\xb6\x87J\xc8\xe7\xe8\xc0V\x7f\xa3$\bd\xbdh\xf1\x03\x84(;\x10l%+\x1c\xf8Ҷ\x85Fi,w2\xe7\xadCOj\x04)\x7f\a\xbdv \xbd\x94\x05\x7f\x9cx\xbe\x0557\x1d\x06\xa0\x0eG\xf0\xb0\x1e\xb0\x02\xdb\x00u*\x80G\xe71\xa0\xc9m\xc8ba\x86lʉ\xe9\rz6\xc35\x8d\xba\xe6^ݢ'\xf0(mkԿ;ہ\x11c\xa7ZP\x02\xd3\x10z#4l\x85\x8e\xf8\x01\x84\xa9'\x96{\xf1\n\x1e\x13\x82\xd1\x1c\xd8K\x17\xc24\x8e?\xacGP\xa6\xb1+\xe8\x88\\X-\x97\xad\xa2q\x02\xa5\xed\xfbh\x14\xbd.\xd30\xa9*\x92\xf5aY\xe3\x16\xf52\xa8\xb6\x10^v\x8aPR\xf4\xb8\x14N\x15)\x11\x93\xa6\xb0\xec\xeb\xef\xfc0\xb3\xe1\xc8-\xbdrC\x06\xf2ʴ\a\aip\xdeQ\x1e\x1e\xa5\xdc]\xd9TNq_\x05\x161t\x8f\x1f7_`\x8c$Wjh\xb1\x9d\xea\t.c}\x18Me\x1a\xf4\xf9^jS\xb6\x89\xa6vV\x19J?\xa4Vh\bB\xaczEa\xecu.\xdd\xd4\xec:\xb1\x14T\b\xd1Ղ\xb0\x9e*\xdc\x19X\x8b\x1e\xf5Z\x04\xfcƵ⪄\x82\x8bpU\xb5\x0e\xb9w\xaa\x9c\xe1=8\x18\x99\xf3Li'\x94\xb1q(\xb9\xb0\x8c-\xdfT\x8d\x92y\xa4\x1a\xebA\xec\x19d@\xfa\x18\xa8y\x06H\xc1\t\xdf\"M\xa5\x93X\xbe$%v\xff҉c\xc2\xfa\x1e˶d\xce\tC \x99\x8f~\x98\x16\xeaR\f0\xdb賑\x8c\xfd\xcd00\xaeL(Lv\x871\x9d\xba\xe6\x0fM\xec\xe7\x1d\x14\xf0{\x8a\xf9\u07b6\x17\xcf\xd7\xd6\x10\xcf\xc5E\xa5'\xabc\x8f\x1b#\\\xe8\xec\x1b\xbaw\x84\xfd\x9f\x0e}\xde\xd0\x17U\xc7E\xbfۊ\x17\x14\xa3>\xeb\xf7\x11y\x83\xe0\xf9L\a\x85\xab\xac\\\x11ӠyU\xa2\xeb\xcd\xdd{ <\xa3\xfe\x8e\"ݙƾ\x91\xe2^qV\xef\f\r\x8c_zC\xbc\xdd\xd3\xfc\n\x19{\x9a\xaf\xe4݉\xf0)V\xe8\r\x12\x86=S\xbf(\xeaf-\x02\xbctJv\xe9b\x1a\b^\x02!X\xa9\xe6(\xf5\x8a\xf0\x99G\x94Ǚ\xa1,ҰΈ9\xf8\x13\xf1\x19\xf6;\xe7\xa0\x18\x18\xe9*\x06%A1\xbc\x83C\x93\xfe\b\xb5\x8cާ\x15\x95\xa5\xfc2\x99^\xb8\x96DG\xe6\xf9\xeb\xf1\xfe\r&\xbd\xddk\xa6ǸP&G\xe3<\x16A\xb5\xfc\x82\xe23\xe6\xd2\xc4q\xa7`\xe4\xef\xf8\x85w\f\xd4lE\xf1\xabSy\x00\xdf\b\xf1\xe3N1\x13>\x9a\xbc\xe7\xa7o\xd8d\x10\x03?\xb7@\n3\x13c\x85P\xa3F\xc2\x1a\xaa\u05fc\xb9^\x03a\x7f\x1awc}/h\x05\xbc\xff\vR3md\xa2֢Ҹ\x02\xf2\xf1\\\x97\xcd&\xee:\x11f\xc6\xf0(\xe7\a֙k\x8c\xdd0^\xec\f8\xbb_\n\xf8\x8c/3\xd2\ao%\x86\x80\xa7ct6\x93\xd9!8\x11\x06~\xa4\xd5\a(\r\x7f\x19\x06\xc9\xff\x01\x00\x00\xff\xff\xe5\x0fY\x99e\x0e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4:K\x93\x1b\xb7\xd1\xf7\xfd\x15]\xeb\x83\xed*\ri\xf9\xfb\xe2J\xf1&\xad\xe2\x14\x13[\xd9\x12W\xba\xb8|\xc0\f\x9a\x1cxg\x00\x04\xc0\x90b\x1c\xff\xf7T\x03\x987H\xeeR\xb6\x85\xcb.\xf1h\xf4\xfb\x85ɲ\xec\x86i\xf1\x01\x8d\x15J\xae\x80i\x81\x1f\x1dJ\xfae\x17\x8f\x7f\xb5\v\xa1\x96\xfb\x977\x8fB\xf2\x15\xdc5֩\xfa\x1dZ\u0558\x02\xdf\xe0VHᄒ75:ƙc\xab\x1b\x00&\xa5r\x8c\xa6-\xfd\x04(\x94tFU\x15\x9al\x87r\xf1\xd8\xe4\x987\xa2\xe2h<\xf0\xf6\xea\xfd7\x8b\x97\xdf-\xfer\x03 Y\x8d+Њ\xefU\xd5Ԙ\xb3\xe2\xb1\xd1v\xb1\xc7\n\x8dZ\buc5\x16\x04{gT\xa3W\xd0/\x84\xb3\xf1ހ\xf3\xbd\xe2\x1f<\x98\xd7\x1e\x8c_\xa9\x84u\xffL\xad\xfe \xac\xf3;t\xd5\x18V͑\xf0\x8b\xb6Tƽ\xed/\xca@\xef\xf3\xb0\"䮩\x98\x99\x1d\xbc\x01\xb0\x85Ҹ\x02\x7fN\xb3\x02\xf9\r@$\xde\xc3ɀq\xee\xd9ɪ{#\xa4CsG\x10dw\vG[\x18\xa1\x9dg\xd7\x04s\xb0\x8e\xb9Ƃm\x8a\x12\x98\x85\xb7xX\xae\xe5\xbdQ;\x836\xa0\r\xf0\x8bU\xf2\x9e\xb9r\x05\x8b\xb0}\xa1Kf1\xae\x06\xb6o\xfcB\x9crGB\xd9:#\xe4.\x85ă\xa8\x11xc\xbc\xb8\x89\xfa\x02\xc1\x95\xc2ΰ;0K\x18\x1a\xe7\xc9N\xe3\xe2\xd7\t\xa2u\xac\xd6S\xa4\x06G\x03V\x9c9L\xe1t\xa7j]\xa1C\x0e\xf9\xd1aK\xc9V\x99\x9a\xb9\x15\b\xe9\xbe\xfb\xff\xd3\xec\x88\xfcZ\xf8\xa3o\x94\x1c\xf3\xe65\xcd\xc2`:`B\xb2ڡI2H9V}\n\"\x8e\x00\xbc\x1e\x9c\x0f\x98\x04\xb8\xc3\xf9\x8b\xa8\xacea\xb0Fy\x1dB\xa2?=\xc7f\bz\xb8\xaa\x8dPF\xb8\xe3\n^~\xf3T4\xc9>@m\xc1\x95\bQy6N\x19\xb6C\xf8A\x15A\xd1\x0e%\x9a\xa8hy\xd4\xfeR5\x15\x87\xbc\x15\f\x80u\xca$\x95Mc\xb1\b\xa7\"\xdc\x16\xecD\xe3\xc6w\xfe\x11\x06Q\x18dI\x83h\xdd\xe9\xc2\xef\x10J\xa6\xad\xe2\xd5\x0e\x9fd\x11C\x96Jű\xe3\x1f\xce\xd0\x12\x16\xb4Q\x05Z{\xc6P\t\xc6\b\x91\xb7\xfd\xc4E\x06\x95\xe8\xf7\xb4\xf84\xbaR\x8c\xa3\x01\xa7\xa0d\x92WHd0p\x86I\xbb\x8d*2\x17`{\xec\xe1\xa8Ǩ\xbc\x8f\v\xa7\xd0\t\xbb\xf6/\x83\xbb.J\xac\xd9*\xeeU\x1a\xe5\xab\xfb\xf5\x87\xffی\xa6I\x8d\x95F\xe3D\xeb\xef\xc3\x18\x84\xcd\xc1,\x8c\xc9\xfdo6Z\x03\xa0\v\xc2)\xe0\x14?\xd1z6\xc4@\x80<\xe2\x14\xd8#,\x18\xd4\x06-\x99\x96\xd7(\xb5\x05&A\xe5\xbf`\xe1\x16\x13\xd0\x1b4\x04\xa6\xb5\x85B\xc9=\x1a\a\x06\v\xb5\x93\xe2?\x1dlK\xbc\xa6K+\xe6\xd0:o\x8cF\xb2\n\xf6\xacj\xf0\x050\xc9'\x90kv\x04\x83t'4r\x00\xcf\x1f\xb0S<~T\x06AȭZA霶\xab\xe5r'\\\x9bL\x14\xaa\xae\x1b)\xdcq\xe9\xf3\x02\x917N\x19\xbb\xe4\xb8\xc7ji\xc5.c\xa6(\x85\xc3\xc25\x06\x97L\x8b\xcc\x13\"}B\xb1\xa8\xf9\x17&\xa6\x1fvt\xedL\xd0a\xf8\x1c\xe0\x19⡬\x80\x8c\x80EP\x81\xc4^\n4E\xac{\xf7\xb7\xcd\x03\xb4\x98\x04I\x05\xa1\xf4[g|i\xe5C\xdc\x14rK:O\xe7\xb6F\xd5\x1e&J\xae\x95\x90\xce\xff(*\x81ҁm\xf2Z8R\x83\x7f7h\x1d\x89n\n\xf6\xce'\\\x90\x93-\x91\a\xe0\xd3\rk\tw\xac\xc6\xea\x8eY\xfc\x93eER\xb1\x19\t\xe1I\xd2\x1a\xa6\x91\xd3́\xbd\x83\x856\t\x13\x86a\xc6;%:W\xaaB6\xe5%\x85\xbb\v4S\x00L\t\xcbG[W2\xd7\xe2F\x9bL#圷4\x94|\x968\xb4\xe2\x17\xf0\x8a720\xb8E\x83>\x1b\t\xbe_+\x1f!\x1c\x13\xb2\xf5i\xa1^\x01\xa7\x12\x98\xe5A\x89\x90\xc3\xd46\xe0\xac}\xc0\x99@\x99\xc4\xf8\xd5\xfd\xba\r\x86-\x13#\xee\xb3xw\x91?4\xb6\x02+\xee3\x87\xcbw'5\x97\xc6z\x1b\x90\xf0\x11\xc1)`\xa0\x05\x168\x8a\xc6 \xa4u\xc8x\x9c$'h0\xae\xbd\b\x9e\xfe$\x92\x10\x8a\xca\x18\xb5I&\xc0(\xf2\b\x0e\xff\xd8\xfc\xeb\xed\xf2\xef*\xd0\x01\xac\xa0\xd4\xcc\xd7z>\xdf~\xd1\xd5{\x1c\xad0ȩz\xc3Eͤآu\x8b\b\r\x8d\xfd\xe9۟\xd3\xfc\x03\xf8^\x19\xc0\x8f\x8c\xaa\xa6\x17 \x02ϻ`֪\x8d\xb0\x81\xf0\x0e\"\x1c\x84+=\xa2Z\xf1H\xe0\xc1\x93\xe0\xd8#Yr \xa1A\xa8\xc4c\xc2~¸\xf5\xd9\\\x8f\xe6\xafd=\xbf\xdd\xc2W\xc1y\xdd\xd2\xcfۀF\x97\xb6\f\r\xacG'X\x99\x11\xbb\x1d\xf6y\xffLY(\xccR\x80\xfa\x1a\x94!Z\xa5\x1a\x80\xf0\x80IN!> \x9f\xa1\xf7ӷ?\xdf\xc2Wc\x1e\x9c\xb8JH\x8e\x1f\xe1[\xf2>\x9e7Z\xf1\xaf\x17\xf0\xe0\xf5\xe0(\x1d\xfbH7\x15\xa5\xb2(A\xc9\xea\x18\x12\xe0=\x82U5\xc2\x01\xab*\v\t\"\x87\x03;\x82ڞ\xb8\xa7\x15\x11\xa9&\x03͌;\x9b$F>\x9c7\x9ay\xd6Ԏ\xa7ًϢ\x9ed\xbd\x9f-\x03y\"'|\xb9\xf0\t\x9c\x18\x96^Wp\xe2\xb1\xc9\xd1Ht\xe8\x99\xc1Ua\x89\x0f\x05jg\x97j\x8ff/\xf0\xb0<(\xf3(\xe4.#ê\xd4\xed\xd2\xf7\xc1\x96_\xf8?\xd7\x12\xee\xdbT\x9fJ\xbd\a\xf2\xf9X@\xb7\xdb\xe55\x1ch\xb3\xfb\xa7Ǯ\x93|\xd8Ąs\n\x93l\xfeP\x8a\xa2lk\xbd\x81\xb7\xad\x19\x0f\xee\x98\xc9\xe3g\xb2\x1d\xe2sc\b\xa3c\x16\x9b\xb8\x19\x93\x9c\xfe\xb7\xc2:\x9a\xbf\x86\xb1\x8d\xf8$\xe7\xf2~\xfd\xe6sZT#\xae\xf1$'j\x980>f=VY\xcdt\x16v3\xa7jQLvS\x0e\xbf\xe6$\xa4\xad@s!\xfd{7\xda\xdc&\xa8\x89j\xa0\xdb\xf3\xac\xfcӱ]\"\xe1\x1bv\xb1ϥ\x85g\xf9uY\x15\x1e\xd8\xce\x023\b\fj\xa6I#\x1e\U0005814cC3A\xe9\x02e\x04]c\x10\x98\xd6\x15\xc5\xf4\x90E$ \xc6\xfc7\xb2\x87YO\xdf)\x86$E\xd9v\xa56蜐\x9f\x919\xef'\x88\xfc\xbe\x8c\xeazv\x85\x92[\xb1\x8b\xdd\xce9\xa7dSU,\xafp\x05\xce4\xa7j\xae\xb3\x8c|\xa0-\xe7\xe9\x7f?\xd8\xdaj\xf8\x85\x06c\x9a\xaaQ\xdbqN\fʦ\x9e\xa3\x92\xc1\xa3҂%\xe6\rZ7\xb3^Z\xb8\xbd}\x8e\x8d\x05\xa5\xbc\xa6\xe4\x0eep\xaa*\x8d\x8a\x1e\x13\xf8\xb62u\xaa\xaf\xf2\x92B\x7f\x86o\xa0\xea\x9eʑ1\xdeY\xba]2\xd93\xe8.\xb7SZ\xf1\xc9\xcc\xd8\rN\x16\x03}O\xea!\xf9\x86\xf63\xbaH\xe1\x91-\xf24\x04G\xd7>\xbdQ\xda}m\x1f\x89\n;\xed\x90w\x8d\xfek$\xfej\n\xc4\xf7~\r\x8fF!j\xecJ\xff\xb1\xaf\v\xc5]\x8e\xa0\rj\x96\xec\n\x81\xef\xdc[\xdf\xc2\xfc\xd2\x06`\xc2Bc\x91\xfb\x0e\xda\xec\xee\x19\x84\xf6\x9d\x893\x87\x19\x9d\xbf\xce_\xa4\x1bS\xe1\xcdo\xf8RrU\x97j\x0ef\xceB\xd6r\xcd?ᴏ\x8d)\x8e\xf5\xe0:~\x05h\xc8}\x15JE\xf2\x96\x89\n9\xb4o\xdcτ\x92\xe3\x96R\x9c\xe0\xe3\xda>ND\xeft\xfdw^\x92\t&\xcc\x13\x9e?R\x98ӧ\xc6\v\x92\\O\xb6C\xa9\xaa(/\xd9\xd49\x1a2L\xff\xe0\t\x12\x0fT\xf7\x17%\x93\xbb\xa4\x93k\x1f\xec\x10*f\x1d\xe4\xfd'\x03)\xe2\x87/\xa6Sʆ/\x9c\xfd\xa8\xd1Z\xb6\xbb\xe4\xce\x7f\f\xbbB\xe7.\x1e\x01\x96\xabƥ\xed\xf7K\x1b]\xd0\xf3\xba\x87ɦ\xd8\xd8\xfb1W\xb6\xcen\xdbT\x95?3\x8c\x1b\xfd\xa7\x1d\x1e\xab\x1c\xd3\x19\xff\x99\xd6\xe19\x04Kf/\xb1\xea\x9e\xf6\xa4\xfcq\x17\xec\xce:d8\x13\xd8\xdf\xe2!1\xdb\xfa\xb9\xc4\xd2}t\x9e\x89\xa5ٗ\x18\xc3\xc5\xd0\x1bOq\xae]K\xc2\xec\xbesH\xac}\xef\xbdʳ\x98\x1d\xf1\xbb\xc6mv\xbd\xf5\xde\xf2\xfcg\v3\xfb\x1b\xe7\x1fL\xf2\xa1\xd8RM\x88\xfe|\xabA\x01Rl\xa4\xc5'\x01ﺜ\x02.\xac\xaeر\xa3ŗ~d\xaa\xe9\xf7\x91ޢZ\x8f\xa9\xf1T*{\xbe\xc3\xdd}-\x92\xaek\xcf\xfb\v\xb8\xe03\xfc\xba:\xed\f\x7f\x8f\x1bΤ\xe2V2mK\xe5\xd6o.\xa8Ʀ\xdb\xd8\xdac_V\xfa\xc0\xe2\x9f\xde⦨\n\tT{\xef\xf6,g1\xfex\xe8\x1a-ތ \\\x88\xfb\xf1[\xa6Ttݐ\x17 \a\xe4\x1fv\xef\xa6_p\xbc\xe8\x82\fs\xb1A\x1e\xe2Q\xaa\xab\xa0\xa4\xaf#\x94\x99\xbf\xb2\xc3\xc5@>&\xe8ό\xe1Iu\x9aMz\xcc\xf9\x00v|\xd3\x1c\xce4y\xf7ܿ\x82_\x7f\xbb\xf9_\x00\x00\x00\xff\xff\xfc¬w\xb0(\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z͒\x1b\xb7\x11\xbe\xefSt\xad\x0f\xb6\xab4d\xa4$\xae\x14o\xd2*Nmbo\xb6DI\x17\x97\x0f\xe0\xa09\x03s\x06\x80\x01\f\xb9\xb4\xe3wO5\x80\x19\xce\x0fH.\xa9\x925\x17i\xf1\xd3\xf8\xf0u\xa3\xbb\xd1`\x96e7L\x8b\x8fh\xacPr\x01L\v|r(\xe9/;\xdb\xfc\xc3΄\x9ao_\xdel\x84\xe4\v\xb8k\xacS\xf5;\xb4\xaa19\xbeŵ\x90\xc2\t%ojt\x8c3\xc7\x167\x00LJ\xe5\x185[\xfa\x13 W\xd2\x19UUh\xb2\x02\xe5lӬpՈ\x8a\xa3\xf1\xc2ۥ\xb7\x7f\x99\xbd\xfcn\xf6\xf7\x1b\x00\xc9j\\\x80V|\xab\xaa\xa6F\x83\xd6)\x83v\xb6\xc5\n\x8d\x9a\tuc5\xe6$\xbc0\xaa\xd1\v8t\x84\xc9q\xe1\x00\xfaQ\xf1\x8f^λ \xc7wUº\xff$\xbb\x7f\x10\xd6\xf9!\xbaj\f\xab\x128|\xaf-\x95q\x0f\x87\xb52\xd0[\x13z\x84,\x9a\x8a\x99\xe9\xcc\x1b\x00\x9b+\x8d\v\xf0\x135ˑ\xdf\x00D\x06\xbc\xa0\f\x18\xe7\x9eSV=\x1a!\x1d\x9a;\x12!\xbbe8\xda\xdc\b\xed\xd3%\xb3\x18{\x03\xf9K\xdf\x11\x9bܞ0[g\x84,R(ދ\x1a\x817\xc6+\x9d\xf6\x9f#\xb8R\xd8)\xbc\x1d\xb3\x04\xd18\xbf\xf14\x18\xdfO\"\xadc\xb5\x1e\xa3\xeaM\r\xb08s\x98\x02u\xa7j]\xa1C\x0e\xab\xbd\xc3v+kej\xe6\x16 \xa4\xfb\xeeo\xc7\xf9\x88\x84\xcd\xfcԷJ\x0e\xc9yC\xad\xd0k\x0eHH[\x05\x9a$Cʱ\xeaS\x808\x12\xf0\xa67? \tr\xfb\xedg\xa1\x90\xe9\x81Z\x83+\x11ް|\xd3hX:eX\x81\xf0\x83ʃ\nw%\x1a\xf4#Va\x04\x9dm\x10\xa4;e\x92\xaaӘ\xcf\xc2\xd8(\xac\x955\xd2\xdfp\xa1\xcfb_\xb9A\x96\xb4\xaf\xd6I\xcd\xfc\b\xa1d\xda\xc8^\x17\xf8,\x03\xeb\x13)\x15\xc7\x1ek\x13\\\u00826*GkO\x18>\t\x19 y84\x9c\xa5\xa8D?\xa6\x05\xd4\xe8J1\x8e\x06\x9c\x82\x92I^aС3L\xdau\xb4\x8c\xa9\n\xdbi\xef\xf7z\b\xe5C+\xaf\xd73\xc1\x14\x86n_\x067\x98\x97X\xb3E\x1c\xab4\xca\u05cf\xf7\x1f\xff\xba\x1c4\x03Ѣ\xd18\xd1\xfa\xd1\xf0\xf5BR\xaf\x15\x86{\xfe_6\xe8\x03\xa0\x05\xc2,\xe0\x14\x9b\xd0z.\xa2\x7fE\x1e1\x05\x8e\x84\x05\x83ڠE\x19\xa2\x1553\tj\xf5\v\xe6n6\x12\xbdDCb\xc8\xed7\x15\xa7\x90\xb6E\xe3\xc0`\xae\n)~\xebd[\"\x9c\x16\xad\x98C\xeb\xfcA4\x92U\xb0eU\x83/\x80I>\x92\\\xb3=\x18\xa45\xa1\x91=y~\x82\x1d\xe3\xf8\xd1[\x93\\\xab\x05\x94\xcei\xbb\x98\xcf\v\xe1\xda@\x9d\xab\xban\xa4p\xfb\xb9\x8f\xb9b\xd58e\xec\x9c\xe3\x16\xab\xb9\x15E\xc6L^\n\x87\xb9k\fΙ\x16\x99߈\xf4\xc1zV\xf3\xafL\f\xedv\xb0\xecD\xd1\xe1\xf3\xe1\xf5\x02\xf5P\xbc\xa5\x93\xc0\xa2\xa8\xb0Ń\x16\xa8\x89\xa8{\xf7\xcf\xe5{h\x91\x04M\x05\xa5\x1c\x86Nxi\xf5Cl\n\xb9&çyk\xa3j/\x13%\xd7JH\xe7\xff\xc8+\x81ҁmV\xb5pd\x06\xbf6h\x1d\xa9n,\xf6\xce'3\xb0\xa2\x03E~\x80\x8f\a\xdcK\xb8c5Vw\xcc⟬+Ҋ\xcdH\t\xcf\xd2V?E\x1b\x0f\x0e\xf4\xf6:\xda\xfc\xea\x88j\xc7\xfem\xa91'\xcd\x12\xb94U\xacE\x8c$ke\x80M\xc6\x0f\x99J\xbb\x00\xfa\x92\x11e<\xe8\x9c\xd9\xd1\xf7&%\xa8E,{\x8e<\xc6;\x1b\x03U5\fT\xfdo\x12#\rje\x85Sf\x7f\x88\x94c\x938\xaa\x1d\xfar&s\xac\xae\xd9ޝ\x9f\tBr\xe2\x1d;\x93&g\x14\xa4z\xa0J\x16\x8a\x0e\xd9D\x1dp\xefh\x1cٹE\x97ެ<\x1aل\x84C\x8e\t\xfd\\r\xbc\xed\x95R\x15\xb21\x9bZ\xf13\x9b~T\xd1q\x18\\\xa3A\x1f\xff\x83\x9b\xd5\xca;cDŽl\xddGH\xb9\xc1\xa9\xc4>V\xe4n\x8e\xa9\xe6\xb8\x1d\u0089\x90\x94\x04\xfc\xfa\xf1\xbe\r;\xadeE\xe8\x93\xc8\xd2\xe7'i\x16\xf4\xad\x05V\xdc\a\xea\xf3k'-\x84\xbe\xfbu\x00\xe1}\xafS\xc0@\v\xccq\x10\xf7@H\xeb\x90\xf1\xd8H\xee\xc6`\xec{\x11|\xeaQ\x90\x10\xaeE1>\x92J\x80\x91\x8f\x17\x1c\xfe\xbd\xfc\xef\xc3\xfc_*\xec\x03XN\x99\x90\xbf\xab`\x8dҽ\xe8\xee+\x1c\xad0\xc8\xe9\xf6\x81\xb3\x9aI\xb1F\xebfQ\x1a\x1a\xfbӫ\x9f\xd3\xfc\x01|\xaf\f\xe0\x13\xa3\xa4\xff\x05\x88\xc0y\x176Z\xab\x116l\xbc\x93\b;\xe1J\x0fT+\x1e7\xb8\xf3[plC'&l\xa1A\xa8\xc4\x06\xd3\xec\x03\xdc\xfa\xe4\xe9\x00\xf3wr)\x7f\xdc\xc27\xc1I\xdcҟ\xb7\x01F\x97 \xf4\xbd\xce\x01\x8e+\x99\x03gDQ\xe0!ў\x18\v\x054\n\x05߂2\xb4W\xa9z\"\xbc`\xd2Sp\xc4\xc8'\xf0~z\xf5\xf3-|3\xe4\xe0\xc8RBr|\x82Wt\xc6=7Z\xf1og\xf0\xde\xdb\xc1^:\xf6D+奲(A\xc9j\x1f\xf2\xcd-\x82U5\xc2\x0e\xab*\v\xa9\x18\x87\x1dۃZ\x1fY\xa7U\x11\x99&\x03͌;\x99\x8eE\x1eN\x1f\x9ai~\xd2~\xcf;/>_y\xd6\xe9\xfdb\xb1\xfe\x99L\xf8\xc4\xfc\x13\x98\xe8_u\xae`bӬ\xd0Ht\xe8\xc9\xe0*\xb7\xc4C\x8e\xdaٹڢ\xd9\n\xdc\xcdw\xcal\x84,22\xc6,h\xdd\xce}1g\xfe\x95\xff\xe7ڍ\xfb:˧\xee\xde\v\xf9r\x14\xd0\xeav~\r\x03m\x1e\xfd\xfc\xd8u\x94\x87e\xcc\xec\xc62\xe9\xcc\xefJ\x91\x97\xed\xad\xaa\xe7mkƃ;fr\xff\x85\xce\x0e\xf1\xdc\x18B\xb4\xcfb)2c\x92\xd3\xff\xad\xb0\x8eگ!\xb6\x11\x9f\xe4\\>ܿ\xfd\x92'\xaa\x11\xd7x\x92#\xb7\x85\xf0=e\aTY\xcdt\x16F3\xa7j\x91\x8fFS\xae|\xcfIIk\x81\xe6L\xf6\xf7n0\xb8\xcd\xda\x13Yw7梴\xdbJ\xa6m\xa9\xdc\xfd\xdb38\x96\xdd\xc0\x16\xc3A\x871\xe9leё8\x99k>\x03\xcfR\xfc\x96p[ID4\xb4\xc5T\xa9B\xe4\xac\x02\xeb\xdbd,VF\x98\xad\xec)\xa0T=r\f\xb7_U\xec\xe1\xf5\xbe\xe0ḧ\x1dB\x1e\x8enQ+#\n!Yu\xf0\xd8\xfe\xea(Y\xcd\xfc_\t[\xad\x99\xd6B\x16\x17q\xdbַ\x96蜐E\"\xd1\xef\x97\xdfO]\aN\x9e\x93\xf3.\xe0\xc3\b\b0\x83\xc0hO\xa4\xaa\r\uecd0uj&(e\xa4\xac0\xa6\xd6+\x04\xa6uEy]\xc8$S\xbe\xa9\xad\xd6\xe5J\xaeE\x11+\xa7S\xa6dSUlU\xe1\x02\x9ci\x8e]ڒǽ_(<\xa3\xf1\x0f\xbd\xa1\xad\xbaϔ*ӻ\x1a\x140\xa7\x9bA\xd9\xd4S(\x19l\x94\x16,\xd1N\x87s☨\xe3\xf6\xf6\x12\x93\n'\xff\f\a\xe1Μ*8D\xc7\x11\xaf!\xf1\x8a\x1d\xdcG:\x9a_\xeaP\f\xfe\xdaНj\x880K\xd7VFc\xb4\xe27c\xd2\xfa\xbex\xd4y\xf0\xa4\xe3\x8e\xe1\xa1\x1f\xf5\x06\n\x9eU\x96\xf2\x85\xf2K\nS\xe19,\xf2\x1e\xd2\x00\xd7>\x92\xd1\x05\xe3\xea\xd2\x14\xdda\xb5C\u07bd!\\S\xb7y=\x16\xe2\vʆ\xc7C\"j\xec\x8a\x1c\xd1N̡\xec\x12B\x8c6\xa8Y\xd2\"\xc0?\nX_\x18\xfd\xda\x06i\xc2Bc\x91{\xdf:Y\xfchL\xe0\xccaF\xf3\xafs \xe9bWx\x9e\xeb\xbf\xc2\\U\xf9\x9a\x8a\x99r\xc8:\xda\xfc\xfbP\xfb0\x98\xa2\xec \xaf#,\x88C\xee\xafܠ$\xac\x99\xa8\x90C\xf7,}1\xf3\t\xd0\xd3d\xecs\x92_\xa3\xb5\xac8\xe7\xb4~\f\xa3B\xe5-N\x01\xb6R\x8d;b\x95_\xdbx\xb4.\x8a\xc9R\xf1sH\x1e\x14\xf70\xe4\xf1'\xb7)\x9a\x84Z\xfa\xcfp\x17a\xf4E\xcdsEJ\x1a\x93r5\x1d\xe4Ӿ\x06Nİ\a\xdc%Z\xdb\x13\x9c\xe8z\x8cn!\xd15\xf9=@\xbf3T\x92S9Mۗ\x94\xd9=\xb6'\xfa\xbe\xf7\xc7\xe5\"\xb6#\xbek\x1cBW\x87.U\xd5\xfa\x00\xffH.\x9bz\x85\x86T\xb1Je\xc4\xc0$\xefk.UL\xe8$\xb4a8\x88\x8a\xf5\xb0X@\xf7\xa7\xdc)\xe0\xc2\xea\x8a\xed\xbb\xcd\xf8\x1b\x1c\x1d\xe9\xf4s\xc2\xe1\\\xb5\xbe\x8a\"ϑ\xbc\xedt\xa5\xba\xfb\xd1B\xfa~z:Ӈ3پ\xef\xef~\x8c\xf0yV8\x91w\x0e\x7f\x1cr\x8d\x81,\a\x12\xce\x05\x8b\xf8c\x95\xcb}\xfcp\x99?ӽ'ٛ4z\xe4\xbc';>y\xf5[\x9aU\xf7\x1e\xbc\x80\xdf\xff\xb8\xf9\x7f\x00\x00\x00\xff\xffϡa\xa5-&\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ks\xdc8sw\xff\n\x94sؤJ#ǕG\xa5ts\xb4v\xac|\xdf\xca*\xc9\xf1\x9e1d\xcf\x10\x9f@\x80\v\x80\x1a\xcf&\xf9\xef)4\x1e|\fHbF\x1a\xednjyQ\x89\x04\x1a@\xbf\xbb\xd1\xc0\xacV\xab7\xb4a\xdf@i&\xc5\x15\xa1\r\x83\xef\x06\x84\xfdO_>\xfe\x9b\xbed\xf2\xdd\xd3\xfb7\x8fL\x94W\xe4\xba\xd5F\xd6\xf7\xa0e\xab\n\xf8\x116L0äxS\x83\xa1%5\xf4\xea\r!T\bi\xa8}\xad\xed\xbf\x84\x14R\x18%9\a\xb5ڂ\xb8|lװn\x19/A!\xf00\xf4\xd3?^\xbe\xff\xd7\xcb\x7fyC\x88\xa05\\\x11\x05\xdaH\x05\xfa\xf2\t8(y\xc9\xe4\x1b\xdd@aan\x95l\x9b+\xd2}p}\xfcxn\xae\xf7\xae;\xbe\xe1L\x9b\xbf\xf4\xdf\xfe\x95i\x83_\x1a\xde*ʻ\xc1𥮤2\xb7\x1d\xc0\x15Q\xbe\xb9fb\xdbr\xaab\x877\x84\xe8B6pE\xb0}C\v(\xdf\x10\xe2\x17\x85\xfdW~=O\xef\x1d\x88\xa2\x82\x9a:\xc0\x84\xc8\x06ć\xbb\x9bo\xff\xf40xMH\t\xbaP\xac1\x88\x9a\xffY\xc5\xf7$,\x810M(\xf9\x86(\xb0\xb3A\x92\x10SQC\x144\n4\b\xa3\x89\xa9\x80Ц\xe1\xac@\x8a\x10\xb9\xe9A\n\xbd4\xd9(Yw\xd0ִxl\x1bb$\xa1\xc4P\xb5\x05C\xfeҮA\t0\xa0I\xc1[m@]F@\x8d\x92\r(\xc3\x02\xba\xdc\xd3\xe3\xaa\xde۹\x85\xd9\xc7\xe2\xc2\xf5\"\xa5e/pK\xf0\xf8\x84ң\x8f\xc8\r1\x15\xd3\xddR\xc3\xf2\b\x15D\xae\xff\x06\x85\xb9\x1c\x81~\x00e\xc1X궼\xb4\\\xf9\x04\xca\"\xab\x90[\xc1~\x8d\xb0\xb5]\xb8\x1d\x94S\x03\xda\x10&\f(A9y\xa2\xbc\x85\vBE9\x82\\\xd3=Q`\xc7$\xad\xe8\xc1\xc3\x0ez<\x8f\x9f\x90xb#\xafHeL\xa3\xaf\u07bd\xdb2\x13d\xad\x90u\xdd\nf\xf6\xefPlغ5R\xe9w%<\x01\x7f\xa7\xd9vEUQ1\x03\x85i\x15\xbc\xa3\r[\xe1B\x04\xca\xdbe]\xfe]$\xea`X\xb3\xb7<\xaa\x8dbb\xdb\xfb\x80\xa2r\x04y\xac\x109\xc6s\xa0\xdc\x12;*\xd8W\x16u\xf7\x1f\x1f\xbe\xf6\x99\x92iO\x94\x1eoN\xd1\xc7b\x93\x89\r(\xd7\x0fY\xd3\xc2\x04Q6\x92\t\x83\xff\x14\x9c\x810D\xb7\xeb\x9a\x19\xcb\x06\xbf\xb4\xa0-\xbf\xcb1\xd8k\xd4Gd\r\xa4mJj\xa0\x1c7\xb8\x11\xe4\x9a\xd6\xc0\xaf\xa9\x86W\xa6\x95\xa5\x8a^Y\"dQ\xab\xafeǍ\x1dz{\x1f\x82\xae\x9c \xad\xd7\"\x0f\r\x14\x03I\xb3\xdd\xd8&\xa8\x8b\x8dT\x03%c\xbb\fq\x94\x16~\xfb8-b\xd5\xe2\xf8\xcb\x12\x97\xd9\xe7\xdfco\xcbovf\xad`\xbf\xb4\x80\xcaԉ?\x1c\xea+\xd5S\xfa\xc3Dzј\xba\x93\x88\xb6\x0f|/x[B\x19\xf5\xfa\xc1\x02s\x96\xf1\xf1\x00\n\x9aCʄ\x15\"k\x97\xecZD\xf7\x15\x158U@\x844\txL8x\x84\t\xc4@\x92&\xd8\xd0@\x9d\x98\xf1\xec\x92\t\x11-\xe7t\xcd\xe1\x8a\x18\xd5\x1e\xa2\xd1\xf5\xa5J\xd1\xfd\x04\xb6\x82o\xf0,dE ^\xd5pV ɣBA|\xfdqQŴU\x94a\x95w\x92\xb3b\xbf\x80\xaf\x8f\xc9NAZ\xbd\xec\xfa\x15\x925T\xf4\x89I\x95\x12\x03\xa9\xb0iϞwjZZ-遌m\\悓Ȫ\xa4|\\b\x88϶Mg\x1dH\x81\xaef\\\x8a\xa7\xb6\xb7\xddk \xf0\x1d\x8a\xd6$\xa6IH٢i\x92\x8a4R\x9bi\xbaO\xab.\xd2w\x8eR\x1fg\x98\xe6`eIVw\x8fW\u0081\xa8\x16\a\x03\x85,\x05\xd8eԖ\xa8][%[\xd7v\x12)dM5\x94D\x8aɑ\x91]Z\x0eڏU\"gtz\xe8\xa2[?z<\x84\xd35p\xa2\x81Ca\xa4:Df\x0eJݓ\xa3X'P\x99ЦC\t\xe8\x160\x03\x92XN\xdfU\xac\xa8\x9c\x87a\xd9\x13\xe1\x90R\x82\xb6\xda\x04]\xe6\xfd\xd4\"\xc9\x12\xf9\xfd sڣ{\x16\xc4j\f/\xa5Q\xba'C\rwO\x12\xb5\x9d\xee=\xd0-\xfe\xbd\x91\xb3\xcb\xfe\xff\x89\xd8`LN`\xda\x19\xf9'\xe8~f\xf3\xf4$\xdfb\x84\a\xfa\x92\xdcl\bԍ\xd9_\x10f\xc2\xdb%I\xa0\x9c\xf7\xc6\xf8\x03\xd3\xe6x\xa6\xcf$M\x8eL\x9c\x890q\x88? ]\xd0d{\x84=\x82Igs\x0e\x9f\\np\xcf#$\\\xff\xd43\xc0\xa1\x9d\x93\x0f\x8b\x1d\x9e\xec\vD\x04\xc6\xf0\xb9l\xe0\x1e/\n\x89\xdcI\xfa\xc9\xd4%\xe1\t\xb8?a\x99Y\xac\xd2\x1f\xa3\x9f\xfaD\x0e\xf8A;ZZ\x89\xa9\x98\xcfij@\x99\xc9%\xa8{\xbeQ\xce\xca8\x90\x93\x91\x1bqAn\xa5\xb1\x7f0@\xd3\xc8(?Jз\xd2\xe0\x9b\xb3`\xd4M\xfc\x9c\xf8t#\xa0\xa0\t\xa7\xe5-\xc2\xfa9?g\xd3,\xb7E\xdc3Mn\x84\x8dW\x1cJ2\x87\xc2\xf4\xae\x1b\xce\rT\xb7\x1a\xd3uB\x8a\x15\xda\xcc\xe4H\x1e\xdfR\r\xd0\xfd\xecA\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\xef\xabǘ/XY\x93\xb3\xf2\x10\x8c\xac3p\xe0uw\xb9\xbc\x9e\x95\x95ٌV\x81\x13\x16\x9bN$G\xa7\x9b\xe6 \xe5\x19\xe8@+\x8e.\xce\"uiY\xe2\xe6\x1a\xe5wGX\x94#x\xe1X\xd5Л\xbb3\xc15m\xacZ\xf8okiQ\x9a\xfe\x974\x94)}I>\xe0N\x19\x87\xc17\x9f\x87\xeb\x81\xc9\x18\xb2\xb1CY\xfey\xa2\xdc\xda~\xab\xc0\x05\x01\xee<\x01\xb99\xf0\x8b.Ȯ\x92ڙ\xed\r\x03\x8e\xfb\x15o\x1fa\xff\xf6\xc2\x0e\xbf8d_ɼ\xbd\x11o\x9d\x0fq\xa00\xa2\xc3!\x05ߓ\xb7\xf8\xed\xeds\\\xa9LN\xcdl6`њ6y\x1c*\x92\xc9\xfa\xee\x19pL?7\xdf%当=\xb7\xda,\x16m\xa46\x9f\xd3yÉ\xf9܅\x1eC\xcf8\x91c[\x8c\x18|\x1e-\xea{\xebDn\f(\x9fKt6 \xc4\x1fό\xccR\xbb2\xfd\xc9\xc6d \x8d\xf9]\x8b\xe0\x05nr\x1b79S<\xc6a\xb5x9\xd2\xdb\xff\xf8\xbd\x97ϴ\x92k\xff\xef/\xe4\xa5\x1d\xeaB\xd65\x1d\xefjfM\xf5\xda\xf5\f<\xed\x019\xea\xabm\x8b\xf2\x9ck\x91;\x1e\xc2\xfd\xcb\x1d3\x15\x13\x84\x06\xb5\x01\xca3\x14%\x8dL\xe5\xb0SOE5Y\x03\x88\x98\xa2\xff=\xb8\x125\x1378\x00y\x7f\x06\xd7#\xa2\xeb\x9c\xce\xeeu\xa4I\xa4||\xe1LV#K\xb2\xab@\xc1\x801\x0e\xf3\xee\xe8\xa9\niz)\x8b#\x1c\xd2F\x96?h\xb2aJ\x9b\xfe\x144iu.\xad\x8f$\x9f\x9d\xf7WV\x83l\xcd9\x11\xfc\xb1\x1bf\xb0\xd7\\\xd3\xef\xacnkBk\xd9:cnX\x1dwu=zw\x94\x99\xb8m\x85\xf9\x1b#-\t\x1a\x0e\x06\xc8\x1a6\xe9\xfd\xde\xd4SH\xa1Y\t*T)8\xb21i\x05sC\x19oS\xbbD\xa9\xe7\xd8\bX|T\xea\xa4\x00\xf8\x8b\xeb\xd9\xcb;Vr7DP\xe6\xdaq#\r\b\xdb\x10f\b\x88\xc2b\x1c\x94S\xc98\x84G\x06\xa2\x86\xe5\xea\xb9<\x05n\x1f\x10m\x9d\x87\x80\x15\n$\x13\xb3)\xb7~\xf3O\x94\xf1s\x90\xcdr\xde'\xa9\ue056\xa7\xe4h~\xeeu' t\xabp\xf3\xdf\xe9\x8e\x1d\xe3ys\xb6\x94#\x9c\xb6\xa2\xa8\x00\x95\x90\x18\xea\x06\a\x9e\tm\x80\xe6\xf2\x82\xf5\x8aZ!\x98\xd8\xe6\xd1.;\x11\xda=\x0e\xd5k)9\xd0\xe9]\xc8\uec78~\x05M\xf4s7\xcc35QG\x04\xb7m\x8etȦ\xa8UZ\x84\x1a\x03u\xe3DN\x12Պ\xbeu9\x83\":&\f\xf7\xb3x\xc9\xf8\x9a\t\x96A\xdb\x01]o\x043}\xe7т8\xab\xf3h\a\x88\xee\xc0)\x19\xb6\x9b\x01\x00+\xa0!\x0e\xc1\xb9G\xae9\u0091\\\x03\xa1e\t\xa5\xcb]ZWć%\xae\xf0m\xa2\xb8!\xb9\xba\xe3=\xc1,ʆg\x10tb\x1eV=\xc1\xaa\x15\x8fB\xee\xc4\n\x83q}\xb4\x0e91K\xf5\xdc\xe1\xcd\xc9\xcahY\xbf\xe4\xab\xe9%-4\xe4\xd7|\x9e\n\xfe\xd3\x19\xb4L6\xdf\x1c\x95\xf0\x98\xe3\x82%\xbd\xe6\n\xb0'>.\xcebn\xfc\x99\xce~S\xfa\xda\x15K?\xab,\xee&\r\xaa\xe7\x14\xee*0\x15\xa8P\x9a\xbd\u0092\xf4rv\x87\xb4\v^b\x9d\x9ce\xaa\xe0\"\xbb\xf2\xcfQ\xe5\x1cF7-\xe7\x17\x96\xb7i˓ᰑ(b\x87\x9c\x95U?\x96\xf6\x18r\xaa/\xb2\xf1د\xb4\x18\xd6\x17\xc6*\x88P`(\xc3ȞƩ\xf5baio\x7f\x7fXN\x81\xf9\xbf0\xfd\u07fc\xf40\xa3R\"\x1f\x8d\xb9U\x9a\x11\x89\tX\t\x06롱\xab\xaf\xf0\xed|\xa1\xef\xef\v\xa7\x06\xea/\x8d\x97\x98I\x176\x03\xad\t8\xa3z\x13\xb4\x06\xadv\xae@\xb4\x03>gh\xfb\x7f(\xdc)\x88\x00&ů_+\b\xe2\xeb\xab\xf7\x99&\xffL*\xd9&\xaa\xfafP\xb6Pݱ\xbc\xe0A\xa1\x87\xdfP\x00C\x9f\xde_\x0e\xbf\x18\xe9\xcb>0\x8b\x96\x00\x84AQ\x97\x99e\xa2dO\xacl)\x0fR\u06dd!p\f\xd4\xf1Y\x02\x9aTD0\xee\x180\xf4\x1f0\x1c\xf9Ҹm\x99\xa3Uܼ/\x9aW\x1drrMȰ\xe6c\xc2\x1a\x1e\xbb}\xf1\"U\xb0\xbfI\xad\xc7\xf1\x15\x1e9\x91\xc4B5\xc7\t5\x1c\x99\xc5b\xcf\xdeoɩ\xd28&\xe6>[E\xc6\xcb\xd7ad\xe1g\xb9\xe6\xe2\x18윽\xbe\xe2\x15\xab*^\xa7\x96\"\xb3\x82\xe2\xe5J!\xf3\xa2ϓJ\x01\x96\x03\x96\xe9*\x88\xc5ڇg\x054'-i\xb1\xa6\xe1\x98J\x86E\xea\xe4\x89٫\xd5*\xbcZ\x85\xc2\xeb\xd6%\xccr\xd1\xec\xc7c*\x0fb\x9c\xf4\x13m\x1a&\xb6\x87L\x91\xcb:\xb3l\xb3\xcc2\xb7\xa3\x89\fx\xa6\x1f\xcet\xd1\xe1D\xe8\xeb\x8eK'\"ɐ\xb6d\xc2\xc8K\xf2A\xec=\xdc\x04\x9c^\xf8(\xa498\xc8f\xa7\xb5c\x9c\xf7Ok!\xd8yP\xfe̤\xa6\xb5\x9bՔ\xb7\x9f\xa4\xabT\x03\xa7\xfc\xa4\xc0\xf1\xcb\bF?;\xfa\x9a\x9e\x7f\xddr\xc3\x1a\x0e֣{be\xf2\f\x99\xa9`\x1f\x91\xfc7\x89'\xa4\xd6{\x84\xf4\xe5>\xca\xe2\xe5(\x88\xa1\x9a\xec\x80sBS\xdcq\xb0\xfc\u009dL.\xe4\n\x8f\x04Z\xf2\x06&\xf1\xe7\x99/\x9c\x14\xe310\xa4^\x9d\x80[P\x81\xa7\x9bub!\x93\xe60G\x8b\x1e\xf8\xe5.\xba\xc0w\xbf\xb4\xa0\xf6D>a\t\x83\xf7\u07ba\xb3\n^\xddh\x1bc\x06\x05\xe8\x95\xf1Ԧ\xc2A(\xd3)(\xf2A8_b<\x1f\xecc5_\x17\xaaYun\xa3\xb0\xe4\x18\x13݅\x8c\xbd\x13ݖ\xdc\xfeܢ\xfe\xf3\x06nLJn\x8b\xbeR\xbe?\xfb\x1b\x15\xeb\x9fR\xa4\x9f\xb7\x1d\xb4X\x94\x7f\xae@n)\x94\xcb\xf6^\xf3\x8a\xee\x8f\xdbD=c\x91\xfd9\x8a\xeb31\x95SL\x7f\x1c\x9e^\xa1x\xfeU\x8b\xe6_\xabX>\xbbH>k\x1f3{\xd3*w\x9b\xf1Ī\xef\xe5]\xf7\xf9\xa2\xf7\x8cb\xf7\x8c\x9d\xb4\xe5E\x9e\xb0\xbc\x8cb\xf6\xe3\x8a\xd83h\x96+\x8a\xafX\xac\xfe\x8aE\xea\xaf]\x9c\xbe\xc0Y\v\x9f\x8f+B?y\a&l\xf5\xdf\xca\x12\xee\xa42K\xc1\xc9ݸ}b'\xb5\x17\xb0I^\x12\x11\x9a&V\x89!\x86\x0f/N[Tz\xd33\xb8\xd3?\xc9\xd2\xcemi\x8f\xe5~\xd4\xfc\xe0\xac\xf2\x06\x14\bw\xcd\xc7\x7f>|\xb9\x8d\xf0S>\xaf\xf7\x8cG\xd7K8\x0f\xa6\xf4\xc8\xf1[s\xbe\x98\xc9a\v}\x80\x17\xde\x17\xa1\r\xfb\x0f\xbc\xef\xed\x19\xe9\xa0\x0fw7\b#\xf8ix\x81\\\xac\xa2\x88;\x96k\xb0\x16+\xa2jR,n6\x03\x88Ê\xdf\xfe5JP\xba+\xb3\x82\xc5d\xa1\xc6\xcb\n\xdeݍ\x9b\xc7\xd4(\x9f\xac\xd3(\xf6D:\x8e\xac\x98*W\rUf\x8fl\xa3/\x06s\bff.\x9d3\xa9X\x0f\xaf\x01K\xa27\xdc\xfe\x85{\x91\xfbf\xb8\xdb;\xc6\xdd)\xf3\x98>\x7f\xb2x\xf2\xe4\x05\xe71m\xb1W\x88\xa9\xc4\xebd\x81ɋ\xa5\xc9\xd417\x05%e`\xe1ڠ\x9ej\xa0\xe4Z\x8a\r\xdb\xfeD\x9b`F\x1c>'\x95\x85O\xd14\x16\xb4\x05養u\xb5ihwzPi,a%t.eU~B\xc8w\x01\xb0\x06\xb7\xbd\xed\xb4R\\B\x03j\xd5\xe5ۺی\xf6\xcd\xf4l\xf5\xc5(d\xf5\xb7\xdc\fj\x17\xac\x1a4\xa0\x84\xff\x96\x9a\xab/\xb8y\xc0z\x9b\xdet\xf7q\xb2\x16\x1bv\x86\x96q\xfc\xe0x9\xd1fT\xac\x93\x00>ʧt\x18\xdcHUS\x13$\x00\x13z\xd4\xe1\xddݚ\xf6\xd0@q9$\xf9\x9f:\xf9O\x9d\xfc\xa7N~Y\x9dl\x95\xdbݷ\x93R\xe1\xf7\xb1\xf7\xbc\xefI9\x8f\xe9\xff\x04\x18\xdb\x1f\xddO-h\xa3\xab\xc45x\xcf\xf3?\xf1\x86HCM\xfb\x9cE:\x00\x83u\xb2\xa2\xeay\x90;\b>fX6J+vKjp\xe0\xfe\xa4\x15\xe3\x17\xbd\xec\xed\xeb\x94\xe9d^\xb1u\xf2\xe5Z\x0e=\x13\xea\aw$\xacj;\xc4\xd4\t\x05:\x8b\xe1v\xc6\xc1\x8f\xf9\xc4B\xe6\xd5Ly\x06\xe3\x84\xeb\x98\x10_\xb9\xb8\"\xc9[\x9a2ob\xfaM\x11=\xa3\xd5tQA\xd9r8\xf5\x1eև^\xff\xe5\x9bX\xc3h\x19w\xb1Zd\xf7\f\xb4\xf5\xb0\x86w\xbezJx\xc8}JN\x05ᘰqW>\x16\xeev\xe0\xa2\x00\xad7-\x0f\x95\xa3\x85\x02j\xa0\f͙\x8e3>\xaa\xf6\xb1m\xb8\xa4%(\xe7\x92-\xa0\xf5\xbf\x06\x8dG<[\xe0\xcbVu\xd7\xed\xce^U\xfa,\xcd\xd5PE9\a\xfe\x89q\xd0?ʝ\xb0\xf3\xca\x10ȻT\xbf\xdeY٢U֬\xef\x89h\xeb5(\xa2\xc1\x98\xe9\x04\xdeF\xaa\xf9S+\x0e\xefL\x18\xd8B*\xe7\xb9S\xcc\xc0CC\x95\x06\x9cQ\xc6\n~\x1euq\x19\xc1\r\xa7[W\x9e\\\xb2\x82\x1a\x88\x06\x18G\x98\x9a>\xf6\xd7\b\x8b\xef\xb1ZTNlDd\v\xf5\xd41\xb9I\xb1\x9e\xba\xf29a\xaa\x93\x97>;\x8b\\\xd0\xc6\xe0\xa1D\xa4#\x12\xd1x\x18x\x91\xfa\xe8\xde\xe7\x01\xd8iN\xf3GK|\x11\xb36\xb4ND\t\xcbz\xe7\xfa\x10\f^ծ\xca^-t\xff\xd2\xdbX\xf4LvT\xc7\x03.I\u07fb\x83\xed\xc0\xa0\xabnACI\xe0\t\x04\xb1\xa2H\x19\x87r\x8eS\xbf\xe2\xe6\x9ez\x02\xf5\x83\x8ep\xb0:۲\xf8\x83\xa1\xcaĩ\x1f\xfa1.\x86\xbb\"%5\xb0\xb2\xbdOs\xdd\xd2WW+ub\x89\x06\x9e6\xf6\xe2Q\x84\xa3\x90\xd6\xfa\xb93\xc25hM\xb7!1\xb8\x03\x05d\v\xc2\xe2=\xee\xf7$=\xa6p\xcc\xda\x1b\x8bAb\x80\x16\xa6\xa5~\x00\xe7\xc2Ŋ\x96pg\x10\x9cC\x1d^1\xe2\f:\x9f\xaa3\x18\xdc`D\xb4\xc5\xde)ʄ85v3\x1dv癚\xaf\x11ʔz\xf4\xeb\x1b\xfc8\x82/z\xf1\x8d,ي\x8a\x8a\xed\xe4!\xe3J\xc9v[\x05ޜr\x88H\xd9b\xe4ܠ*\xd0\xe1ǜL\xabD\xaf\x90\xc2\u05fdMi\xe98\xddi\x1f\xe5\x19\x8aZu\x87\r;U5c\U000f3cc4\x13\x10\x17m\x7f\x02\"\xd5{Q\xcc\x1e\x8b<ܣ:ʵL\"!j\xe3\x17CB\x848\x85\x84\xbe/\xd1E<\xbf\x1b\x8cL\xf9('\xa2cމ\xc1%\u0383Z^t\xdf\t\x1a\xba;ǡC\x0f\x82\xbf\x93\xd2n\x03\b\xc7D\xbe8v:\xee\xfd\xfdF\xacO\xd1\xdb\xfaxr\xec\xfam\x04ct,\xddF\xb1\xdd0!\xde\xfc{\xb6Iɋ\xfbż5\x87\x7f8\xf8\xfa\xca\xc7\xcbwT\t&\xb6'a\xe4g\xdf7\x11\xcf{\xb0\xe7\x8c\xe8\xc3\xcc_,\xa6O\x9a\xa5\x83\x97\xc8\xe0e\x0f\xcf~$\xff\xe6\xff\x02\x00\x00\xff\xffJ\xb7g~\xf1r\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?\xbfp\x10jQ\u0605\x90\xdd~\x82>Ǟ\x8e#\x88\xa3\xafc\xba\xb9U\x1f\x8d\x9b\x90ϢO\x04\xe6\x88\xf1\xacT\x1e&\xfaF\x14\xc0\xcc\xceX(\x83\xaaoåN\f8,\xe4`\x15\x85\ac\x90\xde~P\xe3\x04\x91uQ\xf0u\x01\x97d\x19\x0f\xd0l\\I\x8f\x11\xed+\x18+\xb2S\x92\xccA\x1c!\x98\xf6?\xf4(C\xf1\x16\x7f\x00\xc6#\xe0==1\xb8+\x8a\x0e\xd1\xfbԊ\xe2Vi\xc8\xd0\xf1\xbf\xf4\x01\x85\x80\x82\x82\x18\xa9X\xa1\xe4=h\x87Ec\xe0\xd1\xc0\x00\nh\xce\xd0W\xd7h\x96\x85d\x9b\x1a\xdd\xf8%C-\x11\x95\x11!\x8d\x05\x1e\x11\xe6\x13\xf0\x0e\xbe\xa1E\x80\xfc\xaa\xa8\x8d\x05}\x93\xa9\n\xf2\xb0j7j\xceRy\xf8\xe1 d\x1f\xf4\x15\"\x03\xe4C\xe6*-he,&\xdam\xfc\x87\xe6\x91\x16\xf1\x90\xd5~\bm`7\xa9[\fXlx\xfe\xfb\xf3\v\x92\x80~\xef\xfd~\f\xe3\x1a\x1a2\xcd\xd2\xcd\xe4&\x8d\xb7\x10\x16\xca\bu'u\xd4\f\xbes\xad\xf9\xee\x00כ\x15\xc8\x17\xe0{\f\xf6\x80\xf32T\xfbN\xbc\x1f\xf6\xff\xaf\xc8\xfd\xd3\xf2\xdb\xd0*>\x17\x12\xf9\\\bc{l6n\xe9\x0f\xc9:\x16w{\x02I\a\x13\xd5\xe4\x14W\xffA\x88yҹ\x13\x9b,\x8dl\xfa\t\xf0OEɭR\x0f)\xd4\xfb\x1f\xac\u05ee\xfb\xb1\x8cv\x9a\xd8\x1a\xb6\xfcQ(m\x86k\xcb\xf0\r\xb2\xdaF5\v\xb7,\x17\x9b\rh\x84E{#\xcdV\xca!b\x1d\x8e\xf9XGeE+\f\xc6\xd52\x1dYJԈ\r\x85\xa2\xfa(T\xe7\xe0`hA\x0eD.\x1eE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7T\xa8$\xa0\x8f_bl\xb4_5N\x89\xb0\xfer\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccr\v3\x05_C\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fm\xb5\x83\x99\x00\xcb(\xc4݊l\xeb\xdcW\x144\x82\xc5r\x05\x86\x96\x92xU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8\xe6\xc8(\xbe\xffJD\x0fV\xe7Ha\x9f\xd0$\x8c6Y\x92\xe7C\x94\xf4Hq\x01f\xd9Y\xd2\x146|Mah\xcf\x7f\xdc\xdb\x7f\xda#ʯ\x8bw\xc7M\x98\x19\xac\x9b\x9cS/˸\xa6\x9b\x7f\x12\xbe\x91ɺ\xf1\x16k\x16\xcf>u[^\xd0V\x8agH~\xc16\xa2\xb0@N\xd5\x14\xa2l\x06\xe7NI\xa0T\v\xcchg\xddf\xdb\x0f͆[B\x8b\x01\xad\x86\x00\x9c\x83\x1e\xa2\x1c\xe2A\x02Hָ\x16\xb4\xd3,\xdcJ\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\xffp\xc18{\x80\x9ds\xb1\xb8d\xc87\x1e*'\xa2\xa0\x81\xd2(HS<\xc0\x8e@\x8d\xe7E\x8c\x979\xd2\xe2\xca\x03\x8cl\x95\xc6J\x8f\xae\x88\x9f߀rt\xc3\x0fD\x98\x94\xd9Ԗ\x86\xa8~\xfa\x8cd%\xc4\xcb\f\xbd\x14J\xe0ˑ\xc3N\x16\xa7n_\xfdL\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\xfb\x87\x03t\x96\x04\x89\xd8\u0378q\xb6\x14\x05\xb5\xe1\x870l%1$s$\x9a\xd1\x1d%X\xb9.]gemh\x7fZ*\xb9p\xcbbc\xbdy\x1e(\xddc\xc1I:\xf6\x9dޢ1r\xbf\xb8T\xaf\x82g\x90\x87-:\xcaA\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\xa2\xceXY\xe0\\O\xac\x19\xa4%\xa9z$\x8d\xe9p\xf5Tb=\x93L\xe4E\x90ە$\x05\xddl\xe0y\xd6k\xa6\xdc\x1c\xa3b:cq.@\xc9ik\xedoh\xe9i6\xfe\x9dU\\h\xb3d\xef(\x1d\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\xa3\x9c\xd4mWa\x9d\xaf\xe4\xb9\xf3e\xf6\x14O\xe3\xf8(Y\xec\xd89\xfdv\xfe\\\xf7n\x86DϨ\xda\x13\xe5\x92W\xe9\x92L\xc9\xc6s\x02\r\fփC\x84\x8d\x9b\xac[\f\x10\xa6(\x90,ʕ2\x91d\x91\bZ\t\x82~\xad\x8cu\xeb\x90=\x7f\x7ft\xa1R\x85\xc5I\xc67\x1643V\xe9\x90NJ\x8a?e)\xbe[n\xb7`\xc0\xefC\xf9EO\a\x18\xa3\xd8\xf3V78\xabr\xee\xf6¨#\x9e\x91\xf7Dm+\xad20Ѽ\x88\xb6$ڦ\x91\xac\xa5.\x1d\x9au]\ue8bfM\x92\xd6NY\x94\x0ee\x9e#\x8f\xa4;\"2\xfa\xf0\xad\xb3D\x8d\xda\x05\xffN\x91\xd6cpdt\x00\xa6,\xf90\x87:\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\x90:\xe9\xb2\xcd\xe7\xc4ہ\xa4[\xfe\b>\xdd\x17d\xa6jIKa\xa8\a\xb0\x9b\x19\x10\x1dk\x9c\x15H\xb4w\x9d\xc6\xd1\f̱\xb2 I\x12rrݬ\xdb\xe4#\x17i\xebV\xec8\xb6\xdaC9\x9cc\xe5\xf8y\x14\x12<\xbbg\x10J\xfeM\x94u\xc9x\x89<$\xb7C\x94\xd0\x1cCp\xecn\xd2>\xb1\x05\x19-\xabp\x96U\x05X\xf0i\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\xb4c.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;z\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*\x9fbK76D\x80\xd1%\x1e<\xa7\x9b\x1b:\az\xbc\x1e\b\xf7K\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~3d(\xd3\x0e@\xf1=\x84\xf3\xb9dR\xba\xe7\x9a?+\xee\xfc2\x80\x85\xc2\x12\xdc\xd4W\x8c\x03ʺ\xb0\xa2*\xdaK\xecb\x01\xe7\x16v\xcdeE?+:\"\xefo\xea\xfa\xf2\xb5\x91\xf8\xe5 \xaa\xe1\x86=AQ0\x1e\x9b\x9b{T\xc8\xdc婙Z\x00\xdaF\x9c\xe5\xfe2&\x7f\xe3ꅛ.t\x1b\x00Y\xd82\xb6\xd4\xc7\xe5\u16fe\x0e\x1a\xb0T=\xb6登x\x83\xbe\xfdR\x83\xde1\xbaw\xac\xf1\xcd\xdaC\xa5~\xa2\x1b\fL\x83\xfa\xf1\xea\xf0О\xc9^\x80Ӫ\a\xf6N:\x8f`\x88\x13\xb5A\xbd\xd3\x06t\xa8Te\xecr>\x16&\xe8>\b\xa9\x1a\b\x91\xa6)\xce\xff\x9cS\x96/\x11ޝ\"\xc0K\xf2\x80\xe6y\xaf\xdf\xf1\xf4䱧&ӓQ\x92NI\xbeD\xb87'\xe0\x9b實\x9f\x82\x9c\xbf\xf1\xfc§\x1e_\xea\xb4\xe3\f\ua95en\x9cO\xbbW:\xcd\xf8\xea\xa7\x18_\xf3\xf4\xe2\xacS\x8b\xc9\xe9Y\xb32\x0e\xe6\xa4V=\xe3\xb8]Z.\xc1\xf4)\xc4\xc4Ӈ\x89\x99\x06i\x83?r؉\xa7\v\xe7\x9f*L\xe4\xef\x9c)\xfdʧ\a_\xf9\xd4\xe0\xf78-\x98 \x81\tU\xe6\x9f\n|\xf6\x96\x94\xd29\xe8\xc9m\xbf9R;)\xaf\xa9\xb1\\\x1f\xb1\xc1\xbeV\xb8M\x16k\xf5b\x002K\xfe\xf5\x03z\xe9\xe2\xd068Jf\xc7#\xea\xedK\xb6\xeeZ\xdf!\xf6O`\xb8\xadK\x03\x15G\x03@\x81\x1b\xa5fE]\x85\x0f<\xdb\x0ez\xd8r\xc36J\x97ܲ\xf3f\xb3\xf8\x8d\xeb\x00\xff>_2\xf6Q5\xb9:\xdd\xfbҌ(\xabb\x87\x91\x18;\xef6x\x9e\x94D\xa53\xf4|\xad\n\x91E|\xce\xd1{\xf5\\\x83\xbdˆ\xe8濬\x93-\x12\v|\xb0\xb9\b\xb7.\xf6\xafdv\x97\xe0\x1f\xb9V\xc2+\xf1'z\xa3\xea\x04\xabn\xef\xaeW\x04+\x88\x11=~\xd5$(6,_\x03\xba\f\xed\xd8\x0f\xe9\x93զ\a\xb5\x9f#\xdc}\xe1\x03r\xf7\x9cKp[\xbcj\xce\x14j\xad\xeb\x95\xc3\xe5PO(_\\\xee\x98\xf2\xefu\b\x9d/*\xae\xed\xce%\x13]\xf4\xf0\bv}j\xd5젵\xda\x7f\xae\xa6[zd\x0f/\xd5\xd0N\xf6\xae\xea'\x0f\f\xe9\xf9\x1c\x9c\x0e\x9f\xaa\x9els*2Z\xa5\xf9=|R\xeeA\xa2\x141\xe9\xb7\xe8=W\xe5=\xb7\x90\xaf\xed'aL\xd1\xfb\xb1\r\x01\xb6\xe73\xf6.\xfaGl\x8f|\xca\xc0\xda\xe292r{\xfbɍ\x94ށy\xef\x9ftA}l\x00Y\x10(࠭\xf1\xbf[\xf5D\x17\xe0\xc7טë+\x9d\x87߀\x0e\x8aP\n\xefQì\xabB\xf1\x1c\xf4\x15\xbd<\x930\xe2\x9fz\r\x06\xee@\xff\xfd\x1ao7#\xe3\t=\xbf`\x96\fztE\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{\xa3\x81\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}\xec\xbd/\x13tB\x8az\xbf\x1bo\xd9\t\x11:ډ|\xfa\xb8\x12\x8f\xc1\xe2ƨLPTA\x0f\n\xd1I\xa4\x97\xbbC\xfcP\x80x@:j\x03_\x9e$\xe8\xaf\xc1\x02\x99\x95\x8c\xbd\xdb2\xad\xfd~ڃ\x16}\xaf\xc5*\xec{\x04\xc6\x00\x00Sa\x9f˸\x97\x80\xc2\xf6\x9a0͋p\xfb\xf4\x9cP!qK7\xee\xb0-\xc6\x1f\xb0Z4\x0fm\x9d%\x90۽\x7f\xd4\a<\xfe\x0e\xa0{()㕭uЮ\xb5\xa6[\xd6\x11\b\xb8Kȏ{\t\xb0} \xee\x18\x06\xb7/\xb4\xb5\xfb\x0f\x93oȎ\xc0i\xde\xf2\x8b>\f\xe6\"j\xf7\xc6\xeb\x02\xe1\x1f\xc7\xe3\xd1\x19\x838߸\a\xdf&\x88\xf0\xa9\xad96\xe0f\x188d\xff\x84ܫ\x8e\x84.ݟ\x18\xc35\xd6iN\xb9z9\xa2\x86\xe1\xb2\xfe\x9b\x18\x13ƏB.\xd8g؏\xd8\x17\xec\x83\xc4A\xec\x13\xc0\x9dw\x84\x9c\xb6VH;\xce\x19\xe2cӊ\x0e\x9b\x8eh\xc8i\xb1\xbd\x1b\xc0\x18d\xb2ӣOM\x15w\xda\u0530ߊ1o\x94v\xcc2\x1c\xe8\xef\xf6~\x8dj\xf0\x83\xda;\xa6\xb9G\xd5\xc8\xdeGz=0\xefH\x8e\xf7һ_\xeau\xfb\xa0\x02\xfb\xdb\xdf\xcf\xfe?\x00\x00\xff\xff0\xe5e\x05\x8f|\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?\xbfp\x10jQ\u0605\x90\xdd~\x82>Ǟ\x8e#\x88\xa3\xafc\xba\xb9U\x1f\x8d\x9b\x90ϢO\x04\xe6\x88\xf1\xacT\x1e&\xfaF\x14\xc0\xcc\xceX(\x83\xaaoåN\f8,\xe4`\x15\x85\ac\x90\xde~P\xe3\x04\x91uQ\xf0u\x01\x97d\x19\x0f\xd0l\\I\x8f\x11\xed+\x18+\xb2S\x92\xccA\x1c!\x98\xf6?\xf4(C\xf1\x16\x7f\x00\xc6#\xe0==1\xb8+\x8a\x0e\xd1\xfbԊ\xe2Vi\xc8\xd0\xf1\xbf\xf4\x01\x85\x80\x82\x82\x18\xa9X\xa1\xe4=h\x87Ec\xe0\xd1\xc0\x00\nh\xce\xd0W\xd7h\x96\x85d\x9b\x1a\xdd\xf8%C-\x11\x95\x11!\x8d\x05\x1e\x11\xe6\x13\xf0\x0e\xbe\xa1E\x80\xfc\xaa\xa8\x8d\x05}\x93\xa9\n\xf2\xb0j7j\xceRy\xf8\xe1 d\x1f\xf4\x15\"\x03\xe4C\xe6*-he,&\xdam\xfc\x87\xe6\x91\x16\xf1\x90\xd5~\bm`7\xa9[\fXlx\xfe\xfb\xf3\v\x92\x80~\xef\xfd~\f\xe3\x1a\x1a2\xcd\xd2\xcd\xe4&\x8d\xb7\x10\x16\xca\bu'u\xd4\f\xbes\xad\xf9\xee\x00כ\x15\xc8\x17\xe0{\f\xf6\x80\xf32T\xfbN\xbc\x1f\xf6\xff\xaf\xc8\xfd\xd3\xf2\xdb\xd0*>\x17\x12\xf9\\\bc{l6n\xe9\x0f\xc9:\x16w{\x02I\a\x13\xd5\xe4\x14W\xffA\x88yҹ\x13\x9b,\x8dl\xfa\t\xf0OEɭR\x0f)\xd4\xfb\x1f\xac\u05ee\xfb\xb1\x8cv\x9a\xd8\x1a\xb6\xfcQ(m\x86k\xcb\xf0\r\xb2\xdaF5\v\xb7,\x17\x9b\rh\x84E{#\xcdV\xca!b\x1d\x8e\xf9XGeE+\f\xc6\xd52\x1dYJԈ\r\x85\xa2\xfa(T\xe7\xe0`hA\x0eD.\x1eE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7T\xa8$\xa0\x8f_bl\xb4_5N\x89\xb0\xfer\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccr\v3\x05_C\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fm\xb5\x83\x99\x00\xcb(\xc4݊l\xeb\xdcW\x144\x82\xc5r\x05\x86\x96\x92xU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8\xe6\xc8(\xbe\xffJD\x0fV\xe7Ha\x9f\xd0$\x8c6Y\x92\xe7C\x94\xf4Hq\x01f\xd9Y\xd2\x146|Mah\xcf\x7f\xdc\xdb\x7f\xda#ʯ\x8bw\xc7M\x98\x19\xac\x9b\x9cS/˸\xa6\x9b\x7f\x12\xbe\x91ɺ\xf1\x16k\x16\xcf>u[^\xd0V\x8agH~\xc16\xa2\xb0@N\xd5\x14\xa2l\x06\xe7NI\xa0T\v\xcchg\xddf\xdb\x0f͆[B\x8b\x01\xad\x86\x00\x9c\x83\x1e\xa2\x1c\xe2A\x02Hָ\x16\xb4\xd3,\xdcJ\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\xffp\xc18{\x80\x9ds\xb1\xb8d\xc87\x1e*'\xa2\xa0\x81\xd2(HS<\xc0\x8e@\x8d\xe7E\x8c\x979\xd2\xe2\xca\x03\x8cl\x95\xc6J\x8f\xae\x88\x9f߀rt\xc3\x0fD\x98\x94\xd9Ԗ\x86\xa8~\xfa\x8cd%\xc4\xcb\f\xbd\x14J\xe0ˑ\xc3N\x16\xa7n_\xfdL\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\xfb\x87\x03t\x96\x04\x89\xd8\u0378q\xb6\x14\x05\xb5\xe1\x870l%1$s$\x9a\xd1\x1d%X\xb9.]gemh\x7fZ*\xb9p\xcbbc\xbdy\x1e(\xddc\xc1I:\xf6\x9dޢ1r\xbf\xb8T\xaf\x82g\x90\x87-:\xcaA\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\xa2\xceXY\xe0\\O\xac\x19\xa4%\xa9z$\x8d\xe9p\xf5Tb=\x93L\xe4E\x90ە$\x05\xddl\xe0y\xd6k\xa6\xdc\x1c\xa3b:cq.@\xc9ik\xedoh\xe9i6\xfe\x9dU\\h\xb3d\xef(\x1d\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\xa3\x9c\xd4mWa\x9d\xaf\xe4\xb9\xf3e\xf6\x14O\xe3\xf8(Y\xec\xd89\xfdv\xfe\\\xf7n\x86DϨ\xda\x13\xe5\x92W\xe9\x92L\xc9\xc6s\x02\r\fփC\x84\x8d\x9b\xac[\f\x10\xa6(\x90,ʕ2\x91d\x91\bZ\t\x82~\xad\x8cu\xeb\x90=\x7f\x7ft\xa1R\x85\xc5I\xc67\x1643V\xe9\x90NJ\x8a?e)\xbe[n\xb7`\xc0\xefC\xf9EO\a\x18\xa3\xd8\xf3V78\xabr\xee\xf6¨#\x9e\x91\xf7Dm+\xad20Ѽ\x88\xb6$ڦ\x91\xac\xa5.\x1d\x9au]\ue8bfM\x92\xd6NY\x94\x0ee\x9e#\x8f\xa4;\"2\xfa\xf0\xad\xb3D\x8d\xda\x05\xffN\x91\xd6cpdt\x00\xa6,\xf90\x87:\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\x90:\xe9\xb2\xcd\xe7\xc4ہ\xa4[\xfe\b>\xdd\x17d\xa6jIKa\xa8\a\xb0\x9b\x19\x10\x1dk\x9c\x15H\xb4w\x9d\xc6\xd1\f̱\xb2 I\x12rrݬ\xdb\xe4#\x17i\xebV\xec8\xb6\xdaC9\x9cc\xe5\xf8y\x14\x12<\xbbg\x10J\xfeM\x94u\xc9x\x89<$\xb7C\x94\xd0\x1cCp\xecn\xd2>\xb1\x05\x19-\xabp\x96U\x05X\xf0i\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\xb4c.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;z\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*\x9fbK76D\x80\xd1%\x1e<\xa7\x9b\x1b:\az\xbc\x1e\b\xf7K\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~3d(\xd3\x0e\xc0\xde\xc5Σ\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^\xc4\x17\v\x9a\xb7\xb0k.\\\xfaY\xd11\x7f\x7f\xdbؗ\xafͬ]\x0e\"3n\xd8\x13\x14\x05\xe31\xfd\xb2G\x85\xcc]\x00\x9b\xa9\x05\xa0}GM\xe5/\x94\xf2\xb7\xc6^\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]0\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ɿ\xe0@\xafu\x1c\xda\xcaG\xc9\xecxD\xbd\xbd\xd5\xd6]\xeb;\xc4\xfe\x19\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\xee\"\xffc\xd7{*\xf1'zg\xeb\x04+\x87\xef\xaeW\x04+\x88\x11=\xe0\xd5$Y6,_\x03\xba\f\xed\xd8\x0f\xe9\x93զ\a\xb5\x9f\xe7\xdc}\xa5\x04r\xf7$Mp[\xbcj\xce\x14j\xad\xeb\x95\xc3\xe5PO(_\\\xee\x98\xf2o\x8e\b\x9d/*\xae\xed\xce%D]\xf4\xf0\bv}j\xe5\uf835\xda\x7fr\xa7[zd\x0f\xaf\xed\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\xca\xc0_\xd4#\xbc\x8f\xae\xbe\xf6\x9f\xdd\x194\x19I\xaf\fP\xe9\xa2\xfc\b\x05ۜJ\xba\xa7\xfcyj/\x9e/\x19P\xf1\xf7\x9c?gq\xf2\xa6\x0fj\xfc%\x1a\xba\x05>t\x1a\xf3\xaa药\x1d\xbb\xbe\xa3\xb8\xb5Q\xa5~\xea\xfb\xb85,O\x86$\x89\b,!\x0f>\xces*2Z\xa5\xf9=|R\xeeQ\xa5\x141\xe9\xb7\xe8=\xb9\xe5=\xb7\x90s\xee'aL\xd1\xfb\xb1\r\x01\xb6gL\xf6\x1e+@l\x8f|\x8e\xc1\xda\xe292r{\xfbɍ\x94\u07b2y\uf7e5A}l\x00Y\x10(࠭\xf1\xbf[\xf5D\x97\xf8\xc7ט\xc3\xcb1\x9d\xc7\xeb\x80\x0e\xbbP\x1a\xf2Qì\xabB\xf1\x1c\xf4\x15\xbd\x9e\x930\xe2\x9fz\r\x06\xee@\xff\r\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˸\u05cc\xc2\xf6\x9a0ͫv\xfb\xf4\x9cP!qK7\xee\xb0-\xc6\x1f\xe1Z4\x8f\x85\x9d%\x90۽\xe1\xd4\a<\xfe\x96\xa1{\xec)㕭uЮ\xb5\xa6\x9b\xe2\x11\b\xb8\x8bԏ{Ͱ}\xe4\xee\x18\x06\xb7\xaf̵\xfb\x0f\x93\xef\xe0\x8e\xc0i\xde#\x8c>n\xe6\"j\xf7N\xed\x02\xe1\x1f\xc7\xe3\xd1\x19\x838߸G\xeb&\x88\xf0\xa9\xad96\xe0f\x188d\xff\fޫ\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\x011\xefH\x8e\xf7һ_\xeau\xfb(\x04\xfb\xdb\xdf\xcf\xfe?\x00\x00\xff\xff\xf8FZ\xc4S}\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"), } diff --git a/pkg/apis/velero/v1/backup_types.go b/pkg/apis/velero/v1/backup_types.go index e4e734279..65bf1ae81 100644 --- a/pkg/apis/velero/v1/backup_types.go +++ b/pkg/apis/velero/v1/backup_types.go @@ -23,6 +23,9 @@ import ( type Metadata struct { Labels map[string]string `json:"labels,omitempty"` + // +optional + // +nullable + Annotations map[string]string `json:"annotations,omitempty"` } // BackupSpec defines the specification for a Velero backup. diff --git a/pkg/apis/velero/v1/zz_generated.deepcopy.go b/pkg/apis/velero/v1/zz_generated.deepcopy.go index c40fbb806..106beaa79 100644 --- a/pkg/apis/velero/v1/zz_generated.deepcopy.go +++ b/pkg/apis/velero/v1/zz_generated.deepcopy.go @@ -895,6 +895,13 @@ func (in *Metadata) DeepCopyInto(out *Metadata) { (*out)[key] = val } } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metadata. diff --git a/pkg/builder/backup_builder.go b/pkg/builder/backup_builder.go index 0553116a4..056f198c2 100644 --- a/pkg/builder/backup_builder.go +++ b/pkg/builder/backup_builder.go @@ -109,8 +109,23 @@ func (b *BackupBuilder) FromSchedule(schedule *velerov1api.Schedule) *BackupBuil b.object.Spec = schedule.Spec.Template b.ObjectMeta(WithLabelsMap(labels)) - if schedule.Annotations != nil { - b.ObjectMeta(WithAnnotationsMap(schedule.Annotations)) + var annotations map[string]string + + // Check if there's explicit Annotations defined in the Schedule object template + // and if present then copy it to the backup object. + if schedule.Spec.Template.Metadata.Annotations != nil { + logger := logging.DefaultLogger(logging.LogLevelFlag(logrus.InfoLevel).Parse(), logging.NewFormatFlag().Parse()) + annotations = schedule.Spec.Template.Metadata.Annotations + logger.WithFields(logrus.Fields{ + "backup": fmt.Sprintf("%s/%s", b.object.GetNamespace(), b.object.GetName()), + "annotations": schedule.Spec.Template.Metadata.Annotations, + }).Info("Schedule.template.metadata.annotations set - using those annotations instead of schedule.annotations for backup object") + } else { + annotations = schedule.Annotations + } + + if annotations != nil { + b.ObjectMeta(WithAnnotationsMap(annotations)) } if boolptr.IsSetToTrue(schedule.Spec.UseOwnerReferencesInBackup) { diff --git a/pkg/builder/backup_builder_test.go b/pkg/builder/backup_builder_test.go new file mode 100644 index 000000000..c7f3ef000 --- /dev/null +++ b/pkg/builder/backup_builder_test.go @@ -0,0 +1,84 @@ +/* +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 builder + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" +) + +func TestBackupFromSchedule(t *testing.T) { + tests := []struct { + name string + schedule *velerov1api.Schedule + expectedLabels map[string]string + expectedAnnotations map[string]string + }{ + { + name: "no schedule labels/annotations and no template overrides", + schedule: ForSchedule("velero", "test"). + Result(), + expectedLabels: map[string]string{velerov1api.ScheduleNameLabel: "test"}, + expectedAnnotations: nil, + }, + { + name: "schedule labels/annotations are copied when no template override is set", + schedule: ForSchedule("velero", "test"). + ObjectMeta( + WithLabels("schedule-label", "schedule-value"), + WithAnnotations("schedule-annotation", "schedule-value"), + ). + Result(), + expectedLabels: map[string]string{ + "schedule-label": "schedule-value", + velerov1api.ScheduleNameLabel: "test", + }, + expectedAnnotations: map[string]string{"schedule-annotation": "schedule-value"}, + }, + { + name: "template.metadata.labels/annotations override schedule labels/annotations", + schedule: ForSchedule("velero", "test"). + ObjectMeta( + WithLabels("schedule-label", "schedule-value"), + WithAnnotations("schedule-annotation", "schedule-value"), + ). + Template(velerov1api.BackupSpec{ + Metadata: velerov1api.Metadata{ + Labels: map[string]string{"template-label": "template-value"}, + Annotations: map[string]string{"template-annotation": "template-value"}, + }, + }). + Result(), + expectedLabels: map[string]string{ + "template-label": "template-value", + velerov1api.ScheduleNameLabel: "test", + }, + expectedAnnotations: map[string]string{"template-annotation": "template-value"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + backup := ForBackup("velero", "test-backup").FromSchedule(test.schedule).Result() + assert.Equal(t, test.expectedLabels, backup.GetLabels()) + assert.Equal(t, test.expectedAnnotations, backup.GetAnnotations()) + }) + } +} diff --git a/site/content/docs/main/api-types/schedule.md b/site/content/docs/main/api-types/schedule.md index ef3df4324..ef2d14b07 100644 --- a/site/content/docs/main/api-types/schedule.md +++ b/site/content/docs/main/api-types/schedule.md @@ -155,11 +155,13 @@ spec: uploaderConfig: # ParallelFilesUpload is the number of files parallel uploads to perform when using the uploader. parallelFilesUpload: 10 - # The labels you want on backup objects, created from this schedule (instead of copying the labels you have on schedule object itself). - # When this field is set, the labels from the Schedule resource are not copied to the Backup resource. + # The labels/annotations you want on backup objects, created from this schedule (instead of copying the labels/annotations you have on schedule object itself). + # When this field is set, the labels/annotations from the Schedule resource are not copied to the Backup resource. metadata: labels: labelname: somelabelvalue + annotations: + annotationname: someannotationvalue # Actions to perform at different times during a backup. The only hook supported is # executing a command in a container in a pod using the pod exec API. Optional. hooks: From a266a2577e46eee733e78975cee32b4507e749df Mon Sep 17 00:00:00 2001 From: wolf-06 Date: Thu, 30 Jul 2026 16:35:47 +0530 Subject: [PATCH 115/194] add auto-documenting help command to Makefile Signed-off-by: wolf-06 --- Makefile | 79 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index bb766c7c9..8d7e99951 100644 --- a/Makefile +++ b/Makefile @@ -160,20 +160,35 @@ GOBIN=$$(pwd)/.go/bin PROTOC_GEN_GO_VERSION := $(shell go list -m -f '{{.Version}}' google.golang.org/protobuf) GOIMPORTS_VERSION := $(shell go list -m -f '{{.Version}}' golang.org/x/tools) +# ============================================================================== +# ================================ COMMANDS ==================================== +# ============================================================================== + +# ================================== +# Help +# ================================== +# To document a new target, add "## " at the end of the target line. +# Example: new-target: ## Description of the new target + +.PHONY: help +help: ## Display this help message + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) + + # If you want to build all binaries, see the 'all-build' rule. # If you want to build all containers, see the 'all-containers' rule. -all: +all: ## Build all binaries @$(MAKE) build -build-%: +build-%: ## Build specific binary @$(MAKE) --no-print-directory ARCH=$* build -all-build: $(addprefix build-, $(CLI_PLATFORMS)) +all-build: $(addprefix build-, $(CLI_PLATFORMS)) ## Build for all CLI platforms -all-containers: +all-containers: ## Build all containers @$(MAKE) --no-print-directory container -local: build-dirs +local: build-dirs ## Build locally # Add DEBUG=1 to enable debug locally GOOS=$(GOOS) \ GOARCH=$(GOARCH) \ @@ -187,7 +202,7 @@ local: build-dirs OUTPUT_DIR=$$(pwd)/_output/bin/$(GOOS)/$(GOARCH) \ ./hack/build.sh -build: _output/bin/$(GOOS)/$(GOARCH)/$(BIN) +build: _output/bin/$(GOOS)/$(GOARCH)/$(BIN) ## Build the velero binary (use build-- for specific targets) _output/bin/$(GOOS)/$(GOARCH)/$(BIN): build-dirs @echo "building: $@" @@ -207,7 +222,7 @@ _output/bin/$(GOOS)/$(GOARCH)/$(BIN): build-dirs TTY := $(shell tty -s && echo "-t") # Example: make shell CMD="date > datefile" -shell: build-dirs build-env +shell: build-dirs build-env ## Run a shell in the build container @# bind-mount the Velero root dir in at /github.com/vmware-tanzu/velero @# because the Kubernetes code-generator tools require the project to @# exist in a directory hierarchy ending like this (but *NOT* necessarily @@ -230,7 +245,7 @@ shell: build-dirs build-env $(BUILDER_IMAGE) \ /bin/sh $(CMD) -container: +container: ## Build the docker container (use container-- for specific targets) ifneq ($(CONTAINER_TOOL),docker) $(error $(DOCKER_ONLY_ERROR)) endif @@ -312,7 +327,7 @@ endif @echo "built container: $(IMAGE):$(VERSION)-windows-$(BUILDX_OSVERSION)-$(BUILDX_ARCH)" -push-manifest: +push-manifest: ## Push multi-arch manifest ifneq ($(CONTAINER_TOOL),docker) $(error $(DOCKER_ONLY_ERROR)) endif @@ -335,36 +350,36 @@ endif @docker manifest inspect --insecure=$(INSECURE_REGISTRY) $(IMAGE_TAG) SKIP_TESTS ?= -test: build-dirs +test: build-dirs ## Run unit tests ifneq ($(SKIP_TESTS), 1) @$(MAKE) shell CMD="-c 'hack/test.sh $(WHAT)'" endif -test-local: build-dirs +test-local: build-dirs ## Run unit tests locally ifneq ($(SKIP_TESTS), 1) hack/test.sh $(WHAT) endif -verify: +verify: ## Run all verify scripts ifneq ($(SKIP_TESTS), 1) @$(MAKE) shell CMD="-c 'hack/verify-all.sh'" endif -lint: +lint: ## Run linter ifneq ($(SKIP_TESTS), 1) @$(MAKE) shell CMD="-c 'hack/lint.sh'" endif -local-lint: +local-lint: ## Run linter locally ifneq ($(SKIP_TESTS), 1) @hack/lint.sh endif -update: +update: ## Run all update scripts @$(MAKE) shell CMD="-c 'hack/update-all.sh'" # update-crd is for development purpose only, it is faster than update, so is a shortcut when you want to generate CRD changes only -update-crd: +update-crd: ## Update generated CRD code @$(MAKE) shell CMD="-c 'hack/update-3generated-crd-code.sh'" build-dirs: @@ -392,7 +407,7 @@ else $(CONTAINER_TOOL) pull -q $(BUILDER_IMAGE) || $(MAKE) build-image endif -build-image: +build-image: ## Build the builder image @# When we build a new image we just untag the old one. @# This makes sure we don't leave the orphaned image behind. $(eval old_id=$(shell $(CONTAINER_TOOL) image inspect --format '{{ .ID }}' ${BUILDER_IMAGE} 2>/dev/null)) @@ -409,7 +424,7 @@ endif $(CONTAINER_TOOL) rmi -f $$id || true; \ fi -push-build-image: +push-build-image: ## Push the builder image ifneq ($(CONTAINER_TOOL),docker) $(error $(DOCKER_ONLY_ERROR)) endif @@ -423,10 +438,10 @@ else docker push $(BUILDER_IMAGE) endif -build-image-hugo: +build-image-hugo: ## Build the hugo image for docs cd site && $(CONTAINER_TOOL) build --pull -t $(HUGO_IMAGE) . -clean: +clean: ## Clean up build artifacts and modcache # if we have a cached image then use it to run go clean --modcache # this test checks if we there is an image id in the BUILDER_IMAGE_CACHED variable. ifneq ($(strip $(BUILDER_IMAGE_CACHED)),) @@ -438,21 +453,21 @@ endif .PHONY: modules -modules: +modules: ## Tidy go modules go mod tidy .PHONY: verify-modules -verify-modules: modules +verify-modules: modules ## Verify go modules are up to date @if !(git diff --quiet HEAD -- go.sum go.mod); then \ echo "go module files are out of date, please commit the changes to go.mod and go.sum"; exit 1; \ fi -ci: verify-modules verify all test +ci: verify-modules verify all test ## Run CI checks -changelog: +changelog: ## Generate changelog hack/release-tools/changelog.sh # release builds a GitHub release using goreleaser within the build container. @@ -470,7 +485,7 @@ changelog: # RELEASE_NOTES_FILE=changelogs/CHANGELOG-1.2.md \ # PUBLISH=true \ # make release -release: +release: ## Build a GitHub release using goreleaser $(MAKE) shell CMD="-c '\ GITHUB_TOKEN=$(GITHUB_TOKEN) \ RELEASE_NOTES_FILE=$(RELEASE_NOTES_FILE) \ @@ -478,7 +493,7 @@ release: REGISTRY=$(REGISTRY) \ ./hack/release-tools/goreleaser.sh'" -serve-docs: build-image-hugo +serve-docs: build-image-hugo ## Serve the documentation site locally $(CONTAINER_TOOL) run \ --rm \ -v "$$(pwd)/site:/project" \ @@ -487,18 +502,18 @@ serve-docs: build-image-hugo server --bind=0.0.0.0 --enableGitInfo=false # gen-docs generates a new versioned docs directory under site/content/docs. # Please read the documentation in the script for instructions on how to use it. -gen-docs: +gen-docs: ## Generate a new versioned docs directory @hack/release-tools/gen-docs.sh .PHONY: test-e2e -test-e2e: local +test-e2e: local ## Run end-to-end tests $(MAKE) -e VERSION=$(VERSION) -C test/ run-e2e .PHONY: test-perf -test-perf: local +test-perf: local ## Run performance tests $(MAKE) -e VERSION=$(VERSION) -C test/ run-perf -go-generate: +go-generate: ## Run go generate go generate ./pkg/... # requires an authenticated gh cli @@ -510,11 +525,11 @@ go-generate: new-changelog: GH_LOGIN ?= $(shell gh pr view --json author --jq .author.login 2> /dev/null) new-changelog: GH_PR_NUMBER ?= $(shell gh pr view --json number --jq .number 2> /dev/null) new-changelog: CHANGELOG_BODY ?= '$(shell gh pr view --json title --jq .title)' -new-changelog: +new-changelog: ## Create a new changelog file for a PR @if [ "$(GH_LOGIN)" = "" ]; then \ echo "branch does not have PR or cli not logged in, try 'gh auth login' or 'gh pr create'"; \ exit 1; \ fi @mkdir -p ./changelogs/unreleased/ && \ echo $(CHANGELOG_BODY) > ./changelogs/unreleased/$(GH_PR_NUMBER)-$(GH_LOGIN) && \ - echo \"$(CHANGELOG_BODY)\" added to "./changelogs/unreleased/$(GH_PR_NUMBER)-$(GH_LOGIN)" \ No newline at end of file + echo \"$(CHANGELOG_BODY)\" added to "./changelogs/unreleased/$(GH_PR_NUMBER)-$(GH_LOGIN)" From 9063ee5fb7f0913f22e25aec0bfab7503be6eafe Mon Sep 17 00:00:00 2001 From: Chlins Zhang Date: Fri, 31 Jul 2026 00:24:51 +0800 Subject: [PATCH 116/194] Replace rebase action with GitHub CLI (#10093) * Replace rebase action with GitHub CLI Signed-off-by: chlins * Add contents write permission for rebase workflow Updating the PR branch pushes to the head branch, which requires contents: write for the GITHUB_TOKEN. Signed-off-by: chlins --------- Signed-off-by: chlins --- .github/workflows/rebase.yml | 36 +++++++++++++++++++++--------- changelogs/unreleased/10093-chlins | 1 + 2 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/10093-chlins diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml index 064bef70a..6db2503f0 100644 --- a/.github/workflows/rebase.yml +++ b/.github/workflows/rebase.yml @@ -1,18 +1,32 @@ -on: +name: Automatic Rebase + +on: issue_comment: types: [created] -name: Automatic Rebase + +permissions: {} + jobs: rebase: name: Rebase - if: github.repository == 'velero-io/velero' && github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') + if: >- + github.repository == 'velero-io/velero' && + github.event.issue.pull_request != null && + github.event.comment.body == '/rebase' && + contains( + fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), + github.event.comment.author_association + ) runs-on: ubuntu-latest + permissions: + # contents: write is required because updating the pull request branch + # pushes commits to the head branch. + contents: write + pull-requests: write steps: - - name: Checkout the latest code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Automatic Rebase - uses: cirrus-actions/rebase@1.8 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Rebase pull request + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.issue.number }} + run: gh pr update-branch "$PR_NUMBER" --repo "$GH_REPO" --rebase diff --git a/changelogs/unreleased/10093-chlins b/changelogs/unreleased/10093-chlins new file mode 100644 index 000000000..143c6b856 --- /dev/null +++ b/changelogs/unreleased/10093-chlins @@ -0,0 +1 @@ +Replace rebase action with GitHub CLI From 95e76381fda49e802a10156502283b7117aafb27 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 30 Jul 2026 09:27:15 -0700 Subject: [PATCH 117/194] Site: update homepage CTA and add LinkedIn to footer (#10113) * site: update homepage CTA and add LinkedIn to footer Replace stale 'How Do You Use Velero?' link (GitHub issue #1327 from 2019) with 'Join the Velero Community' pointing to the community page. Add the new Velero LinkedIn page to the footer social links. Signed-off-by: Shubham Pampattiwar * site: fix invisible CNCF logo in footer The footer uses a white background but the CNCF logo was a white SVG (cncf-white.svg), making it invisible. Switch to the color version from the CNCF artwork repository. Signed-off-by: Shubham Pampattiwar --------- Signed-off-by: Shubham Pampattiwar --- site/config.yaml | 5 +- site/content/_index.md | 6 +-- site/static/img/cncf-color.svg | 88 ++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 site/static/img/cncf-color.svg diff --git a/site/config.yaml b/site/config.yaml index 6eddc1e14..9edc08541 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -8,7 +8,7 @@ frontmatter: params: author: Velero Authors logo: Velero.svg - cncf_logo: cncf-white.svg + cncf_logo: cncf-color.svg hero: backgroundColor: med-blue versioning: true @@ -63,6 +63,9 @@ params: - title: Twitter fa_icon: fab fa-twitter url: https://twitter.com/projectvelero + - title: LinkedIn + fa_icon: fab fa-linkedin + url: https://www.linkedin.com/company/project-velero - title: Slack fa_icon: fab fa-slack url: https://kubernetes.slack.com/messages/velero diff --git a/site/content/_index.md b/site/content/_index.md index 79426ecc8..7d27dc709 100644 --- a/site/content/_index.md +++ b/site/content/_index.md @@ -32,7 +32,7 @@ secondary_ctas: url: /blog/Velero-is-an-Open-Source-Tool-to-Back-up-and-Migrate-Kubernetes-Clusters/ # Velero.io word list : ignore content: Learn about Velero and how to protect your Kubernetes resources and volumes. cta2: - title: How Do You Use Velero? - url: https://github.com/velero-io/velero/issues/1327 - content: See how Velero is helping others and tell the world how you use Velero. + title: Join the Velero Community + url: /community/ + content: Connect with other Velero users on Slack, attend community meetings, and contribute to the project. --- \ No newline at end of file diff --git a/site/static/img/cncf-color.svg b/site/static/img/cncf-color.svg new file mode 100644 index 000000000..6ed428836 --- /dev/null +++ b/site/static/img/cncf-color.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + From 96bf9e2ec18d4d6c5707666914eabddec0644c43 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 30 Jul 2026 12:12:03 -0700 Subject: [PATCH 118/194] site: add blog post for Velero joining CNCF Sandbox Announce Velero's acceptance into the CNCF Sandbox, covering the governance change, project history, current maintainers, and how to get involved. This is the first blog post since v1.11 in 2023. Signed-off-by: Shubham Pampattiwar --- .../2026-07-30-Velero-Joins-CNCF-Sandbox.md | 69 +++++++++++++++ site/static/img/cncf-color.svg | 88 +++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 site/content/posts/2026-07-30-Velero-Joins-CNCF-Sandbox.md create mode 100644 site/static/img/cncf-color.svg diff --git a/site/content/posts/2026-07-30-Velero-Joins-CNCF-Sandbox.md b/site/content/posts/2026-07-30-Velero-Joins-CNCF-Sandbox.md new file mode 100644 index 000000000..cf5d0f61d --- /dev/null +++ b/site/content/posts/2026-07-30-Velero-Joins-CNCF-Sandbox.md @@ -0,0 +1,69 @@ +--- +title: "Velero Joins the CNCF Sandbox" +excerpt: Velero has been accepted into the Cloud Native Computing Foundation as a Sandbox project, bringing Kubernetes-native backup and disaster recovery under vendor-neutral, community-driven governance. +author_name: Shubham Pampattiwar +slug: Velero-Joins-CNCF-Sandbox +categories: ['velero','announcements'] +image: /img/cncf-color.svg +tags: ['Velero Team', 'Shubham Pampattiwar', 'CNCF'] +--- + +![CNCF Logo](/img/cncf-color.svg) + +We are excited to announce that Velero has been accepted into the [Cloud Native Computing Foundation (CNCF)](https://www.cncf.io/) as a Sandbox project. This marks a significant milestone for the project, placing Velero under vendor-neutral, community-driven governance alongside other foundational cloud native tools. + +The CNCF Technical Oversight Committee (TOC) accepted the [Sandbox application](https://github.com/cncf/sandbox/issues/457), and the transition was formally announced at KubeCon + CloudNativeCon Europe 2026 in Amsterdam. + +## What This Means + +Joining the CNCF Sandbox means Velero is now governed by the same open, vendor-neutral principles that guide projects like Kubernetes, Prometheus, and Envoy. In practice, this means: + +- **Vendor-neutral governance**: No single company controls the project roadmap. Decisions are made through consensus-based processes with supermajority voting. +- **Community-driven development**: The project's direction is shaped by its maintainers and contributors, who represent multiple organizations. +- **Long-term sustainability**: CNCF provides a neutral home that ensures the project's continuity regardless of changes in any single company's priorities. + +For existing Velero users, nothing changes in how you use the tool. Velero continues to operate at the Kubernetes API layer, providing backup, restore, disaster recovery, and migration capabilities for your clusters and applications. + +## Our Journey + +Velero's journey began at Heptio, the Kubernetes company founded by Joe Beda and Craig McLuckie, where it was originally known as Ark. After VMware acquired Heptio in 2019, the project continued to grow under VMware's stewardship. Following Broadcom's acquisition of VMware, the decision was made to contribute Velero to the CNCF, ensuring the project's future under community governance. + +Throughout these transitions, one thing has remained constant: a growing and engaged open source community. Today, Velero has over 10,000 GitHub stars, 1,500+ forks, 500M+ Docker Hub pulls, and is used by organizations across industries for Kubernetes data protection. + +We are grateful to Broadcom for contributing Velero to the CNCF and to everyone who has contributed to the project over the years. + +## Current Maintainers + +Velero is maintained by engineers from multiple organizations, reflecting the project's vendor-neutral nature: + +| Maintainer | GitHub | Affiliation | +|---|---|---| +| Daniel Jiang | [@reasonerjt](https://github.com/reasonerjt) | Broadcom | +| Wenkai Yin | [@ywk253100](https://github.com/ywk253100) | Broadcom | +| Xun Jiang | [@blackpiglet](https://github.com/blackpiglet) | Broadcom | +| Yonghui Li | [@Lyndon-Li](https://github.com/Lyndon-Li) | Broadcom | +| Scott Seago | [@sseago](https://github.com/sseago) | Red Hat (OpenShift) | +| Shubham Pampattiwar | [@shubham-pampattiwar](https://github.com/shubham-pampattiwar) | Red Hat (OpenShift) | +| Tiger Kaovilai | [@kaovilai](https://github.com/kaovilai) | Red Hat (OpenShift) | +| Anshul Ahuja | [@anshulahuja98](https://github.com/anshulahuja98) | Microsoft (Azure) | + +## What's Next + +Joining the CNCF Sandbox is the beginning of a new chapter for Velero. Here is what we are focused on: + +- **Growing the community**: We want more contributors, more adopters, and more voices shaping the project's direction. Whether you are a user, operator, or developer, there is a place for you in the Velero community. +- **Strengthening the project**: We are continuing to improve Velero's core capabilities around backup performance, data protection, and ecosystem integration. +- **Path to Incubation**: Our goal is to demonstrate the community health, adoption, and maturity needed to advance to CNCF Incubation status. + +## Get Involved + +We welcome contributions of all kinds -- code, documentation, bug reports, feature requests, and feedback. + +- **Slack**: Join [#velero-users](https://kubernetes.slack.com/messages/velero) and [#velero-dev](https://kubernetes.slack.com/messages/velero-dev) on Kubernetes Slack +- **GitHub**: [github.com/velero-io/velero](https://github.com/velero-io/velero) +- **Community Meetings**: We hold bi-weekly community meetings alternating between US/Europe and US/Asia-friendly time zones. See the [community page](https://velero.io/community/) for details. +- **LinkedIn**: Follow us at [Project Velero](https://www.linkedin.com/company/project-velero) +- **Twitter/X**: [@projectvelero](https://twitter.com/projectvelero) +- **Contributing**: Check out our [contribution guide](https://velero.io/docs/main/start-contributing/) to get started. + +We are excited about this new chapter and look forward to building the future of Kubernetes data protection together with the community. diff --git a/site/static/img/cncf-color.svg b/site/static/img/cncf-color.svg new file mode 100644 index 000000000..6ed428836 --- /dev/null +++ b/site/static/img/cncf-color.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + From 0f86521735cb98ee5f8cd71ffee6cd14a9ac7caa Mon Sep 17 00:00:00 2001 From: chlins Date: Wed, 29 Jul 2026 13:43:52 +0800 Subject: [PATCH 119/194] Verify extracted item paths stay inside the backup directory archive.GetItemFilePath/GetVersionedItemFilePath joined the group resource, namespace and name into a path without checking the result against rootDir. Those components can come from backup contents - the additional items a RestoreItemAction returns are built from annotations on a backed up object - so a component containing ".." resolved to an arbitrary file on the Velero pod, which was then Stat'd, unmarshalled and restored as a Kubernetes object. Both helpers now return an error when the joined path escapes rootDir, and all callers handle it. rootDir is empty when building an entry path inside the backup tarball, so "." is used as the containment base for that relative form. Signed-off-by: chlins --- changelogs/unreleased/10102-chlins | 1 + internal/delete/delete_item_action_handler.go | 5 +- pkg/archive/filesystem.go | 32 ++++++- pkg/archive/filesystem_test.go | 89 +++++++++++++++++-- pkg/backup/item_backupper.go | 22 +++-- pkg/restore/restore.go | 50 ++++++++--- 6 files changed, 168 insertions(+), 31 deletions(-) create mode 100644 changelogs/unreleased/10102-chlins diff --git a/changelogs/unreleased/10102-chlins b/changelogs/unreleased/10102-chlins new file mode 100644 index 000000000..70b4b5c44 --- /dev/null +++ b/changelogs/unreleased/10102-chlins @@ -0,0 +1 @@ +Verify extracted item paths stay inside the backup directory diff --git a/internal/delete/delete_item_action_handler.go b/internal/delete/delete_item_action_handler.go index 2a16044ee..89a638331 100644 --- a/internal/delete/delete_item_action_handler.go +++ b/internal/delete/delete_item_action_handler.go @@ -114,7 +114,10 @@ func InvokeDeleteActions(ctx *Context) error { // Process individual items from the backup for _, item := range items { - itemPath := archive.GetItemFilePath(dir, resource, namespace, item) + itemPath, err := archive.GetItemFilePath(dir, resource, namespace, item) + if err != nil { + return errors.Wrapf(err, "could not build item path: %v", item) + } // obj is the Unstructured item from the backup obj, err := archive.Unmarshal(ctx.Filesystem, itemPath) diff --git a/pkg/archive/filesystem.go b/pkg/archive/filesystem.go index 73b0d1dcf..310ab64dc 100644 --- a/pkg/archive/filesystem.go +++ b/pkg/archive/filesystem.go @@ -19,7 +19,9 @@ package archive import ( "encoding/json" "path/filepath" + "strings" + "github.com/cockroachdb/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -27,13 +29,37 @@ import ( ) // GetItemFilePath returns an item's file path once extracted from a Velero backup archive. -func GetItemFilePath(rootDir, groupResource, namespace, name string) string { +func GetItemFilePath(rootDir, groupResource, namespace, name string) (string, error) { return GetVersionedItemFilePath(rootDir, groupResource, namespace, name, "") } // GetVersionedItemFilePath returns an item's file path once extracted from a Velero backup archive, with version included. -func GetVersionedItemFilePath(rootDir, groupResource, namespace, name, versionPath string) string { - return filepath.Join(rootDir, velerov1api.ResourcesDir, groupResource, versionPath, GetScopeDir(namespace), namespace, name+".json") +// +// The namespace and name components can originate from backup contents - for example the +// additional items a RestoreItemAction returns are built from annotations on a backed up +// object - so the joined path is verified to stay within rootDir. Without that check a +// component containing ".." escapes the extracted backup directory and addresses an +// arbitrary file on the Velero pod's filesystem. +func GetVersionedItemFilePath(rootDir, groupResource, namespace, name, versionPath string) (string, error) { + path := filepath.Join(rootDir, velerov1api.ResourcesDir, groupResource, versionPath, GetScopeDir(namespace), namespace, name+".json") + + // rootDir is empty when building the path of an entry inside the backup tarball rather + // than of an extracted file on disk; "." is the containment base for that relative form. + base := rootDir + if base == "" { + base = "." + } + + rel, err := filepath.Rel(base, path) + if err != nil { + return "", errors.Wrapf(err, "error resolving item path for %q/%q", namespace, name) + } + + if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return "", errors.Errorf("invalid item path for %q/%q: escapes the backup directory", namespace, name) + } + + return path, nil } // GetScopeDir returns NamespaceScopedDir if namespace is present, or ClusterScopedDir if empty diff --git a/pkg/archive/filesystem_test.go b/pkg/archive/filesystem_test.go index bf7f16c76..c6225ff85 100644 --- a/pkg/archive/filesystem_test.go +++ b/pkg/archive/filesystem_test.go @@ -27,31 +27,104 @@ import ( ) func TestGetItemFilePath(t *testing.T) { - res := GetItemFilePath("root", "resource", "", "item") + res, err := GetItemFilePath("root", "resource", "", "item") + require.NoError(t, err) assert.Equal(t, "root/resources/resource/cluster/item.json", res) - res = GetItemFilePath("root", "resource", "namespace", "item") + res, err = GetItemFilePath("root", "resource", "namespace", "item") + require.NoError(t, err) assert.Equal(t, "root/resources/resource/namespaces/namespace/item.json", res) - res = GetItemFilePath("", "resource", "", "item") + res, err = GetItemFilePath("", "resource", "", "item") + require.NoError(t, err) assert.Equal(t, "resources/resource/cluster/item.json", res) - res = GetVersionedItemFilePath("root", "resource", "", "item", "") + res, err = GetVersionedItemFilePath("root", "resource", "", "item", "") + require.NoError(t, err) assert.Equal(t, "root/resources/resource/cluster/item.json", res) - res = GetVersionedItemFilePath("root", "resource", "namespace", "item", "") + res, err = GetVersionedItemFilePath("root", "resource", "namespace", "item", "") + require.NoError(t, err) assert.Equal(t, "root/resources/resource/namespaces/namespace/item.json", res) - res = GetVersionedItemFilePath("root", "resource", "namespace", "item", "v1") + res, err = GetVersionedItemFilePath("root", "resource", "namespace", "item", "v1") + require.NoError(t, err) assert.Equal(t, "root/resources/resource/v1/namespaces/namespace/item.json", res) - res = GetVersionedItemFilePath("root", "resource", "", "item", "v1") + res, err = GetVersionedItemFilePath("root", "resource", "", "item", "v1") + require.NoError(t, err) assert.Equal(t, "root/resources/resource/v1/cluster/item.json", res) - res = GetVersionedItemFilePath("", "resource", "", "item", "") + res, err = GetVersionedItemFilePath("", "resource", "", "item", "") + require.NoError(t, err) assert.Equal(t, "resources/resource/cluster/item.json", res) } +// TestGetItemFilePathRejectsPathTraversal verifies that a name or namespace containing +// ".." cannot address a file outside the extracted backup directory. These components can +// come from backup contents, for example the additional items a RestoreItemAction builds +// from annotations on a backed up object. +func TestGetItemFilePathRejectsPathTraversal(t *testing.T) { + tests := []struct { + name string + rootDir string + groupResource string + namespace string + itemName string + }{ + { + name: "traversal in name escapes root", + rootDir: "/tmp/restore-dir", + groupResource: "secrets", + namespace: "x", + itemName: "../../../../../../root/.docker/config", + }, + { + name: "traversal in namespace escapes root", + rootDir: "/tmp/restore-dir", + groupResource: "secrets", + namespace: "../../../../../../etc", + itemName: "passwd", + }, + { + name: "traversal in group resource escapes root", + rootDir: "/tmp/restore-dir", + groupResource: "../../../../../../etc", + namespace: "", + itemName: "passwd", + }, + { + name: "traversal escapes archive-relative root", + rootDir: "", + groupResource: "secrets", + namespace: "x", + itemName: "../../../../../../escape", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res, err := GetItemFilePath(tc.rootDir, tc.groupResource, tc.namespace, tc.itemName) + require.Error(t, err) + assert.Contains(t, err.Error(), "escapes the backup directory") + assert.Empty(t, res) + + res, err = GetVersionedItemFilePath(tc.rootDir, tc.groupResource, tc.namespace, tc.itemName, "v1") + require.Error(t, err) + assert.Contains(t, err.Error(), "escapes the backup directory") + assert.Empty(t, res) + }) + } +} + +// TestGetItemFilePathAllowsInnerDotDot verifies the containment check does not reject a +// path whose ".." segments resolve back inside the root directory. +func TestGetItemFilePathAllowsInnerDotDot(t *testing.T) { + res, err := GetItemFilePath("root", "resource", "namespaces/..", "item") + require.NoError(t, err) + assert.Equal(t, "root/resources/resource/namespaces/item.json", res) +} + func TestGetScopeDir(t *testing.T) { res := GetScopeDir("") assert.Equal(t, velerov1api.ClusterScopedDir, res) diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index f43888252..c180092a5 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -351,16 +351,28 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti if versionPath == preferredGVR.Version { // backing up preferred version backup without API Group version - for backward compatibility log.Debugf("Resource %s/%s, version= %s, preferredVersion=%s", groupResource.String(), name, versionPath, preferredGVR.Version) - itemFiles = append(itemFiles, getFileForArchive(namespace, name, groupResource.String(), "", itemBytes)) + fileForArchive, err := getFileForArchive(namespace, name, groupResource.String(), "", itemBytes) + if err != nil { + return false, itemFiles, err + } + itemFiles = append(itemFiles, fileForArchive) versionPath = versionPath + velerov1api.PreferredVersionDir } - itemFiles = append(itemFiles, getFileForArchive(namespace, name, groupResource.String(), versionPath, itemBytes)) + fileForArchive, err := getFileForArchive(namespace, name, groupResource.String(), versionPath, itemBytes) + if err != nil { + return false, itemFiles, err + } + itemFiles = append(itemFiles, fileForArchive) return true, itemFiles, nil } -func getFileForArchive(namespace, name, groupResource, versionPath string, itemBytes []byte) FileForArchive { - filePath := archive.GetVersionedItemFilePath("", groupResource, namespace, name, versionPath) +func getFileForArchive(namespace, name, groupResource, versionPath string, itemBytes []byte) (FileForArchive, error) { + filePath, err := archive.GetVersionedItemFilePath("", groupResource, namespace, name, versionPath) + if err != nil { + return FileForArchive{}, err + } + hdr := &tar.Header{ Name: filePath, Size: int64(len(itemBytes)), @@ -368,7 +380,7 @@ func getFileForArchive(namespace, name, groupResource, versionPath string, itemB Mode: 0755, ModTime: time.Now(), } - return FileForArchive{FilePath: filePath, Header: hdr, FileBytes: itemBytes} + return FileForArchive{FilePath: filePath, Header: hdr, FileBytes: itemBytes}, nil } // backupPodVolumes triggers pod volume backups of the specified pod volumes, and returns a list of PodVolumeBackups diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index e7a284fb1..336add4de 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -1011,11 +1011,13 @@ func (ctx *restoreContext) processSelectedResource( if namespace != "" && !existingNamespaces.Has(targetNS) { logger := ctx.log.WithField("namespace", namespace) - ns := getNamespace( - logger, - archive.GetItemFilePath(ctx.restoreDir, "namespaces", "", namespace), - targetNS, - ) + nsPath, err := archive.GetItemFilePath(ctx.restoreDir, "namespaces", "", namespace) + if err != nil { + errs.AddVeleroError(err) + continue + } + + ns := getNamespace(logger, nsPath, targetNS) _, nsCreated, err := kube.EnsureNamespaceExistsAndIsReady( ns, ctx.namespaceClient, @@ -1440,7 +1442,13 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso // If the namespace scoped resource should be restored, ensure that the // namespace into which the resource is being restored into exists. // This is the *remapped* namespace that we are ensuring exists. - nsToEnsure := getNamespace(restoreLogger, archive.GetItemFilePath(ctx.restoreDir, "namespaces", "", obj.GetNamespace()), namespace) + nsPath, err := archive.GetItemFilePath(ctx.restoreDir, "namespaces", "", obj.GetNamespace()) + if err != nil { + errs.AddVeleroError(err) + return warnings, errs, itemExists + } + + nsToEnsure := getNamespace(restoreLogger, nsPath, namespace) _, nsCreated, err := kube.EnsureNamespaceExistsAndIsReady(nsToEnsure, ctx.namespaceClient, ctx.resourceTerminatingTimeout, ctx.resourceDeletionStatusTracker) if err != nil { errs.AddVeleroError(err) @@ -1693,7 +1701,17 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso var filteredAdditionalItems []velero.ResourceIdentifier for _, additionalItem := range executeOutput.AdditionalItems { - itemPath := archive.GetItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name) + itemPath, err := archive.GetItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name) + if err != nil { + restoreLogger.WithError(err).WithFields(logrus.Fields{ + "additionalResource": additionalItem.GroupResource.String(), + "additionalResourceNamespace": additionalItem.Namespace, + "additionalResourceName": additionalItem.Name, + }).Warn("unable to restore additional item") + warnings.Add(additionalItem.Namespace, err) + + continue + } if _, err := ctx.fileSystem.Stat(itemPath); err != nil { restoreLogger.WithError(err).WithFields(logrus.Fields{ @@ -2671,9 +2689,9 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original // Peek-and-map logic for unresolvable kinds if rf == nil && len(items) > 0 { - peekPath := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, items[0]) - // Ignore unmarshal errors during peek; the main restore loop will catch and report them - if obj, err := archive.Unmarshal(ctx.fileSystem, peekPath); err == nil { + peekPath, pathErr := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, items[0]) + // Ignore path and unmarshal errors during peek; the main restore loop will catch and report them + if obj, err := archive.Unmarshal(ctx.fileSystem, peekPath); pathErr == nil && err == nil { actualKind := obj.GroupVersionKind().Kind for _, filter := range nsFilter.resourceFilterMap { for _, k := range filter.originalKinds { @@ -2714,9 +2732,9 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original // Note: Unlike the namespaced path, this fallback is always reachable // because the main restore loop does not have a fast-path skip for // unlisted cluster-scoped resources. - peekPath := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, items[0]) - // Ignore unmarshal errors during peek; the main restore loop will catch and report them - if obj, err := archive.Unmarshal(ctx.fileSystem, peekPath); err == nil { + peekPath, pathErr := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, items[0]) + // Ignore path and unmarshal errors during peek; the main restore loop will catch and report them + if obj, err := archive.Unmarshal(ctx.fileSystem, peekPath); pathErr == nil && err == nil { actualKind := obj.GroupVersionKind().Kind for _, filter := range ctx.clusterScopedFilterMap { for _, k := range filter.originalKinds { @@ -2742,7 +2760,11 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original } for _, item := range items { - itemPath := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, item) + itemPath, err := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, item) + if err != nil { + errs.Add(targetNamespace, err) + continue + } obj, err := archive.Unmarshal(ctx.fileSystem, itemPath) if err != nil { From 4220c7abe8ac9c6e424811ef3b38a7ee14b0986d Mon Sep 17 00:00:00 2001 From: chlins Date: Thu, 30 Jul 2026 16:37:18 +0800 Subject: [PATCH 120/194] 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 f0797c91048d6d8c149fe407e6d7a9b53b55a461 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 31 Jul 2026 16:20:21 +0800 Subject: [PATCH 121/194] Use "" as parentSnapshot for DU when BackupType is incremental. Signed-off-by: Xun Jiang --- changelogs/unreleased/10126-blackpiglet | 1 + pkg/backup/actions/csi/pvc_action.go | 10 +++------- pkg/backup/actions/csi/pvc_action_test.go | 7 +++---- 3 files changed, 7 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/10126-blackpiglet diff --git a/changelogs/unreleased/10126-blackpiglet b/changelogs/unreleased/10126-blackpiglet new file mode 100644 index 000000000..451b1c2a0 --- /dev/null +++ b/changelogs/unreleased/10126-blackpiglet @@ -0,0 +1 @@ +Use "" as parentSnapshot for DU when BackupType is incremental. \ No newline at end of file diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index 259ec5783..69676da39 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -536,14 +536,10 @@ func newDataUpload( vsc *snapshotv1api.VolumeSnapshotContent, fsType string, ) *velerov2alpha1.DataUpload { - var parentSnapshot string - switch backup.Spec.BackupType { - case velerov1api.BackupTypeFull: + parentSnapshot := "" + + if backup.Spec.BackupType == velerov1api.BackupTypeFull { parentSnapshot = veleroshared.DataUploadParentSnapshotNone - case velerov1api.BackupTypeIncremental: - parentSnapshot = veleroshared.DataUploadParentSnapshotAuto - default: - parentSnapshot = veleroshared.DataUploadParentSnapshotAuto } dataUpload := &velerov2alpha1.DataUpload{ diff --git a/pkg/backup/actions/csi/pvc_action_test.go b/pkg/backup/actions/csi/pvc_action_test.go index 9c8405efc..b454cae9d 100644 --- a/pkg/backup/actions/csi/pvc_action_test.go +++ b/pkg/backup/actions/csi/pvc_action_test.go @@ -45,7 +45,6 @@ import ( crclient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" - veleroshared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/builder" @@ -162,7 +161,7 @@ func TestExecute(t *testing.T) { SourcePVC: "testPVC", SourceNamespace: "velero", OperationTimeout: metav1.Duration{Duration: 1 * time.Minute}, - ParentSnapshot: veleroshared.DataUploadParentSnapshotAuto, + ParentSnapshot: "", }, }, }, @@ -2199,7 +2198,7 @@ func TestNewDataUpload(t *testing.T) { backupType: velerov1api.BackupTypeIncremental, vsClassName: ptr.To("test-vs-class"), uploaderConfig: &velerov1api.UploaderConfigForBackup{ParallelFilesUpload: 10}, - expectedParentSnap: "auto", + expectedParentSnap: "", expectedDataMoverCfg: map[string]string{ uploaderUtil.ParallelFilesUpload: "10", }, @@ -2209,7 +2208,7 @@ func TestNewDataUpload(t *testing.T) { backupType: "", vsClassName: ptr.To("test-vs-class"), uploaderConfig: &velerov1api.UploaderConfigForBackup{ParallelFilesUpload: 0}, - expectedParentSnap: "auto", + expectedParentSnap: "", expectedDataMoverCfg: nil, }, } From c9f784ef66d0401ae6d27040bf97cf1e8024abc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:12:21 +0000 Subject: [PATCH 122/194] Bump actions/checkout from 6 to 7 Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/e2e-test-kind.yaml | 4 ++-- .github/workflows/get-go-version.yaml | 2 +- .github/workflows/nightly-trivy-scan.yml | 2 +- .github/workflows/pr-changelog-check.yml | 2 +- .github/workflows/pr-ci-check.yml | 2 +- .github/workflows/pr-codespell.yml | 2 +- .github/workflows/pr-containers.yml | 2 +- .github/workflows/pr-filepath-check.yml | 2 +- .github/workflows/pr-goreleaser.yml | 2 +- .github/workflows/pr-linter-check.yml | 2 +- .github/workflows/push-builder.yml | 2 +- .github/workflows/push.yml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index 42dcaa707..00bc9e10b 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -23,7 +23,7 @@ jobs: minio-dockerfile-sha: ${{ steps.minio-version.outputs.dockerfile_sha }} steps: - name: Check out the code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go version uses: actions/setup-go@v6 @@ -136,7 +136,7 @@ jobs: fail-fast: false steps: - name: Check out the code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go version uses: actions/setup-go@v6 diff --git a/.github/workflows/get-go-version.yaml b/.github/workflows/get-go-version.yaml index 7a74fd845..fa4fb5e00 100644 --- a/.github/workflows/get-go-version.yaml +++ b/.github/workflows/get-go-version.yaml @@ -17,7 +17,7 @@ jobs: version: ${{ steps.pick-version.outputs.version }} steps: - name: Check out the code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - id: pick-version run: | diff --git a/.github/workflows/nightly-trivy-scan.yml b/.github/workflows/nightly-trivy-scan.yml index be0aa4dcf..cff2a29b5 100644 --- a/.github/workflows/nightly-trivy-scan.yml +++ b/.github/workflows/nightly-trivy-scan.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 diff --git a/.github/workflows/pr-changelog-check.yml b/.github/workflows/pr-changelog-check.yml index f9fb14f37..67c1a6221 100644 --- a/.github/workflows/pr-changelog-check.yml +++ b/.github/workflows/pr-changelog-check.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Changelog check if: ${{ !(contains(github.event.pull_request.labels.*.name, 'kind/changelog-not-required') || contains(github.event.pull_request.labels.*.name, 'Design') || contains(github.event.pull_request.labels.*.name, 'Website') || contains(github.event.pull_request.labels.*.name, 'Documentation'))}} diff --git a/.github/workflows/pr-ci-check.yml b/.github/workflows/pr-ci-check.yml index b189a622a..01e86dc08 100644 --- a/.github/workflows/pr-ci-check.yml +++ b/.github/workflows/pr-ci-check.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false steps: - name: Check out the code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go version uses: actions/setup-go@v6 diff --git a/.github/workflows/pr-codespell.yml b/.github/workflows/pr-codespell.yml index b65ae7ae5..a2d22dd73 100644 --- a/.github/workflows/pr-codespell.yml +++ b/.github/workflows/pr-codespell.yml @@ -9,7 +9,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Codespell uses: codespell-project/actions-codespell@master diff --git a/.github/workflows/pr-containers.yml b/.github/workflows/pr-containers.yml index 910192171..ac5bced23 100644 --- a/.github/workflows/pr-containers.yml +++ b/.github/workflows/pr-containers.yml @@ -14,7 +14,7 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 name: Checkout - name: Set up QEMU diff --git a/.github/workflows/pr-filepath-check.yml b/.github/workflows/pr-filepath-check.yml index 9b8ca593d..5ec2cb03b 100644 --- a/.github/workflows/pr-filepath-check.yml +++ b/.github/workflows/pr-filepath-check.yml @@ -9,7 +9,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Validate file paths for Go module compatibility run: | diff --git a/.github/workflows/pr-goreleaser.yml b/.github/workflows/pr-goreleaser.yml index 802080cb5..0cbec3329 100644 --- a/.github/workflows/pr-goreleaser.yml +++ b/.github/workflows/pr-goreleaser.yml @@ -14,7 +14,7 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 name: Checkout - name: Verify .goreleaser.yml and try a dryrun release. diff --git a/.github/workflows/pr-linter-check.yml b/.github/workflows/pr-linter-check.yml index 6ed7f073d..6f8057be6 100644 --- a/.github/workflows/pr-linter-check.yml +++ b/.github/workflows/pr-linter-check.yml @@ -18,7 +18,7 @@ jobs: needs: get-go-version steps: - name: Check out the code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go version uses: actions/setup-go@v6 diff --git a/.github/workflows/push-builder.yml b/.github/workflows/push-builder.yml index 8e3e59c15..164d9104a 100644 --- a/.github/workflows/push-builder.yml +++ b/.github/workflows/push-builder.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: # The default value is "1" which fetches only a single commit. If we merge PR without squash or rebase, # there are at least two commits: the first one is the merge commit and the second one is the real commit diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index b010aa76d..d4e5c6575 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -23,7 +23,7 @@ jobs: needs: get-go-version steps: - name: Check out the code - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Go version uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 From 2685a5dd1d01975898d5537954ecab542f43e6f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:12:38 +0000 Subject: [PATCH 123/194] Bump docker/setup-buildx-action from 3 to 4 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pr-containers.yml | 2 +- .github/workflows/push.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-containers.yml b/.github/workflows/pr-containers.yml index 910192171..073f8e04c 100644 --- a/.github/workflows/pr-containers.yml +++ b/.github/workflows/pr-containers.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 with: version: latest diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index b010aa76d..1e989a6e2 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -37,7 +37,7 @@ jobs: platforms: all - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 with: version: latest - name: Build From 8ec4b224968693eba686cffb305781a802051f34 Mon Sep 17 00:00:00 2001 From: Jay2006sawant Date: Mon, 3 Aug 2026 09:44:00 +0530 Subject: [PATCH 124/194] fix: return errors correctly in block restore validation and BatchForget Signed-off-by: Jay2006sawant --- .../fix-error-handling-Jay2006sawant | 9 ++++ pkg/repository/provider/unified_repo.go | 2 +- pkg/repository/provider/unified_repo_test.go | 35 ++++++++++++++ pkg/uploader/block/uploader.go | 6 +-- pkg/uploader/block/uploader_test.go | 48 +++++++++++++++++++ 5 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/fix-error-handling-Jay2006sawant diff --git a/changelogs/unreleased/fix-error-handling-Jay2006sawant b/changelogs/unreleased/fix-error-handling-Jay2006sawant new file mode 100644 index 000000000..82a05f6c3 --- /dev/null +++ b/changelogs/unreleased/fix-error-handling-Jay2006sawant @@ -0,0 +1,9 @@ +fix: return errors correctly in block restore validation and BatchForget + +Block uploader Restore used errors.Wrapf with a stale nil err after +successful getSourceSize, causing size validation failures to return +(0, nil). flushZeroBlocks had the same pattern on short writes. + +BatchForget dropped delete errors when flush also failed. + +Signed-off-by: Jay2006sawant diff --git a/pkg/repository/provider/unified_repo.go b/pkg/repository/provider/unified_repo.go index bfe1a2bd9..664750c77 100644 --- a/pkg/repository/provider/unified_repo.go +++ b/pkg/repository/provider/unified_repo.go @@ -384,7 +384,7 @@ func (urp *unifiedRepoProvider) BatchForget(ctx context.Context, snapshotIDs []s err = bkRepo.Flush(ctx) if err != nil { - return []error{errors.Wrap(err, "error to flush repo")} + errs = append(errs, errors.Wrap(err, "error to flush repo")) } log.Debug("Forget snapshot complete") diff --git a/pkg/repository/provider/unified_repo_test.go b/pkg/repository/provider/unified_repo_test.go index e0e0a8b8f..2cd9bf576 100644 --- a/pkg/repository/provider/unified_repo_test.go +++ b/pkg/repository/provider/unified_repo_test.go @@ -1062,6 +1062,41 @@ func TestBatchForget(t *testing.T) { }, expectedErr: []string{"error to flush repo: fake-error-4"}, }, + { + name: "delete and flush fail", + getter: new(credmock.SecretStore), + credStoreReturn: "fake-password", + funcTable: localFuncTable{ + getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) { + return map[string]string{}, nil + }, + getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) { + return map[string]string{}, nil + }, + }, + repoService: new(reposervicenmocks.BackupRepoService), + backupRepo: new(reposervicenmocks.BackupRepo), + retFuncOpen: []any{ + func(context.Context, udmrepo.RepoOptions) udmrepo.BackupRepo { + return backupRepo + }, + + func(context.Context, udmrepo.RepoOptions) error { + return nil + }, + }, + retFuncDelete: func(context.Context, udmrepo.ID) error { + return errors.New("fake-delete-error") + }, + retFuncFlush: func(context.Context) error { + return errors.New("fake-flush-error") + }, + snapshots: []string{"snapshot-1"}, + expectedErr: []string{ + "error to delete manifest snapshot-1: fake-delete-error", + "error to flush repo: fake-flush-error", + }, + }, } for _, tc := range testCases { diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 1d74bd462..824c0ae9f 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -169,11 +169,11 @@ func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bi } if sourceSize > meta.SubObjects[0].Size { - return 0, errors.Wrapf(err, "unexpected size (%v vs. %v) for bdev object %s", meta.SubObjects[0].Size, sourceSize, meta.SubObjects[0].Name) + return 0, errors.Errorf("unexpected size (%v vs. %v) for bdev object %s", meta.SubObjects[0].Size, sourceSize, meta.SubObjects[0].Name) } if sourceSize > dest.size { - return 0, errors.Wrapf(err, "dest dev(%s) size is too small (%v vs. %v)", dest.path, dest.size, sourceSize) + return 0, errors.Errorf("dest dev(%s) size is too small (%v vs. %v)", dest.path, dest.size, sourceSize) } reader, err := blkup.repoWriter.OpenObject(blkup.ctx, meta.SubObjects[0].ID) @@ -616,7 +616,7 @@ func flushZeroBlocks(dest *os.File, start int64, length int64, zeroBlock []byte, } if writeSize != n { - return errors.Wrapf(err, "short write zero buffer at %v, length %v", start+written, writeSize) + return errors.Errorf("short write zero buffer at %v, length %v", start+written, writeSize) } written += int64(writeSize) diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index 1765eb045..3b8930476 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -689,4 +689,52 @@ func TestBlockUploaderRestore(t *testing.T) { require.NoError(t, err) assert.Equal(t, int64(1048576), written) }) + + t.Run("source size tag larger than object size", func(t *testing.T) { + ctx := context.Background() + repoWriter := udmrepomocks.NewBackupRepo(t) + blkup := NewUploader(ctx, repoWriter, nil, logrus.New()) + + meta := &udmrepo.Metadata{ + SubObjects: []udmrepo.ObjectMetadata{ + {ID: "data-id", Name: "bdev", Size: 1048576}, + }, + } + repoWriter.On("ReadMetadata", mock.Anything, udmrepo.ID("root-id")).Return(meta, nil) + + snap := udmrepo.Snapshot{ + RootObject: udmrepo.ObjectMetadata{ID: "root-id"}, + Tags: map[string]string{bdevSourceSizeTag: "2097152"}, + } + dest := destInfo{size: 4194304, path: "/dev/target"} + iterMock := cbtmocks.NewIterator(t) + + _, err := blkup.Restore(snap, dest, iterMock, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "unexpected size (1048576 vs. 2097152) for bdev object bdev") + }) + + t.Run("destination smaller than source size", func(t *testing.T) { + ctx := context.Background() + repoWriter := udmrepomocks.NewBackupRepo(t) + blkup := NewUploader(ctx, repoWriter, nil, logrus.New()) + + meta := &udmrepo.Metadata{ + SubObjects: []udmrepo.ObjectMetadata{ + {ID: "data-id", Name: "bdev", Size: 1048576}, + }, + } + repoWriter.On("ReadMetadata", mock.Anything, udmrepo.ID("root-id")).Return(meta, nil) + + snap := udmrepo.Snapshot{ + RootObject: udmrepo.ObjectMetadata{ID: "root-id"}, + Tags: map[string]string{bdevSourceSizeTag: "1048576"}, + } + dest := destInfo{size: 512, path: "/dev/small"} + iterMock := cbtmocks.NewIterator(t) + + _, err := blkup.Restore(snap, dest, iterMock, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "dest dev(/dev/small) size is too small") + }) } From 8fe02224b9df5a43043894106f041f0ee3d5fea7 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 3 Aug 2026 13:25:18 +0800 Subject: [PATCH 125/194] set CBT service to uploader Signed-off-by: Lyndon-Li --- pkg/cmd/cli/datamover/backup.go | 1 + pkg/datamover/backup_micro_service.go | 6 +++++- pkg/datapath/data_path.go | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/cli/datamover/backup.go b/pkg/cmd/cli/datamover/backup.go index 07ac7dc18..aa0b2bcfb 100644 --- a/pkg/cmd/cli/datamover/backup.go +++ b/pkg/cmd/cli/datamover/backup.go @@ -331,6 +331,7 @@ func (s *dataMoverBackup) createDataPathService() (dataPathService, error) { s.config.changeID, s.config.volumeID, s.config.snapshotID, + s.cbtService, s.logger, ), nil } diff --git a/pkg/datamover/backup_micro_service.go b/pkg/datamover/backup_micro_service.go index cb5aeb3fe..53409b461 100644 --- a/pkg/datamover/backup_micro_service.go +++ b/pkg/datamover/backup_micro_service.go @@ -34,6 +34,7 @@ import ( "github.com/vmware-tanzu/velero/internal/credentials" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" + "github.com/vmware-tanzu/velero/pkg/cbtservice" "github.com/vmware-tanzu/velero/pkg/datapath" "github.com/vmware-tanzu/velero/pkg/repository" "github.com/vmware-tanzu/velero/pkg/uploader" @@ -71,6 +72,7 @@ type BackupMicroService struct { changeID string volumeID string snapshotID string + cbtService cbtservice.Service } type dataPathResult struct { @@ -80,7 +82,7 @@ type dataPathResult struct { func NewBackupMicroService(ctx context.Context, client client.Client, kubeClient kubernetes.Interface, dataUploadName string, namespace string, nodeName string, sourceTargetPath datapath.AccessPoint, dataPathMgr *datapath.Manager, repoEnsurer *repository.Ensurer, cred *credentials.CredentialGetter, - duInformer cache.Informer, changeID string, volumeID string, snapshotID string, log logrus.FieldLogger) *BackupMicroService { + duInformer cache.Informer, changeID string, volumeID string, snapshotID string, cbtService cbtservice.Service, log logrus.FieldLogger) *BackupMicroService { return &BackupMicroService{ ctx: ctx, client: client, @@ -98,6 +100,7 @@ func NewBackupMicroService(ctx context.Context, client client.Client, kubeClient changeID: changeID, volumeID: volumeID, snapshotID: snapshotID, + cbtService: cbtService, } } @@ -210,6 +213,7 @@ func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string, VolumeID: r.volumeID, ChangeID: r.changeID, SnapshotID: r.snapshotID, + CBTService: r.cbtService, }); err != nil { return "", errors.Wrap(err, "error starting data path backup") } diff --git a/pkg/datapath/data_path.go b/pkg/datapath/data_path.go index 6e36ce6af..2ec750805 100644 --- a/pkg/datapath/data_path.go +++ b/pkg/datapath/data_path.go @@ -57,6 +57,7 @@ type BackupStartParam struct { VolumeID string ChangeID string SnapshotID string + CBTService cbtservice.Service } // RestoreStartParam define the input param for restore start @@ -203,6 +204,7 @@ func (dp *generalDataPath) StartBackup(source AccessPoint, uploaderConfig map[st VolumeID: backupParam.VolumeID, ChangeID: backupParam.ChangeID, }, + Service: backupParam.CBTService, }, source.VolMode, uploaderConfig, From 2649b2554c05ba4dc35d8a9facf5943b7d1e45e3 Mon Sep 17 00:00:00 2001 From: chlins Date: Mon, 3 Aug 2026 13:28:31 +0800 Subject: [PATCH 126/194] 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 02fe822860ed02ddff044dafd9d1a61a8bb1b85f Mon Sep 17 00:00:00 2001 From: chlins Date: Mon, 3 Aug 2026 13:46:18 +0800 Subject: [PATCH 127/194] Pin e2e third-party clones to reviewed commits Pin bitnami/containers and distributed-data-generator to fixed SHAs instead of building default-branch HEAD, and add a minimal permissions block. Signed-off-by: chlins --- .github/workflows/e2e-test-kind.yaml | 56 +++++++++++----------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index 42dcaa707..f3fe8d8fd 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -1,6 +1,14 @@ name: "Run the E2E test on kind" +permissions: + contents: read env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + # Reviewed commit pins for third-party sources this workflow clones and executes. + # Bump them deliberately after reviewing the upstream changes. + # bitnami/containers: [bitnami/minio] Release 2026.7.17-debian-12-r0 + BITNAMI_CONTAINERS_COMMIT: 19fb570e551f15ab0c8264aafa93774266761b8d + # vmware-tanzu-experiments/distributed-data-generator: main as of 2025-07-15 + KIBISHII_COMMIT: bce0469e5f9dd33f31432fab22ff90ad6f2b45ca on: push: pull_request: @@ -19,8 +27,6 @@ jobs: build: runs-on: ubuntu-latest needs: get-go-version - outputs: - minio-dockerfile-sha: ${{ steps.minio-version.outputs.dockerfile_sha }} steps: - name: Check out the code uses: actions/checkout@v6 @@ -56,45 +62,22 @@ jobs: run: | IMAGE=velero VERSION=pr-test BUILD_OUTPUT_TYPE=docker make container docker save velero:pr-test-linux-amd64 -o ./velero.tar - # Check and build MinIO image once for all e2e tests - - name: Check Bitnami MinIO Dockerfile version - id: minio-version - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - url="https://api.github.com/repos/bitnami/containers/commits?path=bitnami/minio/2026/debian-12/Dockerfile&per_page=1" - - response="$(curl --fail-with-body -sS \ - --retry 5 \ - --retry-delay 2 \ - --retry-all-errors \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${GH_TOKEN}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "$url")" - - DOCKERFILE_SHA="$(echo "$response" | jq -r '.[0].sha // empty')" - - if [ -z "$DOCKERFILE_SHA" ]; then - echo "Failed to resolve Bitnami MinIO Dockerfile SHA from GitHub API response" - echo "$response" - exit 1 - fi - - echo "dockerfile_sha=${DOCKERFILE_SHA}" >> "$GITHUB_OUTPUT" + # Build the MinIO image once for all e2e tests, from the reviewed bitnami/containers commit. - name: Cache MinIO Image uses: actions/cache@v4 id: minio-cache with: path: ./minio-image.tar - key: minio-bitnami-${{ steps.minio-version.outputs.dockerfile_sha }} + key: minio-bitnami-${{ env.BITNAMI_CONTAINERS_COMMIT }} - name: Build MinIO Image from Bitnami Dockerfile if: steps.minio-cache.outputs.cache-hit != 'true' run: | - echo "Building MinIO image from Bitnami Dockerfile..." - git clone --depth 1 https://github.com/bitnami/containers.git /tmp/bitnami-containers + set -euo pipefail + echo "Building MinIO image from Bitnami Dockerfile at ${BITNAMI_CONTAINERS_COMMIT}..." + git init -q /tmp/bitnami-containers + git -C /tmp/bitnami-containers remote add origin https://github.com/bitnami/containers.git + git -C /tmp/bitnami-containers fetch --depth 1 origin "${BITNAMI_CONTAINERS_COMMIT}" + git -C /tmp/bitnami-containers checkout -q "${BITNAMI_CONTAINERS_COMMIT}" cd /tmp/bitnami-containers/bitnami/minio/2026/debian-12 docker build -t bitnami/minio:local . docker save bitnami/minio:local > ${{ github.workspace }}/minio-image.tar @@ -149,7 +132,7 @@ jobs: id: minio-cache with: path: ./minio-image.tar - key: minio-bitnami-${{ needs.build.outputs.minio-dockerfile-sha }} + key: minio-bitnami-${{ env.BITNAMI_CONTAINERS_COMMIT }} - name: Load MinIO Image run: | echo "Loading MinIO image..." @@ -189,7 +172,10 @@ jobs: curl -LO https://dl.k8s.io/release/v${{ matrix.k8s }}/bin/linux/amd64/kubectl sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl - git clone https://github.com/vmware-tanzu-experiments/distributed-data-generator.git -b main /tmp/kibishii + git init -q /tmp/kibishii + git -C /tmp/kibishii remote add origin https://github.com/vmware-tanzu-experiments/distributed-data-generator.git + git -C /tmp/kibishii fetch --depth 1 origin "${KIBISHII_COMMIT}" + git -C /tmp/kibishii checkout -q "${KIBISHII_COMMIT}" GOPATH=~/go \ CLOUD_PROVIDER=kind \ From 036e9944e4a9bdc963b7bb8805c46f4ca97a834f Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 3 Aug 2026 13:52:20 +0800 Subject: [PATCH 128/194] empty CBT secret ns when secret is not set Signed-off-by: Lyndon-Li --- pkg/cbtservice/csi_service_impl.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/cbtservice/csi_service_impl.go b/pkg/cbtservice/csi_service_impl.go index 4d0ea3fca..f4ea23de7 100644 --- a/pkg/cbtservice/csi_service_impl.go +++ b/pkg/cbtservice/csi_service_impl.go @@ -86,6 +86,12 @@ func (s *ServiceImpl) GetAllocatedBlocks(ctx context.Context, snapshot string, r return err } + saNamespace := "" + if s.SAName != "" { + // The SA is created in the same namespace as Velero server. vsNamespace is the namespace of Velero server. + saNamespace = s.vsNamespace + } + args := iterator.Args{ SnapshotName: snapshot, Emitter: &emitterImpl{ @@ -95,7 +101,7 @@ func (s *ServiceImpl) GetAllocatedBlocks(ctx context.Context, snapshot string, r Clients: clients, Namespace: s.vsNamespace, // DataUpload is created in the same namespace as Velero server. vsNamespace is the namespace of the Velero server. - SANamespace: s.vsNamespace, // The SA is created in the same namespace as Velero server. vsNamespace is the namespace of Velero server. + SANamespace: saNamespace, SAName: s.SAName, TokenExpirySecs: iterator.DefaultTokenExpirySeconds, MaxResults: 0, // If 0 then the CSI driver decides the value. From 46f5adb7a37f9096835c65b27b1814dc67422f7b Mon Sep 17 00:00:00 2001 From: Jay2006sawant Date: Mon, 3 Aug 2026 11:26:00 +0530 Subject: [PATCH 129/194] fix(provider): return immediately when BatchForget flush fails Signed-off-by: Jay2006sawant --- pkg/repository/provider/unified_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/repository/provider/unified_repo.go b/pkg/repository/provider/unified_repo.go index 664750c77..b30e4618b 100644 --- a/pkg/repository/provider/unified_repo.go +++ b/pkg/repository/provider/unified_repo.go @@ -384,7 +384,7 @@ func (urp *unifiedRepoProvider) BatchForget(ctx context.Context, snapshotIDs []s err = bkRepo.Flush(ctx) if err != nil { - errs = append(errs, errors.Wrap(err, "error to flush repo")) + return append(errs, errors.Wrap(err, "error to flush repo")) } log.Debug("Forget snapshot complete") From f22b7c86d78adb383cc309ab11efefad53dbc0ed Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 3 Aug 2026 14:11:37 +0800 Subject: [PATCH 130/194] upload progress every 10s Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 2a1446b83..1d74bd462 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" @@ -62,10 +63,11 @@ type Uploader interface { } type blockUploader struct { - ctx context.Context - repoWriter udmrepo.BackupRepo - progress uploader.ProgressUpdater - log logrus.FieldLogger + ctx context.Context + repoWriter udmrepo.BackupRepo + progress uploader.ProgressUpdater + log logrus.FieldLogger + lastProgressUpdate time.Time } func NewUploader(ctx context.Context, repoWriter udmrepo.BackupRepo, progress uploader.ProgressUpdater, log logrus.FieldLogger) Uploader { @@ -198,6 +200,17 @@ func (blkup *blockUploader) backupObject(dev *os.File, dest udmrepo.ObjectWriter return id, backupSize, objectSize, err } +func (blkup *blockUploader) UpdateProgress(p *uploader.Progress) { + if blkup.progress == nil { + return + } + + if time.Since(blkup.lastProgressUpdate) >= 10*time.Second || p.BytesDone == p.TotalBytes { + blkup.progress.UpdateProgress(p) + blkup.lastProgressUpdate = time.Now() + } +} + type readResult struct { buffer []byte offset int64 @@ -233,7 +246,7 @@ func (blkup *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.Object go func() { defer wg.Done() defer close(quit) - written, lastPos, writeErr = backupWriteProc(blkup.ctx, writer, resultChan, list, aligned, totalCount, int(blockSize), blkup.progress) + written, lastPos, writeErr = backupWriteProc(blkup.ctx, writer, resultChan, list, aligned, totalCount, int(blockSize), blkup) }() wg.Wait() @@ -250,7 +263,7 @@ func (blkup *blockUploader) backupData(reader io.ReaderAt, writer udmrepo.Object written += s - blkup.progress.UpdateProgress(&uploader.Progress{BytesDone: aligned, TotalBytes: aligned}) + blkup.UpdateProgress(&uploader.Progress{BytesDone: aligned, TotalBytes: aligned}) } return written, aligned, nil @@ -419,7 +432,7 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit go func() { defer wg.Done() defer close(quit) - written, writeErr = restoreWriteProc(blkup.ctx, dest, resultChan, list, totalLength, totalCount, int(blockSize), destPath, blkup.progress, blkup.log) + written, writeErr = restoreWriteProc(blkup.ctx, dest, resultChan, list, totalLength, totalCount, int(blockSize), destPath, blkup, blkup.log) }() wg.Wait() From 466148dfbe700ecbd9f1c2dc8b3d9e3b85800544 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Thu, 16 Jul 2026 15:38:37 +0800 Subject: [PATCH 131/194] add documentation for fine-grained restore filters Signed-off-by: Adam Zhang --- changelogs/unreleased/10016-adam-jian-zhang | 1 + .../docs/main/fine-grained-restore-filters.md | 654 ++++++++++++++++++ site/data/docs/main-toc.yml | 2 + 3 files changed, 657 insertions(+) create mode 100644 changelogs/unreleased/10016-adam-jian-zhang create mode 100644 site/content/docs/main/fine-grained-restore-filters.md diff --git a/changelogs/unreleased/10016-adam-jian-zhang b/changelogs/unreleased/10016-adam-jian-zhang new file mode 100644 index 000000000..1fab47983 --- /dev/null +++ b/changelogs/unreleased/10016-adam-jian-zhang @@ -0,0 +1 @@ +Fix issue #9938, add use guide for restore fine-grained filters via resource policy diff --git a/site/content/docs/main/fine-grained-restore-filters.md b/site/content/docs/main/fine-grained-restore-filters.md new file mode 100644 index 000000000..107b3bd61 --- /dev/null +++ b/site/content/docs/main/fine-grained-restore-filters.md @@ -0,0 +1,654 @@ +--- +title: "Fine-Grained Restore Filters" +layout: docs +--- + +This guide explains how to use Velero's **fine-grained restore filters**: per-namespace, per-kind rules with independent label selectors and resource name patterns. Configuration lives in a **ResourcePolicy ConfigMap**, using the exact same format introduced for fine-grained backup filters. + +For architecture and pipeline details, see the [design document](https://github.com/velero-io/velero/blob/main/design/restore-filter-enhancement/fine-grained-restore-filters-design.md). + +--- + +## Introduction + +Velero's traditional restore filters apply the same namespace list, resource types, and label selector to every namespace being restored. Common scenarios need more control: + +- **Selective restore from a full backup** — restore only specific application components from a namespace, leaving out monitoring or logging resources that were also backed up. +- **Cross-environment migration** — restore StatefulSets and PVCs in a database namespace, but only Deployments and Services in a frontend namespace. +- **Filter by resource name** — restore `app-config` and `app-secret` without restoring `monitoring-config` from the same namespace. +- **Restore-time override** — apply different label selectors during restore than were used during backup to handle environment differences. + +Fine-grained filters add two optional sections to the ResourcePolicy ConfigMap: + +| Section | Scope | Behavior | +|---------|-------|----------| +| `namespacedFilterPolicies` | Namespaces you match (exact name or glob) | **Exclusive allowlist** — only resource kinds listed in `resourceFilters` (or covered by a catch-all) are restored for those namespaces, provided they pass global filters. | +| `clusterScopedFilterPolicy` | Cluster-scoped resources globally | **Refinement overlay** — listed kinds get per-kind label and name rules; unlisted cluster-scoped kinds still use global RestoreSpec filters. | + +**Backward compatible:** if you omit the `ResourcePolicy` reference, restores behave exactly as they do today. + +--- + +## Prerequisites and wiring + +### What you need + +- A ResourcePolicy ConfigMap in the Velero namespace (`velero` by default). +- Permission to create Restores that reference the ConfigMap. + +### End-to-end pattern + +Every example below follows the same three steps: + +1. **Create or update** a ConfigMap with `data.policy` containing `version: v1` and your filter rules. +2. **Create a Restore** that includes the target namespaces and references the ConfigMap. +3. **Verify** with `velero restore describe` and inspect the restored resources. + +### Minimal skeleton + +Use this once; later examples show only the `policy:` body. + +**ResourcePolicy ConfigMap:** + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-restore-filter-policy + namespace: velero +data: + policy: | + version: v1 + namespacedFilterPolicies: + - namespaces: + - my-namespace + resourceFilters: + - kinds: [ConfigMap] + labelSelector: + app: my-app +``` + +**Restore:** + +```yaml +apiVersion: velero.io/v1 +kind: Restore +metadata: + name: my-restore + namespace: velero +spec: + backupName: my-backup + includedNamespaces: + - my-namespace + resourcePolicy: + kind: configmap + name: my-restore-filter-policy +``` + +**CLI equivalent:** + +```bash +velero restore create my-restore \ + --from-backup my-backup \ + --include-namespaces my-namespace \ + --resource-policies-configmap my-restore-filter-policy +``` + +**Verify:** + +```bash +velero restore describe my-restore +``` + +### Important: Interaction with Global Filters + +The restore pipeline evaluates **global resource filters first**: +- `RestoreSpec.IncludedResources` and `RestoreSpec.ExcludedResources` act as a global gate. +- A resource kind **must** pass the global gate before per-namespace filters are evaluated. +- **A namespace policy cannot re-include a globally excluded kind.** If you globally exclude `secrets`, listing `Secret` in a namespace policy will have no effect. + +--- + +## Examples + +Each example includes: **goal**, **policy YAML**, **restore notes**, and **expected outcome**. + +--- + +### Example 0 — Baseline (no new filters) + +**Goal:** Confirm that namespaces without a `namespacedFilterPolicies` entry still use global RestoreSpec filters. + +**Policy:** Omit `namespacedFilterPolicies` and `clusterScopedFilterPolicy` entirely. + +**Restore:** + +```yaml +spec: + includedNamespaces: + - ns-a + - ns-b + # No resourcePolicy — global filters only +``` + +**Expected outcome:** All resources in included namespaces follow `includedNamespaces`, `labelSelector`, `includedResources`, and related global fields — same as before this feature. + +--- + +### Example 1 — Per-namespace kinds and labels + +**Goal:** In `ns-a`, restore only ConfigMaps, Secrets, Deployments, and Pods with `app=my-app`. In `ns-b`, use global filters (no policy entry for that namespace). + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [ConfigMap, Secret, Deployment, Pod] + labelSelector: + app: my-app +``` + +**Restore:** + +```yaml +spec: + includedNamespaces: + - ns-a + - ns-b + resourcePolicy: + kind: configmap + name: per-namespace-resource-filter-policy +``` + +**Expected outcome:** + +- **ns-a:** Only listed kinds with label `app=my-app` (e.g. `app-config`, `app-secret`, `app-deployment`). Resources like `monitoring-config` (different labels) are excluded. +- **ns-b:** Everything allowed by global filters (no namespace policy match). + +--- + +### Example 2 — Exact resource names + +**Goal:** Restore only two ConfigMaps by exact name, optionally requiring a label. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - target-namespace + resourceFilters: + - kinds: [ConfigMap] + names: [vm-1, vm-2] + labelSelector: + resource-type: VirtualMachine +``` + +**Expected outcome:** Only `vm-1` and `vm-2` ConfigMaps with `resource-type=VirtualMachine` are restored. `vm-3` and other ConfigMaps are skipped. + +--- + +### Example 3 — Glob name patterns with exclusions + +**Goal:** Restore `app-*` ConfigMaps and Secrets in `production`, but exclude temporary and debug names. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - production + resourceFilters: + - kinds: [ConfigMap, Secret] + names: ["app-*"] + excludedNames: ["*-tmp", "*-debug"] +``` + +**Expected outcome:** + +- **Included:** `app-config`, `app-cache-config`, `app-secret` +- **Excluded:** `app-tmp-config`, `app-debug-config`, `monitoring-tmp-secret` + +`excludedNames` takes precedence over `names` when both match. + +--- + +### Example 4 — Per-kind label selectors + +**Goal:** Apply different label rules to different resource types in the same namespace. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - target-namespace + resourceFilters: + - kinds: [ConfigMap] + orLabelSelectors: + - app: production-workload-1 + component: vm-group + - app: production-workload-2 + component: vm-service +``` + +**Expected outcome:** ConfigMaps matching either label combination are restored; other ConfigMaps in the namespace are not. + +--- + +### Example 5 — OR label selectors across kinds + +**Goal:** Restore ConfigMaps, Secrets, or Deployments that match any of several label conditions. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [ConfigMap, Secret] + orLabelSelectors: + - app: my-app + - app: monitoring + - kinds: [Deployment] + orLabelSelectors: + - app: my-app + - app: monitoring + - component: backend +``` + +**Expected outcome:** Resources included if they match **any** map in `orLabelSelectors` for their kind. + +--- + +### Example 6 — Multiple criteria on one kind + +**Goal:** Combine exact names with OR label selectors for a single kind. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - target-namespace + resourceFilters: + - kinds: [ConfigMap] + names: [vm-1, vm-2] + orLabelSelectors: + - resource-type: VirtualMachine + - component: vm-group + - component: vm-service +``` + +**Expected outcome:** Only `vm-1` and `vm-2` that also satisfy one of the label OR branches. + +--- + +### Example 7 — One policy entry, multiple namespaces + +**Goal:** Apply the same rules to `ns-a`, `ns-b`, and `production` in a single policy block. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + - ns-b + - production + resourceFilters: + - kinds: [ConfigMap] + - kinds: [Deployment] + labelSelector: + tier: web +``` + +**Expected outcome:** + +- All ConfigMaps in those namespaces (no label filter on that entry). +- Deployments with `tier=web` only. + +--- + +### Example 8 — Namespace glob patterns and ordering + +**Goal:** Different restore breadth for `team-frontend-prod`, `team-frontend-dev`, and `team-backend-test` using glob patterns. + +**Policy (correct order — most specific first):** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - team-frontend-prod # exact match + resourceFilters: + - kinds: [Deployment, Service, ConfigMap, Secret, PersistentVolumeClaim] + - namespaces: + - "team-frontend-*" # pattern match + resourceFilters: + - kinds: [Deployment, Service, ConfigMap] + - namespaces: + - "team-*" # broad pattern + resourceFilters: + - kinds: [Deployment, Service] +``` + +**Expected outcome:** + +| Namespace | Matched policy | Kinds restored | +|-----------|----------------|-----------------| +| `team-frontend-prod` | First entry (exact) | 5 kinds | +| `team-frontend-dev` | `team-frontend-*` | 3 kinds | +| `team-backend-test` | `team-*` | 2 kinds | + +Velero uses **first-match** semantics: the first policy entry whose namespace pattern matches wins. + +--- + +### Example 9 — Catch-all by label + +**Goal:** Restore any resource kind that has a given label, without listing every kind. Kind-specific entries override the catch-all. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: ["*"] # catch-all + labelSelector: + app: common-app + - kinds: [ConfigMap, Secret] # override for these kinds + labelSelector: + app: specialized-app +``` + +**Rules:** + +- At most **one** catch-all per namespace policy entry. +- Catch-all entries **cannot** use `names` or `excludedNames`. +- Catch-all does **not** inherit `RestoreSpec.LabelSelector`. + +**Expected outcome:** ConfigMaps and Secrets use `app=specialized-app`; all other kinds listed only via catch-all use `app=common-app`. + +--- + +### Example 10 — Catch-all with per-kind name overrides + +**Goal:** Pin critical Deployments and Secrets by exact name; restore everything else with a label convention. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [Deployment] + names: [api-server, worker] + - kinds: [Secret] + names: [db-credentials, tls-cert] + - kinds: ["*"] + labelSelector: + restore: "true" +``` + +**Expected outcome:** + +- Deployments: only `api-server` and `worker` +- Secrets: only `db-credentials` and `tls-cert` +- Other kinds (ConfigMap, Service, …): resources with `restore=true` only + +--- + +### Example 11 — Override-only catch-all (no label on catch-all) + +**Goal:** Apply a strict name filter to one kind while restoring all other kinds without listing them or adding labels. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [Secret] + names: [app-secret] + - kinds: ["*"] # no labelSelector — all other kinds included +``` + +**Expected outcome:** + +- Secrets: only `app-secret` +- Other kinds in `ns-a`: all instances restored (subject to global filters) + +--- + +### Example 12 — Cluster-scoped refinement + +**Goal:** Refine which cluster-scoped resources are restored by name and label, without replacing global cluster-scoped inclusion. + +**Policy:** + +```yaml +version: v1 +clusterScopedFilterPolicy: + resourceFilters: + - kinds: [StorageClass] + names: ["my-app-*"] + - kinds: [ClusterRole, ClusterRoleBinding] + labelSelector: + app: my-app +``` + +**Restore (required):** You must still include cluster-scoped kinds on the Restore: + +```yaml +spec: + includeClusterResources: true + resourcePolicy: + kind: configmap + name: cluster-scoped-filter-policy +``` + +**Expected outcome:** + +- StorageClasses matching `my-app-*` only +- ClusterRoles and ClusterRoleBindings with `app=my-app` only +- Other cluster-scoped resources: restored according to global filters. + +**Differences from namespace policies:** + +- **Not** an allowlist — unlisted cluster-scoped kinds fall back to global filters. +- **No catch-all** — `kinds: []` or `kinds: ["*"]` is invalid and fails validation. + +--- + +### Example 13 — Global `ExcludedResources` and namespace filters + +**Goal:** Understand that global **exclusions** cannot be overridden per namespace. + +**Restore:** +```yaml +spec: + excludedResources: + - secrets +``` + +**Policy:** +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-a + resourceFilters: + - kinds: [ConfigMap, Secret, Deployment] + labelSelector: + app: my-app +``` + +**Result:** No Secrets are restored — the namespace policy cannot re-include a globally excluded kind. Velero logs a warning at restore start if you list an excluded kind in `namespacedFilterPolicies`. + +--- + +### Example 14 — Same ConfigMap for Backup and Restore + +**Goal:** Use a single ConfigMap for both backup and restore operations. + +**Policy:** + +```yaml +version: v1 +volumePolicies: + - conditions: + capacity: "0,10Gi" + action: + type: fs-backup +namespacedFilterPolicies: + - namespaces: + - production + resourceFilters: + - kinds: [ConfigMap, Secret] + names: ["app-*"] +``` + +**Expected outcome:** The restore pipeline safely ignores `volumePolicies` and `includeExcludePolicy` (which are backup-specific) and only processes `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. + +--- + +### Example 15 — `velero.io/exclude-from-backup=true` always wins + +**Goal:** Ensure explicitly excluded resources never appear in the restore. + +If a resource was backed up (perhaps before the label was added, or manually modified in the archive) but has `velero.io/exclude-from-backup: "true"`, the restore pipeline honors it. Any item carrying this label is skipped regardless of whether it matches global or per-namespace restore filters. + +--- + +## Concepts reference + +### `resourceFilters` fields + +| Field | Description | +|-------|-------------| +| `kinds` | Resource type names (e.g. `ConfigMap`, `deployments`). Empty or `["*"]` = catch-all (namespace policies only). | +| `labelSelector` | Equality labels (`key: value`), AND across keys. No `in`, `exists`, etc. — use `orLabelSelectors` for OR. | +| `orLabelSelectors` | List of label maps; match if **any** map matches (AND within each map). Mutually exclusive with `labelSelector`. | +| `names` | Exact names or glob patterns to include. | +| `excludedNames` | Patterns to exclude; wins over `names` when both match. | + +### Glob pattern syntax + +Name and namespace patterns use the same glob style as elsewhere in Velero (`gobwas/glob`): + +- Supported: `*`, `?`, `[abc]`, `[a-z]` +- Not supported: `**`, regex, `|`, `()`, `!`, `{}`, `,` + +Examples: `app-*`, `team-frontend-*`, `*-tmp`. + +### Precedence cheat sheet + +**Namespaces** + +1. `RestoreSpec.ExcludedNamespaces` — excluded namespaces are never restored. +2. `namespacedFilterPolicies` — first matching pattern (exact match checked before globs in pattern order). +3. No match — use global RestoreSpec filters. + +**Namespace-scoped resources (when a namespace policy matches)** + +1. Global `RestoreSpec.IncludedResources` / `ExcludedResources` apply first. +2. Only kinds in `resourceFilters` (or catch-all) are allowlisted for restoration. +3. Per-kind `labelSelector` / `orLabelSelectors` replace global selectors. +4. Per-kind `names` / `excludedNames` filter by resource name. +5. Label `velero.io/exclude-from-backup=true` always excludes. +6. **Plugin Additional Items** bypass fine-grained filters to ensure dependencies (like PVs) are restored. + +**Cluster-scoped resources** + +1. Must be allowed by global cluster settings (`includeClusterResources`). +2. If `clusterScopedFilterPolicy` lists the kind, apply its label and name rules. +3. If not listed in `clusterScopedFilterPolicy`, use global RestoreSpec filters. +4. `velero.io/exclude-from-backup=true` always excludes. + +### Catch-all summary + +| Rule | Detail | +|------|--------| +| Syntax | `kinds: ["*"]` or `kinds: []` | +| Count | At most one catch-all per `namespacedFilterPolicies` entry | +| Names | `names` / `excludedNames` not allowed on catch-all | +| Override | Kind-specific entries take precedence over catch-all | +| Label inheritance | Does not use `RestoreSpec.LabelSelector` | +| Cluster-scoped | Catch-all **not** supported in `clusterScopedFilterPolicy` | + +--- + +## Troubleshooting and validation + +### Verify a restore + +```bash +velero restore describe RESTORE_NAME +velero restore logs RESTORE_NAME +``` + +The output of `velero restore describe` will show the `Resource Policy` field if a ConfigMap was used. + +### Common misconfigurations + +| Symptom | Likely cause | Fix | +|---------|----------------|-----| +| Fewer resources than expected in `team-frontend-prod` | Broad namespace pattern listed before specific one | Reorder policies: most specific `namespaces` first | +| Namespace policy lists Secrets but none restored | `RestoreSpec.ExcludedResources` excludes `secrets` globally | Remove global exclusion or accept no Secrets | +| `ClusterRole` in namespace policy has no effect | Cluster-scoped kind in `namespacedFilterPolicies` | Move rule to `clusterScopedFilterPolicy`; check logs for warning | +| Catch-all does not use restore-wide label | By design | Set `labelSelector` on the catch-all entry | +| Cluster-scoped policy validation error on `kinds: ["*"]` | Catch-all not allowed for cluster policy | List each cluster-scoped kind explicitly | + +### Velero logs + +```bash +kubectl logs -n velero deployment/velero | grep -i "namespacedFilterPolicies\|clusterScopedFilterPolicy" +kubectl logs -n velero deployment/velero | grep "globally excluded by RestoreSpec.ExcludedResources" +``` + +### Validation errors (policy ConfigMap) + +Velero validates the ResourcePolicy when a restore starts. Common errors: + +| Error (summary) | Cause | +|-----------------|--------| +| `at least one namespace must be specified` | Empty `namespaces: []` | +| `at least one resourceFilter must be specified` | Empty `resourceFilters: []` | +| `names or excludedNames cannot be specified when kinds is empty` | Name patterns on catch-all entry | +| `only one resource filter with empty kinds is allowed` | Multiple catch-alls in one policy entry | +| `kind "X" appears in both resourceFilters[...]` | Same kind in two entries | +| `labelSelector and orLabelSelectors cannot co-exist` | Both set in one entry | +| `duplicate namespace pattern` | Same namespace string in two policy entries | +| `invalid glob pattern` | Bad characters in namespace or name pattern | +| `clusterScopedFilterPolicy... kinds must be specified (catch-all is not supported)` | Empty or `["*"]` kinds in cluster policy | + +### Silent edge cases (no error) + +- Namespace pattern matches no existing namespace in the backup — policy loaded but never applied. +- Kind listed but no instances in namespace — empty result, restore still succeeds. +- `excludedNames` narrows `names` — e.g. `names: ["app-*"]` + `excludedNames: ["app-config"]` excludes `app-config` only. + +--- + +## Related links + +- [Fine-grained restore filters design](https://github.com/velero-io/velero/blob/main/design/restore-filter-enhancement/fine-grained-restore-filters-design.md) diff --git a/site/data/docs/main-toc.yml b/site/data/docs/main-toc.yml index 6008d5d66..82f4bbd8f 100644 --- a/site/data/docs/main-toc.yml +++ b/site/data/docs/main-toc.yml @@ -35,6 +35,8 @@ toc: url: /resource-filtering - page: Fine-Grained Backup Filters url: /fine-grained-backup-filters + - page: Fine-grained restore filters + url: /fine-grained-restore-filters - page: Namespace glob patterns url: /namespace-glob-patterns - page: Backup reference From 24d109bc888ac6f1f36a1e819271127cdc5ef198 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Fri, 17 Jul 2026 13:49:22 +0800 Subject: [PATCH 132/194] address review comments - update example 14 to be an validation error case when user tried to reuse backup side resource policies which contains fields not accepted by restore side - enhance example 8 with exact match on namespace - update the validation errors to match implementation Signed-off-by: Adam Zhang --- .../docs/main/fine-grained-restore-filters.md | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/site/content/docs/main/fine-grained-restore-filters.md b/site/content/docs/main/fine-grained-restore-filters.md index 107b3bd61..18c40aefa 100644 --- a/site/content/docs/main/fine-grained-restore-filters.md +++ b/site/content/docs/main/fine-grained-restore-filters.md @@ -325,32 +325,37 @@ namespacedFilterPolicies: **Goal:** Different restore breadth for `team-frontend-prod`, `team-frontend-dev`, and `team-backend-test` using glob patterns. -**Policy (correct order — most specific first):** +**Note on Precedence:** Exact namespace matches always take precedence regardless of where they are listed. However, if multiple glob patterns could match a namespace, they are evaluated in the order they appear. Always list specific globs before broad globs. + +**Policy:** ```yaml version: v1 namespacedFilterPolicies: + # Globs must be ordered specific-to-broad - namespaces: - - team-frontend-prod # exact match - resourceFilters: - - kinds: [Deployment, Service, ConfigMap, Secret, PersistentVolumeClaim] - - namespaces: - - "team-frontend-*" # pattern match + - "team-frontend-*" # specific pattern match resourceFilters: - kinds: [Deployment, Service, ConfigMap] - namespaces: - "team-*" # broad pattern resourceFilters: - kinds: [Deployment, Service] + + # Exact matches always win, even if placed at the bottom + - namespaces: + - team-frontend-prod # exact match + resourceFilters: + - kinds: [Deployment, Service, ConfigMap, Secret, PersistentVolumeClaim] ``` **Expected outcome:** | Namespace | Matched policy | Kinds restored | |-----------|----------------|-----------------| -| `team-frontend-prod` | First entry (exact) | 5 kinds | -| `team-frontend-dev` | `team-frontend-*` | 3 kinds | -| `team-backend-test` | `team-*` | 2 kinds | +| `team-frontend-prod` | `team-frontend-prod` (Exact match priority) | 5 kinds | +| `team-frontend-dev` | `team-frontend-*` (First matching glob) | 3 kinds | +| `team-backend-test` | `team-*` (First matching glob) | 2 kinds | Velero uses **first-match** semantics: the first policy entry whose namespace pattern matches wins. @@ -506,9 +511,9 @@ namespacedFilterPolicies: --- -### Example 14 — Same ConfigMap for Backup and Restore +### Example 14 — Separate ConfigMaps for Backup and Restore -**Goal:** Use a single ConfigMap for both backup and restore operations. +**Goal:** Understand why you cannot use a single ConfigMap for both backup and restore operations if it contains backup-specific policies. **Policy:** @@ -527,7 +532,7 @@ namespacedFilterPolicies: names: ["app-*"] ``` -**Expected outcome:** The restore pipeline safely ignores `volumePolicies` and `includeExcludePolicy` (which are backup-specific) and only processes `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. +**Expected outcome:** The restore operation will **fail validation**. The Velero restore pipeline strictly rejects any ResourcePolicy ConfigMap containing `volumePolicies` or `includeExcludePolicy`. To avoid this, the restore-side ConfigMap should contain only the restore-supported sections (`namespacedFilterPolicies` and/or `clusterScopedFilterPolicy`). --- @@ -633,8 +638,8 @@ Velero validates the ResourcePolicy when a restore starts. Common errors: |-----------------|--------| | `at least one namespace must be specified` | Empty `namespaces: []` | | `at least one resourceFilter must be specified` | Empty `resourceFilters: []` | -| `names or excludedNames cannot be specified when kinds is empty` | Name patterns on catch-all entry | -| `only one resource filter with empty kinds is allowed` | Multiple catch-alls in one policy entry | +| `names or excludedNames cannot be specified for catch-all filters` | Name patterns on catch-all entry | +| `only one catch-all resource filter is allowed` | Multiple catch-alls in one policy entry | | `kind "X" appears in both resourceFilters[...]` | Same kind in two entries | | `labelSelector and orLabelSelectors cannot co-exist` | Both set in one entry | | `duplicate namespace pattern` | Same namespace string in two policy entries | From 861292be04a6e5d11ad5e6f3f982a04999be4988 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Sat, 25 Jul 2026 12:19:17 +0800 Subject: [PATCH 133/194] add support for set-based label selectors Signed-off-by: Adam Zhang --- .../docs/main/fine-grained-restore-filters.md | 115 ++++++++++++++---- 1 file changed, 91 insertions(+), 24 deletions(-) diff --git a/site/content/docs/main/fine-grained-restore-filters.md b/site/content/docs/main/fine-grained-restore-filters.md index 18c40aefa..81ff18fbd 100644 --- a/site/content/docs/main/fine-grained-restore-filters.md +++ b/site/content/docs/main/fine-grained-restore-filters.md @@ -65,7 +65,8 @@ data: resourceFilters: - kinds: [ConfigMap] labelSelector: - app: my-app + matchLabels: + app: my-app ``` **Restore:** @@ -149,7 +150,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret, Deployment, Pod] labelSelector: - app: my-app + matchLabels: + app: my-app ``` **Restore:** @@ -186,7 +188,8 @@ namespacedFilterPolicies: - kinds: [ConfigMap] names: [vm-1, vm-2] labelSelector: - resource-type: VirtualMachine + matchLabels: + resource-type: VirtualMachine ``` **Expected outcome:** Only `vm-1` and `vm-2` ConfigMaps with `resource-type=VirtualMachine` are restored. `vm-3` and other ConfigMaps are skipped. @@ -233,14 +236,63 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap] orLabelSelectors: - - app: production-workload-1 - component: vm-group - - app: production-workload-2 - component: vm-service + - matchLabels: + app: production-workload-1 + component: vm-group + - matchLabels: + app: production-workload-2 + component: vm-service ``` **Expected outcome:** ConfigMaps matching either label combination are restored; other ConfigMaps in the namespace are not. +**Note:** Prefer `matchExpressions` with `In` for value-OR on a single key (see next example). Use `orLabelSelectors` when you need OR across **independent multi-key groups**. `labelSelector` and `orLabelSelectors` cannot appear in the same `resourceFilters` entry. + +--- + +### Example 4b — Set-based label selectors (`matchExpressions`) + +**Goal:** Restore Deployments and Pods that are in `prod` or `staging`, belong to `app=my-app`, and do **not** carry a skip label. + +**Policy:** + +```yaml +version: v1 +namespacedFilterPolicies: + - namespaces: + - production + resourceFilters: + - kinds: [Deployment, Pod] + labelSelector: + matchLabels: + app: my-app + matchExpressions: + - key: environment + operator: In + values: [prod, staging] + - key: do-not-restore + operator: DoesNotExist +``` + +**Supported operators:** `In`, `NotIn`, `Exists`, `DoesNotExist` (same as Kubernetes / Velero global `--selector`). + +**Other useful patterns:** + +```yaml +# Exclude environments +matchExpressions: + - key: environment + operator: NotIn + values: [dev, test] + +# Require a label key to be present (any value) +matchExpressions: + - key: tier + operator: Exists +``` + +**Expected outcome:** Only Deployments/Pods with `app=my-app`, `environment` in `{prod, staging}`, and without `do-not-restore` are restored. + --- ### Example 5 — OR label selectors across kinds @@ -257,16 +309,21 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret] orLabelSelectors: - - app: my-app - - app: monitoring + - matchLabels: + app: my-app + - matchLabels: + app: monitoring - kinds: [Deployment] orLabelSelectors: - - app: my-app - - app: monitoring - - component: backend + - matchLabels: + app: my-app + - matchLabels: + app: monitoring + - matchLabels: + component: backend ``` -**Expected outcome:** Resources included if they match **any** map in `orLabelSelectors` for their kind. +**Expected outcome:** Resources included if they match **any** selector in `orLabelSelectors` for their kind (AND within each selector, OR across the list). --- @@ -285,9 +342,12 @@ namespacedFilterPolicies: - kinds: [ConfigMap] names: [vm-1, vm-2] orLabelSelectors: - - resource-type: VirtualMachine - - component: vm-group - - component: vm-service + - matchLabels: + resource-type: VirtualMachine + - matchLabels: + component: vm-group + - matchLabels: + component: vm-service ``` **Expected outcome:** Only `vm-1` and `vm-2` that also satisfy one of the label OR branches. @@ -311,7 +371,8 @@ namespacedFilterPolicies: - kinds: [ConfigMap] - kinds: [Deployment] labelSelector: - tier: web + matchLabels: + tier: web ``` **Expected outcome:** @@ -375,10 +436,12 @@ namespacedFilterPolicies: resourceFilters: - kinds: ["*"] # catch-all labelSelector: - app: common-app + matchLabels: + app: common-app - kinds: [ConfigMap, Secret] # override for these kinds labelSelector: - app: specialized-app + matchLabels: + app: specialized-app ``` **Rules:** @@ -409,7 +472,8 @@ namespacedFilterPolicies: names: [db-credentials, tls-cert] - kinds: ["*"] labelSelector: - restore: "true" + matchLabels: + restore: "true" ``` **Expected outcome:** @@ -458,7 +522,8 @@ clusterScopedFilterPolicy: names: ["my-app-*"] - kinds: [ClusterRole, ClusterRoleBinding] labelSelector: - app: my-app + matchLabels: + app: my-app ``` **Restore (required):** You must still include cluster-scoped kinds on the Restore: @@ -504,7 +569,8 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret, Deployment] labelSelector: - app: my-app + matchLabels: + app: my-app ``` **Result:** No Secrets are restored — the namespace policy cannot re-include a globally excluded kind. Velero logs a warning at restore start if you list an excluded kind in `namespacedFilterPolicies`. @@ -551,8 +617,8 @@ If a resource was backed up (perhaps before the label was added, or manually mod | Field | Description | |-------|-------------| | `kinds` | Resource type names (e.g. `ConfigMap`, `deployments`). Empty or `["*"]` = catch-all (namespace policies only). | -| `labelSelector` | Equality labels (`key: value`), AND across keys. No `in`, `exists`, etc. — use `orLabelSelectors` for OR. | -| `orLabelSelectors` | List of label maps; match if **any** map matches (AND within each map). Mutually exclusive with `labelSelector`. | +| `labelSelector` | Kubernetes-style selector with `matchLabels` and/or `matchExpressions` (`In`, `NotIn`, `Exists`, `DoesNotExist`). All requirements are AND-ed. | +| `orLabelSelectors` | List of selectors; match if **any** entry matches (AND within each, OR across the list). Use for OR of multi-key groups; prefer `In` for value-OR on one key. Mutually exclusive with `labelSelector`. | | `names` | Exact names or glob patterns to include. | | `excludedNames` | Patterns to exclude; wins over `names` when both match. | @@ -642,6 +708,7 @@ Velero validates the ResourcePolicy when a restore starts. Common errors: | `only one catch-all resource filter is allowed` | Multiple catch-alls in one policy entry | | `kind "X" appears in both resourceFilters[...]` | Same kind in two entries | | `labelSelector and orLabelSelectors cannot co-exist` | Both set in one entry | +| `invalid label selector` | Bad operator, values, or label key/value syntax | | `duplicate namespace pattern` | Same namespace string in two policy entries | | `invalid glob pattern` | Bad characters in namespace or name pattern | | `clusterScopedFilterPolicy... kinds must be specified (catch-all is not supported)` | Empty or `["*"]` kinds in cluster policy | From ef1b8a6ced0e27a718c60cac2ceb3ca5e5f99298 Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Mon, 3 Aug 2026 16:12:54 +0800 Subject: [PATCH 134/194] address review comments - update resource-filtering to incorporate fine-grained filters - update backward compatibility parts to focus on this feature itself - update example 3 to be more percise Signed-off-by: Adam Zhang --- .../docs/main/fine-grained-restore-filters.md | 6 +- site/content/docs/main/resource-filtering.md | 219 ++++++++++-------- 2 files changed, 130 insertions(+), 95 deletions(-) diff --git a/site/content/docs/main/fine-grained-restore-filters.md b/site/content/docs/main/fine-grained-restore-filters.md index 81ff18fbd..0f7c52c26 100644 --- a/site/content/docs/main/fine-grained-restore-filters.md +++ b/site/content/docs/main/fine-grained-restore-filters.md @@ -25,7 +25,7 @@ Fine-grained filters add two optional sections to the ResourcePolicy ConfigMap: | `namespacedFilterPolicies` | Namespaces you match (exact name or glob) | **Exclusive allowlist** — only resource kinds listed in `resourceFilters` (or covered by a catch-all) are restored for those namespaces, provided they pass global filters. | | `clusterScopedFilterPolicy` | Cluster-scoped resources globally | **Refinement overlay** — listed kinds get per-kind label and name rules; unlisted cluster-scoped kinds still use global RestoreSpec filters. | -**Backward compatible:** if you omit the `ResourcePolicy` reference, restores behave exactly as they do today. +**Backward compatible:** Fine-grained restore filters are optional. If a restore does not reference a ResourcePolicy, Velero relies solely on standard RestoreSpec filters (includedNamespaces, includedResources, labelSelector, etc.). --- @@ -210,13 +210,13 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap, Secret] names: ["app-*"] - excludedNames: ["*-tmp", "*-debug"] + excludedNames: ["*-tmp-*", "*-debug-*", "*-tmp", "*-debug"] ``` **Expected outcome:** - **Included:** `app-config`, `app-cache-config`, `app-secret` -- **Excluded:** `app-tmp-config`, `app-debug-config`, `monitoring-tmp-secret` +- **Excluded:** `app-config-tmp`, `app-tmp-config`, `app-debug-config`, `monitoring-tmp-secret` `excludedNames` takes precedence over `names` when both match. diff --git a/site/content/docs/main/resource-filtering.md b/site/content/docs/main/resource-filtering.md index 88584b362..d0c678462 100644 --- a/site/content/docs/main/resource-filtering.md +++ b/site/content/docs/main/resource-filtering.md @@ -5,8 +5,8 @@ layout: docs *Filter objects by namespace, type, labels or resource policies.* -This page describes how to filter resource for backup and restore. -User could use the include and exclude flags with the `velero backup` and `velero restore` commands. And user could also use resource policies to handle backup. +This page describes how to filter resources for backup and restore. +Users can use include and exclude flags with the `velero backup` and `velero restore` commands. Users can also use resource policies for fine-grained resource filtering during backup and restore, as well as volume handling during backup. By default, Velero includes all objects in a backup or restore when no filtering options are used. ## Includes @@ -229,104 +229,139 @@ Kubernetes namespace resources to exclude from the backup, formatted as resource ``` ## Resource policies -Velero provides resource policies to filter resources to do backup, which may contain `includeExcludePolicy` and `volumePolicies`. -### Creating resource policies +Velero provides resource policies (defined in a ConfigMap and referenced via `--resource-policies-configmap` or `spec.resourcePolicy`) to define fine-grained resource filters and volume handling rules. -Below is the two-step of using resource policies in backup: -1. Creating resource policies configmap +Resource policies support both **Backup** and **Restore** operations, though certain policy sections are specific to backup workflows. - Users need to create one configmap in Velero install namespace from a YAML file that defined resource policies. The creating command would be like the below: +### Supported policy sections by operation + +| Policy Section | Description | Supported Operations | Learn More | +| --- | --- | --- | --- | +| `namespacedFilterPolicies` | Fine-grained per-namespace and per-kind filters with label selectors and resource name patterns. | **Backup** & **Restore** | [Fine-Grained Backup Filters](fine-grained-backup-filters.md) / [Fine-Grained Restore Filters](fine-grained-restore-filters.md) | +| `clusterScopedFilterPolicy` | Fine-grained cluster-scoped filter overlays with per-kind label selectors and resource name patterns. | **Backup** & **Restore** | [Fine-Grained Backup Filters](fine-grained-backup-filters.md) / [Fine-Grained Restore Filters](fine-grained-restore-filters.md) | +| `volumePolicies` | Rules to control volume data backup methods (`skip`, `snapshot`, `fs-backup`) based on conditions. | **Backup** only | See [VolumePolicy](#volumepolicy-backup-only) | +| `includeExcludePolicy` | Reusable scoped resource include/exclude filters. | **Backup** only | See [IncludeExcludePolicy](#includeexcludepolicy-backup-only) | + +### Creating and referencing resource policies + +Using resource policies is a two-step process: + +1. **Create the resource policies ConfigMap** + + Create a ConfigMap in the Velero installation namespace (typically `velero`) containing your YAML policy definition: ```bash kubectl create cm --from-file -n velero ``` -2. Creating a backup reference to the defined resource policies - Users create a backup with the flag `--resource-policies-configmap`, which will reference the current backup to the defined resource policies. The creating command would be like the below: - ```bash - velero backup create --resource-policies-configmap - ``` - This flag could also be combined with the other include and exclude filters above +2. **Reference the resource policies ConfigMap in a Backup or Restore** + + * **For Backup:** Reference the ConfigMap via CLI flag or in the Backup CR spec: + ```bash + velero backup create --resource-policies-configmap + ``` + Or in `Backup.spec`: + ```yaml + spec: + resourcePolicy: + kind: ConfigMap + name: + ``` + + * **For Restore:** Reference the ConfigMap via CLI flag or in the Restore CR spec: + ```bash + velero restore create --from-backup --resource-policies-configmap + ``` + Or in `Restore.spec`: + ```yaml + spec: + resourcePolicy: + kind: ConfigMap + name: + ``` + + These flags and fields can also be combined with standard include and exclude options. ### YAML template -The policies YAML config file would look like this: -- Yaml template: - ```yaml - # currently only supports v1 version - version: v1 - # The filters in includeExcludePolicy work the same as the scoped resources filters in the Spec of a Backup - # NOTE: similar to scoped filters in Backup Spec, the includeExcludePolicy does not work with --include-resources, --exclude-resources and --include-cluster-resources filters in Backup. - includeExcludePolicy: - includedClusterScopedResources: - - "crd" - - "pv" - excludedClusterScopedResources: [] - includedNamespaceScopedResources: - - "pod" - - "service" - - "deployment" - - "pvc" - excludedNamespaceScopedResources: - - "configmap" - - "secret" - volumePolicies: - # each policy consists of a list of conditions and an action - # we could have lots of policies, but if the resource matched the first policy, the latter will be ignored - # each key in the object is one condition, and one policy will apply to resources that meet ALL conditions - # NOTE: capacity or storageClass is suited for [Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes), and pod [Volume](https://kubernetes.io/docs/concepts/storage/volumes) not support it. - - conditions: - # capacity condition matches the volumes whose capacity falls into the range - capacity: "10,100Gi" - # pv matches specific csi driver - csi: - driver: ebs.csi.aws.com - # pv matches one of the storage class list - storageClass: - - gp2 - - standard - # pvc matches specific phase(s) - pvcPhase: - - Pending - # pvc matches specific volume mode - pvcVolumeMode: Block - # pvc matches specific access mode(s) - pvcAccessModes: - - ReadWriteOnce - action: - type: skip - - conditions: - capacity: "0,100Gi" - # nfs volume source with specific server and path (nfs could be empty or only config server or path) - nfs: - server: 192.168.200.90 - path: /mnt/data - action: - type: skip - - conditions: - nfs: - server: 192.168.200.90 - action: - type: fs-backup - - conditions: - # nfs could be empty which matches any nfs volume source - nfs: {} - action: - type: skip - - conditions: - # csi could be empty which matches any csi volume source - csi: {} - action: - type: snapshot - - conditions: - volumeTypes: - - emptyDir - - downwardAPI - - configmap - - cinder - action: - type: skip - ``` -### IncludeExcludePolicy + +The policies YAML config file showing all supported sections: + +```yaml +# Currently supports v1 version +version: v1 + +# Fine-grained namespace-scoped filters (Supported for both Backup and Restore) +namespacedFilterPolicies: + - namespace: "app-ns-*" + resourceFilters: + - kind: "deployment" + labelSelector: + matchLabels: + app: frontend + includedResourceNames: + - "web-*" + - kind: "secret" + excludedResourceNames: + - "sensitive-secret" + +# Fine-grained cluster-scoped filter overlay (Supported for both Backup and Restore) +clusterScopedFilterPolicy: + resourceFilters: + - kind: "storageclass" + labelSelector: + matchLabels: + tier: gold + +# Volume handling policies (Supported for Backup ONLY) +volumePolicies: + - conditions: + capacity: "10,100Gi" + csi: + driver: ebs.csi.aws.com + storageClass: + - gp2 + - standard + pvcPhase: + - Pending + pvcVolumeMode: Block + pvcAccessModes: + - ReadWriteOnce + action: + type: skip + - conditions: + nfs: {} + action: + type: fs-backup + +# Legacy scoped resource include/exclude filters (Supported for Backup ONLY) +# NOTE: Cannot be combined with --include-resources, --exclude-resources, or --include-cluster-resources in Backup. +includeExcludePolicy: + includedClusterScopedResources: + - "crd" + - "pv" + excludedClusterScopedResources: [] + includedNamespaceScopedResources: + - "pod" + - "service" + - "deployment" + - "pvc" + excludedNamespaceScopedResources: + - "configmap" + - "secret" +``` + +### Fine-grained backup and restore filters + +`namespacedFilterPolicies` and `clusterScopedFilterPolicy` allow defining per-namespace and per-kind rules with independent label selectors and resource name patterns. + +* **During Backup:** Controls which resources are backed up from matching namespaces or kinds. +* **During Restore:** Controls which resources are restored from a backup archive without modifying the backup itself. + +For comprehensive guides, syntax details, and detailed examples, see: +* [Fine-Grained Backup Filters](fine-grained-backup-filters.md) +* [Fine-Grained Restore Filters](fine-grained-restore-filters.md) + +### IncludeExcludePolicy (Backup only) The `includeExcludePolicy` is used to filter resources based on the namespace-scoped and cluster-scoped resources. User can use it to define a group of filters and reuse them across different backups. @@ -365,7 +400,7 @@ velero backup create --resource-policies-configmap my-policy --inc The backup will include all resources in namespace `my-workload-ns`, including `configmap` and `event`, and all CRDs and `apiservices` in the cluster. -### VolumePolicy +### VolumePolicy (Backup only) VolumePolicy is a data structure to control how velero handle the volumes matching certain conditions. #### Supported VolumePolicy actions From 1249e699990ebee2cee0b45a09ca4792c77b5843 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 3 Aug 2026 17:09:30 +0800 Subject: [PATCH 135/194] empty sa namespace when secret is empty for getChangedBlocks Signed-off-by: Lyndon-Li --- pkg/cbtservice/csi_service_impl.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/cbtservice/csi_service_impl.go b/pkg/cbtservice/csi_service_impl.go index f4ea23de7..235477bd4 100644 --- a/pkg/cbtservice/csi_service_impl.go +++ b/pkg/cbtservice/csi_service_impl.go @@ -116,6 +116,12 @@ func (s *ServiceImpl) GetChangedBlocks(ctx context.Context, snapshot string, cha return err } + saNamespace := "" + if s.SAName != "" { + // The SA is created in the same namespace as Velero server. vsNamespace is the namespace of Velero server. + saNamespace = s.vsNamespace + } + args := iterator.Args{ SnapshotName: snapshot, PrevSnapshotID: changeID, @@ -126,7 +132,7 @@ func (s *ServiceImpl) GetChangedBlocks(ctx context.Context, snapshot string, cha Clients: clients, Namespace: s.vsNamespace, - SANamespace: s.vsNamespace, + SANamespace: saNamespace, SAName: s.SAName, TokenExpirySecs: iterator.DefaultTokenExpirySeconds, MaxResults: 0, // If 0 then the CSI driver decides the value. From 0ba682902e2106be31fa12a164e6cbe36fe5f68d Mon Sep 17 00:00:00 2001 From: wolf-06 Date: Mon, 3 Aug 2026 14:54:59 +0530 Subject: [PATCH 136/194] update the description and regex pattern Signed-off-by: wolf-06 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8d7e99951..31981217e 100644 --- a/Makefile +++ b/Makefile @@ -172,12 +172,12 @@ GOIMPORTS_VERSION := $(shell go list -m -f '{{.Version}}' golang.org/x/tools) .PHONY: help help: ## Display this help message - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z0-9_%-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) # If you want to build all binaries, see the 'all-build' rule. # If you want to build all containers, see the 'all-containers' rule. -all: ## Build all binaries +all: ## Build the default velero binary @$(MAKE) build build-%: ## Build specific binary From c3ef38c225b503bb9f000ec6a3dcfb8435d06c98 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Mon, 3 Aug 2026 18:02:36 +0800 Subject: [PATCH 137/194] Modify the ParentSnapshot to "" and ForceFull to true when ParentSnapshot is "none". Signed-off-by: Xun Jiang --- pkg/datamover/backup_micro_service.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/datamover/backup_micro_service.go b/pkg/datamover/backup_micro_service.go index cb5aeb3fe..577a091f7 100644 --- a/pkg/datamover/backup_micro_service.go +++ b/pkg/datamover/backup_micro_service.go @@ -23,23 +23,22 @@ import ( "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" - "sigs.k8s.io/controller-runtime/pkg/client" - cachetool "k8s.io/client-go/tools/cache" "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/internal/credentials" + veleroshared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/datapath" "github.com/vmware-tanzu/velero/pkg/repository" "github.com/vmware-tanzu/velero/pkg/uploader" "github.com/vmware-tanzu/velero/pkg/util/kube" - - apierrors "k8s.io/apimachinery/pkg/api/errors" ) const ( @@ -202,10 +201,18 @@ func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string, velerov1api.AsyncOperationIDLabel: du.Labels[velerov1api.AsyncOperationIDLabel], } + // Modify the ParentSnapshot to "" and ForceFull to true when ParentSnapshot is "none". + parentSnapshot := du.Spec.ParentSnapshot + forceFull := false + if du.Spec.ParentSnapshot == veleroshared.DataUploadParentSnapshotNone { + parentSnapshot = "" + forceFull = true + } + if err := dp.StartBackup(r.sourceTargetPath, du.Spec.DataMoverConfig, &datapath.BackupStartParam{ RealSource: GetRealSource(du.Spec.SourceNamespace, du.Spec.SourcePVC), - ParentSnapshot: du.Spec.ParentSnapshot, - ForceFull: false, + ParentSnapshot: parentSnapshot, + ForceFull: forceFull, Tags: tags, VolumeID: r.volumeID, ChangeID: r.changeID, From b74f8c9511d6d0543627ca410decba6fc4adf0ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:37:25 -0400 Subject: [PATCH 138/194] Bump github/codeql-action from 3 to 4.37.3 (#10135) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.37.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4.37.3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.37.3 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nightly-trivy-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly-trivy-scan.yml b/.github/workflows/nightly-trivy-scan.yml index be0aa4dcf..fc63b27d2 100644 --- a/.github/workflows/nightly-trivy-scan.yml +++ b/.github/workflows/nightly-trivy-scan.yml @@ -31,6 +31,6 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4.37.3 with: sarif_file: 'trivy-results.sarif' \ No newline at end of file From f011fc4ef658cd0e1c948c618f08b85c249bba04 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 27 Jul 2026 20:51:29 -0700 Subject: [PATCH 139/194] Add --default-resource-modifier-configmap server flag Add DefaultResourceModifierConfigMap field to the server Config struct and bind it as a CLI flag. When set, it references a ConfigMap name in the Velero namespace containing default resource modifier rules to apply to all restores. Follows the existing pattern used by --backup-repository-configmap and --repo-maintenance-job-configmap. Signed-off-by: Shubham Pampattiwar --- pkg/cmd/server/config/config.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/server/config/config.go b/pkg/cmd/server/config/config.go index c8080da21..08b58a1bd 100644 --- a/pkg/cmd/server/config/config.go +++ b/pkg/cmd/server/config/config.go @@ -182,6 +182,7 @@ type Config struct { ItemBlockWorkerCount int ConcurrentBackups int GlobalBackupVolumePoliciesConfigMap string + DefaultResourceModifierConfigMap string } func GetDefaultConfig() *Config { @@ -282,4 +283,10 @@ func (c *Config) BindFlags(flags *pflag.FlagSet) { c.GlobalBackupVolumePoliciesConfigMap, "The name of a ConfigMap in the Velero install namespace holding global backup volume policies that are merged into every backup. Optional.", ) + flags.StringVar( + &c.DefaultResourceModifierConfigMap, + "default-resource-modifier-configmap", + c.DefaultResourceModifierConfigMap, + "The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores. Ignored when a per-restore resource modifier is specified.", + ) } From 70e70f14e252661dff7b5fc54361c584b2715db0 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 08:41:20 -0700 Subject: [PATCH 140/194] Add SkipDefaultResourceModifier field to RestoreSpec Add *bool field following existing RestoreSpec conventions (RestorePVs, PreserveNodePorts, IncludeClusterResources). When true, the server default resource modifier is skipped for this restore. Signed-off-by: Shubham Pampattiwar --- config/crd/v1/bases/velero.io_restores.yaml | 8 ++++++++ config/crd/v1/crds/crds.go | 6 +++--- pkg/apis/velero/v1/restore_types.go | 8 ++++++++ pkg/apis/velero/v1/zz_generated.deepcopy.go | 5 +++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/config/crd/v1/bases/velero.io_restores.yaml b/config/crd/v1/bases/velero.io_restores.yaml index 89f4baff8..aa4e167af 100644 --- a/config/crd/v1/bases/velero.io_restores.yaml +++ b/config/crd/v1/bases/velero.io_restores.yaml @@ -467,6 +467,14 @@ spec: from. If specified, and BackupName is empty, Velero will restore from the most recent successful backup created from this schedule. type: string + skipDefaultResourceModifier: + description: |- + SkipDefaultResourceModifier controls whether the server-configured default + resource modifier is applied to this restore. + When true, the default modifier is skipped even if configured on the server. + Has no effect when a per-restore ResourceModifier is specified. + nullable: true + type: boolean uploaderConfig: description: UploaderConfig specifies the configuration for the restore. nullable: true diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index f309e5d4d..209c02fb2 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -30,14 +30,14 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8aTɡS\xf7\xe7\u074b!%[\x96hk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L4\xea\x11\x9dW\xd6,A4\n\xff 4\xfc\xe5\xe7O\xbf\xf8\xb9\xb2\x8b\xc3\xebٓ2\xe5\x12V\xc1\x93\xad\xd7\xe8mp\x12\xdf\xe1N\x19EʚY\x8d$JAb9\x03\x10\xc6X\x12\xbc\xed\xf9\x13@ZC\xcej\x8d\xae\xa8\xd0̟\xc2\x16\xb7A\xe9\x12]4\u07b9>\xfc0\x7f\xfd\xf3\xfc\xa7\x19\x80\x115.a+\xe4Sh\x1c6\xd6+\xb2N\xa1\x9f\x1fP\xa3\xb3seg\xbeA\xc9\xd6+gC\xb3\x84\xf3A\xd2n=\xa7\xa8\xdfFC\xeb\xce\xd01\x1ei\xe5\xe9\xb7\xec\xf1G\xe5)\x8a4:8\xa1s\x81\xc4c\xbf\xb7\x8e>\x9f\x9d\x15\xc0\"\xe9H\x99*h\xe1F\xba\xec\xdbK\xdb\xe0\x12\xa2f#$\x963\x80\xb6\b\xd1R\x01\xa2,cY\x85~p\xca\x10\xba\x95ա6'?_\xbd5\x0f\x82\xf6K\x98w\x85\x9fK\x87\xb1\xe6_T\x8d\x9eD\xddDٮ\x96o*l\xbf\xe9\xc8\xceKA86\xc6E\x9d\x9fc\xfdrl\xf0\xc2ʹF\xd0;K\x16=9e\xaa\xd9Y\xf8\xf0:\x95B\xee\xb1\x16\xcbV\xd66h\xde<|x\xfcqs\xb1\r\xd08۠#\xd5\x153\xad^g\xf6v\x01J\xf4ҩ\x86b\xdf\xfc]\\\x9c\x01\xb0\x83\xa4\x05%\xb7(z\xa0=v5Ʋ\x8d\t\xec\x0eh\xaf<\xc3\xe6УIM\xcb\xdb\u0080\xdd~EI\xf3\x81\xe9\r:6\xc3\xd8\a]rg\x1f\xd0\x118\x94\xb62\xeaϓm\x0fd\xa3S-\b=AD\xd1\b\r\a\xa1\x03~\x0f\u0094\x03˵8\x82C\xf6\t\xc1\xf4\xecE\x05?\x8c\xe3\x93u\b\xca\xec\xec\x12\xf6D\x8d_.\x16\x95\xa2n^\xa5\xad\xeb`\x14\x1d\x17q\xf4\xd46\x90u~Q\xe2\x01\xf5«\xaa\x10N\xee\x15\xa1\xa4\xe0p!\x1aU\xc4DL\x9c\xd9y]~\xe7\xda\t\xf7\x17nG@\xa7\x15\x87\xec\x0exx\xea@y\x10\xad\xa9\x94\xe2\x19\x05\xde\xe2ҭ\x7f\xdd|\x81.\x92\x84T\x02\xe5,:\xaaK\x87\x0fWS\x99\x1d\xba\xa4\xb7s\xb6\x8e6є\x8dU\x86\xe2\x87\xd4\n\r\x81\x0f\xdbZ\x11\xb7\xc1\xef\x01=1tC\xb3\xab\xc8i\xb0E\b\r\x8fN9\x14\xf8``%j\xd4+\xe1\xf1?ƊQ\xf1\x05\x83\xf0,\xb4\xfaL=\x14N\xe5\xed\x1dt,{\x05\xda!sn\x1a\x94\x8c,\x17\x97U\xd5N\xc94S;\xeb@\x8c\xe4/+\x95\xa7\x00^\x89D7d\x9d\xa8\xf0\xa3M6\x87BSm\xc7\xebm\xceP\x171\xd3V\xe2\x04\xcc\vf\f\xd2^P\x8f\fH(s\xe2\x94l\x927\x90\x89\xe8\bf\n#\x8c\xc4\xf7\xb1\x1f\x8d\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xad+\xc6͒\x89v\x1f\x8f\x8c\xf3\xd0\xcb<\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa0\x9d2\x9dD\x8b\x01X-\xd2<\xee\xc0\xec@\x91R֮\xc0\x86q \xfa\xa0\r\x14\x9e0az\xf5\xf8DzB\xa3¹\a\xa1-]=\"}\xe4E\xc59]s\xb8\"FU0@\x9b\xb5\x94\x1c\xa8\x98 \xcegІe\xe7 \x8d\x83\x14!\x8c\xf2\x1f:\x14@o\x82>\x00\xa1\x11Оf\xd6m\xe1\xbcE\xd8.U\xa2c*\x15dv:\xbb\xf2\xd3$\x03\x8eS\xb3\x90\x84K\xb1\x05\xe5z\xb7V/\b\x98\x02+p9\xb13\x90\x02n\xa7Y\xb2\xa9\xec\xe4tI\xacv\x0f\xca\x00\x13\xda\x00\x8d\b\xe7\x13\xf8\x03_\xacu\x86\xfc\xday\xa4\xb7ֱ\xce\xc3B\xa37\x9d\xa4\xf0\xe9\xfd(D\xef\xb6p\x96\xa1w\xec\x1d\xe1%:\xf411m\xbc\x17;5\xe1jò\xd2\x0f\xbbqKF\xed\x81\x06c\x1b]\xfc\xe9b\x81\x1c\xee\xf6\xda\xedC\x13\xaa\xa0&K\xb2݄\xa24\x87~mf\xa0\x88PqԞ$\xf2\x93*E\x0f\x03ܬ\x17Fg\xe4\xe7\x10\xcc#\x8e\x8aP\xed\x85yz\xdc\xef?3W\xcf\xc3G\x8d\x01\x02ʄ\xe5\x9f]\xabwا\xdd\xc2֒MH\x13\x81\xe7\xfc:\xc8q\xcd:\u00ad߉Xg\x91\xf9!!\xafe\xcb\v\xef?$\xa5vR>LQ\xe7\a[\xa7Y-\x92\f\x03Qd\r;\xbagRyԛ\xa9\x16\xbe@V\x99\xa8\xd6SCr\xb6ـ\xb2p\xca\x1dՠ]\xfc`\x98 \xc3\xeb\x1a\xd22#яGx4\x8c\xb4lḂ\x86n\xfd\x88\xe3Y2\x14;P\xeb^\xe3d\x9c\xb3=\xcb+\xcaq^\xa6\"s\xf8\xd0z\\1+3\xc2\xe4ޘ\xa3\x92\xe9\x8as\b\x02R\x96I\x9d%\xa4\x14`}\xde®\t\xfaU\x871_S\xeb\xab\xc8!\xec\t2KU\x1c\xb4\xef*G7\xb2\xb1\x19\x8b\x86)\x18\xa1!\x9c\xae\x81\x13\r\x1c2#U\x9c\"S|v%\xc5\b\x0e\x102b\xf9\xba+\x8d\x06\x81\x11\x90\x04\x97p;\x96휫g\x85\b\xe1\x90\\\x82u\xf8\f\xa1e\xc9#\xd3ESF\x99\xef;\x19\xd3\xf5\xa6Lh\xfd1\xbc\x98\xfe7%\xc1f6%J\xdaF\xbf\xba\x94\xad\xc5!\xbe\xa6m\xca?'a\x83\xe5?AhG\xb4\x9f`\xb8,Y\xa6\a\xe5\xd6R\x95\x81\xbe\xb4\xee\x14z:\v\xc2L\xf8uJ\x13:>W/\x8a\xd8!\xc2\xd7͛\xf9B\x9fȚ\x14\x9dx&\xc6\xd4]\xfc\x03\xf2\x05\xa7\x8c[?c$\xf3\xe4\xa7v\xab\x05a\x9b\x9a\xe8\xf9\x82l\x187\xa0\x8e\xa8\x7f\x92\xa9\x0f\x9c9\a1Rf=\x82\xfb\x1a&۽\xffb]0\xddl\xee%\xd2帱sd\x83\xb7ߝ\x9e'\xe0\x12\x8c\xef3\x17m\u0557\xb8bj\xff\x82\xae\xd5ۏ\xef\xe2\xeb\xabvI\x90\xbc\x1e\"\x13J\xe7\xca\xdb#\x8c\xda\xe3\xf3.|\xf8\x82>P\xbd\x00r\xb1\xea\x05\xa1\xe4\x01\x0e\xceu\xa1\x82X\xfe\xd0P9\xa1{\x05\xb8Y\x85r\xf6\x00\a\x04\x13\xdf}\xea\x97Tip\xe5\x01\x0e)Վhh\xc7Ĵ\xdfU\xb3t\xb2? !p\xd3!U\f\\\xf1\xaa\x10\xd9뉗D[\x12J\xa0\xfd\th&\x89J\xbb\x8f\xf6\xf6-J\xc0w\xda\xf1\xd2j̎\x95hV1\xe2 7\xc9\fu\xe5\x9er\x96\xd7\x1d9\x1dY\x89\x05\xf9(\x8d\xfd\xe7\xfd\x17\xa6\xfd\x0e\xef;\t\xfa\xa34\xf8˳P\xd4\r\xfc9\xe9\x19v|,B\xce\xca[\x82\xb5\xf7(ݜf\xa5\xad\xa6=\xd3d%\xecrő$\xb1+\u070evݹ\x8e\x8aJ\xe3\xf6\xa2\x90b\xe9\xc26\xb1\x9e<\xbd\xa5\xea\x90\xfbɝ\xfa\x0e\xef\xecdᾸMqN3\xc8\xc3v\r\xee\xd6R\x03[\x96%\xf6W\x80\xda\x02)\xad\tO\x93\x88D\xc3걙'>i\xb3w\xbb|Y>\xd4\xc9\x0fK;\xe5,=\x04#\x8b\x04\x1ax\u06ddO㳴:\x9bP+H\xc2dՁ\xcd\xdc\xe1\xaa)Dy\x029p\x16G\x17g\x92\xbb4\xcf15\x88\xf2\x9b\x193\xca\fY\x98k\x1aZcwSpAq\xab\xe5\x7f\xecL\x8b\xda\xf4\x7f\xa4\xa4L\xe9K\xf2\x16s}8t\xbe\xf9\xa0Y\vLB\x97\x98\xabc\xe5gO\xb9\x9d\xfb\xad\x01\x17\x04\xb8\xf3\x04\xe4\xa6\xe7\x17-\xc8\xe3Nj7mכ8\x17\x0fpp;\x86\x93]\xb6\x8d\xcc\xc5J\\8\x1f\xa2g0j\x87C\n~ \x17\xf8\xed\xe2)\xaeT\xa2\xa4&V\xeb\x88hA\xcb4\t\xc5\\\xabTG\xdd.X\x83\x13b\x1b\xd69D\xd6\xc9\x1e\xc36IDK\xa9#\x1b\xf9\x03C\x99\x10\xde\x1b\xa9\x8d\x8b\x97u|\xe6h@M\x86 \x1a\xa1\x1b\x97\xd8%U\xc8±Fy*\xf4\xdb.w;\xd0\xe0\xf7+|`\xce\x01\xb5+\xbb\x8bF\xbf\x9d\xb5\xbfp\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xe6\x8bH\x96H\x1b\xf7:\xe6H\xdd*\xc9媌\x87@CIwy-!f\xae\x17\xde\x7fi\x05D\xad\xeeۿ\xa7dl\xee\xb8\bfY\x16\x05=\xce\xdfJ\x1a\xe2\xb5k\x19\xb4\xc1\x03r\x8b\x0f\xb5\xad\xd0\x12\xa4\xce\xe5\xb5\x00~\r\x8eB\xc1\xc4\n; o\x9e\xc1\xb1\xf064\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fN\x95K\x89[\x05\n:\xcc\xebG\xd5\xd1\x0f\x15Ҵ\x02\x123\xdc\xcdR\xe6\xdfi\xb2aJ\x9b\xf6\x10\xf4@\x9aJ\x14\xcc̅\x97x\xaf\xd4I\xeb\xaeO\xae\xe5Q\x02\x99\xcf[s\x84I\xc4\x1c\xf7\x97\x80\xb0\ra\x86\x80\xc8d%0\x80c\xf5\x18\xbbp\xc4u\x16\x96\xa5*I\x9a\xf6\x93\xc1\x1c\xb4XY\xa2\xa401\x1a\xe9iW\xff@Y?Q-Vf\xb2\xcd\fe\xb1\xc5\xcai:\x11R\xdcڙ\x8a\x05\xfd\u008a\xaa \xb4\xb0<\xc2ɜ\x15\xd0ez\x93\xf8f[\xe04a\xa4\u0558\x92\x83\x01\x9f\xbc\x968\x86L\n\xcdr\xa8'W/\bR\x10J6\x94\xf1J%Z\xc0Y䝳\x14\xf1\x96\xe0|k\x8c\xb4ΗH\x8a\x84hn\xa2\xaf8n\x8dK\x95\xee\xf1M\xb9Y\n\xe6{Y\xa5b\x12\xd3\x02\xcf\xech\xf9DJ*\x0e\xdf<\xadԡ~\xf3\xb4\xc6\xca7Ok\xa2|\xf3\xb4\xbeyZ)5\xbfyZ\xdf<\xadv\xf9\x97\xf0\xb4\xa6F\xe4\x0e:\x0e|\x9c\x1cE\xc2V\xf5\xd8\x10G\xe0\xfb\xe4\n\x9f\x03\xfe\xa4\\\xccU\x1cT$\xf1\x7f \xad;f\xb4\x9aɣNδZ\x13dޝ\xbb\x9ap%\x9f\x90u\x1f:=_\xd6\xfdj\x14♲\xee\xfd\xb0\xa7}\xec\x93r\xee\x03Q\xe6eg/|\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x99\xe8\xff\x85\x13s{Ycg\x94\x8fg\xcf\xe2O\x96\x91(K/\xfet\xf1\xf5\x91\xff<\x04\x1f$q\x9fv\xfe\xe0w\x04\xaa]\x81\xb6\xd3ºYx_\xa7\x18\x9fEnS3\xf1k\"F`uE\xf2\x88\x8a_\xab-0P|*\xfd\x8c\U001044ea\xab\b\x9c\xa4\xb3\xaaT\x1fD\xb6SR\xc8J\xfb\xa8\x84\x85\xf56s'\xfd\x03Ș\xb0F5\xfc?\xc8NV\x91L\xf0\x11\xf2Md\x04N#\xdfI\x0e\xf4\x9b\xd0`\xe8\xfe\xcde\xf7\x8b\x91>Up\xe8l\xf3\xe3\x0e\x04\uec0bm\xfb\x00@\xb8\xa8\xc1\xdfXp,`\x11@R\x11\xc1\xb8\x93\xbc\xfa\x9a\x87\xb6ܑO\xa5\x8b=\xcd\xf6;\xc6c*iɄ'\xa7\x10vS\x04\a\xfcҹ\xbb\xddg92\xf1\xbb\xa4\x06\xceO\bL\x89\x88M$\xff\x9d\x90\xf2\x97\x98[\xfc\xe4\xed\xf9\x94\xa4\xbe9+\xe6gK\xe0;\x7f\xda^\x12}\xa6S\xf4\xe6P\xe7\xd9\xd3\xf1^0\t\xefeR\xef\x12\x13\xeeΗ9\x9f\x16\x8f=)sl:t0\x9c47\x99*7\x19Z\x98Bl6J\x93)ps\x12\xdf&\xb9\x93\xa6f/\x96\xda\xf6b\tm/\x9b\xc66*E\xa3\x1f\xe7$\xaa\xc5\xef\xeb!\x93\x93m\xef>\xb5^\x85sJ\\r,nP\xe9\xf8K)ǩl\x93\xaa\xe3n\x9f\xb4\x1e\xfct\x04\xc3\njpE_ȧ/*nX\xc9q\xe3w\xcf\xf2hp\xc4\xec\xe0P_\xf8\xf1\xabģ\xb2\xfe\xe6\x9aO\x9fk-\xbb\xb8\xf4\x9b\xe8\xd4\xfc\x9ef\xbb#\xe8;\xaa\xc9F\xaa\x82\x1arQoX\xbev\xc0\xed\xdf\x17\x97\x84|\x90u\x0eG\xfb\x1e!͊\x92\x1f\xec\n\x85\\\xb4\x1b\x9c&\x01Qi\v\xbd\xddHβ\x88\xef\x16\xbdK\xcaU\xee]\xee\x817\\e\xed\x14\x87\xd2V\x8c\xbbn\xe8\xe6u\xaf\xec\xdcH\xce\xe5\xe3\xdcXE\xc9\xfe\x82\x97\xb3?!\x9a\xf5\xf6f\x850\x82x\xe0m\xefu2Y\x8d\xcd\x1a\xec\xb4\xdc\xe09\xa4\xfb\xabM\ab7/\xb3}\xcb1\xe4\xeeB\xeb\xe0\x16xәIk]nVn\x1cC\xbdX\x99\xa1\xe2@$f\x00\x99\x1dS\xf9\xb2\xa4\xca\x1c\\bɢ3\x860\x97\x8eE\xa3\x06g\x8f\xfe%\xddQ\U00086ef9qG\xf5Pv7\xa9\x8fiw\xca8\x86O[N\x9e\xb3<\xe38\x86ݒ%R*\xf2s4S\xedlQ>\xedoR\xfeY\xee\xe1]4\xda\xd7!\xcf\xedQ\xf5H:Y\x80\xe8.\t\x1e̪]\x03^ \xdc\xff\xf4\x84\xfc\xb0е\xbf\x03\xf6\x94@\xd9m\x17D\x04\xbfp#n\xe8,f\x9f\xf0&\xff\x03\xb9\xb9\xc75Zmڼ\x8a\xfa5Z\b\x95\x85\xcd\xeb\b\x1c\xdf\xe0\xfb\xf3\xa7\xd2i#\x15\xdd\xc2O\xd2]\x96>\xc5\xf6n\xed\xce%\xfa\xde\xeb\t\xf9\xaeAib\x17\x06\xfbkۏ\x8059\xea\xbdK\x98\xed(g^+m\f?\x85\xefww?9\xac\f+\xe0\xf2]\xe5\xd23\xacM\xd4`I\x1c\xb0u\x90\xd6\xf6\xbf;\xf9\x88\x97\x15\xc7\xe3\x98\xe1\xf1\x8b\x06\x19\x05\x98\x1c\x8f)\x93\xb3P\xaaJ.i\x0e\xeaZ\x8a\r\xdbN`\xf7K\xa7\xf2\xd14\x9b\xe1\x8f\x1e\xb9z\x8e\n\xf0Ϝ3a}\x1e\u0381\x7f`\x1c\xb4\x1bV\x82\x01\xbe鷪\xedqU\xac\x9d\x0f\xb7\xb1\x1f\xeb\x0e\x06\xe68\x87\x16\x86\xa2KP\u058brA\xebJ\aY\x1dF\xbc\xe1\b\x13\x06\xb6\xd0_\x05\x8eX`w\v6N\x9f\xc1\x9c\xe0Z\xe6\xc7X|\xab\x83\xfc\xfdp\xcb#N\xb6B^\xb1\x1b\x02\x9d\x13rs\x7f\xadI%r\f\x17\xdf\xff\xe5v\x96\xd4\xed;7\xed\am\x9d2\xaa\xf7\xf1V-\xe7\xb8e/\x9cw,7\x11\x04\x86\xe0\xb4\x1etyd\xc6_4vޛa\x87\x96ڡ\xf1%\xa3\xeft\r\x13s[\xf1\xfd\x95>\x11\xfaί\x8bV\\Y\xef\x1f\x96\x16\xc4i^\xeb\xd0\xeb3\xddy\xe1iF\xee\xfav5\x04\xee\x14\x13\xd7\x7f\x9e\xe6\x89j\xdcG\xf7I&\xad\x8f\xee,\x83\x16\x81X\xcb\xf8\xf9qGU?\xed\x12zl\xe9\x1c\x8e,\x9c\xf9\xa3\x9c\xfb\x83\x99\x05hM\xb7\xe1\xf6\xf9G\xbb\xf4\u0602\x00\x17\x9es\x9b'\x11\xa0\xcd)\xbe\xee\xdd\xebNehf*\xea;\b\tɭZ\xdfi\xc2e\f*>@\xc3\u0093oaM6\x93P_J\xa6R\xd6p\xef늖6\xe8\t#w\x9aG\xfa\x80\xb3->Ae9\xb7\xa5jM\xb7\xb0\xcc$\xe7\x80ֺ?\xae\xe7\xd4u\x7fV\xf23P=\x89ڇv]\xbf\x03\xe8\xb8\xed6\xbe\xa9K\xcf\xc7g\xd8\fSм\x88\xd8\x1b\x90Ďg9ʎ\n\xd1\xe7\x02\xfb#m\xd7\rZ\xe7Ͳ\x8f\xf3\xfa\xd7\x02\x17\xcd\v`\x91q\x16\xf4W\xa9\x16\xa4`\xc2\xfeCE\xee6\xf0B\xe3Y\xe3\xdfI\xf9p\x1bqb{\x83\xff\xa1\xae\xd8lu0ᆍ\a\\ײ\xf2\xbb\xef\xb5C\x1b\xdfV\xc1\x97\x04μ\xdcD\x98#\xf3A\x0f\x9d\xc1\x88\xee\x0f\x1dH\x93S\x81\xeby\x00\xd6mx\x92\x8e\xf3\xc3\xe2\x18\xf2\xd1\xf3\x97\r\xec\xd6K\v\xde\rh\xeeO\x18\xe8(\xecHE\x81\xd4\x17u\xb4\r\xfa)\xab^O\xe6!g\xb2G\xe3\x1f\x9a\xdaCtt\xc3l\xb9{\x03\bv\x9c\xc0\xf3.\xd8\xf1Y\x8d\t\u1ff1u\xea\xbb\x16Z\v\xb7\x90%6\x18\xa5\x1bz\x99\xef#\xf4\xb7+\x96\xe4\xaf\x15T\x11\x1a,\xc3Cv\xb7\x86\xaa~\xc8\xd7\x1dۇ\x1c3:P\x1b#UV\xe2Fɭ\x02\xdd\x17\xd6%\xf9\x1be\x86\x89\xed\a\xa9nx\xb5e\xe2\xd3\xf0\x11\xa5\xb1\xca7T\x19f\x85ݍ'6P&(g\x7f\x8fٵ\xf6\xc7i@׃\v\xac%I\x18\xc6Їw`}\xdc\xc1\xb8@Ԅ\x96\x9e\xae\xa7\xf8+\x81'S6\xb5\xf6%\x1a_$t{I>ʨa\xf0\xe9P\xac\vӺd\xa0\xcd\x126\x1b\xa9\x8cۭ^.\tۄ\xe0\x83\xb59\x187s\x8f\x8e\x12\x16\xdbf\xae\x13M\x9a\xe9\v\x83\xde\nga\xbcz\xbf\xa0\a\xb73E\xb3\xac\xb2\x1e\xd6km(\x8f88O2\xfc\x18\xe5\xf9\x1e\x1f\xd8\xfc\xe5I;y\xab6\xa0~\xd0\x11\xfbq$\xc5\xcb?\x9c\xd7\xc7-\x8a ȣb\xc6X\x9fJ\x8e\xa4\x12xR\x19\xeb[qN\xb4%\xf5I\xd1G\xe2\xcc\xe8j8%'\r\xe5\xbb\x1aʐy\xf6X\xe3K\x92\xf5+\xa6>\xfb\xc8ײl\xcevTl\aoT\xd8)YmwA\x92\a\x9ci\x92W\x80\xc1Z4):\xbc\x10m*%Z\xa9\x04#\xc7\xd4I\x10\x06\x1c.\xcd\x1e\xf0\xbdU\xf7\x02\xb3\x7fz\xfb\xb5\x7f\xb3e\xb9Q\xb2X\xfa~1\x96\xba\xf0;\xf9\x8aI빘]\x94\xea\xc4y\xed\xfeY\x04\x94\x84\xb2\x04A\xa8\xf6='\xdclu\xf24\xf5\x9b\x9d\x1an\xa4f\t\xde~\x94\xe3\x7fm\x03\b\f/\xc3\xdf]f\xf8\x15\f\xf6\x19\xc3㓿2\x00\xf6T\x18\xb7\x9c\xa8\xa7\xc8\v7\x89]\xccZ\xc8h;\xb1=)Hsہ0\x11\x9f\xc1\xee\xe2,\xba\xf5\xe9\x1a\xee\xe2\xb2k\xff\\l\rxA4\x13\xe1\x05s\x97\xfa\xe1\xa4?\xba\x13(\xf0aM\xa9\xe2٘\xe3\x01\x97.B/\x1bk\xd9מ\xc4\xfb\x93\x97\xe2\xf7G0\x8e\x0e\xa1\xe3;\xaau\x95\xb0|\xfe\x03\x8b\xed\a`\x1aofQ\xf9\xe3\xef~\xb8|\x9f\xb4ԋSdl凋\xba\xe1%\\\xf7\xdd\xd4\x1b\x0eV\xdb4@wQ9K\xe7\xf6g\x8c\xa6\x9d3\x94\x16\xde\xea?O,i\x7f\xc6 ڳE\xd0\u038b\xf2#\xc5\a\xadO\xd2ڿ\xf9\xb6\x91\x10\x9a\a{\xee Z+\x86\x16\x06\xfe\xa2Q\xb4\xe8\x9c\xdb\xfb\x11\xedt\u07b2\x16\xbe'\xff\xcb\xff\a\x00\x00\xff\xff\x11\r8\xff\x9b\x84\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfe\x15\x84\xeeav7\xba\xe5u\xdcG\\\xe8\xcd#\xdb;\x1d3ck-\x8d\xf6\x99\xae\xca\xeefDA\rP-\xf7\xde\xdd\x7f\xbf \x81\xfa袪\xa8VK\xe3\xdd5/\xb6\xba !?I\x92\x04\x96\xcb\xe5+Z\xb2{P\x9aIqEh\xc9\xe0\x8b\x01a\xffҗ\x0f\xff\xad/\x99|\xbd\x7f\xf3ꁉ\xfc\x8a\\W\xda\xc8\xe23hY\xa9\f\xde\xc1\x86\tf\x98\x14\xaf\n04\xa7\x86^\xbd\"\x84\n!\r\xb5?k\xfb'!\x99\x14FI\xceA-\xb7 .\x1f\xaa5\xac+\xc6sP\b\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xad+\xc6͒\x89v\x1f\x8f\x8c\xf3\xd0\xcb<\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa0\x9d2\x9dD\x8b\x01X-\xd2<\xee\xc0\xec@\x91R֮\xc0\x86q \xfa\xa0\r\x14\x9e0az\xf5\xf8DzB\xa3¹\a\xa1-]=\"}\xe4E\xc59]s\xb8\"FU0@\x9b\xb5\x94\x1c\xa8\x98 \xcegІe\xe7 \x8d\x83\x14!\x8c\xf2\x1f:\x14@o\x82>\x00\xa1\x11Оf\xd6m\xe1\xbcE\xd8.U\xa2c*\x15dv:\xbb\xf2\xd3$\x03\x8eS\xb3\x90\x84K\xb1\x05\xe5z\xb7V/\b\x98\x02+p9\xb13\x90\x02n\xa7Y\xb2\xa9\xec\xe4tI\xacv\x0f\xca\x00\x13\xda\x00\x8d\b\xe7\x13\xf8\x03_\xacu\x86\xfc\xday\xa4\xb7ֱ\xce\xc3B\xa37\x9d\xa4\xf0\xe9\xfd(D\xef\xb6p\x96\xa1w\xec\x1d\xe1%:\xf411m\xbc\x17;5\xe1jò\xd2\x0f\xbbqKF\xed\x81\x06c\x1b]\xfc\xe9b\x81\x1c\xee\xf6\xda\xedC\x13\xaa\xa0&K\xb2݄\xa24\x87~mf\xa0\x88PqԞ$\xf2\x93*E\x0f\x03ܬ\x17Fg\xe4\xe7\x10\xcc#\x8e\x8aP\xed\x85yz\xdc\xef?3W\xcf\xc3G\x8d\x01\x02ʄ\xe5\x9f]\xabwا\xdd\xc2֒MH\x13\x81\xe7\xfc:\xc8q\xcd:\u00ad߉Xg\x91\xf9!!\xafe\xcb\v\xef?$\xa5vR>LQ\xe7\a[\xa7Y-\x92\f\x03Qd\r;\xbagRyԛ\xa9\x16\xbe@V\x99\xa8\xd6SCr\xb6ـ\xb2p\xca\x1dՠ]\xfc`\x98 \xc3\xeb\x1a\xd22#яGx4\x8c\xb4lḂ\x86n\xfd\x88\xe3Y2\x14;P\xeb^\xe3d\x9c\xb3=\xcb+\xcaq^\xa6\"s\xf8\xd0z\\1+3\xc2\xe4ޘ\xa3\x92\xe9\x8as\b\x02R\x96I\x9d%\xa4\x14`}\xde®\t\xfaU\x871_S\xeb\xab\xc8!\xec\t2KU\x1c\xb4\xef*G7\xb2\xb1\x19\x8b\x86)\x18\xa1!\x9c\xae\x81\x13\r\x1c2#U\x9c\"S|v%\xc5\b\x0e\x102b\xf9\xba+\x8d\x06\x81\x11\x90\x04\x97p;\x96휫g\x85\b\xe1\x90\\\x82u\xf8\f\xa1e\xc9#\xd3ESF\x99\xef;\x19\xd3\xf5\xa6Lh\xfd1\xbc\x98\xfe7%\xc1f6%J\xdaF\xbf\xba\x94\xad\xc5!\xbe\xa6m\xca?'a\x83\xe5?AhG\xb4\x9f`\xb8,Y\xa6\a\xe5\xd6R\x95\x81\xbe\xb4\xee\x14z:\v\xc2L\xf8uJ\x13:>W/\x8a\xd8!\xc2\xd7͛\xf9B\x9fȚ\x14\x9dx&\xc6\xd4]\xfc\x03\xf2\x05\xa7\x8c[?c$\xf3\xe4\xa7v\xab\x05a\x9b\x9a\xe8\xf9\x82l\x187\xa0\x8e\xa8\x7f\x92\xa9\x0f\x9c9\a1Rf=\x82\xfb\x1a&۽\xffb]0\xddl\xee%\xd2帱sd\x83\xb7ߝ\x9e'\xe0\x12\x8c\xef3\x17m\u0557\xb8bj\xff\x82\xae\xd5ۏ\xef\xe2\xeb\xabvI\x90\xbc\x1e\"\x13J\xe7\xca\xdb#\x8c\xda\xe3\xf3.|\xf8\x82>P\xbd\x00r\xb1\xea\x05\xa1\xe4\x01\x0e\xceu\xa1\x82X\xfe\xd0P9\xa1{\x05\xb8Y\x85r\xf6\x00\a\x04\x13\xdf}\xea\x97Tip\xe5\x01\x0e)Վhh\xc7Ĵ\xdfU\xb3t\xb2? !p\xd3!U\f\\\xf1\xaa\x10\xd9뉗D[\x12J\xa0\xfd\th&\x89J\xbb\x8f\xf6\xf6-J\xc0w\xda\xf1\xd2j̎\x95hV1\xe2 7\xc9\fu\xe5\x9er\x96\xd7\x1d9\x1dY\x89\x05\xf9(\x8d\xfd\xe7\xfd\x17\xa6\xfd\x0e\xef;\t\xfa\xa34\xf8˳P\xd4\r\xfc9\xe9\x19v|,B\xce\xca[\x82\xb5\xf7(ݜf\xa5\xad\xa6=\xd3d%\xecrő$\xb1+\u070evݹ\x8e\x8aJ\xe3\xf6\xa2\x90b\xe9\xc26\xb1\x9e<\xbd\xa5\xea\x90\xfbɝ\xfa\x0e\xef\xecdᾸMqN3\xc8\xc3v\r\xee\xd6R\x03[\x96%\xf6W\x80\xda\x02)\xad\tO\x93\x88D\xc3걙'>i\xb3w\xbb|Y>\xd4\xc9\x0fK;\xe5,=\x04#\x8b\x04\x1ax\u06ddO㳴:\x9bP+H\xc2dՁ\xcd\xdc\xe1\xaa)Dy\x029p\x16G\x17g\x92\xbb4\xcf15\x88\xf2\x9b\x193\xca\fY\x98k\x1aZcwSpAq\xab\xe5\x7f\xecL\x8b\xda\xf4\x7f\xa4\xa4L\xe9K\xf2\x16s}8t\xbe\xf9\xa0Y\vLB\x97\x98\xabc\xe5gO\xb9\x9d\xfb\xad\x01\x17\x04\xb8\xf3\x04\xe4\xa6\xe7\x17-\xc8\xe3Nj7mכ8\x17\x0fpp;\x86\x93]\xb6\x8d\xcc\xc5J\\8\x1f\xa2g0j\x87C\n~ \x17\xf8\xed\xe2)\xaeT\xa2\xa4&V\xeb\x88hA\xcb4\t\xc5\\\xabTG\xdd.X\x83\x13b\x1b\xd69D\xd6\xc9\x1e\xc36IDK\xa9#\x1b\xf9\x03C\x99\x10\xde\x1b\xa9\x8d\x8b\x97u|\xe6h@M\x86 \x1a\xa1\x1b\x97\xd8%U\xc8±Fy*\xf4\xdb.w;\xd0\xe0\xf7+|`\xce\x01\xb5+\xbb\x8bF\xbf\x9d\xb5\xbfp\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xe6\x8bH\x96H\x1b\xf7:\xe6H\xdd*\xc9媌\x87@CIwy-!f\xae\x17\xde\x7fi\x05D\xad\xeeۿ\xa7dl\xee\xb8\bfY\x16\x05=\xce\xdfJ\x1a\xe2\xb5k\x19\xb4\xc1\x03r\x8b\x0f\xb5\xad\xd0\x12\xa4\xce\xe5\xb5\x00~\r\x8eB\xc1\xc4\n; o\x9e\xc1\xb1\xf064\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fN\x95K\x89[\x05\n:\xcc\xebG\xd5\xd1\x0f\x15Ҵ\x02\x123\xdc\xcdR\xe6\xdfi\xb2aJ\x9b\xf6\x10\xf4@\x9aJ\x14\xcc̅\x97x\xaf\xd4I\xeb\xaeO\xae\xe5Q\x02\x99\xcf[s\x84I\xc4\x1c\xf7\x97\x80\xb0\ra\x86\x80\xc8d%0\x80c\xf5\x18\xbbp\xc4u\x16\x96\xa5*I\x9a\xf6\x93\xc1\x1c\xb4XY\xa2\xa401\x1a\xe9iW\xff@Y?Q-Vf\xb2\xcd\fe\xb1\xc5\xcai:\x11R\xdcڙ\x8a\x05\xfd\u008a\xaa \xb4\xb0<\xc2ɜ\x15\xd0ez\x93\xf8f[\xe04a\xa4\u0558\x92\x83\x01\x9f\xbc\x968\x86L\n\xcdr\xa8'W/\bR\x10J6\x94\xf1J%Z\xc0Y䝳\x14\xf1\x96\xe0|k\x8c\xb4ΗH\x8a\x84hn\xa2\xaf8n\x8dK\x95\xee\xf1M\xb9Y\n\xe6{Y\xa5b\x12\xd3\x02\xcf\xech\xf9DJ*\x0e\xdf<\xadԡ~\xf3\xb4\xc6\xca7Ok\xa2|\xf3\xb4\xbeyZ)5\xbfyZ\xdf<\xadv\xf9\x97\xf0\xb4\xa6F\xe4\x0e:\x0e|\x9c\x1cE\xc2V\xf5\xd8\x10G\xe0\xfb\xe4\n\x9f\x03\xfe\xa4\\\xccU\x1cT$\xf1\x7f \xad;f\xb4\x9aɣNδZ\x13dޝ\xbb\x9ap%\x9f\x90u\x1f:=_\xd6\xfdj\x14♲\xee\xfd\xb0\xa7}\xec\x93r\xee\x03Q\xe6eg/|\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x99\xe8\xff\x85\x13s{Ycg\x94\x8fg\xcf\xe2O\x96\x91(K/\xfet\xf1\xf5\x91\xff<\x04\x1f$q\x9fv\xfe\xe0w\x04\xaa]\x81\xb6\xd3ºYx_\xa7\x18\x9fEnS3\xf1k\"F`uE\xf2\x88\x8a_\xab-0P|*\xfd\x8c\U001044ea\xab\b\x9c\xa4\xb3\xaaT\x1fD\xb6SR\xc8J\xfb\xa8\x84\x85\xf56s'\xfd\x03Ș\xb0F5\xfc?\xc8NV\x91L\xf0\x11\xf2Md\x04N#\xdfI\x0e\xf4\x9b\xd0`\xe8\xfe\xcde\xf7\x8b\x91>Up\xe8l\xf3\xe3\x0e\x04\uec0bm\xfb\x00@\xb8\xa8\xc1\xdfXp,`\x11@R\x11\xc1\xb8\x93\xbc\xfa\x9a\x87\xb6ܑO\xa5\x8b=\xcd\xf6;\xc6c*iɄ'\xa7\x10vS\x04\a\xfcҹ\xbb\xddg92\xf1\xbb\xa4\x06\xceO\bL\x89\x88M$\xff\x9d\x90\xf2\x97\x98[\xfc\xe4\xed\xf9\x94\xa4\xbe9+\xe6gK\xe0;\x7f\xda^\x12}\xa6S\xf4\xe6P\xe7\xd9\xd3\xf1^0\t\xefeR\xef\x12\x13\xeeΗ9\x9f\x16\x8f=)sl:t0\x9c47\x99*7\x19Z\x98Bl6J\x93)ps\x12\xdf&\xb9\x93\xa6f/\x96\xda\xf6b\tm/\x9b\xc66*E\xa3\x1f\xe7$\xaa\xc5\xef\xeb!\x93\x93-\x7f)a;\x95\fRu\xdcד\xd6W\x9f\x8e`X\xc6\a\xd7\xee\x85|\xe4\xa2↕\x1c7R\xf7,\x8f\x06\x1b\xcc\x0e\x0e\xf5\x05\x1a\xbfJ\xf53!Y\x13\x9f\xe7\x9d\xee9y\xcbB\xaa\x1c\xd4\xe8\xb6O\xaa\x14\x8e\xca_\xcaڦ;\x90\xa3\xfd\x8ep럭\xd5\xf1\x97qz\xf07\xb0\xe2]\xbbCۗV\xd2Z\xdeFg/\xaaq\x7f\xbaΤ\xbf\x80\xd7mWi(\xa9\xc2K\x9d\xd7\a\x97\xce\x12\x9d\x9a\xdf\xd3lw\x04}G5\xd9HUPC.\xea\r\xc0\xd7\x0e\xb8\xfd\xfb⒐\x0f\xb2Ήh\xdfˣYQ\xf2\x83]\xa1\x90\x8bv\x83\xd3$ *m\xa1\xb7\x1b\xc9Y\x16\xf1ݢw3\xb9ʽ\xcb2\xf0ƨ\xac\x9d2Pڊq\xd7\rݼ\xee\x15\x98\x1bɹ|\x9c\xb9\xf6\xa7%\xfb\v^v\xfe\x84\xe8\xd0ۛ\x15\xc2\b⁷\xa7\xd7\xc9Y56k\xb0\xd3r\x83\xe7\x90\xee\xaf6\x1d\x88\xdd<\xc7\xf6\xad\xc1\x90\xbb\v\xa2\x83[\xe0Mg&\xadu\xb9Y\xb9q\f\xf5be\x86\x8a\x03\x91\x98QcvL\xe5˒*sp\x89\x1a\x8b\xce\x18\xc2\\:\x16\xdd\x19\x9c=\xfa\x97^G\xc9\x1b\xee\xba\xc6\x1d\xcaC\xd9\xdd\xf4=\xa6\xdd)\xe3\x18>\xbd8yn\xf1\x8c\xe3\x18vK\x96H\xa9\xc8\xcf\xd1̯\xb3Eʹ\xbf\x99\xf8g\xb9\x87w\xd1\xe8Y\x87<\xb7G\xd5#\xe9Y\x01\xa2\xbbtw0Ku\rx!o\xff\xd3\x13\xf2\xadB\xd7\xfeN\xd5S\x02e\xb7]\x10\x11\xfc\xc2\r\xb3\xa1\xb3\x98}\u009b\xf1\x0f\xe4\xe6\x1e\xd7h\xb5i\xf3*\xea\xd7h!T\x166\x83#p|\x83\xefϟ\x9a\xa6\x8dTt\v?Iw\xf9\xf8\x14ۻ\xb5;\x97\xd2{\xaf'\xe4\x8f\x06\xa5\x89]\xc0\xeb\xafA?\x02\xd6\xe4|\xf7.5\xb6\xa3\x9cyM\xb31\xfc\x14\xbe\xdf\xdd\xfd\xe4\xb02\xac\x80\xcbw\x95Kw\xb06Q\x83%q\xc0\xd6AZ\xdb\xff\xee\xe4#^\xfe\x1b\x8fc\x86\xc7$\x1ad\x14`\xb29\xa6 \xceB\xa9*\xb9\xa49\xa8k)6l;\x81\xdd/\x9d\xcaG\xd3l\x86?z\xe4\xea9*\xc0?s\x0e\x82\xf5y8\a\xfe\x81q\xd0nX\t\x06\xf8\xa6ߪ\xb6\xc7U\xb1v>\xdc\xc6~\xac;\x18\x98\xe3\x1cZ\x18\x8a.AY/\xca\x05\xad+\x1ddu\x18\xf1\x86#L\x18\xd8B\x7f\x158b\x81ݭ\xd28}\x06s\x82k\x99\x1fc\xf1\xad\x0e\xf2\xf7\xc3-\x8f8\xd9\ny\xc5n\xdcsN\xc8\xcd\xfd\xb5&\x95\xc81\\|\xff\x97\xdbYR\xb7\xef\xdc\\\x1f\xb4uʨ\xde\xc7[\xb5\x9c㖽pޱ\xdcD\x10\x18\x82\xd3z \xe5\x91\x19\x7fq\xd7yoZ\x1dZ\xf2\f=\xfd\x80W\xfaO?\xfe\xe0n\xfe\xf7O\xc6xu\xac\x14^\x93\xea_\x05\xc0kE\x9f\xf0\xfeC'\xf9K\xbf5\x06\x8a\xd2\xc4|\x8dis\xf8\xfd\x18\xc0\xdaO\x93\x86\xf2\x96V\xd2P!\xe6i\xeb\x83\xc8\xc6\x12˼5\x1a\xe1\xe6\x98>\xc6\bp\xed\xcfC\x9c\x8d\x005\xc0!\x02\xe8*\xcb@\xebM\xc5\xf9\xa1>\x8e\xf1\x95P\xe3\x03e\xfc|\xa4p\xd0\x06\x05\xc1\xa27\ni\x12a\x9f\xee\r\"\x0f\x9a\x1e\x8e*\xcd#\x85\xe7\x82φԆ\x16'=\xd8p\xdd\a\x83o\x19\xa9\xbc\x95TI\xeb\xb1Sݰ?6\xb94\xe0\\K\\dYh\x90\x13\u0603 vvv$\x0e\xcfẗ́\xe2O\xb8\xba\x19.\xccw!\x14\x12}\xb1\x89\xf8h\x87Ɨ\x81\xbe\xd35L\xcc\x15\xc5\xf7L\xfaD\xe8;\xbf.Zqe\xbd\x7fXZ\x10\xa7y\xadC\xaf\xb9t照\x19\xb9\xeb\xdb\xd5\x10\xb8SL\\\xff\xb9\x97'\xaaq\x1f\xdd'\x99\xb4>\xba\xb3\fZ\x04b-\xe3\xe7\xc7\x1dU\xfd\xb4Kݱ\xa5s8\xb2p\x86\x8er\xee\x0f:\x16\xa05݆\xdb\xdc\x1f\xed\xd2c\v\x02\\x\xcem\x9eD\x806\xa7\xe2\xbaw\x99;\x95\xa1\x99\xa9\xa8\xef $\xf8\xb6j}\xa7\t\x971\xa8\xf8\xa0\v\vO\xa8\x855\xd9LB})\x99JYý\xaf+Zڠ'\x8c\xdci\x1e\xbd\x03ζ\xf8\xa4\x93\xe5ܖ\xaa5\xdd\xc22\x93\x9c\x03Z\xeb\xfe\xb8\x9eS\xd7\xfd\xd9\xc3\xcf@\xf5$j\x1f\xdau\xfd\x0e\xa0\xe3\xb6\xdb\xf8\xa6.\xdd\x1d\x9f53LA\xf3\xc2`o@\x12;\x9e\xe5(;*D\x9f\xdf돴]7h\x9d7\xcb>\xce\xeb_\xdf[4/jE\xc6Y\xd0_\xa5Z\x90\x82\t\xfb\x0f\x15\xb9\xdb\xc0\v\x8dg\x8d\x7f'\xe5\xc3mĉ\xed\r\xfe\x87\xbab\xb3\xd5\xc1\x84\x1b6\x1e\x18]\xcb\xca\xef\xbe\xd7\x0em|[\x05o\xe6?\xf3r\x13a\x8e\xcc\a=t\x06#\xba?t MN\x05\xae\xe7\x01X\xb7\xe1\x897\xce\x0f\x8bc\xc8G\xcfI6\xb0[/\x17x7\xa0\xb9\x8f`\xa0\xa3\xb0#\x15\x05R_|\xd16觬z=\x99\x87\x9c\xc9\x1e\x8d\x7fhj\x0f\xd1\xd1\r\xb3\xe5\xee\r \xd8q\x02ϻ`\xc7g*&\x84\xff\xc6֩\xef.h-\xdcB\x96\xd8`\x94n襻\x8f\xd0߮X\x92\xbfVPEh\xb0\f\x0f\xc3\xdd\x1a\xaa\xfa!_w\f\x1er\xcc\xe8@m\x8cTY\x89\x1b%\xb7\nt_X\x97\xe4o\x94\x19&\xb6\x1f\xa4\xba\xe1Ֆ\x89O\xc3G~\xc6*\xdfPe\x98\x15v7\x9e\xd8@\x99\xa0\x9c\xfd=f\xd7\xda\x1f\xa7\x01]\x0f.\xb0\x96$a\x18C\x1fށ\xf5q\a\xe3\x02Q\x13Zz\xba\x9e\xe2\xaf\x04\x9eL\xd9\xd4ڗh|\x91\xd0\xed%\xf9(\xa3\x86\xc1\xa7C\xb1.L뒁6K\xd8l\xa42n\xb7z\xb9$l\x13\x82\x0f\xd6\xe6`\xdc\xcc=\xe2IXl\x9b\xb9N4i\xa6/\fz+\x9c\x85\xf1*\xfb\x82\x1e\xdc\xce\x14Ͳ\xcazX\xaf\xb5\xa1<\xe2\xe0<\xc9\xf0c\x94\xe7{|\xb0\xf2\x97'\xed\xe4\xadڀ\xfaAG\xecǑ\x14/\xd3p^\x1f\xb7(\x82 \x8f\x8a\x19c}*9\x92J\xe0Ie\xaco\xc59і\xd4'E\x1f\x893\xa3\xabᔜ4\x94\xefj(C\xe6\xd9c\x8d/3֯\x82\xfa\xec#_˲9\xdbQ\xb1\x1d\xbc\xa1`\xa7d\xb5\xdd\x05I\x1ep\xa6I^\x01\x06kѤ\xe8\xf0ⲩ\x94h\xa5\x12\x8c\x1c\xfb&A\x18p\xb84{\xc0\xf7K\u074b\xc6\xfe)\xeb\xd7\xfe\r\x94\xe5F\xc9b\xe9\xfb\xc5X\xea\xc2\xef\xe4+&\xad\xe7bvQ\xaa\x13\xe7\xb5\xfbg\x06P\x12\xca\x12\x04\xa1\xda\xf7\x9cpS\xd4\xc9\xd3\xd4ovj\xb8\x91\x9a%x\xfbQ\x8e\xff\xb5\r 0\xbc\f\x7fw\x99\xe1W0\xd8g\f\x8fO\xfe\b>\xec\xa90n9QO\x91\x17n\x12\xbb\x98\xb5\x90\xd1vb{R\x90\xe6\xb6\x03a\">\x83\xdd\xc5Yt\xeb\xd35\xdcE`\xd7\xfe\xf9\xd5\x1a\xf0\x82h&\u008b\xe0.\xf5\xc3I\x7ft'P\xe0C\x95Rų1\xc7\x03.]\x84^6ֲ\xaf=\x89\xf7'/\xc5\xef\x8f`\x1c\x1d\xea\xc6wI\xeb*a\xf9\xfc\a\x16\xdb\x0f\xc04\xde̢\xf2\xc7\xdf\xfd\xb0\xf6>i\xa9\x17\xa7\xc8\xd8\xca\x0f\x17u\xc3K\xb8\xee;\xa47\x1c\xac\xb6i\x80\xee\xa2r\x96\xce\xed\xcf\x18M;g(-\xbc}\x7f\x9eX\xd2\xfe\x8cA\xb4g\x8b\xa0\x9d\x17\xe5G\x8a\x0fD\x9f\xa4\xb5\x7f\xf3m#!4\x0f\xf6\xdcA\xb4V\f-\f\xfcE\xa3h\xd19\xb7\xf7#\xda\xe9\xbce-|O\xfe\x97\xff\x0f\x00\x00\xff\xff9i\xfd\xfe\xeb\x83\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccZK\x93۸\x11\xbe\xebWt\xed\x1e\xf6\xb2\x94줒J\xe96\x96\x93*W\xc6\xf1\xd4hvr]\bhJ\xb0@\x80\x01@\xc9\xca㿧\x1a\x0f\x89\xe2C\x0f;q\u008b-\x12h\xf4\xf3\xeb\x0f\xc0\x14E1a\xb5|E\xeb\xa4\xd1s`\xb5\xc4/\x1e5\xfdr\xd3\xed\x1f\xdcT\x9a\xd9\xee\xedd+\xb5\x98âq\xdeT\xcf\xe8Lc9\xbe\xc7Rj\xe9\xa5ѓ\n=\x13̳\xf9\x04\x80im<\xa3\u05ce~\x02p\xa3\xbd5J\xa1-֨\xa7\xdbf\x85\xabF*\x816\b\xcfK\xef\xdeL\xdf\xfe~\xfa\xbb\t\x80f\x15\xcea\xc5\xf8\xb6\xa9\x9d7\x96\xadQ\x19\x1eENw\xa8К\xa94\x13W#\xa7\x15\xd6\xd64\xf5\x1cN\x1f\xa2\x84\xb4z\xd4\xfc]\x10\xb6\x8c\xc2\x1e\x93\xb0\xf0]I\xe7\xff<>\xe6Q:\x1f\xc6ժ\xb1L\x8d\xa9\x15\x86\xb8\x8d\xb1\xfe/\xa7\xa5\vX9\x15\xbfH\xbdn\x14\xb3#\xd3'\x00\x8e\x9b\x1a\xe7\x10f\u05cc\xa3\x98\x00$\xd7\x04i\x050!\x82\xb3\x99z\xb2R{\xb4\v\xa3\x9aJ\x1f\xd7\x12踕\xb5\x0fΌ\xb6@2\x06\xb25\xe0<\xf3\x8d\x03\xd7\xf0\r0\a\x0f;&\x15[)\x9c\xfd\xa2Y\xfe\x7f\x90\a\xf0\xd9\x19\xfd\xc4\xfcf\x0e\xd38kZo\x98\xcb_c\x8c\x9eZo\xfc\x81\fp\xdeJ\xbd\x1eR\xe9\x919\xffʔ\x14A\x93\x17Y!H\a~\x83\xa0\x98\xf3\xe0\xe9\x05\xfd\x8a\x1e\x02r\x11B\xf6\x10\xec\x99K\xeb\x00좔\xe0\xa3aMUo\xad3\xb5I\x15x\xedH\x89\xfaӛ\xa4}Kl\xce\xef)\xb7x\x14\xe9<\xab\xea3\xb9\x0fk\x1c\x13v\xe6\x8a\xf7X\xb2F\xf9\xb6\xa9\x14%\xd5\xce\xcbs\xb3j\xe4S\x11g\x9d\xad\xf8\xfe\xec]\\ue\x8cB\x16\xa5\xc4Q\xbb\xb71\v\xf9\x06+6O\x83M\x8d\xfa\xe1\xe9\xc3\xebo\x97g\xafa(\x91:EA\x81c\xad\xd8l\xd0\"\xbc\x86\xfa\x8bqsɴ\xa3L\x00\xb3\xfa\x8cܟ\x82X[S\xa3\xf52\x17K|ZX\xd4z\xdb\xd1\xe9\x9f\xc5\xd97\x002#\xce\x02A\xa0\x841\xafR\xfd\xa0H\x96\x83)\xc1o\xa4\x03\x8b\xb5E\x87:\xc2\x14\xbdf:)8\xed\x88^\xa2%1Tۍ\x12\x84e;\xb4\x1e,r\xb3\xd6\xf2\xefG\xd9\x0e\xbcI\xc9\xec\xd1y\b\x15\xaa\x99\xa2dm\xf0g`Zt$W\xec\x00\x16iMhtK^\x98\xe0\xbaz|\xa4j\x90\xba4s\xd8x_\xbb\xf9l\xb6\x96>#47U\xd5h\xe9\x0f\xb3\x00\xb6r\xd5xc\xddL\xe0\x0e\xd5\xcc\xc9u\xc1,\xdfH\x8f\xdc7\x16g\xac\x96E0DGH\xadď6a\xba;[\xb6W\xd2\xf1\t\x90zGx\b^c\xcaDQ\xd1\xc4S\x14\xe8\x15\xb9\xee\xf9\x8f\xcb\x17Ț\xc4HŠ\x9c\x86\xf6\xfc\x92\xe3Cޔ\xbaD\x1b\xe7\x95\xd6TA&jQ\x1b\xa9}\xf8\xc1\x95D\xed\xc15\xabJzJ\x83\xbf5\xe8<\x85\xae+v\x11\xba\x18\xac\x10\x9a:\x80Dw\xc0\a\r\vV\xa1Z0\x87\xdf9V\x14\x15WP\x10n\x8aV\xbb7w\aG\xf7\xb6>\xe4\x9e:\x12\xdaA4X\xd6\xc8\xcf\xeaN\xa0\x93\x96*\xc33\x8f\xa1\xba:\x0eJP1ޔ\xf33\f\x12\xf40\xceѹ\x8fF`\xf7KG\xe5\x87\xe3\xc03\x1dk\xb4\x95t\xa1\xbdBil\xb7\xf3\xb0#\x92\xb7\x9f\x8cx݀\x03\xa0n\xaa\xbe\"\x05<#\x13\x9f\xb4:\x8c|\xfa\xab\x95\xbe\xbf\xd0H \xe9\x89*.\x0f\x9a?\xa1\x95F\\1\xfe]g\xf8\xd1\x05\x1b\xb3\x872\xe4\xbf\xf6\xea@\xd8\xe5\x0e\x9a\xf7Q;?\x0fO\x1f2\x82\xc7\xdaJ\x85\x99|5\x85\x87TԦ\x847 \xa4#\"\xe1\x82о\xb3t\xa3\x02ј\x83\xb7\xcd]\xe6s\xa3K\xb9\xee\x1b\xdd\xe6Fc\x19sEt\xc7s\x8b\xb0\x12\xa1\x16eGm\xcdN\n\xb4\x05Շ,%O\x9a46v\x90R\xa2\x12=l\x1a\xad\xb2`\x8aEAE\xcdԕ\x18.\x8e\x03\x03\x93fR\xc7\f>\t\bXc\xabԚ\xb5G-\xb0\xdbm\x826&\x00\x9aC\x01{\xe97\x11)\xd5P\xdd\xc1\xc5ڣg\x8b\x87\xa1\xd7\x1d\xdd_6H#c\xe3Ep\xc8-\xfa\x90m\xa8(}(\x95\xa6\x00\x1f\x1b\x17\xb0\xb6\x8b\x13\xf9\t\x84/\xcf\xde\xe2\xa1\xefh\xb8\x16\xdcD\x85FT\x0e$j\x0e?\xfcpݤ^w\xcb\x0fQ\xf7l\xa8\xc5\x12-\xea\x1e\x9b\xc8\xcfK\xe8Q\x944\x94aX\x96Ƚܡ:\x84\x9eD\xe0\xf93\xac\x1a\x0f\xa2\xc1\x105Ʒ{f\x85\x03n\xaa\x9ay\xb9\x92J\xfa\x03H7\"\x9f)e\xf6(Rı\xaa\xfda\n\x1f\xb4\xf3LstG\x1eD\x1e\x8b\xa9\xc0t\x1c\x95\xaa8\x10:f\x8700\x8a\xaf\x8c\xf3\xc0\xd1R:\xaa\x03\xec\xad\xd1\xeb1c\a\xda!\xed\x01\xadF\x8f\xa1#\n\xc3\x1d5C\x8e\xb5w3\xb3C\xbb\x93\xb8\x9f\xed\x8d\xddJ\xbd.H\xc1\"\x81\xcf,\xec\xecf?\x86\x7f\xbe&\vL\x1dq\xe2\x86\xe4]\x86Z?\x10\xbd\xf5\x1b\x8c-b\x19s\xd0X \x02A\xa9]\xa5܍\xc8:TvC\xbc\xbc\xfd\xe4\x90\x0f\xf5\x8f-\xf6[\xc7\x05P\x01\xf8R\x9c|[T\xac.\xe2h\xe6M%\xf9\xa4km\xcc\xfb\xcb\xf8\x937+R\vɉܞ\xe3F\xdeĉ\xb3=̀\x1b\xba\xbb\x9c1\xb4\x1cvS47q\x85+\x1a\x7fj\x8f=m}#t\xa7\xfe\xef\xd0\x13\xeft\xa0\x91\xf8\x01\xb3}?\a\xc0\xe4FkB*o\x80\x1d\xdb\xc0O\xae\xdb\xff\xeeD\xcfU÷8\xe0\xf8\x9e)\xef\xc2\xc0\xec\xe38\x8dti\x1c\x86\xc6tM\r\xb8^\x11\x9c-\xd0ޢ\xcb(\xf2-\x1eH\u0091[0X<\xc0\xaa\xd1BaVu\xbfAM\xdb1Y\x1e\x88\xec\xbf<.\xb3c\x03\x01K[\xa7\xec\xde1 yO\xbb\x00JA1\x87_\x1c\xa6u\x9f\xb1\x04\xa9\x9dG\xd6#\xe9\xf1\x89\xbdq\x0e\xab\xc3\x00\u05fa\xd9A\xcfX~\xbb\x8f\x82\xae\xe4\xa1\xd4 8\xc6\xc4J\xb0\x92\xfa{\xde\x0f-\x1e\x02\xc4\x12\xdf \"}\xe6ґe\xeetth\xd0i\xf1\fdR\xc7\x02a\xd5\xd8\":\x1fR\x01\x8by9H䇃q\xb9.\xe0\x12\xb3\xe89\xfb>v1*\x13\x80\xdd\xc80\xe0z\xb2\xc0E\xa6\x017\xb0\x8d\x9e\x99\xa39\x05w\xb2\x0e\xf8\x0e\xcc\x03\xfe\xfb\xec\x03\xeef \xf0\xddY\bܖ)\x97\xd9\b|\x13#\xb9\xe0\x8bK\\\x05\xae\xf2\x15\xb8\xc8Y`\x94\xb7\xc05\xee\x02w\xf2\x17\bx\x82\xa5\xfcr\x032?\x85\x81\xb9\x93\xd6\xcco\xa8kH\x81\xc0\x06\xfaj<\xa1\x18q\xd0q\xd3\xfb)\x85\xef+\xfa\xee%\xd2\x17չ\x87\xf7e@\xbfB\x8c\x9eҰ\xa3\x17\xf2\xef\x04 \xe7\a c\x04mТ\xdd\xf1\xb4\xfdO\xf1X\x81\x0f\xa0\xf8\x992\xaf\xfd\x19\x17\x8e'\xf2\x99\xff\x10K\xa3Ͱ\xb1\x16]m\xb4\xa0\xb6w\xdb\xe1\xc4I\xe5\xff\xdc\x11\xc5pX\x8bs\xfa\xda\xf9\x96\xa3p\xd3\xf9\\\xb8߸\xfb\x84.\xde\xfa\xb4Ͽ\xccʡݵ\x0e\xe9:6~\x97\xb3\xb9\xc1\xce\xd6:\xb0#\xaa\xa4\xa1\xd1\xe1\xc8\"4\xad\xe9d`F\x9b\x17\xfa\xd0<\xa4\x03m\xf64\xb9%-v=\x13\xe9M8\xb4dZ\xa4\xe3b\xfa4 y/\x95\xa2\x1ef\xb12\xe4,\xd4^Zj\x96,\xb4\xb1\xddo\xa6o\xfewg\x81\x8a9\xbf\xc6Qq\xff\x99\xa6\x00[\x99\xc6\x0f\xf4\xfeV\xc2\x0f\xd6t\xb8e\xbfG\xc7\xf0\xb7\x03\xd7\xe8\t\x8d\xc9\x11፵\xe1\xb2._\"ݱ\xd1\x1cC\xe0\x87Ο8\xb4\xbf\xf5\xff\x00\xe2\x06\xbb\x06\xbbt\xefe촭\xb8&'\xb7\xdf4\xab\xe3\x15\xec\x1c\xfe\xf1\xafɿ\x03\x00\x00\xff\xff%\xff\\)\x99#\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVK\x8f\xdb6\x10\xbe\xfbW\f\xd0k%wQ\xb4(tk69,\xda\x06\xc6n\x90;M\x8e-f)\x92\x9d\x19z\xeb>\xfe{AR\xf2C\xb67\xc9%\xba\x99\xf3\xe4\xf7\xcd|t\xd34\v\x15\xedG$\xb6\xc1w\xa0\xa2ſ\x04}\xfe\xc5\xed\xf3/\xdcڰ\xdc\xdd-\x9e\xad7\x1d\xdc'\x960<\"\x87D\x1a\xdf\xe2\xc6z+6\xf8ŀ\xa2\x8c\x12\xd5-\x00\x94\xf7AT>\xe6\xfc\x13@\a/\x14\x9cCj\xb6\xe8\xdb\xe7\xb4\xc6u\xb2\xce \x95\xe4S\xe9\xdd\x0f\xed\xdd\xcf\xedO\v\x00\xaf\x06\xec\xc0\xa0C\xc1\xb5\xd2\xcf)\x12\xfe\x99\x90\x85\xdb\x1d:\xa4\xd0ڰ\xe0\x88:\xe7\xdfRH\xb1\x83\xa3\xa1Ə\xb5k\xdfoK\xaa7%\xd5cMU\xacβ\xfcv\xcb\xe3w;zE\x97H\xb9\xeb\r\x15\a\xee\x03\xc9\xfbc\xd1\x06̚\xaa\xc5\xfamr\x8a\xae\x06/\x00X\x87\x88\x1d\x94ب4\x9a\x05\xc0\bH\xc9Հ2\xa6@\xac܊\xac\x17\xa4\xfb\xe0\xd2\xe0\x8f\x95\x905\xd9(\x05\xc2\x0f=\x96\xcbC\u0600\xf4\b\xb5\x1cH\x805\x8e\x1d\x98\x12\a\xf0\x89\x83_)\xe9;h3\x92mu͍\x8c\x0e\x95\x847\xf3c\xd9\xe7\x86Y\xc8\xfa\xed\xad\x16X\x94$\x9e\x9a(um\xf0@'ȟ7P\xfc\xdb\xd8+>\xaf\xfeT\f\xb7*W\x9f\xdd]EZ\xf78\xa8n\xf4\r\x11\xfd\xaf\xab\x87\x8f?>\x9d\x1d\xc3y\xafWH\aˠ\xa6N3p\x155\b\x1e!\x10\f\x81&T\xb9=$\x8d\x14\"\x92؉\xff\xfa\x9d\xac\xd5\xc9鬅\x7f\x9b3\x1b@\xee\xbaF\x81\xc9\xfb\x85\\@\x1c\x87\x02\xcdx\xd1\n\xaee \x8c\x84\x8c\xben\\>V\x1e\xc2\xfa\x13jig\xa9\x9f\x90r\x9a<\xaeə\xbc\x96;$\x01B\x1d\xb6\xde\xfe}\xc8\xcd\xf9\u07b9\xa8SR \xc9c畃\x9dr\t\xbf\a\xe5\xcd,\xf3\xa0\xf6@\x98kB\xf2'\xf9J\x00\xcf\xfb\xf8#\x83h\xfd&tЋD\xee\x96˭\x95Ilt\x18\x86\xe4\xad\xec\x97E7\xec:I ^\x1aܡ[\xb2\xdd6\x8ato\x05\xb5$¥\x8a\xb6)\x17\xf1Ep\xda\xc1|G\xa3<\xf1Yً\xe9\xa9_ч\xaf\xa0'\xabE\x9d\x91\x9a\xaa^\xf1\xc8B>\xca\xd0=\xbe{\xfa\x00S'\x95\xa9J\xca\xd1\xf5\x02\x97\x89\x9f\x8c\xa6\xf5\x1b\xa4\x1a\xb7\xa10\x94\x9c\xe8M\f\xd6K\xf9\xa1\x9dE/\xc0i=X\xe1ib3u\xf3\xb4\xf7E\x90\xb3\x02\xa4h\x94\xa0\x99;b\xa3\x13Q\x19\xbe\x83ΫkA_\x8a\x05\x12\x05\xba8\x9d5\xf5\xae8\x95\xff\x12\xcaz\x06\xe5\xf7c H\xaf\x04^\x90\xf2\x86萲Z\xa1\x01\x93.\xf0\x1ba9}\x93\"\x05\x8d|\xb1\x8a\x00Vp\xb8\xd2\xd3+\xec\xe4\xcf'\xe7\xd4\xdaa\aB\to0\xab\x88\xd4~f+o\xdfg Xe\x9fk\x1c\x1c\xde\xfbϒP\xe0\xf6i\xb8\xac\xd4\xc0{|\xb9r\xfa\xe0W\x14\xb6\x84<\x1f\xf9l\\U\xf4p\xfe\x1e\xbc\x82\xd2ա\xbc8\xe4,\x85\xe6\x04E\x96@j{\x8a+\xa7\xf5A\xe9;\xf8\xe7\xbf\xc5\xff\x01\x00\x00\xff\xffߙ6&\xcb\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcW\xcdn\xe36\x10\xbe\xfb)\x06\xe8\xa5\x05Vr\x83\xa2E\xe1[\xeb\xec!\xd8t\x1b\xc4\xdb\xdc)j$\xb1\xa1H\x96\x1c:\x9b\xa2\x0f_\f)\xf9G\x96\x1d粺y8\x9c\x9fof\xbe\xa1\x8b\xa2X\b\xa7\x9e\xd0\ae\xcd\n\x84S\xf8\x95\xd0\xf0\xafP>\xff\x1aJe\x97ۛų2\xf5\n\xd61\x90\xed\x1f1\xd8\xe8%\xdeb\xa3\x8c\"e͢G\x12\xb5 \xb1Z\x00\bc,\t\x16\a\xfe\t \xad!o\xb5F_\xb4h\xca\xe7Xa\x15\x95\xae\xd1'\xe3\xa3\xeb\xed\x8f\xe5\xcd/\xe5\xcf\v\x00#z\\Am_\x8c\xb6\xa2\xf6\xf8O\xc4@\xa1ܢFoKe\x17\xc1\xa1dۭ\xb7ѭ`\x7f\x90\xef\x0e~s̷\x83\x99\xc7l&\x9dh\x15\xe8\xd3\xdc\xe9\xbd\x1a4\x9c\x8e^\xe8\xd3 \xd2a謧\xcf{G\x05\xf0y>R\xa6\x8dZ\xf8\x93\x9b\v\x80 \xad\xc3\x15\xa4\x8bNH\xac\x17\x00C\xf6\xc9P1$\xbe\xbdɦd\x87\xbd\xc8\x1e\x00\xacC\xf3\xdb\xc3\xdd\xd3O\x9b#1@\x8dAz\xe5(a\xf8_\xb1\x93\xc347P\x01\x04\f\xe1\x00\xd9]\x84 \f\bO\xaa\x11\x92\xa0\xf1\xb6\x87J\xc8\xe7\xe8\xc0V\x7f\xa3$\bd\xbdh\xf1\x03\x84(;\x10l%+\x1c\xf8Ҷ\x85Fi,w2\xe7\xadCOj\x04)\x7f\a\xbdv \xbd\x94\x05\x7f\x9cx\xbe\x0557\x1d\x06\xa0\x0eG\xf0\xb0\x1e\xb0\x02\xdb\x00u*\x80G\xe71\xa0\xc9m\xc8ba\x86lʉ\xe9\rz6\xc35\x8d\xba\xe6^ݢ'\xf0(mkԿ;ہ\x11c\xa7ZP\x02\xd3\x10z#4l\x85\x8e\xf8\x01\x84\xa9'\x96{\xf1\n\x1e\x13\x82\xd1\x1c\xd8K\x17\xc24\x8e?\xacGP\xa6\xb1+\xe8\x88\\X-\x97\xad\xa2q\x02\xa5\xed\xfbh\x14\xbd.\xd30\xa9*\x92\xf5aY\xe3\x16\xf52\xa8\xb6\x10^v\x8aPR\xf4\xb8\x14N\x15)\x11\x93\xa6\xb0\xec\xeb\xef\xfc0\xb3\xe1\xc8-\xbdrC\x06\xf2ʴ\a\aip\xdeQ\x1e\x1e\xa5\xdc]\xd9TNq_\x05\x161t\x8f\x1f7_`\x8c$Wjh\xb1\x9d\xea\t.c}\x18Me\x1a\xf4\xf9^jS\xb6\x89\xa6vV\x19J?\xa4Vh\bB\xaczEa\xecu.\xdd\xd4\xec:\xb1\x14T\b\xd1Ղ\xb0\x9e*\xdc\x19X\x8b\x1e\xf5Z\x04\xfcƵ⪄\x82\x8bpU\xb5\x0e\xb9w\xaa\x9c\xe1=8\x18\x99\xf3Li'\x94\xb1q(\xb9\xb0\x8c-\xdfT\x8d\x92y\xa4\x1a\xebA\xec\x19d@\xfa\x18\xa8y\x06H\xc1\t\xdf\"M\xa5\x93X\xbe$%v\xff҉c\xc2\xfa\x1e˶d\xce\tC \x99\x8f~\x98\x16\xeaR\f0\xdb賑\x8c\xfd\xcd00\xaeL(Lv\x871\x9d\xba\xe6\x0fM\xec\xe7\x1d\x14\xf0{\x8a\xf9\u07b6\x17\xcf\xd7\xd6\x10\xcf\xc5E\xa5'\xabc\x8f\x1b#\\\xe8\xec\x1b\xbaw\x84\xfd\x9f\x0e}\xde\xd0\x17U\xc7E\xbfۊ\x17\x14\xa3>\xeb\xf7\x11y\x83\xe0\xf9L\a\x85\xab\xac\\\x11ӠyU\xa2\xeb\xcd\xdd{ <\xa3\xfe\x8e\"ݙƾ\x91\xe2^qV\xef\f\r\x8c_zC\xbc\xdd\xd3\xfc\n\x19{\x9a\xaf\xe4݉\xf0)V\xe8\r\x12\x86=S\xbf(\xeaf-\x02\xbctJv\xe9b\x1a\b^\x02!X\xa9\xe6(\xf5\x8a\xf0\x99G\x94Ǚ\xa1,ҰΈ9\xf8\x13\xf1\x19\xf6;\xe7\xa0\x18\x18\xe9*\x06%A1\xbc\x83C\x93\xfe\b\xb5\x8cާ\x15\x95\xa5\xfc2\x99^\xb8\x96DG\xe6\xf9\xeb\xf1\xfe\r&\xbd\xddk\xa6ǸP&G\xe3<\x16A\xb5\xfc\x82\xe23\xe6\xd2\xc4q\xa7`\xe4\xef\xf8\x85w\f\xd4lE\xf1\xabSy\x00\xdf\b\xf1\xe3N1\x13>\x9a\xbc\xe7\xa7o\xd8d\x10\x03?\xb7@\n3\x13c\x85P\xa3F\xc2\x1a\xaa\u05fc\xb9^\x03a\x7f\x1awc}/h\x05\xbc\xff\vR3md\xa2֢Ҹ\x02\xf2\xf1\\\x97\xcd&\xee:\x11f\xc6\xf0(\xe7\a֙k\x8c\xdd0^\xec\f8\xbb_\n\xf8\x8c/3\xd2\ao%\x86\x80\xa7ct6\x93\xd9!8\x11\x06~\xa4\xd5\a(\r\x7f\x19\x06\xc9\xff\x01\x00\x00\xff\xff\xe5\x0fY\x99e\x0e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4:K\x93\x1b\xb7\xd1\xf7\xfd\x15]\xeb\x83\xed*\ri\xf9\xfb\xe2J\xf1&\xad\xe2\x14\x13[\xd9\x12W\xba\xb8|\xc0\f\x9a\x1cxg\x00\x04\xc0\x90b\x1c\xff\xf7T\x03\x987H\xeeR\xb6\x85\xcb.\xf1h\xf4\xfb\x85ɲ\xec\x86i\xf1\x01\x8d\x15J\xae\x80i\x81\x1f\x1dJ\xfae\x17\x8f\x7f\xb5\v\xa1\x96\xfb\x977\x8fB\xf2\x15\xdc5֩\xfa\x1dZ\u0558\x02\xdf\xe0VHᄒ75:ƙc\xab\x1b\x00&\xa5r\x8c\xa6-\xfd\x04(\x94tFU\x15\x9al\x87r\xf1\xd8\xe4\x987\xa2\xe2h<\xf0\xf6\xea\xfd7\x8b\x97\xdf-\xfer\x03 Y\x8d+Њ\xefU\xd5Ԙ\xb3\xe2\xb1\xd1v\xb1\xc7\n\x8dZ\buc5\x16\x04{gT\xa3W\xd0/\x84\xb3\xf1ހ\xf3\xbd\xe2\x1f<\x98\xd7\x1e\x8c_\xa9\x84u\xffL\xad\xfe \xac\xf3;t\xd5\x18V͑\xf0\x8b\xb6Tƽ\xed/\xca@\xef\xf3\xb0\"䮩\x98\x99\x1d\xbc\x01\xb0\x85Ҹ\x02\x7fN\xb3\x02\xf9\r@$\xde\xc3ɀq\xee\xd9ɪ{#\xa4CsG\x10dw\vG[\x18\xa1\x9dg\xd7\x04s\xb0\x8e\xb9Ƃm\x8a\x12\x98\x85\xb7xX\xae\xe5\xbdQ;\x836\xa0\r\xf0\x8bU\xf2\x9e\xb9r\x05\x8b\xb0}\xa1Kf1\xae\x06\xb6o\xfcB\x9crGB\xd9:#\xe4.\x85ă\xa8\x11xc\xbc\xb8\x89\xfa\x02\xc1\x95\xc2ΰ;0K\x18\x1a\xe7\xc9N\xe3\xe2\xd7\t\xa2u\xac\xd6S\xa4\x06G\x03V\x9c9L\xe1t\xa7j]\xa1C\x0e\xf9\xd1aK\xc9V\x99\x9a\xb9\x15\b\xe9\xbe\xfb\xff\xd3\xec\x88\xfcZ\xf8\xa3o\x94\x1c\xf3\xe65\xcd\xc2`:`B\xb2ڡI2H9V}\n\"\x8e\x00\xbc\x1e\x9c\x0f\x98\x04\xb8\xc3\xf9\x8b\xa8\xacea\xb0Fy\x1dB\xa2?=\xc7f\bz\xb8\xaa\x8dPF\xb8\xe3\n^~\xf3T4\xc9>@m\xc1\x95\bQy6N\x19\xb6C\xf8A\x15A\xd1\x0e%\x9a\xa8hy\xd4\xfeR5\x15\x87\xbc\x15\f\x80u\xca$\x95Mc\xb1\b\xa7\"\xdc\x16\xecD\xe3\xc6w\xfe\x11\x06Q\x18dI\x83h\xdd\xe9\xc2\xef\x10J\xa6\xad\xe2\xd5\x0e\x9fd\x11C\x96Jű\xe3\x1f\xce\xd0\x12\x16\xb4Q\x05Z{\xc6P\t\xc6\b\x91\xb7\xfd\xc4E\x06\x95\xe8\xf7\xb4\xf84\xbaR\x8c\xa3\x01\xa7\xa0d\x92WHd0p\x86I\xbb\x8d*2\x17`{\xec\xe1\xa8Ǩ\xbc\x8f\v\xa7\xd0\t\xbb\xf6/\x83\xbb.J\xac\xd9*\xeeU\x1a\xe5\xab\xfb\xf5\x87\xffی\xa6I\x8d\x95F\xe3D\xeb\xef\xc3\x18\x84\xcd\xc1,\x8c\xc9\xfdo6Z\x03\xa0\v\xc2)\xe0\x14?\xd1z6\xc4@\x80<\xe2\x14\xd8#,\x18\xd4\x06-\x99\x96\xd7(\xb5\x05&A\xe5\xbf`\xe1\x16\x13\xd0\x1b4\x04\xa6\xb5\x85B\xc9=\x1a\a\x06\v\xb5\x93\xe2?\x1dlK\xbc\xa6K+\xe6\xd0:o\x8cF\xb2\n\xf6\xacj\xf0\x050\xc9'\x90kv\x04\x83t'4r\x00\xcf\x1f\xb0S<~T\x06AȭZA霶\xab\xe5r'\\\x9bL\x14\xaa\xae\x1b)\xdcq\xe9\xf3\x02\x917N\x19\xbb\xe4\xb8\xc7ji\xc5.c\xa6(\x85\xc3\xc25\x06\x97L\x8b\xcc\x13\"}B\xb1\xa8\xf9\x17&\xa6\x1fvt\xedL\xd0a\xf8\x1c\xe0\x19⡬\x80\x8c\x80EP\x81\xc4^\n4E\xac{\xf7\xb7\xcd\x03\xb4\x98\x04I\x05\xa1\xf4[g|i\xe5C\xdc\x14rK:O\xe7\xb6F\xd5\x1e&J\xae\x95\x90\xce\xff(*\x81ҁm\xf2Z8R\x83\x7f7h\x1d\x89n\n\xf6\xce'\\\x90\x93-\x91\a\xe0\xd3\rk\tw\xac\xc6\xea\x8eY\xfc\x93eER\xb1\x19\t\xe1I\xd2\x1a\xa6\x91\xd3́\xbd\x83\x856\t\x13\x86a\xc6;%:W\xaaB6\xe5%\x85\xbb\v4S\x00L\t\xcbG[W2\xd7\xe2F\x9bL#圷4\x94|\x968\xb4\xe2\x17\xf0\x8a720\xb8E\x83>\x1b\t\xbe_+\x1f!\x1c\x13\xb2\xf5i\xa1^\x01\xa7\x12\x98\xe5A\x89\x90\xc3\xd46\xe0\xac}\xc0\x99@\x99\xc4\xf8\xd5\xfd\xba\r\x86-\x13#\xee\xb3xw\x91?4\xb6\x02+\xee3\x87\xcbw'5\x97\xc6z\x1b\x90\xf0\x11\xc1)`\xa0\x05\x168\x8a\xc6 \xa4u\xc8x\x9c$'h0\xae\xbd\b\x9e\xfe$\x92\x10\x8a\xca\x18\xb5I&\xc0(\xf2\b\x0e\xff\xd8\xfc\xeb\xed\xf2\xef*\xd0\x01\xac\xa0\xd4\xcc\xd7z>\xdf~\xd1\xd5{\x1c\xad0ȩz\xc3Eͤآu\x8b\b\r\x8d\xfd\xe9۟\xd3\xfc\x03\xf8^\x19\xc0\x8f\x8c\xaa\xa6\x17 \x02ϻ`֪\x8d\xb0\x81\xf0\x0e\"\x1c\x84+=\xa2Z\xf1H\xe0\xc1\x93\xe0\xd8#Yr \xa1A\xa8\xc4c\xc2~¸\xf5\xd9\\\x8f\xe6\xafd=\xbf\xdd\xc2W\xc1y\xdd\xd2\xcfۀF\x97\xb6\f\r\xacG'X\x99\x11\xbb\x1d\xf6y\xffLY(\xccR\x80\xfa\x1a\x94!Z\xa5\x1a\x80\xf0\x80IN!> \x9f\xa1\xf7ӷ?\xdf\xc2Wc\x1e\x9c\xb8JH\x8e\x1f\xe1[\xf2>\x9e7Z\xf1\xaf\x17\xf0\xe0\xf5\xe0(\x1d\xfbH7\x15\xa5\xb2(A\xc9\xea\x18\x12\xe0=\x82U5\xc2\x01\xab*\v\t\"\x87\x03;\x82ڞ\xb8\xa7\x15\x11\xa9&\x03͌;\x9b$F>\x9c7\x9ay\xd6Ԏ\xa7ًϢ\x9ed\xbd\x9f-\x03y\"'|\xb9\xf0\t\x9c\x18\x96^Wp\xe2\xb1\xc9\xd1Ht\xe8\x99\xc1Ua\x89\x0f\x05jg\x97j\x8ff/\xf0\xb0<(\xf3(\xe4.#ê\xd4\xed\xd2\xf7\xc1\x96_\xf8?\xd7\x12\xee\xdbT\x9fJ\xbd\a\xf2\xf9X@\xb7\xdb\xe55\x1ch\xb3\xfb\xa7Ǯ\x93|\xd8Ąs\n\x93l\xfeP\x8a\xa2lk\xbd\x81\xb7\xad\x19\x0f\xee\x98\xc9\xe3g\xb2\x1d\xe2sc\b\xa3c\x16\x9b\xb8\x19\x93\x9c\xfe\xb7\xc2:\x9a\xbf\x86\xb1\x8d\xf8$\xe7\xf2~\xfd\xe6sZT#\xae\xf1$'j\x980>f=VY\xcdt\x16v3\xa7jQLvS\x0e\xbf\xe6$\xa4\xad@s!\xfd{7\xda\xdc&\xa8\x89j\xa0\xdb\xf3\xac\xfcӱ]\"\xe1\x1bv\xb1ϥ\x85g\xf9uY\x15\x1e\xd8\xce\x023\b\fj\xa6I#\x1e\U0005814cC3A\xe9\x02e\x04]c\x10\x98\xd6\x15\xc5\xf4\x90E$ \xc6\xfc7\xb2\x87YO\xdf)\x86$E\xd9v\xa56蜐\x9f\x919\xef'\x88\xfc\xbe\x8c\xeazv\x85\x92[\xb1\x8b\xdd\xce9\xa7dSU,\xafp\x05\xce4\xa7j\xae\xb3\x8c|\xa0-\xe7\xe9\x7f?\xd8\xdaj\xf8\x85\x06c\x9a\xaaQ\xdbqN\fʦ\x9e\xa3\x92\xc1\xa3҂%\xe6\rZ7\xb3^Z\xb8\xbd}\x8e\x8d\x05\xa5\xbc\xa6\xe4\x0eep\xaa*\x8d\x8a\x1e\x13\xf8\xb62u\xaa\xaf\xf2\x92B\x7f\x86o\xa0\xea\x9eʑ1\xdeY\xba]2\xd93\xe8.\xb7SZ\xf1\xc9\xcc\xd8\rN\x16\x03}O\xea!\xf9\x86\xf63\xbaH\xe1\x91-\xf24\x04G\xd7>\xbdQ\xda}m\x1f\x89\n;\xed\x90w\x8d\xfek$\xfej\n\xc4\xf7~\r\x8fF!j\xecJ\xff\xb1\xaf\v\xc5]\x8e\xa0\rj\x96\xec\n\x81\xef\xdc[\xdf\xc2\xfc\xd2\x06`\xc2Bc\x91\xfb\x0e\xda\xec\xee\x19\x84\xf6\x9d\x893\x87\x19\x9d\xbf\xce_\xa4\x1bS\xe1\xcdo\xf8RrU\x97j\x0ef\xceB\xd6r\xcd?ᴏ\x8d)\x8e\xf5\xe0:~\x05h\xc8}\x15JE\xf2\x96\x89\n9\xb4o\xdcτ\x92\xe3\x96R\x9c\xe0\xe3\xda>ND\xeft\xfdw^\x92\t&\xcc\x13\x9e?R\x98ӧ\xc6\v\x92\\O\xb6C\xa9\xaa(/\xd9\xd49\x1a2L\xff\xe0\t\x12\x0fT\xf7\x17%\x93\xbb\xa4\x93k\x1f\xec\x10*f\x1d\xe4\xfd'\x03)\xe2\x87/\xa6Sʆ/\x9c\xfd\xa8\xd1Z\xb6\xbb\xe4\xce\x7f\f\xbbB\xe7.\x1e\x01\x96\xabƥ\xed\xf7K\x1b]\xd0\xf3\xba\x87ɦ\xd8\xd8\xfb1W\xb6\xcen\xdbT\x95?3\x8c\x1b\xfd\xa7\x1d\x1e\xab\x1c\xd3\x19\xff\x99\xd6\xe19\x04Kf/\xb1\xea\x9e\xf6\xa4\xfcq\x17\xec\xce:d8\x13\xd8\xdf\xe2!1\xdb\xfa\xb9\xc4\xd2}t\x9e\x89\xa5ٗ\x18\xc3\xc5\xd0\x1bOq\xae]K\xc2\xec\xbesH\xac}\xef\xbdʳ\x98\x1d\xf1\xbb\xc6mv\xbd\xf5\xde\xf2\xfcg\v3\xfb\x1b\xe7\x1fL\xf2\xa1\xd8RM\x88\xfe|\xabA\x01Rl\xa4\xc5'\x01ﺜ\x02.\xac\xaeر\xa3ŗ~d\xaa\xe9\xf7\x91ޢZ\x8f\xa9\xf1T*{\xbe\xc3\xdd}-\x92\xaek\xcf\xfb\v\xb8\xe03\xfc\xba:\xed\f\x7f\x8f\x1bΤ\xe2V2mK\xe5\xd6o.\xa8Ʀ\xdb\xd8\xdac_V\xfa\xc0\xe2\x9f\xde⦨\n\tT{\xef\xf6,g1\xfex\xe8\x1a-ތ \\\x88\xfb\xf1[\xa6Ttݐ\x17 \a\xe4\x1fv\xef\xa6_p\xbc\xe8\x82\fs\xb1A\x1e\xe2Q\xaa\xab\xa0\xa4\xaf#\x94\x99\xbf\xb2\xc3\xc5@>&\xe8ό\xe1Iu\x9aMz\xcc\xf9\x00v|\xd3\x1c\xce4y\xf7ܿ\x82_\x7f\xbb\xf9_\x00\x00\x00\xff\xff\xfc¬w\xb0(\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z͒\x1b\xb7\x11\xbe\xefSt\xad\x0f\xb6\xab4d\xa4$\xae\x14o\xd2*Nmbo\xb6DI\x17\x97\x0f\xe0\xa09\x03s\x06\x80\x01\f\xb9\xb4\xe3wO5\x80\x19\xce\x0fH.\xa9\x925\x17i\xf1\xd3\xf8\xf0u\xa3\xbb\xd1`\x96e7L\x8b\x8fh\xacPr\x01L\v|r(\xe9/;\xdb\xfc\xc3΄\x9ao_\xdel\x84\xe4\v\xb8k\xacS\xf5;\xb4\xaa19\xbeŵ\x90\xc2\t%ojt\x8c3\xc7\x167\x00LJ\xe5\x185[\xfa\x13 W\xd2\x19UUh\xb2\x02\xe5lӬpՈ\x8a\xa3\xf1\xc2ۥ\xb7\x7f\x99\xbd\xfcn\xf6\xf7\x1b\x00\xc9j\\\x80V|\xab\xaa\xa6F\x83\xd6)\x83v\xb6\xc5\n\x8d\x9a\tuc5\xe6$\xbc0\xaa\xd1\v8t\x84\xc9q\xe1\x00\xfaQ\xf1\x8f^λ \xc7wUº\xff$\xbb\x7f\x10\xd6\xf9!\xbaj\f\xab\x128|\xaf-\x95q\x0f\x87\xb52\xd0[\x13z\x84,\x9a\x8a\x99\xe9\xcc\x1b\x00\x9b+\x8d\v\xf0\x135ˑ\xdf\x00D\x06\xbc\xa0\f\x18\xe7\x9eSV=\x1a!\x1d\x9a;\x12!\xbbe8\xda\xdc\b\xed\xd3%\xb3\x18{\x03\xf9K\xdf\x11\x9bܞ0[g\x84,R(ދ\x1a\x817\xc6+\x9d\xf6\x9f#\xb8R\xd8)\xbc\x1d\xb3\x04\xd18\xbf\xf14\x18\xdfO\"\xadc\xb5\x1e\xa3\xeaM\r\xb08s\x98\x02u\xa7j]\xa1C\x0e\xab\xbd\xc3v+kej\xe6\x16 \xa4\xfb\xeeo\xc7\xf9\x88\x84\xcd\xfcԷJ\x0e\xc9yC\xad\xd0k\x0eHH[\x05\x9a$Cʱ\xeaS\x808\x12\xf0\xa67? \tr\xfb\xedg\xa1\x90\xe9\x81Z\x83+\x11ް|\xd3hX:eX\x81\xf0\x83ʃ\nw%\x1a\xf4#Va\x04\x9dm\x10\xa4;e\x92\xaaӘ\xcf\xc2\xd8(\xac\x955\xd2\xdfp\xa1\xcfb_\xb9A\x96\xb4\xaf\xd6I\xcd\xfc\b\xa1d\xda\xc8^\x17\xf8,\x03\xeb\x13)\x15\xc7\x1ek\x13\\\u00826*GkO\x18>\t\x19 y84\x9c\xa5\xa8D?\xa6\x05\xd4\xe8J1\x8e\x06\x9c\x82\x92I^aС3L\xdau\xb4\x8c\xa9\n\xdbi\xef\xf7z\b\xe5C+\xaf\xd73\xc1\x14\x86n_\x067\x98\x97X\xb3E\x1c\xab4\xca\u05cf\xf7\x1f\xff\xba\x1c4\x03Ѣ\xd18\xd1\xfa\xd1\xf0\xf5BR\xaf\x15\x86{\xfe_6\xe8\x03\xa0\x05\xc2,\xe0\x14\x9b\xd0z.\xa2\x7fE\x1e1\x05\x8e\x84\x05\x83ڠE\x19\xa2\x1553\tj\xf5\v\xe6n6\x12\xbdDCb\xc8\xed7\x15\xa7\x90\xb6E\xe3\xc0`\xae\n)~\xebd[\"\x9c\x16\xad\x98C\xeb\xfcA4\x92U\xb0eU\x83/\x80I>\x92\\\xb3=\x18\xa45\xa1\x91=y~\x82\x1d\xe3\xf8\xd1[\x93\\\xab\x05\x94\xcei\xbb\x98\xcf\v\xe1\xda@\x9d\xab\xban\xa4p\xfb\xb9\x8f\xb9b\xd58e\xec\x9c\xe3\x16\xab\xb9\x15E\xc6L^\n\x87\xb9k\fΙ\x16\x99߈\xf4\xc1zV\xf3\xafL\f\xedv\xb0\xecD\xd1\xe1\xf3\xe1\xf5\x02\xf5P\xbc\xa5\x93\xc0\xa2\xa8\xb0Ń\x16\xa8\x89\xa8{\xf7\xcf\xe5{h\x91\x04M\x05\xa5\x1c\x86Nxi\xf5Cl\n\xb9&çyk\xa3j/\x13%\xd7JH\xe7\xff\xc8+\x81ҁmV\xb5pd\x06\xbf6h\x1d\xa9n,\xf6\xce'3\xb0\xa2\x03E~\x80\x8f\a\xdcK\xb8c5Vw\xcc⟬+Ҋ\xcdH\t\xcf\xd2V?E\x1b\x0f\x0e\xf4\xf6:\xda\xfc\xea\x88j\xc7\xfem\xa91'\xcd\x12\xb94U\xacE\x8c$ke\x80M\xc6\x0f\x99J\xbb\x00\xfa\x92\x11e<\xe8\x9c\xd9\xd1\xf7&%\xa8E,{\x8e<\xc6;\x1b\x03U5\fT\xfdo\x12#\rje\x85Sf\x7f\x88\x94c\x938\xaa\x1d\xfar&s\xac\xae\xd9ޝ\x9f\tBr\xe2\x1d;\x93&g\x14\xa4z\xa0J\x16\x8a\x0e\xd9D\x1dp\xefh\x1cٹE\x97ެ<\x1aل\x84C\x8e\t\xfd\\r\xbc\xed\x95R\x15\xb21\x9bZ\xf13\x9b~T\xd1q\x18\\\xa3A\x1f\xff\x83\x9b\xd5\xca;cDŽl\xddGH\xb9\xc1\xa9\xc4>V\xe4n\x8e\xa9\xe6\xb8\x1d\u0089\x90\x94\x04\xfc\xfa\xf1\xbe\r;\xadeE\xe8\x93\xc8\xd2\xe7'i\x16\xf4\xad\x05V\xdc\a\xea\xf3k'-\x84\xbe\xfbu\x00\xe1}\xafS\xc0@\v\xccq\x10\xf7@H\xeb\x90\xf1\xd8H\xee\xc6`\xec{\x11|\xeaQ\x90\x10\xaeE1>\x92J\x80\x91\x8f\x17\x1c\xfe\xbd\xfc\xef\xc3\xfc_*\xec\x03XN\x99\x90\xbf\xab`\x8dҽ\xe8\xee+\x1c\xad0\xc8\xe9\xf6\x81\xb3\x9aI\xb1F\xebfQ\x1a\x1a\xfbӫ\x9f\xd3\xfc\x01|\xaf\f\xe0\x13\xa3\xa4\xff\x05\x88\xc0y\x176Z\xab\x116l\xbc\x93\b;\xe1J\x0fT+\x1e7\xb8\xf3[plC'&l\xa1A\xa8\xc4\x06\xd3\xec\x03\xdc\xfa\xe4\xe9\x00\xf3wr)\x7f\xdc\xc27\xc1I\xdcҟ\xb7\x01F\x97 \xf4\xbd\xce\x01\x8e+\x99\x03gDQ\xe0!ў\x18\v\x054\n\x05߂2\xb4W\xa9z\"\xbc`\xd2Sp\xc4\xc8'\xf0~z\xf5\xf3-|3\xe4\xe0\xc8RBr|\x82Wt\xc6=7Z\xf1og\xf0\xde\xdb\xc1^:\xf6D+奲(A\xc9j\x1f\xf2\xcd-\x82U5\xc2\x0e\xab*\v\xa9\x18\x87\x1dۃZ\x1fY\xa7U\x11\x99&\x03͌;\x99\x8eE\x1eN\x1f\x9ai~\xd2~\xcf;/>_y\xd6\xe9\xfdb\xb1\xfe\x99L\xf8\xc4\xfc\x13\x98\xe8_u\xae`bӬ\xd0Ht\xe8\xc9\xe0*\xb7\xc4C\x8e\xdaٹڢ\xd9\n\xdc\xcdw\xcal\x84,22\xc6,h\xdd\xce}1g\xfe\x95\xff\xe7ڍ\xfb:˧\xee\xde\v\xf9r\x14\xd0\xeav~\r\x03m\x1e\xfd\xfc\xd8u\x94\x87e\xcc\xec\xc62\xe9\xcc\xefJ\x91\x97\xed\xad\xaa\xe7mkƃ;fr\xff\x85\xce\x0e\xf1\xdc\x18B\xb4\xcfb)2c\x92\xd3\xff\xad\xb0\x8eگ!\xb6\x11\x9f\xe4\\>ܿ\xfd\x92'\xaa\x11\xd7x\x92#\xb7\x85\xf0=e\aTY\xcdt\x16F3\xa7j\x91\x8fFS\xae|\xcfIIk\x81\xe6L\xf6\xf7n0\xb8\xcd\xda\x13Yw7梴\xdbJ\xa6m\xa9\xdc\xfd\xdb38\x96\xdd\xc0\x16\xc3A\x871\xe9leё8\x99k>\x03\xcfR\xfc\x96p[ID4\xb4\xc5T\xa9B\xe4\xac\x02\xeb\xdbd,VF\x98\xad\xec)\xa0T=r\f\xb7_U\xec\xe1\xf5\xbe\xe0ḧ\x1dB\x1e\x8enQ+#\n!Yu\xf0\xd8\xfe\xea(Y\xcd\xfc_\t[\xad\x99\xd6B\x16\x17q\xdbַ\x96蜐E\"\xd1\xef\x97\xdfO]\aN\x9e\x93\xf3.\xe0\xc3\b\b0\x83\xc0hO\xa4\xaa\r\uecd0uj&(e\xa4\xac0\xa6\xd6+\x04\xa6uEy]\xc8$S\xbe\xa9\xad\xd6\xe5J\xaeE\x11+\xa7S\xa6dSUlU\xe1\x02\x9ci\x8e]ڒǽ_(<\xa3\xf1\x0f\xbd\xa1\xad\xbaϔ*ӻ\x1a\x140\xa7\x9bA\xd9\xd4S(\x19l\x94\x16,\xd1N\x87s☨\xe3\xf6\xf6\x12\x93\n'\xff\f\a\xe1Μ*8D\xc7\x11\xaf!\xf1\x8a\x1d\xdcG:\x9a_\xeaP\f\xfe\xdaНj\x880K\xd7VFc\xb4\xe27c\xd2\xfa\xbex\xd4y\xf0\xa4\xe3\x8e\xe1\xa1\x1f\xf5\x06\n\x9eU\x96\xf2\x85\xf2K\nS\xe19,\xf2\x1e\xd2\x00\xd7>\x92\xd1\x05\xe3\xea\xd2\x14\xdda\xb5C\u07bd!\\S\xb7y=\x16\xe2\vʆ\xc7C\"j\xec\x8a\x1c\xd1N̡\xec\x12B\x8c6\xa8Y\xd2\"\xc0?\nX_\x18\xfd\xda\x06i\xc2Bc\x91{\xdf:Y\xfchL\xe0\xccaF\xf3\xafs \xe9bWx\x9e\xeb\xbf\xc2\\U\xf9\x9a\x8a\x99r\xc8:\xda\xfc\xfbP\xfb0\x98\xa2\xec \xaf#,\x88C\xee\xafܠ$\xac\x99\xa8\x90C\xf7,}1\xf3\t\xd0\xd3d\xecs\x92_\xa3\xb5\xac8\xe7\xb4~\f\xa3B\xe5-N\x01\xb6R\x8d;b\x95_\xdbx\xb4.\x8a\xc9R\xf1sH\x1e\x14\xf70\xe4\xf1'\xb7)\x9a\x84Z\xfa\xcfp\x17a\xf4E\xcdsEJ\x1a\x93r5\x1d\xe4Ӿ\x06Nİ\a\xdc%Z\xdb\x13\x9c\xe8z\x8cn!\xd15\xf9=@\xbf3T\x92S9Mۗ\x94\xd9=\xb6'\xfa\xbe\xf7\xc7\xe5\"\xb6#\xbek\x1cBW\x87.U\xd5\xfa\x00\xffH.\x9bz\x85\x86T\xb1Je\xc4\xc0$\xefk.UL\xe8$\xb4a8\x88\x8a\xf5\xb0X@\xf7\xa7\xdc)\xe0\xc2\xea\x8a\xed\xbb\xcd\xf8\x1b\x1c\x1d\xe9\xf4s\xc2\xe1\\\xb5\xbe\x8a\"ϑ\xbc\xedt\xa5\xba\xfb\xd1B\xfa~z:Ӈ3پ\xef\xef~\x8c\xf0yV8\x91w\x0e\x7f\x1cr\x8d\x81,\a\x12\xce\x05\x8b\xf8c\x95\xcb}\xfcp\x99?ӽ'ٛ4z\xe4\xbc';>y\xf5[\x9aU\xf7\x1e\xbc\x80\xdf\xff\xb8\xf9\x7f\x00\x00\x00\xff\xffϡa\xa5-&\x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=Ks\xdc8sw\xff\n\x94sؤJ#ǕG\xa5ts\xb4v\xac|\xdf\xca*\xc9\xf1\x9e1d\xcf\x10\x9f@\x80\v\x80\x1a\xcf&\xf9\xef)4\x1e|\fHbF\x1a\xednjyQ\x89\x04\x1a@\xbf\xbb\xd1\xc0\xacV\xab7\xb4a\xdf@i&\xc5\x15\xa1\r\x83\xef\x06\x84\xfdO_>\xfe\x9b\xbed\xf2\xdd\xd3\xfb7\x8fL\x94W\xe4\xba\xd5F\xd6\xf7\xa0e\xab\n\xf8\x116L0äxS\x83\xa1%5\xf4\xea\r!T\bi\xa8}\xad\xed\xbf\x84\x14R\x18%9\a\xb5ڂ\xb8|lװn\x19/A!\xf00\xf4\xd3?^\xbe\xff\xd7\xcb\x7fyC\x88\xa05\\\x11\x05\xdaH\x05\xfa\xf2\t8(y\xc9\xe4\x1b\xdd@aan\x95l\x9b+\xd2}p}\xfcxn\xae\xf7\xae;\xbe\xe1L\x9b\xbf\xf4\xdf\xfe\x95i\x83_\x1a\xde*ʻ\xc1𥮤2\xb7\x1d\xc0\x15Q\xbe\xb9fb\xdbr\xaab\x877\x84\xe8B6pE\xb0}C\v(\xdf\x10\xe2\x17\x85\xfdW~=O\xef\x1d\x88\xa2\x82\x9a:\xc0\x84\xc8\x06ć\xbb\x9bo\xff\xf40xMH\t\xbaP\xac1\x88\x9a\xffY\xc5\xf7$,\x810M(\xf9\x86(\xb0\xb3A\x92\x10SQC\x144\n4\b\xa3\x89\xa9\x80Ц\xe1\xac@\x8a\x10\xb9\xe9A\n\xbd4\xd9(Yw\xd0ִxl\x1bb$\xa1\xc4P\xb5\x05C\xfeҮA\t0\xa0I\xc1[m@]F@\x8d\x92\r(\xc3\x02\xba\xdc\xd3\xe3\xaa\xde۹\x85\xd9\xc7\xe2\xc2\xf5\"\xa5e/pK\xf0\xf8\x84ң\x8f\xc8\r1\x15\xd3\xddR\xc3\xf2\b\x15D\xae\xff\x06\x85\xb9\x1c\x81~\x00e\xc1X궼\xb4\\\xf9\x04\xca\"\xab\x90[\xc1~\x8d\xb0\xb5]\xb8\x1d\x94S\x03\xda\x10&\f(A9y\xa2\xbc\x85\vBE9\x82\\\xd3=Q`\xc7$\xad\xe8\xc1\xc3\x0ez<\x8f\x9f\x90xb#\xafHeL\xa3\xaf\u07bd\xdb2\x13d\xad\x90u\xdd\nf\xf6\xefPlغ5R\xe9w%<\x01\x7f\xa7\xd9vEUQ1\x03\x85i\x15\xbc\xa3\r[\xe1B\x04\xca\xdbe]\xfe]$\xea`X\xb3\xb7<\xaa\x8dbb\xdb\xfb\x80\xa2r\x04y\xac\x109\xc6s\xa0\xdc\x12;*\xd8W\x16u\xf7\x1f\x1f\xbe\xf6\x99\x92iO\x94\x1eoN\xd1\xc7b\x93\x89\r(\xd7\x0fY\xd3\xc2\x04Q6\x92\t\x83\xff\x14\x9c\x810D\xb7\xeb\x9a\x19\xcb\x06\xbf\xb4\xa0-\xbf\xcb1\xd8k\xd4Gd\r\xa4mJj\xa0\x1c7\xb8\x11\xe4\x9a\xd6\xc0\xaf\xa9\x86W\xa6\x95\xa5\x8a^Y\"dQ\xab\xafeǍ\x1dz{\x1f\x82\xae\x9c \xad\xd7\"\x0f\r\x14\x03I\xb3\xdd\xd8&\xa8\x8b\x8dT\x03%c\xbb\fq\x94\x16~\xfb8-b\xd5\xe2\xf8\xcb\x12\x97\xd9\xe7\xdfco\xcbovf\xad`\xbf\xb4\x80\xcaԉ?\x1c\xea+\xd5S\xfa\xc3Dzј\xba\x93\x88\xb6\x0f|/x[B\x19\xf5\xfa\xc1\x02s\x96\xf1\xf1\x00\n\x9aCʄ\x15\"k\x97\xecZD\xf7\x15\x158U@\x844\txL8x\x84\t\xc4@\x92&\xd8\xd0@\x9d\x98\xf1\xec\x92\t\x11-\xe7t\xcd\xe1\x8a\x18\xd5\x1e\xa2\xd1\xf5\xa5J\xd1\xfd\x04\xb6\x82o\xf0,dE ^\xd5pV ɣBA|\xfdqQŴU\x94a\x95w\x92\xb3b\xbf\x80\xaf\x8f\xc9NAZ\xbd\xec\xfa\x15\x925T\xf4\x89I\x95\x12\x03\xa9\xb0iϞwjZZ-遌m\\悓Ȫ\xa4|\\b\x88϶Mg\x1dH\x81\xaef\\\x8a\xa7\xb6\xb7\xddk \xf0\x1d\x8a\xd6$\xa6IH٢i\x92\x8a4R\x9bi\xbaO\xab.\xd2w\x8eR\x1fg\x98\xe6`eIVw\x8fW\u0081\xa8\x16\a\x03\x85,\x05\xd8eԖ\xa8][%[\xd7v\x12)dM5\x94D\x8aɑ\x91]Z\x0eڏU\"gtz\xe8\xa2[?z<\x84\xd35p\xa2\x81Ca\xa4:Df\x0eJݓ\xa3X'P\x99ЦC\t\xe8\x160\x03\x92XN\xdfU\xac\xa8\x9c\x87a\xd9\x13\xe1\x90R\x82\xb6\xda\x04]\xe6\xfd\xd4\"\xc9\x12\xf9\xfd sڣ{\x16\xc4j\f/\xa5Q\xba'C\rwO\x12\xb5\x9d\xee=\xd0-\xfe\xbd\x91\xb3\xcb\xfe\xff\x89\xd8`LN`\xda\x19\xf9'\xe8~f\xf3\xf4$\xdfb\x84\a\xfa\x92\xdcl\bԍ\xd9_\x10f\xc2\xdb%I\xa0\x9c\xf7\xc6\xf8\x03\xd3\xe6x\xa6\xcf$M\x8eL\x9c\x890q\x88? ]\xd0d{\x84=\x82Igs\x0e\x9f\\np\xcf#$\\\xff\xd43\xc0\xa1\x9d\x93\x0f\x8b\x1d\x9e\xec\vD\x04\xc6\xf0\xb9l\xe0\x1e/\n\x89\xdcI\xfa\xc9\xd4%\xe1\t\xb8?a\x99Y\xac\xd2\x1f\xa3\x9f\xfaD\x0e\xf8A;ZZ\x89\xa9\x98\xcfij@\x99\xc9%\xa8{\xbeQ\xce\xca8\x90\x93\x91\x1bqAn\xa5\xb1\x7f0@\xd3\xc8(?Jз\xd2\xe0\x9b\xb3`\xd4M\xfc\x9c\xf8t#\xa0\xa0\t\xa7\xe5-\xc2\xfa9?g\xd3,\xb7E\xdc3Mn\x84\x8dW\x1cJ2\x87\xc2\xf4\xae\x1b\xce\rT\xb7\x1a\xd3uB\x8a\x15\xda\xcc\xe4H\x1e\xdfR\r\xd0\xfd\xecA\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\xef\xabǘ/XY\x93\xb3\xf2\x10\x8c\xac3p\xe0uw\xb9\xbc\x9e\x95\x95ٌV\x81\x13\x16\x9bN$G\xa7\x9b\xe6 \xe5\x19\xe8@+\x8e.\xce\"uiY\xe2\xe6\x1a\xe5wGX\x94#x\xe1X\xd5Л\xbb3\xc15m\xacZ\xf8okiQ\x9a\xfe\x974\x94)}I>\xe0N\x19\x87\xc17\x9f\x87\xeb\x81\xc9\x18\xb2\xb1CY\xfey\xa2\xdc\xda~\xab\xc0\x05\x01\xee<\x01\xb99\xf0\x8b.Ȯ\x92ڙ\xed\r\x03\x8e\xfb\x15o\x1fa\xff\xf6\xc2\x0e\xbf8d_ɼ\xbd\x11o\x9d\x0fq\xa00\xa2\xc3!\x05ߓ\xb7\xf8\xed\xeds\\\xa9LN\xcdl6`њ6y\x1c*\x92\xc9\xfa\xee\x19pL?7\xdf%当=\xb7\xda,\x16m\xa46\x9f\xd3yÉ\xf9܅\x1eC\xcf8\x91c[\x8c\x18|\x1e-\xea{\xebDn\f(\x9fKt6 \xc4\x1fό\xccR\xbb2\xfd\xc9\xc6d \x8d\xf9]\x8b\xe0\x05nr\x1b79S<\xc6a\xb5x9\xd2\xdb\xff\xf8\xbd\x97ϴ\x92k\xff\xef/\xe4\xa5\x1d\xeaB\xd65\x1d\xefjfM\xf5\xda\xf5\f<\xed\x019\xea\xabm\x8b\xf2\x9ck\x91;\x1e\xc2\xfd\xcb\x1d3\x15\x13\x84\x06\xb5\x01\xca3\x14%\x8dL\xe5\xb0SOE5Y\x03\x88\x98\xa2\xff=\xb8\x125\x1378\x00y\x7f\x06\xd7#\xa2\xeb\x9c\xce\xeeu\xa4I\xa4||\xe1LV#K\xb2\xab@\xc1\x801\x0e\xf3\xee\xe8\xa9\niz)\x8b#\x1c\xd2F\x96?h\xb2aJ\x9b\xfe\x144iu.\xad\x8f$\x9f\x9d\xf7WV\x83l\xcd9\x11\xfc\xb1\x1bf\xb0\xd7\\\xd3\xef\xacnkBk\xd9:cnX\x1dwu=zw\x94\x99\xb8m\x85\xf9\x1b#-\t\x1a\x0e\x06\xc8\x1a6\xe9\xfd\xde\xd4SH\xa1Y\t*T)8\xb21i\x05sC\x19oS\xbbD\xa9\xe7\xd8\bX|T\xea\xa4\x00\xf8\x8b\xeb\xd9\xcb;Vr7DP\xe6\xdaq#\r\b\xdb\x10f\b\x88\xc2b\x1c\x94S\xc98\x84G\x06\xa2\x86\xe5\xea\xb9<\x05n\x1f\x10m\x9d\x87\x80\x15\n$\x13\xb3)\xb7~\xf3O\x94\xf1s\x90\xcdr\xde'\xa9\ue056\xa7\xe4h~\xeeu' t\xabp\xf3\xdf\xe9\x8e\x1d\xe3ys\xb6\x94#\x9c\xb6\xa2\xa8\x00\x95\x90\x18\xea\x06\a\x9e\tm\x80\xe6\xf2\x82\xf5\x8aZ!\x98\xd8\xe6\xd1.;\x11\xda=\x0e\xd5k)9\xd0\xe9]\xc8\uec78~\x05M\xf4s7\xcc35QG\x04\xb7m\x8etȦ\xa8UZ\x84\x1a\x03u\xe3DN\x12Պ\xbeu9\x83\":&\f\xf7\xb3x\xc9\xf8\x9a\t\x96A\xdb\x01]o\x043}\xe7т8\xab\xf3h\a\x88\xee\xc0)\x19\xb6\x9b\x01\x00+\xa0!\x0e\xc1\xb9G\xae9\u0091\\\x03\xa1e\t\xa5\xcb]ZWć%\xae\xf0m\xa2\xb8!\xb9\xba\xe3=\xc1,ʆg\x10tb\x1eV=\xc1\xaa\x15\x8fB\xee\xc4\n\x83q}\xb4\x0e91K\xf5\xdc\xe1\xcd\xc9\xcahY\xbf\xe4\xab\xe9%-4\xe4\xd7|\x9e\n\xfe\xd3\x19\xb4L6\xdf\x1c\x95\xf0\x98\xe3\x82%\xbd\xe6\n\xb0'>.\xcebn\xfc\x99\xce~S\xfa\xda\x15K?\xab,\xee&\r\xaa\xe7\x14\xee*0\x15\xa8P\x9a\xbd\u0092\xf4rv\x87\xb4\v^b\x9d\x9ce\xaa\xe0\"\xbb\xf2\xcfQ\xe5\x1cF7-\xe7\x17\x96\xb7i˓ᰑ(b\x87\x9c\x95U?\x96\xf6\x18r\xaa/\xb2\xf1د\xb4\x18\xd6\x17\xc6*\x88P`(\xc3ȞƩ\xf5baio\x7f\x7fXN\x81\xf9\xbf0\xfd\u07fc\xf40\xa3R\"\x1f\x8d\xb9U\x9a\x11\x89\tX\t\x06롱\xab\xaf\xf0\xed|\xa1\xef\xef\v\xa7\x06\xea/\x8d\x97\x98I\x176\x03\xad\t8\xa3z\x13\xb4\x06\xadv\xae@\xb4\x03>gh\xfb\x7f(\xdc)\x88\x00&ů_+\b\xe2\xeb\xab\xf7\x99&\xffL*\xd9&\xaa\xfafP\xb6Pݱ\xbc\xe0A\xa1\x87\xdfP\x00C\x9f\xde_\x0e\xbf\x18\xe9\xcb>0\x8b\x96\x00\x84AQ\x97\x99e\xa2dO\xacl)\x0fR\u06dd!p\f\xd4\xf1Y\x02\x9aTD0\xee\x180\xf4\x1f0\x1c\xf9Ҹm\x99\xa3Uܼ/\x9aW\x1drrMȰ\xe6c\xc2\x1a\x1e\xbb}\xf1\"U\xb0\xbfI\xad\xc7\xf1\x15\x1e9\x91\xc4B5\xc7\t5\x1c\x99\xc5b\xcf\xdeoɩ\xd28&\xe6>[E\xc6\xcb\xd7ad\xe1g\xb9\xe6\xe2\x18윽\xbe\xe2\x15\xab*^\xa7\x96\"\xb3\x82\xe2\xe5J!\xf3\xa2ϓJ\x01\x96\x03\x96\xe9*\x88\xc5ڇg\x054'-i\xb1\xa6\xe1\x98J\x86E\xea\xe4\x89٫\xd5*\xbcZ\x85\xc2\xeb\xd6%\xccr\xd1\xec\xc7c*\x0fb\x9c\xf4\x13m\x1a&\xb6\x87L\x91\xcb:\xb3l\xb3\xcc2\xb7\xa3\x89\fx\xa6\x1f\xcet\xd1\xe1D\xe8\xeb\x8eK'\"ɐ\xb6d\xc2\xc8K\xf2A\xec=\xdc\x04\x9c^\xf8(\xa498\xc8f\xa7\xb5c\x9c\xf7Ok!\xd8yP\xfe̤\xa6\xb5\x9bՔ\xb7\x9f\xa4\xabT\x03\xa7\xfc\xa4\xc0\xf1\xcb\bF?;\xfa\x9a\x9e\x7f\xddr\xc3\x1a\x0e֣{be\xf2\f\x99\xa9`\x1f\x91\xfc7\x89'\xa4\xd6{\x84\xf4\xe5>\xca\xe2\xe5(\x88\xa1\x9a\xec\x80sBS\xdcq\xb0\xfc\u009dL.\xe4\n\x8f\x04Z\xf2\x06&\xf1\xe7\x99/\x9c\x14\xe310\xa4^\x9d\x80[P\x81\xa7\x9bub!\x93\xe60G\x8b\x1e\xf8\xe5.\xba\xc0w\xbf\xb4\xa0\xf6D>a\t\x83\xf7\u07ba\xb3\n^\xddh\x1bc\x06\x05\xe8\x95\xf1Ԧ\xc2A(\xd3)(\xf2A8_b<\x1f\xecc5_\x17\xaaYun\xa3\xb0\xe4\x18\x13݅\x8c\xbd\x13ݖ\xdc\xfeܢ\xfe\xf3\x06nLJn\x8b\xbeR\xbe?\xfb\x1b\x15\xeb\x9fR\xa4\x9f\xb7\x1d\xb4X\x94\x7f\xae@n)\x94\xcb\xf6^\xf3\x8a\xee\x8f\xdbD=c\x91\xfd9\x8a\xeb31\x95SL\x7f\x1c\x9e^\xa1x\xfeU\x8b\xe6_\xabX>\xbbH>k\x1f3{\xd3*w\x9b\xf1Ī\xef\xe5]\xf7\xf9\xa2\xf7\x8cb\xf7\x8c\x9d\xb4\xe5E\x9e\xb0\xbc\x8cb\xf6\xe3\x8a\xd83h\x96+\x8a\xafX\xac\xfe\x8aE\xea\xaf]\x9c\xbe\xc0Y\v\x9f\x8f+B?y\a&l\xf5\xdf\xca\x12\xee\xa42K\xc1\xc9ݸ}b'\xb5\x17\xb0I^\x12\x11\x9a&V\x89!\x86\x0f/N[Tz\xd33\xb8\xd3?\xc9\xd2\xcemi\x8f\xe5~\xd4\xfc\xe0\xac\xf2\x06\x14\bw\xcd\xc7\x7f>|\xb9\x8d\xf0S>\xaf\xf7\x8cG\xd7K8\x0f\xa6\xf4\xc8\xf1[s\xbe\x98\xc9a\v}\x80\x17\xde\x17\xa1\r\xfb\x0f\xbc\xef\xed\x19\xe9\xa0\x0fw7\b#\xf8ix\x81\\\xac\xa2\x88;\x96k\xb0\x16+\xa2jR,n6\x03\x88Ê\xdf\xfe5JP\xba+\xb3\x82\xc5d\xa1\xc6\xcb\n\xdeݍ\x9b\xc7\xd4(\x9f\xac\xd3(\xf6D:\x8e\xac\x98*W\rUf\x8fl\xa3/\x06s\bff.\x9d3\xa9X\x0f\xaf\x01K\xa27\xdc\xfe\x85{\x91\xfbf\xb8\xdb;\xc6\xdd)\xf3\x98>\x7f\xb2x\xf2\xe4\x05\xe71m\xb1W\x88\xa9\xc4\xebd\x81ɋ\xa5\xc9\xd417\x05%e`\xe1ڠ\x9ej\xa0\xe4Z\x8a\r\xdb\xfeD\x9b`F\x1c>'\x95\x85O\xd14\x16\xb4\x05養u\xb5ihwzPi,a%t.eU~B\xc8w\x01\xb0\x06\xb7\xbd\xed\xb4R\\B\x03j\xd5\xe5ۺی\xf6\xcd\xf4l\xf5\xc5(d\xf5\xb7\xdc\fj\x17\xac\x1a4\xa0\x84\xff\x96\x9a\xab/\xb8y\xc0z\x9b\xdet\xf7q\xb2\x16\x1bv\x86\x96q\xfc\xe0x9\xd1fT\xac\x93\x00>ʧt\x18\xdcHUS\x13$\x00\x13z\xd4\xe1\xddݚ\xf6\xd0@q9$\xf9\x9f:\xf9O\x9d\xfc\xa7N~Y\x9dl\x95\xdbݷ\x93R\xe1\xf7\xb1\xf7\xbc\xefI9\x8f\xe9\xff\x04\x18\xdb\x1f\xddO-h\xa3\xab\xc45x\xcf\xf3?\xf1\x86HCM\xfb\x9cE:\x00\x83u\xb2\xa2\xeay\x90;\b>fX6J+vKjp\xe0\xfe\xa4\x15\xe3\x17\xbd\xec\xed\xeb\x94\xe9d^\xb1u\xf2\xe5Z\x0e=\x13\xea\aw$\xacj;\xc4\xd4\t\x05:\x8b\xe1v\xc6\xc1\x8f\xf9\xc4B\xe6\xd5Ly\x06\xe3\x84\xeb\x98\x10_\xb9\xb8\"\xc9[\x9a2ob\xfaM\x11=\xa3\xd5tQA\xd9r8\xf5\x1eև^\xff\xe5\x9bX\xc3h\x19w\xb1Zd\xf7\f\xb4\xf5\xb0\x86w\xbezJx\xc8}JN\x05ᘰqW>\x16\xeev\xe0\xa2\x00\xad7-\x0f\x95\xa3\x85\x02j\xa0\f͙\x8e3>\xaa\xf6\xb1m\xb8\xa4%(\xe7\x92-\xa0\xf5\xbf\x06\x8dG<[\xe0\xcbVu\xd7\xed\xce^U\xfa,\xcd\xd5PE9\a\xfe\x89q\xd0?ʝ\xb0\xf3\xca\x10ȻT\xbf\xdeY٢U֬\xef\x89h\xeb5(\xa2\xc1\x98\xe9\x04\xdeF\xaa\xf9S+\x0e\xefL\x18\xd8B*\xe7\xb9S\xcc\xc0CC\x95\x06\x9cQ\xc6\n~\x1euq\x19\xc1\r\xa7[W\x9e\\\xb2\x82\x1a\x88\x06\x18G\x98\x9a>\xf6\xd7\b\x8b\xef\xb1ZTNlDd\v\xf5\xd41\xb9I\xb1\x9e\xba\xf29a\xaa\x93\x97>;\x8b\\\xd0\xc6\xe0\xa1D\xa4#\x12\xd1x\x18x\x91\xfa\xe8\xde\xe7\x01\xd8iN\xf3GK|\x11\xb36\xb4ND\t\xcbz\xe7\xfa\x10\f^ծ\xca^-t\xff\xd2\xdbX\xf4LvT\xc7\x03.I\u07fb\x83\xed\xc0\xa0\xabnACI\xe0\t\x04\xb1\xa2H\x19\x87r\x8eS\xbf\xe2\xe6\x9ez\x02\xf5\x83\x8ep\xb0:۲\xf8\x83\xa1\xcaĩ\x1f\xfa1.\x86\xbb\"%5\xb0\xb2\xbdOs\xdd\xd2WW+ub\x89\x06\x9e6\xf6\xe2Q\x84\xa3\x90\xd6\xfa\xb93\xc25hM\xb7!1\xb8\x03\x05d\v\xc2\xe2=\xee\xf7$=\xa6p\xcc\xda\x1b\x8bAb\x80\x16\xa6\xa5~\x00\xe7\xc2Ŋ\x96pg\x10\x9cC\x1d^1\xe2\f:\x9f\xaa3\x18\xdc`D\xb4\xc5\xde)ʄ85v3\x1dv癚\xaf\x11ʔz\xf4\xeb\x1b\xfc8\x82/z\xf1\x8d,ي\x8a\x8a\xed\xe4!\xe3J\xc9v[\x05ޜr\x88H\xd9b\xe4ܠ*\xd0\xe1ǜL\xabD\xaf\x90\xc2\u05fdMi\xe98\xddi\x1f\xe5\x19\x8aZu\x87\r;U5c\U000f3cc4\x13\x10\x17m\x7f\x02\"\xd5{Q\xcc\x1e\x8b<ܣ:ʵL\"!j\xe3\x17CB\x848\x85\x84\xbe/\xd1E<\xbf\x1b\x8cL\xf9('\xa2cމ\xc1%\u0383Z^t\xdf\t\x1a\xba;ǡC\x0f\x82\xbf\x93\xd2n\x03\b\xc7D\xbe8v:\xee\xfd\xfdF\xacO\xd1\xdb\xfaxr\xec\xfam\x04ct,\xddF\xb1\xdd0!\xde\xfc{\xb6Iɋ\xfbż5\x87\x7f8\xf8\xfa\xca\xc7\xcbwT\t&\xb6'a\xe4g\xdf7\x11\xcf{\xb0\xe7\x8c\xe8\xc3\xcc_,\xa6O\x9a\xa5\x83\x97\xc8\xe0e\x0f\xcf~$\xff\xe6\xff\x02\x00\x00\xff\xffJ\xb7g~\xf1r\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?\xbfp\x10jQ\u0605\x90\xdd~\x82>Ǟ\x8e#\x88\xa3\xafc\xba\xb9U\x1f\x8d\x9b\x90ϢO\x04\xe6\x88\xf1\xacT\x1e&\xfaF\x14\xc0\xcc\xceX(\x83\xaaoåN\f8,\xe4`\x15\x85\ac\x90\xde~P\xe3\x04\x91uQ\xf0u\x01\x97d\x19\x0f\xd0l\\I\x8f\x11\xed+\x18+\xb2S\x92\xccA\x1c!\x98\xf6?\xf4(C\xf1\x16\x7f\x00\xc6#\xe0==1\xb8+\x8a\x0e\xd1\xfbԊ\xe2Vi\xc8\xd0\xf1\xbf\xf4\x01\x85\x80\x82\x82\x18\xa9X\xa1\xe4=h\x87Ec\xe0\xd1\xc0\x00\nh\xce\xd0W\xd7h\x96\x85d\x9b\x1a\xdd\xf8%C-\x11\x95\x11!\x8d\x05\x1e\x11\xe6\x13\xf0\x0e\xbe\xa1E\x80\xfc\xaa\xa8\x8d\x05}\x93\xa9\n\xf2\xb0j7j\xceRy\xf8\xe1 d\x1f\xf4\x15\"\x03\xe4C\xe6*-he,&\xdam\xfc\x87\xe6\x91\x16\xf1\x90\xd5~\bm`7\xa9[\fXlx\xfe\xfb\xf3\v\x92\x80~\xef\xfd~\f\xe3\x1a\x1a2\xcd\xd2\xcd\xe4&\x8d\xb7\x10\x16\xca\bu'u\xd4\f\xbes\xad\xf9\xee\x00כ\x15\xc8\x17\xe0{\f\xf6\x80\xf32T\xfbN\xbc\x1f\xf6\xff\xaf\xc8\xfd\xd3\xf2\xdb\xd0*>\x17\x12\xf9\\\bc{l6n\xe9\x0f\xc9:\x16w{\x02I\a\x13\xd5\xe4\x14W\xffA\x88yҹ\x13\x9b,\x8dl\xfa\t\xf0OEɭR\x0f)\xd4\xfb\x1f\xac\u05ee\xfb\xb1\x8cv\x9a\xd8\x1a\xb6\xfcQ(m\x86k\xcb\xf0\r\xb2\xdaF5\v\xb7,\x17\x9b\rh\x84E{#\xcdV\xca!b\x1d\x8e\xf9XGeE+\f\xc6\xd52\x1dYJԈ\r\x85\xa2\xfa(T\xe7\xe0`hA\x0eD.\x1eE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7T\xa8$\xa0\x8f_bl\xb4_5N\x89\xb0\xfer\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccr\v3\x05_C\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fm\xb5\x83\x99\x00\xcb(\xc4݊l\xeb\xdcW\x144\x82\xc5r\x05\x86\x96\x92xU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8\xe6\xc8(\xbe\xffJD\x0fV\xe7Ha\x9f\xd0$\x8c6Y\x92\xe7C\x94\xf4Hq\x01f\xd9Y\xd2\x146|Mah\xcf\x7f\xdc\xdb\x7f\xda#ʯ\x8bw\xc7M\x98\x19\xac\x9b\x9cS/˸\xa6\x9b\x7f\x12\xbe\x91ɺ\xf1\x16k\x16\xcf>u[^\xd0V\x8agH~\xc16\xa2\xb0@N\xd5\x14\xa2l\x06\xe7NI\xa0T\v\xcchg\xddf\xdb\x0f͆[B\x8b\x01\xad\x86\x00\x9c\x83\x1e\xa2\x1c\xe2A\x02Hָ\x16\xb4\xd3,\xdcJ\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\xffp\xc18{\x80\x9ds\xb1\xb8d\xc87\x1e*'\xa2\xa0\x81\xd2(HS<\xc0\x8e@\x8d\xe7E\x8c\x979\xd2\xe2\xca\x03\x8cl\x95\xc6J\x8f\xae\x88\x9f߀rt\xc3\x0fD\x98\x94\xd9Ԗ\x86\xa8~\xfa\x8cd%\xc4\xcb\f\xbd\x14J\xe0ˑ\xc3N\x16\xa7n_\xfdL\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\xfb\x87\x03t\x96\x04\x89\xd8\u0378q\xb6\x14\x05\xb5\xe1\x870l%1$s$\x9a\xd1\x1d%X\xb9.]gemh\x7fZ*\xb9p\xcbbc\xbdy\x1e(\xddc\xc1I:\xf6\x9dޢ1r\xbf\xb8T\xaf\x82g\x90\x87-:\xcaA\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\xa2\xceXY\xe0\\O\xac\x19\xa4%\xa9z$\x8d\xe9p\xf5Tb=\x93L\xe4E\x90ە$\x05\xddl\xe0y\xd6k\xa6\xdc\x1c\xa3b:cq.@\xc9ik\xedoh\xe9i6\xfe\x9dU\\h\xb3d\xef(\x1d\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\xa3\x9c\xd4mWa\x9d\xaf\xe4\xb9\xf3e\xf6\x14O\xe3\xf8(Y\xec\xd89\xfdv\xfe\\\xf7n\x86DϨ\xda\x13\xe5\x92W\xe9\x92L\xc9\xc6s\x02\r\fփC\x84\x8d\x9b\xac[\f\x10\xa6(\x90,ʕ2\x91d\x91\bZ\t\x82~\xad\x8cu\xeb\x90=\x7f\x7ft\xa1R\x85\xc5I\xc67\x1643V\xe9\x90NJ\x8a?e)\xbe[n\xb7`\xc0\xefC\xf9EO\a\x18\xa3\xd8\xf3V78\xabr\xee\xf6¨#\x9e\x91\xf7Dm+\xad20Ѽ\x88\xb6$ڦ\x91\xac\xa5.\x1d\x9au]\ue8bfM\x92\xd6NY\x94\x0ee\x9e#\x8f\xa4;\"2\xfa\xf0\xad\xb3D\x8d\xda\x05\xffN\x91\xd6cpdt\x00\xa6,\xf90\x87:\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\x90:\xe9\xb2\xcd\xe7\xc4ہ\xa4[\xfe\b>\xdd\x17d\xa6jIKa\xa8\a\xb0\x9b\x19\x10\x1dk\x9c\x15H\xb4w\x9d\xc6\xd1\f̱\xb2 I\x12rrݬ\xdb\xe4#\x17i\xebV\xec8\xb6\xdaC9\x9cc\xe5\xf8y\x14\x12<\xbbg\x10J\xfeM\x94u\xc9x\x89<$\xb7C\x94\xd0\x1cCp\xecn\xd2>\xb1\x05\x19-\xabp\x96U\x05X\xf0i\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\xb4c.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;z\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*\x9fbK76D\x80\xd1%\x1e<\xa7\x9b\x1b:\az\xbc\x1e\b\xf7K\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~3d(\xd3\x0e\xc0\xde\xc5Σ\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^\xc4\x17\v\x9a\xb7\xb0k.\\\xfaY\xd11\x7f\x7f\xdbؗ\xafͬ]\x0e\"3n\xd8\x13\x14\x05\xe31\xfd\xb2G\x85\xcc]\x00\x9b\xa9\x05\xa0}GM\xe5/\x94\xf2\xb7\xc6^\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]0\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ɿ\xe0@\xafu\x1c\xda\xcaG\xc9\xecxD\xbd\xbd\xd5\xd6]\xeb;\xc4\xfe\x19\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\xee\"\xffc\xd7{*\xf1'zg\xeb\x04+\x87\xef\xaeW\x04+\x88\x11=\xe0\xd5$Y6,_\x03\xba\f\xed\xd8\x0f\xe9\x93զ\a\xb5\x9f\xe7\xdc}\xa5\x04r\xf7$Mp[\xbcj\xce\x14j\xad\xeb\x95\xc3\xe5PO(_\\\xee\x98\xf2o\x8e\b\x9d/*\xae\xed\xce%D]\xf4\xf0\bv}j\xe5\uf835\xda\x7fr\xa7[zd\x0f\xaf\xed\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\xca\xc0_\xd4#\xbc\x8f\xae\xbe\xf6\x9f\xdd\x194\x19I\xaf\fP\xe9\xa2\xfc\b\x05ۜJ\xba\xa7\xfcyj/\x9e/\x19P\xf1\xf7\x9c?gq\xf2\xa6\x0fj\xfc%\x1a\xba\x05>t\x1a\xf3\xaa药\x1d\xbb\xbe\xa3\xb8\xb5Q\xa5~\xea\xfb\xb85,O\x86$\x89\b,!\x0f>\xces*2Z\xa5\xf9=|R\xeeQ\xa5\x141\xe9\xb7\xe8=\xb9\xe5=\xb7\x90s\xee'aL\xd1\xfb\xb1\r\x01\xb6gL\xf6\x1e+@l\x8f|\x8e\xc1\xda\xe292r{\xfbɍ\x94\u07b2y\uf7e5A}l\x00Y\x10(࠭\xf1\xbf[\xf5D\x97\xf8\xc7ט\xc3\xcb1\x9d\xc7\xeb\x80\x0e\xbbP\x1a\xf2Qì\xabB\xf1\x1c\xf4\x15\xbd\x9e\x930\xe2\x9fz\r\x06\xee@\xff\r\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˸\u05cc\xc2\xf6\x9a0ͫv\xfb\xf4\x9cP!qK7\xee\xb0-\xc6\x1f\xe1Z4\x8f\x85\x9d%\x90۽\xe1\xd4\a<\xfe\x96\xa1{\xec)㕭uЮ\xb5\xa6\x9b\xe2\x11\b\xb8\x8bԏ{Ͱ}\xe4\xee\x18\x06\xb7\xaf̵\xfb\x0f\x93\xef\xe0\x8e\xc0i\xde#\x8c>n\xe6\"j\xf7N\xed\x02\xe1\x1f\xc7\xe3\xd1\x19\x838߸G\xeb&\x88\xf0\xa9\xad96\xe0f\x188d\xff\fޫ\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\x011\xefH\x8e\xf7һ_\xeau\xfb(\x04\xfb\xdb\xdf\xcf\xfe?\x00\x00\xff\xff\xf8FZ\xc4S}\x00\x00"), + []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?\xbfp\x10jQ\u0605\x90\xdd~\x82>Ǟ\x8e#\x88\xa3\xafc\xba\xb9U\x1f\x8d\x9b\x90ϢO\x04\xe6\x88\xf1\xacT\x1e&\xfaF\x14\xc0\xcc\xceX(\x83\xaaoåN\f8,\xe4`\x15\x85\ac\x90\xde~P\xe3\x04\x91uQ\xf0u\x01\x97d\x19\x0f\xd0l\\I\x8f\x11\xed+\x18+\xb2S\x92\xccA\x1c!\x98\xf6?\xf4(C\xf1\x16\x7f\x00\xc6#\xe0==1\xb8+\x8a\x0e\xd1\xfbԊ\xe2Vi\xc8\xd0\xf1\xbf\xf4\x01\x85\x80\x82\x82\x18\xa9X\xa1\xe4=h\x87Ec\xe0\xd1\xc0\x00\nh\xce\xd0W\xd7h\x96\x85d\x9b\x1a\xdd\xf8%C-\x11\x95\x11!\x8d\x05\x1e\x11\xe6\x13\xf0\x0e\xbe\xa1E\x80\xfc\xaa\xa8\x8d\x05}\x93\xa9\n\xf2\xb0j7j\xceRy\xf8\xe1 d\x1f\xf4\x15\"\x03\xe4C\xe6*-he,&\xdam\xfc\x87\xe6\x91\x16\xf1\x90\xd5~\bm`7\xa9[\fXlx\xfe\xfb\xf3\v\x92\x80~\xef\xfd~\f\xe3\x1a\x1a2\xcd\xd2\xcd\xe4&\x8d\xb7\x10\x16\xca\bu'u\xd4\f\xbes\xad\xf9\xee\x00כ\x15\xc8\x17\xe0{\f\xf6\x80\xf32T\xfbN\xbc\x1f\xf6\xff\xaf\xc8\xfd\xd3\xf2\xdb\xd0*>\x17\x12\xf9\\\bc{l6n\xe9\x0f\xc9:\x16w{\x02I\a\x13\xd5\xe4\x14W\xffA\x88yҹ\x13\x9b,\x8dl\xfa\t\xf0OEɭR\x0f)\xd4\xfb\x1f\xac\u05ee\xfb\xb1\x8cv\x9a\xd8\x1a\xb6\xfcQ(m\x86k\xcb\xf0\r\xb2\xdaF5\v\xb7,\x17\x9b\rh\x84E{#\xcdV\xca!b\x1d\x8e\xf9XGeE+\f\xc6\xd52\x1dYJԈ\r\x85\xa2\xfa(T\xe7\xe0`hA\x0eD.\x1eE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7T\xa8$\xa0\x8f_bl\xb4_5N\x89\xb0\xfer\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccr\v3\x05_C\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fm\xb5\x83\x99\x00\xcb(\xc4݊l\xeb\xdcW\x144\x82\xc5r\x05\x86\x96\x92xU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8\xe6\xc8(\xbe\xffJD\x0fV\xe7Ha\x9f\xd0$\x8c6Y\x92\xe7C\x94\xf4Hq\x01f\xd9Y\xd2\x146|Mah\xcf\x7f\xdc\xdb\x7f\xda#ʯ\x8bw\xc7M\x98\x19\xac\x9b\x9cS/˸\xa6\x9b\x7f\x12\xbe\x91ɺ\xf1\x16k\x16\xcf>u[^\xd0V\x8agH~\xc16\xa2\xb0@N\xd5\x14\xa2l\x06\xe7NI\xa0T\v\xcchg\xddf\xdb\x0f͆[B\x8b\x01\xad\x86\x00\x9c\x83\x1e\xa2\x1c\xe2A\x02Hָ\x16\xb4\xd3,\xdcJ\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\xffp\xc18{\x80\x9ds\xb1\xb8d\xc87\x1e*'\xa2\xa0\x81\xd2(HS<\xc0\x8e@\x8d\xe7E\x8c\x979\xd2\xe2\xca\x03\x8cl\x95\xc6J\x8f\xae\x88\x9f߀rt\xc3\x0fD\x98\x94\xd9Ԗ\x86\xa8~\xfa\x8cd%\xc4\xcb\f\xbd\x14J\xe0ˑ\xc3N\x16\xa7n_\xfdL\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\xfb\x87\x03t\x96\x04\x89\xd8\u0378q\xb6\x14\x05\xb5\xe1\x870l%1$s$\x9a\xd1\x1d%X\xb9.]gemh\x7fZ*\xb9p\xcbbc\xbdy\x1e(\xddc\xc1I:\xf6\x9dޢ1r\xbf\xb8T\xaf\x82g\x90\x87-:\xcaA\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\xa2\xceXY\xe0\\O\xac\x19\xa4%\xa9z$\x8d\xe9p\xf5Tb=\x93L\xe4E\x90ە$\x05\xddl\xe0y\xd6k\xa6\xdc\x1c\xa3b:cq.@\xc9ik\xedoh\xe9i6\xfe\x9dU\\h\xb3d\xef(\x1d\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\xa3\x9c\xd4mWa\x9d\xaf\xe4\xb9\xf3e\xf6\x14O\xe3\xf8(Y\xec\xd89\xfdv\xfe\\\xf7n\x86DϨ\xda\x13\xe5\x92W\xe9\x92L\xc9\xc6s\x02\r\fփC\x84\x8d\x9b\xac[\f\x10\xa6(\x90,ʕ2\x91d\x91\bZ\t\x82~\xad\x8cu\xeb\x90=\x7f\x7ft\xa1R\x85\xc5I\xc67\x1643V\xe9\x90NJ\x8a?e)\xbe[n\xb7`\xc0\xefC\xf9EO\a\x18\xa3\xd8\xf3V78\xabr\xee\xf6¨#\x9e\x91\xf7Dm+\xad20Ѽ\x88\xb6$ڦ\x91\xac\xa5.\x1d\x9au]\ue8bfM\x92\xd6NY\x94\x0ee\x9e#\x8f\xa4;\"2\xfa\xf0\xad\xb3D\x8d\xda\x05\xffN\x91\xd6cpdt\x00\xa6,\xf90\x87:\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\x90:\xe9\xb2\xcd\xe7\xc4ہ\xa4[\xfe\b>\xdd\x17d\xa6jIKa\xa8\a\xb0\x9b\x19\x10\x1dk\x9c\x15H\xb4w\x9d\xc6\xd1\f̱\xb2 I\x12rrݬ\xdb\xe4#\x17i\xebV\xec8\xb6\xdaC9\x9cc\xe5\xf8y\x14\x12<\xbbg\x10J\xfeM\x94u\xc9x\x89<$\xb7C\x94\xd0\x1cCp\xecn\xd2>\xb1\x05\x19-\xabp\x96U\x05X\xf0i\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\xb4c.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;z\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*\x9fbK76D\x80\xd1%\x1e<\xa7\x9b\x1b:\az\xbc\x1e\b\xf7K\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~3d(\xd3\x0e@\xf1=\x84\xf3\xb9dR\xba\xe7\x9a?+\xee\xfc2\x80\x85\xc2\x12\xdc\xd4W\x8c\x03ʺ\xb0\xa2*\xdaK\xecb\x01\xe7\x16v\xcdeE?+:\"\xefo\xea\xfa\xf2\xb5\x91\xf8\xe5 \xaa\xe1\x86=AQ0\x1e\x9b\x9b{T\xc8\xdc婙Z\x00\xdaF\x9c\xe5\xfe2&\x7f\xe3ꅛ.t\x1b\x00Y\xd82\xb6\xd4\xc7\xe5\u16fe\x0e\x1a\xb0T=\xb6登x\x83\xbe\xfdR\x83\xde1\xbaw\xac\xf1\xcd\xdaC\xa5~\xa2\x1b\fL\x83\xfa\xf1\xea\xf0О\xc9^\x80Ӫ\a\xf6N:\x8f`\x88\x13\xb5A\xbd\xd3\x06t\xa8Te\xecr>\x16&\xe8>\b\xa9\x1a\b\x91\xa6)\xce\xff\x9cS\x96/\x11ޝ\"\xc0K\xf2\x80\xe6y\xaf\xdf\xf1\xf4䱧&ӓQ\x92NI\xbeD\xb87'\xe0\x9b實\x9f\x82\x9c\xbf\xf1\xfc§\x1e_\xea\xb4\xe3\f\ua95en\x9cO\xbbW:\xcd\xf8\xea\xa7\x18_\xf3\xf4\xe2\xacS\x8b\xc9\xe9Y\xb32\x0e\xe6\xa4V=\xe3\xb8]Z.\xc1\xf4)\xc4\xc4Ӈ\x89\x99\x06i\x83?r؉\xa7\v\xe7\x9f*L\xe4\xef\x9c)\xfdʧ\a_\xf9\xd4\xe0\xf78-\x98 \x81\tU\xe6\x9f\n|\xf6\x96\x94\xd29\xe8\xc9m\xbf9R;)\xaf\xa9\xb1\\\x1f\xb1\xc1\xbeV\xb8M\x16k\xf5b\x002K\xfe\xf5\x03z\xe9\xe2\xd068Jf\xc7#\xea\xedK\xb6\xeeZ\xdf!\xf6O`\xb8\xadK\x03\x15G\x03@\x81\x1b\xa5fE]\x85\x0f<\xdb\x0ez\xd8r\xc36J\x97ܲ\xf3f\xb3\xf8\x8d\xeb\x00\xff>_2\xf6Q5\xb9:\xdd\xfbҌ(\xabb\x87\x91\x18;\xef6x\x9e\x94D\xa53\xf4|\xad\n\x91E|\xce\xd1{\xf5\\\x83\xbdˆ\xe8濬\x93-\x12\v|\xb0\xb9\b\xb7.\xf6\xafdv\x97\xe0\x1f\xb9V\xc2+\xf1'z\xa3\xea\x04\xabn\xef\xaeW\x04+\x88\x11=~\xd5$(6,_\x03\xba\f\xed\xd8\x0f\xe9\x93զ\a\xb5\x9f#\xdc}\xe1\x03r\xf7\x9cKp[\xbcj\xce\x14j\xad\xeb\x95\xc3\xe5PO(_\\\xee\x98\xf2\xefu\b\x9d/*\xae\xed\xce%\x13]\xf4\xf0\bv}j\xd5젵\xda\x7f\xae\xa6[zd\x0f/\xd5\xd0N\xf6\xae\xea'\x0f\f\xe9\xf9\x1c\x9c\x0e\x9f\xaa\x9els*2Z\xa5\xf9=|R\xeeA\xa2\x141\xe9\xb7\xe8=W\xe5=\xb7\x90\xaf\xed'aL\xd1\xfb\xb1\r\x01\xb6\xe73\xf6.\xfaGl\x8f|\xca\xc0\xda\xe292r{\xfbɍ\x94ށy\xef\x9ftA}l\x00Y\x10(࠭\xf1\xbf[\xf5D\x17\xe0\xc7טë+\x9d\x87߀\x0e\x8aP\n\xefQì\xabB\xf1\x1c\xf4\x15\xbd<\x930\xe2\x9fz\r\x06\xee@\xff\xfd\x1ao7#\xe3\t=\xbf`\x96\fztE\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{\xa3\x81\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}\xec\xbd/\x13tB\x8az\xbf\x1bo\xd9\t\x11:ډ|\xfa\xb8\x12\x8f\xc1\xe2ƨLPTA\x0f\n\xd1I\xa4\x97\xbbC\xfcP\x80x@:j\x03_\x9e$\xe8\xaf\xc1\x02\x99\x95\x8c\xbd\xdb2\xad\xfd~ڃ\x16}\xaf\xc5*\xec{\x04\xc6\x00\x00Sa\x9f˸\x97\x80\xc2\xf6\x9a0͋p\xfb\xf4\x9cP!qK7\xee\xb0-\xc6\x1f\xb0Z4\x0fm\x9d%\x90۽\x7f\xd4\a<\xfe\x0e\xa0{()㕭uЮ\xb5\xa6[\xd6\x11\b\xb8Kȏ{\t\xb0} \xee\x18\x06\xb7/\xb4\xb5\xfb\x0f\x93oȎ\xc0i\xde\xf2\x8b>\f\xe6\"j\xf7\xc6\xeb\x02\xe1\x1f\xc7\xe3\xd1\x19\x838߸\a\xdf&\x88\xf0\xa9\xad96\xe0f\x188d\xff\x84ܫ\x8e\x84.ݟ\x18\xc35\xd6iN\xb9z9\xa2\x86\xe1\xb2\xfe\x9b\x18\x13ƏB.\xd8g؏\xd8\x17\xec\x83\xc4A\xec\x13\xc0\x9dw\x84\x9c\xb6VH;\xce\x19\xe2cӊ\x0e\x9b\x8eh\xc8i\xb1\xbd\x1b\xc0\x18d\xb2ӣOM\x15w\xda\u0530ߊ1o\x94v\xcc2\x1c\xe8\xef\xf6~\x8dj\xf0\x83\xda;\xa6\xb9G\xd5\xc8\xdeGz=0\xefH\x8e\xf7һ_\xeau\xfb\xa0\x02\xfb\xdb\xdf\xcf\xfe?\x00\x00\xff\xff0\xe5e\x05\x8f|\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"), } diff --git a/pkg/apis/velero/v1/restore_types.go b/pkg/apis/velero/v1/restore_types.go index c01686241..2ef791270 100644 --- a/pkg/apis/velero/v1/restore_types.go +++ b/pkg/apis/velero/v1/restore_types.go @@ -135,6 +135,14 @@ type RestoreSpec struct { // +nullable ResourcePolicy *corev1api.TypedLocalObjectReference `json:"resourcePolicy,omitempty"` + // SkipDefaultResourceModifier controls whether the server-configured default + // resource modifier is applied to this restore. + // When true, the default modifier is skipped even if configured on the server. + // Has no effect when a per-restore ResourceModifier is specified. + // +optional + // +nullable + SkipDefaultResourceModifier *bool `json:"skipDefaultResourceModifier,omitempty"` + // UploaderConfig specifies the configuration for the restore. // +optional // +nullable diff --git a/pkg/apis/velero/v1/zz_generated.deepcopy.go b/pkg/apis/velero/v1/zz_generated.deepcopy.go index 106beaa79..ffbbf0cf8 100644 --- a/pkg/apis/velero/v1/zz_generated.deepcopy.go +++ b/pkg/apis/velero/v1/zz_generated.deepcopy.go @@ -1427,6 +1427,11 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) { *out = new(corev1.TypedLocalObjectReference) (*in).DeepCopyInto(*out) } + if in.SkipDefaultResourceModifier != nil { + in, out := &in.SkipDefaultResourceModifier, &out.SkipDefaultResourceModifier + *out = new(bool) + **out = **in + } if in.UploaderConfig != nil { in, out := &in.UploaderConfig, &out.UploaderConfig *out = new(UploaderConfigForRestore) From 8ef8ab3b1d7ddb3c50d28eb1627552b421c17040 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 09:39:55 -0700 Subject: [PATCH 141/194] Implement default resource modifier in restore controller Thread DefaultResourceModifierConfigMap from server config through to restoreReconciler. Refactor validateAndComplete to use a shared loadResourceModifierConfigMap helper that handles both default and per-restore ConfigMap loading. Precedence: per-restore modifier takes exclusive precedence over the default. Default ConfigMap errors are non-fatal (warn and proceed). SkipDefaultResourceModifier opt-out is respected. Includes unit tests covering: default-only, per-restore override, skip flag, missing default (non-fatal), missing per-restore (fatal), and no modifier configured. Signed-off-by: Shubham Pampattiwar --- pkg/cmd/server/server.go | 1 + pkg/controller/restore_controller.go | 79 +++++++++--- pkg/controller/restore_controller_test.go | 139 ++++++++++++++++++++++ 3 files changed, 200 insertions(+), 19 deletions(-) diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 83627f9d1..7aff5e946 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -881,6 +881,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string s.config.DisableInformerCache, s.crClient, s.config.ResourceTimeout, + s.config.DefaultResourceModifierConfigMap, ) if err = r.SetupWithManager(s.mgr); err != nil { diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index 5b055bc6c..48c57413e 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -55,6 +55,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/framework" pkgrestore "github.com/vmware-tanzu/velero/pkg/restore" + "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/collections" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/logging" @@ -109,10 +110,11 @@ type restoreReconciler struct { defaultItemOperationTimeout time.Duration disableInformerCache bool - newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager - backupStoreGetter persistence.ObjectBackupStoreGetter - globalCrClient client.Client - resourceTimeout time.Duration + newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager + backupStoreGetter persistence.ObjectBackupStoreGetter + globalCrClient client.Client + resourceTimeout time.Duration + defaultResourceModifierConfigMap string } type backupInfo struct { @@ -135,6 +137,7 @@ func NewRestoreReconciler( disableInformerCache bool, globalCrClient client.Client, resourceTimeout time.Duration, + defaultResourceModifierConfigMap string, ) *restoreReconciler { r := &restoreReconciler{ ctx: ctx, @@ -154,8 +157,9 @@ func NewRestoreReconciler( newPluginManager: newPluginManager, backupStoreGetter: backupStoreGetter, - globalCrClient: globalCrClient, - resourceTimeout: resourceTimeout, + globalCrClient: globalCrClient, + resourceTimeout: resourceTimeout, + defaultResourceModifierConfigMap: defaultResourceModifierConfigMap, } // Move the periodical backup and restore metrics computing logic from controllers to here. @@ -432,26 +436,63 @@ func (r *restoreReconciler) validateAndComplete(ctx context.Context, restore *ap var resourceModifiers *resourcemodifiers.ResourceModifiers if restore.Spec.ResourceModifier != nil && strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) { - ResourceModifierConfigMap := &corev1api.ConfigMap{} - err := r.kbClient.Get(ctx, client.ObjectKey{Namespace: restore.Namespace, Name: restore.Spec.ResourceModifier.Name}, ResourceModifierConfigMap) - if err != nil { - restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("failed to get resource modifiers configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name)) + resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, restore.Spec.ResourceModifier.Name, false) + if resourceModifiers == nil && len(restore.Status.ValidationErrors) > 0 { return backupInfo{}, nil, nil } - resourceModifiers, err = resourcemodifiers.GetResourceModifiersFromConfig(ResourceModifierConfigMap) - if err != nil { - restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, "Error in parsing resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name).Error()) - return backupInfo{}, nil, nil - } else if err = resourceModifiers.Validate(); err != nil { - restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, "Validation error in resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name).Error()) - return backupInfo{}, nil, nil - } - r.logger.Infof("Retrieved Resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name) + } else if r.defaultResourceModifierConfigMap != "" && !boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) { + resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, r.defaultResourceModifierConfigMap, true) } return info, resourceModifiers, restoreResPolicies } +// loadResourceModifierConfigMap loads and validates a resource modifier ConfigMap. +// When isDefault is true, errors are non-fatal (logged as warnings, returns nil). +// When isDefault is false, errors are added to restore.Status.ValidationErrors. +func (r *restoreReconciler) loadResourceModifierConfigMap( + ctx context.Context, restore *api.Restore, cmName string, isDefault bool, +) *resourcemodifiers.ResourceModifiers { + cm := &corev1api.ConfigMap{} + if err := r.kbClient.Get(ctx, client.ObjectKey{Namespace: restore.Namespace, Name: cmName}, cm); err != nil { + if isDefault { + r.logger.WithError(err).Warnf("Failed to retrieve default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName) + return nil + } + restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, + fmt.Sprintf("failed to get resource modifiers configmap %s/%s", restore.Namespace, cmName)) + return nil + } + + modifiers, err := resourcemodifiers.GetResourceModifiersFromConfig(cm) + if err != nil { + if isDefault { + r.logger.WithError(err).Warnf("Error parsing default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName) + return nil + } + restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, + errors.Wrapf(err, "Error in parsing resource modifiers provided in configmap %s/%s", restore.Namespace, cmName).Error()) + return nil + } + + if err = modifiers.Validate(); err != nil { + if isDefault { + r.logger.WithError(err).Warnf("Validation error in default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName) + return nil + } + restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, + errors.Wrapf(err, "Validation error in resource modifiers provided in configmap %s/%s", restore.Namespace, cmName).Error()) + return nil + } + + source := "per-restore" + if isDefault { + source = "default" + } + r.logger.Infof("Retrieved %s resource modifiers from configmap %s/%s", source, restore.Namespace, cmName) + return modifiers +} + // backupXorScheduleProvided returns true if exactly one of BackupName and // ScheduleName are non-empty for the restore, or false otherwise. func backupXorScheduleProvided(restore *api.Restore) bool { diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index 6a2f4d8d1..ab33b5b0e 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -116,6 +116,7 @@ func TestFetchBackupInfo(t *testing.T) { false, fakeGlobalClient, 10*time.Minute, + "", ) if test.backupStoreError == nil { @@ -197,6 +198,7 @@ func TestProcessQueueItemSkips(t *testing.T) { false, fakeGlobalClient, 10*time.Minute, + "", ) _, err := r.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{ @@ -579,6 +581,7 @@ func TestRestoreReconcile(t *testing.T) { false, fakeGlobalClient, 10*time.Minute, + "", ) r.clock = clocktesting.NewFakeClock(now) @@ -767,6 +770,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) { false, fakeGlobalClient, 10*time.Minute, + "", ) restore := &velerov1api.Restore{ @@ -863,6 +867,7 @@ func TestValidateAndCompleteWithResourcePolicySpecified(t *testing.T) { false, fakeGlobalClient, 10*time.Minute, + "", ) restore := &velerov1api.Restore{ @@ -992,6 +997,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { false, fakeGlobalClient, 10*time.Minute, + "", ) restore := &velerov1api.Restore{ @@ -1110,6 +1116,139 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { assert.Contains(t, restore3.Status.ValidationErrors[0], "Validation error in resource modifiers provided in configmap") } +func TestValidateAndCompleteWithDefaultResourceModifier(t *testing.T) { + formatFlag := logging.FormatText + + validCMData := map[string]string{ + "modifiers.yaml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n mergePatches:\n - patchData: |\n metadata:\n annotations:\n k8s.ovn.org/pod-networks: null\n", + } + + setupReconciler := func(t *testing.T, defaultCM string) *restoreReconciler { + t.Helper() + fakeClient := velerotest.NewFakeControllerRuntimeClient(t) + fakeGlobalClient := velerotest.NewFakeControllerRuntimeClient(t) + pluginManager := &pluginmocks.Manager{} + backupStore := &persistencemocks.BackupStore{} + + r := NewRestoreReconciler( + t.Context(), + velerov1api.DefaultNamespace, + nil, + fakeClient, + velerotest.NewLogger(), + logrus.DebugLevel, + func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, + NewFakeSingleObjectBackupStoreGetter(backupStore), + metrics.NewServerMetrics(), + formatFlag, + 60*time.Minute, + false, + fakeGlobalClient, + 10*time.Minute, + defaultCM, + ) + + location := builder.ForBackupStorageLocation("velero", "default").Provider("myCloud").Bucket("bucket").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result() + require.NoError(t, r.kbClient.Create(t.Context(), location)) + require.NoError(t, r.kbClient.Create(t.Context(), + defaultBackup().ObjectMeta(builder.WithName("backup-1")).StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(), + )) + return r + } + + newRestore := func(perRestoreCM string, skip *bool) *velerov1api.Restore { + restore := &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: velerov1api.DefaultNamespace, + Name: "restore-1", + }, + Spec: velerov1api.RestoreSpec{ + BackupName: "backup-1", + SkipDefaultResourceModifier: skip, + }, + } + if perRestoreCM != "" { + restore.Spec.ResourceModifier = &corev1api.TypedLocalObjectReference{ + Kind: resourcemodifiers.ConfigmapRefType, + Name: perRestoreCM, + } + } + return restore + } + + t.Run("default modifier applied when no per-restore modifier", func(t *testing.T) { + r := setupReconciler(t, "default-rm") + require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace}, + Data: validCMData, + })) + + restore := newRestore("", nil) + _, rm, _ := r.validateAndComplete(t.Context(), restore) + assert.NotNil(t, rm) + assert.Empty(t, restore.Status.ValidationErrors) + }) + + t.Run("per-restore modifier takes exclusive precedence", func(t *testing.T) { + r := setupReconciler(t, "default-rm") + require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace}, + Data: validCMData, + })) + require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "per-restore-rm", Namespace: velerov1api.DefaultNamespace}, + Data: validCMData, + })) + + restore := newRestore("per-restore-rm", nil) + _, rm, _ := r.validateAndComplete(t.Context(), restore) + assert.NotNil(t, rm) + assert.Empty(t, restore.Status.ValidationErrors) + }) + + t.Run("skip default modifier when SkipDefaultResourceModifier is true", func(t *testing.T) { + r := setupReconciler(t, "default-rm") + require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace}, + Data: validCMData, + })) + + skipTrue := true + restore := newRestore("", &skipTrue) + _, rm, _ := r.validateAndComplete(t.Context(), restore) + assert.Nil(t, rm) + assert.Empty(t, restore.Status.ValidationErrors) + }) + + t.Run("default modifier missing is non-fatal", func(t *testing.T) { + r := setupReconciler(t, "nonexistent-cm") + + restore := newRestore("", nil) + _, rm, _ := r.validateAndComplete(t.Context(), restore) + assert.Nil(t, rm) + assert.Empty(t, restore.Status.ValidationErrors) + }) + + t.Run("per-restore modifier missing is fatal", func(t *testing.T) { + r := setupReconciler(t, "") + + restore := newRestore("nonexistent-cm", nil) + _, rm, _ := r.validateAndComplete(t.Context(), restore) + assert.Nil(t, rm) + assert.NotEmpty(t, restore.Status.ValidationErrors) + assert.Contains(t, restore.Status.ValidationErrors[0], "failed to get resource modifiers configmap") + }) + + t.Run("no default configured and no per-restore modifier", func(t *testing.T) { + r := setupReconciler(t, "") + + restore := newRestore("", nil) + _, rm, _ := r.validateAndComplete(t.Context(), restore) + assert.Nil(t, rm) + assert.Empty(t, restore.Status.ValidationErrors) + }) +} + func TestBackupXorScheduleProvided(t *testing.T) { r := &velerov1api.Restore{} assert.False(t, backupXorScheduleProvided(r)) From 34bc3c7e1a660e8805dd4d11f78be850e12abaac Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 09:42:13 -0700 Subject: [PATCH 142/194] Add --skip-default-resource-modifier flag to restore CLI When set, the server-configured default resource modifier is skipped for this restore. Only sets the *bool field when the flag is true, leaving it nil otherwise. Signed-off-by: Shubham Pampattiwar --- pkg/cmd/cli/restore/create.go | 59 ++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/pkg/cmd/cli/restore/create.go b/pkg/cmd/cli/restore/create.go index 3f59b6a6b..c76097176 100644 --- a/pkg/cmd/cli/restore/create.go +++ b/pkg/cmd/cli/restore/create.go @@ -85,32 +85,33 @@ Notes: } type CreateOptions struct { - BackupName string - ScheduleName string - RestoreName string - RestoreVolumes flag.OptionalBool - PreserveNodePorts flag.OptionalBool - Labels flag.Map - Annotations flag.Map - IncludeNamespaces flag.StringArray - ExcludeNamespaces flag.StringArray - ExistingResourcePolicy string - IncludeResources flag.StringArray - ExcludeResources flag.StringArray - StatusIncludeResources flag.StringArray - StatusExcludeResources flag.StringArray - NamespaceMappings flag.Map - Selector flag.LabelSelector - OrSelector flag.OrLabelSelector - IncludeClusterResources flag.OptionalBool - Wait bool - AllowPartiallyFailed flag.OptionalBool - ItemOperationTimeout time.Duration - ResourceModifierConfigMap string - ResourcePoliciesConfigMap string - WriteSparseFiles flag.OptionalBool - ParallelFilesDownload int - client kbclient.WithWatch + BackupName string + ScheduleName string + RestoreName string + RestoreVolumes flag.OptionalBool + PreserveNodePorts flag.OptionalBool + Labels flag.Map + Annotations flag.Map + IncludeNamespaces flag.StringArray + ExcludeNamespaces flag.StringArray + ExistingResourcePolicy string + IncludeResources flag.StringArray + ExcludeResources flag.StringArray + StatusIncludeResources flag.StringArray + StatusExcludeResources flag.StringArray + NamespaceMappings flag.Map + Selector flag.LabelSelector + OrSelector flag.OrLabelSelector + IncludeClusterResources flag.OptionalBool + Wait bool + AllowPartiallyFailed flag.OptionalBool + ItemOperationTimeout time.Duration + ResourceModifierConfigMap string + ResourcePoliciesConfigMap string + SkipDefaultResourceModifier bool + WriteSparseFiles flag.OptionalBool + ParallelFilesDownload int + client kbclient.WithWatch } func NewCreateOptions() *CreateOptions { @@ -164,6 +165,8 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) { flags.StringVar(&o.ResourcePoliciesConfigMap, "resource-policies-configmap", "", "Reference to the ConfigMap containing restore resource filter policies") + flags.BoolVar(&o.SkipDefaultResourceModifier, "skip-default-resource-modifier", false, "Skip applying the server-configured default resource modifier for this restore") + f = flags.VarPF(&o.WriteSparseFiles, "write-sparse-files", "", "Whether to write sparse files during restoring volumes") f.NoOptDefVal = cmd.TRUE @@ -362,6 +365,10 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { }, } + if o.SkipDefaultResourceModifier { + restore.Spec.SkipDefaultResourceModifier = boolptr.True() + } + if len([]string(o.StatusIncludeResources)) > 0 { restore.Spec.RestoreStatus = &api.RestoreStatusSpec{ IncludedResources: o.StatusIncludeResources, From c509e5369c6710ef1e4b6ca42b04ffdedb410202 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 10:10:21 -0700 Subject: [PATCH 143/194] Wire default resource modifier through install path and builder Add --default-resource-modifier-configmap to the install CLI and deployment builder so administrators can configure it during velero install. Wire through VeleroOptions and podTemplateConfig following the existing --backup-repository-configmap pattern. Add SkipDefaultResourceModifier builder method to RestoreBuilder. Signed-off-by: Shubham Pampattiwar --- pkg/builder/restore_builder.go | 6 + pkg/cmd/cli/install/install.go | 202 +++++++++++++++++---------------- pkg/install/deployment.go | 73 +++++++----- pkg/install/resources.go | 91 ++++++++------- 4 files changed, 201 insertions(+), 171 deletions(-) diff --git a/pkg/builder/restore_builder.go b/pkg/builder/restore_builder.go index 22e880a98..472e51a21 100644 --- a/pkg/builder/restore_builder.go +++ b/pkg/builder/restore_builder.go @@ -181,3 +181,9 @@ func (b *RestoreBuilder) ResourcePoliciesConfigmap(name string) *RestoreBuilder } return b } + +// SkipDefaultResourceModifier sets whether to skip the server default resource modifier. +func (b *RestoreBuilder) SkipDefaultResourceModifier(val bool) *RestoreBuilder { + b.object.Spec.SkipDefaultResourceModifier = &val + return b +} diff --git a/pkg/cmd/cli/install/install.go b/pkg/cmd/cli/install/install.go index 0df53eb32..67c9517da 100644 --- a/pkg/cmd/cli/install/install.go +++ b/pkg/cmd/cli/install/install.go @@ -42,60 +42,61 @@ import ( // Options collects all the options for installing Velero into a Kubernetes cluster. type Options struct { - Namespace string - Image string - BucketName string - Prefix string - ProviderName string - PodAnnotations flag.Map - PodLabels flag.Map - ServiceAccountAnnotations flag.Map - ServiceAccountName string - VeleroPodCPURequest string - VeleroPodMemRequest string - VeleroPodCPULimit string - VeleroPodMemLimit string - NodeAgentPodCPURequest string - NodeAgentPodMemRequest string - NodeAgentPodCPULimit string - NodeAgentPodMemLimit string - RestoreOnly bool - SecretFile string - NoSecret bool - DryRun bool - BackupStorageConfig flag.Map - VolumeSnapshotConfig flag.Map - UseNodeAgent bool - UseNodeAgentWindows bool - PrivilegedNodeAgent bool - Wait bool - UseVolumeSnapshots bool - DefaultRepoMaintenanceFrequency time.Duration - GarbageCollectionFrequency time.Duration - PodVolumeOperationTimeout time.Duration - Plugins flag.StringArray - NoDefaultBackupLocation bool - CRDsOnly bool - CACertFile string - Features string - DefaultVolumesToFsBackup bool - UploaderType string - DefaultSnapshotMoveData bool - CSISnapshotEarlyFrequentPolling bool - DisableInformerCache bool - ScheduleSkipImmediately bool - PodResources kubeutil.PodResources - KeepLatestMaintenanceJobs int - BackupRepoConfigMap string - RepoMaintenanceJobConfigMap string - NodeAgentConfigMap string - ItemBlockWorkerCount int - ConcurrentBackups int - NodeAgentDisableHostPath bool - kubeletRootDir string - Apply bool - ServerPriorityClassName string - NodeAgentPriorityClassName string + Namespace string + Image string + BucketName string + Prefix string + ProviderName string + PodAnnotations flag.Map + PodLabels flag.Map + ServiceAccountAnnotations flag.Map + ServiceAccountName string + VeleroPodCPURequest string + VeleroPodMemRequest string + VeleroPodCPULimit string + VeleroPodMemLimit string + NodeAgentPodCPURequest string + NodeAgentPodMemRequest string + NodeAgentPodCPULimit string + NodeAgentPodMemLimit string + RestoreOnly bool + SecretFile string + NoSecret bool + DryRun bool + BackupStorageConfig flag.Map + VolumeSnapshotConfig flag.Map + UseNodeAgent bool + UseNodeAgentWindows bool + PrivilegedNodeAgent bool + Wait bool + UseVolumeSnapshots bool + DefaultRepoMaintenanceFrequency time.Duration + GarbageCollectionFrequency time.Duration + PodVolumeOperationTimeout time.Duration + Plugins flag.StringArray + NoDefaultBackupLocation bool + CRDsOnly bool + CACertFile string + Features string + DefaultVolumesToFsBackup bool + UploaderType string + DefaultSnapshotMoveData bool + CSISnapshotEarlyFrequentPolling bool + DisableInformerCache bool + ScheduleSkipImmediately bool + PodResources kubeutil.PodResources + KeepLatestMaintenanceJobs int + BackupRepoConfigMap string + RepoMaintenanceJobConfigMap string + DefaultResourceModifierConfigMap string + NodeAgentConfigMap string + ItemBlockWorkerCount int + ConcurrentBackups int + NodeAgentDisableHostPath bool + kubeletRootDir string + Apply bool + ServerPriorityClassName string + NodeAgentPriorityClassName string } // BindFlags adds command line values to the options struct. @@ -189,6 +190,12 @@ func (o *Options) BindFlags(flags *pflag.FlagSet) { o.RepoMaintenanceJobConfigMap, "The name of ConfigMap containing repository maintenance Job configurations.", ) + flags.StringVar( + &o.DefaultResourceModifierConfigMap, + "default-resource-modifier-configmap", + o.DefaultResourceModifierConfigMap, + "The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores.", + ) flags.StringVar( &o.NodeAgentConfigMap, "node-agent-configmap", @@ -298,49 +305,50 @@ func (o *Options) AsVeleroOptions() (*install.VeleroOptions, error) { } return &install.VeleroOptions{ - Namespace: o.Namespace, - Image: o.Image, - ProviderName: o.ProviderName, - Bucket: o.BucketName, - Prefix: o.Prefix, - PodAnnotations: o.PodAnnotations.Data(), - PodLabels: o.PodLabels.Data(), - ServiceAccountAnnotations: o.ServiceAccountAnnotations.Data(), - ServiceAccountName: o.ServiceAccountName, - VeleroPodResources: veleroPodResources, - NodeAgentPodResources: nodeAgentPodResources, - SecretData: secretData, - RestoreOnly: o.RestoreOnly, - UseNodeAgent: o.UseNodeAgent, - UseNodeAgentWindows: o.UseNodeAgentWindows, - PrivilegedNodeAgent: o.PrivilegedNodeAgent, - UseVolumeSnapshots: o.UseVolumeSnapshots, - BSLConfig: o.BackupStorageConfig.Data(), - VSLConfig: o.VolumeSnapshotConfig.Data(), - DefaultRepoMaintenanceFrequency: o.DefaultRepoMaintenanceFrequency, - GarbageCollectionFrequency: o.GarbageCollectionFrequency, - PodVolumeOperationTimeout: o.PodVolumeOperationTimeout, - Plugins: o.Plugins, - NoDefaultBackupLocation: o.NoDefaultBackupLocation, - CACertData: caCertData, - Features: strings.Split(o.Features, ","), - DefaultVolumesToFsBackup: o.DefaultVolumesToFsBackup, - UploaderType: o.UploaderType, - DefaultSnapshotMoveData: o.DefaultSnapshotMoveData, - CSISnapshotEarlyFrequentPolling: o.CSISnapshotEarlyFrequentPolling, - DisableInformerCache: o.DisableInformerCache, - ScheduleSkipImmediately: o.ScheduleSkipImmediately, - PodResources: o.PodResources, - KeepLatestMaintenanceJobs: o.KeepLatestMaintenanceJobs, - BackupRepoConfigMap: o.BackupRepoConfigMap, - RepoMaintenanceJobConfigMap: o.RepoMaintenanceJobConfigMap, - NodeAgentConfigMap: o.NodeAgentConfigMap, - ItemBlockWorkerCount: o.ItemBlockWorkerCount, - ConcurrentBackups: o.ConcurrentBackups, - KubeletRootDir: o.kubeletRootDir, - NodeAgentDisableHostPath: o.NodeAgentDisableHostPath, - ServerPriorityClassName: o.ServerPriorityClassName, - NodeAgentPriorityClassName: o.NodeAgentPriorityClassName, + Namespace: o.Namespace, + Image: o.Image, + ProviderName: o.ProviderName, + Bucket: o.BucketName, + Prefix: o.Prefix, + PodAnnotations: o.PodAnnotations.Data(), + PodLabels: o.PodLabels.Data(), + ServiceAccountAnnotations: o.ServiceAccountAnnotations.Data(), + ServiceAccountName: o.ServiceAccountName, + VeleroPodResources: veleroPodResources, + NodeAgentPodResources: nodeAgentPodResources, + SecretData: secretData, + RestoreOnly: o.RestoreOnly, + UseNodeAgent: o.UseNodeAgent, + UseNodeAgentWindows: o.UseNodeAgentWindows, + PrivilegedNodeAgent: o.PrivilegedNodeAgent, + UseVolumeSnapshots: o.UseVolumeSnapshots, + BSLConfig: o.BackupStorageConfig.Data(), + VSLConfig: o.VolumeSnapshotConfig.Data(), + DefaultRepoMaintenanceFrequency: o.DefaultRepoMaintenanceFrequency, + GarbageCollectionFrequency: o.GarbageCollectionFrequency, + PodVolumeOperationTimeout: o.PodVolumeOperationTimeout, + Plugins: o.Plugins, + NoDefaultBackupLocation: o.NoDefaultBackupLocation, + CACertData: caCertData, + Features: strings.Split(o.Features, ","), + DefaultVolumesToFsBackup: o.DefaultVolumesToFsBackup, + UploaderType: o.UploaderType, + DefaultSnapshotMoveData: o.DefaultSnapshotMoveData, + CSISnapshotEarlyFrequentPolling: o.CSISnapshotEarlyFrequentPolling, + DisableInformerCache: o.DisableInformerCache, + ScheduleSkipImmediately: o.ScheduleSkipImmediately, + PodResources: o.PodResources, + KeepLatestMaintenanceJobs: o.KeepLatestMaintenanceJobs, + BackupRepoConfigMap: o.BackupRepoConfigMap, + RepoMaintenanceJobConfigMap: o.RepoMaintenanceJobConfigMap, + DefaultResourceModifierConfigMap: o.DefaultResourceModifierConfigMap, + NodeAgentConfigMap: o.NodeAgentConfigMap, + ItemBlockWorkerCount: o.ItemBlockWorkerCount, + ConcurrentBackups: o.ConcurrentBackups, + KubeletRootDir: o.kubeletRootDir, + NodeAgentDisableHostPath: o.NodeAgentDisableHostPath, + ServerPriorityClassName: o.ServerPriorityClassName, + NodeAgentPriorityClassName: o.NodeAgentPriorityClassName, }, nil } diff --git a/pkg/install/deployment.go b/pkg/install/deployment.go index e9474f1fe..6bea8b0be 100644 --- a/pkg/install/deployment.go +++ b/pkg/install/deployment.go @@ -34,37 +34,38 @@ import ( type podTemplateOption func(*podTemplateConfig) type podTemplateConfig struct { - image string - envVars []corev1api.EnvVar - restoreOnly bool - annotations map[string]string - labels map[string]string - resources corev1api.ResourceRequirements - withSecret bool - defaultRepoMaintenanceFrequency time.Duration - garbageCollectionFrequency time.Duration - podVolumeOperationTimeout time.Duration - plugins []string - features []string - defaultVolumesToFsBackup bool - serviceAccountName string - uploaderType string - defaultSnapshotMoveData bool - csiSnapshotEarlyFrequentPolling bool - privilegedNodeAgent bool - disableInformerCache bool - scheduleSkipImmediately bool - podResources kube.PodResources - keepLatestMaintenanceJobs int - backupRepoConfigMap string - repoMaintenanceJobConfigMap string - nodeAgentConfigMap string - itemBlockWorkerCount int - concurrentBackups int - forWindows bool - kubeletRootDir string - nodeAgentDisableHostPath bool - priorityClassName string + image string + envVars []corev1api.EnvVar + restoreOnly bool + annotations map[string]string + labels map[string]string + resources corev1api.ResourceRequirements + withSecret bool + defaultRepoMaintenanceFrequency time.Duration + garbageCollectionFrequency time.Duration + podVolumeOperationTimeout time.Duration + plugins []string + features []string + defaultVolumesToFsBackup bool + serviceAccountName string + uploaderType string + defaultSnapshotMoveData bool + csiSnapshotEarlyFrequentPolling bool + privilegedNodeAgent bool + disableInformerCache bool + scheduleSkipImmediately bool + podResources kube.PodResources + keepLatestMaintenanceJobs int + backupRepoConfigMap string + repoMaintenanceJobConfigMap string + defaultResourceModifierConfigMap string + nodeAgentConfigMap string + itemBlockWorkerCount int + concurrentBackups int + forWindows bool + kubeletRootDir string + nodeAgentDisableHostPath bool + priorityClassName string } func WithImage(image string) podTemplateOption { @@ -229,6 +230,12 @@ func WithRepoMaintenanceJobConfigMap(repoMaintenanceJobConfigMap string) podTemp } } +func WithDefaultResourceModifierConfigMap(name string) podTemplateOption { + return func(c *podTemplateConfig) { + c.defaultResourceModifierConfigMap = name + } +} + func WithItemBlockWorkerCount(itemBlockWorkerCount int) podTemplateOption { return func(c *podTemplateConfig) { c.itemBlockWorkerCount = itemBlockWorkerCount @@ -350,6 +357,10 @@ func Deployment(namespace string, opts ...podTemplateOption) *appsv1api.Deployme args = append(args, fmt.Sprintf("--repo-maintenance-job-configmap=%s", c.repoMaintenanceJobConfigMap)) } + if len(c.defaultResourceModifierConfigMap) > 0 { + args = append(args, fmt.Sprintf("--default-resource-modifier-configmap=%s", c.defaultResourceModifierConfigMap)) + } + if c.itemBlockWorkerCount > 0 { args = append(args, fmt.Sprintf("--item-block-worker-count=%d", c.itemBlockWorkerCount)) } diff --git a/pkg/install/resources.go b/pkg/install/resources.go index c4ec6f1bc..9f9543300 100644 --- a/pkg/install/resources.go +++ b/pkg/install/resources.go @@ -234,49 +234,50 @@ func appendUnstructured(list *unstructured.UnstructuredList, obj runtime.Object) } type VeleroOptions struct { - Namespace string - Image string - ProviderName string - Bucket string - Prefix string - PodAnnotations map[string]string - PodLabels map[string]string - ServiceAccountAnnotations map[string]string - ServiceAccountName string - VeleroPodResources corev1api.ResourceRequirements - NodeAgentPodResources corev1api.ResourceRequirements - SecretData []byte - RestoreOnly bool - UseNodeAgent bool - UseNodeAgentWindows bool - PrivilegedNodeAgent bool - UseVolumeSnapshots bool - BSLConfig map[string]string - VSLConfig map[string]string - DefaultRepoMaintenanceFrequency time.Duration - GarbageCollectionFrequency time.Duration - PodVolumeOperationTimeout time.Duration - Plugins []string - NoDefaultBackupLocation bool - CACertData []byte - Features []string - DefaultVolumesToFsBackup bool - UploaderType string - DefaultSnapshotMoveData bool - CSISnapshotEarlyFrequentPolling bool - DisableInformerCache bool - ScheduleSkipImmediately bool - PodResources kube.PodResources - KeepLatestMaintenanceJobs int - BackupRepoConfigMap string - RepoMaintenanceJobConfigMap string - NodeAgentConfigMap string - ItemBlockWorkerCount int - ConcurrentBackups int - KubeletRootDir string - NodeAgentDisableHostPath bool - ServerPriorityClassName string - NodeAgentPriorityClassName string + Namespace string + Image string + ProviderName string + Bucket string + Prefix string + PodAnnotations map[string]string + PodLabels map[string]string + ServiceAccountAnnotations map[string]string + ServiceAccountName string + VeleroPodResources corev1api.ResourceRequirements + NodeAgentPodResources corev1api.ResourceRequirements + SecretData []byte + RestoreOnly bool + UseNodeAgent bool + UseNodeAgentWindows bool + PrivilegedNodeAgent bool + UseVolumeSnapshots bool + BSLConfig map[string]string + VSLConfig map[string]string + DefaultRepoMaintenanceFrequency time.Duration + GarbageCollectionFrequency time.Duration + PodVolumeOperationTimeout time.Duration + Plugins []string + NoDefaultBackupLocation bool + CACertData []byte + Features []string + DefaultVolumesToFsBackup bool + UploaderType string + DefaultSnapshotMoveData bool + CSISnapshotEarlyFrequentPolling bool + DisableInformerCache bool + ScheduleSkipImmediately bool + PodResources kube.PodResources + KeepLatestMaintenanceJobs int + BackupRepoConfigMap string + RepoMaintenanceJobConfigMap string + DefaultResourceModifierConfigMap string + NodeAgentConfigMap string + ItemBlockWorkerCount int + ConcurrentBackups int + KubeletRootDir string + NodeAgentDisableHostPath bool + ServerPriorityClassName string + NodeAgentPriorityClassName string } func AllCRDs() *unstructured.UnstructuredList { @@ -407,6 +408,10 @@ func AllResources(o *VeleroOptions) *unstructured.UnstructuredList { deployOpts = append(deployOpts, WithRepoMaintenanceJobConfigMap(o.RepoMaintenanceJobConfigMap)) } + if len(o.DefaultResourceModifierConfigMap) > 0 { + deployOpts = append(deployOpts, WithDefaultResourceModifierConfigMap(o.DefaultResourceModifierConfigMap)) + } + deploy := Deployment(o.Namespace, deployOpts...) if err := appendUnstructured(resources, deploy); err != nil { From d87a66393dff288527786251e5e2aa797ea00f2d Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 10:16:02 -0700 Subject: [PATCH 144/194] Add changelog for PR #10098 Signed-off-by: Shubham Pampattiwar --- changelogs/unreleased/10098-shubham-pampattiwar | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/10098-shubham-pampattiwar diff --git a/changelogs/unreleased/10098-shubham-pampattiwar b/changelogs/unreleased/10098-shubham-pampattiwar new file mode 100644 index 000000000..0c48c1631 --- /dev/null +++ b/changelogs/unreleased/10098-shubham-pampattiwar @@ -0,0 +1 @@ +Implement server default restore resource modifier From a6f800c591b5c7bae9a0263aba5c782679e80d8e Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 10:44:31 -0700 Subject: [PATCH 145/194] Address review feedback on default resource modifier - Fix fallthrough bug: when ResourceModifier is set with a non-ConfigMap kind, do not fall through to applying the server default. The outer check on ResourceModifier != nil now prevents default application regardless of the Kind value. - Include underlying error in fatal validation message for ConfigMap retrieval failures. - Strengthen exclusive precedence test: default ConfigMap intentionally does not exist while per-restore does, proving the default is never consulted. - Add test for invalid default ConfigMap data (non-fatal, warn and proceed). Signed-off-by: Shubham Pampattiwar --- pkg/controller/restore_controller.go | 12 ++++++----- pkg/controller/restore_controller_test.go | 25 +++++++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index 48c57413e..daf5cbf0c 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -435,10 +435,12 @@ func (r *restoreReconciler) validateAndComplete(ctx context.Context, restore *ap } var resourceModifiers *resourcemodifiers.ResourceModifiers - if restore.Spec.ResourceModifier != nil && strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) { - resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, restore.Spec.ResourceModifier.Name, false) - if resourceModifiers == nil && len(restore.Status.ValidationErrors) > 0 { - return backupInfo{}, nil, nil + if restore.Spec.ResourceModifier != nil { + if strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) { + resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, restore.Spec.ResourceModifier.Name, false) + if resourceModifiers == nil && len(restore.Status.ValidationErrors) > 0 { + return backupInfo{}, nil, nil + } } } else if r.defaultResourceModifierConfigMap != "" && !boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) { resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, r.defaultResourceModifierConfigMap, true) @@ -460,7 +462,7 @@ func (r *restoreReconciler) loadResourceModifierConfigMap( return nil } restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, - fmt.Sprintf("failed to get resource modifiers configmap %s/%s", restore.Namespace, cmName)) + fmt.Sprintf("failed to get resource modifiers configmap %s/%s: %v", restore.Namespace, cmName, err)) return nil } diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index ab33b5b0e..26c607efe 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -1189,12 +1189,10 @@ func TestValidateAndCompleteWithDefaultResourceModifier(t *testing.T) { assert.Empty(t, restore.Status.ValidationErrors) }) - t.Run("per-restore modifier takes exclusive precedence", func(t *testing.T) { - r := setupReconciler(t, "default-rm") - require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace}, - Data: validCMData, - })) + t.Run("per-restore modifier takes exclusive precedence over default", func(t *testing.T) { + // Default ConfigMap does NOT exist, but per-restore does. + // If default were applied, it would fail. Per-restore should succeed. + r := setupReconciler(t, "nonexistent-default") require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Name: "per-restore-rm", Namespace: velerov1api.DefaultNamespace}, Data: validCMData, @@ -1220,6 +1218,21 @@ func TestValidateAndCompleteWithDefaultResourceModifier(t *testing.T) { assert.Empty(t, restore.Status.ValidationErrors) }) + t.Run("default modifier with invalid data is non-fatal", func(t *testing.T) { + r := setupReconciler(t, "invalid-default") + require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid-default", Namespace: velerov1api.DefaultNamespace}, + Data: map[string]string{ + "modifiers.yaml": "not-valid-yaml: [", + }, + })) + + restore := newRestore("", nil) + _, rm, _ := r.validateAndComplete(t.Context(), restore) + assert.Nil(t, rm) + assert.Empty(t, restore.Status.ValidationErrors) + }) + t.Run("default modifier missing is non-fatal", func(t *testing.T) { r := setupReconciler(t, "nonexistent-cm") From 2be71e3c3b113d56156f7b0e05ce2aaaf1638bbf Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 10:58:05 -0700 Subject: [PATCH 146/194] Add docs, example ConfigMap, describer, and review fixes - Add Default Resource Modifiers section to restore-resource-modifiers.md - Add --default-resource-modifier-configmap to customize-installation.md - Add examples/default-resource-modifier-cni.yaml with CNI annotation stripping rules for OVN-K and Multus - Update restore describer to show SkipDefaultResourceModifier when set - Log warning when ResourceModifier Kind is not ConfigMap instead of silently doing nothing - Add deployment_test.go coverage for the new server flag Signed-off-by: Shubham Pampattiwar --- examples/default-resource-modifier-cni.yaml | 18 ++++++++ pkg/cmd/util/output/restore_describer.go | 4 ++ pkg/controller/restore_controller.go | 2 + pkg/install/deployment_test.go | 4 ++ .../docs/main/customize-installation.md | 2 + .../docs/main/restore-resource-modifiers.md | 45 ++++++++++++++++++- 6 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 examples/default-resource-modifier-cni.yaml diff --git a/examples/default-resource-modifier-cni.yaml b/examples/default-resource-modifier-cni.yaml new file mode 100644 index 000000000..352180f5a --- /dev/null +++ b/examples/default-resource-modifier-cni.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: default-restore-resource-modifiers + namespace: velero +data: + resource-modifiers.yaml: | + version: v1 + resourceModifierRules: + - conditions: + groupResource: pods + mergePatches: + - patchData: | + metadata: + annotations: + k8s.ovn.org/pod-networks: null + k8s.v1.cni.cncf.io/network-status: null + k8s.v1.cni.cncf.io/networks-status: null diff --git a/pkg/cmd/util/output/restore_describer.go b/pkg/cmd/util/output/restore_describer.go index c33da9f69..e94b2dedd 100644 --- a/pkg/cmd/util/output/restore_describer.go +++ b/pkg/cmd/util/output/restore_describer.go @@ -219,6 +219,10 @@ func DescribeRestore( DescribeResourceModifier(d, restore.Spec.ResourceModifier) } + if boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) { + d.Printf("Skip Default Resource Modifier:\ttrue\n") + } + if restore.Spec.ResourcePolicy != nil { d.Println() DescribeResourcePolicies(d, restore.Spec.ResourcePolicy) diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index daf5cbf0c..a7f0f7429 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -441,6 +441,8 @@ func (r *restoreReconciler) validateAndComplete(ctx context.Context, restore *ap if resourceModifiers == nil && len(restore.Status.ValidationErrors) > 0 { return backupInfo{}, nil, nil } + } else { + r.logger.Warnf("Unsupported resource modifier kind %q, only %q is supported", restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) } } else if r.defaultResourceModifierConfigMap != "" && !boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) { resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, r.defaultResourceModifierConfigMap, true) diff --git a/pkg/install/deployment_test.go b/pkg/install/deployment_test.go index 0cfcb65dd..6e9ff6ec5 100644 --- a/pkg/install/deployment_test.go +++ b/pkg/install/deployment_test.go @@ -109,6 +109,10 @@ func TestDeployment(t *testing.T) { assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2) assert.Equal(t, "--repo-maintenance-job-configmap=test-repo-maintenance-config", deploy.Spec.Template.Spec.Containers[0].Args[1]) + deploy = Deployment("velero", WithDefaultResourceModifierConfigMap("default-restore-modifiers")) + assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2) + assert.Equal(t, "--default-resource-modifier-configmap=default-restore-modifiers", deploy.Spec.Template.Spec.Containers[0].Args[1]) + assert.Equal(t, &corev1api.Affinity{ NodeAffinity: &corev1api.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{ diff --git a/site/content/docs/main/customize-installation.md b/site/content/docs/main/customize-installation.md index e42d6a3f8..e9561eea9 100644 --- a/site/content/docs/main/customize-installation.md +++ b/site/content/docs/main/customize-installation.md @@ -501,6 +501,7 @@ By far, `velero install` supports the following parameters to specify the extern * --backup-repository-configmap: [backup repository configuration document][15] * --node-agent-configmap: [node-agent concurrency configuration document][16], and there are some other documents specify other parts of node-agent-config. * --repo-maintenance-job-configmap: [repository maintenance configuration document][17] +* --default-resource-modifier-configmap: [default restore resource modifier document][18]. When set, the referenced ConfigMap's resource modifier rules apply automatically to all restores that don't specify a per-restore modifier. From v1.17, Velero adds verification for the ConfigMaps in CLI and server side, which means `velero install` CLI will fail and velero server and node-agent pod will exit if the specified ConfigMaps don't exist or are invalid. @@ -539,3 +540,4 @@ The new workflow is: [15]: backup-repository-configuration.md [16]: node-agent-concurrency.md [17]: repository-maintenance.md +[18]: restore-resource-modifiers.md#default-resource-modifiers diff --git a/site/content/docs/main/restore-resource-modifiers.md b/site/content/docs/main/restore-resource-modifiers.md index 0c1f2f217..39248ad30 100644 --- a/site/content/docs/main/restore-resource-modifiers.md +++ b/site/content/docs/main/restore-resource-modifiers.md @@ -184,4 +184,47 @@ resourceModifierRules: ### Wildcard Support for GroupResource The user can specify a wildcard for groupResource in the conditions' struct. This will allow the user to apply the patches for all the resources of a particular group or all resources in all groups. For example, `*.apps` will apply to all the resources in the `apps` group, `*` will apply to all the resources in core group, `*.*` will apply to all the resources in all groups. -- If both `*.groupName` and `namespaces` are specified, the patches will be applied to all the namespaced resources in this group in the specified namespaces and all the cluster resources in this group. \ No newline at end of file +- If both `*.groupName` and `namespaces` are specified, the patches will be applied to all the namespaced resources in this group in the specified namespaces and all the cluster resources in this group. + +## Default Resource Modifiers + +Velero supports a server-level default resource modifier that applies automatically to all restores without requiring per-restore configuration. +This is useful for common transformations like stripping stale CNI annotations that can break workloads after restore. + +### Configuration + +1. Create a ConfigMap in the Velero namespace with your default resource modifier rules: + +```bash +kubectl apply -f examples/default-resource-modifier-cni.yaml +``` + +2. Configure the Velero server to use it, either during install: + +```bash +velero install --default-resource-modifier-configmap=default-restore-resource-modifiers ... +``` + +Or by editing an existing deployment: + +```bash +kubectl -n velero edit deploy velero +# Add to the server args: --default-resource-modifier-configmap=default-restore-resource-modifiers +``` + +### Precedence + +When a per-restore modifier is specified via `--resource-modifier-configmap`, it takes exclusive precedence and the default is not applied. + +### Opt-out + +To skip the default modifier for a specific restore without specifying a per-restore modifier: + +```bash +velero restore create --from-backup my-backup --skip-default-resource-modifier +``` + +### Error Handling + +If the default ConfigMap is missing or contains invalid data, Velero logs a warning and proceeds with the restore. +Per-restore modifier errors remain fatal and cause the restore to fail validation. \ No newline at end of file From bd9563396776e3ac8835d541fd14f1e4242a2663 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 12:22:36 -0700 Subject: [PATCH 147/194] Add skip log and improve test coverage - Log when SkipDefaultResourceModifier skips the default modifier - Add test for unsupported ResourceModifier Kind (warns, does not apply default) - Add test for default ConfigMap with invalid rules (validation failure is non-fatal) - loadResourceModifierConfigMap now at 100% coverage Signed-off-by: Shubham Pampattiwar --- pkg/controller/restore_controller.go | 8 ++++-- pkg/controller/restore_controller_test.go | 32 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index a7f0f7429..e4eb68144 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -444,8 +444,12 @@ func (r *restoreReconciler) validateAndComplete(ctx context.Context, restore *ap } else { r.logger.Warnf("Unsupported resource modifier kind %q, only %q is supported", restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) } - } else if r.defaultResourceModifierConfigMap != "" && !boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) { - resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, r.defaultResourceModifierConfigMap, true) + } else if r.defaultResourceModifierConfigMap != "" { + if boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) { + r.logger.Infof("Skipping default resource modifier configmap %s/%s as SkipDefaultResourceModifier is set", restore.Namespace, r.defaultResourceModifierConfigMap) + } else { + resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, r.defaultResourceModifierConfigMap, true) + } } return info, resourceModifiers, restoreResPolicies diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index 26c607efe..738ad43db 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -1260,6 +1260,38 @@ func TestValidateAndCompleteWithDefaultResourceModifier(t *testing.T) { assert.Nil(t, rm) assert.Empty(t, restore.Status.ValidationErrors) }) + + t.Run("unsupported resource modifier kind does not apply default", func(t *testing.T) { + r := setupReconciler(t, "default-rm") + require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace}, + Data: validCMData, + })) + + restore := newRestore("", nil) + restore.Spec.ResourceModifier = &corev1api.TypedLocalObjectReference{ + Kind: "Secret", + Name: "some-secret", + } + _, rm, _ := r.validateAndComplete(t.Context(), restore) + assert.Nil(t, rm) + assert.Empty(t, restore.Status.ValidationErrors) + }) + + t.Run("default modifier validation failure is non-fatal", func(t *testing.T) { + r := setupReconciler(t, "invalid-validation") + require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid-validation", Namespace: velerov1api.DefaultNamespace}, + Data: map[string]string{ + "modifiers.yaml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n patches:\n - operation: invalid\n path: \"/spec\"\n value: \"test\"\n", + }, + })) + + restore := newRestore("", nil) + _, rm, _ := r.validateAndComplete(t.Context(), restore) + assert.Nil(t, rm) + assert.Empty(t, restore.Status.ValidationErrors) + }) } func TestBackupXorScheduleProvided(t *testing.T) { From ee2c3a4cd23d8bb8eb6b4e84c9f9cfc9901c636b Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 12:24:47 -0700 Subject: [PATCH 148/194] Add test coverage for --skip-default-resource-modifier CLI flag Add the flag to the existing TestCreateCommand test to verify flag binding and option parsing. Signed-off-by: Shubham Pampattiwar --- pkg/cmd/cli/restore/create_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/cli/restore/create_test.go b/pkg/cmd/cli/restore/create_test.go index 9a6a92608..643340e22 100644 --- a/pkg/cmd/cli/restore/create_test.go +++ b/pkg/cmd/cli/restore/create_test.go @@ -105,6 +105,7 @@ func TestCreateCommand(t *testing.T) { flags.Parse([]string{"--item-operation-timeout", itemOperationTimeout}) flags.Parse([]string{"--resource-modifier-configmap", resourceModifierConfigMap}) flags.Parse([]string{"--resource-policies-configmap", ResourcePoliciesConfigMap}) + flags.Parse([]string{"--skip-default-resource-modifier"}) flags.Parse([]string{"--write-sparse-files", writeSparseFiles}) flags.Parse([]string{"--parallel-files-download", "2"}) client := velerotest.NewFakeControllerRuntimeClient(t).(kbclient.WithWatch) @@ -145,6 +146,7 @@ func TestCreateCommand(t *testing.T) { require.Equal(t, itemOperationTimeout, o.ItemOperationTimeout.String()) require.Equal(t, resourceModifierConfigMap, o.ResourceModifierConfigMap) require.Equal(t, ResourcePoliciesConfigMap, o.ResourcePoliciesConfigMap) + require.True(t, o.SkipDefaultResourceModifier) require.Equal(t, writeSparseFiles, o.WriteSparseFiles.String()) require.Equal(t, parallel, o.ParallelFilesDownload) }) From b025fe3a9bdb7e539f1f43c530ce73118c47d6c9 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 28 Jul 2026 12:26:39 -0700 Subject: [PATCH 149/194] Add test for DefaultResourceModifierConfigMap in AllResources Verify the --default-resource-modifier-configmap flag is wired through VeleroOptions to the deployment args via AllResources. Signed-off-by: Shubham Pampattiwar --- config/crd/v1/crds/crds.go | 4 ++-- pkg/install/resources_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 209c02fb2..60395b71e 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -30,14 +30,14 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8aTɡS\xf7\xe7\u074b!%[\x96hk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L4\xea\x11\x9dW\xd6,A4\n\xff 4\xfc\xe5\xe7O\xbf\xf8\xb9\xb2\x8b\xc3\xebٓ2\xe5\x12V\xc1\x93\xad\xd7\xe8mp\x12\xdf\xe1N\x19EʚY\x8d$JAb9\x03\x10\xc6X\x12\xbc\xed\xf9\x13@ZC\xcej\x8d\xae\xa8\xd0̟\xc2\x16\xb7A\xe9\x12]4\u07b9>\xfc0\x7f\xfd\xf3\xfc\xa7\x19\x80\x115.a+\xe4Sh\x1c6\xd6+\xb2N\xa1\x9f\x1fP\xa3\xb3seg\xbeA\xc9\xd6+gC\xb3\x84\xf3A\xd2n=\xa7\xa8\xdfFC\xeb\xce\xd01\x1ei\xe5\xe9\xb7\xec\xf1G\xe5)\x8a4:8\xa1s\x81\xc4c\xbf\xb7\x8e>\x9f\x9d\x15\xc0\"\xe9H\x99*h\xe1F\xba\xec\xdbK\xdb\xe0\x12\xa2f#$\x963\x80\xb6\b\xd1R\x01\xa2,cY\x85~p\xca\x10\xba\x95ա6'?_\xbd5\x0f\x82\xf6K\x98w\x85\x9fK\x87\xb1\xe6_T\x8d\x9eD\xddDٮ\x96o*l\xbf\xe9\xc8\xceKA86\xc6E\x9d\x9fc\xfdrl\xf0\xc2ʹF\xd0;K\x16=9e\xaa\xd9Y\xf8\xf0:\x95B\xee\xb1\x16\xcbV\xd66h\xde<|x\xfcqs\xb1\r\xd08۠#\xd5\x153\xad^g\xf6v\x01J\xf4ҩ\x86b\xdf\xfc]\\\x9c\x01\xb0\x83\xa4\x05%\xb7(z\xa0=v5Ʋ\x8d\t\xec\x0eh\xaf<\xc3\xe6УIM\xcb\xdb\u0080\xdd~EI\xf3\x81\xe9\r:6\xc3\xd8\a]rg\x1f\xd0\x118\x94\xb62\xeaϓm\x0fd\xa3S-\b=AD\xd1\b\r\a\xa1\x03~\x0f\u0094\x03˵8\x82C\xf6\t\xc1\xf4\xecE\x05?\x8c\xe3\x93u\b\xca\xec\xec\x12\xf6D\x8d_.\x16\x95\xa2n^\xa5\xad\xeb`\x14\x1d\x17q\xf4\xd46\x90u~Q\xe2\x01\xf5«\xaa\x10N\xee\x15\xa1\xa4\xe0p!\x1aU\xc4DL\x9c\xd9y]~\xe7\xda\t\xf7\x17nG@\xa7\x15\x87\xec\x0exx\xea@y\x10\xad\xa9\x94\xe2\x19\x05\xde\xe2ҭ\x7f\xdd|\x81.\x92\x84T\x02\xe5,:\xaaK\x87\x0fWS\x99\x1d\xba\xa4\xb7s\xb6\x8e6є\x8dU\x86\xe2\x87\xd4\n\r\x81\x0f\xdbZ\x11\xb7\xc1\xef\x01=1tC\xb3\xab\xc8i\xb0E\b\r\x8fN9\x14\xf8``%j\xd4+\xe1\xf1?ƊQ\xf1\x05\x83\xf0,\xb4\xfaL=\x14N\xe5\xed\x1dt,{\x05\xda!sn\x1a\x94\x8c,\x17\x97U\xd5N\xc94S;\xeb@\x8c\xe4/+\x95\xa7\x00^\x89D7d\x9d\xa8\xf0\xa3M6\x87BSm\xc7\xebm\xceP\x171\xd3V\xe2\x04\xcc\vf\f\xd2^P\x8f\fH(s\xe2\x94l\x927\x90\x89\xe8\bf\n#\x8c\xc4\xf7\xb1\x1f\x8d\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xad+\xc6͒\x89v\x1f\x8f\x8c\xf3\xd0\xcb<\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa0\x9d2\x9dD\x8b\x01X-\xd2<\xee\xc0\xec@\x91R֮\xc0\x86q \xfa\xa0\r\x14\x9e0az\xf5\xf8DzB\xa3¹\a\xa1-]=\"}\xe4E\xc59]s\xb8\"FU0@\x9b\xb5\x94\x1c\xa8\x98 \xcegІe\xe7 \x8d\x83\x14!\x8c\xf2\x1f:\x14@o\x82>\x00\xa1\x11Оf\xd6m\xe1\xbcE\xd8.U\xa2c*\x15dv:\xbb\xf2\xd3$\x03\x8eS\xb3\x90\x84K\xb1\x05\xe5z\xb7V/\b\x98\x02+p9\xb13\x90\x02n\xa7Y\xb2\xa9\xec\xe4tI\xacv\x0f\xca\x00\x13\xda\x00\x8d\b\xe7\x13\xf8\x03_\xacu\x86\xfc\xday\xa4\xb7ֱ\xce\xc3B\xa37\x9d\xa4\xf0\xe9\xfd(D\xef\xb6p\x96\xa1w\xec\x1d\xe1%:\xf411m\xbc\x17;5\xe1jò\xd2\x0f\xbbqKF\xed\x81\x06c\x1b]\xfc\xe9b\x81\x1c\xee\xf6\xda\xedC\x13\xaa\xa0&K\xb2݄\xa24\x87~mf\xa0\x88PqԞ$\xf2\x93*E\x0f\x03ܬ\x17Fg\xe4\xe7\x10\xcc#\x8e\x8aP\xed\x85yz\xdc\xef?3W\xcf\xc3G\x8d\x01\x02ʄ\xe5\x9f]\xabwا\xdd\xc2֒MH\x13\x81\xe7\xfc:\xc8q\xcd:\u00ad߉Xg\x91\xf9!!\xafe\xcb\v\xef?$\xa5vR>LQ\xe7\a[\xa7Y-\x92\f\x03Qd\r;\xbagRyԛ\xa9\x16\xbe@V\x99\xa8\xd6SCr\xb6ـ\xb2p\xca\x1dՠ]\xfc`\x98 \xc3\xeb\x1a\xd22#яGx4\x8c\xb4lḂ\x86n\xfd\x88\xe3Y2\x14;P\xeb^\xe3d\x9c\xb3=\xcb+\xcaq^\xa6\"s\xf8\xd0z\\1+3\xc2\xe4ޘ\xa3\x92\xe9\x8as\b\x02R\x96I\x9d%\xa4\x14`}\xde®\t\xfaU\x871_S\xeb\xab\xc8!\xec\t2KU\x1c\xb4\xef*G7\xb2\xb1\x19\x8b\x86)\x18\xa1!\x9c\xae\x81\x13\r\x1c2#U\x9c\"S|v%\xc5\b\x0e\x102b\xf9\xba+\x8d\x06\x81\x11\x90\x04\x97p;\x96휫g\x85\b\xe1\x90\\\x82u\xf8\f\xa1e\xc9#\xd3ESF\x99\xef;\x19\xd3\xf5\xa6Lh\xfd1\xbc\x98\xfe7%\xc1f6%J\xdaF\xbf\xba\x94\xad\xc5!\xbe\xa6m\xca?'a\x83\xe5?AhG\xb4\x9f`\xb8,Y\xa6\a\xe5\xd6R\x95\x81\xbe\xb4\xee\x14z:\v\xc2L\xf8uJ\x13:>W/\x8a\xd8!\xc2\xd7͛\xf9B\x9fȚ\x14\x9dx&\xc6\xd4]\xfc\x03\xf2\x05\xa7\x8c[?c$\xf3\xe4\xa7v\xab\x05a\x9b\x9a\xe8\xf9\x82l\x187\xa0\x8e\xa8\x7f\x92\xa9\x0f\x9c9\a1Rf=\x82\xfb\x1a&۽\xffb]0\xddl\xee%\xd2帱sd\x83\xb7ߝ\x9e'\xe0\x12\x8c\xef3\x17m\u0557\xb8bj\xff\x82\xae\xd5ۏ\xef\xe2\xeb\xabvI\x90\xbc\x1e\"\x13J\xe7\xca\xdb#\x8c\xda\xe3\xf3.|\xf8\x82>P\xbd\x00r\xb1\xea\x05\xa1\xe4\x01\x0e\xceu\xa1\x82X\xfe\xd0P9\xa1{\x05\xb8Y\x85r\xf6\x00\a\x04\x13\xdf}\xea\x97Tip\xe5\x01\x0e)Վhh\xc7Ĵ\xdfU\xb3t\xb2? !p\xd3!U\f\\\xf1\xaa\x10\xd9뉗D[\x12J\xa0\xfd\th&\x89J\xbb\x8f\xf6\xf6-J\xc0w\xda\xf1\xd2j̎\x95hV1\xe2 7\xc9\fu\xe5\x9er\x96\xd7\x1d9\x1dY\x89\x05\xf9(\x8d\xfd\xe7\xfd\x17\xa6\xfd\x0e\xef;\t\xfa\xa34\xf8˳P\xd4\r\xfc9\xe9\x19v|,B\xce\xca[\x82\xb5\xf7(ݜf\xa5\xad\xa6=\xd3d%\xecrő$\xb1+\u070evݹ\x8e\x8aJ\xe3\xf6\xa2\x90b\xe9\xc26\xb1\x9e<\xbd\xa5\xea\x90\xfbɝ\xfa\x0e\xef\xecdᾸMqN3\xc8\xc3v\r\xee\xd6R\x03[\x96%\xf6W\x80\xda\x02)\xad\tO\x93\x88D\xc3걙'>i\xb3w\xbb|Y>\xd4\xc9\x0fK;\xe5,=\x04#\x8b\x04\x1ax\u06ddO㳴:\x9bP+H\xc2dՁ\xcd\xdc\xe1\xaa)Dy\x029p\x16G\x17g\x92\xbb4\xcf15\x88\xf2\x9b\x193\xca\fY\x98k\x1aZcwSpAq\xab\xe5\x7f\xecL\x8b\xda\xf4\x7f\xa4\xa4L\xe9K\xf2\x16s}8t\xbe\xf9\xa0Y\vLB\x97\x98\xabc\xe5gO\xb9\x9d\xfb\xad\x01\x17\x04\xb8\xf3\x04\xe4\xa6\xe7\x17-\xc8\xe3Nj7mכ8\x17\x0fpp;\x86\x93]\xb6\x8d\xcc\xc5J\\8\x1f\xa2g0j\x87C\n~ \x17\xf8\xed\xe2)\xaeT\xa2\xa4&V\xeb\x88hA\xcb4\t\xc5\\\xabTG\xdd.X\x83\x13b\x1b\xd69D\xd6\xc9\x1e\xc36IDK\xa9#\x1b\xf9\x03C\x99\x10\xde\x1b\xa9\x8d\x8b\x97u|\xe6h@M\x86 \x1a\xa1\x1b\x97\xd8%U\xc8±Fy*\xf4\xdb.w;\xd0\xe0\xf7+|`\xce\x01\xb5+\xbb\x8bF\xbf\x9d\xb5\xbfp\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xe6\x8bH\x96H\x1b\xf7:\xe6H\xdd*\xc9媌\x87@CIwy-!f\xae\x17\xde\x7fi\x05D\xad\xeeۿ\xa7dl\xee\xb8\bfY\x16\x05=\xce\xdfJ\x1a\xe2\xb5k\x19\xb4\xc1\x03r\x8b\x0f\xb5\xad\xd0\x12\xa4\xce\xe5\xb5\x00~\r\x8eB\xc1\xc4\n; o\x9e\xc1\xb1\xf064\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fN\x95K\x89[\x05\n:\xcc\xebG\xd5\xd1\x0f\x15Ҵ\x02\x123\xdc\xcdR\xe6\xdfi\xb2aJ\x9b\xf6\x10\xf4@\x9aJ\x14\xcc̅\x97x\xaf\xd4I\xeb\xaeO\xae\xe5Q\x02\x99\xcf[s\x84I\xc4\x1c\xf7\x97\x80\xb0\ra\x86\x80\xc8d%0\x80c\xf5\x18\xbbp\xc4u\x16\x96\xa5*I\x9a\xf6\x93\xc1\x1c\xb4XY\xa2\xa401\x1a\xe9iW\xff@Y?Q-Vf\xb2\xcd\fe\xb1\xc5\xcai:\x11R\xdcڙ\x8a\x05\xfd\u008a\xaa \xb4\xb0<\xc2ɜ\x15\xd0ez\x93\xf8f[\xe04a\xa4\u0558\x92\x83\x01\x9f\xbc\x968\x86L\n\xcdr\xa8'W/\bR\x10J6\x94\xf1J%Z\xc0Y䝳\x14\xf1\x96\xe0|k\x8c\xb4ΗH\x8a\x84hn\xa2\xaf8n\x8dK\x95\xee\xf1M\xb9Y\n\xe6{Y\xa5b\x12\xd3\x02\xcf\xech\xf9DJ*\x0e\xdf<\xadԡ~\xf3\xb4\xc6\xca7Ok\xa2|\xf3\xb4\xbeyZ)5\xbfyZ\xdf<\xadv\xf9\x97\xf0\xb4\xa6F\xe4\x0e:\x0e|\x9c\x1cE\xc2V\xf5\xd8\x10G\xe0\xfb\xe4\n\x9f\x03\xfe\xa4\\\xccU\x1cT$\xf1\x7f \xad;f\xb4\x9aɣNδZ\x13dޝ\xbb\x9ap%\x9f\x90u\x1f:=_\xd6\xfdj\x14♲\xee\xfd\xb0\xa7}\xec\x93r\xee\x03Q\xe6eg/|\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x99\xe8\xff\x85\x13s{Ycg\x94\x8fg\xcf\xe2O\x96\x91(K/\xfet\xf1\xf5\x91\xff<\x04\x1f$q\x9fv\xfe\xe0w\x04\xaa]\x81\xb6\xd3ºYx_\xa7\x18\x9fEnS3\xf1k\"F`uE\xf2\x88\x8a_\xab-0P|*\xfd\x8c\U001044ea\xab\b\x9c\xa4\xb3\xaaT\x1fD\xb6SR\xc8J\xfb\xa8\x84\x85\xf56s'\xfd\x03Ș\xb0F5\xfc?\xc8NV\x91L\xf0\x11\xf2Md\x04N#\xdfI\x0e\xf4\x9b\xd0`\xe8\xfe\xcde\xf7\x8b\x91>Up\xe8l\xf3\xe3\x0e\x04\uec0bm\xfb\x00@\xb8\xa8\xc1\xdfXp,`\x11@R\x11\xc1\xb8\x93\xbc\xfa\x9a\x87\xb6ܑO\xa5\x8b=\xcd\xf6;\xc6c*iɄ'\xa7\x10vS\x04\a\xfcҹ\xbb\xddg92\xf1\xbb\xa4\x06\xceO\bL\x89\x88M$\xff\x9d\x90\xf2\x97\x98[\xfc\xe4\xed\xf9\x94\xa4\xbe9+\xe6gK\xe0;\x7f\xda^\x12}\xa6S\xf4\xe6P\xe7\xd9\xd3\xf1^0\t\xefeR\xef\x12\x13\xeeΗ9\x9f\x16\x8f=)sl:t0\x9c47\x99*7\x19Z\x98Bl6J\x93)ps\x12\xdf&\xb9\x93\xa6f/\x96\xda\xf6b\tm/\x9b\xc66*E\xa3\x1f\xe7$\xaa\xc5\xef\xeb!\x93\x93-\x7f)a;\x95\fRu\xdcד\xd6W\x9f\x8e`X\xc6\a\xd7\xee\x85|\xe4\xa2↕\x1c7R\xf7,\x8f\x06\x1b\xcc\x0e\x0e\xf5\x05\x1a\xbfJ\xf53!Y\x13\x9f\xe7\x9d\xee9y\xcbB\xaa\x1c\xd4\xe8\xb6O\xaa\x14\x8e\xca_\xcaڦ;\x90\xa3\xfd\x8ep럭\xd5\xf1\x97qz\xf07\xb0\xe2]\xbbCۗV\xd2Z\xdeFg/\xaaq\x7f\xbaΤ\xbf\x80\xd7mWi(\xa9\xc2K\x9d\xd7\a\x97\xce\x12\x9d\x9a\xdf\xd3lw\x04}G5\xd9HUPC.\xea\r\xc0\xd7\x0e\xb8\xfd\xfb⒐\x0f\xb2Ήh\xdfˣYQ\xf2\x83]\xa1\x90\x8bv\x83\xd3$ *m\xa1\xb7\x1b\xc9Y\x16\xf1ݢw3\xb9ʽ\xcb2\xf0ƨ\xac\x9d2Pڊq\xd7\rݼ\xee\x15\x98\x1bɹ|\x9c\xb9\xf6\xa7%\xfb\v^v\xfe\x84\xe8\xd0ۛ\x15\xc2\b⁷\xa7\xd7\xc9Y56k\xb0\xd3r\x83\xe7\x90\xee\xaf6\x1d\x88\xdd<\xc7\xf6\xad\xc1\x90\xbb\v\xa2\x83[\xe0Mg&\xadu\xb9Y\xb9q\f\xf5be\x86\x8a\x03\x91\x98QcvL\xe5˒*sp\x89\x1a\x8b\xce\x18\xc2\\:\x16\xdd\x19\x9c=\xfa\x97^G\xc9\x1b\xee\xba\xc6\x1d\xcaC\xd9\xdd\xf4=\xa6\xdd)\xe3\x18>\xbd8yn\xf1\x8c\xe3\x18vK\x96H\xa9\xc8\xcf\xd1̯\xb3Eʹ\xbf\x99\xf8g\xb9\x87w\xd1\xe8Y\x87<\xb7G\xd5#\xe9Y\x01\xa2\xbbtw0Ku\rx!o\xff\xd3\x13\xf2\xadB\xd7\xfeN\xd5S\x02e\xb7]\x10\x11\xfc\xc2\r\xb3\xa1\xb3\x98}\u009b\xf1\x0f\xe4\xe6\x1e\xd7h\xb5i\xf3*\xea\xd7h!T\x166\x83#p|\x83\xefϟ\x9a\xa6\x8dTt\v?Iw\xf9\xf8\x14ۻ\xb5;\x97\xd2{\xaf'\xe4\x8f\x06\xa5\x89]\xc0\xeb\xafA?\x02\xd6\xe4|\xf7.5\xb6\xa3\x9cyM\xb31\xfc\x14\xbe\xdf\xdd\xfd\xe4\xb02\xac\x80\xcbw\x95Kw\xb06Q\x83%q\xc0\xd6AZ\xdb\xff\xee\xe4#^\xfe\x1b\x8fc\x86\xc7$\x1ad\x14`\xb29\xa6 \xceB\xa9*\xb9\xa49\xa8k)6l;\x81\xdd/\x9d\xcaG\xd3l\x86?z\xe4\xea9*\xc0?s\x0e\x82\xf5y8\a\xfe\x81q\xd0nX\t\x06\xf8\xa6ߪ\xb6\xc7U\xb1v>\xdc\xc6~\xac;\x18\x98\xe3\x1cZ\x18\x8a.AY/\xca\x05\xad+\x1ddu\x18\xf1\x86#L\x18\xd8B\x7f\x158b\x81ݭ\xd28}\x06s\x82k\x99\x1fc\xf1\xad\x0e\xf2\xf7\xc3-\x8f8\xd9\ny\xc5n\xdcsN\xc8\xcd\xfd\xb5&\x95\xc81\\|\xff\x97\xdbYR\xb7\xef\xdc\\\x1f\xb4uʨ\xde\xc7[\xb5\x9c㖽pޱ\xdcD\x10\x18\x82\xd3z \xe5\x91\x19\x7fq\xd7yoZ\x1dZ\xf2\f=\xfd\x80W\xfaO?\xfe\xe0n\xfe\xf7O\xc6xu\xac\x14^\x93\xea_\x05\xc0kE\x9f\xf0\xfeC'\xf9K\xbf5\x06\x8a\xd2\xc4|\x8dis\xf8\xfd\x18\xc0\xdaO\x93\x86\xf2\x96V\xd2P!\xe6i\xeb\x83\xc8\xc6\x12˼5\x1a\xe1\xe6\x98>\xc6\bp\xed\xcfC\x9c\x8d\x005\xc0!\x02\xe8*\xcb@\xebM\xc5\xf9\xa1>\x8e\xf1\x95P\xe3\x03e\xfc|\xa4p\xd0\x06\x05\xc1\xa27\ni\x12a\x9f\xee\r\"\x0f\x9a\x1e\x8e*\xcd#\x85\xe7\x82φԆ\x16'=\xd8p\xdd\a\x83o\x19\xa9\xbc\x95TI\xeb\xb1Sݰ?6\xb94\xe0\\K\\dYh\x90\x13\u0603 vvv$\x0e\xcfẗ́\xe2O\xb8\xba\x19.\xccw!\x14\x12}\xb1\x89\xf8h\x87Ɨ\x81\xbe\xd35L\xcc\x15\xc5\xf7L\xfaD\xe8;\xbf.Zqe\xbd\x7fXZ\x10\xa7y\xadC\xaf\xb9t照\x19\xb9\xeb\xdb\xd5\x10\xb8SL\\\xff\xb9\x97'\xaaq\x1f\xdd'\x99\xb4>\xba\xb3\fZ\x04b-\xe3\xe7\xc7\x1dU\xfd\xb4Kݱ\xa5s8\xb2p\x86\x8er\xee\x0f:\x16\xa05݆\xdb\xdc\x1f\xed\xd2c\v\x02\\x\xcem\x9eD\x806\xa7\xe2\xbaw\x99;\x95\xa1\x99\xa9\xa8\xef $\xf8\xb6j}\xa7\t\x971\xa8\xf8\xa0\v\vO\xa8\x855\xd9LB})\x99JYý\xaf+Zڠ'\x8c\xdci\x1e\xbd\x03ζ\xf8\xa4\x93\xe5ܖ\xaa5\xdd\xc22\x93\x9c\x03Z\xeb\xfe\xb8\x9eS\xd7\xfd\xd9\xc3\xcf@\xf5$j\x1f\xdau\xfd\x0e\xa0\xe3\xb6\xdb\xf8\xa6.\xdd\x1d\x9f53LA\xf3\xc2`o@\x12;\x9e\xe5(;*D\x9f\xdf돴]7h\x9d7\xcb>\xce\xeb_\xdf[4/jE\xc6Y\xd0_\xa5Z\x90\x82\t\xfb\x0f\x15\xb9\xdb\xc0\v\x8dg\x8d\x7f'\xe5\xc3mĉ\xed\r\xfe\x87\xbab\xb3\xd5\xc1\x84\x1b6\x1e\x18]\xcb\xca\xef\xbe\xd7\x0em|[\x05o\xe6?\xf3r\x13a\x8e\xcc\a=t\x06#\xba?t MN\x05\xae\xe7\x01X\xb7\xe1\x897\xce\x0f\x8bc\xc8G\xcfI6\xb0[/\x17x7\xa0\xb9\x8f`\xa0\xa3\xb0#\x15\x05R_|\xd16觬z=\x99\x87\x9c\xc9\x1e\x8d\x7fhj\x0f\xd1\xd1\r\xb3\xe5\xee\r \xd8q\x02ϻ`\xc7g*&\x84\xff\xc6֩\xef.h-\xdcB\x96\xd8`\x94n襻\x8f\xd0߮X\x92\xbfVPEh\xb0\f\x0f\xc3\xdd\x1a\xaa\xfa!_w\f\x1er\xcc\xe8@m\x8cTY\x89\x1b%\xb7\nt_X\x97\xe4o\x94\x19&\xb6\x1f\xa4\xba\xe1Ֆ\x89O\xc3G~\xc6*\xdfPe\x98\x15v7\x9e\xd8@\x99\xa0\x9c\xfd=f\xd7\xda\x1f\xa7\x01]\x0f.\xb0\x96$a\x18C\x1fށ\xf5q\a\xe3\x02Q\x13Zz\xba\x9e\xe2\xaf\x04\x9eL\xd9\xd4ڗh|\x91\xd0\xed%\xf9(\xa3\x86\xc1\xa7C\xb1.L뒁6K\xd8l\xa42n\xb7z\xb9$l\x13\x82\x0f\xd6\xe6`\xdc\xcc=\xe2IXl\x9b\xb9N4i\xa6/\fz+\x9c\x85\xf1*\xfb\x82\x1e\xdc\xce\x14Ͳ\xcazX\xaf\xb5\xa1<\xe2\xe0<\xc9\xf0c\x94\xe7{|\xb0\xf2\x97'\xed\xe4\xadڀ\xfaAG\xecǑ\x14/\xd3p^\x1f\xb7(\x82 \x8f\x8a\x19c}*9\x92J\xe0Ie\xaco\xc59і\xd4'E\x1f\x893\xa3\xabᔜ4\x94\xefj(C\xe6\xd9c\x8d/3֯\x82\xfa\xec#_˲9\xdbQ\xb1\x1d\xbc\xa1`\xa7d\xb5\xdd\x05I\x1ep\xa6I^\x01\x06kѤ\xe8\xf0ⲩ\x94h\xa5\x12\x8c\x1c\xfb&A\x18p\xb84{\xc0\xf7K\u074b\xc6\xfe)\xeb\xd7\xfe\r\x94\xe5F\xc9b\xe9\xfb\xc5X\xea\xc2\xef\xe4+&\xad\xe7bvQ\xaa\x13\xe7\xb5\xfbg\x06P\x12\xca\x12\x04\xa1\xda\xf7\x9cpS\xd4\xc9\xd3\xd4ovj\xb8\x91\x9a%x\xfbQ\x8e\xff\xb5\r 0\xbc\f\x7fw\x99\xe1W0\xd8g\f\x8fO\xfe\b>\xec\xa90n9QO\x91\x17n\x12\xbb\x98\xb5\x90\xd1vb{R\x90\xe6\xb6\x03a\">\x83\xdd\xc5Yt\xeb\xd35\xdcE`\xd7\xfe\xf9\xd5\x1a\xf0\x82h&\u008b\xe0.\xf5\xc3I\x7ft'P\xe0C\x95Rų1\xc7\x03.]\x84^6ֲ\xaf=\x89\xf7'/\xc5\xef\x8f`\x1c\x1d\xea\xc6wI\xeb*a\xf9\xfc\a\x16\xdb\x0f\xc04\xde̢\xf2\xc7\xdf\xfd\xb0\xf6>i\xa9\x17\xa7\xc8\xd8\xca\x0f\x17u\xc3K\xb8\xee;\xa47\x1c\xac\xb6i\x80\xee\xa2r\x96\xce\xed\xcf\x18M;g(-\xbc}\x7f\x9eX\xd2\xfe\x8cA\xb4g\x8b\xa0\x9d\x17\xe5G\x8a\x0fD\x9f\xa4\xb5\x7f\xf3m#!4\x0f\xf6\xdcA\xb4V\f-\f\xfcE\xa3h\xd19\xb7\xf7#\xda\xe9\xbce-|O\xfe\x97\xff\x0f\x00\x00\xff\xff9i\xfd\xfe\xeb\x83\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfe\x15\x84\xeeav7\xba\xe5u\xdcG\\\xe8\xcd#\xdb;\x1d3ck-\x8d\xf6\x99\xae\xca\xeefDA\rP-\xf7\xde\xdd\x7f\xbf \x81\xfa袪\xa8VK\xe3\xdd5/\xb6\xba !?I\x92\x04\x96\xcb\xe5+Z\xb2{P\x9aIqEh\xc9\xe0\x8b\x01a\xffҗ\x0f\xff\xad/\x99|\xbd\x7f\xf3ꁉ\xfc\x8a\\W\xda\xc8\xe23hY\xa9\f\xde\xc1\x86\tf\x98\x14\xaf\n04\xa7\x86^\xbd\"\x84\n!\r\xb5?k\xfb'!\x99\x14FI\xceA-\xb7 .\x1f\xaa5\xac+\xc6sP\b\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xad+\xc6͒\x89v\x1f\x8f\x8c\xf3\xd0\xcb<\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa0\x9d2\x9dD\x8b\x01X-\xd2<\xee\xc0\xec@\x91R֮\xc0\x86q \xfa\xa0\r\x14\x9e0az\xf5\xf8DzB\xa3¹\a\xa1-]=\"}\xe4E\xc59]s\xb8\"FU0@\x9b\xb5\x94\x1c\xa8\x98 \xcegІe\xe7 \x8d\x83\x14!\x8c\xf2\x1f:\x14@o\x82>\x00\xa1\x11Оf\xd6m\xe1\xbcE\xd8.U\xa2c*\x15dv:\xbb\xf2\xd3$\x03\x8eS\xb3\x90\x84K\xb1\x05\xe5z\xb7V/\b\x98\x02+p9\xb13\x90\x02n\xa7Y\xb2\xa9\xec\xe4tI\xacv\x0f\xca\x00\x13\xda\x00\x8d\b\xe7\x13\xf8\x03_\xacu\x86\xfc\xday\xa4\xb7ֱ\xce\xc3B\xa37\x9d\xa4\xf0\xe9\xfd(D\xef\xb6p\x96\xa1w\xec\x1d\xe1%:\xf411m\xbc\x17;5\xe1jò\xd2\x0f\xbbqKF\xed\x81\x06c\x1b]\xfc\xe9b\x81\x1c\xee\xf6\xda\xedC\x13\xaa\xa0&K\xb2݄\xa24\x87~mf\xa0\x88PqԞ$\xf2\x93*E\x0f\x03ܬ\x17Fg\xe4\xe7\x10\xcc#\x8e\x8aP\xed\x85yz\xdc\xef?3W\xcf\xc3G\x8d\x01\x02ʄ\xe5\x9f]\xabwا\xdd\xc2֒MH\x13\x81\xe7\xfc:\xc8q\xcd:\u00ad߉Xg\x91\xf9!!\xafe\xcb\v\xef?$\xa5vR>LQ\xe7\a[\xa7Y-\x92\f\x03Qd\r;\xbagRyԛ\xa9\x16\xbe@V\x99\xa8\xd6SCr\xb6ـ\xb2p\xca\x1dՠ]\xfc`\x98 \xc3\xeb\x1a\xd22#яGx4\x8c\xb4lḂ\x86n\xfd\x88\xe3Y2\x14;P\xeb^\xe3d\x9c\xb3=\xcb+\xcaq^\xa6\"s\xf8\xd0z\\1+3\xc2\xe4ޘ\xa3\x92\xe9\x8as\b\x02R\x96I\x9d%\xa4\x14`}\xde®\t\xfaU\x871_S\xeb\xab\xc8!\xec\t2KU\x1c\xb4\xef*G7\xb2\xb1\x19\x8b\x86)\x18\xa1!\x9c\xae\x81\x13\r\x1c2#U\x9c\"S|v%\xc5\b\x0e\x102b\xf9\xba+\x8d\x06\x81\x11\x90\x04\x97p;\x96휫g\x85\b\xe1\x90\\\x82u\xf8\f\xa1e\xc9#\xd3ESF\x99\xef;\x19\xd3\xf5\xa6Lh\xfd1\xbc\x98\xfe7%\xc1f6%J\xdaF\xbf\xba\x94\xad\xc5!\xbe\xa6m\xca?'a\x83\xe5?AhG\xb4\x9f`\xb8,Y\xa6\a\xe5\xd6R\x95\x81\xbe\xb4\xee\x14z:\v\xc2L\xf8uJ\x13:>W/\x8a\xd8!\xc2\xd7͛\xf9B\x9fȚ\x14\x9dx&\xc6\xd4]\xfc\x03\xf2\x05\xa7\x8c[?c$\xf3\xe4\xa7v\xab\x05a\x9b\x9a\xe8\xf9\x82l\x187\xa0\x8e\xa8\x7f\x92\xa9\x0f\x9c9\a1Rf=\x82\xfb\x1a&۽\xffb]0\xddl\xee%\xd2帱sd\x83\xb7ߝ\x9e'\xe0\x12\x8c\xef3\x17m\u0557\xb8bj\xff\x82\xae\xd5ۏ\xef\xe2\xeb\xabvI\x90\xbc\x1e\"\x13J\xe7\xca\xdb#\x8c\xda\xe3\xf3.|\xf8\x82>P\xbd\x00r\xb1\xea\x05\xa1\xe4\x01\x0e\xceu\xa1\x82X\xfe\xd0P9\xa1{\x05\xb8Y\x85r\xf6\x00\a\x04\x13\xdf}\xea\x97Tip\xe5\x01\x0e)Վhh\xc7Ĵ\xdfU\xb3t\xb2? !p\xd3!U\f\\\xf1\xaa\x10\xd9뉗D[\x12J\xa0\xfd\th&\x89J\xbb\x8f\xf6\xf6-J\xc0w\xda\xf1\xd2j̎\x95hV1\xe2 7\xc9\fu\xe5\x9er\x96\xd7\x1d9\x1dY\x89\x05\xf9(\x8d\xfd\xe7\xfd\x17\xa6\xfd\x0e\xef;\t\xfa\xa34\xf8˳P\xd4\r\xfc9\xe9\x19v|,B\xce\xca[\x82\xb5\xf7(ݜf\xa5\xad\xa6=\xd3d%\xecrő$\xb1+\u070evݹ\x8e\x8aJ\xe3\xf6\xa2\x90b\xe9\xc26\xb1\x9e<\xbd\xa5\xea\x90\xfbɝ\xfa\x0e\xef\xecdᾸMqN3\xc8\xc3v\r\xee\xd6R\x03[\x96%\xf6W\x80\xda\x02)\xad\tO\x93\x88D\xc3걙'>i\xb3w\xbb|Y>\xd4\xc9\x0fK;\xe5,=\x04#\x8b\x04\x1ax\u06ddO㳴:\x9bP+H\xc2dՁ\xcd\xdc\xe1\xaa)Dy\x029p\x16G\x17g\x92\xbb4\xcf15\x88\xf2\x9b\x193\xca\fY\x98k\x1aZcwSpAq\xab\xe5\x7f\xecL\x8b\xda\xf4\x7f\xa4\xa4L\xe9K\xf2\x16s}8t\xbe\xf9\xa0Y\vLB\x97\x98\xabc\xe5gO\xb9\x9d\xfb\xad\x01\x17\x04\xb8\xf3\x04\xe4\xa6\xe7\x17-\xc8\xe3Nj7mכ8\x17\x0fpp;\x86\x93]\xb6\x8d\xcc\xc5J\\8\x1f\xa2g0j\x87C\n~ \x17\xf8\xed\xe2)\xaeT\xa2\xa4&V\xeb\x88hA\xcb4\t\xc5\\\xabTG\xdd.X\x83\x13b\x1b\xd69D\xd6\xc9\x1e\xc36IDK\xa9#\x1b\xf9\x03C\x99\x10\xde\x1b\xa9\x8d\x8b\x97u|\xe6h@M\x86 \x1a\xa1\x1b\x97\xd8%U\xc8±Fy*\xf4\xdb.w;\xd0\xe0\xf7+|`\xce\x01\xb5+\xbb\x8bF\xbf\x9d\xb5\xbfp\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xe6\x8bH\x96H\x1b\xf7:\xe6H\xdd*\xc9媌\x87@CIwy-!f\xae\x17\xde\x7fi\x05D\xad\xeeۿ\xa7dl\xee\xb8\bfY\x16\x05=\xce\xdfJ\x1a\xe2\xb5k\x19\xb4\xc1\x03r\x8b\x0f\xb5\xad\xd0\x12\xa4\xce\xe5\xb5\x00~\r\x8eB\xc1\xc4\n; o\x9e\xc1\xb1\xf064\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fN\x95K\x89[\x05\n:\xcc\xebG\xd5\xd1\x0f\x15Ҵ\x02\x123\xdc\xcdR\xe6\xdfi\xb2aJ\x9b\xf6\x10\xf4@\x9aJ\x14\xcc̅\x97x\xaf\xd4I\xeb\xaeO\xae\xe5Q\x02\x99\xcf[s\x84I\xc4\x1c\xf7\x97\x80\xb0\ra\x86\x80\xc8d%0\x80c\xf5\x18\xbbp\xc4u\x16\x96\xa5*I\x9a\xf6\x93\xc1\x1c\xb4XY\xa2\xa401\x1a\xe9iW\xff@Y?Q-Vf\xb2\xcd\fe\xb1\xc5\xcai:\x11R\xdcڙ\x8a\x05\xfd\u008a\xaa \xb4\xb0<\xc2ɜ\x15\xd0ez\x93\xf8f[\xe04a\xa4\u0558\x92\x83\x01\x9f\xbc\x968\x86L\n\xcdr\xa8'W/\bR\x10J6\x94\xf1J%Z\xc0Y䝳\x14\xf1\x96\xe0|k\x8c\xb4ΗH\x8a\x84hn\xa2\xaf8n\x8dK\x95\xee\xf1M\xb9Y\n\xe6{Y\xa5b\x12\xd3\x02\xcf\xech\xf9DJ*\x0e\xdf<\xadԡ~\xf3\xb4\xc6\xca7Ok\xa2|\xf3\xb4\xbeyZ)5\xbfyZ\xdf<\xadv\xf9\x97\xf0\xb4\xa6F\xe4\x0e:\x0e|\x9c\x1cE\xc2V\xf5\xd8\x10G\xe0\xfb\xe4\n\x9f\x03\xfe\xa4\\\xccU\x1cT$\xf1\x7f \xad;f\xb4\x9aɣNδZ\x13dޝ\xbb\x9ap%\x9f\x90u\x1f:=_\xd6\xfdj\x14♲\xee\xfd\xb0\xa7}\xec\x93r\xee\x03Q\xe6eg/|\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x99\xe8\xff\x85\x13s{Ycg\x94\x8fg\xcf\xe2O\x96\x91(K/\xfet\xf1\xf5\x91\xff<\x04\x1f$q\x9fv\xfe\xe0w\x04\xaa]\x81\xb6\xd3ºYx_\xa7\x18\x9fEnS3\xf1k\"F`uE\xf2\x88\x8a_\xab-0P|*\xfd\x8c\U001044ea\xab\b\x9c\xa4\xb3\xaaT\x1fD\xb6SR\xc8J\xfb\xa8\x84\x85\xf56s'\xfd\x03Ș\xb0F5\xfc?\xc8NV\x91L\xf0\x11\xf2Md\x04N#\xdfI\x0e\xf4\x9b\xd0`\xe8\xfe\xcde\xf7\x8b\x91>Up\xe8l\xf3\xe3\x0e\x04\uec0bm\xfb\x00@\xb8\xa8\xc1\xdfXp,`\x11@R\x11\xc1\xb8\x93\xbc\xfa\x9a\x87\xb6ܑO\xa5\x8b=\xcd\xf6;\xc6c*iɄ'\xa7\x10vS\x04\a\xfcҹ\xbb\xddg92\xf1\xbb\xa4\x06\xceO\bL\x89\x88M$\xff\x9d\x90\xf2\x97\x98[\xfc\xe4\xed\xf9\x94\xa4\xbe9+\xe6gK\xe0;\x7f\xda^\x12}\xa6S\xf4\xe6P\xe7\xd9\xd3\xf1^0\t\xefeR\xef\x12\x13\xeeΗ9\x9f\x16\x8f=)sl:t0\x9c47\x99*7\x19Z\x98Bl6J\x93)ps\x12\xdf&\xb9\x93\xa6f/\x96\xda\xf6b\tm/\x9b\xc66*E\xa3\x1f\xe7$\xaa\xc5\xef\xeb!\x93\x93m\xef>\xb5^\x85sJ\\r,nP\xe9\xf8K)ǩl\x93\xaa\xe3n\x9f\xb4\x1e\xfct\x04\xc3\njpE_ȧ/*nX\xc9q\xe3w\xcf\xf2hp\xc4\xec\xe0P_\xf8\xf1\xabģ\xb2\xfe\xe6\x9aO\x9fk-\xbb\xb8\xf4\x9b\xe8\xd4\xfc\x9ef\xbb#\xe8;\xaa\xc9F\xaa\x82\x1arQoX\xbev\xc0\xed\xdf\x17\x97\x84|\x90u\x0eG\xfb\x1e!͊\x92\x1f\xec\n\x85\\\xb4\x1b\x9c&\x01Qi\v\xbd\xddHβ\x88\xef\x16\xbdK\xcaU\xee]\xee\x817\\e\xed\x14\x87\xd2V\x8c\xbbn\xe8\xe6u\xaf\xec\xdcH\xce\xe5\xe3\xdcXE\xc9\xfe\x82\x97\xb3?!\x9a\xf5\xf6f\x850\x82x\xe0m\xefu2Y\x8d\xcd\x1a\xec\xb4\xdc\xe09\xa4\xfb\xabM\ab7/\xb3}\xcb1\xe4\xeeB\xeb\xe0\x16xәIk]nVn\x1cC\xbdX\x99\xa1\xe2@$f\x00\x99\x1dS\xf9\xb2\xa4\xca\x1c\\bɢ3\x860\x97\x8eE\xa3\x06g\x8f\xfe%\xddQ\U00086ef9qG\xf5Pv7\xa9\x8fiw\xca8\x86O[N\x9e\xb3<\xe38\x86ݒ%R*\xf2s4S\xedlQ>\xedoR\xfeY\xee\xe1]4\xda\xd7!\xcf\xedQ\xf5H:Y\x80\xe8.\t\x1e̪]\x03^ \xdc\xff\xf4\x84\xfc\xb0е\xbf\x03\xf6\x94@\xd9m\x17D\x04\xbfp#n\xe8,f\x9f\xf0&\xff\x03\xb9\xb9\xc75Zmڼ\x8a\xfa5Z\b\x95\x85\xcd\xeb\b\x1c\xdf\xe0\xfb\xf3\xa7\xd2i#\x15\xdd\xc2O\xd2]\x96>\xc5\xf6n\xed\xce%\xfa\xde\xeb\t\xf9\xaeAib\x17\x06\xfbkۏ\x8059\xea\xbdK\x98\xed(g^+m\f?\x85\xefww?9\xac\f+\xe0\xf2]\xe5\xd23\xacM\xd4`I\x1c\xb0u\x90\xd6\xf6\xbf;\xf9\x88\x97\x15\xc7\xe3\x98\xe1\xf1\x8b\x06\x19\x05\x98\x1c\x8f)\x93\xb3P\xaaJ.i\x0e\xeaZ\x8a\r\xdbN`\xf7K\xa7\xf2\xd14\x9b\xe1\x8f\x1e\xb9z\x8e\n\xf0Ϝ3a}\x1e\u0381\x7f`\x1c\xb4\x1bV\x82\x01\xbe鷪\xedqU\xac\x9d\x0f\xb7\xb1\x1f\xeb\x0e\x06\xe68\x87\x16\x86\xa2KP\u058brA\xebJ\aY\x1dF\xbc\xe1\b\x13\x06\xb6\xd0_\x05\x8eX`w\v6N\x9f\xc1\x9c\xe0Z\xe6\xc7X|\xab\x83\xfc\xfdp\xcb#N\xb6B^\xb1\x1b\x02\x9d\x13rs\x7f\xadI%r\f\x17\xdf\xff\xe5v\x96\xd4\xed;7\xed\am\x9d2\xaa\xf7\xf1V-\xe7\xb8e/\x9cw,7\x11\x04\x86\xe0\xb4\x1etyd\xc6_4vޛa\x87\x96ڡ\xf1%\xa3\xeft\r\x13s[\xf1\xfd\x95>\x11\xfaί\x8bV\\Y\xef\x1f\x96\x16\xc4i^\xeb\xd0\xeb3\xddy\xe1iF\xee\xfav5\x04\xee\x14\x13\xd7\x7f\x9e\xe6\x89j\xdcG\xf7I&\xad\x8f\xee,\x83\x16\x81X\xcb\xf8\xf9qGU?\xed\x12zl\xe9\x1c\x8e,\x9c\xf9\xa3\x9c\xfb\x83\x99\x05hM\xb7\xe1\xf6\xf9G\xbb\xf4\u0602\x00\x17\x9es\x9b'\x11\xa0\xcd)\xbe\xee\xdd\xebNehf*\xea;\b\tɭZ\xdfi\xc2e\f*>@\xc3\u0093oaM6\x93P_J\xa6R\xd6p\xef늖6\xe8\t#w\x9aG\xfa\x80\xb3->Ae9\xb7\xa5jM\xb7\xb0\xcc$\xe7\x80ֺ?\xae\xe7\xd4u\x7fV\xf23P=\x89ڇv]\xbf\x03\xe8\xb8\xed6\xbe\xa9K\xcf\xc7g\xd8\fSм\x88\xd8\x1b\x90Ďg9ʎ\n\xd1\xe7\x02\xfb#m\xd7\rZ\xe7Ͳ\x8f\xf3\xfa\xd7\x02\x17\xcd\v`\x91q\x16\xf4W\xa9\x16\xa4`\xc2\xfeCE\xee6\xf0B\xe3Y\xe3\xdfI\xf9p\x1bqb{\x83\xff\xa1\xae\xd8lu0ᆍ\a\\ײ\xf2\xbb\xef\xb5C\x1b\xdfV\xc1\x97\x04μ\xdcD\x98#\xf3A\x0f\x9d\xc1\x88\xee\x0f\x1dH\x93S\x81\xeby\x00\xd6mx\x92\x8e\xf3\xc3\xe2\x18\xf2\xd1\xf3\x97\r\xec\xd6K\v\xde\rh\xeeO\x18\xe8(\xecHE\x81\xd4\x17u\xb4\r\xfa)\xab^O\xe6!g\xb2G\xe3\x1f\x9a\xdaCtt\xc3l\xb9{\x03\bv\x9c\xc0\xf3.\xd8\xf1Y\x8d\t\u1ff1u\xea\xbb\x16Z\v\xb7\x90%6\x18\xa5\x1bz\x99\xef#\xf4\xb7+\x96\xe4\xaf\x15T\x11\x1a,\xc3Cv\xb7\x86\xaa~\xc8\xd7\x1dۇ\x1c3:P\x1b#UV\xe2Fɭ\x02\xdd\x17\xd6%\xf9\x1be\x86\x89\xed\a\xa9nx\xb5e\xe2\xd3\xf0\x11\xa5\xb1\xca7T\x19f\x85ݍ'6P&(g\x7f\x8fٵ\xf6\xc7i@׃\v\xac%I\x18\xc6Їw`}\xdc\xc1\xb8@Ԅ\x96\x9e\xae\xa7\xf8+\x81'S6\xb5\xf6%\x1a_$t{I>ʨa\xf0\xe9P\xac\vӺd\xa0\xcd\x126\x1b\xa9\x8cۭ^.\tۄ\xe0\x83\xb59\x187s\x8f\x8e\x12\x16\xdbf\xae\x13M\x9a\xe9\v\x83\xde\nga\xbcz\xbf\xa0\a\xb73E\xb3\xac\xb2\x1e\xd6km(\x8f88O2\xfc\x18\xe5\xf9\x1e\x1f\xd8\xfc\xe5I;y\xab6\xa0~\xd0\x11\xfbq$\xc5\xcb?\x9c\xd7\xc7-\x8a ȣb\xc6X\x9fJ\x8e\xa4\x12xR\x19\xeb[qN\xb4%\xf5I\xd1G\xe2\xcc\xe8j8%'\r\xe5\xbb\x1aʐy\xf6X\xe3K\x92\xf5+\xa6>\xfb\xc8ײl\xcevTl\aoT\xd8)YmwA\x92\a\x9ci\x92W\x80\xc1Z4):\xbc\x10m*%Z\xa9\x04#\xc7\xd4I\x10\x06\x1c.\xcd\x1e\xf0\xbdU\xf7\x02\xb3\x7fz\xfb\xb5\x7f\xb3e\xb9Q\xb2X\xfa~1\x96\xba\xf0;\xf9\x8aI빘]\x94\xea\xc4y\xed\xfeY\x04\x94\x84\xb2\x04A\xa8\xf6='\xdclu\xf24\xf5\x9b\x9d\x1an\xa4f\t\xde~\x94\xe3\x7fm\x03\b\f/\xc3\xdf]f\xf8\x15\f\xf6\x19\xc3㓿2\x00\xf6T\x18\xb7\x9c\xa8\xa7\xc8\v7\x89]\xccZ\xc8h;\xb1=)Hsہ0\x11\x9f\xc1\xee\xe2,\xba\xf5\xe9\x1a\xee\xe2\xb2k\xff\\l\rxA4\x13\xe1\x05s\x97\xfa\xe1\xa4?\xba\x13(\xf0aM\xa9\xe2٘\xe3\x01\x97.B/\x1bk\xd9מ\xc4\xfb\x93\x97\xe2\xf7G0\x8e\x0e\xa1\xe3;\xaau\x95\xb0|\xfe\x03\x8b\xed\a`\x1aofQ\xf9\xe3\xef~\xb8|\x9f\xb4ԋSdl凋\xba\xe1%\\\xf7\xdd\xd4\x1b\x0eV\xdb4@wQ9K\xe7\xf6g\x8c\xa6\x9d3\x94\x16\xde\xea?O,i\x7f\xc6 ڳE\xd0\u038b\xf2#\xc5\a\xadO\xd2ڿ\xf9\xb6\x91\x10\x9a\a{\xee Z+\x86\x16\x06\xfe\xa2Q\xb4\xe8\x9c\xdb\xfb\x11\xedt\u07b2\x16\xbe'\xff\xcb\xff\a\x00\x00\xff\xff\x11\r8\xff\x9b\x84\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccZK\x93۸\x11\xbe\xebWt\xed\x1e\xf6\xb2\x94줒J\xe96\x96\x93*W\xc6\xf1\xd4hvr]\bhJ\xb0@\x80\x01@\xc9\xca㿧\x1a\x0f\x89\xe2C\x0f;q\u008b-\x12h\xf4\xf3\xeb\x0f\xc0\x14E1a\xb5|E\xeb\xa4\xd1s`\xb5\xc4/\x1e5\xfdr\xd3\xed\x1f\xdcT\x9a\xd9\xee\xedd+\xb5\x98âq\xdeT\xcf\xe8Lc9\xbe\xc7Rj\xe9\xa5ѓ\n=\x13̳\xf9\x04\x80im<\xa3\u05ce~\x02p\xa3\xbd5J\xa1-֨\xa7\xdbf\x85\xabF*\x816\b\xcfK\xef\xdeL\xdf\xfe~\xfa\xbb\t\x80f\x15\xcea\xc5\xf8\xb6\xa9\x9d7\x96\xadQ\x19\x1eENw\xa8К\xa94\x13W#\xa7\x15\xd6\xd64\xf5\x1cN\x1f\xa2\x84\xb4z\xd4\xfc]\x10\xb6\x8c\xc2\x1e\x93\xb0\xf0]I\xe7\xff<>\xe6Q:\x1f\xc6ժ\xb1L\x8d\xa9\x15\x86\xb8\x8d\xb1\xfe/\xa7\xa5\vX9\x15\xbfH\xbdn\x14\xb3#\xd3'\x00\x8e\x9b\x1a\xe7\x10f\u05cc\xa3\x98\x00$\xd7\x04i\x050!\x82\xb3\x99z\xb2R{\xb4\v\xa3\x9aJ\x1f\xd7\x12踕\xb5\x0fΌ\xb6@2\x06\xb25\xe0<\xf3\x8d\x03\xd7\xf0\r0\a\x0f;&\x15[)\x9c\xfd\xa2Y\xfe\x7f\x90\a\xf0\xd9\x19\xfd\xc4\xfcf\x0e\xd38kZo\x98\xcb_c\x8c\x9eZo\xfc\x81\fp\xdeJ\xbd\x1eR\xe9\x919\xffʔ\x14A\x93\x17Y!H\a~\x83\xa0\x98\xf3\xe0\xe9\x05\xfd\x8a\x1e\x02r\x11B\xf6\x10\xec\x99K\xeb\x00좔\xe0\xa3aMUo\xad3\xb5I\x15x\xedH\x89\xfaӛ\xa4}Kl\xce\xef)\xb7x\x14\xe9<\xab\xea3\xb9\x0fk\x1c\x13v\xe6\x8a\xf7X\xb2F\xf9\xb6\xa9\x14%\xd5\xce\xcbs\xb3j\xe4S\x11g\x9d\xad\xf8\xfe\xec]\\ue\x8cB\x16\xa5\xc4Q\xbb\xb71\v\xf9\x06+6O\x83M\x8d\xfa\xe1\xe9\xc3\xebo\x97g\xafa(\x91:EA\x81c\xad\xd8l\xd0\"\xbc\x86\xfa\x8bqsɴ\xa3L\x00\xb3\xfa\x8cܟ\x82X[S\xa3\xf52\x17K|ZX\xd4z\xdb\xd1\xe9\x9f\xc5\xd97\x002#\xce\x02A\xa0\x841\xafR\xfd\xa0H\x96\x83)\xc1o\xa4\x03\x8b\xb5E\x87:\xc2\x14\xbdf:)8\xed\x88^\xa2%1Tۍ\x12\x84e;\xb4\x1e,r\xb3\xd6\xf2\xefG\xd9\x0e\xbcI\xc9\xec\xd1y\b\x15\xaa\x99\xa2dm\xf0g`Zt$W\xec\x00\x16iMhtK^\x98\xe0\xbaz|\xa4j\x90\xba4s\xd8x_\xbb\xf9l\xb6\x96>#47U\xd5h\xe9\x0f\xb3\x00\xb6r\xd5xc\xddL\xe0\x0e\xd5\xcc\xc9u\xc1,\xdfH\x8f\xdc7\x16g\xac\x96E0DGH\xadď6a\xba;[\xb6W\xd2\xf1\t\x90zGx\b^c\xcaDQ\xd1\xc4S\x14\xe8\x15\xb9\xee\xf9\x8f\xcb\x17Ț\xc4HŠ\x9c\x86\xf6\xfc\x92\xe3Cޔ\xbaD\x1b\xe7\x95\xd6TA&jQ\x1b\xa9}\xf8\xc1\x95D\xed\xc15\xabJzJ\x83\xbf5\xe8<\x85\xae+v\x11\xba\x18\xac\x10\x9a:\x80Dw\xc0\a\r\vV\xa1Z0\x87\xdf9V\x14\x15WP\x10n\x8aV\xbb7w\aG\xf7\xb6>\xe4\x9e:\x12\xdaA4X\xd6\xc8\xcf\xeaN\xa0\x93\x96*\xc33\x8f\xa1\xba:\x0eJP1ޔ\xf33\f\x12\xf40\xceѹ\x8fF`\xf7KG\xe5\x87\xe3\xc03\x1dk\xb4\x95t\xa1\xbdBil\xb7\xf3\xb0#\x92\xb7\x9f\x8cx݀\x03\xa0n\xaa\xbe\"\x05<#\x13\x9f\xb4:\x8c|\xfa\xab\x95\xbe\xbf\xd0H \xe9\x89*.\x0f\x9a?\xa1\x95F\\1\xfe]g\xf8\xd1\x05\x1b\xb3\x872\xe4\xbf\xf6\xea@\xd8\xe5\x0e\x9a\xf7Q;?\x0fO\x1f2\x82\xc7\xdaJ\x85\x99|5\x85\x87TԦ\x847 \xa4#\"\xe1\x82о\xb3t\xa3\x02ј\x83\xb7\xcd]\xe6s\xa3K\xb9\xee\x1b\xdd\xe6Fc\x19sEt\xc7s\x8b\xb0\x12\xa1\x16eGm\xcdN\n\xb4\x05Շ,%O\x9a46v\x90R\xa2\x12=l\x1a\xad\xb2`\x8aEAE\xcdԕ\x18.\x8e\x03\x03\x93fR\xc7\f>\t\bXc\xabԚ\xb5G-\xb0\xdbm\x826&\x00\x9aC\x01{\xe97\x11)\xd5P\xdd\xc1\xc5ڣg\x8b\x87\xa1\xd7\x1d\xdd_6H#c\xe3Ep\xc8-\xfa\x90m\xa8(}(\x95\xa6\x00\x1f\x1b\x17\xb0\xb6\x8b\x13\xf9\t\x84/\xcf\xde\xe2\xa1\xefh\xb8\x16\xdcD\x85FT\x0e$j\x0e?\xfcpݤ^w\xcb\x0fQ\xf7l\xa8\xc5\x12-\xea\x1e\x9b\xc8\xcfK\xe8Q\x944\x94aX\x96Ƚܡ:\x84\x9eD\xe0\xf93\xac\x1a\x0f\xa2\xc1\x105Ʒ{f\x85\x03n\xaa\x9ay\xb9\x92J\xfa\x03H7\"\x9f)e\xf6(Rı\xaa\xfda\n\x1f\xb4\xf3LstG\x1eD\x1e\x8b\xa9\xc0t\x1c\x95\xaa8\x10:f\x8700\x8a\xaf\x8c\xf3\xc0\xd1R:\xaa\x03\xec\xad\xd1\xeb1c\a\xda!\xed\x01\xadF\x8f\xa1#\n\xc3\x1d5C\x8e\xb5w3\xb3C\xbb\x93\xb8\x9f\xed\x8d\xddJ\xbd.H\xc1\"\x81\xcf,\xec\xecf?\x86\x7f\xbe&\vL\x1dq\xe2\x86\xe4]\x86Z?\x10\xbd\xf5\x1b\x8c-b\x19s\xd0X \x02A\xa9]\xa5܍\xc8:TvC\xbc\xbc\xfd\xe4\x90\x0f\xf5\x8f-\xf6[\xc7\x05P\x01\xf8R\x9c|[T\xac.\xe2h\xe6M%\xf9\xa4km\xcc\xfb\xcb\xf8\x937+R\vɉܞ\xe3F\xdeĉ\xb3=̀\x1b\xba\xbb\x9c1\xb4\x1cvS47q\x85+\x1a\x7fj\x8f=m}#t\xa7\xfe\xef\xd0\x13\xeft\xa0\x91\xf8\x01\xb3}?\a\xc0\xe4FkB*o\x80\x1d\xdb\xc0O\xae\xdb\xff\xeeD\xcfU÷8\xe0\xf8\x9e)\xef\xc2\xc0\xec\xe38\x8dti\x1c\x86\xc6tM\r\xb8^\x11\x9c-\xd0ޢ\xcb(\xf2-\x1eH\u0091[0X<\xc0\xaa\xd1BaVu\xbfAM\xdb1Y\x1e\x88\xec\xbf<.\xb3c\x03\x01K[\xa7\xec\xde1 yO\xbb\x00JA1\x87_\x1c\xa6u\x9f\xb1\x04\xa9\x9dG\xd6#\xe9\xf1\x89\xbdq\x0e\xab\xc3\x00\u05fa\xd9A\xcfX~\xbb\x8f\x82\xae\xe4\xa1\xd4 8\xc6\xc4J\xb0\x92\xfa{\xde\x0f-\x1e\x02\xc4\x12\xdf \"}\xe6ґe\xeetth\xd0i\xf1\fdR\xc7\x02a\xd5\xd8\":\x1fR\x01\x8by9H䇃q\xb9.\xe0\x12\xb3\xe89\xfb>v1*\x13\x80\xdd\xc80\xe0z\xb2\xc0E\xa6\x017\xb0\x8d\x9e\x99\xa39\x05w\xb2\x0e\xf8\x0e\xcc\x03\xfe\xfb\xec\x03\xeef \xf0\xddY\bܖ)\x97\xd9\b|\x13#\xb9\xe0\x8bK\\\x05\xae\xf2\x15\xb8\xc8Y`\x94\xb7\xc05\xee\x02w\xf2\x17\bx\x82\xa5\xfcr\x032?\x85\x81\xb9\x93\xd6\xcco\xa8kH\x81\xc0\x06\xfaj<\xa1\x18q\xd0q\xd3\xfb)\x85\xef+\xfa\xee%\xd2\x17չ\x87\xf7e@\xbfB\x8c\x9eҰ\xa3\x17\xf2\xef\x04 \xe7\a c\x04mТ\xdd\xf1\xb4\xfdO\xf1X\x81\x0f\xa0\xf8\x992\xaf\xfd\x19\x17\x8e'\xf2\x99\xff\x10K\xa3Ͱ\xb1\x16]m\xb4\xa0\xb6w\xdb\xe1\xc4I\xe5\xff\xdc\x11\xc5pX\x8bs\xfa\xda\xf9\x96\xa3p\xd3\xf9\\\xb8߸\xfb\x84.\xde\xfa\xb4Ͽ\xccʡݵ\x0e\xe9:6~\x97\xb3\xb9\xc1\xce\xd6:\xb0#\xaa\xa4\xa1\xd1\xe1\xc8\"4\xad\xe9d`F\x9b\x17\xfa\xd0<\xa4\x03m\xf64\xb9%-v=\x13\xe9M8\xb4dZ\xa4\xe3b\xfa4 y/\x95\xa2\x1ef\xb12\xe4,\xd4^Zj\x96,\xb4\xb1\xddo\xa6o\xfewg\x81\x8a9\xbf\xc6Qq\xff\x99\xa6\x00[\x99\xc6\x0f\xf4\xfeV\xc2\x0f\xd6t\xb8e\xbfG\xc7\xf0\xb7\x03\xd7\xe8\t\x8d\xc9\x11፵\xe1\xb2._\"ݱ\xd1\x1cC\xe0\x87Ο8\xb4\xbf\xf5\xff\x00\xe2\x06\xbb\x06\xbbt\xefe촭\xb8&'\xb7\xdf4\xab\xe3\x15\xec\x1c\xfe\xf1\xafɿ\x03\x00\x00\xff\xff%\xff\\)\x99#\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVK\x8f\xdb6\x10\xbe\xfbW\f\xd0k%wQ\xb4(tk69,\xda\x06\xc6n\x90;M\x8e-f)\x92\x9d\x19z\xeb>\xfe{AR\xf2C\xb67\xc9%\xba\x99\xf3\xe4\xf7\xcd|t\xd34\v\x15\xedG$\xb6\xc1w\xa0\xa2ſ\x04}\xfe\xc5\xed\xf3/\xdcڰ\xdc\xdd-\x9e\xad7\x1d\xdc'\x960<\"\x87D\x1a\xdf\xe2\xc6z+6\xf8ŀ\xa2\x8c\x12\xd5-\x00\x94\xf7AT>\xe6\xfc\x13@\a/\x14\x9cCj\xb6\xe8\xdb\xe7\xb4\xc6u\xb2\xce \x95\xe4S\xe9\xdd\x0f\xed\xdd\xcf\xedO\v\x00\xaf\x06\xec\xc0\xa0C\xc1\xb5\xd2\xcf)\x12\xfe\x99\x90\x85\xdb\x1d:\xa4\xd0ڰ\xe0\x88:\xe7\xdfRH\xb1\x83\xa3\xa1Ə\xb5k\xdfoK\xaa7%\xd5cMU\xacβ\xfcv\xcb\xe3w;zE\x97H\xb9\xeb\r\x15\a\xee\x03\xc9\xfbc\xd1\x06̚\xaa\xc5\xfamr\x8a\xae\x06/\x00X\x87\x88\x1d\x94ب4\x9a\x05\xc0\bH\xc9Հ2\xa6@\xac܊\xac\x17\xa4\xfb\xe0\xd2\xe0\x8f\x95\x905\xd9(\x05\xc2\x0f=\x96\xcbC\u0600\xf4\b\xb5\x1cH\x805\x8e\x1d\x98\x12\a\xf0\x89\x83_)\xe9;h3\x92mu͍\x8c\x0e\x95\x847\xf3c\xd9\xe7\x86Y\xc8\xfa\xed\xad\x16X\x94$\x9e\x9a(um\xf0@'ȟ7P\xfc\xdb\xd8+>\xaf\xfeT\f\xb7*W\x9f\xdd]EZ\xf78\xa8n\xf4\r\x11\xfd\xaf\xab\x87\x8f?>\x9d\x1d\xc3y\xafWH\aˠ\xa6N3p\x155\b\x1e!\x10\f\x81&T\xb9=$\x8d\x14\"\x92؉\xff\xfa\x9d\xac\xd5\xc9鬅\x7f\x9b3\x1b@\xee\xbaF\x81\xc9\xfb\x85\\@\x1c\x87\x02\xcdx\xd1\n\xaee \x8c\x84\x8c\xben\\>V\x1e\xc2\xfa\x13jig\xa9\x9f\x90r\x9a<\xaeə\xbc\x96;$\x01B\x1d\xb6\xde\xfe}\xc8\xcd\xf9\u07b9\xa8SR \xc9c畃\x9dr\t\xbf\a\xe5\xcd,\xf3\xa0\xf6@\x98kB\xf2'\xf9J\x00\xcf\xfb\xf8#\x83h\xfd&tЋD\xee\x96˭\x95Ilt\x18\x86\xe4\xad\xec\x97E7\xec:I ^\x1aܡ[\xb2\xdd6\x8ato\x05\xb5$¥\x8a\xb6)\x17\xf1Ep\xda\xc1|G\xa3<\xf1Yً\xe9\xa9_ч\xaf\xa0'\xabE\x9d\x91\x9a\xaa^\xf1\xc8B>\xca\xd0=\xbe{\xfa\x00S'\x95\xa9J\xca\xd1\xf5\x02\x97\x89\x9f\x8c\xa6\xf5\x1b\xa4\x1a\xb7\xa10\x94\x9c\xe8M\f\xd6K\xf9\xa1\x9dE/\xc0i=X\xe1ib3u\xf3\xb4\xf7E\x90\xb3\x02\xa4h\x94\xa0\x99;b\xa3\x13Q\x19\xbe\x83ΫkA_\x8a\x05\x12\x05\xba8\x9d5\xf5\xae8\x95\xff\x12\xcaz\x06\xe5\xf7c H\xaf\x04^\x90\xf2\x86萲Z\xa1\x01\x93.\xf0\x1ba9}\x93\"\x05\x8d|\xb1\x8a\x00Vp\xb8\xd2\xd3+\xec\xe4\xcf'\xe7\xd4\xdaa\aB\to0\xab\x88\xd4~f+o\xdfg Xe\x9fk\x1c\x1c\xde\xfbϒP\xe0\xf6i\xb8\xac\xd4\xc0{|\xb9r\xfa\xe0W\x14\xb6\x84<\x1f\xf9l\\U\xf4p\xfe\x1e\xbc\x82\xd2ա\xbc8\xe4,\x85\xe6\x04E\x96@j{\x8a+\xa7\xf5A\xe9;\xf8\xe7\xbf\xc5\xff\x01\x00\x00\xff\xffߙ6&\xcb\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcW\xcdn\xe36\x10\xbe\xfb)\x06\xe8\xa5\x05Vr\x83\xa2E\xe1[\xeb\xec!\xd8t\x1b\xc4\xdb\xdc)j$\xb1\xa1H\x96\x1c:\x9b\xa2\x0f_\f)\xf9G\x96\x1d粺y8\x9c\x9fof\xbe\xa1\x8b\xa2X\b\xa7\x9e\xd0\ae\xcd\n\x84S\xf8\x95\xd0\xf0\xafP>\xff\x1aJe\x97ۛų2\xf5\n\xd61\x90\xed\x1f1\xd8\xe8%\xdeb\xa3\x8c\"e͢G\x12\xb5 \xb1Z\x00\bc,\t\x16\a\xfe\t \xad!o\xb5F_\xb4h\xca\xe7Xa\x15\x95\xae\xd1'\xe3\xa3\xeb\xed\x8f\xe5\xcd/\xe5\xcf\v\x00#z\\Am_\x8c\xb6\xa2\xf6\xf8O\xc4@\xa1ܢFoKe\x17\xc1\xa1dۭ\xb7ѭ`\x7f\x90\xef\x0e~s̷\x83\x99\xc7l&\x9dh\x15\xe8\xd3\xdc\xe9\xbd\x1a4\x9c\x8e^\xe8\xd3 \xd2a謧\xcf{G\x05\xf0y>R\xa6\x8dZ\xf8\x93\x9b\v\x80 \xad\xc3\x15\xa4\x8bNH\xac\x17\x00C\xf6\xc9P1$\xbe\xbdɦd\x87\xbd\xc8\x1e\x00\xacC\xf3\xdb\xc3\xdd\xd3O\x9b#1@\x8dAz\xe5(a\xf8_\xb1\x93\xc347P\x01\x04\f\xe1\x00\xd9]\x84 \f\bO\xaa\x11\x92\xa0\xf1\xb6\x87J\xc8\xe7\xe8\xc0V\x7f\xa3$\bd\xbdh\xf1\x03\x84(;\x10l%+\x1c\xf8Ҷ\x85Fi,w2\xe7\xadCOj\x04)\x7f\a\xbdv \xbd\x94\x05\x7f\x9cx\xbe\x0557\x1d\x06\xa0\x0eG\xf0\xb0\x1e\xb0\x02\xdb\x00u*\x80G\xe71\xa0\xc9m\xc8ba\x86lʉ\xe9\rz6\xc35\x8d\xba\xe6^ݢ'\xf0(mkԿ;ہ\x11c\xa7ZP\x02\xd3\x10z#4l\x85\x8e\xf8\x01\x84\xa9'\x96{\xf1\n\x1e\x13\x82\xd1\x1c\xd8K\x17\xc24\x8e?\xacGP\xa6\xb1+\xe8\x88\\X-\x97\xad\xa2q\x02\xa5\xed\xfbh\x14\xbd.\xd30\xa9*\x92\xf5aY\xe3\x16\xf52\xa8\xb6\x10^v\x8aPR\xf4\xb8\x14N\x15)\x11\x93\xa6\xb0\xec\xeb\xef\xfc0\xb3\xe1\xc8-\xbdrC\x06\xf2ʴ\a\aip\xdeQ\x1e\x1e\xa5\xdc]\xd9TNq_\x05\x161t\x8f\x1f7_`\x8c$Wjh\xb1\x9d\xea\t.c}\x18Me\x1a\xf4\xf9^jS\xb6\x89\xa6vV\x19J?\xa4Vh\bB\xaczEa\xecu.\xdd\xd4\xec:\xb1\x14T\b\xd1Ղ\xb0\x9e*\xdc\x19X\x8b\x1e\xf5Z\x04\xfcƵ⪄\x82\x8bpU\xb5\x0e\xb9w\xaa\x9c\xe1=8\x18\x99\xf3Li'\x94\xb1q(\xb9\xb0\x8c-\xdfT\x8d\x92y\xa4\x1a\xebA\xec\x19d@\xfa\x18\xa8y\x06H\xc1\t\xdf\"M\xa5\x93X\xbe$%v\xff҉c\xc2\xfa\x1e˶d\xce\tC \x99\x8f~\x98\x16\xeaR\f0\xdb賑\x8c\xfd\xcd00\xaeL(Lv\x871\x9d\xba\xe6\x0fM\xec\xe7\x1d\x14\xf0{\x8a\xf9\u07b6\x17\xcf\xd7\xd6\x10\xcf\xc5E\xa5'\xabc\x8f\x1b#\\\xe8\xec\x1b\xbaw\x84\xfd\x9f\x0e}\xde\xd0\x17U\xc7E\xbfۊ\x17\x14\xa3>\xeb\xf7\x11y\x83\xe0\xf9L\a\x85\xab\xac\\\x11ӠyU\xa2\xeb\xcd\xdd{ <\xa3\xfe\x8e\"ݙƾ\x91\xe2^qV\xef\f\r\x8c_zC\xbc\xdd\xd3\xfc\n\x19{\x9a\xaf\xe4݉\xf0)V\xe8\r\x12\x86=S\xbf(\xeaf-\x02\xbctJv\xe9b\x1a\b^\x02!X\xa9\xe6(\xf5\x8a\xf0\x99G\x94Ǚ\xa1,ҰΈ9\xf8\x13\xf1\x19\xf6;\xe7\xa0\x18\x18\xe9*\x06%A1\xbc\x83C\x93\xfe\b\xb5\x8cާ\x15\x95\xa5\xfc2\x99^\xb8\x96DG\xe6\xf9\xeb\xf1\xfe\r&\xbd\xddk\xa6ǸP&G\xe3<\x16A\xb5\xfc\x82\xe23\xe6\xd2\xc4q\xa7`\xe4\xef\xf8\x85w\f\xd4lE\xf1\xabSy\x00\xdf\b\xf1\xe3N1\x13>\x9a\xbc\xe7\xa7o\xd8d\x10\x03?\xb7@\n3\x13c\x85P\xa3F\xc2\x1a\xaa\u05fc\xb9^\x03a\x7f\x1awc}/h\x05\xbc\xff\vR3md\xa2֢Ҹ\x02\xf2\xf1\\\x97\xcd&\xee:\x11f\xc6\xf0(\xe7\a֙k\x8c\xdd0^\xec\f8\xbb_\n\xf8\x8c/3\xd2\ao%\x86\x80\xa7ct6\x93\xd9!8\x11\x06~\xa4\xd5\a(\r\x7f\x19\x06\xc9\xff\x01\x00\x00\xff\xff\xe5\x0fY\x99e\x0e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4:K\x93\x1b\xb7\xd1\xf7\xfd\x15]\xeb\x83\xed*\ri\xf9\xfb\xe2J\xf1&\xad\xe2\x14\x13[\xd9\x12W\xba\xb8|\xc0\f\x9a\x1cxg\x00\x04\xc0\x90b\x1c\xff\xf7T\x03\x987H\xeeR\xb6\x85\xcb.\xf1h\xf4\xfb\x85ɲ\xec\x86i\xf1\x01\x8d\x15J\xae\x80i\x81\x1f\x1dJ\xfae\x17\x8f\x7f\xb5\v\xa1\x96\xfb\x977\x8fB\xf2\x15\xdc5֩\xfa\x1dZ\u0558\x02\xdf\xe0VHᄒ75:ƙc\xab\x1b\x00&\xa5r\x8c\xa6-\xfd\x04(\x94tFU\x15\x9al\x87r\xf1\xd8\xe4\x987\xa2\xe2h<\xf0\xf6\xea\xfd7\x8b\x97\xdf-\xfer\x03 Y\x8d+Њ\xefU\xd5Ԙ\xb3\xe2\xb1\xd1v\xb1\xc7\n\x8dZ\buc5\x16\x04{gT\xa3W\xd0/\x84\xb3\xf1ހ\xf3\xbd\xe2\x1f<\x98\xd7\x1e\x8c_\xa9\x84u\xffL\xad\xfe \xac\xf3;t\xd5\x18V͑\xf0\x8b\xb6Tƽ\xed/\xca@\xef\xf3\xb0\"䮩\x98\x99\x1d\xbc\x01\xb0\x85Ҹ\x02\x7fN\xb3\x02\xf9\r@$\xde\xc3ɀq\xee\xd9ɪ{#\xa4CsG\x10dw\vG[\x18\xa1\x9dg\xd7\x04s\xb0\x8e\xb9Ƃm\x8a\x12\x98\x85\xb7xX\xae\xe5\xbdQ;\x836\xa0\r\xf0\x8bU\xf2\x9e\xb9r\x05\x8b\xb0}\xa1Kf1\xae\x06\xb6o\xfcB\x9crGB\xd9:#\xe4.\x85ă\xa8\x11xc\xbc\xb8\x89\xfa\x02\xc1\x95\xc2ΰ;0K\x18\x1a\xe7\xc9N\xe3\xe2\xd7\t\xa2u\xac\xd6S\xa4\x06G\x03V\x9c9L\xe1t\xa7j]\xa1C\x0e\xf9\xd1aK\xc9V\x99\x9a\xb9\x15\b\xe9\xbe\xfb\xff\xd3\xec\x88\xfcZ\xf8\xa3o\x94\x1c\xf3\xe65\xcd\xc2`:`B\xb2ڡI2H9V}\n\"\x8e\x00\xbc\x1e\x9c\x0f\x98\x04\xb8\xc3\xf9\x8b\xa8\xacea\xb0Fy\x1dB\xa2?=\xc7f\bz\xb8\xaa\x8dPF\xb8\xe3\n^~\xf3T4\xc9>@m\xc1\x95\bQy6N\x19\xb6C\xf8A\x15A\xd1\x0e%\x9a\xa8hy\xd4\xfeR5\x15\x87\xbc\x15\f\x80u\xca$\x95Mc\xb1\b\xa7\"\xdc\x16\xecD\xe3\xc6w\xfe\x11\x06Q\x18dI\x83h\xdd\xe9\xc2\xef\x10J\xa6\xad\xe2\xd5\x0e\x9fd\x11C\x96Jű\xe3\x1f\xce\xd0\x12\x16\xb4Q\x05Z{\xc6P\t\xc6\b\x91\xb7\xfd\xc4E\x06\x95\xe8\xf7\xb4\xf84\xbaR\x8c\xa3\x01\xa7\xa0d\x92WHd0p\x86I\xbb\x8d*2\x17`{\xec\xe1\xa8Ǩ\xbc\x8f\v\xa7\xd0\t\xbb\xf6/\x83\xbb.J\xac\xd9*\xeeU\x1a\xe5\xab\xfb\xf5\x87\xffی\xa6I\x8d\x95F\xe3D\xeb\xef\xc3\x18\x84\xcd\xc1,\x8c\xc9\xfdo6Z\x03\xa0\v\xc2)\xe0\x14?\xd1z6\xc4@\x80<\xe2\x14\xd8#,\x18\xd4\x06-\x99\x96\xd7(\xb5\x05&A\xe5\xbf`\xe1\x16\x13\xd0\x1b4\x04\xa6\xb5\x85B\xc9=\x1a\a\x06\v\xb5\x93\xe2?\x1dlK\xbc\xa6K+\xe6\xd0:o\x8cF\xb2\n\xf6\xacj\xf0\x050\xc9'\x90kv\x04\x83t'4r\x00\xcf\x1f\xb0S<~T\x06AȭZA霶\xab\xe5r'\\\x9bL\x14\xaa\xae\x1b)\xdcq\xe9\xf3\x02\x917N\x19\xbb\xe4\xb8\xc7ji\xc5.c\xa6(\x85\xc3\xc25\x06\x97L\x8b\xcc\x13\"}B\xb1\xa8\xf9\x17&\xa6\x1fvt\xedL\xd0a\xf8\x1c\xe0\x19⡬\x80\x8c\x80EP\x81\xc4^\n4E\xac{\xf7\xb7\xcd\x03\xb4\x98\x04I\x05\xa1\xf4[g|i\xe5C\xdc\x14rK:O\xe7\xb6F\xd5\x1e&J\xae\x95\x90\xce\xff(*\x81ҁm\xf2Z8R\x83\x7f7h\x1d\x89n\n\xf6\xce'\\\x90\x93-\x91\a\xe0\xd3\rk\tw\xac\xc6\xea\x8eY\xfc\x93eER\xb1\x19\t\xe1I\xd2\x1a\xa6\x91\xd3́\xbd\x83\x856\t\x13\x86a\xc6;%:W\xaaB6\xe5%\x85\xbb\v4S\x00L\t\xcbG[W2\xd7\xe2F\x9bL#圷4\x94|\x968\xb4\xe2\x17\xf0\x8a720\xb8E\x83>\x1b\t\xbe_+\x1f!\x1c\x13\xb2\xf5i\xa1^\x01\xa7\x12\x98\xe5A\x89\x90\xc3\xd46\xe0\xac}\xc0\x99@\x99\xc4\xf8\xd5\xfd\xba\r\x86-\x13#\xee\xb3xw\x91?4\xb6\x02+\xee3\x87\xcbw'5\x97\xc6z\x1b\x90\xf0\x11\xc1)`\xa0\x05\x168\x8a\xc6 \xa4u\xc8x\x9c$'h0\xae\xbd\b\x9e\xfe$\x92\x10\x8a\xca\x18\xb5I&\xc0(\xf2\b\x0e\xff\xd8\xfc\xeb\xed\xf2\xef*\xd0\x01\xac\xa0\xd4\xcc\xd7z>\xdf~\xd1\xd5{\x1c\xad0ȩz\xc3Eͤآu\x8b\b\r\x8d\xfd\xe9۟\xd3\xfc\x03\xf8^\x19\xc0\x8f\x8c\xaa\xa6\x17 \x02ϻ`֪\x8d\xb0\x81\xf0\x0e\"\x1c\x84+=\xa2Z\xf1H\xe0\xc1\x93\xe0\xd8#Yr \xa1A\xa8\xc4c\xc2~¸\xf5\xd9\\\x8f\xe6\xafd=\xbf\xdd\xc2W\xc1y\xdd\xd2\xcfۀF\x97\xb6\f\r\xacG'X\x99\x11\xbb\x1d\xf6y\xffLY(\xccR\x80\xfa\x1a\x94!Z\xa5\x1a\x80\xf0\x80IN!> \x9f\xa1\xf7ӷ?\xdf\xc2Wc\x1e\x9c\xb8JH\x8e\x1f\xe1[\xf2>\x9e7Z\xf1\xaf\x17\xf0\xe0\xf5\xe0(\x1d\xfbH7\x15\xa5\xb2(A\xc9\xea\x18\x12\xe0=\x82U5\xc2\x01\xab*\v\t\"\x87\x03;\x82ڞ\xb8\xa7\x15\x11\xa9&\x03͌;\x9b$F>\x9c7\x9ay\xd6Ԏ\xa7ًϢ\x9ed\xbd\x9f-\x03y\"'|\xb9\xf0\t\x9c\x18\x96^Wp\xe2\xb1\xc9\xd1Ht\xe8\x99\xc1Ua\x89\x0f\x05jg\x97j\x8ff/\xf0\xb0<(\xf3(\xe4.#ê\xd4\xed\xd2\xf7\xc1\x96_\xf8?\xd7\x12\xee\xdbT\x9fJ\xbd\a\xf2\xf9X@\xb7\xdb\xe55\x1ch\xb3\xfb\xa7Ǯ\x93|\xd8Ąs\n\x93l\xfeP\x8a\xa2lk\xbd\x81\xb7\xad\x19\x0f\xee\x98\xc9\xe3g\xb2\x1d\xe2sc\b\xa3c\x16\x9b\xb8\x19\x93\x9c\xfe\xb7\xc2:\x9a\xbf\x86\xb1\x8d\xf8$\xe7\xf2~\xfd\xe6sZT#\xae\xf1$'j\x980>f=VY\xcdt\x16v3\xa7jQLvS\x0e\xbf\xe6$\xa4\xad@s!\xfd{7\xda\xdc&\xa8\x89j\xa0\xdb\xf3\xac\xfcӱ]\"\xe1\x1bv\xb1ϥ\x85g\xf9uY\x15\x1e\xd8\xce\x023\b\fj\xa6I#\x1e\U0005814cC3A\xe9\x02e\x04]c\x10\x98\xd6\x15\xc5\xf4\x90E$ \xc6\xfc7\xb2\x87YO\xdf)\x86$E\xd9v\xa56蜐\x9f\x919\xef'\x88\xfc\xbe\x8c\xeazv\x85\x92[\xb1\x8b\xdd\xce9\xa7dSU,\xafp\x05\xce4\xa7j\xae\xb3\x8c|\xa0-\xe7\xe9\x7f?\xd8\xdaj\xf8\x85\x06c\x9a\xaaQ\xdbqN\fʦ\x9e\xa3\x92\xc1\xa3҂%\xe6\rZ7\xb3^Z\xb8\xbd}\x8e\x8d\x05\xa5\xbc\xa6\xe4\x0eep\xaa*\x8d\x8a\x1e\x13\xf8\xb62u\xaa\xaf\xf2\x92B\x7f\x86o\xa0\xea\x9eʑ1\xdeY\xba]2\xd93\xe8.\xb7SZ\xf1\xc9\xcc\xd8\rN\x16\x03}O\xea!\xf9\x86\xf63\xbaH\xe1\x91-\xf24\x04G\xd7>\xbdQ\xda}m\x1f\x89\n;\xed\x90w\x8d\xfek$\xfej\n\xc4\xf7~\r\x8fF!j\xecJ\xff\xb1\xaf\v\xc5]\x8e\xa0\rj\x96\xec\n\x81\xef\xdc[\xdf\xc2\xfc\xd2\x06`\xc2Bc\x91\xfb\x0e\xda\xec\xee\x19\x84\xf6\x9d\x893\x87\x19\x9d\xbf\xce_\xa4\x1bS\xe1\xcdo\xf8RrU\x97j\x0ef\xceB\xd6r\xcd?ᴏ\x8d)\x8e\xf5\xe0:~\x05h\xc8}\x15JE\xf2\x96\x89\n9\xb4o\xdcτ\x92\xe3\x96R\x9c\xe0\xe3\xda>ND\xeft\xfdw^\x92\t&\xcc\x13\x9e?R\x98ӧ\xc6\v\x92\\O\xb6C\xa9\xaa(/\xd9\xd49\x1a2L\xff\xe0\t\x12\x0fT\xf7\x17%\x93\xbb\xa4\x93k\x1f\xec\x10*f\x1d\xe4\xfd'\x03)\xe2\x87/\xa6Sʆ/\x9c\xfd\xa8\xd1Z\xb6\xbb\xe4\xce\x7f\f\xbbB\xe7.\x1e\x01\x96\xabƥ\xed\xf7K\x1b]\xd0\xf3\xba\x87ɦ\xd8\xd8\xfb1W\xb6\xcen\xdbT\x95?3\x8c\x1b\xfd\xa7\x1d\x1e\xab\x1c\xd3\x19\xff\x99\xd6\xe19\x04Kf/\xb1\xea\x9e\xf6\xa4\xfcq\x17\xec\xce:d8\x13\xd8\xdf\xe2!1\xdb\xfa\xb9\xc4\xd2}t\x9e\x89\xa5ٗ\x18\xc3\xc5\xd0\x1bOq\xae]K\xc2\xec\xbesH\xac}\xef\xbdʳ\x98\x1d\xf1\xbb\xc6mv\xbd\xf5\xde\xf2\xfcg\v3\xfb\x1b\xe7\x1fL\xf2\xa1\xd8RM\x88\xfe|\xabA\x01Rl\xa4\xc5'\x01ﺜ\x02.\xac\xaeر\xa3ŗ~d\xaa\xe9\xf7\x91ޢZ\x8f\xa9\xf1T*{\xbe\xc3\xdd}-\x92\xaek\xcf\xfb\v\xb8\xe03\xfc\xba:\xed\f\x7f\x8f\x1bΤ\xe2V2mK\xe5\xd6o.\xa8Ʀ\xdb\xd8\xdac_V\xfa\xc0\xe2\x9f\xde⦨\n\tT{\xef\xf6,g1\xfex\xe8\x1a-ތ \\\x88\xfb\xf1[\xa6Ttݐ\x17 \a\xe4\x1fv\xef\xa6_p\xbc\xe8\x82\fs\xb1A\x1e\xe2Q\xaa\xab\xa0\xa4\xaf#\x94\x99\xbf\xb2\xc3\xc5@>&\xe8ό\xe1Iu\x9aMz\xcc\xf9\x00v|\xd3\x1c\xce4y\xf7ܿ\x82_\x7f\xbb\xf9_\x00\x00\x00\xff\xff\xfc¬w\xb0(\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z͒\x1b\xb7\x11\xbe\xefSt\xad\x0f\xb6\xab4d\xa4$\xae\x14o\xd2*Nmbo\xb6DI\x17\x97\x0f\xe0\xa09\x03s\x06\x80\x01\f\xb9\xb4\xe3wO5\x80\x19\xce\x0fH.\xa9\x925\x17i\xf1\xd3\xf8\xf0u\xa3\xbb\xd1`\x96e7L\x8b\x8fh\xacPr\x01L\v|r(\xe9/;\xdb\xfc\xc3΄\x9ao_\xdel\x84\xe4\v\xb8k\xacS\xf5;\xb4\xaa19\xbeŵ\x90\xc2\t%ojt\x8c3\xc7\x167\x00LJ\xe5\x185[\xfa\x13 W\xd2\x19UUh\xb2\x02\xe5lӬpՈ\x8a\xa3\xf1\xc2ۥ\xb7\x7f\x99\xbd\xfcn\xf6\xf7\x1b\x00\xc9j\\\x80V|\xab\xaa\xa6F\x83\xd6)\x83v\xb6\xc5\n\x8d\x9a\tuc5\xe6$\xbc0\xaa\xd1\v8t\x84\xc9q\xe1\x00\xfaQ\xf1\x8f^λ \xc7wUº\xff$\xbb\x7f\x10\xd6\xf9!\xbaj\f\xab\x128|\xaf-\x95q\x0f\x87\xb52\xd0[\x13z\x84,\x9a\x8a\x99\xe9\xcc\x1b\x00\x9b+\x8d\v\xf0\x135ˑ\xdf\x00D\x06\xbc\xa0\f\x18\xe7\x9eSV=\x1a!\x1d\x9a;\x12!\xbbe8\xda\xdc\b\xed\xd3%\xb3\x18{\x03\xf9K\xdf\x11\x9bܞ0[g\x84,R(ދ\x1a\x817\xc6+\x9d\xf6\x9f#\xb8R\xd8)\xbc\x1d\xb3\x04\xd18\xbf\xf14\x18\xdfO\"\xadc\xb5\x1e\xa3\xeaM\r\xb08s\x98\x02u\xa7j]\xa1C\x0e\xab\xbd\xc3v+kej\xe6\x16 \xa4\xfb\xeeo\xc7\xf9\x88\x84\xcd\xfcԷJ\x0e\xc9yC\xad\xd0k\x0eHH[\x05\x9a$Cʱ\xeaS\x808\x12\xf0\xa67? \tr\xfb\xedg\xa1\x90\xe9\x81Z\x83+\x11ް|\xd3hX:eX\x81\xf0\x83ʃ\nw%\x1a\xf4#Va\x04\x9dm\x10\xa4;e\x92\xaaӘ\xcf\xc2\xd8(\xac\x955\xd2\xdfp\xa1\xcfb_\xb9A\x96\xb4\xaf\xd6I\xcd\xfc\b\xa1d\xda\xc8^\x17\xf8,\x03\xeb\x13)\x15\xc7\x1ek\x13\\\u00826*GkO\x18>\t\x19 y84\x9c\xa5\xa8D?\xa6\x05\xd4\xe8J1\x8e\x06\x9c\x82\x92I^aС3L\xdau\xb4\x8c\xa9\n\xdbi\xef\xf7z\b\xe5C+\xaf\xd73\xc1\x14\x86n_\x067\x98\x97X\xb3E\x1c\xab4\xca\u05cf\xf7\x1f\xff\xba\x1c4\x03Ѣ\xd18\xd1\xfa\xd1\xf0\xf5BR\xaf\x15\x86{\xfe_6\xe8\x03\xa0\x05\xc2,\xe0\x14\x9b\xd0z.\xa2\x7fE\x1e1\x05\x8e\x84\x05\x83ڠE\x19\xa2\x1553\tj\xf5\v\xe6n6\x12\xbdDCb\xc8\xed7\x15\xa7\x90\xb6E\xe3\xc0`\xae\n)~\xebd[\"\x9c\x16\xad\x98C\xeb\xfcA4\x92U\xb0eU\x83/\x80I>\x92\\\xb3=\x18\xa45\xa1\x91=y~\x82\x1d\xe3\xf8\xd1[\x93\\\xab\x05\x94\xcei\xbb\x98\xcf\v\xe1\xda@\x9d\xab\xban\xa4p\xfb\xb9\x8f\xb9b\xd58e\xec\x9c\xe3\x16\xab\xb9\x15E\xc6L^\n\x87\xb9k\fΙ\x16\x99߈\xf4\xc1zV\xf3\xafL\f\xedv\xb0\xecD\xd1\xe1\xf3\xe1\xf5\x02\xf5P\xbc\xa5\x93\xc0\xa2\xa8\xb0Ń\x16\xa8\x89\xa8{\xf7\xcf\xe5{h\x91\x04M\x05\xa5\x1c\x86Nxi\xf5Cl\n\xb9&çyk\xa3j/\x13%\xd7JH\xe7\xff\xc8+\x81ҁmV\xb5pd\x06\xbf6h\x1d\xa9n,\xf6\xce'3\xb0\xa2\x03E~\x80\x8f\a\xdcK\xb8c5Vw\xcc⟬+Ҋ\xcdH\t\xcf\xd2V?E\x1b\x0f\x0e\xf4\xf6:\xda\xfc\xea\x88j\xc7\xfem\xa91'\xcd\x12\xb94U\xacE\x8c$ke\x80M\xc6\x0f\x99J\xbb\x00\xfa\x92\x11e<\xe8\x9c\xd9\xd1\xf7&%\xa8E,{\x8e<\xc6;\x1b\x03U5\fT\xfdo\x12#\rje\x85Sf\x7f\x88\x94c\x938\xaa\x1d\xfar&s\xac\xae\xd9ޝ\x9f\tBr\xe2\x1d;\x93&g\x14\xa4z\xa0J\x16\x8a\x0e\xd9D\x1dp\xefh\x1cٹE\x97ެ<\x1aل\x84C\x8e\t\xfd\\r\xbc\xed\x95R\x15\xb21\x9bZ\xf13\x9b~T\xd1q\x18\\\xa3A\x1f\xff\x83\x9b\xd5\xca;cDŽl\xddGH\xb9\xc1\xa9\xc4>V\xe4n\x8e\xa9\xe6\xb8\x1d\u0089\x90\x94\x04\xfc\xfa\xf1\xbe\r;\xadeE\xe8\x93\xc8\xd2\xe7'i\x16\xf4\xad\x05V\xdc\a\xea\xf3k'-\x84\xbe\xfbu\x00\xe1}\xafS\xc0@\v\xccq\x10\xf7@H\xeb\x90\xf1\xd8H\xee\xc6`\xec{\x11|\xeaQ\x90\x10\xaeE1>\x92J\x80\x91\x8f\x17\x1c\xfe\xbd\xfc\xef\xc3\xfc_*\xec\x03XN\x99\x90\xbf\xab`\x8dҽ\xe8\xee+\x1c\xad0\xc8\xe9\xf6\x81\xb3\x9aI\xb1F\xebfQ\x1a\x1a\xfbӫ\x9f\xd3\xfc\x01|\xaf\f\xe0\x13\xa3\xa4\xff\x05\x88\xc0y\x176Z\xab\x116l\xbc\x93\b;\xe1J\x0fT+\x1e7\xb8\xf3[plC'&l\xa1A\xa8\xc4\x06\xd3\xec\x03\xdc\xfa\xe4\xe9\x00\xf3wr)\x7f\xdc\xc27\xc1I\xdcҟ\xb7\x01F\x97 \xf4\xbd\xce\x01\x8e+\x99\x03gDQ\xe0!ў\x18\v\x054\n\x05߂2\xb4W\xa9z\"\xbc`\xd2Sp\xc4\xc8'\xf0~z\xf5\xf3-|3\xe4\xe0\xc8RBr|\x82Wt\xc6=7Z\xf1og\xf0\xde\xdb\xc1^:\xf6D+奲(A\xc9j\x1f\xf2\xcd-\x82U5\xc2\x0e\xab*\v\xa9\x18\x87\x1dۃZ\x1fY\xa7U\x11\x99&\x03͌;\x99\x8eE\x1eN\x1f\x9ai~\xd2~\xcf;/>_y\xd6\xe9\xfdb\xb1\xfe\x99L\xf8\xc4\xfc\x13\x98\xe8_u\xae`bӬ\xd0Ht\xe8\xc9\xe0*\xb7\xc4C\x8e\xdaٹڢ\xd9\n\xdc\xcdw\xcal\x84,22\xc6,h\xdd\xce}1g\xfe\x95\xff\xe7ڍ\xfb:˧\xee\xde\v\xf9r\x14\xd0\xeav~\r\x03m\x1e\xfd\xfc\xd8u\x94\x87e\xcc\xec\xc62\xe9\xcc\xefJ\x91\x97\xed\xad\xaa\xe7mkƃ;fr\xff\x85\xce\x0e\xf1\xdc\x18B\xb4\xcfb)2c\x92\xd3\xff\xad\xb0\x8eگ!\xb6\x11\x9f\xe4\\>ܿ\xfd\x92'\xaa\x11\xd7x\x92#\xb7\x85\xf0=e\aTY\xcdt\x16F3\xa7j\x91\x8fFS\xae|\xcfIIk\x81\xe6L\xf6\xf7n0\xb8\xcd\xda\x13Yw7梴\xdbJ\xa6m\xa9\xdc\xfd\xdb38\x96\xdd\xc0\x16\xc3A\x871\xe9leё8\x99k>\x03\xcfR\xfc\x96p[ID4\xb4\xc5T\xa9B\xe4\xac\x02\xeb\xdbd,VF\x98\xad\xec)\xa0T=r\f\xb7_U\xec\xe1\xf5\xbe\xe0ḧ\x1dB\x1e\x8enQ+#\n!Yu\xf0\xd8\xfe\xea(Y\xcd\xfc_\t[\xad\x99\xd6B\x16\x17q\xdbַ\x96蜐E\"\xd1\xef\x97\xdfO]\aN\x9e\x93\xf3.\xe0\xc3\b\b0\x83\xc0hO\xa4\xaa\r\uecd0uj&(e\xa4\xac0\xa6\xd6+\x04\xa6uEy]\xc8$S\xbe\xa9\xad\xd6\xe5J\xaeE\x11+\xa7S\xa6dSUlU\xe1\x02\x9ci\x8e]ڒǽ_(<\xa3\xf1\x0f\xbd\xa1\xad\xbaϔ*ӻ\x1a\x140\xa7\x9bA\xd9\xd4S(\x19l\x94\x16,\xd1N\x87s☨\xe3\xf6\xf6\x12\x93\n'\xff\f\a\xe1Μ*8D\xc7\x11\xaf!\xf1\x8a\x1d\xdcG:\x9a_\xeaP\f\xfe\xdaНj\x880K\xd7VFc\xb4\xe27c\xd2\xfa\xbex\xd4y\xf0\xa4\xe3\x8e\xe1\xa1\x1f\xf5\x06\n\x9eU\x96\xf2\x85\xf2K\nS\xe19,\xf2\x1e\xd2\x00\xd7>\x92\xd1\x05\xe3\xea\xd2\x14\xdda\xb5C\u07bd!\\S\xb7y=\x16\xe2\vʆ\xc7C\"j\xec\x8a\x1c\xd1N̡\xec\x12B\x8c6\xa8Y\xd2\"\xc0?\nX_\x18\xfd\xda\x06i\xc2Bc\x91{\xdf:Y\xfchL\xe0\xccaF\xf3\xafs \xe9bWx\x9e\xeb\xbf\xc2\\U\xf9\x9a\x8a\x99r\xc8:\xda\xfc\xfbP\xfb0\x98\xa2\xec \xaf#,\x88C\xee\xafܠ$\xac\x99\xa8\x90C\xf7,}1\xf3\t\xd0\xd3d\xecs\x92_\xa3\xb5\xac8\xe7\xb4~\f\xa3B\xe5-N\x01\xb6R\x8d;b\x95_\xdbx\xb4.\x8a\xc9R\xf1sH\x1e\x14\xf70\xe4\xf1'\xb7)\x9a\x84Z\xfa\xcfp\x17a\xf4E\xcdsEJ\x1a\x93r5\x1d\xe4Ӿ\x06Nİ\a\xdc%Z\xdb\x13\x9c\xe8z\x8cn!\xd15\xf9=@\xbf3T\x92S9Mۗ\x94\xd9=\xb6'\xfa\xbe\xf7\xc7\xe5\"\xb6#\xbek\x1cBW\x87.U\xd5\xfa\x00\xffH.\x9bz\x85\x86T\xb1Je\xc4\xc0$\xefk.UL\xe8$\xb4a8\x88\x8a\xf5\xb0X@\xf7\xa7\xdc)\xe0\xc2\xea\x8a\xed\xbb\xcd\xf8\x1b\x1c\x1d\xe9\xf4s\xc2\xe1\\\xb5\xbe\x8a\"ϑ\xbc\xedt\xa5\xba\xfb\xd1B\xfa~z:Ӈ3پ\xef\xef~\x8c\xf0yV8\x91w\x0e\x7f\x1cr\x8d\x81,\a\x12\xce\x05\x8b\xf8c\x95\xcb}\xfcp\x99?ӽ'ٛ4z\xe4\xbc';>y\xf5[\x9aU\xf7\x1e\xbc\x80\xdf\xff\xb8\xf9\x7f\x00\x00\x00\xff\xffϡa\xa5-&\x00\x00"), []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?\xbfp\x10jQ\u0605\x90\xdd~\x82>Ǟ\x8e#\x88\xa3\xafc\xba\xb9U\x1f\x8d\x9b\x90ϢO\x04\xe6\x88\xf1\xacT\x1e&\xfaF\x14\xc0\xcc\xceX(\x83\xaaoåN\f8,\xe4`\x15\x85\ac\x90\xde~P\xe3\x04\x91uQ\xf0u\x01\x97d\x19\x0f\xd0l\\I\x8f\x11\xed+\x18+\xb2S\x92\xccA\x1c!\x98\xf6?\xf4(C\xf1\x16\x7f\x00\xc6#\xe0==1\xb8+\x8a\x0e\xd1\xfbԊ\xe2Vi\xc8\xd0\xf1\xbf\xf4\x01\x85\x80\x82\x82\x18\xa9X\xa1\xe4=h\x87Ec\xe0\xd1\xc0\x00\nh\xce\xd0W\xd7h\x96\x85d\x9b\x1a\xdd\xf8%C-\x11\x95\x11!\x8d\x05\x1e\x11\xe6\x13\xf0\x0e\xbe\xa1E\x80\xfc\xaa\xa8\x8d\x05}\x93\xa9\n\xf2\xb0j7j\xceRy\xf8\xe1 d\x1f\xf4\x15\"\x03\xe4C\xe6*-he,&\xdam\xfc\x87\xe6\x91\x16\xf1\x90\xd5~\bm`7\xa9[\fXlx\xfe\xfb\xf3\v\x92\x80~\xef\xfd~\f\xe3\x1a\x1a2\xcd\xd2\xcd\xe4&\x8d\xb7\x10\x16\xca\bu'u\xd4\f\xbes\xad\xf9\xee\x00כ\x15\xc8\x17\xe0{\f\xf6\x80\xf32T\xfbN\xbc\x1f\xf6\xff\xaf\xc8\xfd\xd3\xf2\xdb\xd0*>\x17\x12\xf9\\\bc{l6n\xe9\x0f\xc9:\x16w{\x02I\a\x13\xd5\xe4\x14W\xffA\x88yҹ\x13\x9b,\x8dl\xfa\t\xf0OEɭR\x0f)\xd4\xfb\x1f\xac\u05ee\xfb\xb1\x8cv\x9a\xd8\x1a\xb6\xfcQ(m\x86k\xcb\xf0\r\xb2\xdaF5\v\xb7,\x17\x9b\rh\x84E{#\xcdV\xca!b\x1d\x8e\xf9XGeE+\f\xc6\xd52\x1dYJԈ\r\x85\xa2\xfa(T\xe7\xe0`hA\x0eD.\x1eE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7T\xa8$\xa0\x8f_bl\xb4_5N\x89\xb0\xfer\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccr\v3\x05_C\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fm\xb5\x83\x99\x00\xcb(\xc4݊l\xeb\xdcW\x144\x82\xc5r\x05\x86\x96\x92xU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8\xe6\xc8(\xbe\xffJD\x0fV\xe7Ha\x9f\xd0$\x8c6Y\x92\xe7C\x94\xf4Hq\x01f\xd9Y\xd2\x146|Mah\xcf\x7f\xdc\xdb\x7f\xda#ʯ\x8bw\xc7M\x98\x19\xac\x9b\x9cS/˸\xa6\x9b\x7f\x12\xbe\x91ɺ\xf1\x16k\x16\xcf>u[^\xd0V\x8agH~\xc16\xa2\xb0@N\xd5\x14\xa2l\x06\xe7NI\xa0T\v\xcchg\xddf\xdb\x0f͆[B\x8b\x01\xad\x86\x00\x9c\x83\x1e\xa2\x1c\xe2A\x02Hָ\x16\xb4\xd3,\xdcJ\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\xffp\xc18{\x80\x9ds\xb1\xb8d\xc87\x1e*'\xa2\xa0\x81\xd2(HS<\xc0\x8e@\x8d\xe7E\x8c\x979\xd2\xe2\xca\x03\x8cl\x95\xc6J\x8f\xae\x88\x9f߀rt\xc3\x0fD\x98\x94\xd9Ԗ\x86\xa8~\xfa\x8cd%\xc4\xcb\f\xbd\x14J\xe0ˑ\xc3N\x16\xa7n_\xfdL\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\xfb\x87\x03t\x96\x04\x89\xd8\u0378q\xb6\x14\x05\xb5\xe1\x870l%1$s$\x9a\xd1\x1d%X\xb9.]gemh\x7fZ*\xb9p\xcbbc\xbdy\x1e(\xddc\xc1I:\xf6\x9dޢ1r\xbf\xb8T\xaf\x82g\x90\x87-:\xcaA\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\xa2\xceXY\xe0\\O\xac\x19\xa4%\xa9z$\x8d\xe9p\xf5Tb=\x93L\xe4E\x90ە$\x05\xddl\xe0y\xd6k\xa6\xdc\x1c\xa3b:cq.@\xc9ik\xedoh\xe9i6\xfe\x9dU\\h\xb3d\xef(\x1d\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\xa3\x9c\xd4mWa\x9d\xaf\xe4\xb9\xf3e\xf6\x14O\xe3\xf8(Y\xec\xd89\xfdv\xfe\\\xf7n\x86DϨ\xda\x13\xe5\x92W\xe9\x92L\xc9\xc6s\x02\r\fփC\x84\x8d\x9b\xac[\f\x10\xa6(\x90,ʕ2\x91d\x91\bZ\t\x82~\xad\x8cu\xeb\x90=\x7f\x7ft\xa1R\x85\xc5I\xc67\x1643V\xe9\x90NJ\x8a?e)\xbe[n\xb7`\xc0\xefC\xf9EO\a\x18\xa3\xd8\xf3V78\xabr\xee\xf6¨#\x9e\x91\xf7Dm+\xad20Ѽ\x88\xb6$ڦ\x91\xac\xa5.\x1d\x9au]\ue8bfM\x92\xd6NY\x94\x0ee\x9e#\x8f\xa4;\"2\xfa\xf0\xad\xb3D\x8d\xda\x05\xffN\x91\xd6cpdt\x00\xa6,\xf90\x87:\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\x90:\xe9\xb2\xcd\xe7\xc4ہ\xa4[\xfe\b>\xdd\x17d\xa6jIKa\xa8\a\xb0\x9b\x19\x10\x1dk\x9c\x15H\xb4w\x9d\xc6\xd1\f̱\xb2 I\x12rrݬ\xdb\xe4#\x17i\xebV\xec8\xb6\xdaC9\x9cc\xe5\xf8y\x14\x12<\xbbg\x10J\xfeM\x94u\xc9x\x89<$\xb7C\x94\xd0\x1cCp\xecn\xd2>\xb1\x05\x19-\xabp\x96U\x05X\xf0i\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\xb4c.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;z\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*\x9fbK76D\x80\xd1%\x1e<\xa7\x9b\x1b:\az\xbc\x1e\b\xf7K\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~3d(\xd3\x0e@\xf1=\x84\xf3\xb9dR\xba\xe7\x9a?+\xee\xfc2\x80\x85\xc2\x12\xdc\xd4W\x8c\x03ʺ\xb0\xa2*\xdaK\xecb\x01\xe7\x16v\xcdeE?+:\"\xefo\xea\xfa\xf2\xb5\x91\xf8\xe5 \xaa\xe1\x86=AQ0\x1e\x9b\x9b{T\xc8\xdc婙Z\x00\xdaF\x9c\xe5\xfe2&\x7f\xe3ꅛ.t\x1b\x00Y\xd82\xb6\xd4\xc7\xe5\u16fe\x0e\x1a\xb0T=\xb6登x\x83\xbe\xfdR\x83\xde1\xbaw\xac\xf1\xcd\xdaC\xa5~\xa2\x1b\fL\x83\xfa\xf1\xea\xf0О\xc9^\x80Ӫ\a\xf6N:\x8f`\x88\x13\xb5A\xbd\xd3\x06t\xa8Te\xecr>\x16&\xe8>\b\xa9\x1a\b\x91\xa6)\xce\xff\x9cS\x96/\x11ޝ\"\xc0K\xf2\x80\xe6y\xaf\xdf\xf1\xf4䱧&ӓQ\x92NI\xbeD\xb87'\xe0\x9b實\x9f\x82\x9c\xbf\xf1\xfc§\x1e_\xea\xb4\xe3\f\ua95en\x9cO\xbbW:\xcd\xf8\xea\xa7\x18_\xf3\xf4\xe2\xacS\x8b\xc9\xe9Y\xb32\x0e\xe6\xa4V=\xe3\xb8]Z.\xc1\xf4)\xc4\xc4Ӈ\x89\x99\x06i\x83?r؉\xa7\v\xe7\x9f*L\xe4\xef\x9c)\xfdʧ\a_\xf9\xd4\xe0\xf78-\x98 \x81\tU\xe6\x9f\n|\xf6\x96\x94\xd29\xe8\xc9m\xbf9R;)\xaf\xa9\xb1\\\x1f\xb1\xc1\xbeV\xb8M\x16k\xf5b\x002K\xfe\xf5\x03z\xe9\xe2\xd068Jf\xc7#\xea\xedK\xb6\xeeZ\xdf!\xf6O`\xb8\xadK\x03\x15G\x03@\x81\x1b\xa5fE]\x85\x0f<\xdb\x0ez\xd8r\xc36J\x97ܲ\xf3f\xb3\xf8\x8d\xeb\x00\xff>_2\xf6Q5\xb9:\xdd\xfbҌ(\xabb\x87\x91\x18;\xef6x\x9e\x94D\xa53\xf4|\xad\n\x91E|\xce\xd1{\xf5\\\x83\xbdˆ\xe8濬\x93-\x12\v|\xb0\xb9\b\xb7.\xf6\xafdv\x97\xe0\x1f\xb9V\xc2+\xf1'z\xa3\xea\x04\xabn\xef\xaeW\x04+\x88\x11=~\xd5$(6,_\x03\xba\f\xed\xd8\x0f\xe9\x93զ\a\xb5\x9f#\xdc}\xe1\x03r\xf7\x9cKp[\xbcj\xce\x14j\xad\xeb\x95\xc3\xe5PO(_\\\xee\x98\xf2\xefu\b\x9d/*\xae\xed\xce%\x13]\xf4\xf0\bv}j\xd5젵\xda\x7f\xae\xa6[zd\x0f/\xd5\xd0N\xf6\xae\xea'\x0f\f\xe9\xf9\x1c\x9c\x0e\x9f\xaa\x9els*2Z\xa5\xf9=|R\xeeA\xa2\x141\xe9\xb7\xe8=W\xe5=\xb7\x90\xaf\xed'aL\xd1\xfb\xb1\r\x01\xb6\xe73\xf6.\xfaGl\x8f|\xca\xc0\xda\xe292r{\xfbɍ\x94ށy\xef\x9ftA}l\x00Y\x10(࠭\xf1\xbf[\xf5D\x17\xe0\xc7טë+\x9d\x87߀\x0e\x8aP\n\xefQì\xabB\xf1\x1c\xf4\x15\xbd<\x930\xe2\x9fz\r\x06\xee@\xff\xfd\x1ao7#\xe3\t=\xbf`\x96\fztE\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{\xa3\x81\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}\xec\xbd/\x13tB\x8az\xbf\x1bo\xd9\t\x11:ډ|\xfa\xb8\x12\x8f\xc1\xe2ƨLPTA\x0f\n\xd1I\xa4\x97\xbbC\xfcP\x80x@:j\x03_\x9e$\xe8\xaf\xc1\x02\x99\x95\x8c\xbd\xdb2\xad\xfd~ڃ\x16}\xaf\xc5*\xec{\x04\xc6\x00\x00Sa\x9f˸\x97\x80\xc2\xf6\x9a0͋p\xfb\xf4\x9cP!qK7\xee\xb0-\xc6\x1f\xb0Z4\x0fm\x9d%\x90۽\x7f\xd4\a<\xfe\x0e\xa0{()㕭uЮ\xb5\xa6[\xd6\x11\b\xb8Kȏ{\t\xb0} \xee\x18\x06\xb7/\xb4\xb5\xfb\x0f\x93oȎ\xc0i\xde\xf2\x8b>\f\xe6\"j\xf7\xc6\xeb\x02\xe1\x1f\xc7\xe3\xd1\x19\x838߸\a\xdf&\x88\xf0\xa9\xad96\xe0f\x188d\xff\x84ܫ\x8e\x84.ݟ\x18\xc35\xd6iN\xb9z9\xa2\x86\xe1\xb2\xfe\x9b\x18\x13ƏB.\xd8g؏\xd8\x17\xec\x83\xc4A\xec\x13\xc0\x9dw\x84\x9c\xb6VH;\xce\x19\xe2cӊ\x0e\x9b\x8eh\xc8i\xb1\xbd\x1b\xc0\x18d\xb2ӣOM\x15w\xda\u0530ߊ1o\x94v\xcc2\x1c\xe8\xef\xf6~\x8dj\xf0\x83\xda;\xa6\xb9G\xd5\xc8\xdeGz=0\xefH\x8e\xf7һ_\xeau\xfb\xa0\x02\xfb\xdb\xdf\xcf\xfe?\x00\x00\xff\xff0\xe5e\x05\x8f|\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?\xbfp\x10jQ\u0605\x90\xdd~\x82>Ǟ\x8e#\x88\xa3\xafc\xba\xb9U\x1f\x8d\x9b\x90ϢO\x04\xe6\x88\xf1\xacT\x1e&\xfaF\x14\xc0\xcc\xceX(\x83\xaaoåN\f8,\xe4`\x15\x85\ac\x90\xde~P\xe3\x04\x91uQ\xf0u\x01\x97d\x19\x0f\xd0l\\I\x8f\x11\xed+\x18+\xb2S\x92\xccA\x1c!\x98\xf6?\xf4(C\xf1\x16\x7f\x00\xc6#\xe0==1\xb8+\x8a\x0e\xd1\xfbԊ\xe2Vi\xc8\xd0\xf1\xbf\xf4\x01\x85\x80\x82\x82\x18\xa9X\xa1\xe4=h\x87Ec\xe0\xd1\xc0\x00\nh\xce\xd0W\xd7h\x96\x85d\x9b\x1a\xdd\xf8%C-\x11\x95\x11!\x8d\x05\x1e\x11\xe6\x13\xf0\x0e\xbe\xa1E\x80\xfc\xaa\xa8\x8d\x05}\x93\xa9\n\xf2\xb0j7j\xceRy\xf8\xe1 d\x1f\xf4\x15\"\x03\xe4C\xe6*-he,&\xdam\xfc\x87\xe6\x91\x16\xf1\x90\xd5~\bm`7\xa9[\fXlx\xfe\xfb\xf3\v\x92\x80~\xef\xfd~\f\xe3\x1a\x1a2\xcd\xd2\xcd\xe4&\x8d\xb7\x10\x16\xca\bu'u\xd4\f\xbes\xad\xf9\xee\x00כ\x15\xc8\x17\xe0{\f\xf6\x80\xf32T\xfbN\xbc\x1f\xf6\xff\xaf\xc8\xfd\xd3\xf2\xdb\xd0*>\x17\x12\xf9\\\bc{l6n\xe9\x0f\xc9:\x16w{\x02I\a\x13\xd5\xe4\x14W\xffA\x88yҹ\x13\x9b,\x8dl\xfa\t\xf0OEɭR\x0f)\xd4\xfb\x1f\xac\u05ee\xfb\xb1\x8cv\x9a\xd8\x1a\xb6\xfcQ(m\x86k\xcb\xf0\r\xb2\xdaF5\v\xb7,\x17\x9b\rh\x84E{#\xcdV\xca!b\x1d\x8e\xf9XGeE+\f\xc6\xd52\x1dYJԈ\r\x85\xa2\xfa(T\xe7\xe0`hA\x0eD.\x1eE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7T\xa8$\xa0\x8f_bl\xb4_5N\x89\xb0\xfer\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccr\v3\x05_C\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fm\xb5\x83\x99\x00\xcb(\xc4݊l\xeb\xdcW\x144\x82\xc5r\x05\x86\x96\x92xU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8\xe6\xc8(\xbe\xffJD\x0fV\xe7Ha\x9f\xd0$\x8c6Y\x92\xe7C\x94\xf4Hq\x01f\xd9Y\xd2\x146|Mah\xcf\x7f\xdc\xdb\x7f\xda#ʯ\x8bw\xc7M\x98\x19\xac\x9b\x9cS/˸\xa6\x9b\x7f\x12\xbe\x91ɺ\xf1\x16k\x16\xcf>u[^\xd0V\x8agH~\xc16\xa2\xb0@N\xd5\x14\xa2l\x06\xe7NI\xa0T\v\xcchg\xddf\xdb\x0f͆[B\x8b\x01\xad\x86\x00\x9c\x83\x1e\xa2\x1c\xe2A\x02Hָ\x16\xb4\xd3,\xdcJ\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\xffp\xc18{\x80\x9ds\xb1\xb8d\xc87\x1e*'\xa2\xa0\x81\xd2(HS<\xc0\x8e@\x8d\xe7E\x8c\x979\xd2\xe2\xca\x03\x8cl\x95\xc6J\x8f\xae\x88\x9f߀rt\xc3\x0fD\x98\x94\xd9Ԗ\x86\xa8~\xfa\x8cd%\xc4\xcb\f\xbd\x14J\xe0ˑ\xc3N\x16\xa7n_\xfdL\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\xfb\x87\x03t\x96\x04\x89\xd8\u0378q\xb6\x14\x05\xb5\xe1\x870l%1$s$\x9a\xd1\x1d%X\xb9.]gemh\x7fZ*\xb9p\xcbbc\xbdy\x1e(\xddc\xc1I:\xf6\x9dޢ1r\xbf\xb8T\xaf\x82g\x90\x87-:\xcaA\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\xa2\xceXY\xe0\\O\xac\x19\xa4%\xa9z$\x8d\xe9p\xf5Tb=\x93L\xe4E\x90ە$\x05\xddl\xe0y\xd6k\xa6\xdc\x1c\xa3b:cq.@\xc9ik\xedoh\xe9i6\xfe\x9dU\\h\xb3d\xef(\x1d\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\xa3\x9c\xd4mWa\x9d\xaf\xe4\xb9\xf3e\xf6\x14O\xe3\xf8(Y\xec\xd89\xfdv\xfe\\\xf7n\x86DϨ\xda\x13\xe5\x92W\xe9\x92L\xc9\xc6s\x02\r\fփC\x84\x8d\x9b\xac[\f\x10\xa6(\x90,ʕ2\x91d\x91\bZ\t\x82~\xad\x8cu\xeb\x90=\x7f\x7ft\xa1R\x85\xc5I\xc67\x1643V\xe9\x90NJ\x8a?e)\xbe[n\xb7`\xc0\xefC\xf9EO\a\x18\xa3\xd8\xf3V78\xabr\xee\xf6¨#\x9e\x91\xf7Dm+\xad20Ѽ\x88\xb6$ڦ\x91\xac\xa5.\x1d\x9au]\ue8bfM\x92\xd6NY\x94\x0ee\x9e#\x8f\xa4;\"2\xfa\xf0\xad\xb3D\x8d\xda\x05\xffN\x91\xd6cpdt\x00\xa6,\xf90\x87:\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\x90:\xe9\xb2\xcd\xe7\xc4ہ\xa4[\xfe\b>\xdd\x17d\xa6jIKa\xa8\a\xb0\x9b\x19\x10\x1dk\x9c\x15H\xb4w\x9d\xc6\xd1\f̱\xb2 I\x12rrݬ\xdb\xe4#\x17i\xebV\xec8\xb6\xdaC9\x9cc\xe5\xf8y\x14\x12<\xbbg\x10J\xfeM\x94u\xc9x\x89<$\xb7C\x94\xd0\x1cCp\xecn\xd2>\xb1\x05\x19-\xabp\x96U\x05X\xf0i\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\xb4c.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;z\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*\x9fbK76D\x80\xd1%\x1e<\xa7\x9b\x1b:\az\xbc\x1e\b\xf7K\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~3d(\xd3\x0e\xc0\xde\xc5Σ\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^\xc4\x17\v\x9a\xb7\xb0k.\\\xfaY\xd11\x7f\x7f\xdbؗ\xafͬ]\x0e\"3n\xd8\x13\x14\x05\xe31\xfd\xb2G\x85\xcc]\x00\x9b\xa9\x05\xa0}GM\xe5/\x94\xf2\xb7\xc6^\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]0\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ɿ\xe0@\xafu\x1c\xda\xcaG\xc9\xecxD\xbd\xbd\xd5\xd6]\xeb;\xc4\xfe\x19\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\xee\"\xffc\xd7{*\xf1'zg\xeb\x04+\x87\xef\xaeW\x04+\x88\x11=\xe0\xd5$Y6,_\x03\xba\f\xed\xd8\x0f\xe9\x93զ\a\xb5\x9f\xe7\xdc}\xa5\x04r\xf7$Mp[\xbcj\xce\x14j\xad\xeb\x95\xc3\xe5PO(_\\\xee\x98\xf2o\x8e\b\x9d/*\xae\xed\xce%D]\xf4\xf0\bv}j\xe5\uf835\xda\x7fr\xa7[zd\x0f\xaf\xed\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\xca\xc0_\xd4#\xbc\x8f\xae\xbe\xf6\x9f\xdd\x194\x19I\xaf\fP\xe9\xa2\xfc\b\x05ۜJ\xba\xa7\xfcyj/\x9e/\x19P\xf1\xf7\x9c?gq\xf2\xa6\x0fj\xfc%\x1a\xba\x05>t\x1a\xf3\xaa药\x1d\xbb\xbe\xa3\xb8\xb5Q\xa5~\xea\xfb\xb85,O\x86$\x89\b,!\x0f>\xces*2Z\xa5\xf9=|R\xeeQ\xa5\x141\xe9\xb7\xe8=\xb9\xe5=\xb7\x90s\xee'aL\xd1\xfb\xb1\r\x01\xb6gL\xf6\x1e+@l\x8f|\x8e\xc1\xda\xe292r{\xfbɍ\x94\u07b2y\uf7e5A}l\x00Y\x10(࠭\xf1\xbf[\xf5D\x97\xf8\xc7ט\xc3\xcb1\x9d\xc7\xeb\x80\x0e\xbbP\x1a\xf2Qì\xabB\xf1\x1c\xf4\x15\xbd\x9e\x930\xe2\x9fz\r\x06\xee@\xff\r\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˸\u05cc\xc2\xf6\x9a0ͫv\xfb\xf4\x9cP!qK7\xee\xb0-\xc6\x1f\xe1Z4\x8f\x85\x9d%\x90۽\xe1\xd4\a<\xfe\x96\xa1{\xec)㕭uЮ\xb5\xa6\x9b\xe2\x11\b\xb8\x8bԏ{Ͱ}\xe4\xee\x18\x06\xb7\xaf̵\xfb\x0f\x93\xef\xe0\x8e\xc0i\xde#\x8c>n\xe6\"j\xf7N\xed\x02\xe1\x1f\xc7\xe3\xd1\x19\x838߸G\xeb&\x88\xf0\xa9\xad96\xe0f\x188d\xff\fޫ\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\x011\xefH\x8e\xf7һ_\xeau\xfb(\x04\xfb\xdb\xdf\xcf\xfe?\x00\x00\xff\xff\xf8FZ\xc4S}\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"), } diff --git a/pkg/install/resources_test.go b/pkg/install/resources_test.go index bafa3a684..29b99ed41 100644 --- a/pkg/install/resources_test.go +++ b/pkg/install/resources_test.go @@ -118,6 +118,32 @@ func TestAllResources(t *testing.T) { assert.Len(t, ds, 2) } +func TestAllResourcesWithDefaultResourceModifierConfigMap(t *testing.T) { + option := &VeleroOptions{ + Namespace: "velero", + SecretData: []byte{'a'}, + DefaultResourceModifierConfigMap: "default-rm", + } + list := AllResources(option) + + for _, item := range list.Items { + if item.GetKind() == "Deployment" && item.GetName() == "velero" { + containers, _, _ := unstructured.NestedSlice(item.Object, "spec", "template", "spec", "containers") + args, _, _ := unstructured.NestedStringSlice(containers[0].(map[string]any), "args") + found := false + for _, arg := range args { + if arg == "--default-resource-modifier-configmap=default-rm" { + found = true + break + } + } + assert.True(t, found, "expected --default-resource-modifier-configmap=default-rm in deployment args") + return + } + } + t.Fatal("velero deployment not found in AllResources output") +} + func TestAllResourcesWithPriorityClassName(t *testing.T) { testCases := []struct { name string From 738dfe8bb960fd8b9b126db97067b5681e9a0988 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 3 Aug 2026 15:52:31 -0700 Subject: [PATCH 150/194] site: add blog posts for Velero v1.17 and v1.18 releases Add release blog posts for v1.17 (Sep 2025) and v1.18 (Mar 2026) covering major features, breaking changes, and community contributions. v1.17 highlights: VolumeGroupSnapshot support, modernized fs-backup, Windows cluster support, priority class support. v1.18 highlights: concurrent backup processing, cache volume support for data movers, incremental backup size reporting, VolumePolicy enhancements. Signed-off-by: Shubham Pampattiwar --- site/content/posts/2025-09-15-Velero-1.17.md | 99 +++++++++++++++++++ site/content/posts/2026-03-06-Velero-1.18.md | 93 +++++++++++++++++ site/static/img/posts/post-1.17.jpg | Bin 0 -> 55638 bytes site/static/img/posts/post-1.18.jpg | Bin 0 -> 104909 bytes 4 files changed, 192 insertions(+) create mode 100644 site/content/posts/2025-09-15-Velero-1.17.md create mode 100644 site/content/posts/2026-03-06-Velero-1.18.md create mode 100644 site/static/img/posts/post-1.17.jpg create mode 100644 site/static/img/posts/post-1.18.jpg diff --git a/site/content/posts/2025-09-15-Velero-1.17.md b/site/content/posts/2025-09-15-Velero-1.17.md new file mode 100644 index 000000000..88394f3d6 --- /dev/null +++ b/site/content/posts/2025-09-15-Velero-1.17.md @@ -0,0 +1,99 @@ +--- +title: "Velero 1.17: Volume Group Snapshots, Modernized fs-backup, and Windows Support" +excerpt: Velero 1.17 introduces VolumeGroupSnapshot support for crash-consistent multi-volume backups, a modernized fs-backup architecture, Windows cluster support, and significant scalability improvements for data movers. +author_name: Shubham Pampattiwar +slug: Velero-1.17 +categories: ['velero','release'] +image: /img/posts/post-1.17.jpg +tags: ['Velero Team', 'Shubham Pampattiwar', 'Velero Release'] +--- + +We are pleased to announce the release of [Velero v1.17](https://github.com/velero-io/velero/releases/tag/v1.17.0). This is a feature-rich release that delivers volume group snapshot support, a modernized fs-backup architecture, Windows workload backup/restore, and major scalability improvements for data movers. + +### Full list of changes can be found [here](https://github.com/velero-io/velero/releases/tag/v1.17.0) + +## Release Highlights + +### Volume Group Snapshot Support + +Velero 1.17 supports [volume group snapshots](https://kubernetes.io/blog/2024/12/18/kubernetes-1-32-volume-group-snapshot-beta/), a beta feature in Kubernetes, for both CSI snapshot backup and CSI snapshot data movement. This allows snapshots to be taken from multiple volumes at the same point-in-time to achieve write order consistency, which is important for achieving better data consistency when multiple correlated volumes are backed up together. + +See the [documentation](https://velero.io/docs/v1.17/volume-group-snapshots/) for details. + +### Modernized fs-backup + +The fs-backup subsystem has been rebuilt on the micro-service architecture, bringing several benefits: + +- **Feature parity**: Load concurrency control, cancel, and resume on restart are now available for fs-backup. +- **Improved robustness**: Running backups and restores survive node-agent restarts. Resource allocation is more granular, so the failure of one backup/restore does not impact others. +- **Steady resource usage**: Node-agent pods no longer request large amounts of memory and hold it for extended periods. + +See the [design document](https://github.com/vmware-tanzu/velero/tree/v1.17.0/design/Implemented/vgdp-micro-service-for-fs-backup/vgdp-micro-service-for-fs-backup.md) for details. + +### Windows Cluster Support for fs-backup + +Velero fs-backup now supports backing up and restoring Windows workloads. By leveraging the new micro-service architecture, data mover pods can run on Windows nodes and handle Windows volumes. Together with CSI snapshot data movement for Windows delivered in v1.16, Velero now supports Windows workload backup/restore across all scenarios. + +### Priority Class Support + +[Kubernetes priority classes](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#priorityclass) are now supported across all Velero modules. Users can configure priority classes separately for Velero server, node-agent, data mover pods, and backup repository maintenance jobs. + +See the [design document](https://github.com/vmware-tanzu/velero/tree/v1.17.0/design/Implemented/priority-class-name-support_design.md) for details. + +### Include/Exclude Policy for Resource Policy + +Velero resource policy now supports `includeExcludePolicy` alongside the existing `volumePolicy`. This allows users to set include/exclude filters for resources in a resource policy configmap, making these filters reusable across multiple backups. + +## Scalability and Resiliency Improvements + +### Reduced Data Mover Pod Congestion + +A new `PrepareQueueLength` setting in node-agent configuration limits how many data mover pods and volumes are created ahead of available data path quota. This prevents excessive cluster resource consumption, particularly helpful in large-scale environments. This applies to both fs-backup and CSI snapshot data movement. + +See the [design document](https://github.com/vmware-tanzu/velero/tree/v1.17.0/design/Implemented/node-agent-load-soothing.md) for details. + +### Enhanced Node-Agent Restart Handling + +Data movements in all phases now survive node-agent restarts and resume automatically. Orphaned data movements from scenarios like cluster node absence are canceled appropriately after restart. + +### Restore Node-Selection for CSI Snapshot Data Movement + +CSI snapshot data movement restore now has the same node-selection capability as backup. Users can specify which nodes can or cannot run data mover pods for both backup and restore, with per-storage-class configuration for environments where a storage class is not usable by all cluster nodes. + +## Breaking Changes + +### Deprecation of Restic + +Per the [Velero deprecation policy](https://github.com/vmware-tanzu/velero/tree/v1.17.0/GOVERNANCE.md#deprecation-policy), backup under the Restic path is removed in v1.17. `--uploader-type=restic` is no longer a valid installation configuration. Restores from previous Restic-path backups remain supported until v1.19. + +### Repository Maintenance Job Configuration + +Repository maintenance job configurations have been moved from Velero server parameters to a repository maintenance job configmap. The following server parameters are removed: `--keep-latest-maintenance-jobs`, `--maintenance-job-cpu-request`, `--maintenance-job-mem-request`, `--maintenance-job-cpu-limit`, `--maintenance-job-mem-limit`. + +## Community Contributions + +Thank you to everyone who contributed to this release: + +- [@Lyndon-Li](https://github.com/Lyndon-Li) -- modernized fs-backup, Windows support, data mover scalability, node-agent restart handling +- [@blackpiglet](https://github.com/blackpiglet) -- configmap validation, maintenance job improvements, VolumeSnapshot cleanup +- [@shubham-pampattiwar](https://github.com/shubham-pampattiwar) -- VolumeGroupSnapshot support, VGS documentation, maintenance job configmap, VGS PVC plugin +- [@sseago](https://github.com/sseago) -- hook tracking improvements +- [@kaovilai](https://github.com/kaovilai) -- priority class restore ordering, ResticIdentifier fix +- [@reasonerjt](https://github.com/reasonerjt) -- include/exclude resource policy, BSL availability metrics +- [@ywk253100](https://github.com/ywk253100) -- server version check improvements +- [@priyansh17](https://github.com/priyansh17) -- context-based logging, Azure credential cleanup +- [@amastbau](https://github.com/amastbau) -- label selector restore fix +- [@longxiucai](https://github.com/longxiucai) -- parameterized kubelet mount path +- [@farodin91](https://github.com/farodin91) -- bug fixes +- [@flx5](https://github.com/flx5) -- bug fixes +- [@hu-keyu](https://github.com/hu-keyu) -- bug fixes +- [@pandurangkhandeparker](https://github.com/pandurangkhandeparker) -- bug fixes +- [@vishal-chdhry](https://github.com/vishal-chdhry) -- bug fixes + +## Join the Community + +- **Slack**: [#velero-users](https://kubernetes.slack.com/messages/velero) and [#velero-dev](https://kubernetes.slack.com/messages/velero-dev) on Kubernetes Slack +- **GitHub**: [github.com/velero-io/velero](https://github.com/velero-io/velero) +- **Community Meetings**: Bi-weekly, alternating US/Europe and US/Asia time zones. See the [community page](https://velero.io/community/) for details. +- **LinkedIn**: [Project Velero](https://www.linkedin.com/company/project-velero) +- **Twitter/X**: [@projectvelero](https://twitter.com/projectvelero) diff --git a/site/content/posts/2026-03-06-Velero-1.18.md b/site/content/posts/2026-03-06-Velero-1.18.md new file mode 100644 index 000000000..6a27372a8 --- /dev/null +++ b/site/content/posts/2026-03-06-Velero-1.18.md @@ -0,0 +1,93 @@ +--- +title: "Velero 1.18: Concurrent Backups, Cache Volumes, and More" +excerpt: Velero 1.18 introduces concurrent backup processing, cache volume support for data movers, incremental backup size reporting, and several scalability and performance improvements. +author_name: Shubham Pampattiwar +slug: Velero-1.18 +categories: ['velero','release'] +image: /img/posts/post-1.18.jpg +tags: ['Velero Team', 'Shubham Pampattiwar', 'Velero Release'] +--- + +We are pleased to announce the release of [Velero v1.18](https://github.com/velero-io/velero/releases/tag/v1.18.0). This release brings significant improvements in concurrency, performance, and observability, with contributions from engineers across multiple organizations. + +### Full list of changes can be found [here](https://github.com/velero-io/velero/releases/tag/v1.18.0) + +## Release Highlights + +### Concurrent Backup Processing + +Velero can now process multiple backups concurrently. This is a major usability improvement for multi-tenant environments -- backups submitted by different users or teams run simultaneously without interfering with each other. + +Previously, backups were serialized, meaning a long-running backup would block all other pending backups. With concurrent processing, backup throughput scales with available resources. + +See the [design document](https://github.com/vmware-tanzu/velero/blob/main/design/Implemented/concurrent-backup-processing.md) for details. + +### Cache Volume Support for Data Movers + +Velero 1.18 allows users to configure cache volumes for data mover pods during restore operations for both CSI snapshot data movement and fs-backup. This solves several real-world problems: + +- Data mover pods failing when a pod's ephemeral disk is limited +- Multiple data mover pods failing to run concurrently on a single node due to disk constraints +- Combined with backup repository cache limit configuration, appropriately sized cache volumes improve restore throughput + +See the [design document](https://github.com/vmware-tanzu/velero/blob/main/design/Implemented/backup-repo-cache-volume.md) for details. + +### Incremental Backup Size Reporting + +Users can now observe the incremental size of data mover backups for CSI snapshot data movement and fs-backup. This provides visibility into data reduction from incremental backups, helping teams understand and optimize their backup storage usage. + +### Wildcard Namespace Filtering + +Velero now supports Glob regular expressions for namespace filters during backup and restore. This allows users to filter namespaces in batch -- for example, backing up all namespaces matching `team-*` or excluding `test-*` namespaces. + +### VolumePolicy Enhancements + +VolumePolicy receives two improvements in this release: + +- **PVC Phase support**: Users can now filter volumes by PVC phase, enabling actions like skipping PVCs in Pending or Lost status from backups to avoid failures caused by unbound volumes. +- **VolumeGroupSnapshot integration**: Volume policies now apply to VolumeGroupSnapshot PVC filtering, building on the VolumeGroupSnapshot support introduced in v1.17. + +## Scalability and Resiliency + +### Prevent Velero Server OOM for Large Backup Repositories + +Some backup repository operations are now executed outside the Velero server process, preventing OOM kills when working with large repositories. + +### VolumePolicy Performance + +VolumePolicy evaluation has been optimized for environments with large numbers of pods and PVCs, resulting in significantly improved performance through a PVC-to-Pod cache that avoids redundant lookups. + +### Events for Data Mover Pod Diagnostics + +Events are now recorded in data mover pod diagnostics, giving users more information for troubleshooting when data mover pods fail. + +## Breaking Changes + +### Deprecation of PVC Selected Node Feature + +Per the [Velero deprecation policy](https://github.com/vmware-tanzu/velero/blob/main/GOVERNANCE.md#deprecation-policy), the PVC selected node feature is deprecated in v1.18. Velero now handles PVC selected-node annotations automatically, so no user action is required. + +## Community Contributions + +This release includes contributions from across the Velero community. Thank you to everyone who contributed: + +- [@sseago](https://github.com/sseago) -- concurrent backup processing, incremental size reporting +- [@Lyndon-Li](https://github.com/Lyndon-Li) -- cache volume support, data mover diagnostics +- [@blackpiglet](https://github.com/blackpiglet) -- maintenance job improvements, restore ordering fixes +- [@shubham-pampattiwar](https://github.com/shubham-pampattiwar) -- VolumePolicy performance, VolumeGroupSnapshot filtering, Prometheus metrics +- [@kaovilai](https://github.com/kaovilai) -- BSL secret-based CA certificate support +- [@mpryc](https://github.com/mpryc) -- plugin init container DNS fix +- [@mjnagel](https://github.com/mjnagel) -- install command `--apply` flag +- [@Joeavaikath](https://github.com/Joeavaikath) -- backup label cleanup +- [@0xLeo258](https://github.com/0xLeo258) -- VolumeSnapshotter cache concurrency control +- [@clementnuss](https://github.com/clementnuss) -- bug fixes +- [@priyansh17](https://github.com/priyansh17) -- backend improvements +- [@T4iFooN-IX](https://github.com/T4iFooN-IX) -- documentation fixes + +## Join the Community + +- **Slack**: [#velero-users](https://kubernetes.slack.com/messages/velero) and [#velero-dev](https://kubernetes.slack.com/messages/velero-dev) on Kubernetes Slack +- **GitHub**: [github.com/velero-io/velero](https://github.com/velero-io/velero) +- **Community Meetings**: Bi-weekly, alternating US/Europe and US/Asia time zones. See the [community page](https://velero.io/community/) for details. +- **LinkedIn**: [Project Velero](https://www.linkedin.com/company/project-velero) +- **Twitter/X**: [@projectvelero](https://twitter.com/projectvelero) diff --git a/site/static/img/posts/post-1.17.jpg b/site/static/img/posts/post-1.17.jpg new file mode 100644 index 0000000000000000000000000000000000000000..93f4b9ad5637135c9a97c246b9eaf79e1e42f5c7 GIT binary patch literal 55638 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%0D&x=d)BQeR|Hbl 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�T=H%wym7CnmoHP6E?Yib?BuH)m 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%0DGOTdX|c$%1BpcUY)0&TF98Zk*Jj$K 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 Date: Tue, 4 Aug 2026 13:26:47 +0800 Subject: [PATCH 151/194] Drop node-agent host path mounts from data mover pods The CSI snapshot and generic restore exposers access data through PVCs, so they no longer inherit the node-agent host path volumes. Also drop all capabilities on the data mover container. Signed-off-by: chlins --- changelogs/unreleased/10150-chlins | 1 + pkg/exposer/csi_snapshot.go | 30 +++- pkg/exposer/generic_restore.go | 30 +++- pkg/exposer/image.go | 58 ++++++- pkg/exposer/image_daemonset_test.go | 75 +++++++++ pkg/exposer/image_test.go | 236 +++++++++++++++++++++++++++- 6 files changed, 406 insertions(+), 24 deletions(-) create mode 100644 changelogs/unreleased/10150-chlins create mode 100644 pkg/exposer/image_daemonset_test.go diff --git a/changelogs/unreleased/10150-chlins b/changelogs/unreleased/10150-chlins new file mode 100644 index 000000000..158a3f43c --- /dev/null +++ b/changelogs/unreleased/10150-chlins @@ -0,0 +1 @@ +Drop node-agent host path mounts from data mover pods diff --git a/pkg/exposer/csi_snapshot.go b/pkg/exposer/csi_snapshot.go index ed510c798..3fd78cb9b 100644 --- a/pkg/exposer/csi_snapshot.go +++ b/pkg/exposer/csi_snapshot.go @@ -684,7 +684,9 @@ func (e *csiSnapshotExposer) createBackupPod( containerName := string(ownerObject.UID) volumeName := string(ownerObject.UID) - podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS) + // The backup pod reads the data through the backup PVC only, so the node-agent's host + // path volumes to the kubelet root directory are not inherited. + podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS, hostPathVolumesOfNodeAgent...) if err != nil { return nil, errors.Wrap(err, "error to get inherited pod info from node-agent") } @@ -750,6 +752,7 @@ func (e *csiSnapshotExposer) createBackupPod( } var securityCtx *corev1api.PodSecurityContext + var containerSecurityCtx *corev1api.SecurityContext nodeSelector := map[string]string{} podOS := corev1api.PodOS{} if nodeOS == kube.NodeOSWindows { @@ -788,6 +791,18 @@ func (e *csiSnapshotExposer) createBackupPod( RunAsUser: &userID, } + // The backup pod runs as root so that it can read the backup data regardless of the + // ownership, but it doesn't need any capability beyond that. + containerSecurityCtx = &corev1api.SecurityContext{ + AllowPrivilegeEscalation: boolptr.False(), + Capabilities: &corev1api.Capabilities{ + Drop: []corev1api.Capability{"ALL"}, + }, + SeccompProfile: &corev1api.SeccompProfile{ + Type: corev1api.SeccompProfileTypeRuntimeDefault, + }, + } + if spcNoRelabeling { securityCtx.SELinuxOptions = &corev1api.SELinuxOptions{ Type: "spc_t", @@ -859,12 +874,13 @@ func (e *csiSnapshotExposer) createBackupPod( "data-mover", "backup", }, - Args: args, - VolumeMounts: volumeMounts, - VolumeDevices: volumeDevices, - Env: podInfo.env, - EnvFrom: podInfo.envFrom, - Resources: resources, + Args: args, + VolumeMounts: volumeMounts, + VolumeDevices: volumeDevices, + Env: podInfo.env, + EnvFrom: podInfo.envFrom, + Resources: resources, + SecurityContext: containerSecurityCtx, }, }, PriorityClassName: priorityClassName, diff --git a/pkg/exposer/generic_restore.go b/pkg/exposer/generic_restore.go index 0f4b9c5b4..46851803c 100644 --- a/pkg/exposer/generic_restore.go +++ b/pkg/exposer/generic_restore.go @@ -628,7 +628,9 @@ func (e *genericRestoreExposer) createRestorePod( affinity = &kube.LoadAffinity{} } - podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS) + // The restore pod writes the data through the restore PVC only, so the node-agent's host + // path volumes to the kubelet root directory are not inherited. + podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS, hostPathVolumesOfNodeAgent...) if err != nil { return nil, errors.Wrap(err, "error to get inherited pod info from node-agent") } @@ -692,6 +694,7 @@ func (e *genericRestoreExposer) createRestorePod( args = append(args, podInfo.logLevelArgs...) var securityCtx *corev1api.PodSecurityContext + var containerSecurityCtx *corev1api.SecurityContext podOS := corev1api.PodOS{} if nodeOS == kube.NodeOSWindows { userID := "ContainerAdministrator" @@ -729,6 +732,18 @@ func (e *genericRestoreExposer) createRestorePod( RunAsUser: &userID, } + // The restore pod runs as root so that it can restore the data with the original + // ownership, but it doesn't need any capability beyond that. + containerSecurityCtx = &corev1api.SecurityContext{ + AllowPrivilegeEscalation: boolptr.False(), + Capabilities: &corev1api.Capabilities{ + Drop: []corev1api.Capability{"ALL"}, + }, + SeccompProfile: &corev1api.SeccompProfile{ + Type: corev1api.SeccompProfileTypeRuntimeDefault, + }, + } + podOS.Name = kube.NodeOSLinux affinity.NodeSelector.MatchExpressions = append(affinity.NodeSelector.MatchExpressions, metav1.LabelSelectorRequirement{ @@ -781,12 +796,13 @@ func (e *genericRestoreExposer) createRestorePod( "data-mover", "restore", }, - Args: args, - VolumeMounts: volumeMounts, - VolumeDevices: volumeDevices, - Env: podInfo.env, - EnvFrom: podInfo.envFrom, - Resources: resources, + Args: args, + VolumeMounts: volumeMounts, + VolumeDevices: volumeDevices, + Env: podInfo.env, + EnvFrom: podInfo.envFrom, + Resources: resources, + SecurityContext: containerSecurityCtx, }, }, PriorityClassName: priorityClassName, diff --git a/pkg/exposer/image.go b/pkg/exposer/image.go index 2157d8175..aa774221b 100644 --- a/pkg/exposer/image.go +++ b/pkg/exposer/image.go @@ -27,6 +27,19 @@ import ( "github.com/vmware-tanzu/velero/pkg/nodeagent" ) +const ( + // hostPluginsVolumeName is the name of the node-agent volume that mounts the kubelet + // plugins directory from the host. + hostPluginsVolumeName = "host-plugins" +) + +// hostPathVolumesOfNodeAgent lists the node-agent volumes that expose the kubelet root +// directory of the host. They are only required by fs-backup, which resolves and accesses +// pod volume data through the kubelet pod directory. Other exposers access data through +// PVCs only, so they must exclude these volumes from the inherited pod info to avoid +// granting data mover pods unnecessary access to the host file system. +var hostPathVolumesOfNodeAgent = []string{nodeagent.HostPodVolumeMount, hostPluginsVolumeName} + type inheritedPodInfo struct { image string serviceAccount string @@ -41,7 +54,11 @@ type inheritedPodInfo struct { imagePullSecrets []corev1api.LocalObjectReference } -func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veleroNamespace string, osType string) (inheritedPodInfo, error) { +// getInheritedPodInfo collects the pod info to be inherited by the hosting pods from the +// node-agent pod template. Volumes whose name is listed in excludedVolumes, together with +// their volume mounts, are dropped from the result. Names that are not found in the +// node-agent pod template are ignored. +func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veleroNamespace string, osType string, excludedVolumes ...string) (inheritedPodInfo, error) { podInfo := inheritedPodInfo{} podSpec, err := nodeagent.GetPodSpec(ctx, client, veleroNamespace, osType) @@ -58,8 +75,7 @@ func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veler podInfo.env = podSpec.Containers[0].Env podInfo.envFrom = podSpec.Containers[0].EnvFrom - podInfo.volumeMounts = podSpec.Containers[0].VolumeMounts - podInfo.volumes = podSpec.Volumes + podInfo.volumeMounts, podInfo.volumes = excludeVolumes(podSpec.Containers[0].VolumeMounts, podSpec.Volumes, excludedVolumes) podInfo.dnsPolicy = podSpec.DNSPolicy podInfo.dnsConfig = podSpec.DNSConfig @@ -81,3 +97,39 @@ func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veler return podInfo, nil } + +// excludeVolumes removes the volumes matching the given names, as well as the volume mounts +// referring to them, from the given volumes and volume mounts. An excluded name that doesn't +// match any volume is a no-op, so callers don't need to know how the node-agent daemonset is +// configured. Volumes that are not excluded, including the ones customized by users, are kept +// as is. +func excludeVolumes(volumeMounts []corev1api.VolumeMount, volumes []corev1api.Volume, excludedVolumes []string) ([]corev1api.VolumeMount, []corev1api.Volume) { + if len(excludedVolumes) == 0 { + return volumeMounts, volumes + } + + excluded := make(map[string]struct{}, len(excludedVolumes)) + for _, name := range excludedVolumes { + excluded[name] = struct{}{} + } + + var retainedMounts []corev1api.VolumeMount + for _, volumeMount := range volumeMounts { + if _, found := excluded[volumeMount.Name]; found { + continue + } + + retainedMounts = append(retainedMounts, volumeMount) + } + + var retainedVolumes []corev1api.Volume + for _, volume := range volumes { + if _, found := excluded[volume.Name]; found { + continue + } + + retainedVolumes = append(retainedVolumes, volume) + } + + return retainedMounts, retainedVolumes +} diff --git a/pkg/exposer/image_daemonset_test.go b/pkg/exposer/image_daemonset_test.go new file mode 100644 index 000000000..0941d44f7 --- /dev/null +++ b/pkg/exposer/image_daemonset_test.go @@ -0,0 +1,75 @@ +package exposer + +import ( + "context" + "testing" + + appsv1api "k8s.io/api/apps/v1" + corev1api "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes/fake" + + "github.com/vmware-tanzu/velero/pkg/install" +) + +// TestInheritedPodInfoAgainstRealDaemonSet guards the exclusion against the node-agent +// daemonset that is actually installed, so that a host path volume added to the daemonset +// later is not silently inherited by the data mover pods. +func TestInheritedPodInfoAgainstRealDaemonSet(t *testing.T) { + nodeAgent := install.DaemonSet("velero") + client := fake.NewSimpleClientset(&appsv1api.DaemonSet{ + ObjectMeta: nodeAgent.ObjectMeta, + Spec: nodeAgent.Spec, + }) + + hostPathVolumes := func(volumes []corev1api.Volume) []string { + names := []string{} + for _, volume := range volumes { + if volume.HostPath != nil { + names = append(names, volume.Name) + } + } + return names + } + + // The installed daemonset must carry host path volumes, otherwise this test is vacuous. + if len(hostPathVolumes(nodeAgent.Spec.Template.Spec.Volumes)) == 0 { + t.Fatal("the installed node-agent daemonset is expected to have host path volumes") + } + + // fs-backup resolves pod volume data through the kubelet pod directory, so it keeps them. + fsBackupInfo, err := getInheritedPodInfo(context.Background(), client, "velero", "linux") + if err != nil { + t.Fatalf("error to get inherited pod info for fs-backup: %v", err) + } + + if len(hostPathVolumes(fsBackupInfo.volumes)) == 0 { + t.Error("fs-backup is expected to inherit the host path volumes") + } + + // The data mover pods access data through PVCs, so they must not get any host path. + dataMoverInfo, err := getInheritedPodInfo(context.Background(), client, "velero", "linux", hostPathVolumesOfNodeAgent...) + if err != nil { + t.Fatalf("error to get inherited pod info for data mover: %v", err) + } + + if inherited := hostPathVolumes(dataMoverInfo.volumes); len(inherited) > 0 { + t.Errorf("data mover pods are not expected to inherit host path volumes, but got %v", inherited) + } + + // The other volumes, e.g., the scratch volume, are still required. + if len(dataMoverInfo.volumes) == 0 { + t.Error("data mover pods are expected to inherit the volumes other than the host path ones") + } + + // Every remaining mount must still have its backing volume. + volumeNames := map[string]struct{}{} + for _, volume := range dataMoverInfo.volumes { + volumeNames[volume.Name] = struct{}{} + } + + for _, volumeMount := range dataMoverInfo.volumeMounts { + if _, exist := volumeNames[volumeMount.Name]; !exist { + t.Errorf("volume mount %q doesn't have a backing volume", volumeMount.Name) + } + } +} diff --git a/pkg/exposer/image_test.go b/pkg/exposer/image_test.go index 5c47f5c04..d93a667ba 100644 --- a/pkg/exposer/image_test.go +++ b/pkg/exposer/image_test.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" + "github.com/vmware-tanzu/velero/pkg/nodeagent" "github.com/vmware-tanzu/velero/pkg/util/kube" appsv1api "k8s.io/api/apps/v1" @@ -187,16 +188,118 @@ func TestGetInheritedPodInfo(t *testing.T) { }, } + daemonSetWithHostPath := &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "node-agent", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DaemonSet", + }, + Spec: appsv1api.DaemonSetSpec{ + Template: corev1api.PodTemplateSpec{ + Spec: corev1api.PodSpec{ + Containers: []corev1api.Container{ + { + Name: "container-1", + Image: "image-1", + VolumeMounts: []corev1api.VolumeMount{ + { + Name: nodeagent.HostPodVolumeMount, + MountPath: "/host_pods", + }, + { + Name: hostPluginsVolumeName, + MountPath: "/var/lib/kubelet/plugins", + }, + { + Name: "scratch", + MountPath: "/scratch", + }, + { + Name: "user-credentials", + MountPath: "/credentials", + }, + }, + }, + }, + Volumes: []corev1api.Volume{ + { + Name: nodeagent.HostPodVolumeMount, + VolumeSource: corev1api.VolumeSource{ + HostPath: &corev1api.HostPathVolumeSource{ + Path: "/var/lib/kubelet/pods", + }, + }, + }, + { + Name: hostPluginsVolumeName, + VolumeSource: corev1api.VolumeSource{ + HostPath: &corev1api.HostPathVolumeSource{ + Path: "/var/lib/kubelet/plugins", + }, + }, + }, + { + Name: "scratch", + VolumeSource: corev1api.VolumeSource{ + EmptyDir: new(corev1api.EmptyDirVolumeSource), + }, + }, + { + Name: "user-credentials", + VolumeSource: corev1api.VolumeSource{ + Secret: &corev1api.SecretVolumeSource{ + SecretName: "user-credentials", + }, + }, + }, + }, + ServiceAccountName: "sa-1", + }, + }, + }, + } + + scratchAndCredentialMounts := []corev1api.VolumeMount{ + { + Name: "scratch", + MountPath: "/scratch", + }, + { + Name: "user-credentials", + MountPath: "/credentials", + }, + } + + scratchAndCredentialVolumes := []corev1api.Volume{ + { + Name: "scratch", + VolumeSource: corev1api.VolumeSource{ + EmptyDir: new(corev1api.EmptyDirVolumeSource), + }, + }, + { + Name: "user-credentials", + VolumeSource: corev1api.VolumeSource{ + Secret: &corev1api.SecretVolumeSource{ + SecretName: "user-credentials", + }, + }, + }, + } + scheme := runtime.NewScheme() appsv1api.AddToScheme(scheme) tests := []struct { - name string - namespace string - client kubernetes.Interface - kubeClientObj []runtime.Object - result inheritedPodInfo - expectErr string + name string + namespace string + client kubernetes.Interface + kubeClientObj []runtime.Object + excludedVolumes []string + result inheritedPodInfo + expectErr string }{ { name: "ds is not found", @@ -329,12 +432,131 @@ func TestGetInheritedPodInfo(t *testing.T) { }, }, }, + { + name: "no excluded volume, host path volumes are inherited", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + daemonSetWithHostPath, + }, + result: inheritedPodInfo{ + image: "image-1", + serviceAccount: "sa-1", + volumeMounts: daemonSetWithHostPath.Spec.Template.Spec.Containers[0].VolumeMounts, + volumes: daemonSetWithHostPath.Spec.Template.Spec.Volumes, + }, + }, + { + name: "host path volumes and their mounts are excluded", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + daemonSetWithHostPath, + }, + excludedVolumes: hostPathVolumesOfNodeAgent, + result: inheritedPodInfo{ + image: "image-1", + serviceAccount: "sa-1", + volumeMounts: scratchAndCredentialMounts, + volumes: scratchAndCredentialVolumes, + }, + }, + { + name: "excluding a volume that doesn't exist doesn't affect the others", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + daemonSetWithNoLog, + }, + excludedVolumes: hostPathVolumesOfNodeAgent, + result: inheritedPodInfo{ + image: "image-1", + serviceAccount: "sa-1", + env: []corev1api.EnvVar{ + { + Name: "env-1", + Value: "value-1", + }, + { + Name: "env-2", + Value: "value-2", + }, + }, + envFrom: []corev1api.EnvFromSource{ + { + ConfigMapRef: &corev1api.ConfigMapEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-configmap", + }, + }, + }, + { + SecretRef: &corev1api.SecretEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-secret", + }, + }, + }, + }, + volumeMounts: []corev1api.VolumeMount{ + { + Name: "volume-1", + }, + { + Name: "volume-2", + }, + }, + volumes: []corev1api.Volume{ + { + Name: "volume-1", + }, + { + Name: "volume-2", + }, + }, + }, + }, + { + name: "excluding all volumes results in empty volumes and mounts", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + daemonSetWithNoLog, + }, + excludedVolumes: []string{"volume-1", "volume-2"}, + result: inheritedPodInfo{ + image: "image-1", + serviceAccount: "sa-1", + env: []corev1api.EnvVar{ + { + Name: "env-1", + Value: "value-1", + }, + { + Name: "env-2", + Value: "value-2", + }, + }, + envFrom: []corev1api.EnvFromSource{ + { + ConfigMapRef: &corev1api.ConfigMapEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-configmap", + }, + }, + }, + { + SecretRef: &corev1api.SecretEnvSource{ + LocalObjectReference: corev1api.LocalObjectReference{ + Name: "test-secret", + }, + }, + }, + }, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) - info, err := getInheritedPodInfo(t.Context(), fakeKubeClient, test.namespace, kube.NodeOSLinux) + info, err := getInheritedPodInfo(t.Context(), fakeKubeClient, test.namespace, kube.NodeOSLinux, test.excludedVolumes...) if test.expectErr == "" { require.NoError(t, err) From bfda68ca3a061ee7f250ea493a0f2abd467f232c Mon Sep 17 00:00:00 2001 From: chlins Date: Tue, 4 Aug 2026 14:22:01 +0800 Subject: [PATCH 152/194] Address review comments on host path exclusion Detect the host path volumes by their source instead of their name, so the customized ones are excluded as well. Drop the container capability changes since the data mover needs them to access the data, and fix the copyright headers. Signed-off-by: chlins --- pkg/exposer/csi_snapshot.go | 30 ++++-------- pkg/exposer/generic_restore.go | 30 ++++-------- pkg/exposer/image.go | 68 +++++++++++++-------------- pkg/exposer/image_daemonset_test.go | 20 +++++++- pkg/exposer/image_test.go | 72 ++++++++++------------------- pkg/exposer/pod_volume.go | 4 +- 6 files changed, 92 insertions(+), 132 deletions(-) diff --git a/pkg/exposer/csi_snapshot.go b/pkg/exposer/csi_snapshot.go index 3fd78cb9b..2e8e08889 100644 --- a/pkg/exposer/csi_snapshot.go +++ b/pkg/exposer/csi_snapshot.go @@ -1,5 +1,5 @@ /* -Copyright The Velero Contributors. +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. @@ -686,7 +686,7 @@ func (e *csiSnapshotExposer) createBackupPod( // The backup pod reads the data through the backup PVC only, so the node-agent's host // path volumes to the kubelet root directory are not inherited. - podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS, hostPathVolumesOfNodeAgent...) + podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS, excludeHostPathVolumes) if err != nil { return nil, errors.Wrap(err, "error to get inherited pod info from node-agent") } @@ -752,7 +752,6 @@ func (e *csiSnapshotExposer) createBackupPod( } var securityCtx *corev1api.PodSecurityContext - var containerSecurityCtx *corev1api.SecurityContext nodeSelector := map[string]string{} podOS := corev1api.PodOS{} if nodeOS == kube.NodeOSWindows { @@ -791,18 +790,6 @@ func (e *csiSnapshotExposer) createBackupPod( RunAsUser: &userID, } - // The backup pod runs as root so that it can read the backup data regardless of the - // ownership, but it doesn't need any capability beyond that. - containerSecurityCtx = &corev1api.SecurityContext{ - AllowPrivilegeEscalation: boolptr.False(), - Capabilities: &corev1api.Capabilities{ - Drop: []corev1api.Capability{"ALL"}, - }, - SeccompProfile: &corev1api.SeccompProfile{ - Type: corev1api.SeccompProfileTypeRuntimeDefault, - }, - } - if spcNoRelabeling { securityCtx.SELinuxOptions = &corev1api.SELinuxOptions{ Type: "spc_t", @@ -874,13 +861,12 @@ func (e *csiSnapshotExposer) createBackupPod( "data-mover", "backup", }, - Args: args, - VolumeMounts: volumeMounts, - VolumeDevices: volumeDevices, - Env: podInfo.env, - EnvFrom: podInfo.envFrom, - Resources: resources, - SecurityContext: containerSecurityCtx, + Args: args, + VolumeMounts: volumeMounts, + VolumeDevices: volumeDevices, + Env: podInfo.env, + EnvFrom: podInfo.envFrom, + Resources: resources, }, }, PriorityClassName: priorityClassName, diff --git a/pkg/exposer/generic_restore.go b/pkg/exposer/generic_restore.go index 46851803c..16a114e64 100644 --- a/pkg/exposer/generic_restore.go +++ b/pkg/exposer/generic_restore.go @@ -1,5 +1,5 @@ /* -Copyright The Velero Contributors. +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. @@ -630,7 +630,7 @@ func (e *genericRestoreExposer) createRestorePod( // The restore pod writes the data through the restore PVC only, so the node-agent's host // path volumes to the kubelet root directory are not inherited. - podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS, hostPathVolumesOfNodeAgent...) + podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS, excludeHostPathVolumes) if err != nil { return nil, errors.Wrap(err, "error to get inherited pod info from node-agent") } @@ -694,7 +694,6 @@ func (e *genericRestoreExposer) createRestorePod( args = append(args, podInfo.logLevelArgs...) var securityCtx *corev1api.PodSecurityContext - var containerSecurityCtx *corev1api.SecurityContext podOS := corev1api.PodOS{} if nodeOS == kube.NodeOSWindows { userID := "ContainerAdministrator" @@ -732,18 +731,6 @@ func (e *genericRestoreExposer) createRestorePod( RunAsUser: &userID, } - // The restore pod runs as root so that it can restore the data with the original - // ownership, but it doesn't need any capability beyond that. - containerSecurityCtx = &corev1api.SecurityContext{ - AllowPrivilegeEscalation: boolptr.False(), - Capabilities: &corev1api.Capabilities{ - Drop: []corev1api.Capability{"ALL"}, - }, - SeccompProfile: &corev1api.SeccompProfile{ - Type: corev1api.SeccompProfileTypeRuntimeDefault, - }, - } - podOS.Name = kube.NodeOSLinux affinity.NodeSelector.MatchExpressions = append(affinity.NodeSelector.MatchExpressions, metav1.LabelSelectorRequirement{ @@ -796,13 +783,12 @@ func (e *genericRestoreExposer) createRestorePod( "data-mover", "restore", }, - Args: args, - VolumeMounts: volumeMounts, - VolumeDevices: volumeDevices, - Env: podInfo.env, - EnvFrom: podInfo.envFrom, - Resources: resources, - SecurityContext: containerSecurityCtx, + Args: args, + VolumeMounts: volumeMounts, + VolumeDevices: volumeDevices, + Env: podInfo.env, + EnvFrom: podInfo.envFrom, + Resources: resources, }, }, PriorityClassName: priorityClassName, diff --git a/pkg/exposer/image.go b/pkg/exposer/image.go index aa774221b..396303d4f 100644 --- a/pkg/exposer/image.go +++ b/pkg/exposer/image.go @@ -1,5 +1,5 @@ /* -Copyright The Velero Contributors. +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. @@ -28,17 +28,16 @@ import ( ) const ( - // hostPluginsVolumeName is the name of the node-agent volume that mounts the kubelet - // plugins directory from the host. - hostPluginsVolumeName = "host-plugins" -) + // excludeHostPathVolumes indicates that the volumes backed by a host path are not + // inherited from the node-agent. The exposers accessing data through PVCs use it so + // that the hosting pods don't get unnecessary access to the host file system. + excludeHostPathVolumes = true -// hostPathVolumesOfNodeAgent lists the node-agent volumes that expose the kubelet root -// directory of the host. They are only required by fs-backup, which resolves and accesses -// pod volume data through the kubelet pod directory. Other exposers access data through -// PVCs only, so they must exclude these volumes from the inherited pod info to avoid -// granting data mover pods unnecessary access to the host file system. -var hostPathVolumesOfNodeAgent = []string{nodeagent.HostPodVolumeMount, hostPluginsVolumeName} + // inheritHostPathVolumes indicates that the volumes backed by a host path are + // inherited from the node-agent. fs-backup uses it because it resolves and accesses + // the pod volume data through the kubelet pod directory on the host. + inheritHostPathVolumes = false +) type inheritedPodInfo struct { image string @@ -55,10 +54,11 @@ type inheritedPodInfo struct { } // getInheritedPodInfo collects the pod info to be inherited by the hosting pods from the -// node-agent pod template. Volumes whose name is listed in excludedVolumes, together with -// their volume mounts, are dropped from the result. Names that are not found in the -// node-agent pod template are ignored. -func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veleroNamespace string, osType string, excludedVolumes ...string) (inheritedPodInfo, error) { +// node-agent pod template. When excludeHostPath is true, the volumes backed by a host path, +// together with their volume mounts, are dropped from the result. The volumes are detected +// by their source instead of their name, so the ones customized in the node-agent daemonset +// are covered as well. +func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veleroNamespace string, osType string, excludeHostPath bool) (inheritedPodInfo, error) { podInfo := inheritedPodInfo{} podSpec, err := nodeagent.GetPodSpec(ctx, client, veleroNamespace, osType) @@ -75,7 +75,7 @@ func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veler podInfo.env = podSpec.Containers[0].Env podInfo.envFrom = podSpec.Containers[0].EnvFrom - podInfo.volumeMounts, podInfo.volumes = excludeVolumes(podSpec.Containers[0].VolumeMounts, podSpec.Volumes, excludedVolumes) + podInfo.volumeMounts, podInfo.volumes = filterVolumes(podSpec.Containers[0].VolumeMounts, podSpec.Volumes, excludeHostPath) podInfo.dnsPolicy = podSpec.DNSPolicy podInfo.dnsConfig = podSpec.DNSConfig @@ -98,22 +98,27 @@ func getInheritedPodInfo(ctx context.Context, client kubernetes.Interface, veler return podInfo, nil } -// excludeVolumes removes the volumes matching the given names, as well as the volume mounts -// referring to them, from the given volumes and volume mounts. An excluded name that doesn't -// match any volume is a no-op, so callers don't need to know how the node-agent daemonset is -// configured. Volumes that are not excluded, including the ones customized by users, are kept -// as is. -func excludeVolumes(volumeMounts []corev1api.VolumeMount, volumes []corev1api.Volume, excludedVolumes []string) ([]corev1api.VolumeMount, []corev1api.Volume) { - if len(excludedVolumes) == 0 { +// filterVolumes removes the volumes backed by a host path, as well as the volume mounts +// referring to them, when excludeHostPath is true. The volumes are recognized by their +// source, so the host path volumes customized in the node-agent daemonset are removed as +// well. The other volumes, including the ones customized by users, are kept as is. +func filterVolumes(volumeMounts []corev1api.VolumeMount, volumes []corev1api.Volume, excludeHostPath bool) ([]corev1api.VolumeMount, []corev1api.Volume) { + if !excludeHostPath { return volumeMounts, volumes } - excluded := make(map[string]struct{}, len(excludedVolumes)) - for _, name := range excludedVolumes { - excluded[name] = struct{}{} + excluded := make(map[string]struct{}) + retainedVolumes := make([]corev1api.Volume, 0, len(volumes)) + for _, volume := range volumes { + if volume.HostPath != nil { + excluded[volume.Name] = struct{}{} + continue + } + + retainedVolumes = append(retainedVolumes, volume) } - var retainedMounts []corev1api.VolumeMount + retainedMounts := make([]corev1api.VolumeMount, 0, len(volumeMounts)) for _, volumeMount := range volumeMounts { if _, found := excluded[volumeMount.Name]; found { continue @@ -122,14 +127,5 @@ func excludeVolumes(volumeMounts []corev1api.VolumeMount, volumes []corev1api.Vo retainedMounts = append(retainedMounts, volumeMount) } - var retainedVolumes []corev1api.Volume - for _, volume := range volumes { - if _, found := excluded[volume.Name]; found { - continue - } - - retainedVolumes = append(retainedVolumes, volume) - } - return retainedMounts, retainedVolumes } diff --git a/pkg/exposer/image_daemonset_test.go b/pkg/exposer/image_daemonset_test.go index 0941d44f7..21ae104df 100644 --- a/pkg/exposer/image_daemonset_test.go +++ b/pkg/exposer/image_daemonset_test.go @@ -1,3 +1,19 @@ +/* +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 exposer import ( @@ -37,7 +53,7 @@ func TestInheritedPodInfoAgainstRealDaemonSet(t *testing.T) { } // fs-backup resolves pod volume data through the kubelet pod directory, so it keeps them. - fsBackupInfo, err := getInheritedPodInfo(context.Background(), client, "velero", "linux") + fsBackupInfo, err := getInheritedPodInfo(context.Background(), client, "velero", "linux", inheritHostPathVolumes) if err != nil { t.Fatalf("error to get inherited pod info for fs-backup: %v", err) } @@ -47,7 +63,7 @@ func TestInheritedPodInfoAgainstRealDaemonSet(t *testing.T) { } // The data mover pods access data through PVCs, so they must not get any host path. - dataMoverInfo, err := getInheritedPodInfo(context.Background(), client, "velero", "linux", hostPathVolumesOfNodeAgent...) + dataMoverInfo, err := getInheritedPodInfo(context.Background(), client, "velero", "linux", excludeHostPathVolumes) if err != nil { t.Fatalf("error to get inherited pod info for data mover: %v", err) } diff --git a/pkg/exposer/image_test.go b/pkg/exposer/image_test.go index d93a667ba..a7672b344 100644 --- a/pkg/exposer/image_test.go +++ b/pkg/exposer/image_test.go @@ -1,5 +1,5 @@ /* -Copyright The Velero Contributors. +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. @@ -209,9 +209,13 @@ func TestGetInheritedPodInfo(t *testing.T) { MountPath: "/host_pods", }, { - Name: hostPluginsVolumeName, + Name: "host-plugins", MountPath: "/var/lib/kubelet/plugins", }, + { + Name: "customized-host-path", + MountPath: "/customized", + }, { Name: "scratch", MountPath: "/scratch", @@ -233,13 +237,23 @@ func TestGetInheritedPodInfo(t *testing.T) { }, }, { - Name: hostPluginsVolumeName, + Name: "host-plugins", VolumeSource: corev1api.VolumeSource{ HostPath: &corev1api.HostPathVolumeSource{ Path: "/var/lib/kubelet/plugins", }, }, }, + { + // A host path volume added by users. It's not named after any + // well-known volume, so it can only be recognized by its source. + Name: "customized-host-path", + VolumeSource: corev1api.VolumeSource{ + HostPath: &corev1api.HostPathVolumeSource{ + Path: "/mnt/customized", + }, + }, + }, { Name: "scratch", VolumeSource: corev1api.VolumeSource{ @@ -297,7 +311,7 @@ func TestGetInheritedPodInfo(t *testing.T) { namespace string client kubernetes.Interface kubeClientObj []runtime.Object - excludedVolumes []string + excludeHostPath bool result inheritedPodInfo expectErr string }{ @@ -433,7 +447,7 @@ func TestGetInheritedPodInfo(t *testing.T) { }, }, { - name: "no excluded volume, host path volumes are inherited", + name: "host path volumes are inherited by default", namespace: "fake-ns", kubeClientObj: []runtime.Object{ daemonSetWithHostPath, @@ -446,12 +460,12 @@ func TestGetInheritedPodInfo(t *testing.T) { }, }, { - name: "host path volumes and their mounts are excluded", + name: "host path volumes and their mounts are excluded, no matter how they are named", namespace: "fake-ns", kubeClientObj: []runtime.Object{ daemonSetWithHostPath, }, - excludedVolumes: hostPathVolumesOfNodeAgent, + excludeHostPath: true, result: inheritedPodInfo{ image: "image-1", serviceAccount: "sa-1", @@ -460,12 +474,12 @@ func TestGetInheritedPodInfo(t *testing.T) { }, }, { - name: "excluding a volume that doesn't exist doesn't affect the others", + name: "excluding host path volumes keeps the others when there is none", namespace: "fake-ns", kubeClientObj: []runtime.Object{ daemonSetWithNoLog, }, - excludedVolumes: hostPathVolumesOfNodeAgent, + excludeHostPath: true, result: inheritedPodInfo{ image: "image-1", serviceAccount: "sa-1", @@ -513,50 +527,12 @@ func TestGetInheritedPodInfo(t *testing.T) { }, }, }, - { - name: "excluding all volumes results in empty volumes and mounts", - namespace: "fake-ns", - kubeClientObj: []runtime.Object{ - daemonSetWithNoLog, - }, - excludedVolumes: []string{"volume-1", "volume-2"}, - result: inheritedPodInfo{ - image: "image-1", - serviceAccount: "sa-1", - env: []corev1api.EnvVar{ - { - Name: "env-1", - Value: "value-1", - }, - { - Name: "env-2", - Value: "value-2", - }, - }, - envFrom: []corev1api.EnvFromSource{ - { - ConfigMapRef: &corev1api.ConfigMapEnvSource{ - LocalObjectReference: corev1api.LocalObjectReference{ - Name: "test-configmap", - }, - }, - }, - { - SecretRef: &corev1api.SecretEnvSource{ - LocalObjectReference: corev1api.LocalObjectReference{ - Name: "test-secret", - }, - }, - }, - }, - }, - }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) - info, err := getInheritedPodInfo(t.Context(), fakeKubeClient, test.namespace, kube.NodeOSLinux, test.excludedVolumes...) + info, err := getInheritedPodInfo(t.Context(), fakeKubeClient, test.namespace, kube.NodeOSLinux, test.excludeHostPath) if test.expectErr == "" { require.NoError(t, err) diff --git a/pkg/exposer/pod_volume.go b/pkg/exposer/pod_volume.go index 0526b2c5e..5d6de1831 100644 --- a/pkg/exposer/pod_volume.go +++ b/pkg/exposer/pod_volume.go @@ -1,5 +1,5 @@ /* -Copyright The Velero Contributors. +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. @@ -365,7 +365,7 @@ func (e *podVolumeExposer) createHostingPod( clientVolumeName := string(ownerObject.UID) clientVolumePath := "/" + clientVolumeName - podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS) + podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS, inheritHostPathVolumes) if err != nil { return nil, errors.Wrap(err, "error to get inherited pod info from node-agent") } From 11545ee63cecab818e64ccb5ae75a0bbedc6df05 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Mon, 3 Aug 2026 11:27:39 +0800 Subject: [PATCH 153/194] Fix logs, CRD, and GetDataMover for CBT features. Modify the logs. Modify the CRD's data mover's comment. Modify the resource policy's GetDataMover for default data mover case. Signed-off-by: Xun Jiang --- changelogs/unreleased/10106-blackpiglet | 1 + config/crd/v1/bases/velero.io_backups.yaml | 2 +- config/crd/v1/bases/velero.io_schedules.yaml | 2 +- config/crd/v1/crds/crds.go | 4 +- .../bases/velero.io_datadownloads.yaml | 2 +- .../v2alpha1/bases/velero.io_datauploads.yaml | 2 +- config/crd/v2alpha1/crds/crds.go | 4 +- .../resourcepolicies/resource_policies.go | 14 +++++-- .../resource_policies_test.go | 41 ++++++++++--------- pkg/apis/velero/v1/backup_types.go | 2 +- .../velero/v2alpha1/data_download_types.go | 2 +- pkg/apis/velero/v2alpha1/data_upload_types.go | 2 +- pkg/controller/backup_controller.go | 5 +++ pkg/controller/backup_controller_test.go | 20 +++++++++ pkg/controller/data_download_controller.go | 6 +-- pkg/controller/data_upload_controller.go | 6 +-- pkg/datamover/backup_micro_service.go | 10 ++--- pkg/datamover/restore_micro_service.go | 8 ++-- pkg/util/datamover/datamover.go | 3 ++ 19 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 changelogs/unreleased/10106-blackpiglet diff --git a/changelogs/unreleased/10106-blackpiglet b/changelogs/unreleased/10106-blackpiglet new file mode 100644 index 000000000..404046c1e --- /dev/null +++ b/changelogs/unreleased/10106-blackpiglet @@ -0,0 +1 @@ +Fix some issues for CBT features: logs, CRD change, GetDataMover. \ No newline at end of file diff --git a/config/crd/v1/bases/velero.io_backups.yaml b/config/crd/v1/bases/velero.io_backups.yaml index 96c425caa..9695d3001 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -59,7 +59,7 @@ spec: datamover: description: |- DataMover specifies the data mover to be used by the backup. - If DataMover is "" or "velero", the built-in data mover will be used. + If DataMover is "" or "velero", the default built-in data mover will be used. type: string defaultVolumesToFsBackup: description: |- diff --git a/config/crd/v1/bases/velero.io_schedules.yaml b/config/crd/v1/bases/velero.io_schedules.yaml index 0b32b298b..876cdf106 100644 --- a/config/crd/v1/bases/velero.io_schedules.yaml +++ b/config/crd/v1/bases/velero.io_schedules.yaml @@ -98,7 +98,7 @@ spec: datamover: description: |- DataMover specifies the data mover to be used by the backup. - If DataMover is "" or "velero", the built-in data mover will be used. + If DataMover is "" or "velero", the default built-in data mover will be used. type: string defaultVolumesToFsBackup: description: |- diff --git a/config/crd/v1/crds/crds.go b/config/crd/v1/crds/crds.go index 60395b71e..d910e72f3 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -30,14 +30,14 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8aTɡS\xf7\xe7\u074b!%[\x96hk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L4\xea\x11\x9dW\xd6,A4\n\xff 4\xfc\xe5\xe7O\xbf\xf8\xb9\xb2\x8b\xc3\xebٓ2\xe5\x12V\xc1\x93\xad\xd7\xe8mp\x12\xdf\xe1N\x19EʚY\x8d$JAb9\x03\x10\xc6X\x12\xbc\xed\xf9\x13@ZC\xcej\x8d\xae\xa8\xd0̟\xc2\x16\xb7A\xe9\x12]4\u07b9>\xfc0\x7f\xfd\xf3\xfc\xa7\x19\x80\x115.a+\xe4Sh\x1c6\xd6+\xb2N\xa1\x9f\x1fP\xa3\xb3seg\xbeA\xc9\xd6+gC\xb3\x84\xf3A\xd2n=\xa7\xa8\xdfFC\xeb\xce\xd01\x1ei\xe5\xe9\xb7\xec\xf1G\xe5)\x8a4:8\xa1s\x81\xc4c\xbf\xb7\x8e>\x9f\x9d\x15\xc0\"\xe9H\x99*h\xe1F\xba\xec\xdbK\xdb\xe0\x12\xa2f#$\x963\x80\xb6\b\xd1R\x01\xa2,cY\x85~p\xca\x10\xba\x95ա6'?_\xbd5\x0f\x82\xf6K\x98w\x85\x9fK\x87\xb1\xe6_T\x8d\x9eD\xddDٮ\x96o*l\xbf\xe9\xc8\xceKA86\xc6E\x9d\x9fc\xfdrl\xf0\xc2ʹF\xd0;K\x16=9e\xaa\xd9Y\xf8\xf0:\x95B\xee\xb1\x16\xcbV\xd66h\xde<|x\xfcqs\xb1\r\xd08۠#\xd5\x153\xad^g\xf6v\x01J\xf4ҩ\x86b\xdf\xfc]\\\x9c\x01\xb0\x83\xa4\x05%\xb7(z\xa0=v5Ʋ\x8d\t\xec\x0eh\xaf<\xc3\xe6УIM\xcb\xdb\u0080\xdd~EI\xf3\x81\xe9\r:6\xc3\xd8\a]rg\x1f\xd0\x118\x94\xb62\xeaϓm\x0fd\xa3S-\b=AD\xd1\b\r\a\xa1\x03~\x0f\u0094\x03˵8\x82C\xf6\t\xc1\xf4\xecE\x05?\x8c\xe3\x93u\b\xca\xec\xec\x12\xf6D\x8d_.\x16\x95\xa2n^\xa5\xad\xeb`\x14\x1d\x17q\xf4\xd46\x90u~Q\xe2\x01\xf5«\xaa\x10N\xee\x15\xa1\xa4\xe0p!\x1aU\xc4DL\x9c\xd9y]~\xe7\xda\t\xf7\x17nG@\xa7\x15\x87\xec\x0exx\xea@y\x10\xad\xa9\x94\xe2\x19\x05\xde\xe2ҭ\x7f\xdd|\x81.\x92\x84T\x02\xe5,:\xaaK\x87\x0fWS\x99\x1d\xba\xa4\xb7s\xb6\x8e6є\x8dU\x86\xe2\x87\xd4\n\r\x81\x0f\xdbZ\x11\xb7\xc1\xef\x01=1tC\xb3\xab\xc8i\xb0E\b\r\x8fN9\x14\xf8``%j\xd4+\xe1\xf1?ƊQ\xf1\x05\x83\xf0,\xb4\xfaL=\x14N\xe5\xed\x1dt,{\x05\xda!sn\x1a\x94\x8c,\x17\x97U\xd5N\xc94S;\xeb@\x8c\xe4/+\x95\xa7\x00^\x89D7d\x9d\xa8\xf0\xa3M6\x87BSm\xc7\xebm\xceP\x171\xd3V\xe2\x04\xcc\vf\f\xd2^P\x8f\fH(s\xe2\x94l\x927\x90\x89\xe8\bf\n#\x8c\xc4\xf7\xb1\x1f\x8d\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xad+\xc6͒\x89v\x1f\x8f\x8c\xf3\xd0\xcb<\xe4\x1d\r\x1dC\xf5\x9d\xfc\xa0\x9d2\x9dD\x8b\x01X-\xd2<\xee\xc0\xec@\x91R֮\xc0\x86q \xfa\xa0\r\x14\x9e0az\xf5\xf8DzB\xa3¹\a\xa1-]=\"}\xe4E\xc59]s\xb8\"FU0@\x9b\xb5\x94\x1c\xa8\x98 \xcegІe\xe7 \x8d\x83\x14!\x8c\xf2\x1f:\x14@o\x82>\x00\xa1\x11Оf\xd6m\xe1\xbcE\xd8.U\xa2c*\x15dv:\xbb\xf2\xd3$\x03\x8eS\xb3\x90\x84K\xb1\x05\xe5z\xb7V/\b\x98\x02+p9\xb13\x90\x02n\xa7Y\xb2\xa9\xec\xe4tI\xacv\x0f\xca\x00\x13\xda\x00\x8d\b\xe7\x13\xf8\x03_\xacu\x86\xfc\xday\xa4\xb7ֱ\xce\xc3B\xa37\x9d\xa4\xf0\xe9\xfd(D\xef\xb6p\x96\xa1w\xec\x1d\xe1%:\xf411m\xbc\x17;5\xe1jò\xd2\x0f\xbbqKF\xed\x81\x06c\x1b]\xfc\xe9b\x81\x1c\xee\xf6\xda\xedC\x13\xaa\xa0&K\xb2݄\xa24\x87~mf\xa0\x88PqԞ$\xf2\x93*E\x0f\x03ܬ\x17Fg\xe4\xe7\x10\xcc#\x8e\x8aP\xed\x85yz\xdc\xef?3W\xcf\xc3G\x8d\x01\x02ʄ\xe5\x9f]\xabwا\xdd\xc2֒MH\x13\x81\xe7\xfc:\xc8q\xcd:\u00ad߉Xg\x91\xf9!!\xafe\xcb\v\xef?$\xa5vR>LQ\xe7\a[\xa7Y-\x92\f\x03Qd\r;\xbagRyԛ\xa9\x16\xbe@V\x99\xa8\xd6SCr\xb6ـ\xb2p\xca\x1dՠ]\xfc`\x98 \xc3\xeb\x1a\xd22#яGx4\x8c\xb4lḂ\x86n\xfd\x88\xe3Y2\x14;P\xeb^\xe3d\x9c\xb3=\xcb+\xcaq^\xa6\"s\xf8\xd0z\\1+3\xc2\xe4ޘ\xa3\x92\xe9\x8as\b\x02R\x96I\x9d%\xa4\x14`}\xde®\t\xfaU\x871_S\xeb\xab\xc8!\xec\t2KU\x1c\xb4\xef*G7\xb2\xb1\x19\x8b\x86)\x18\xa1!\x9c\xae\x81\x13\r\x1c2#U\x9c\"S|v%\xc5\b\x0e\x102b\xf9\xba+\x8d\x06\x81\x11\x90\x04\x97p;\x96휫g\x85\b\xe1\x90\\\x82u\xf8\f\xa1e\xc9#\xd3ESF\x99\xef;\x19\xd3\xf5\xa6Lh\xfd1\xbc\x98\xfe7%\xc1f6%J\xdaF\xbf\xba\x94\xad\xc5!\xbe\xa6m\xca?'a\x83\xe5?AhG\xb4\x9f`\xb8,Y\xa6\a\xe5\xd6R\x95\x81\xbe\xb4\xee\x14z:\v\xc2L\xf8uJ\x13:>W/\x8a\xd8!\xc2\xd7͛\xf9B\x9fȚ\x14\x9dx&\xc6\xd4]\xfc\x03\xf2\x05\xa7\x8c[?c$\xf3\xe4\xa7v\xab\x05a\x9b\x9a\xe8\xf9\x82l\x187\xa0\x8e\xa8\x7f\x92\xa9\x0f\x9c9\a1Rf=\x82\xfb\x1a&۽\xffb]0\xddl\xee%\xd2帱sd\x83\xb7ߝ\x9e'\xe0\x12\x8c\xef3\x17m\u0557\xb8bj\xff\x82\xae\xd5ۏ\xef\xe2\xeb\xabvI\x90\xbc\x1e\"\x13J\xe7\xca\xdb#\x8c\xda\xe3\xf3.|\xf8\x82>P\xbd\x00r\xb1\xea\x05\xa1\xe4\x01\x0e\xceu\xa1\x82X\xfe\xd0P9\xa1{\x05\xb8Y\x85r\xf6\x00\a\x04\x13\xdf}\xea\x97Tip\xe5\x01\x0e)Վhh\xc7Ĵ\xdfU\xb3t\xb2? !p\xd3!U\f\\\xf1\xaa\x10\xd9뉗D[\x12J\xa0\xfd\th&\x89J\xbb\x8f\xf6\xf6-J\xc0w\xda\xf1\xd2j̎\x95hV1\xe2 7\xc9\fu\xe5\x9er\x96\xd7\x1d9\x1dY\x89\x05\xf9(\x8d\xfd\xe7\xfd\x17\xa6\xfd\x0e\xef;\t\xfa\xa34\xf8˳P\xd4\r\xfc9\xe9\x19v|,B\xce\xca[\x82\xb5\xf7(ݜf\xa5\xad\xa6=\xd3d%\xecrő$\xb1+\u070evݹ\x8e\x8aJ\xe3\xf6\xa2\x90b\xe9\xc26\xb1\x9e<\xbd\xa5\xea\x90\xfbɝ\xfa\x0e\xef\xecdᾸMqN3\xc8\xc3v\r\xee\xd6R\x03[\x96%\xf6W\x80\xda\x02)\xad\tO\x93\x88D\xc3걙'>i\xb3w\xbb|Y>\xd4\xc9\x0fK;\xe5,=\x04#\x8b\x04\x1ax\u06ddO㳴:\x9bP+H\xc2dՁ\xcd\xdc\xe1\xaa)Dy\x029p\x16G\x17g\x92\xbb4\xcf15\x88\xf2\x9b\x193\xca\fY\x98k\x1aZcwSpAq\xab\xe5\x7f\xecL\x8b\xda\xf4\x7f\xa4\xa4L\xe9K\xf2\x16s}8t\xbe\xf9\xa0Y\vLB\x97\x98\xabc\xe5gO\xb9\x9d\xfb\xad\x01\x17\x04\xb8\xf3\x04\xe4\xa6\xe7\x17-\xc8\xe3Nj7mכ8\x17\x0fpp;\x86\x93]\xb6\x8d\xcc\xc5J\\8\x1f\xa2g0j\x87C\n~ \x17\xf8\xed\xe2)\xaeT\xa2\xa4&V\xeb\x88hA\xcb4\t\xc5\\\xabTG\xdd.X\x83\x13b\x1b\xd69D\xd6\xc9\x1e\xc36IDK\xa9#\x1b\xf9\x03C\x99\x10\xde\x1b\xa9\x8d\x8b\x97u|\xe6h@M\x86 \x1a\xa1\x1b\x97\xd8%U\xc8±Fy*\xf4\xdb.w;\xd0\xe0\xf7+|`\xce\x01\xb5+\xbb\x8bF\xbf\x9d\xb5\xbfp\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xe6\x8bH\x96H\x1b\xf7:\xe6H\xdd*\xc9媌\x87@CIwy-!f\xae\x17\xde\x7fi\x05D\xad\xeeۿ\xa7dl\xee\xb8\bfY\x16\x05=\xce\xdfJ\x1a\xe2\xb5k\x19\xb4\xc1\x03r\x8b\x0f\xb5\xad\xd0\x12\xa4\xce\xe5\xb5\x00~\r\x8eB\xc1\xc4\n; o\x9e\xc1\xb1\xf064\x96l\x12+\xa7\xb9\xb2ס\x93\x86;\xf5\x0fN\x95K\x89[\x05\n:\xcc\xebG\xd5\xd1\x0f\x15Ҵ\x02\x123\xdc\xcdR\xe6\xdfi\xb2aJ\x9b\xf6\x10\xf4@\x9aJ\x14\xcc̅\x97x\xaf\xd4I\xeb\xaeO\xae\xe5Q\x02\x99\xcf[s\x84I\xc4\x1c\xf7\x97\x80\xb0\ra\x86\x80\xc8d%0\x80c\xf5\x18\xbbp\xc4u\x16\x96\xa5*I\x9a\xf6\x93\xc1\x1c\xb4XY\xa2\xa401\x1a\xe9iW\xff@Y?Q-Vf\xb2\xcd\fe\xb1\xc5\xcai:\x11R\xdcڙ\x8a\x05\xfd\u008a\xaa \xb4\xb0<\xc2ɜ\x15\xd0ez\x93\xf8f[\xe04a\xa4\u0558\x92\x83\x01\x9f\xbc\x968\x86L\n\xcdr\xa8'W/\bR\x10J6\x94\xf1J%Z\xc0Y䝳\x14\xf1\x96\xe0|k\x8c\xb4ΗH\x8a\x84hn\xa2\xaf8n\x8dK\x95\xee\xf1M\xb9Y\n\xe6{Y\xa5b\x12\xd3\x02\xcf\xech\xf9DJ*\x0e\xdf<\xadԡ~\xf3\xb4\xc6\xca7Ok\xa2|\xf3\xb4\xbeyZ)5\xbfyZ\xdf<\xadv\xf9\x97\xf0\xb4\xa6F\xe4\x0e:\x0e|\x9c\x1cE\xc2V\xf5\xd8\x10G\xe0\xfb\xe4\n\x9f\x03\xfe\xa4\\\xccU\x1cT$\xf1\x7f \xad;f\xb4\x9aɣNδZ\x13dޝ\xbb\x9ap%\x9f\x90u\x1f:=_\xd6\xfdj\x14♲\xee\xfd\xb0\xa7}\xec\x93r\xee\x03Q\xe6eg/|\xa2F\x014\x84\xd5\xdd6|\f\xaf!\t\x99\xe8\xff\x85\x13s{Ycg\x94\x8fg\xcf\xe2O\x96\x91(K/\xfet\xf1\xf5\x91\xff<\x04\x1f$q\x9fv\xfe\xe0w\x04\xaa]\x81\xb6\xd3ºYx_\xa7\x18\x9fEnS3\xf1k\"F`uE\xf2\x88\x8a_\xab-0P|*\xfd\x8c\U001044ea\xab\b\x9c\xa4\xb3\xaaT\x1fD\xb6SR\xc8J\xfb\xa8\x84\x85\xf56s'\xfd\x03Ș\xb0F5\xfc?\xc8NV\x91L\xf0\x11\xf2Md\x04N#\xdfI\x0e\xf4\x9b\xd0`\xe8\xfe\xcde\xf7\x8b\x91>Up\xe8l\xf3\xe3\x0e\x04\uec0bm\xfb\x00@\xb8\xa8\xc1\xdfXp,`\x11@R\x11\xc1\xb8\x93\xbc\xfa\x9a\x87\xb6ܑO\xa5\x8b=\xcd\xf6;\xc6c*iɄ'\xa7\x10vS\x04\a\xfcҹ\xbb\xddg92\xf1\xbb\xa4\x06\xceO\bL\x89\x88M$\xff\x9d\x90\xf2\x97\x98[\xfc\xe4\xed\xf9\x94\xa4\xbe9+\xe6gK\xe0;\x7f\xda^\x12}\xa6S\xf4\xe6P\xe7\xd9\xd3\xf1^0\t\xefeR\xef\x12\x13\xeeΗ9\x9f\x16\x8f=)sl:t0\x9c47\x99*7\x19Z\x98Bl6J\x93)ps\x12\xdf&\xb9\x93\xa6f/\x96\xda\xf6b\tm/\x9b\xc66*E\xa3\x1f\xe7$\xaa\xc5\xef\xeb!\x93\x93m\xef>\xb5^\x85sJ\\r,nP\xe9\xf8K)ǩl\x93\xaa\xe3n\x9f\xb4\x1e\xfct\x04\xc3\njpE_ȧ/*nX\xc9q\xe3w\xcf\xf2hp\xc4\xec\xe0P_\xf8\xf1\xabģ\xb2\xfe\xe6\x9aO\x9fk-\xbb\xb8\xf4\x9b\xe8\xd4\xfc\x9ef\xbb#\xe8;\xaa\xc9F\xaa\x82\x1arQoX\xbev\xc0\xed\xdf\x17\x97\x84|\x90u\x0eG\xfb\x1e!͊\x92\x1f\xec\n\x85\\\xb4\x1b\x9c&\x01Qi\v\xbd\xddHβ\x88\xef\x16\xbdK\xcaU\xee]\xee\x817\\e\xed\x14\x87\xd2V\x8c\xbbn\xe8\xe6u\xaf\xec\xdcH\xce\xe5\xe3\xdcXE\xc9\xfe\x82\x97\xb3?!\x9a\xf5\xf6f\x850\x82x\xe0m\xefu2Y\x8d\xcd\x1a\xec\xb4\xdc\xe09\xa4\xfb\xabM\ab7/\xb3}\xcb1\xe4\xeeB\xeb\xe0\x16xәIk]nVn\x1cC\xbdX\x99\xa1\xe2@$f\x00\x99\x1dS\xf9\xb2\xa4\xca\x1c\\bɢ3\x860\x97\x8eE\xa3\x06g\x8f\xfe%\xddQ\U00086ef9qG\xf5Pv7\xa9\x8fiw\xca8\x86O[N\x9e\xb3<\xe38\x86ݒ%R*\xf2s4S\xedlQ>\xedoR\xfeY\xee\xe1]4\xda\xd7!\xcf\xedQ\xf5H:Y\x80\xe8.\t\x1e̪]\x03^ \xdc\xff\xf4\x84\xfc\xb0е\xbf\x03\xf6\x94@\xd9m\x17D\x04\xbfp#n\xe8,f\x9f\xf0&\xff\x03\xb9\xb9\xc75Zmڼ\x8a\xfa5Z\b\x95\x85\xcd\xeb\b\x1c\xdf\xe0\xfb\xf3\xa7\xd2i#\x15\xdd\xc2O\xd2]\x96>\xc5\xf6n\xed\xce%\xfa\xde\xeb\t\xf9\xaeAib\x17\x06\xfbkۏ\x8059\xea\xbdK\x98\xed(g^+m\f?\x85\xefww?9\xac\f+\xe0\xf2]\xe5\xd23\xacM\xd4`I\x1c\xb0u\x90\xd6\xf6\xbf;\xf9\x88\x97\x15\xc7\xe3\x98\xe1\xf1\x8b\x06\x19\x05\x98\x1c\x8f)\x93\xb3P\xaaJ.i\x0e\xeaZ\x8a\r\xdbN`\xf7K\xa7\xf2\xd14\x9b\xe1\x8f\x1e\xb9z\x8e\n\xf0Ϝ3a}\x1e\u0381\x7f`\x1c\xb4\x1bV\x82\x01\xbe鷪\xedqU\xac\x9d\x0f\xb7\xb1\x1f\xeb\x0e\x06\xe68\x87\x16\x86\xa2KP\u058brA\xebJ\aY\x1dF\xbc\xe1\b\x13\x06\xb6\xd0_\x05\x8eX`w\v6N\x9f\xc1\x9c\xe0Z\xe6\xc7X|\xab\x83\xfc\xfdp\xcb#N\xb6B^\xb1\x1b\x02\x9d\x13rs\x7f\xadI%r\f\x17\xdf\xff\xe5v\x96\xd4\xed;7\xed\am\x9d2\xaa\xf7\xf1V-\xe7\xb8e/\x9cw,7\x11\x04\x86\xe0\xb4\x1etyd\xc6_4vޛa\x87\x96ڡ\xf1%\xa3\xeft\r\x13s[\xf1\xfd\x95>\x11\xfaί\x8bV\\Y\xef\x1f\x96\x16\xc4i^\xeb\xd0\xeb3\xddy\xe1iF\xee\xfav5\x04\xee\x14\x13\xd7\x7f\x9e\xe6\x89j\xdcG\xf7I&\xad\x8f\xee,\x83\x16\x81X\xcb\xf8\xf9qGU?\xed\x12zl\xe9\x1c\x8e,\x9c\xf9\xa3\x9c\xfb\x83\x99\x05hM\xb7\xe1\xf6\xf9G\xbb\xf4\u0602\x00\x17\x9es\x9b'\x11\xa0\xcd)\xbe\xee\xdd\xebNehf*\xea;\b\tɭZ\xdfi\xc2e\f*>@\xc3\u0093oaM6\x93P_J\xa6R\xd6p\xef늖6\xe8\t#w\x9aG\xfa\x80\xb3->Ae9\xb7\xa5jM\xb7\xb0\xcc$\xe7\x80ֺ?\xae\xe7\xd4u\x7fV\xf23P=\x89ڇv]\xbf\x03\xe8\xb8\xed6\xbe\xa9K\xcf\xc7g\xd8\fSм\x88\xd8\x1b\x90Ďg9ʎ\n\xd1\xe7\x02\xfb#m\xd7\rZ\xe7Ͳ\x8f\xf3\xfa\xd7\x02\x17\xcd\v`\x91q\x16\xf4W\xa9\x16\xa4`\xc2\xfeCE\xee6\xf0B\xe3Y\xe3\xdfI\xf9p\x1bqb{\x83\xff\xa1\xae\xd8lu0ᆍ\a\\ײ\xf2\xbb\xef\xb5C\x1b\xdfV\xc1\x97\x04μ\xdcD\x98#\xf3A\x0f\x9d\xc1\x88\xee\x0f\x1dH\x93S\x81\xeby\x00\xd6mx\x92\x8e\xf3\xc3\xe2\x18\xf2\xd1\xf3\x97\r\xec\xd6K\v\xde\rh\xeeO\x18\xe8(\xecHE\x81\xd4\x17u\xb4\r\xfa)\xab^O\xe6!g\xb2G\xe3\x1f\x9a\xdaCtt\xc3l\xb9{\x03\bv\x9c\xc0\xf3.\xd8\xf1Y\x8d\t\u1ff1u\xea\xbb\x16Z\v\xb7\x90%6\x18\xa5\x1bz\x99\xef#\xf4\xb7+\x96\xe4\xaf\x15T\x11\x1a,\xc3Cv\xb7\x86\xaa~\xc8\xd7\x1dۇ\x1c3:P\x1b#UV\xe2Fɭ\x02\xdd\x17\xd6%\xf9\x1be\x86\x89\xed\a\xa9nx\xb5e\xe2\xd3\xf0\x11\xa5\xb1\xca7T\x19f\x85ݍ'6P&(g\x7f\x8fٵ\xf6\xc7i@׃\v\xac%I\x18\xc6Їw`}\xdc\xc1\xb8@Ԅ\x96\x9e\xae\xa7\xf8+\x81'S6\xb5\xf6%\x1a_$t{I>ʨa\xf0\xe9P\xac\vӺd\xa0\xcd\x126\x1b\xa9\x8cۭ^.\tۄ\xe0\x83\xb59\x187s\x8f\x8e\x12\x16\xdbf\xae\x13M\x9a\xe9\v\x83\xde\nga\xbcz\xbf\xa0\a\xb73E\xb3\xac\xb2\x1e\xd6km(\x8f88O2\xfc\x18\xe5\xf9\x1e\x1f\xd8\xfc\xe5I;y\xab6\xa0~\xd0\x11\xfbq$\xc5\xcb?\x9c\xd7\xc7-\x8a ȣb\xc6X\x9fJ\x8e\xa4\x12xR\x19\xeb[qN\xb4%\xf5I\xd1G\xe2\xcc\xe8j8%'\r\xe5\xbb\x1aʐy\xf6X\xe3K\x92\xf5+\xa6>\xfb\xc8ײl\xcevTl\aoT\xd8)YmwA\x92\a\x9ci\x92W\x80\xc1Z4):\xbc\x10m*%Z\xa9\x04#\xc7\xd4I\x10\x06\x1c.\xcd\x1e\xf0\xbdU\xf7\x02\xb3\x7fz\xfb\xb5\x7f\xb3e\xb9Q\xb2X\xfa~1\x96\xba\xf0;\xf9\x8aI빘]\x94\xea\xc4y\xed\xfeY\x04\x94\x84\xb2\x04A\xa8\xf6='\xdclu\xf24\xf5\x9b\x9d\x1an\xa4f\t\xde~\x94\xe3\x7fm\x03\b\f/\xc3\xdf]f\xf8\x15\f\xf6\x19\xc3㓿2\x00\xf6T\x18\xb7\x9c\xa8\xa7\xc8\v7\x89]\xccZ\xc8h;\xb1=)Hsہ0\x11\x9f\xc1\xee\xe2,\xba\xf5\xe9\x1a\xee\xe2\xb2k\xff\\l\rxA4\x13\xe1\x05s\x97\xfa\xe1\xa4?\xba\x13(\xf0aM\xa9\xe2٘\xe3\x01\x97.B/\x1bk\xd9מ\xc4\xfb\x93\x97\xe2\xf7G0\x8e\x0e\xa1\xe3;\xaau\x95\xb0|\xfe\x03\x8b\xed\a`\x1aofQ\xf9\xe3\xef~\xb8|\x9f\xb4ԋSdl凋\xba\xe1%\\\xf7\xdd\xd4\x1b\x0eV\xdb4@wQ9K\xe7\xf6g\x8c\xa6\x9d3\x94\x16\xde\xea?O,i\x7f\xc6 ڳE\xd0\u038b\xf2#\xc5\a\xadO\xd2ڿ\xf9\xb6\x91\x10\x9a\a{\xee Z+\x86\x16\x06\xfe\xa2Q\xb4\xe8\x9c\xdb\xfb\x11\xedt\u07b2\x16\xbe'\xff\xcb\xff\a\x00\x00\xff\xff\x11\r8\xff\x9b\x84\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec=]s\x1c)\x92\xef\xfe\x15\x84\xeeav7\xba\xe5u\xdcG\\\xe8\xcd#\xdb;\x1d3ck-\x8d\xf6\x99\xae\xca\xeefDA\rP-\xf7\xde\xdd\x7f\xbf \x81\xfa袪\xa8VK\xe3\xdd5/\xb6\xba !?I\x92\x04\x96\xcb\xe5+Z\xb2{P\x9aIqEh\xc9\xe0\x8b\x01a\xffҗ\x0f\xff\xad/\x99|\xbd\x7f\xf3ꁉ\xfc\x8a\\W\xda\xc8\xe23hY\xa9\f\xde\xc1\x86\tf\x98\x14\xaf\n04\xa7\x86^\xbd\"\x84\n!\r\xb5?k\xfb'!\x99\x14FI\xceA-\xb7 .\x1f\xaa5\xac+\xc6sP\b\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xa0=\xda\xd6\x1d2K&\xda}=2\xceCo\xf3\x88\xe0\x80:\xc6\xea;\xf9A;\xa5:\x89&\x03\xb0Z$z܁ف\"\xa5\xac]\x82\r\xe3@\xf4A\x1b(<\x81\xc24\xeb\xf1\x89\xf4\x84ƅs\x0fB[\xfazD\xfaȋ\x8as\xba\xe6pE\x8c\xaa`\x806k)9P1A\x9cϠ\r\xcb\xceA\x1a\a)B\x18\xe5?t(\x80^\x05}\x00B#\xa0=ͬ\xfb\xc2y\x8b\xb0]\xaaD\xc7T*\xc8\xec\xb4v\xe5\xa7K\x06\x1c\xa7h!\t\x97b\v\xca\xf5n\xad_\x100\x05V\xe0rbg\"\x05\xdcN\xb7dS\xd9I\xea\x92X-\x1f\x94\x01&\xb4\x01\x1a\x11\xce'\xf0\a\xbeX+\r\xf9\xb5\xf3Lo\xad\x83\x9d\x87\x05GoZI\xe1\xd3\xfbQ\x88\xde}\xe1,C/\xd9;\xc4Kt\xeccb\xdax1v\x8a\xc2U\x87e\xa5\x1fv㞌\xda\x05\r\xc66\xba\xf8\xd3\xc5\x029\xdc\xed\xb5ۇ&TAM\x96d\xfb\tEi\x0e\xfd\xda\xcc@\x11\xa1\xe2\xa8=I\xe4'U\x8a\x1e\x06\xb8Y/\x90\xce\xc8\xcf!\x98G\x1c\x15\xa1\xda\v\xf3\xf4\xb8\xdf\x7ff\xae\x9e\x87\x8f\x1a\x03\x05\x94\t\xcb?\xbbf\xef\xb0O\xbb\x05\xae%\x9b\x90&\x02\xcf\xf9w\x90\xe3\xdau\x84[\xbf\x13\xb1\xce\"\xf3CB^˖\x17\xde\x7fHJ\xed\xa4|\x98\xa2\xce\x0f\xb6N\xb3j$\x19\x06\xa4\xc8\x1avtϤ\xf2\xa87S-|\x81\xac2Q\xad\xa7\x86\xe4l\xb3\x01e\xe1\x94;\xaaA\xbb8\xc20A\x86\xd77\xa4eF\xa2\x1f\x8f\xf0h\x18iل\x98\x0f\r\xdd\xfa\x11dzd(v\xa0\xd6\xcd\xc6\xc98g{\x96W\x94\xe3\xbcLE\xe6\xf0\xa1\xf5\xb8bVf\x84ɽ1G%\xd3\x15\xe7\x10\x04\xa4,\x93:KI)\xc0\xfa\xbe\x85]\x1b\xf4\xab\x0ec\xbe\xa6\xd6W\x91C\xd8\x13d\x96\xaa8h\xdfU\x8endc3\x16\rS0RC8]\x03'\x1a8dF\xaa8E\xa6\xf8\xecJ\x8a\x11\x1c d\xc4\xf2uW\x1c\r\x02# \t.\xe5v,\xdb9W\xcf\n\x11\xc2!\xb9\x04\xeb\xf0\x19B˒G\xa6\x8b\xa6\x8c2\xdfw2\xa6\xebM\x99\xd0\xfacx1\xfdoJ\x82\xcdlJ\x94\xb4\x8d~u)[\x8bC|m۔\x7fN\xc2\x06\xcb\x7f\x82Ўh?\xc1\xb0Y\xb2L\x0fʭ\xa5*\x03}i\xdd)\xf4t\x16\x84\x99\xf0\xeb\x94&t|\xae^4\xb1C\x84\xaf\x9b7\xf3\x85>\x915):\xf1L\x8c\xa9\xbb\xf8\a\xe4\vN\x19\xb7~\xc6H\xe6\xc9O\xedV\v\xc265\xd1\xf3\x05\xd90n@\x1dQ\xff$S\x1f8s\x0eb\xa4\xccz\x04\xf77L\xb6{\xffź`\xba\xd9\xe4K\xa4\xcbqc\xe7\xc8\x06o\xbf;=O\xc0%\x18\xe7g.\xea\xaa/q\xc5\xd4\xfe\x05]\xab\xb7\x1f\xdf\xc5\xd7W\xed\x92 y=D&\x94Ε\xb7G\x18\xb5\xc7\xe7]\xf8\xf0\x05}\xa0z\x01\xe4b\xd6\vB\xc9\x03\x1c\x9c\xebB\x05\xb1\xfc\xa1\xa1rB\xf7\np\xd3\n\xe5\xec\x01\x0e\b&\xbe\v\xd5/\xa9\xd2\xe0\xca\x03\x1cR\xaa\x1d\xd1Ў\x89i\xbf\xbbf\xe9d\x7f@B\xe0\xe6C\xaa\x18\xb8\xe2U!\xb2\xe7\x13/\x89\xb6$\x94@\xfb\x13\xd0L\x12\x95v\x1f\xedm\\\x94\x80\xef\xb4\xe3\xa5\u0558\x1d+Ѭb\xc4An\x92\x19\xea\xca=\xe5,\xaf;r:\xb2\x12\v\xf2Q\x1a\xfb\xcf\xfb/L\xfb\x9d\xdew\x12\xf4Gi\xf0\x97g\xa1\xa8\x1b\xf8s\xd23\xec\xfcX\x84\x9c\x95\xb7\x04k\xefU\xba9\xcdJ[M{\xa6\xc9J\xd8\xe5\x8a#IbW\xb8-\xed\xbas\x1d\x15\x95\xc6mF!\xc5҅mb=yzK\xd5!\xf7\x93;\xf5\x1d\xde\xd9\xc9\xc2}q\x9b\xe3\x9cf\x90\x87m\x1bܵ\xa5\x06\xb6,K\xec\xaf\x00\xb5\x05RZ\x13\x9e&\x11\x89\x86\xd5c3O|\xd2f\xefv\xf9\xb2|\xa8\x93 \x96v\xcaYz\bF\x16\t4\xf0\xb6;\x9f\xc6giu6\xa1V\x90\x84ɪ\x03\x9b\xba\xc3US\x88\xf2\x04r\xe0,\x8e.\xce$wi\x9ec\x8a\x10\xe573f\x94\x19\xb20\xd74\xb4\xc6\xee\xa6\xe0\x82\xe2V\xcb\xffؙ\x16\xb5\xe9\xffHI\x99җ\xe4-\xe6\xfcp\xe8|\xf3A\xb3\x16\x98\x84.1g\xc7\xcaϞr;\xf7[\x03.\bp\xe7\t\xc8M\xcf/Z\x90ǝ\xd4nڮ7q.\x1e\xe0\xe0v\x0e'\xbbl\x1b\x99\x8b\x95\xb8p>D\xcf`\xd4\x0e\x87\x14\xfc@.\xf0\xdb\xc5S\\\xa9DIM\xac\xd6\x11т\x96i\x12\x8a9W\xa9\x8e\xba]\xb0\x06'\xc46\xacs\x89\xac\x93=\x86m\x92\x88\x96RG6\xf4\a\x862!\xbc7R\x1b\x17/\xeb\xf8\xccр\x9a\fA4B7.\xc1K\xaa\x90\x8dc\x8d\xf2T\xe8\xb7]\xeev\xa0\xc1\xefW\xf8\xc0\x9c\x03jWv\x17\x8d~;k\x7f\xe1\xf6K\xb0\x13\x9a\xa1ǂmK%3\xd0ѽ\xec\xa6$\xcc\x17\x91l\x916\xeeȗ\xbaU\x92\xcbY\x19\x0f\x81\x86\x92\xee\xf2ZB\xcc\\/\xbc\xff\xd2\n\x88Zݷ\x7fO\xc9\xd8\xdcq\x11̶,\nz\x9cǕ4\xc4k\xd72h\x83\a\xe4\x16\x1fj[\xa1%H\x9d\xcbk\x01\xfc\x1a\x1c\x85\x82\x89\x15v@\xde<\x83c\xe1mh,\xe9$VNse\xafC'\rw\xea\x1f\x9c*\x97\x12\xb7\n\x14t\x98\u05cf\xaa\xa3\x1f*\xa4i\x05$f\xb8\x9b\xa5̿\xd3dÔ6\xed!\xe8\x814\x95(\x98\x99\v/\xf1^\xa9\x93\xd6]\x9f\\ˣD2\x9f\xbf\xe6\b\x93\x889\xee/\x01a\x1b\xc2\f\x01\x91\xc9J`\x00\xc7\xea1v\xe1\x88\xeb,,KU\x924\xed'\x83\xb9h\xb1\xb2DIab4\xd2Ӯ\xfe\x81\xb2~\xc2Z\xac\xccd\x9b\x19\xcaf\x8b\x95\xd3t\"\xa4\xba\xb53\x16\v\xfa\x85\x15UAhay\x84\x939+\xa0\xcb\xf4&\x01ζ\xc0i\xc2H\xab1%\a\x03>\x89-q\f\x99\x14\x9a\xe5PO\xae^\x10\xa4 \x94l(\xe3\x95J\xb4\x80\xb3\xc8;g)\xe2-\xc1\xf9\xd6\x18i\x9d/\x91\x14\t\xd1\xdcD_q\xdc\x1a\x97*\xdd\xe3\x9br\xb3\x14\xcc\xf7\xb2J\xc5$\xa6\a\x9e\xd9\xd1\xf2\t\x95T\x1c\xbeyZ\xa9C\xfd\xe6i\x8d\x95o\x9e\xd6D\xf9\xe6i}\xf3\xb4Rj~\xf3\xb4\xbeyZ\xed\xf2/\xe1iM\x8d\xc8\x1dx\x1c\xf889\x8a\x84\xad\xea\xb1!\x8e\xc0\xf7\xc9\x15>\a\xfcI\xb9\x98\xab8\xa8H\xe2\xff@Zw\xcch5\x93G\x9d\x9ci\xb5&ȼ;\x7f5\xe1J>!\xeb>tz\xbe\xac\xfb\xd5(\xc43e\xdd\xfbaO\xfb\xd8'\xe5\xdc\a\xa2\xcc\xcb\xce^\xf8D\x8d\x02h\b\xab\xbbm\xf8\x18^C\x122\xd1\xff\v'\xe6\xf6\xb2\xc6\xce(\x1fϞş,#Q\x96^\xfc\xe9\xe2\xeb#\xffy\b>H\xe2>\xed\xfc\x01\xf0\bT\xbb\x02m\xa7\x85u\xb3\xf0\xbeN1>\x8bܦf\xe2\xd7D\x8c\xc0\xea\x8a\xe4\x11\x15\xbfV[`\xa0\xf8T\xfa\x19\xe9\t'VW\x118IgV\xa9>\x88l\xa7\xa4\x90\x95\xf6Q\t\v\xebm\xe6N\xfc\a\x901a\x8dj\xf8\x7f\x90\x9d\xac\"\x99\xe0#\xe4\x9b\xc8\b\x9cF\xbe\x93\x1c\xe87\xa1\xc1\xd0\xfd\x9b\xcb\xee\x17#}\xaa\xe0\xd0\x19\xe7\xc7\x1d\b\xdca\x17\xdb\xf6\x01\x80pa\x83\xbf\xb9\xe0X\xc0\"\x80\xa4\"\x82q'y\xf5u\x0fm\xb9#\x9fJ\x17{\x9a\xedw\x8c\xc7TҒ\tON!\xec\xa6\b\x0e\xf8\xa5sw\xbb\xcfrd\xe2wI\r\x9c\x9f\x10\x98\x12\x11\x9bH\xfe;!\xe5/1\xb7\xf8\xc9\xdb\xf3)I}sV\xccϖ\xc0w\xfe\xb4\xbd$\xfaL\xa7\xe8͡γ\xa7\xe3\xbd`\x12\xdeˤ\xde%&ܝ/s>-\x1e{R\xe6\xd8t\xe8`8in2Un2\xb40\x85\xd8l\x94&S\xe0\xe6$\xbeMr'M\xcd^,\xb5\xed\xc5\x12\xda^6\x8dmT\x8aF?\xceIT\x8b\xdf\xdbC&'\xdb\u07bdj\xbd\n甸\xe4Xܠ\xd2\xf1\x97R\x8eS\xd9&U\xc7\xdd>i=\xf8\xe9\b\x86\x15\xd4\xe0\x8a\xbe\x90O_Tܰ\x92\xe3\xc6\xef\x9e\xe5\xd1\xe0\x88\xd9\xc1\xa1\xbe\xf0\xe3W\x89Ge\xfd\r6\x9f>\xd7Zvy\xb42\xa1\x9a<\x02\xe7\x84\xc6\xec@\x0f\xf3\xcc]\xad\x95\xc9%\xd8\xf9\xd3Z\x13\x7f\x91\x89\xbf\x8fk\xe1\xd4\x13O\x03\xe3,\\\xc4BbT\f\xdfz38ѥ\xd8Ǟ\xc7\xed\xd6\r\xf8\xdbo\x15\xa8\x03\xc1{wj\xbf\xac9\xb4\xe6\r\x89\xb6\v\xc7`ڼ\x99\x1d\x8a\xf7\xf7\x16)\x8d\xe9!o\x85\xf3\x12\x8eǃm\xacMk\x16a\xd6P\x8b\u0605S$(X\xbf\xb9\x90u\xebH\xb3)\x87>\xf5t\xd7\xf3.\xc9\xe6/\xca&\xbd\xa0tO\xf5w:\xb5u\xcai\xad\xb4\x84\x85\xc9\xd3YϵD\x9bZ\xa4%\xfb\xa5i\xa7\xaf\xe6mn>\xe3i\xab\xe78e\x95H\xa9\x94SU\xf3\xe8\xf4\x02\xa7\xa8^\xf4\xf4\xd4K\x9d\x9aJ>-\x95\x94\x92\x93\xbck\x9d\x9aRs\xe2\xf1\x9f\xe9=\xe9\xf1\xd3O\t\xa7\x9e\x12v\xab\xa7\x91<\x01\xbd\x84SM\xf3N3%\xf0,U\x15_\xf0\xd4\xd2\v\x9eVz\xe9SJ\x13\x925\xf1y\xdei\xa4\x93\xb7X\xa4\xcaA\x8dnS\xa5J\xe1\xa8\xfc\xa5\xacm\xba\x039ڟ\t\xb7\x14\xdaZ\x1d\x7f\x19\xa7\a\x7fs,\xde\x11<\xb4\xddj%\xad\xe5mt\xf6\xce\x1a\xf7\xa7\xebL\xfa\x8b\x83\xdd\xf6\x9a\x86\x92*\xbc\x8cz}p\xe97ѩ\xf9=\xcdvG\xd0wT\x93\x8dT\x055\xe4\xa2ް|\xed\x80ۿ/.\t\xf9 \xeb\x1c\x8e\xf6=B\x9a\x15%?\xd8\x15\n\xb9h78M\x02\xa2\xd2\x16z\xbb\x91\x9ce\x11\xdf-z\x97\x94\xabܻ\xdc\x03o\xb8\xca\xda)\x0e\xa5\xad\x18w\xdd\xd0\xcd\xeb^ٹ\x91\x9c\xcbǹ\xb1\x8a\x92\xfd\x05/i\x7fB4\xeb\xed\xcd\na\x04\xf1\xc0[\xdf\xebd\xb2\x1a\x9b5\xd8i\xb9\xc1sH\xf7W\x9b\x0e\xc4n^f\xfb\xb6c\xc8\xdd\xc5\xd6\xc1-\xf0\xa63\x93ֺܬ\xdc8\x86z\xb12CŁH\xcc\x002;\xa6\xf2eI\x959\xb8ĒEg\fa.\x1d\x8bF\r\xce\x1e\xfd˺\xa3\xe4\rwt\xe3\x8e\xea\xa1\xecnR\x1f\xd3\xee\x94q\f\x9f\xb6\x9c\xe1\x8d\xfe\ars\x8fk\xb4ڴy\x15\xf5k\xb4\x10*\v\x9b\xd7\x118\xbe\xc1\xf7\xe7O\xa5\xd3F*\xba\x85\x9f\xa4\xbb4}\x8a\xed\xddڝ\xcb\xf4\xbd\xd7\x13\xf2]\x83\xd2\xc4.\f\xf6\u05f7\x1f\x01kr\xd4{\x970\xdbQμV\xda\x18~\n\xdf\xef\xee~rX\x19V\xc0\xe5\xbbʥgX\x9b\xa8\xc1\x928`\xeb \xad\xed\x7fw\xf2\x11/+\x8e\xc71\xc3#\x18\r2\n09\x1eS&g\xa1T\x95\\\xd2\x1cԵ\x14\x1b\xb6\x9d\xc0\xee\x97N\xe5\xa3i6\xc3\x1f=r\xf5\x1c\x15\xe0\x9f9g\xc2\xfa<\x9c\x03\xff\xc08h7\xac\x04\x03|\xd3oU\xdb\xe3\xaaX;\x1fnc?\xd6\x1d\f\xccq\x0e-\fE\x97\xa0\xac\x17\xe5\x82֕\x0e\xb2:\x8cx\xc3\x11&\fl\xa1\xbf\n\x1c\xb1\xc0\xee\x16l\x9c>\x839\xc1\xb5̏\xb1\xf8V\a\xf9\xfb\xe1\x96G\x9cl\x85\xbcb7\x04:'\xe4\xe6\xfeZ\x93J\xe4\x18.\xbe\xff\xcb\xed,\xa9\xdbwn\xdc\x0f\xda:eT\xef\xe3\xadZ\xceq\xcb^8\xefXn\"\b\f\xc1i=\xec\xf2Ȍ\xbfh\xec\xbc7\xc3\x0e-y\x86\x9e\xac\xc0\xa7\b\xa6\x1f\xadp/\x16\xf8\xa7n\xbc:V\n\xafu\xf5\xaf\x19\xe05\xa8Ox\xb7\xa2\x93\xac\xa6\xdf\x1a\x03Eib\xbeƴ9\xfc~\f`\xed\xa7ICyK+i\xa8\x10\xf3\xb4\xf5Adc\x89p\xde\x1a\x8dpsL\x1fc\x04\xb8\xf6\xe77\xceF\x80\x1a\xe0\x10\x01t\x95e\xa0\xf5\xa6\xe2\xfcP\x1f\x1f\xf9J\xa8\xf1\x812~>R8h\x83\x82`\xd1\x1b\x854\x89\xb0OO\a\x91\aM\x0fG\xab\xe6\x91\xc2s\xc1gojC\x8b\x93\x1e\x98\xb8\xee\x83\xc17\x98T\xdeJ\x02\xa5\xf5ةn\xd8\x1f\x9b\\\x1ap\xae%.\xb2,4\xc8\t\xecA\x10;;;\x12\x87\xe7\xc5fB\xf1'r\xdd\f\x17\xe6\xbb\x10\n\x89\xbe4E|\xb4C\xe3\x8bF\xdf\xe9\x1a&\xe6\xb6\xe2;,}\"\xf4\x9d_\x17\xad\xb8\xb2\xde?,-\x88Ӽ֡Wh\xba\xf3\xc2ӌ\xdc\xf5\xedj\b\xdc)&\xae\xffL\xcd\x13ո\x8f\xee\x93LZ\x1f\xddY\x06-\x02\xb1\x96\xf1\xf3㎪~\xda%\xf4\xd8\xd29\x1cY8\xf3G9\xf7\a3\vКn\xc3\xed\xf3\x8fv\xe9\xb1\x05\x01.<\xe76O\"@\x9bS|ݻם\xca\xd0\xccT\xd4w\x10\x12\x92[\xb5\xbeӄ\xcb\x18T|\x80\x86\x85\xa7\xdf\u009al&\xa1\xbe\x94L\xa5\xac\xe1\xde\xd7\x15-m\xd0\x13F\xee4\x8f\xf5\x01g[|\x8a\xcarnK՚na\x99I\xce\x01\xadu\x7f\\ϩ\xeb\xfe\xac\xe4g\xa0z\x12\xb5\x0f\xed\xba~\a\xd0q\xdbm|S\x97\x9e\x8fϱ\x19\xa6\xa0y\x19\xb17 \x89\x1d\xcfr\x94\x1d\x15\xa2\xcf\x06\xf6Gڮ\x1b\xb4Λe\x1f\xe7\xf5\xaf\x06.\x9a\x97\xc0\"\xe3,\xe8\xafR-H\xc1\x84\xfd\x87\x8a\xdcm\xe0\x85Ƴƿ\x93\xf2\xe16\xe2\xc4\xf6\x06\xffC]\xb1\xd9\xea`\xc2\r\x1b\x0f\xb8\xaee\xe5w\xdfk\x876\xbe\xad\x82/\t\x9cy\xb9\x890G\xe6\x83\x1e:\x83\x11\xdd\x1f:\x90&\xa7\x02\xd7\xf3\x00\xac\xdb\xf04\x1d\xe7\x87\xc51\xe4\xa3g0\x1bح\x97\x16\xbc\x1b\xd0ܟ0\xd0Qؑ\x8a\x02\xa9/\xeah\x1b\xf4SV\xbd\x9e\xccC\xced\x8f\xc6?4\xb5\x87\xe8\xe8\x86\xd9r\xf7\x06\x10\xec8\x81\xe7]\xb0\xe3\xb3\x1a\x13\xc2\x7fc\xeb\xd4w-\xb4\x16n!Kl0J7\xf4B\xdfG\xe8oW,\xc9_+\xa8\"4X\x86\a\xedn\rU\xfd\x90\xaf;\xb6\x0f9ft\xa06F\xaa\xacč\x92[\x05\xba/\xacK\xf27\xca\f\x13\xdb\x0fR\xdd\xf0j\xcbħ\xe1#Jc\x95o\xa82\xcc\n\xbb\x1bOl\xa0LP\xce\xfe\x1e\xb3k\xed\x8fӀ\xae\a\x17XK\x920\x8c\xa1\x0f\xef\xc0\xfa\xb8\x83q\x81\xa8\t-=]O\xf1W\x02O\xa6lj\xedK4\xbeH\xe8\xf6\x92|\x94Q\xc3\xe0ӡX\x17\xa6u\xc9@\x9b%l6R\x19\xb7[\xbd\\\x12\xb6\t\xc1\aks0n\xe6\x1e\x1f%,\xb6\xcd\\'\x9a4\xd3\x17\x06\xbd\x15\xce\xc2x\xf5~A\x0fng\x8afYe=\xac\xd7\xdaP\x1eqp\x9ed\xf81\xca\xf3=>\xb4\xf9˓v\xf2Vm@\xfd\xa0#\xf6\xe3H\x8a\x97\x7f8\xaf\x8f[\x14A\x90GŌ\xb1>\x95\x1cI%\xf0\xa42ַ\xe2\x9chKꓢ\x8fę\xd1\xd5pJN\x1a\xcaw5\x94!\xf3\xec\xb1\xc6\x17%\xeb\xd7L}\xf6\x91\xafeٜ\xed\xa8\xd8\x0eި\xb0S\xb2\xda\xee\x82$\x0f8\xd3$\xaf\x00\x83\xb5hRtx)\xdaTJ\xb4R\tF\x8e\xa9\x93 \f8\\\x9a=\u0eeb\xee%f\xff\x04\xf7k\xfff\xcbr\xa3d\xb1\xf4\xfdb,u\xe1w\xf2\x15\x93\xd6s1\xbb(Չ\xf3\xda\xfd\xb3\b(\te\t\x82P\xed{N\xb8\xd9\xea\xe4i\xea7;5\xdcH\xcd\x12\xbc\xfd(\xc7\xff\xda\x06\x10\x18^\x86\xbf\xbb\xcc\xf0+\x18\xec3\x86\xc7'\x7fe\x00\xec\xa90n9QO\x91\x17n\x12\xbb\x98\xb5\x90\xd1vb{R\x90\xe6\xb6\x03a\">\x83\xdd\xc5Yt\xeb\xd35\xdc\xc5e\xd7\xfe\xd9\xd8\x1a\xf0\x82h&\xc2K\xe6.\xf5\xc3I\x7ft'P\xe0ÚRų1\xc7\x03.]\x84^6ֲ\xaf=\x89\xf7'/\xc5\xef\x8f`\x1c\x1dB\xc7wT\xeb*a\xf9\xfc\a\x16\xdb\x0f\xc04\xde̢\xf2\xc7\xdf\xfdp\xf9>i\xa9\x17\xa7\xc8\xd8\xca\x0f\x17u\xc3K\xb8\ueee97\x1c\xac\xb6i\x80\xee\xa2r\x96\xce\xed\xcf\x18M;g(-\xbc\xd9\x7f\x9eX\xd2\xfe\x8cA\xb4g\x8b\xa0\x9d\x17\xe5G\x8a\x0f[\x9f\xa4\xb5\x7f\xf3m#!4\x0f\xf6\xdcA\xb4V\f-\f\xfcE\xa3h\xd19\xb7\xf7#\xda\xe9\xbce-|O\xfe\x97\xff\x0f\x00\x00\xff\xff\x93\xf6\x83\\\xa3\x84\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccZK\x93۸\x11\xbe\xebWt\xed\x1e\xf6\xb2\x94줒J\xe96\x96\x93*W\xc6\xf1\xd4hvr]\bhJ\xb0@\x80\x01@\xc9\xca㿧\x1a\x0f\x89\xe2C\x0f;q\u008b-\x12h\xf4\xf3\xeb\x0f\xc0\x14E1a\xb5|E\xeb\xa4\xd1s`\xb5\xc4/\x1e5\xfdr\xd3\xed\x1f\xdcT\x9a\xd9\xee\xedd+\xb5\x98âq\xdeT\xcf\xe8Lc9\xbe\xc7Rj\xe9\xa5ѓ\n=\x13̳\xf9\x04\x80im<\xa3\u05ce~\x02p\xa3\xbd5J\xa1-֨\xa7\xdbf\x85\xabF*\x816\b\xcfK\xef\xdeL\xdf\xfe~\xfa\xbb\t\x80f\x15\xcea\xc5\xf8\xb6\xa9\x9d7\x96\xadQ\x19\x1eENw\xa8К\xa94\x13W#\xa7\x15\xd6\xd64\xf5\x1cN\x1f\xa2\x84\xb4z\xd4\xfc]\x10\xb6\x8c\xc2\x1e\x93\xb0\xf0]I\xe7\xff<>\xe6Q:\x1f\xc6ժ\xb1L\x8d\xa9\x15\x86\xb8\x8d\xb1\xfe/\xa7\xa5\vX9\x15\xbfH\xbdn\x14\xb3#\xd3'\x00\x8e\x9b\x1a\xe7\x10f\u05cc\xa3\x98\x00$\xd7\x04i\x050!\x82\xb3\x99z\xb2R{\xb4\v\xa3\x9aJ\x1f\xd7\x12踕\xb5\x0fΌ\xb6@2\x06\xb25\xe0<\xf3\x8d\x03\xd7\xf0\r0\a\x0f;&\x15[)\x9c\xfd\xa2Y\xfe\x7f\x90\a\xf0\xd9\x19\xfd\xc4\xfcf\x0e\xd38kZo\x98\xcb_c\x8c\x9eZo\xfc\x81\fp\xdeJ\xbd\x1eR\xe9\x919\xffʔ\x14A\x93\x17Y!H\a~\x83\xa0\x98\xf3\xe0\xe9\x05\xfd\x8a\x1e\x02r\x11B\xf6\x10\xec\x99K\xeb\x00좔\xe0\xa3aMUo\xad3\xb5I\x15x\xedH\x89\xfaӛ\xa4}Kl\xce\xef)\xb7x\x14\xe9<\xab\xea3\xb9\x0fk\x1c\x13v\xe6\x8a\xf7X\xb2F\xf9\xb6\xa9\x14%\xd5\xce\xcbs\xb3j\xe4S\x11g\x9d\xad\xf8\xfe\xec]\\ue\x8cB\x16\xa5\xc4Q\xbb\xb71\v\xf9\x06+6O\x83M\x8d\xfa\xe1\xe9\xc3\xebo\x97g\xafa(\x91:EA\x81c\xad\xd8l\xd0\"\xbc\x86\xfa\x8bqsɴ\xa3L\x00\xb3\xfa\x8cܟ\x82X[S\xa3\xf52\x17K|ZX\xd4z\xdb\xd1\xe9\x9f\xc5\xd97\x002#\xce\x02A\xa0\x841\xafR\xfd\xa0H\x96\x83)\xc1o\xa4\x03\x8b\xb5E\x87:\xc2\x14\xbdf:)8\xed\x88^\xa2%1Tۍ\x12\x84e;\xb4\x1e,r\xb3\xd6\xf2\xefG\xd9\x0e\xbcI\xc9\xec\xd1y\b\x15\xaa\x99\xa2dm\xf0g`Zt$W\xec\x00\x16iMhtK^\x98\xe0\xbaz|\xa4j\x90\xba4s\xd8x_\xbb\xf9l\xb6\x96>#47U\xd5h\xe9\x0f\xb3\x00\xb6r\xd5xc\xddL\xe0\x0e\xd5\xcc\xc9u\xc1,\xdfH\x8f\xdc7\x16g\xac\x96E0DGH\xadď6a\xba;[\xb6W\xd2\xf1\t\x90zGx\b^c\xcaDQ\xd1\xc4S\x14\xe8\x15\xb9\xee\xf9\x8f\xcb\x17Ț\xc4HŠ\x9c\x86\xf6\xfc\x92\xe3Cޔ\xbaD\x1b\xe7\x95\xd6TA&jQ\x1b\xa9}\xf8\xc1\x95D\xed\xc15\xabJzJ\x83\xbf5\xe8<\x85\xae+v\x11\xba\x18\xac\x10\x9a:\x80Dw\xc0\a\r\vV\xa1Z0\x87\xdf9V\x14\x15WP\x10n\x8aV\xbb7w\aG\xf7\xb6>\xe4\x9e:\x12\xdaA4X\xd6\xc8\xcf\xeaN\xa0\x93\x96*\xc33\x8f\xa1\xba:\x0eJP1ޔ\xf33\f\x12\xf40\xceѹ\x8fF`\xf7KG\xe5\x87\xe3\xc03\x1dk\xb4\x95t\xa1\xbdBil\xb7\xf3\xb0#\x92\xb7\x9f\x8cx݀\x03\xa0n\xaa\xbe\"\x05<#\x13\x9f\xb4:\x8c|\xfa\xab\x95\xbe\xbf\xd0H \xe9\x89*.\x0f\x9a?\xa1\x95F\\1\xfe]g\xf8\xd1\x05\x1b\xb3\x872\xe4\xbf\xf6\xea@\xd8\xe5\x0e\x9a\xf7Q;?\x0fO\x1f2\x82\xc7\xdaJ\x85\x99|5\x85\x87TԦ\x847 \xa4#\"\xe1\x82о\xb3t\xa3\x02ј\x83\xb7\xcd]\xe6s\xa3K\xb9\xee\x1b\xdd\xe6Fc\x19sEt\xc7s\x8b\xb0\x12\xa1\x16eGm\xcdN\n\xb4\x05Շ,%O\x9a46v\x90R\xa2\x12=l\x1a\xad\xb2`\x8aEAE\xcdԕ\x18.\x8e\x03\x03\x93fR\xc7\f>\t\bXc\xabԚ\xb5G-\xb0\xdbm\x826&\x00\x9aC\x01{\xe97\x11)\xd5P\xdd\xc1\xc5ڣg\x8b\x87\xa1\xd7\x1d\xdd_6H#c\xe3Ep\xc8-\xfa\x90m\xa8(}(\x95\xa6\x00\x1f\x1b\x17\xb0\xb6\x8b\x13\xf9\t\x84/\xcf\xde\xe2\xa1\xefh\xb8\x16\xdcD\x85FT\x0e$j\x0e?\xfcpݤ^w\xcb\x0fQ\xf7l\xa8\xc5\x12-\xea\x1e\x9b\xc8\xcfK\xe8Q\x944\x94aX\x96Ƚܡ:\x84\x9eD\xe0\xf93\xac\x1a\x0f\xa2\xc1\x105Ʒ{f\x85\x03n\xaa\x9ay\xb9\x92J\xfa\x03H7\"\x9f)e\xf6(Rı\xaa\xfda\n\x1f\xb4\xf3LstG\x1eD\x1e\x8b\xa9\xc0t\x1c\x95\xaa8\x10:f\x8700\x8a\xaf\x8c\xf3\xc0\xd1R:\xaa\x03\xec\xad\xd1\xeb1c\a\xda!\xed\x01\xadF\x8f\xa1#\n\xc3\x1d5C\x8e\xb5w3\xb3C\xbb\x93\xb8\x9f\xed\x8d\xddJ\xbd.H\xc1\"\x81\xcf,\xec\xecf?\x86\x7f\xbe&\vL\x1dq\xe2\x86\xe4]\x86Z?\x10\xbd\xf5\x1b\x8c-b\x19s\xd0X \x02A\xa9]\xa5܍\xc8:TvC\xbc\xbc\xfd\xe4\x90\x0f\xf5\x8f-\xf6[\xc7\x05P\x01\xf8R\x9c|[T\xac.\xe2h\xe6M%\xf9\xa4km\xcc\xfb\xcb\xf8\x937+R\vɉܞ\xe3F\xdeĉ\xb3=̀\x1b\xba\xbb\x9c1\xb4\x1cvS47q\x85+\x1a\x7fj\x8f=m}#t\xa7\xfe\xef\xd0\x13\xeft\xa0\x91\xf8\x01\xb3}?\a\xc0\xe4FkB*o\x80\x1d\xdb\xc0O\xae\xdb\xff\xeeD\xcfU÷8\xe0\xf8\x9e)\xef\xc2\xc0\xec\xe38\x8dti\x1c\x86\xc6tM\r\xb8^\x11\x9c-\xd0ޢ\xcb(\xf2-\x1eH\u0091[0X<\xc0\xaa\xd1BaVu\xbfAM\xdb1Y\x1e\x88\xec\xbf<.\xb3c\x03\x01K[\xa7\xec\xde1 yO\xbb\x00JA1\x87_\x1c\xa6u\x9f\xb1\x04\xa9\x9dG\xd6#\xe9\xf1\x89\xbdq\x0e\xab\xc3\x00\u05fa\xd9A\xcfX~\xbb\x8f\x82\xae\xe4\xa1\xd4 8\xc6\xc4J\xb0\x92\xfa{\xde\x0f-\x1e\x02\xc4\x12\xdf \"}\xe6ґe\xeetth\xd0i\xf1\fdR\xc7\x02a\xd5\xd8\":\x1fR\x01\x8by9H䇃q\xb9.\xe0\x12\xb3\xe89\xfb>v1*\x13\x80\xdd\xc80\xe0z\xb2\xc0E\xa6\x017\xb0\x8d\x9e\x99\xa39\x05w\xb2\x0e\xf8\x0e\xcc\x03\xfe\xfb\xec\x03\xeef \xf0\xddY\bܖ)\x97\xd9\b|\x13#\xb9\xe0\x8bK\\\x05\xae\xf2\x15\xb8\xc8Y`\x94\xb7\xc05\xee\x02w\xf2\x17\bx\x82\xa5\xfcr\x032?\x85\x81\xb9\x93\xd6\xcco\xa8kH\x81\xc0\x06\xfaj<\xa1\x18q\xd0q\xd3\xfb)\x85\xef+\xfa\xee%\xd2\x17չ\x87\xf7e@\xbfB\x8c\x9eҰ\xa3\x17\xf2\xef\x04 \xe7\a c\x04mТ\xdd\xf1\xb4\xfdO\xf1X\x81\x0f\xa0\xf8\x992\xaf\xfd\x19\x17\x8e'\xf2\x99\xff\x10K\xa3Ͱ\xb1\x16]m\xb4\xa0\xb6w\xdb\xe1\xc4I\xe5\xff\xdc\x11\xc5pX\x8bs\xfa\xda\xf9\x96\xa3p\xd3\xf9\\\xb8߸\xfb\x84.\xde\xfa\xb4Ͽ\xccʡݵ\x0e\xe9:6~\x97\xb3\xb9\xc1\xce\xd6:\xb0#\xaa\xa4\xa1\xd1\xe1\xc8\"4\xad\xe9d`F\x9b\x17\xfa\xd0<\xa4\x03m\xf64\xb9%-v=\x13\xe9M8\xb4dZ\xa4\xe3b\xfa4 y/\x95\xa2\x1ef\xb12\xe4,\xd4^Zj\x96,\xb4\xb1\xddo\xa6o\xfewg\x81\x8a9\xbf\xc6Qq\xff\x99\xa6\x00[\x99\xc6\x0f\xf4\xfeV\xc2\x0f\xd6t\xb8e\xbfG\xc7\xf0\xb7\x03\xd7\xe8\t\x8d\xc9\x11፵\xe1\xb2._\"ݱ\xd1\x1cC\xe0\x87Ο8\xb4\xbf\xf5\xff\x00\xe2\x06\xbb\x06\xbbt\xefe촭\xb8&'\xb7\xdf4\xab\xe3\x15\xec\x1c\xfe\xf1\xafɿ\x03\x00\x00\xff\xff%\xff\\)\x99#\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVK\x8f\xdb6\x10\xbe\xfbW\f\xd0k%wQ\xb4(tk69,\xda\x06\xc6n\x90;M\x8e-f)\x92\x9d\x19z\xeb>\xfe{AR\xf2C\xb67\xc9%\xba\x99\xf3\xe4\xf7\xcd|t\xd34\v\x15\xedG$\xb6\xc1w\xa0\xa2ſ\x04}\xfe\xc5\xed\xf3/\xdcڰ\xdc\xdd-\x9e\xad7\x1d\xdc'\x960<\"\x87D\x1a\xdf\xe2\xc6z+6\xf8ŀ\xa2\x8c\x12\xd5-\x00\x94\xf7AT>\xe6\xfc\x13@\a/\x14\x9cCj\xb6\xe8\xdb\xe7\xb4\xc6u\xb2\xce \x95\xe4S\xe9\xdd\x0f\xed\xdd\xcf\xedO\v\x00\xaf\x06\xec\xc0\xa0C\xc1\xb5\xd2\xcf)\x12\xfe\x99\x90\x85\xdb\x1d:\xa4\xd0ڰ\xe0\x88:\xe7\xdfRH\xb1\x83\xa3\xa1Ə\xb5k\xdfoK\xaa7%\xd5cMU\xacβ\xfcv\xcb\xe3w;zE\x97H\xb9\xeb\r\x15\a\xee\x03\xc9\xfbc\xd1\x06̚\xaa\xc5\xfamr\x8a\xae\x06/\x00X\x87\x88\x1d\x94ب4\x9a\x05\xc0\bH\xc9Հ2\xa6@\xac܊\xac\x17\xa4\xfb\xe0\xd2\xe0\x8f\x95\x905\xd9(\x05\xc2\x0f=\x96\xcbC\u0600\xf4\b\xb5\x1cH\x805\x8e\x1d\x98\x12\a\xf0\x89\x83_)\xe9;h3\x92mu͍\x8c\x0e\x95\x847\xf3c\xd9\xe7\x86Y\xc8\xfa\xed\xad\x16X\x94$\x9e\x9a(um\xf0@'ȟ7P\xfc\xdb\xd8+>\xaf\xfeT\f\xb7*W\x9f\xdd]EZ\xf78\xa8n\xf4\r\x11\xfd\xaf\xab\x87\x8f?>\x9d\x1d\xc3y\xafWH\aˠ\xa6N3p\x155\b\x1e!\x10\f\x81&T\xb9=$\x8d\x14\"\x92؉\xff\xfa\x9d\xac\xd5\xc9鬅\x7f\x9b3\x1b@\xee\xbaF\x81\xc9\xfb\x85\\@\x1c\x87\x02\xcdx\xd1\n\xaee \x8c\x84\x8c\xben\\>V\x1e\xc2\xfa\x13jig\xa9\x9f\x90r\x9a<\xaeə\xbc\x96;$\x01B\x1d\xb6\xde\xfe}\xc8\xcd\xf9\u07b9\xa8SR \xc9c畃\x9dr\t\xbf\a\xe5\xcd,\xf3\xa0\xf6@\x98kB\xf2'\xf9J\x00\xcf\xfb\xf8#\x83h\xfd&tЋD\xee\x96˭\x95Ilt\x18\x86\xe4\xad\xec\x97E7\xec:I ^\x1aܡ[\xb2\xdd6\x8ato\x05\xb5$¥\x8a\xb6)\x17\xf1Ep\xda\xc1|G\xa3<\xf1Yً\xe9\xa9_ч\xaf\xa0'\xabE\x9d\x91\x9a\xaa^\xf1\xc8B>\xca\xd0=\xbe{\xfa\x00S'\x95\xa9J\xca\xd1\xf5\x02\x97\x89\x9f\x8c\xa6\xf5\x1b\xa4\x1a\xb7\xa10\x94\x9c\xe8M\f\xd6K\xf9\xa1\x9dE/\xc0i=X\xe1ib3u\xf3\xb4\xf7E\x90\xb3\x02\xa4h\x94\xa0\x99;b\xa3\x13Q\x19\xbe\x83ΫkA_\x8a\x05\x12\x05\xba8\x9d5\xf5\xae8\x95\xff\x12\xcaz\x06\xe5\xf7c H\xaf\x04^\x90\xf2\x86萲Z\xa1\x01\x93.\xf0\x1ba9}\x93\"\x05\x8d|\xb1\x8a\x00Vp\xb8\xd2\xd3+\xec\xe4\xcf'\xe7\xd4\xdaa\aB\to0\xab\x88\xd4~f+o\xdfg Xe\x9fk\x1c\x1c\xde\xfbϒP\xe0\xf6i\xb8\xac\xd4\xc0{|\xb9r\xfa\xe0W\x14\xb6\x84<\x1f\xf9l\\U\xf4p\xfe\x1e\xbc\x82\xd2ա\xbc8\xe4,\x85\xe6\x04E\x96@j{\x8a+\xa7\xf5A\xe9;\xf8\xe7\xbf\xc5\xff\x01\x00\x00\xff\xffߙ6&\xcb\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcW\xcdn\xe36\x10\xbe\xfb)\x06\xe8\xa5\x05Vr\x83\xa2E\xe1[\xeb\xec!\xd8t\x1b\xc4\xdb\xdc)j$\xb1\xa1H\x96\x1c:\x9b\xa2\x0f_\f)\xf9G\x96\x1d粺y8\x9c\x9fof\xbe\xa1\x8b\xa2X\b\xa7\x9e\xd0\ae\xcd\n\x84S\xf8\x95\xd0\xf0\xafP>\xff\x1aJe\x97ۛų2\xf5\n\xd61\x90\xed\x1f1\xd8\xe8%\xdeb\xa3\x8c\"e͢G\x12\xb5 \xb1Z\x00\bc,\t\x16\a\xfe\t \xad!o\xb5F_\xb4h\xca\xe7Xa\x15\x95\xae\xd1'\xe3\xa3\xeb\xed\x8f\xe5\xcd/\xe5\xcf\v\x00#z\\Am_\x8c\xb6\xa2\xf6\xf8O\xc4@\xa1ܢFoKe\x17\xc1\xa1dۭ\xb7ѭ`\x7f\x90\xef\x0e~s̷\x83\x99\xc7l&\x9dh\x15\xe8\xd3\xdc\xe9\xbd\x1a4\x9c\x8e^\xe8\xd3 \xd2a謧\xcf{G\x05\xf0y>R\xa6\x8dZ\xf8\x93\x9b\v\x80 \xad\xc3\x15\xa4\x8bNH\xac\x17\x00C\xf6\xc9P1$\xbe\xbdɦd\x87\xbd\xc8\x1e\x00\xacC\xf3\xdb\xc3\xdd\xd3O\x9b#1@\x8dAz\xe5(a\xf8_\xb1\x93\xc347P\x01\x04\f\xe1\x00\xd9]\x84 \f\bO\xaa\x11\x92\xa0\xf1\xb6\x87J\xc8\xe7\xe8\xc0V\x7f\xa3$\bd\xbdh\xf1\x03\x84(;\x10l%+\x1c\xf8Ҷ\x85Fi,w2\xe7\xadCOj\x04)\x7f\a\xbdv \xbd\x94\x05\x7f\x9cx\xbe\x0557\x1d\x06\xa0\x0eG\xf0\xb0\x1e\xb0\x02\xdb\x00u*\x80G\xe71\xa0\xc9m\xc8ba\x86lʉ\xe9\rz6\xc35\x8d\xba\xe6^ݢ'\xf0(mkԿ;ہ\x11c\xa7ZP\x02\xd3\x10z#4l\x85\x8e\xf8\x01\x84\xa9'\x96{\xf1\n\x1e\x13\x82\xd1\x1c\xd8K\x17\xc24\x8e?\xacGP\xa6\xb1+\xe8\x88\\X-\x97\xad\xa2q\x02\xa5\xed\xfbh\x14\xbd.\xd30\xa9*\x92\xf5aY\xe3\x16\xf52\xa8\xb6\x10^v\x8aPR\xf4\xb8\x14N\x15)\x11\x93\xa6\xb0\xec\xeb\xef\xfc0\xb3\xe1\xc8-\xbdrC\x06\xf2ʴ\a\aip\xdeQ\x1e\x1e\xa5\xdc]\xd9TNq_\x05\x161t\x8f\x1f7_`\x8c$Wjh\xb1\x9d\xea\t.c}\x18Me\x1a\xf4\xf9^jS\xb6\x89\xa6vV\x19J?\xa4Vh\bB\xaczEa\xecu.\xdd\xd4\xec:\xb1\x14T\b\xd1Ղ\xb0\x9e*\xdc\x19X\x8b\x1e\xf5Z\x04\xfcƵ⪄\x82\x8bpU\xb5\x0e\xb9w\xaa\x9c\xe1=8\x18\x99\xf3Li'\x94\xb1q(\xb9\xb0\x8c-\xdfT\x8d\x92y\xa4\x1a\xebA\xec\x19d@\xfa\x18\xa8y\x06H\xc1\t\xdf\"M\xa5\x93X\xbe$%v\xff҉c\xc2\xfa\x1e˶d\xce\tC \x99\x8f~\x98\x16\xeaR\f0\xdb賑\x8c\xfd\xcd00\xaeL(Lv\x871\x9d\xba\xe6\x0fM\xec\xe7\x1d\x14\xf0{\x8a\xf9\u07b6\x17\xcf\xd7\xd6\x10\xcf\xc5E\xa5'\xabc\x8f\x1b#\\\xe8\xec\x1b\xbaw\x84\xfd\x9f\x0e}\xde\xd0\x17U\xc7E\xbfۊ\x17\x14\xa3>\xeb\xf7\x11y\x83\xe0\xf9L\a\x85\xab\xac\\\x11ӠyU\xa2\xeb\xcd\xdd{ <\xa3\xfe\x8e\"ݙƾ\x91\xe2^qV\xef\f\r\x8c_zC\xbc\xdd\xd3\xfc\n\x19{\x9a\xaf\xe4݉\xf0)V\xe8\r\x12\x86=S\xbf(\xeaf-\x02\xbctJv\xe9b\x1a\b^\x02!X\xa9\xe6(\xf5\x8a\xf0\x99G\x94Ǚ\xa1,ҰΈ9\xf8\x13\xf1\x19\xf6;\xe7\xa0\x18\x18\xe9*\x06%A1\xbc\x83C\x93\xfe\b\xb5\x8cާ\x15\x95\xa5\xfc2\x99^\xb8\x96DG\xe6\xf9\xeb\xf1\xfe\r&\xbd\xddk\xa6ǸP&G\xe3<\x16A\xb5\xfc\x82\xe23\xe6\xd2\xc4q\xa7`\xe4\xef\xf8\x85w\f\xd4lE\xf1\xabSy\x00\xdf\b\xf1\xe3N1\x13>\x9a\xbc\xe7\xa7o\xd8d\x10\x03?\xb7@\n3\x13c\x85P\xa3F\xc2\x1a\xaa\u05fc\xb9^\x03a\x7f\x1awc}/h\x05\xbc\xff\vR3md\xa2֢Ҹ\x02\xf2\xf1\\\x97\xcd&\xee:\x11f\xc6\xf0(\xe7\a֙k\x8c\xdd0^\xec\f8\xbb_\n\xf8\x8c/3\xd2\ao%\x86\x80\xa7ct6\x93\xd9!8\x11\x06~\xa4\xd5\a(\r\x7f\x19\x06\xc9\xff\x01\x00\x00\xff\xff\xe5\x0fY\x99e\x0e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4:K\x93\x1b\xb7\xd1\xf7\xfd\x15]\xeb\x83\xed*\ri\xf9\xfb\xe2J\xf1&\xad\xe2\x14\x13[\xd9\x12W\xba\xb8|\xc0\f\x9a\x1cxg\x00\x04\xc0\x90b\x1c\xff\xf7T\x03\x987H\xeeR\xb6\x85\xcb.\xf1h\xf4\xfb\x85ɲ\xec\x86i\xf1\x01\x8d\x15J\xae\x80i\x81\x1f\x1dJ\xfae\x17\x8f\x7f\xb5\v\xa1\x96\xfb\x977\x8fB\xf2\x15\xdc5֩\xfa\x1dZ\u0558\x02\xdf\xe0VHᄒ75:ƙc\xab\x1b\x00&\xa5r\x8c\xa6-\xfd\x04(\x94tFU\x15\x9al\x87r\xf1\xd8\xe4\x987\xa2\xe2h<\xf0\xf6\xea\xfd7\x8b\x97\xdf-\xfer\x03 Y\x8d+Њ\xefU\xd5Ԙ\xb3\xe2\xb1\xd1v\xb1\xc7\n\x8dZ\buc5\x16\x04{gT\xa3W\xd0/\x84\xb3\xf1ހ\xf3\xbd\xe2\x1f<\x98\xd7\x1e\x8c_\xa9\x84u\xffL\xad\xfe \xac\xf3;t\xd5\x18V͑\xf0\x8b\xb6Tƽ\xed/\xca@\xef\xf3\xb0\"䮩\x98\x99\x1d\xbc\x01\xb0\x85Ҹ\x02\x7fN\xb3\x02\xf9\r@$\xde\xc3ɀq\xee\xd9ɪ{#\xa4CsG\x10dw\vG[\x18\xa1\x9dg\xd7\x04s\xb0\x8e\xb9Ƃm\x8a\x12\x98\x85\xb7xX\xae\xe5\xbdQ;\x836\xa0\r\xf0\x8bU\xf2\x9e\xb9r\x05\x8b\xb0}\xa1Kf1\xae\x06\xb6o\xfcB\x9crGB\xd9:#\xe4.\x85ă\xa8\x11xc\xbc\xb8\x89\xfa\x02\xc1\x95\xc2ΰ;0K\x18\x1a\xe7\xc9N\xe3\xe2\xd7\t\xa2u\xac\xd6S\xa4\x06G\x03V\x9c9L\xe1t\xa7j]\xa1C\x0e\xf9\xd1aK\xc9V\x99\x9a\xb9\x15\b\xe9\xbe\xfb\xff\xd3\xec\x88\xfcZ\xf8\xa3o\x94\x1c\xf3\xe65\xcd\xc2`:`B\xb2ڡI2H9V}\n\"\x8e\x00\xbc\x1e\x9c\x0f\x98\x04\xb8\xc3\xf9\x8b\xa8\xacea\xb0Fy\x1dB\xa2?=\xc7f\bz\xb8\xaa\x8dPF\xb8\xe3\n^~\xf3T4\xc9>@m\xc1\x95\bQy6N\x19\xb6C\xf8A\x15A\xd1\x0e%\x9a\xa8hy\xd4\xfeR5\x15\x87\xbc\x15\f\x80u\xca$\x95Mc\xb1\b\xa7\"\xdc\x16\xecD\xe3\xc6w\xfe\x11\x06Q\x18dI\x83h\xdd\xe9\xc2\xef\x10J\xa6\xad\xe2\xd5\x0e\x9fd\x11C\x96Jű\xe3\x1f\xce\xd0\x12\x16\xb4Q\x05Z{\xc6P\t\xc6\b\x91\xb7\xfd\xc4E\x06\x95\xe8\xf7\xb4\xf84\xbaR\x8c\xa3\x01\xa7\xa0d\x92WHd0p\x86I\xbb\x8d*2\x17`{\xec\xe1\xa8Ǩ\xbc\x8f\v\xa7\xd0\t\xbb\xf6/\x83\xbb.J\xac\xd9*\xeeU\x1a\xe5\xab\xfb\xf5\x87\xffی\xa6I\x8d\x95F\xe3D\xeb\xef\xc3\x18\x84\xcd\xc1,\x8c\xc9\xfdo6Z\x03\xa0\v\xc2)\xe0\x14?\xd1z6\xc4@\x80<\xe2\x14\xd8#,\x18\xd4\x06-\x99\x96\xd7(\xb5\x05&A\xe5\xbf`\xe1\x16\x13\xd0\x1b4\x04\xa6\xb5\x85B\xc9=\x1a\a\x06\v\xb5\x93\xe2?\x1dlK\xbc\xa6K+\xe6\xd0:o\x8cF\xb2\n\xf6\xacj\xf0\x050\xc9'\x90kv\x04\x83t'4r\x00\xcf\x1f\xb0S<~T\x06AȭZA霶\xab\xe5r'\\\x9bL\x14\xaa\xae\x1b)\xdcq\xe9\xf3\x02\x917N\x19\xbb\xe4\xb8\xc7ji\xc5.c\xa6(\x85\xc3\xc25\x06\x97L\x8b\xcc\x13\"}B\xb1\xa8\xf9\x17&\xa6\x1fvt\xedL\xd0a\xf8\x1c\xe0\x19⡬\x80\x8c\x80EP\x81\xc4^\n4E\xac{\xf7\xb7\xcd\x03\xb4\x98\x04I\x05\xa1\xf4[g|i\xe5C\xdc\x14rK:O\xe7\xb6F\xd5\x1e&J\xae\x95\x90\xce\xff(*\x81ҁm\xf2Z8R\x83\x7f7h\x1d\x89n\n\xf6\xce'\\\x90\x93-\x91\a\xe0\xd3\rk\tw\xac\xc6\xea\x8eY\xfc\x93eER\xb1\x19\t\xe1I\xd2\x1a\xa6\x91\xd3́\xbd\x83\x856\t\x13\x86a\xc6;%:W\xaaB6\xe5%\x85\xbb\v4S\x00L\t\xcbG[W2\xd7\xe2F\x9bL#圷4\x94|\x968\xb4\xe2\x17\xf0\x8a720\xb8E\x83>\x1b\t\xbe_+\x1f!\x1c\x13\xb2\xf5i\xa1^\x01\xa7\x12\x98\xe5A\x89\x90\xc3\xd46\xe0\xac}\xc0\x99@\x99\xc4\xf8\xd5\xfd\xba\r\x86-\x13#\xee\xb3xw\x91?4\xb6\x02+\xee3\x87\xcbw'5\x97\xc6z\x1b\x90\xf0\x11\xc1)`\xa0\x05\x168\x8a\xc6 \xa4u\xc8x\x9c$'h0\xae\xbd\b\x9e\xfe$\x92\x10\x8a\xca\x18\xb5I&\xc0(\xf2\b\x0e\xff\xd8\xfc\xeb\xed\xf2\xef*\xd0\x01\xac\xa0\xd4\xcc\xd7z>\xdf~\xd1\xd5{\x1c\xad0ȩz\xc3Eͤآu\x8b\b\r\x8d\xfd\xe9۟\xd3\xfc\x03\xf8^\x19\xc0\x8f\x8c\xaa\xa6\x17 \x02ϻ`֪\x8d\xb0\x81\xf0\x0e\"\x1c\x84+=\xa2Z\xf1H\xe0\xc1\x93\xe0\xd8#Yr \xa1A\xa8\xc4c\xc2~¸\xf5\xd9\\\x8f\xe6\xafd=\xbf\xdd\xc2W\xc1y\xdd\xd2\xcfۀF\x97\xb6\f\r\xacG'X\x99\x11\xbb\x1d\xf6y\xffLY(\xccR\x80\xfa\x1a\x94!Z\xa5\x1a\x80\xf0\x80IN!> \x9f\xa1\xf7ӷ?\xdf\xc2Wc\x1e\x9c\xb8JH\x8e\x1f\xe1[\xf2>\x9e7Z\xf1\xaf\x17\xf0\xe0\xf5\xe0(\x1d\xfbH7\x15\xa5\xb2(A\xc9\xea\x18\x12\xe0=\x82U5\xc2\x01\xab*\v\t\"\x87\x03;\x82ڞ\xb8\xa7\x15\x11\xa9&\x03͌;\x9b$F>\x9c7\x9ay\xd6Ԏ\xa7ًϢ\x9ed\xbd\x9f-\x03y\"'|\xb9\xf0\t\x9c\x18\x96^Wp\xe2\xb1\xc9\xd1Ht\xe8\x99\xc1Ua\x89\x0f\x05jg\x97j\x8ff/\xf0\xb0<(\xf3(\xe4.#ê\xd4\xed\xd2\xf7\xc1\x96_\xf8?\xd7\x12\xee\xdbT\x9fJ\xbd\a\xf2\xf9X@\xb7\xdb\xe55\x1ch\xb3\xfb\xa7Ǯ\x93|\xd8Ąs\n\x93l\xfeP\x8a\xa2lk\xbd\x81\xb7\xad\x19\x0f\xee\x98\xc9\xe3g\xb2\x1d\xe2sc\b\xa3c\x16\x9b\xb8\x19\x93\x9c\xfe\xb7\xc2:\x9a\xbf\x86\xb1\x8d\xf8$\xe7\xf2~\xfd\xe6sZT#\xae\xf1$'j\x980>f=VY\xcdt\x16v3\xa7jQLvS\x0e\xbf\xe6$\xa4\xad@s!\xfd{7\xda\xdc&\xa8\x89j\xa0\xdb\xf3\xac\xfcӱ]\"\xe1\x1bv\xb1ϥ\x85g\xf9uY\x15\x1e\xd8\xce\x023\b\fj\xa6I#\x1e\U0005814cC3A\xe9\x02e\x04]c\x10\x98\xd6\x15\xc5\xf4\x90E$ \xc6\xfc7\xb2\x87YO\xdf)\x86$E\xd9v\xa56蜐\x9f\x919\xef'\x88\xfc\xbe\x8c\xeazv\x85\x92[\xb1\x8b\xdd\xce9\xa7dSU,\xafp\x05\xce4\xa7j\xae\xb3\x8c|\xa0-\xe7\xe9\x7f?\xd8\xdaj\xf8\x85\x06c\x9a\xaaQ\xdbqN\fʦ\x9e\xa3\x92\xc1\xa3҂%\xe6\rZ7\xb3^Z\xb8\xbd}\x8e\x8d\x05\xa5\xbc\xa6\xe4\x0eep\xaa*\x8d\x8a\x1e\x13\xf8\xb62u\xaa\xaf\xf2\x92B\x7f\x86o\xa0\xea\x9eʑ1\xdeY\xba]2\xd93\xe8.\xb7SZ\xf1\xc9\xcc\xd8\rN\x16\x03}O\xea!\xf9\x86\xf63\xbaH\xe1\x91-\xf24\x04G\xd7>\xbdQ\xda}m\x1f\x89\n;\xed\x90w\x8d\xfek$\xfej\n\xc4\xf7~\r\x8fF!j\xecJ\xff\xb1\xaf\v\xc5]\x8e\xa0\rj\x96\xec\n\x81\xef\xdc[\xdf\xc2\xfc\xd2\x06`\xc2Bc\x91\xfb\x0e\xda\xec\xee\x19\x84\xf6\x9d\x893\x87\x19\x9d\xbf\xce_\xa4\x1bS\xe1\xcdo\xf8RrU\x97j\x0ef\xceB\xd6r\xcd?ᴏ\x8d)\x8e\xf5\xe0:~\x05h\xc8}\x15JE\xf2\x96\x89\n9\xb4o\xdcτ\x92\xe3\x96R\x9c\xe0\xe3\xda>ND\xeft\xfdw^\x92\t&\xcc\x13\x9e?R\x98ӧ\xc6\v\x92\\O\xb6C\xa9\xaa(/\xd9\xd49\x1a2L\xff\xe0\t\x12\x0fT\xf7\x17%\x93\xbb\xa4\x93k\x1f\xec\x10*f\x1d\xe4\xfd'\x03)\xe2\x87/\xa6Sʆ/\x9c\xfd\xa8\xd1Z\xb6\xbb\xe4\xce\x7f\f\xbbB\xe7.\x1e\x01\x96\xabƥ\xed\xf7K\x1b]\xd0\xf3\xba\x87ɦ\xd8\xd8\xfb1W\xb6\xcen\xdbT\x95?3\x8c\x1b\xfd\xa7\x1d\x1e\xab\x1c\xd3\x19\xff\x99\xd6\xe19\x04Kf/\xb1\xea\x9e\xf6\xa4\xfcq\x17\xec\xce:d8\x13\xd8\xdf\xe2!1\xdb\xfa\xb9\xc4\xd2}t\x9e\x89\xa5ٗ\x18\xc3\xc5\xd0\x1bOq\xae]K\xc2\xec\xbesH\xac}\xef\xbdʳ\x98\x1d\xf1\xbb\xc6mv\xbd\xf5\xde\xf2\xfcg\v3\xfb\x1b\xe7\x1fL\xf2\xa1\xd8RM\x88\xfe|\xabA\x01Rl\xa4\xc5'\x01ﺜ\x02.\xac\xaeر\xa3ŗ~d\xaa\xe9\xf7\x91ޢZ\x8f\xa9\xf1T*{\xbe\xc3\xdd}-\x92\xaek\xcf\xfb\v\xb8\xe03\xfc\xba:\xed\f\x7f\x8f\x1bΤ\xe2V2mK\xe5\xd6o.\xa8Ʀ\xdb\xd8\xdac_V\xfa\xc0\xe2\x9f\xde⦨\n\tT{\xef\xf6,g1\xfex\xe8\x1a-ތ \\\x88\xfb\xf1[\xa6Ttݐ\x17 \a\xe4\x1fv\xef\xa6_p\xbc\xe8\x82\fs\xb1A\x1e\xe2Q\xaa\xab\xa0\xa4\xaf#\x94\x99\xbf\xb2\xc3\xc5@>&\xe8ό\xe1Iu\x9aMz\xcc\xf9\x00v|\xd3\x1c\xce4y\xf7ܿ\x82_\x7f\xbb\xf9_\x00\x00\x00\xff\xff\xfc¬w\xb0(\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z͒\x1b\xb7\x11\xbe\xefSt\xad\x0f\xb6\xab4d\xa4$\xae\x14o\xd2*Nmbo\xb6DI\x17\x97\x0f\xe0\xa09\x03s\x06\x80\x01\f\xb9\xb4\xe3wO5\x80\x19\xce\x0fH.\xa9\x925\x17i\xf1\xd3\xf8\xf0u\xa3\xbb\xd1`\x96e7L\x8b\x8fh\xacPr\x01L\v|r(\xe9/;\xdb\xfc\xc3΄\x9ao_\xdel\x84\xe4\v\xb8k\xacS\xf5;\xb4\xaa19\xbeŵ\x90\xc2\t%ojt\x8c3\xc7\x167\x00LJ\xe5\x185[\xfa\x13 W\xd2\x19UUh\xb2\x02\xe5lӬpՈ\x8a\xa3\xf1\xc2ۥ\xb7\x7f\x99\xbd\xfcn\xf6\xf7\x1b\x00\xc9j\\\x80V|\xab\xaa\xa6F\x83\xd6)\x83v\xb6\xc5\n\x8d\x9a\tuc5\xe6$\xbc0\xaa\xd1\v8t\x84\xc9q\xe1\x00\xfaQ\xf1\x8f^λ \xc7wUº\xff$\xbb\x7f\x10\xd6\xf9!\xbaj\f\xab\x128|\xaf-\x95q\x0f\x87\xb52\xd0[\x13z\x84,\x9a\x8a\x99\xe9\xcc\x1b\x00\x9b+\x8d\v\xf0\x135ˑ\xdf\x00D\x06\xbc\xa0\f\x18\xe7\x9eSV=\x1a!\x1d\x9a;\x12!\xbbe8\xda\xdc\b\xed\xd3%\xb3\x18{\x03\xf9K\xdf\x11\x9bܞ0[g\x84,R(ދ\x1a\x817\xc6+\x9d\xf6\x9f#\xb8R\xd8)\xbc\x1d\xb3\x04\xd18\xbf\xf14\x18\xdfO\"\xadc\xb5\x1e\xa3\xeaM\r\xb08s\x98\x02u\xa7j]\xa1C\x0e\xab\xbd\xc3v+kej\xe6\x16 \xa4\xfb\xeeo\xc7\xf9\x88\x84\xcd\xfcԷJ\x0e\xc9yC\xad\xd0k\x0eHH[\x05\x9a$Cʱ\xeaS\x808\x12\xf0\xa67? \tr\xfb\xedg\xa1\x90\xe9\x81Z\x83+\x11ް|\xd3hX:eX\x81\xf0\x83ʃ\nw%\x1a\xf4#Va\x04\x9dm\x10\xa4;e\x92\xaaӘ\xcf\xc2\xd8(\xac\x955\xd2\xdfp\xa1\xcfb_\xb9A\x96\xb4\xaf\xd6I\xcd\xfc\b\xa1d\xda\xc8^\x17\xf8,\x03\xeb\x13)\x15\xc7\x1ek\x13\\\u00826*GkO\x18>\t\x19 y84\x9c\xa5\xa8D?\xa6\x05\xd4\xe8J1\x8e\x06\x9c\x82\x92I^aС3L\xdau\xb4\x8c\xa9\n\xdbi\xef\xf7z\b\xe5C+\xaf\xd73\xc1\x14\x86n_\x067\x98\x97X\xb3E\x1c\xab4\xca\u05cf\xf7\x1f\xff\xba\x1c4\x03Ѣ\xd18\xd1\xfa\xd1\xf0\xf5BR\xaf\x15\x86{\xfe_6\xe8\x03\xa0\x05\xc2,\xe0\x14\x9b\xd0z.\xa2\x7fE\x1e1\x05\x8e\x84\x05\x83ڠE\x19\xa2\x1553\tj\xf5\v\xe6n6\x12\xbdDCb\xc8\xed7\x15\xa7\x90\xb6E\xe3\xc0`\xae\n)~\xebd[\"\x9c\x16\xad\x98C\xeb\xfcA4\x92U\xb0eU\x83/\x80I>\x92\\\xb3=\x18\xa45\xa1\x91=y~\x82\x1d\xe3\xf8\xd1[\x93\\\xab\x05\x94\xcei\xbb\x98\xcf\v\xe1\xda@\x9d\xab\xban\xa4p\xfb\xb9\x8f\xb9b\xd58e\xec\x9c\xe3\x16\xab\xb9\x15E\xc6L^\n\x87\xb9k\fΙ\x16\x99߈\xf4\xc1zV\xf3\xafL\f\xedv\xb0\xecD\xd1\xe1\xf3\xe1\xf5\x02\xf5P\xbc\xa5\x93\xc0\xa2\xa8\xb0Ń\x16\xa8\x89\xa8{\xf7\xcf\xe5{h\x91\x04M\x05\xa5\x1c\x86Nxi\xf5Cl\n\xb9&çyk\xa3j/\x13%\xd7JH\xe7\xff\xc8+\x81ҁmV\xb5pd\x06\xbf6h\x1d\xa9n,\xf6\xce'3\xb0\xa2\x03E~\x80\x8f\a\xdcK\xb8c5Vw\xcc⟬+Ҋ\xcdH\t\xcf\xd2V?E\x1b\x0f\x0e\xf4\xf6:\xda\xfc\xea\x88j\xc7\xfem\xa91'\xcd\x12\xb94U\xacE\x8c$ke\x80M\xc6\x0f\x99J\xbb\x00\xfa\x92\x11e<\xe8\x9c\xd9\xd1\xf7&%\xa8E,{\x8e<\xc6;\x1b\x03U5\fT\xfdo\x12#\rje\x85Sf\x7f\x88\x94c\x938\xaa\x1d\xfar&s\xac\xae\xd9ޝ\x9f\tBr\xe2\x1d;\x93&g\x14\xa4z\xa0J\x16\x8a\x0e\xd9D\x1dp\xefh\x1cٹE\x97ެ<\x1aل\x84C\x8e\t\xfd\\r\xbc\xed\x95R\x15\xb21\x9bZ\xf13\x9b~T\xd1q\x18\\\xa3A\x1f\xff\x83\x9b\xd5\xca;cDŽl\xddGH\xb9\xc1\xa9\xc4>V\xe4n\x8e\xa9\xe6\xb8\x1d\u0089\x90\x94\x04\xfc\xfa\xf1\xbe\r;\xadeE\xe8\x93\xc8\xd2\xe7'i\x16\xf4\xad\x05V\xdc\a\xea\xf3k'-\x84\xbe\xfbu\x00\xe1}\xafS\xc0@\v\xccq\x10\xf7@H\xeb\x90\xf1\xd8H\xee\xc6`\xec{\x11|\xeaQ\x90\x10\xaeE1>\x92J\x80\x91\x8f\x17\x1c\xfe\xbd\xfc\xef\xc3\xfc_*\xec\x03XN\x99\x90\xbf\xab`\x8dҽ\xe8\xee+\x1c\xad0\xc8\xe9\xf6\x81\xb3\x9aI\xb1F\xebfQ\x1a\x1a\xfbӫ\x9f\xd3\xfc\x01|\xaf\f\xe0\x13\xa3\xa4\xff\x05\x88\xc0y\x176Z\xab\x116l\xbc\x93\b;\xe1J\x0fT+\x1e7\xb8\xf3[plC'&l\xa1A\xa8\xc4\x06\xd3\xec\x03\xdc\xfa\xe4\xe9\x00\xf3wr)\x7f\xdc\xc27\xc1I\xdcҟ\xb7\x01F\x97 \xf4\xbd\xce\x01\x8e+\x99\x03gDQ\xe0!ў\x18\v\x054\n\x05߂2\xb4W\xa9z\"\xbc`\xd2Sp\xc4\xc8'\xf0~z\xf5\xf3-|3\xe4\xe0\xc8RBr|\x82Wt\xc6=7Z\xf1og\xf0\xde\xdb\xc1^:\xf6D+奲(A\xc9j\x1f\xf2\xcd-\x82U5\xc2\x0e\xab*\v\xa9\x18\x87\x1dۃZ\x1fY\xa7U\x11\x99&\x03͌;\x99\x8eE\x1eN\x1f\x9ai~\xd2~\xcf;/>_y\xd6\xe9\xfdb\xb1\xfe\x99L\xf8\xc4\xfc\x13\x98\xe8_u\xae`bӬ\xd0Ht\xe8\xc9\xe0*\xb7\xc4C\x8e\xdaٹڢ\xd9\n\xdc\xcdw\xcal\x84,22\xc6,h\xdd\xce}1g\xfe\x95\xff\xe7ڍ\xfb:˧\xee\xde\v\xf9r\x14\xd0\xeav~\r\x03m\x1e\xfd\xfc\xd8u\x94\x87e\xcc\xec\xc62\xe9\xcc\xefJ\x91\x97\xed\xad\xaa\xe7mkƃ;fr\xff\x85\xce\x0e\xf1\xdc\x18B\xb4\xcfb)2c\x92\xd3\xff\xad\xb0\x8eگ!\xb6\x11\x9f\xe4\\>ܿ\xfd\x92'\xaa\x11\xd7x\x92#\xb7\x85\xf0=e\aTY\xcdt\x16F3\xa7j\x91\x8fFS\xae|\xcfIIk\x81\xe6L\xf6\xf7n0\xb8\xcd\xda\x13Yw7梴\xdbJ\xa6m\xa9\xdc\xfd\xdb38\x96\xdd\xc0\x16\xc3A\x871\xe9leё8\x99k>\x03\xcfR\xfc\x96p[ID4\xb4\xc5T\xa9B\xe4\xac\x02\xeb\xdbd,VF\x98\xad\xec)\xa0T=r\f\xb7_U\xec\xe1\xf5\xbe\xe0ḧ\x1dB\x1e\x8enQ+#\n!Yu\xf0\xd8\xfe\xea(Y\xcd\xfc_\t[\xad\x99\xd6B\x16\x17q\xdbַ\x96蜐E\"\xd1\xef\x97\xdfO]\aN\x9e\x93\xf3.\xe0\xc3\b\b0\x83\xc0hO\xa4\xaa\r\uecd0uj&(e\xa4\xac0\xa6\xd6+\x04\xa6uEy]\xc8$S\xbe\xa9\xad\xd6\xe5J\xaeE\x11+\xa7S\xa6dSUlU\xe1\x02\x9ci\x8e]ڒǽ_(<\xa3\xf1\x0f\xbd\xa1\xad\xbaϔ*ӻ\x1a\x140\xa7\x9bA\xd9\xd4S(\x19l\x94\x16,\xd1N\x87s☨\xe3\xf6\xf6\x12\x93\n'\xff\f\a\xe1Μ*8D\xc7\x11\xaf!\xf1\x8a\x1d\xdcG:\x9a_\xeaP\f\xfe\xdaНj\x880K\xd7VFc\xb4\xe27c\xd2\xfa\xbex\xd4y\xf0\xa4\xe3\x8e\xe1\xa1\x1f\xf5\x06\n\x9eU\x96\xf2\x85\xf2K\nS\xe19,\xf2\x1e\xd2\x00\xd7>\x92\xd1\x05\xe3\xea\xd2\x14\xdda\xb5C\u07bd!\\S\xb7y=\x16\xe2\vʆ\xc7C\"j\xec\x8a\x1c\xd1N̡\xec\x12B\x8c6\xa8Y\xd2\"\xc0?\nX_\x18\xfd\xda\x06i\xc2Bc\x91{\xdf:Y\xfchL\xe0\xccaF\xf3\xafs \xe9bWx\x9e\xeb\xbf\xc2\\U\xf9\x9a\x8a\x99r\xc8:\xda\xfc\xfbP\xfb0\x98\xa2\xec \xaf#,\x88C\xee\xafܠ$\xac\x99\xa8\x90C\xf7,}1\xf3\t\xd0\xd3d\xecs\x92_\xa3\xb5\xac8\xe7\xb4~\f\xa3B\xe5-N\x01\xb6R\x8d;b\x95_\xdbx\xb4.\x8a\xc9R\xf1sH\x1e\x14\xf70\xe4\xf1'\xb7)\x9a\x84Z\xfa\xcfp\x17a\xf4E\xcdsEJ\x1a\x93r5\x1d\xe4Ӿ\x06Nİ\a\xdc%Z\xdb\x13\x9c\xe8z\x8cn!\xd15\xf9=@\xbf3T\x92S9Mۗ\x94\xd9=\xb6'\xfa\xbe\xf7\xc7\xe5\"\xb6#\xbek\x1cBW\x87.U\xd5\xfa\x00\xffH.\x9bz\x85\x86T\xb1Je\xc4\xc0$\xefk.UL\xe8$\xb4a8\x88\x8a\xf5\xb0X@\xf7\xa7\xdc)\xe0\xc2\xea\x8a\xed\xbb\xcd\xf8\x1b\x1c\x1d\xe9\xf4s\xc2\xe1\\\xb5\xbe\x8a\"ϑ\xbc\xedt\xa5\xba\xfb\xd1B\xfa~z:Ӈ3پ\xef\xef~\x8c\xf0yV8\x91w\x0e\x7f\x1cr\x8d\x81,\a\x12\xce\x05\x8b\xf8c\x95\xcb}\xfcp\x99?ӽ'ٛ4z\xe4\xbc';>y\xf5[\x9aU\xf7\x1e\xbc\x80\xdf\xff\xb8\xf9\x7f\x00\x00\x00\xff\xffϡa\xa5-&\x00\x00"), []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?\xbfp\x10jQ\u0605\x90\xdd~\x82>Ǟ\x8e#\x88\xa3\xafc\xba\xb9U\x1f\x8d\x9b\x90ϢO\x04\xe6\x88\xf1\xacT\x1e&\xfaF\x14\xc0\xcc\xceX(\x83\xaaoåN\f8,\xe4`\x15\x85\ac\x90\xde~P\xe3\x04\x91uQ\xf0u\x01\x97d\x19\x0f\xd0l\\I\x8f\x11\xed+\x18+\xb2S\x92\xccA\x1c!\x98\xf6?\xf4(C\xf1\x16\x7f\x00\xc6#\xe0==1\xb8+\x8a\x0e\xd1\xfbԊ\xe2Vi\xc8\xd0\xf1\xbf\xf4\x01\x85\x80\x82\x82\x18\xa9X\xa1\xe4=h\x87Ec\xe0\xd1\xc0\x00\nh\xce\xd0W\xd7h\x96\x85d\x9b\x1a\xdd\xf8%C-\x11\x95\x11!\x8d\x05\x1e\x11\xe6\x13\xf0\x0e\xbe\xa1E\x80\xfc\xaa\xa8\x8d\x05}\x93\xa9\n\xf2\xb0j7j\xceRy\xf8\xe1 d\x1f\xf4\x15\"\x03\xe4C\xe6*-he,&\xdam\xfc\x87\xe6\x91\x16\xf1\x90\xd5~\bm`7\xa9[\fXlx\xfe\xfb\xf3\v\x92\x80~\xef\xfd~\f\xe3\x1a\x1a2\xcd\xd2\xcd\xe4&\x8d\xb7\x10\x16\xca\bu'u\xd4\f\xbes\xad\xf9\xee\x00כ\x15\xc8\x17\xe0{\f\xf6\x80\xf32T\xfbN\xbc\x1f\xf6\xff\xaf\xc8\xfd\xd3\xf2\xdb\xd0*>\x17\x12\xf9\\\bc{l6n\xe9\x0f\xc9:\x16w{\x02I\a\x13\xd5\xe4\x14W\xffA\x88yҹ\x13\x9b,\x8dl\xfa\t\xf0OEɭR\x0f)\xd4\xfb\x1f\xac\u05ee\xfb\xb1\x8cv\x9a\xd8\x1a\xb6\xfcQ(m\x86k\xcb\xf0\r\xb2\xdaF5\v\xb7,\x17\x9b\rh\x84E{#\xcdV\xca!b\x1d\x8e\xf9XGeE+\f\xc6\xd52\x1dYJԈ\r\x85\xa2\xfa(T\xe7\xe0`hA\x0eD.\x1eE^\xf3\x82|\t.37>\xde\xe0\x17\xd3j\x13\x02\xb1\x87\x7fT\xaa]q\x0eM\x18$2\xb1\xb7T\xa8$\xa0\x8f_bl\xb4_5N\x89\xb0\xfer\xb0od\xa6\xae\v0\xbe\xbb\x9c\xdc\xe4V']\xb4\xccr\v3\x05_C\xc1\f\x14\x90Y\xa5\xe3\x14J\x91\x03WR\x95n\x84\xb8#Z\xb6\x1fm\xb5\x83\x99\x00\xcb(\xc4݊l\xeb\xdcW\x144\x82\xc5r\x05\x86\x96\x92xU\x15\x11\xd3ՖI\xe1\xf0\x9dM鍶$h\x90!ܘ.iK\xa2~n\xcb(\xd9۹٧\xfa\xf8\xe6\xc8(\xbe\xffJD\x0fV\xe7Ha\x9f\xd0$\x8c6Y\x92\xe7C\x94\xf4Hq\x01f\xd9Y\xd2\x146|Mah\xcf\x7f\xdc\xdb\x7f\xda#ʯ\x8bw\xc7M\x98\x19\xac\x9b\x9cS/˸\xa6\x9b\x7f\x12\xbe\x91ɺ\xf1\x16k\x16\xcf>u[^\xd0V\x8agH~\xc16\xa2\xb0@N\xd5\x14\xa2l\x06\xe7NI\xa0T\v\xcchg\xddf\xdb\x0f͆[B\x8b\x01\xad\x86\x00\x9c\x83\x1e\xa2\x1c\xe2A\x02Hָ\x16\xb4\xd3,\xdcJ\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\xffp\xc18{\x80\x9ds\xb1\xb8d\xc87\x1e*'\xa2\xa0\x81\xd2(HS<\xc0\x8e@\x8d\xe7E\x8c\x979\xd2\xe2\xca\x03\x8cl\x95\xc6J\x8f\xae\x88\x9f߀rt\xc3\x0fD\x98\x94\xd9Ԗ\x86\xa8~\xfa\x8cd%\xc4\xcb\f\xbd\x14J\xe0ˑ\xc3N\x16\xa7n_\xfdL\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\xfb\x87\x03t\x96\x04\x89\xd8\u0378q\xb6\x14\x05\xb5\xe1\x870l%1$s$\x9a\xd1\x1d%X\xb9.]gemh\x7fZ*\xb9p\xcbbc\xbdy\x1e(\xddc\xc1I:\xf6\x9dޢ1r\xbf\xb8T\xaf\x82g\x90\x87-:\xcaA\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\xa2\xceXY\xe0\\O\xac\x19\xa4%\xa9z$\x8d\xe9p\xf5Tb=\x93L\xe4E\x90ە$\x05\xddl\xe0y\xd6k\xa6\xdc\x1c\xa3b:cq.@\xc9ik\xedoh\xe9i6\xfe\x9dU\\h\xb3d\xef(\x1d\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\xa3\x9c\xd4mWa\x9d\xaf\xe4\xb9\xf3e\xf6\x14O\xe3\xf8(Y\xec\xd89\xfdv\xfe\\\xf7n\x86DϨ\xda\x13\xe5\x92W\xe9\x92L\xc9\xc6s\x02\r\fփC\x84\x8d\x9b\xac[\f\x10\xa6(\x90,ʕ2\x91d\x91\bZ\t\x82~\xad\x8cu\xeb\x90=\x7f\x7ft\xa1R\x85\xc5I\xc67\x1643V\xe9\x90NJ\x8a?e)\xbe[n\xb7`\xc0\xefC\xf9EO\a\x18\xa3\xd8\xf3V78\xabr\xee\xf6¨#\x9e\x91\xf7Dm+\xad20Ѽ\x88\xb6$ڦ\x91\xac\xa5.\x1d\x9au]\ue8bfM\x92\xd6NY\x94\x0ee\x9e#\x8f\xa4;\"2\xfa\xf0\xad\xb3D\x8d\xda\x05\xffN\x91\xd6cpdt\x00\xa6,\xf90\x87:\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\x90:\xe9\xb2\xcd\xe7\xc4ہ\xa4[\xfe\b>\xdd\x17d\xa6jIKa\xa8\a\xb0\x9b\x19\x10\x1dk\x9c\x15H\xb4w\x9d\xc6\xd1\f̱\xb2 I\x12rrݬ\xdb\xe4#\x17i\xebV\xec8\xb6\xdaC9\x9cc\xe5\xf8y\x14\x12<\xbbg\x10J\xfeM\x94u\xc9x\x89<$\xb7C\x94\xd0\x1cCp\xecn\xd2>\xb1\x05\x19-\xabp\x96U\x05X\xf0i\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\xb4c.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;z\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*\x9fbK76D\x80\xd1%\x1e<\xa7\x9b\x1b:\az\xbc\x1e\b\xf7K\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~3d(\xd3\x0e\xc0\xde\xc5Σ\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^\xc4\x17\v\x9a\xb7\xb0k.\\\xfaY\xd11\x7f\x7f\xdbؗ\xafͬ]\x0e\"3n\xd8\x13\x14\x05\xe31\xfd\xb2G\x85\xcc]\x00\x9b\xa9\x05\xa0}GM\xe5/\x94\xf2\xb7\xc6^\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]0\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ɿ\xe0@\xafu\x1c\xda\xcaG\xc9\xecxD\xbd\xbd\xd5\xd6]\xeb;\xc4\xfe\x19\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\xee\"\xffc\xd7{*\xf1'zg\xeb\x04+\x87\xef\xaeW\x04+\x88\x11=\xe0\xd5$Y6,_\x03\xba\f\xed\xd8\x0f\xe9\x93զ\a\xb5\x9f\xe7\xdc}\xa5\x04r\xf7$Mp[\xbcj\xce\x14j\xad\xeb\x95\xc3\xe5PO(_\\\xee\x98\xf2o\x8e\b\x9d/*\xae\xed\xce%D]\xf4\xf0\bv}j\xe5\uf835\xda\x7fr\xa7[zd\x0f\xaf\xed\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\xca\xc0_\xd4#\xbc\x8f\xae\xbe\xf6\x9f\xdd\x194\x19I\xaf\fP\xe9\xa2\xfc\b\x05ۜJ\xba\xa7\xfcyj/\x9e/\x19P\xf1\xf7\x9c?gq\xf2\xa6\x0fj\xfc%\x1a\xba\x05>t\x1a\xf3\xaa药\x1d\xbb\xbe\xa3\xb8\xb5Q\xa5~\xea\xfb\xb85,O\x86$\x89\b,!\x0f>\xces*2Z\xa5\xf9=|R\xeeQ\xa5\x141\xe9\xb7\xe8=\xb9\xe5=\xb7\x90s\xee'aL\xd1\xfb\xb1\r\x01\xb6gL\xf6\x1e+@l\x8f|\x8e\xc1\xda\xe292r{\xfbɍ\x94\u07b2y\uf7e5A}l\x00Y\x10(࠭\xf1\xbf[\xf5D\x97\xf8\xc7ט\xc3\xcb1\x9d\xc7\xeb\x80\x0e\xbbP\x1a\xf2Qì\xabB\xf1\x1c\xf4\x15\xbd\x9e\x930\xe2\x9fz\r\x06\xee@\xff\r\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˸\u05cc\xc2\xf6\x9a0ͫv\xfb\xf4\x9cP!qK7\xee\xb0-\xc6\x1f\xe1Z4\x8f\x85\x9d%\x90۽\xe1\xd4\a<\xfe\x96\xa1{\xec)㕭uЮ\xb5\xa6\x9b\xe2\x11\b\xb8\x8bԏ{Ͱ}\xe4\xee\x18\x06\xb7\xaf̵\xfb\x0f\x93\xef\xe0\x8e\xc0i\xde#\x8c>n\xe6\"j\xf7N\xed\x02\xe1\x1f\xc7\xe3\xd1\x19\x838߸G\xeb&\x88\xf0\xa9\xad96\xe0f\x188d\xff\fޫ\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\x011\xefH\x8e\xf7һ_\xeau\xfb(\x04\xfb\xdb\xdf\xcf\xfe?\x00\x00\xff\xff\xf8FZ\xc4S}\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"), } diff --git a/config/crd/v2alpha1/bases/velero.io_datadownloads.yaml b/config/crd/v2alpha1/bases/velero.io_datadownloads.yaml index 36ab864f9..fa3757a9d 100644 --- a/config/crd/v2alpha1/bases/velero.io_datadownloads.yaml +++ b/config/crd/v2alpha1/bases/velero.io_datadownloads.yaml @@ -92,7 +92,7 @@ spec: datamover: description: |- DataMover specifies the data mover to be used by the backup. - If DataMover is "" or "velero", the built-in data mover will be used. + If DataMover is "" or "velero", the built-in fs data mover will be used. type: string nodeOS: description: NodeOS is OS of the node where the DataDownload is processed. diff --git a/config/crd/v2alpha1/bases/velero.io_datauploads.yaml b/config/crd/v2alpha1/bases/velero.io_datauploads.yaml index 6aed785d3..5e1fd4124 100644 --- a/config/crd/v2alpha1/bases/velero.io_datauploads.yaml +++ b/config/crd/v2alpha1/bases/velero.io_datauploads.yaml @@ -124,7 +124,7 @@ spec: datamover: description: |- DataMover specifies the data mover to be used by the backup. - If DataMover is "" or "velero", the built-in data mover will be used. + If DataMover is "" or "velero", the built-in fs data mover will be used. type: string operationTimeout: description: |- diff --git a/config/crd/v2alpha1/crds/crds.go b/config/crd/v2alpha1/crds/crds.go index 485fafa80..96990c557 100644 --- a/config/crd/v2alpha1/crds/crds.go +++ b/config/crd/v2alpha1/crds/crds.go @@ -29,8 +29,8 @@ import ( ) var rawCRDs = [][]byte{ - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcYK\x93\xe3\xb6\x11\xbeϯ\xe8\xda\x1c\xf6\xb2\xd2d\xf3p\xa5t\xdb\xd1\xc4US\xf1Ϊ\xac\xc9\xdcA\xb2E\xc1\v\x02\b\x1e\x92\xe5$\xff\xdd\xd5\x00I\x81$4z\xd8^\xdd\x044>|\xe8n\xf4\x03\x9c\xcdfwL\xf3W4\x96+\xb9\x00\xa69\xfe\xecP\xd2?;\xff\xfa\x0f;\xe7\xea~\xf7\xf1\xee+\x97\xd5\x02\x96\xde:\xd5\xfc\x88VyS\xe2#n\xb8\xe4\x8e+yנc\x15slq\a\xc0\xa4T\x8eѰ\xa5\xbf\x00\xa5\x92\xce(!\xd0\xccj\x94\xf3\xaf\xbe\xc0\xc2sQ\xa1\t\xe0\xddֻ?\xcf?~7\xff\xfb\x1d\x80d\r.\x80\xf0*\xb5\x97B\xb1\xca\xcew(Ш9WwVcI\xc0\xb5Q^/\xe08\x11\x17\xb6\x9bF\u008f̱\xc7\x16#\f\vnݿ&S?p\xeb´\x16\xde01\xda;\xccح2\xee\xf9\x88?\x83*\"Z.k/\x98\x19.\xba\x03\xb0\xa5Ҹ\x80\xb0F\xb3\x12i\xac=l\xc0\x98\x01\xab\xaa\xa0>&V\x86K\x87f\xa9\x84o\xe4q\a\xb4\xa5\xe1\xda\x05\xf5\xa4|\xc1:\xe6\xbc\x05\xeb\xcb-0\vϸ\xbf\x7f\x92+\xa3j\x836\xf2\x05\xf8\xc9*\xb9bn\xbb\x80y\x14\x9f\xeb-\xb3\xd8\xceF\x1d\xaf\xc3D;\xe4\x0e\xc4\xd7:\xc3e\x9dc\xf0\xc2\x1b\x84ʛ`[:w\x89\xe0\xb6\xdc\x0e\xa9\xed\x99%z\xc6au\x92H\x98'8\xebX\xa3nj\x92\xa5\x91R\xc5\x1c\xe6\b-U\xa3\x05:\xac\xa088쎱Q\xa6an\x01\\\xba\xef\xfevZ\x17\xad\xb2\xe6a飒C\xc5<\xd0($Ñ\tY\xa9F\x93ՎrL\xfc\x16\"\x8e\x00\x1e\x92\xf5\x91I\xc4M\xc7\xcfR!\x97\x03\xb5\x01\xb7Ex`\xe5W\xafa\xed\x94a5\xc2\x0f\xaa\x8c\xe6\xdbo\xd1`\x90(\xa2\x04y/p\xb2\x9d2Y\xd3i,\xe7Q\xb6\x05\xeb\xb0F\xf6\x1bn\xf4\xbb\xfbVi\x90e}\xab\x8bA\xf3 \xc1\x95\xcc;ا\x1a/r\xaeT\x89RU\x98hl\xc0\x89[\xd0F\x95h\xed\x1b\x0eO\x00\x03\x16\xcfǁ\x89j\xa2\xc4\xee/L\xe8-\xfb\x18\x83L\xb9ņ-\xda\x15J\xa3\xfc\xb4zz\xfd\xebz0\fo\x04\fV:K\x91\x82\xe8k\xa3\x9c*\x95\x80\x02\xdd\x1eQF\xd37j\x87\x86\x02`ͥ\xed\x11)\x9cW\xa9\xc01\x98\x93\x7f\a<\x9a\x8d\x93\x06\x83\xf7\x10A\x93Z\x1fhO\x8d\xc6\xf1.|\xb6\xd8\xc7̓\x8c\x8e\xce\xf1\xbf\xd9`\x0e\x80\x8e\x1eWAE)\b\xe3\xb1\xda؊U\xab\xadhn\xa1\x94@6\xd6\"y\xe1gJ\vK%7\xbc\x9e\x1e<-\x7fO\xb9\xc8\x19\x9df\x1c6ْNA\xdeILf!C\xcd:ץо\xe1\xb57\xa7\xec\xbf\xe1(\xaaI\xfc9y\x93\xba\x03\x87]n\xb1qO\xbd\xbb]mVKR\xafS!B\xd9P\xef&\xae9%\t\xf0\xb4I\x10\xb9\x85w\xef@\x19x\x17\x9b\xa5w\x1f\xe2jυ\x9b\xf1A\xfe\xdfs!\xba]\xae\xf2n\xaap\xbe\xacϜ\xfc9\b\x11\x9f/\xebkk\xab)\x1b\x94\xbe\x99n8\x03\xe6\x9d\xca\f\v.\xfdϙ\xf1=\x97\x95\xda\xdbk\x0e\xdb\xd77Tb*\xefn1\xf8\x97\x11\xc6\xc8\xee\x8e\n\xe2`k\xa7`\xcfxRc\xf4\xbb\xdb\x0f\x19\xdc\x027\x94\x90\f:o$\x85\x034\x86\"\xb4\r\x90\xcaOj\x9e7Oj%\xd3v\xab\xdc\xd3\xe3\x993\xae{\xc1.\xee>=v&~\r^\xd7\a\xdfV\x122V\"\xfa]\x15Y\x85\xb4~\x13\xdb5\xff\x05/\xe4K\xa2\x1dc\xa1j^2\x016\x8cɶ\tl\x0f\xd1aO\t\xe5\xfa\xbc1ݴ[K\xf8\x86ڧ\x7f!\xb8ō\xd6C\x88\xee(\xca\U0001a4f3\xc8~\xe6x\xc7vJ\xf8&\x88\x92I\xb0\x02\xafO\xe8\x1a(}P\xb1U T|\xb3AC\x15U(\xb7\xe2ƫ\xd7\xe5{\x9bl\xc27\xe9\x1f\xcaT\r\xd3\x1a+\xea\xed\xc8\x19[\xdb^eU\xc7L\x8d\xee5\x90>\xa3\xa2\x97D\xb4S\x05\x95fd\xa0\xb6\xf6\x0f\x97+\x88\xc1\xeau\x99\xa9\xd4\xe9\xb7z\x9d2<]\xc7\xd0oc_\xe8\x04\x99\x99\x11\xc5\xef\xd7$ؑ\xdbp\x81`\x0f\xd6a\x13T0b\x18-\x95\xb3˙\xb4\bG3\\\xc0i\xe2>\xed\xf6=\xc6-\x04\xf4\ue09dW\xaf\xb92\xad\xb7\x0f\xb8-s$\xd1v\xfdP\x1c\xb2\x98\xd0Řֿn\xe3[^Dx\xf9&\xe3\xe5\x98\xf2\t\xbe\xc5\xe17S\xa6*\x90\x1b\xacr9\xf0\xb4\xe5f\xa0w\xd9\xc1\xf2\xf2Z'\xbf\xf3,_Џdƹs4}L8\xe3\x89a\xa0\x1bͦ1\xe2\xa2\xce'\xbc\xcb\\\xda\xfb\xc4\xd7\xd6\xd6\xec\xa57!\n\xb6o\xb0jsc\xf7\xc3\xca\x12\xb5\xc3\xea\xe1@e\xd1\x05\x95\x13\x11\x90o\xbfJ\xfd[\x1f\xeb&\xd4\xec\xda\x16\xa5\xa3Կ\x9cݒ\x91>\x8dA\xc2\U000c9a52\xbafJ7ֶ\xa7I\x03\xbcP\x0e\x0e\xed\xff\xfbX\xcaвP Q\x89?\xd9\xf4d\x96\xa6\xfe~F\xeb'\x12\xd2\v\xc1\n\x81\vpƟ\xeau\xf2\xad]|\x88N\xdf\x1co\xea\xf3\xa60Sݱ\xfe\x95-\xbc\x86vO\xe09\x95\x1d\xf1z\x85E8\xac\x00w(\x81\xbaw\xc6\x05V\x1df\xa6\xe19\xa7\xf9\f\xe9i-\xfdG*\xbfAkY}\xee\x02}\x8eR\xf1a\xaa]\x02\xac\xa0\xc2{\xdcv\xbc\xb7\xedݾ\xba\x01\xfa}.\xf1\x85\xed\xcf\x1b\\B\xb3~\x86̊dr1\xad\xa7v:\xa8\xc1\x1b\xdd\xd73\xee3\xa3\xdd\xfd\xccL\xad\xdaK\x9f\x99\x9a|\xd3J'\xe3\xabH.1vsY\xcc\xfe\xa3Qf\xee\xfbp\x19\xae\xd2t\xcb\xef\x96\xeb\u07bf\xadl\x95\xe8nx\xf8\xd8#}S\xa0!3\x14\xb9\x0e$<\xc9'V\xcb\x15\x7f=B\xdfL\x05\xa89\xbcl\xa94\x89\x0fB]{Yq\xab\x05;\xf4\x87IK\xe6\f\xf8\xf1\xd6L\xde\xfb\xaf\xad\x9a\xfb\x8fo\xf9\xca\xeb\xed\xce\n\xcetWa\xbe\xff\xa8\xf6\xc7\xec\xf0\xc6s\xd0\xf0#\xe7M\xbd\xdd\x00\xe1\\*h?\xba^\x1f\xc1\x87\xdb|\xcb\xe0\x9d\xd5\xded00\xaf\x12\xec\xf6\xf96\x1d\xf1E\xffMc\x01\xff\xfd\xffݯ\x01\x00\x00\xff\xff];\x85{\xd8 \x00\x00"), - []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcZI\xb3\xdb6\x12\xbe\xbf_\xd1\xe59\xe4b\xe9ų\xa4\xa6t\xb3\xe5Iի\x89\xedW\x96\xe7\xdd!\xb2)\"&\x01\x0e\x16)\x9a忧\x1a\v\t\x92\xd0\x1a'<\xb8\xfc\xb04zC\xf7\xd7\r-\x16\x8b\a\xd6\xf1\x17T\x9aK\xb1\x02\xd6q\xfcŠ\xa0\xbf\xf4\xf2\xeb\xdf\xf5\x92\xcb\xc7\xfd\x9b\x87\xaf\\\x94+X[md\xfb\x19\xb5\xb4\xaa\xc0\xf7Xq\xc1\r\x97\xe2\xa1E\xc3Jf\xd8\xea\x01\x80\t!\r\xa3aM\x7f\x02\x14R\x18%\x9b\x06\xd5b\x87b\xf9\xd5nqkyS\xa2r\xc4\xe3\xd1\xfb\xef\x97o~X\xfe\xed\x01@\xb0\x16W@\xf4l\xd7HV\xea\xe5\x1e\x1bTr\xc9\xe5\x83\xee\xb0 \xb2;%m\xb7\x82a\xc2o\vGzv\xdf3\xc3\xfe\xe5(\xb8\xc1\x86k\xf3\xcf\xc9\xc4O\\\x1b7\xd95V\xb1ft\xaa\x1b\u05f5T\xe6\xe3@y\x01\xa5\xf5\x13\\\xecl\xc3T\xba\xe5\x01@\x17\xb2\xc3\x15\xb8\x1d\x1d+\x90Ƃ\x88\x8e\xc2\x02XY:\xa5\xb1\xe6YqaP\xadec[1\xd0G](\xde\x19\xa7\x94\x81SІ\x19\xabAۢ\x06\xa6\xe1#\x1e\x1e\x9fij\x92;\x85\xda\xf3\n\xf0\xb3\x96♙z\x05K\xbf|\xd9\xd5Lc\x98\xf5zݸ\x890d\x8eĭ6\x8a\x8b]\xee\xfc/\xbcE(\xadr\xf6$\x99\v\x04Ss\x9d2v`\x9a\x98S\x06˓l\xb8y\"\xa6\rk\xbb)?\xc9V\xcfP\xc9\f\xe6\xd8Y˶k\xd0`\tۣ\xc1(D%U\xcb\xcc\n\xb80?\xfc\xf5\xb4&\x82\xaa\x96n\xeb{)\xc6jyG\xa3\x90\f{N\xc8B;TY\xddHÚ\xdf\u0088!\x02\xef\x92\xfd\x9e\x13O7\x1d\xbf\xc8ʓ(\x14\xb6(\xeec\x88\x0f\xbb\xe7ܤ\xa4\xd3\xd9Nq\xa9\xb89\xae\xe0\xcd\xf7ײI\xb7\x02d\x05\xa6FxNJ\xaf\xb6\x83\x8d\x91\x8a\xed\x10~\x92\x85\xf7\xb1C\x8d*\xf8\xd8\xd6/ѵ\xb4M\t\xdbh\x18\x00m\xa4\xca:[\x87\xc5\xd2\xef\nt#ىǍ\xcf\xfc\xc6w\xa1PȲw!Fɥ[\xc1\xa5\xc8_\x88\xb7;\xbc\xea2\xa4\xda\x14\xb2\xc4^u\x98r\xc45tJ\x16\xa8\xf5\x99\xebI\xdbG<|\x1c\x06fj\xf1+\xf6\x7ffMW\xb37>\x18\x165\xb6l\x15v\xc8\x0e\xc5\xdb秗\xbflF\xc3p2\xb4\xb1\xc2h\x8ai\xc4z\xa7\xa4\x91\x85l`\x8b\xe6\x80(\\x\x85V\xeeQQ\x90\xdeq\xa1\x81\x89\xb2\xa7\t\xe9\x82!Ր\xeb;z4\xeb'\x83;\xc9\x0eUjvre\x1a3<\xc6x\xff%i1\x19\x9d\b\xf1\xbf\xc5h\x0e\x80\xe4\xf6\xbb\xa0\xa4\xfc\x88^\xaa\x90\x02\xb0\f\xaa\xf2v\xe3\x1a\x14v\n5]/\xe7U\xb2\x02&@n\x7f\xc6\xc2,'\xa47\xa8\x88L\xbc\x0f\x85\x14{T\x06\x14\x16r'\xf8\x7fz\xda\x1a\x8ct\x876̠6\xeeB*\xc1\x1aس\xc6\xe2\xeb\x89\xf6\xe8k\xd9\x11\x14ҙ`EB\xcfm\xd0S>>H\x85\xc0E%WP\x1b\xd3\xe9\xd5\xe3㎛\b\x16\nٶVps|t\xc6\xe0[k\xa4ҏ%\xee\xb1y\xd4|\xb7`\xaa\xa8\xb9\xc1\xc2X\x85\x8f\xac\xe3\v'\x88p\x80aٖ\x7fR\x01^\xe8ѱ3/\xf4\x9fK\xf47\x98\x87\xf2?]\t\x16Hy\x11\a+\xd0\x10\xa9\xee\xf3?6_ r\xe2-\xe5\x8d2,\x9d\xe9%ڇ\xb4\xc9E\x85\xca䀹l\x1dM\x14e'\xb90\ue3e2\xe1(\fh\xbbm\xb9!7\xf8\xb7Em\xc8tS\xb2k\a\xa8`\x8b`;\n\x05\xe5t\xc1\x93\x805k\xb1Y3\x8d\x7f\xb0\xad\xc8*zAF\xb8\xcaZ)L\x9c.\xf6\xeaM&\"\xd2;a\xda!|l:,Ȧ\xa4V\xda\xc4+\x1er\t\xc5\x00\x96\xac\x1ck'\x7f\xed\xe9˦\x90\xe9\xa2K\xaeF\u07fb\x1c\xa1ȫH\xe2wLu!35\xe3̔~C\x90\x0f{\x14vRs#Ց\b\xfb\xd48u\x83\x93\x16\xa1\xaf`\xa2\xc0\xe6\x1e\xf1\xd6n'pQ\x92Ʊwc\n@\x9e\xaacT\x8a\x9d\xa4\x8b\x95\x18\x02\x9e\f\xad \xaf\xd6h\xf2b\x8aL*\xe3\x02\x06\xd0\v)\xb8\x9d\x8a\xba\x95\xb2A6\xd5`\xa1\xf9F\xb0N\xd7\xd2\\\x10\xf8\xa9\x82\xb8\xf2˱C:|\xbdyzM\xff\xc4q\xf2\xa0=/C\x88\xa7[Fh+o\xb6`\xe7\xf5\xe6\tt\xd8>7\x92\xb0Mö\r\xae\xc0(;\x17\xec\xb4\xc3:\xee\x15ߣ\xca\xcdLo\x8e[\x18\xbd\xd0o\x03\xab\x1d\xa8vC/T\x90`\x94r-\x85A\x91\xb3\xd1Y\xaf\xa2/J\xban\x98\xce\xf2<\xe1l\x93\xae\xcf]\x93H\x10\n\xb7\xc2\xd4,\xcf\x17\xf8\xa4\xeb\xe4\x186\xf1\x1e\x9b\xc1\x81\x9b\xfa.\x89\xfc\x05\xbdZ\xa0dyV\x9ep߽8\xb2:#\xcc\xf3\xcb\xda\xc9{I2J7\xf7H\xb6\x1f\x19\xfd\n\xd9\xc6^\x92\x93n\xc2\xe5)\xe1$E\x01\nfX\x82\xedn睂\x0eWX\xcey^\x8c앙\x1e\v}\"\x92\xcc2\x13\x04\xd0\xf9\x81`\xe5Z\x8a\x8a\xef\xe6g\xa7e\xfe\xb9k{V\xb4Y\xc6K\x8e$\x8dS\x82#N\x16\x0e\xe1.b\xf6#lX\xf1\x9dU\xa7\xa2Qű)g\x00\xe6b\x00\xba\xa0\x0f\xc7\xc4=y\xa4\x97,\xe6\xef\x10R\x13d\xef\xbd$\x8dR>\xfd\xcde\x00\n\xdd\x03E\xae\xe1\xd5+\x90\n^\xf9^ѫ\xd7~\xb7\xe5\x8dY\xf0Qyq\xe0M\x13O\xb9)\x83\xf6%\x05\x15t\xd2^J-Y\x1d|\x9aИ\xa8\xc2P\xf1\xe9\xc47\x12\x0e\x8c'\xb0\xbe?]\xbf\xce\xd0\xddbE\x18P\xa1\xb1JP\x16F\xa5\b\x16iGR\xdaL\x1a:#i\xc7\x14\nse\n\xcd\xca\xf9<\xa20\x91ғ\x1f\xe2\x9a\vx\x85Un4\xe0\x1d\xd7\x18 EHq\xc2\xf8\x04\xa8=\xae\x1f\x8cϬ\x89\xa6O,^\x11ru\x83\n\x8b\xe4\x8c\x18\x9e)\x98\x85(\xc6t\xe0\xee\xaaC\x85\x148?\xce9X)\x81Ae\xc9\xd5\xdcaW\x90c=\xae\xedU\xf3\xf4\xfe\x8c0\xb3\xd5\xe7\xb8?cm\x9d\x00\xa0\v\xb6\x9eb%\xe7\xb3\xf4\xffi\xe6N\xc3}F\xf4܍>ǡ+\xd0~\xdc\\\xc3a\xb24rX\xf1\x06A\x1f\xb5\xc1v̭\xaf\xfb\xbc\xe9\xef`\xa8o\xff\xdesC6c\x12\x91W\xa9\xf8\x8e\xd3}\x17\xfd\xccP\v\x04'\rM3\x97H\x1d\x12\xc8:k\x9f\xac\x9d\x7f\x0f\xe4(\x9b\xf8\xc3\tl0Q:\xb8\xdaϗ!\xf2g\xf2\xc6E\x85<\xbf\xac\xaf2\x0f\x1d\x9cA\x124|\xa8yQ\x8f}\x89\xcfs:\x80a_ѕ~7\xb0\x99\x87\x10\x8b|!8Y3\r\xfe\x93\xe9\xf4\x0eM\xa7Ɔ\xce\xce>\xbf\xac\xaf*\x96]\x1f\xef\xbarٿ#\x04-\xc7\xe0\x1a^\x17duW\xc1̊\x02;\x83\xe5\xbb\xe3GY^r\xfa\xb7\xa3\xc5Ĉ\xb8\xa6\x93\x991\xb5\xebm\"\x05\xb6\xdb\xf2ud\xb7\xef\xbf\xdesM\xdfN\x89\xb8N\x9c*\x93|=\xaf_}\xf4;\xcd4\xc0\x17rp\xd7I\xfaΧh\xda\xe6\x12?]\xcf١3\n\xb1\xe5_2\x83\v\xda\x7f\x1f\xc8\xcbw\n\xfc\xf3Kڹ\xbe\xabm0'3\xd7\x1d\x8b\xb9ص\xd4\xe3\xbbONc\x03\xb9^_\x9e\x1a\x96\x80{\x14 \x05T\x8c7\x04\x1d\x1d\xc9L\x00;O%`(\xff\xc8\x17[\x84\x11*d{\xb5\x97-\x99Q\xc2<\x9a\xfd\x9e\xc6\xec+\x98Ϩm\x93\xc1r\xbfc\x05\xe3\x8f\xf4\xcd*\x9d\xad`\xcewS\x18a\"剄\xb8q*h]\xad\xa4lY3}\x1a\xbb\xd44\x9a,\x87Z6\xc1\xa9\x85m\xb7\xa8\x88[\xf7@\a\x02\x0f\x04L\x8b\x9a\x89]\x16\t\xc5\a&\x84\x86is\n,\xe6^\xf8\xa6\x92\xa5/r\xc3ע\xd6lw)X\x7f\xf0\xab<\n\r[\x80m\xa9@\x19k\xfd;\x1dr\xc8M\x91X\\N\x177%\x89\xd1s\xd7͜|\xda\\\xc1˧\r\x1d\xf2i\xf3[yAa\xdb\\˂*\x95\xccpÅ\xfd%3~\u0894\x87y\xe88[ę\xfa\x82\xa0\xcf\xcc\xd4=H\xa6Z\x85\xf6̰|@\x9d[\xa4\x98\xf8\xad \xbdk\xea^b\x8f\xd6\xe4 \f^\x13\x0eNi\xfe#\x1e2\xa31\xe5f\xa6\x9eC\x1e\xcfL\xcd~\x9a\x91N\xfa\xbey.\\ƹ,\xcd\xfe\xd7\x0f\x99\xb9\x1f]\x82\xbbIρ\xbf\xbb\x8a\xf8\u0601\x1f\xe2\x9b\xfb1\xc3,ʍ;\x81TR$\x16\xcb\x10N\xf6\xf7u\x8c\xa3\xb4\x84/5\xd7\xf1\xcd 6BJ\xae\xbb\x86\x1d{Y.\xa5\x8d>nM߂\xe7Nr\xbe\xd9\xde\xff\x86$\xdf(=\x1f\x95\xe1Bdv\xf3\xf2t\xca\xf9\x16'\x9c\xc9yC\x8b\xe1ʚ\xff\xe9}\xbc\x8a\xbcDaxœ\xf7\xf7\xa1Xs\xef99]N߱n\xab/G\xbf,\xba\xab\xde\x1eQ\xb8\x80D\xc3\x0f\x9drxoC\xc1\x80B\x90{\xf1]O\x7f\xe3\xf1\xba\xcf\xe8̄֎O\xfe\xb9\"V\n\x827\x0e\x1e\xdd\x0e-\xc7\x02\xfd\x91\xa82\xebU\xb3A\xc7y\x99\xd0\x0e]\xfat\xc4n\xfb\xdf\x01\xac\xe0\xbf\xff\x7f\xf85\x00\x00\xff\xff\x02\xf2+ܩ(\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcYK\x93\xe3\xb6\x11\xbeϯ\xe8\xda\x1c\xf6\xb2\xd2d\xf3p\xa5t\xdb\xd1\xc4US\xf1Ϊ\xac\xc9\xdcA\xb2I\xc1\v\x02\b\x1e\x92\xe5$\xff\xdd\xd5\x00IA$4z\xd8^\xdd\x044\xba\xbf~\xa0\x1f\xe0l6\xbbc\x9a\xbf\xa2\xb1\\\xc9\x050\xcd\xf1g\x87\x92\xfe\xd9\xf9\xd7\x7f\xd89W\xf7ۏw_\xb9\xac\x16\xb0\xf4֩\xf6G\xb4ʛ\x12\x1f\xb1\xe6\x92;\xae\xe4]\x8b\x8eU̱\xc5\x1d\x00\x93R9F˖\xfe\x02\x94J:\xa3\x84@3kPο\xfa\x02\v\xcfE\x85&0\xefEo\xff<\xff\xf8\xdd\xfc\xefw\x00\x92\xb5\xb8\x00\xe2W\xa9\x9d\x14\x8aUv\xbeE\x81F\u0379\xba\xb3\x1aKb\xdc\x18\xe5\xf5\x02\x0e\x1b\xf1`'4\x02~d\x8e=v<²\xe0\xd6\xfdk\xb2\xf5\x03\xb7.lk\xe1\r\x13#\xd9a\xc7n\x94q\xcf\a\xfe3\xa8\"G\xcbe\xe3\x053LJ\xee\x00l\xa94. \x9cѬDZ\xeb\x94\r*yl\x98\aZ\x85d9\"!/5h\xb2\xd6Q\x8e\x89\xdf\x02\xc4\x11\x83\x87\xe4|D\x12\xf9\xa6\xebg\xa1Pȁ\xaa\xc1m\x10\x1eX\xf9\xd5kX;eX\x83\xf0\x83*\xa3\xfbv\x1b4\x18(\x8aHA\xd1\v\x9c|\xa7L\xd6u\x1a\xcby\xa4\xed\x98\xf5\xbcF\xfe;\x16\xf4\xbb\xc7Vi\x90ec\xab\xcfA\xf3@\xc1\x95\xcc\aا\x06/\n\xaeԈRU\x98X\xec\b\x13\xb7\xa0\x8d*\xd1\xda7\x02\x9e\x18\x1c\xa1x>,LL\x13)\xb6\x7faBo\xd8ǘd\xca\r\xb6lѝP\x1a\xe5\xa7\xd5\xd3\xeb_\xd7G\xcb\xf0F\xc2`\xa5\xb3\x94)\b\xbe6ʩR\t(\xd0\xed\x10et}\xab\xb6h(\x016\\ځ#\xa5\xf3*%8$s\x8a\xef\xc0\x8fv\xe3\xa6\xc1\x10=\x04Ф\xde\a\x92\xa9\xd18ާώ\xf7\xa1\xf2$\xab#=\xfe7;\xda\x03 \xd5\xe3)\xa8\xa8\x04aT\xab˭Xu֊\xce\xe3\x16\fj\x83\x16e,J\xb4\xcc$\xa8\xe2',\xdd|\xc4z\x8d\x86\xd8P\xb6\xf7\xa2\"e\xb7h\x1c\x18,U#\xf9/\x03o\vN\x05\xa1\x829\xb4.\\F#\x99\x80-\x13\x1e?\x90\xd1F\x9c[\xb6\a\x83$\x13\xbcL\xf8\x85\x03v\x8c\xe33Y\x91\xcbZ-`㜶\x8b\xfb\xfb\x86\xbb\xbe\x1e\x97\xaam\xbd\xe4n\x7f\x1f\xbc\xc1\v\uf531\xf7\x15nQ\xdc[\xde̘)7\xdca\xe9\xbc\xc1{\xa6\xf9,(\"CM\x9e\xb7՟LW\xc1\xed\x91\xd8I \xc6_\xa8\xa4W\xb8\x87\xca+\xdd\nֱ\x8a*\x1e\xbc@Kd\xba\x1f\xff\xb9~\x81\x1eI\xf4Ttʁtb\x97\xde?dM.k4\xf1\\mT\x1bx\xa2\xac\xb4\xe2҅?\xa5\xe0(\x1dX_\xb4\xdcQ\x18\xfcǣu\xe4\xba1\xdbe\xe8Y\xa0@\xf0\x9a\xf2A5&x\x92\xb0d-\x8a%\xb3\xf8\x8d}E^\xb13r\xc2E\xdeJ;\xb11q4o\xb2ѷR'\\\x9bf\x90\xb5ƒ\xbcJ\x86\xa5c\xbc\xe6]%\xa14\xc0\x8eh\x8f-\x94\xbf\xfa\xf4\xcbV\x931ѹp\xa3\xdfC\x8eQ\x8fV&\x89\xbc\xabu\xb6+R\xe2\xb8H\xa5\xbfI}4\xa8\x95\xe5N\x99\xfd\xa1J\x8eC\xe1\xa4W\xe8W2Y\xa2\xb8E\xbde8\t\\Vds\x1cB\x99\x92P\xe4\x1a\x80*\xd9(\xba\\G\xae\x80'G4\x14\xdb\x16]^Q\x99\xadj\\¡\xa7\x84\xb4w\x1c\xab[(%\x90\x8d\xadHQ\xf8\x99\xca\xc2Rɚ7S\xc5\xd3\xf6\xf7T\x88\x9c\xb1i&`\x13\x91\xa4\x05E'!\x99\x85\n5\xebC\x97R{\xcd\x1boN\xf9\xbf\xe6(\xaaI\xfe9y\x93z\x85\x83\x94[|<@\xefoWWՒ\xd2\xebT\xc8P6\xf4\xbbIhNA\x02<\xd5\tGn\xe1\xdd;P\x06\xde\xc5a\xe9݇x\xdas\xe1f\\BmS1;.D/\xe8\xaa\x00\xa7&\xe7\xcb\xfa\x8c\xf2ρ\x88 }Y_\xdb^MѠ\xf4\xedT\xe0\f\x98w*\xb3,\xb8\xf4?g\xd6w\\Vjg\xafQvhq\xa8\xcbT\xde\xdd\xe2\xf3/#\x1e#\xd7;ꉃ\xbb\x9d\x82\x1d\xe3I\x9b1H\xb7\x1f2|\v\xac\xa9&\x19t\xdeH\xca\bh\f%i\x1bX*?i{\xde\xd4\xd4J\xa6\xedF\xb9\xa7\xc73:\xae\a\xc2>\xf5>=\xf6.~\r\x817\xe4ߎ\x122^\"\xf8}#Y\x85\xca~\x13\xda5\xff\x05/\xc4K\xa4=b\xa1\x1a^2\x016\xac\xc9n\x0e\xec\x94\xe8yO\x01\xe5F\xbd1\xdct`K\xf0\x86\xf6gx$\xb8%\x8c\xd6\xc7,zU\x94\xe1\r\xa7`\x91\xc3\xce\xe1\x8em\x95\xf0m %\x97`\x05^\x9f\xb05P\x05\xa1~\xab@\xa8x]\xa3\xa1\xa6*t\\Q\xf0\xeau\xf9\xde&Bx\x9d\xfe\xa1b\xd52\xad\xb1\xa2\U0004e0b1\xf3\xedU^u\xcc4\xe8^\x03\xe83&zIH{SPwF\x0e\xea\xda\xffp\xb9\x02\x19\xac^\x97\x99f\x9d~\xab\xd7)\xc2ӭ\f\xfdj\xfbB\x1advF\x10\xbf_\x13a\x0f\xae\xe6\x02\xc1\xee\xad\xc36\x98`\x840z*\xe7\x973\x95\x11\x0en\xb8\x00\xd3$|:\xf1\x03\x8f[\x00\xe8\xed\x05\x92W\xaf\xb9Nm\xf0\x0f\xb8\rsD\xd1\r\xfeP\xec\xb3<\xa1\xcf1]|݆\xb7\xbc\b\xf0\xf2M\xc4\xcb1\xe4\x13x\x8b\xfdo\x86L\x8d 7X\xe5j\xe0i\xcf\xcd@o\xb3\x8b\xe5\xe5\xedN^\xf2,\xdfӏhƵs\xb4}(8\xe3\x8d\xe3D7\xdaMs\xc4E\xc3Ox\x9a\xb9t\xfc\x89\x0f\xae\x9d\xdbKoB\x16\xec\x9eaU}\xe3\x00\xc4\xca\x12\xb5\xc3\xeaaOm\xd1\x05\x9d\x13\x01\x90o?L\xfd[\x1f\xfa&\xd4\xec\xda)\xa5\x874<\x9e\xddR\x91>\x8d\x99\x84\x17\x14S%}\xcd\x14nloO\x83\x06x\xa1\x1a\x1c^\x00\xde\xc7V\x86\x8e\x85\x06\x89\xba\xfc\x89ГU\x9aF\xfc\x19\x9d\x9fPH/\x04+\x04.\xc0\x19\x7fj\xdc\xc9Ow\xf1-:}v\xbciԛ\xb2\x99ڎ\r\x0fm\xe1A\xb4\x7f\x05ϙ\xec\xc0o0Xd\x87\x15\xe0\x16%\xd0\x00ϸ\xc0\xaa癙y\xceY>\x03z\xdaK\xff\x91\xc6o\xd1Z֜\xbb@\x9f#U|\x9b\xea\x8e\x00+\xa8\xf1\x1e\x8f\x1d\xefmw\xb7\xaf\x1e\x80~\x9fK|\xe1\xf8\xf3\x06\x960\xaf\x9f\x01\xb3\"\x9a\\N\x1b\xa0\x9dNj\xf0\xc6\xf4\xf5\x8c\xbb\xccj\x7f?3[\xab\xee\xd2g\xb6&\x9f\xb5\xd2\xcd\xf80\x92+\x8c\xfd^\x96\xe7\xf0\xdd(\xb3\xf7}\xb8\fWY\xba\xc3w\xcbu\x1f\x9eW6J\xf47<|\uf47e-А\x1b\x8a\xdc\x04\x12^\xe5\x13\xaf嚿\x81\xc30L\x05Vsx\xd9Pk\x12߄\xfa\xf1\xb2\xe2V\v\xb6\x1f\x94I[\xe6\f\xf3í\x99<\xf9_\xdb5\x0f\xdf\xdf\xf2\x9d\xd7ۓ\x15\x9c\x99\xae\xc2\xfe\xf0]폑\xf0Ƌ\xd0\xf1wΛf\xbb#\x0e\xe7JA\xf7\xdd\xf5\xfa\f~,\xe6[&\xef\xac\xf5&\x8b\x01y\x95\xf0\xee^p\xd3\x15_\f\x9f5\x16\xf0\xdf\xff\xdf\xfd\x1a\x00\x00\xff\xff_zG\xb9\xdb \x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcZI\xb3\xdb6\x12\xbe\xbf_\xd1\xe59\xe4b\xe9ų\xa4\xa6t\xb3\xe5Iի\x89\xedW\x96\xe7\xdd!\xb2)\"&\x01\x0e\x16)\x9a忧\x1a\v\t\x92\xd0\x1a'<\xb8\xfc\xb04zC\xf7\xd7\r-\x16\x8b\a\xd6\xf1\x17T\x9aK\xb1\x02\xd6q\xfcŠ\xa0\xbf\xf4\xf2\xeb\xdf\xf5\x92\xcb\xc7\xfd\x9b\x87\xaf\\\x94+X[md\xfb\x19\xb5\xb4\xaa\xc0\xf7Xq\xc1\r\x97\xe2\xa1E\xc3Jf\xd8\xea\x01\x80\t!\r\xa3aM\x7f\x02\x14R\x18%\x9b\x06\xd5b\x87b\xf9\xd5nqkyS\xa2r\xc4\xe3\xd1\xfb\xef\x97o~X\xfe\xed\x01@\xb0\x16W@\xf4l\xd7HV\xea\xe5\x1e\x1bTr\xc9\xe5\x83\xee\xb0 \xb2;%m\xb7\x82a\xc2o\vGzv\xdf3\xc3\xfe\xe5(\xb8\xc1\x86k\xf3\xcf\xc9\xc4O\\\x1b7\xd95V\xb1ft\xaa\x1b\u05f5T\xe6\xe3@y\x01\xa5\xf5\x13\\\xecl\xc3T\xba\xe5\x01@\x17\xb2\xc3\x15\xb8\x1d\x1d+\x90Ƃ\x88\x8e\xc2\x02XY:\xa5\xb1\xe6YqaP\xadec[1\xd0G](\xde\x19\xa7\x94\x81SІ\x19\xabAۢ\x06\xa6\xe1#\x1e\x1e\x9fij\x92;\x85\xda\xf3\n\xf0\xb3\x96♙z\x05K\xbf|\xd9\xd5Lc\x98\xf5zݸ\x890d\x8eĭ6\x8a\x8b]\xee\xfc/\xbcE(\xadr\xf6$\x99\v\x04Ss\x9d2v`\x9a\x98S\x06˓l\xb8y\"\xa6\rk\xbb)?\xc9V\xcfP\xc9\f\xe6\xd8Y˶k\xd0`\tۣ\xc1(D%U\xcb\xcc\n\xb80?\xfc\xf5\xb4&\x82\xaa\x96n\xeb{)\xc6jyG\xa3\x90\f{N\xc8B;TY\xddHÚ\xdf\u0088!\x02\xef\x92\xfd\x9e\x13O7\x1d\xbf\xc8ʓ(\x14\xb6(\xeec\x88\x0f\xbb\xe7ܤ\xa4\xd3\xd9Nq\xa9\xb89\xae\xe0\xcd\xf7ײI\xb7\x02d\x05\xa6FxNJ\xaf\xb6\x83\x8d\x91\x8a\xed\x10~\x92\x85\xf7\xb1C\x8d*\xf8\xd8\xd6/ѵ\xb4M\t\xdbh\x18\x00m\xa4\xca:[\x87\xc5\xd2\xef\nt#ىǍ\xcf\xfc\xc6w\xa1PȲw!Fɥ[\xc1\xa5\xc8_\x88\xb7;\xbc\xea2\xa4\xda\x14\xb2\xc4^u\x98r\xc45tJ\x16\xa8\xf5\x99\xebI\xdbG<|\x1c\x06fj\xf1+\xf6\x7ffMW\xb37>\x18\x165\xb6l\x15v\xc8\x0e\xc5\xdb秗\xbflF\xc3p2\xb4\xb1\xc2h\x8ai\xc4z\xa7\xa4\x91\x85l`\x8b\xe6\x80(\\x\x85V\xeeQQ\x90\xdeq\xa1\x81\x89\xb2\xa7\t\xe9\x82!Ր\xeb;z4\xeb'\x83;\xc9\x0eUjvre\x1a3<\xc6x\xff%i1\x19\x9d\b\xf1\xbf\xc5h\x0e\x80\xe4\xf6\xbb\xa0\xa4\xfc\x88^\xaa\x90\x02\xb0\f\xaa\xf2v\xe3\x1a\x14v\n5]/\xe7U\xb2\x02&@n\x7f\xc6\xc2,'\xa47\xa8\x88L\xbc\x0f\x85\x14{T\x06\x14\x16r'\xf8\x7fz\xda\x1a\x8ct\x876̠6\xeeB*\xc1\x1aس\xc6\xe2\xeb\x89\xf6\xe8k\xd9\x11\x14ҙ`EB\xcfm\xd0S>>H\x85\xc0E%WP\x1b\xd3\xe9\xd5\xe3㎛\b\x16\nٶVps|t\xc6\xe0[k\xa4ҏ%\xee\xb1y\xd4|\xb7`\xaa\xa8\xb9\xc1\xc2X\x85\x8f\xac\xe3\v'\x88p\x80aٖ\x7fR\x01^\xe8ѱ3/\xf4\x9fK\xf47\x98\x87\xf2?]\t\x16Hy\x11\a+\xd0\x10\xa9\xee\xf3?6_ r\xe2-\xe5\x8d2,\x9d\xe9%ڇ\xb4\xc9E\x85\xca䀹l\x1dM\x14e'\xb90\ue3e2\xe1(\fh\xbbm\xb9!7\xf8\xb7Em\xc8tS\xb2k\a\xa8`\x8b`;\n\x05\xe5t\xc1\x93\x805k\xb1Y3\x8d\x7f\xb0\xad\xc8*zAF\xb8\xcaZ)L\x9c.\xf6\xeaM&\"\xd2;a\xda!|l:,Ȧ\xa4V\xda\xc4+\x1er\t\xc5\x00\x96\xac\x1ck'\x7f\xed\xe9˦\x90\xe9\xa2K\xaeF\u07fb\x1c\xa1ȫH\xe2wLu!35\xe3̔~C\x90\x0f{\x14vRs#Ց\b\xfb\xd48u\x83\x93\x16\xa1\xaf`\xa2\xc0\xe6\x1e\xf1\xd6n'pQ\x92Ʊwc\n@\x9e\xaacT\x8a\x9d\xa4\x8b\x95\x18\x02\x9e\f\xad \xaf\xd6h\xf2b\x8aL*\xe3\x02\x06\xd0\v)\xb8\x9d\x8a\xba\x95\xb2A6\xd5`\xa1\xf9F\xb0N\xd7\xd2\\\x10\xf8\xa9\x82\xb8\xf2˱C:|\xbdyzM\xff\xc4q\xf2\xa0=/C\x88\xa7[Fh+o\xb6`\xe7\xf5\xe6\tt\xd8>7\x92\xb0Mö\r\xae\xc0(;\x17\xec\xb4\xc3:\xee\x15ߣ\xca\xcdLo\x8e[\x18\xbd\xd0o\x03\xab\x1d\xa8vC/T\x90`\x94r-\x85A\x91\xb3\xd1Y\xaf\xa2/J\xban\x98\xce\xf2<\xe1l\x93\xae\xcf]\x93H\x10\n\xb7\xc2\xd4,\xcf\x17\xf8\xa4\xeb\xe4\x186\xf1\x1e\x9b\xc1\x81\x9b\xfa.\x89\xfc\x05\xbdZ\xa0dyV\x9ep߽8\xb2:#\xcc\xf3\xcb\xda\xc9{I2J7\xf7H\xb6\x1f\x19\xfd\n\xd9\xc6^\x92\x93n\xc2\xe5)\xe1$E\x01\nfX\x82\xedn睂\x0eWX\xcey^\x8c앙\x1e\v}\"\x92\xcc2\x13\x04\xd0\xf9\x81`\xe5Z\x8a\x8a\xef\xe6g\xa7e\xfe\xb9k{V\xb4Y\xc6K\x8e$\x8dS\x82#N\x16\x0e\xe1.b\xf6#lX\xf1\x9dU\xa7\xa2Qű)g\x00\xe6b\x00\xba\xa0\x0f\xc7\xc4=y\xa4\x97,\xe6\xef\x10R\x13d\xef\xbd$\x8dR>\xfd\xcde\x00\n\xdd\x03E\xae\xe1\xd5+\x90\n^\xf9^ѫ\xd7~\xb7\xe5\x8dYp\x01\x95N\x8f9\xf0\xa6\x89\aݔD\xfb\xaa\x82j:i/e\x97\xac\x1a>MhL\xb4a\xa8\xfet\x1a0\x12\x0e\x8c'Ⱦ?]\xbf\xce\xd0\xddbE0P\xa1\xb1JP\"F\xa5\b\x19iGR\xdaL&:#i\xc7\x14\nse\x16\xcd\xca\xf9<\xa20\x91ғ\x1fB\x9b\x8by\x85Un4@\x1e\xd7\x1b EHq\xc2\xfe\x84\xa9=\xb4\x1f\xecϬ\x89\xd6O,^\x11xu\x83\n\x8b\xe4\x8c\x18\xa1)\x9e\x85@\xc6t\xe0\xee\xaaC\x85\x148?\xce9X)\x81Ae\xc9\xd5\xdcaW\x90c=\xb4\xedU\xf3\xf4\xfe\x8c0\xb3\xd5\xe7\xb8?cm\x9d`\xa0\v\xb6\x9e\xc2%\xe7\xb3\xf4\xffi\xf2N#~F\xf4ܥ>ǡ\xab\xd1~\xdc\\\xc3a\xb24rX\xf1\x06A\x1f\xb5\xc1v̭/\xfd\xbc\xe9\xef`\xa8\xef\x00\xdfsC6c\x12\x91W\xa9\xf8\x8e\xd3}\x17\xfd\xccP\x0e\x04'\r}3\x97K\x1d\x18\xc8:k\x9f\xaf\x9d\x7f\x0f\xe4(\xa1\xf8\xc3\to0Q:\xc4\xdaϗ!\xf8gR\xc7E\x85<\xbf\xac\xaf2\x0f\x1d\x9c\x01\x134|\xa8yQ\x8f}\x89\xcf\xd3:\x80a_\xd1U\x7f7\xb0\x99G\x11\x8b|-8Y3\r\xfe\x93\xe9\xf4\x0eM\xa7Ɔ\xce\xce>\xbf\xac\xaf\xaa\x97]+ﺊ\xd9?%\x04-\xc7\xe0\x1a\x1e\x18duW\xcd̊\x02;\x83\xe5\xbb\xe3GY^r\xfa\xb7\xa3\xc5Ĉ\xb8\xa6\x99\x991\xb5ko\"\x05\xb6\xdb\xf2ud\xb7o\xc1\xdesM\xdfN\x89\xb8f\x9c*\x93|=/a}\xf4;\xcd4\xc0\x17rp\xd7L\xfaΧh\xda\xe6\x12?]\xcf١3\n\xb1\xeb_2\x83\v\xda\x7f\x1f\xce\xcb7\v\xfc\vLڼ\xbe\xabs0'3\xd7\x1d\x8b\xb9\xd8u\xd5\xe3\xd3ONc\x03\xb9^_\x9e\x1a\x96\x80{\x14 \x05T\x8c7\x84\x1e\x1d\xc9L\x00;O%`(\xff\xce\x17\xbb\x84\x11*d۵\x97-\x99Q\xc2<\x9a\xfd\x9e\xc6싘Ϩm\x93\xc1r\xbfc\x11\xe3\x8f\xf4\xfd*\x9d-b\xce7T\x18a\"剄\xb8q*h]\xad\xa4le3}\x1d\xbb\xd47\x9a,\x87Z6\xc1\xa9\x85m\xb7\xa8\x88[\xf7F\a\x02\x0f\x04L\x8b\x9a\x89]\x16\t\xc57&\x84\x86is\n,\xe6\x1e\xf9\xa6\x92\xa5\x8fr\xc3ע\xd6lw)X\x7f\xf0\xab<\n\r[\x80m\xa9@\x19k\xfd;\x1dr\xc8M\x91X\\N\x177%\x89ы\xd7͜|\xda\\\xc1˧\r\x1d\xf2i\xf3[yAa\xdb\\ׂ*\x95\xccpÅ\xfd%3~\u0894\x87y\xe88[ę\xfa\x82\xa0\xcf\xcc\xd4=H\xa6Z\x85\xf6̰|@\x9d[\xa4\x98\xf8\xad \xbd\xeb\xeb^b\x8f\xd6\xe4 \f^\x13\x0eNi\xfe#\x1e2\xa31\xe5f\xa6\x9eC\x1e\xcfL\xcd~\x9d\x91N\xfa\xd6y.\\ƹ,\xcd\xfe\a\x10\x99\xb9\x1f]\x82\xbbIρ\xbf\xbb\x8a\xf8\u0604\x1f\xe2\x9b\xfb=\xc3,ʍ\x9b\x81TR$\x16\xcb\x10N\xf6\xf7u\x8c\xa3\xb4\x84/5\xd7\xf1\xd9 6BJ\xae\xbb\x86\x1d{Y.\xa5\x8d>nM\x9f\x83\xe7Nr\xbe\xdf\xde\xff\x8c$\xdf+=\x1f\x95\xe1Bdv\xf3\xf2t\xca\xf9\x16'\x9c\xc9yC\x8b\xe1ʚ\xff\xe9}\xbc\x8a\xbcDaxœ'\xf8\xa1XsO:9]N\x9f\xb2n\xab/G?.\xba\xab\xde\x1eQ\xb8\x80D\xc3o\x9drxoC\xc1\x80B\x90{\xf4]O\x7f\xe6\xf1\xba\xcf\xe8̄֎O\xfe\xb9\"V\n\x827\x0e\x1e\xdd\x0e-\xc7\x02\xfd\x91\xa82\xebU\xb3A\xc7y\x99\xd0\x0e\x8d\xfat\xc4n\xfb\x9f\x02\xac\xe0\xbf\xff\x7f\xf85\x00\x00\xff\xff \xad\x88\xba\xac(\x00\x00"), } var CRDs = crds() diff --git a/internal/resourcepolicies/resource_policies.go b/internal/resourcepolicies/resource_policies.go index 39504d6ff..08e0f8588 100644 --- a/internal/resourcepolicies/resource_policies.go +++ b/internal/resourcepolicies/resource_policies.go @@ -31,7 +31,7 @@ import ( crclient "sigs.k8s.io/controller-runtime/pkg/client" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - datamover "github.com/vmware-tanzu/velero/pkg/util/datamover" + "github.com/vmware-tanzu/velero/pkg/util/datamover" "github.com/vmware-tanzu/velero/pkg/util/wildcard" ) @@ -59,6 +59,7 @@ const ( // validDataMovers is the set of data mover values accepted in the snapshot // action's dataMover parameter. var validDataMovers = map[string]struct{}{ + datamover.DataMoverTypeEmpty: {}, datamover.DataMoverTypeVelero: {}, datamover.DataMoverTypeVeleroFs: {}, datamover.DataMoverTypeVeleroBlock: {}, @@ -90,14 +91,21 @@ func (a *Action) GetDataMover() (string, error) { if !ok { return datamover.GetDefaultBuiltInDataMover(), nil } + dataMover, ok := raw.(string) if !ok { return "", fmt.Errorf("parameter %q must be a string, got %T", DataMoverParameter, raw) } if _, ok := validDataMovers[dataMover]; !ok { - return "", fmt.Errorf("invalid %q value %q, valid values are %q, %q, %q", - DataMoverParameter, dataMover, datamover.DataMoverTypeVelero, datamover.DataMoverTypeVeleroFs, datamover.DataMoverTypeVeleroBlock) + return "", fmt.Errorf("invalid %q value %q, valid values are %q, %q, %q, %q", + DataMoverParameter, dataMover, datamover.DataMoverTypeEmpty, datamover.DataMoverTypeVelero, datamover.DataMoverTypeVeleroFs, datamover.DataMoverTypeVeleroBlock) } + + // Return default data mover for backup's volume policy, when the data mover's original value is legacy value: "" or "velero". + if dataMover == datamover.DataMoverTypeEmpty || dataMover == datamover.DataMoverTypeVelero { + dataMover = datamover.GetDefaultBuiltInDataMover() + } + return dataMover, nil } diff --git a/internal/resourcepolicies/resource_policies_test.go b/internal/resourcepolicies/resource_policies_test.go index 7a7da6d3d..aae458b7e 100644 --- a/internal/resourcepolicies/resource_policies_test.go +++ b/internal/resourcepolicies/resource_policies_test.go @@ -31,6 +31,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerotest "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/util/datamover" ) func pvcVolumeMode(mode corev1api.PersistentVolumeMode) *corev1api.PersistentVolumeMode { @@ -2999,10 +3000,10 @@ namespacedFilterPolicies: func TestActionGetDataMover(t *testing.T) { testCases := []struct { - name string - action *Action - expectedMove string - expectErr bool + name string + action *Action + expectedDataMover string + expectErr bool }{ { name: "nil action", @@ -3010,29 +3011,29 @@ func TestActionGetDataMover(t *testing.T) { expectErr: true, }, { - name: "snapshot action without parameters returns default mover", - action: &Action{Type: Snapshot}, - expectedMove: "velero-fs", + name: "snapshot action without parameters returns default mover", + action: &Action{Type: Snapshot}, + expectedDataMover: datamover.GetDefaultBuiltInDataMover(), }, { - name: "snapshot action without dataMover parameter returns default mover", - action: &Action{Type: Snapshot, Parameters: map[string]any{"other": "value"}}, - expectedMove: "velero-fs", + name: "snapshot action without dataMover parameter returns default mover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"other": "value"}}, + expectedDataMover: datamover.GetDefaultBuiltInDataMover(), }, { - name: "snapshot action with velero dataMover", - action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": "velero"}}, - expectedMove: "velero", + name: "snapshot action with velero dataMover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": "velero"}}, + expectedDataMover: datamover.GetDefaultBuiltInDataMover(), }, { - name: "snapshot action with velero-fs dataMover", - action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": "velero-fs"}}, - expectedMove: "velero-fs", + name: "snapshot action with velero-fs dataMover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": datamover.DataMoverTypeVeleroFs}}, + expectedDataMover: datamover.DataMoverTypeVeleroFs, }, { - name: "snapshot action with velero-block dataMover", - action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": "velero-block"}}, - expectedMove: "velero-block", + name: "snapshot action with velero-block dataMover", + action: &Action{Type: Snapshot, Parameters: map[string]any{"dataMover": datamover.DataMoverTypeVeleroBlock}}, + expectedDataMover: datamover.DataMoverTypeVeleroBlock, }, { name: "non-snapshot action returns error", @@ -3059,7 +3060,7 @@ func TestActionGetDataMover(t *testing.T) { return } require.NoError(t, err) - assert.Equal(t, tc.expectedMove, dataMover) + assert.Equal(t, tc.expectedDataMover, dataMover) }) } } diff --git a/pkg/apis/velero/v1/backup_types.go b/pkg/apis/velero/v1/backup_types.go index 65bf1ae81..dd3125fa7 100644 --- a/pkg/apis/velero/v1/backup_types.go +++ b/pkg/apis/velero/v1/backup_types.go @@ -179,7 +179,7 @@ type BackupSpec struct { SnapshotMoveData *bool `json:"snapshotMoveData,omitempty"` // DataMover specifies the data mover to be used by the backup. - // If DataMover is "" or "velero", the built-in data mover will be used. + // If DataMover is "" or "velero", the default built-in data mover will be used. // +optional DataMover string `json:"datamover,omitempty"` diff --git a/pkg/apis/velero/v2alpha1/data_download_types.go b/pkg/apis/velero/v2alpha1/data_download_types.go index 220bd382b..297a064b8 100644 --- a/pkg/apis/velero/v2alpha1/data_download_types.go +++ b/pkg/apis/velero/v2alpha1/data_download_types.go @@ -32,7 +32,7 @@ type DataDownloadSpec struct { BackupStorageLocation string `json:"backupStorageLocation"` // DataMover specifies the data mover to be used by the backup. - // If DataMover is "" or "velero", the built-in data mover will be used. + // If DataMover is "" or "velero", the built-in fs data mover will be used. // +optional DataMover string `json:"datamover,omitempty"` diff --git a/pkg/apis/velero/v2alpha1/data_upload_types.go b/pkg/apis/velero/v2alpha1/data_upload_types.go index 56225f387..606502254 100644 --- a/pkg/apis/velero/v2alpha1/data_upload_types.go +++ b/pkg/apis/velero/v2alpha1/data_upload_types.go @@ -36,7 +36,7 @@ type DataUploadSpec struct { SourcePVC string `json:"sourcePVC"` // DataMover specifies the data mover to be used by the backup. - // If DataMover is "" or "velero", the built-in data mover will be used. + // If DataMover is "" or "velero", the built-in fs data mover will be used. // +optional DataMover string `json:"datamover,omitempty"` diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 167fb7eaf..569ff18d1 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -58,6 +58,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/framework" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/collections" + "github.com/vmware-tanzu/velero/pkg/util/datamover" "github.com/vmware-tanzu/velero/pkg/util/encode" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/logging" @@ -431,6 +432,10 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel request.Spec.BackupType = velerov1api.BackupTypeIncremental } + if len(request.Spec.DataMover) == 0 || request.Spec.DataMover == datamover.DataMoverTypeVelero { + request.Spec.DataMover = datamover.GetDefaultBuiltInDataMover() + } + // calculate expiration request.Status.Expiration = &metav1.Time{Time: b.clock.Now().Add(request.Spec.TTL.Duration)} diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index b86434796..13bac2e4c 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -64,6 +64,7 @@ import ( ibav1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/itemblockaction/v1" velerotest "github.com/vmware-tanzu/velero/pkg/test" "github.com/vmware-tanzu/velero/pkg/util/boolptr" + "github.com/vmware-tanzu/velero/pkg/util/datamover" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/logging" ) @@ -805,6 +806,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -846,6 +848,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -891,6 +894,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -933,6 +937,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -975,6 +980,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1018,6 +1024,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1061,6 +1068,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1104,6 +1112,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1147,6 +1156,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1191,6 +1201,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFailed, @@ -1235,6 +1246,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFailed, @@ -1279,6 +1291,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1324,6 +1337,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1369,6 +1383,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1414,6 +1429,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1460,6 +1476,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1505,6 +1522,7 @@ func TestProcessBackupCompletions(t *testing.T) { ExcludedClusterScopedResources: autoExcludeClusterScopedResources, ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources, BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1556,6 +1574,7 @@ func TestProcessBackupCompletions(t *testing.T) { IncludedNamespaceScopedResources: []string{"pods"}, ExcludedNamespaceScopedResources: append([]string{"secrets"}, autoExcludeNamespaceScopedResources...), BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, @@ -1607,6 +1626,7 @@ func TestProcessBackupCompletions(t *testing.T) { IncludedNamespaceScopedResources: []string{"pods"}, ExcludedNamespaceScopedResources: append([]string{"secrets"}, autoExcludeNamespaceScopedResources...), BackupType: velerov1api.BackupTypeIncremental, + DataMover: datamover.GetDefaultBuiltInDataMover(), }, Status: velerov1api.BackupStatus{ Phase: velerov1api.BackupPhaseFinalizing, diff --git a/pkg/controller/data_download_controller.go b/pkg/controller/data_download_controller.go index 7e06c459d..337d10936 100644 --- a/pkg/controller/data_download_controller.go +++ b/pkg/controller/data_download_controller.go @@ -466,7 +466,7 @@ func (r *DataDownloadReconciler) OnDataDownloadCompleted(ctx context.Context, na defer r.dataPathMgr.RemoveAsyncBR(ddName) log := r.logger.WithField("datadownload", ddName) - log.Info("Async fs restore data path completed") + log.Info("Async restore data path completed") var dd velerov2alpha1api.DataDownload if err := r.client.Get(ctx, types.NamespacedName{Name: ddName, Namespace: namespace}, &dd); err != nil { @@ -513,7 +513,7 @@ func (r *DataDownloadReconciler) OnDataDownloadFailed(ctx context.Context, names log := r.logger.WithField("datadownload", ddName) - log.WithError(err).Error("Async fs restore data path failed") + log.WithError(err).Error("Async restore data path failed") var dd velerov2alpha1api.DataDownload if getErr := r.client.Get(ctx, types.NamespacedName{Name: ddName, Namespace: namespace}, &dd); getErr != nil { @@ -528,7 +528,7 @@ func (r *DataDownloadReconciler) OnDataDownloadCancelled(ctx context.Context, na log := r.logger.WithField("datadownload", ddName) - log.Warn("Async fs backup data path canceled") + log.Warn("Async restore data path canceled") var dd velerov2alpha1api.DataDownload if getErr := r.client.Get(ctx, types.NamespacedName{Name: ddName, Namespace: namespace}, &dd); getErr != nil { diff --git a/pkg/controller/data_upload_controller.go b/pkg/controller/data_upload_controller.go index 78e4d1ed3..61ccfef58 100644 --- a/pkg/controller/data_upload_controller.go +++ b/pkg/controller/data_upload_controller.go @@ -482,7 +482,7 @@ func (r *DataUploadReconciler) OnDataUploadCompleted(ctx context.Context, namesp log := r.logger.WithField("dataupload", duName) - log.Info("Async fs backup data path completed") + log.Info("Async backup data path completed") var du velerov2alpha1api.DataUpload if err := r.client.Get(ctx, types.NamespacedName{Name: duName, Namespace: namespace}, &du); err != nil { @@ -534,7 +534,7 @@ func (r *DataUploadReconciler) OnDataUploadFailed(ctx context.Context, namespace log := r.logger.WithField("dataupload", duName) - log.WithError(err).Error("Async fs backup data path failed") + log.WithError(err).Error("Async backup data path failed") var du velerov2alpha1api.DataUpload if getErr := r.client.Get(ctx, types.NamespacedName{Name: duName, Namespace: namespace}, &du); getErr != nil { @@ -549,7 +549,7 @@ func (r *DataUploadReconciler) OnDataUploadCancelled(ctx context.Context, namesp log := r.logger.WithField("dataupload", duName) - log.Warn("Async fs backup data path canceled") + log.Warn("Async backup data path canceled") du := &velerov2alpha1api.DataUpload{} if getErr := r.client.Get(ctx, types.NamespacedName{Name: duName, Namespace: namespace}, du); getErr != nil { diff --git a/pkg/datamover/backup_micro_service.go b/pkg/datamover/backup_micro_service.go index 81912f600..7398d6480 100644 --- a/pkg/datamover/backup_micro_service.go +++ b/pkg/datamover/backup_micro_service.go @@ -225,7 +225,7 @@ func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string, return "", errors.Wrap(err, "error starting data path backup") } - log.Info("Async fs backup data path started") + log.Info("Async backup data path started") r.eventRecorder.Event(du, false, datapath.EventReasonStarted, "Data path for %s started", du.Name) result := "" @@ -240,7 +240,7 @@ func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string, } if err != nil { - log.WithError(err).Error("Async fs backup was not completed") + log.WithError(err).Error("Async backup was not completed") } r.eventRecorder.EndingEvent(du, false, datapath.EventReasonStopped, "Data path for %s stopped", du.Name) @@ -277,12 +277,12 @@ func (r *BackupMicroService) OnDataUploadCompleted(ctx context.Context, namespac } } - log.Info("Async fs backup completed") + log.Info("Async backup completed") } func (r *BackupMicroService) OnDataUploadFailed(ctx context.Context, namespace string, duName string, err error) { log := r.logger.WithField("dataupload", duName) - log.WithError(err).Error("Async fs backup data path failed") + log.WithError(err).Error("Async backup data path failed") r.eventRecorder.Event(r.dataUpload, false, datapath.EventReasonFailed, "Data path for data upload %s failed, error %v", r.dataUploadName, err) r.resultSignal <- dataPathResult{ @@ -292,7 +292,7 @@ func (r *BackupMicroService) OnDataUploadFailed(ctx context.Context, namespace s func (r *BackupMicroService) OnDataUploadCancelled(ctx context.Context, namespace string, duName string) { log := r.logger.WithField("dataupload", duName) - log.Warn("Async fs backup data path canceled") + log.Warn("Async backup data path canceled") r.eventRecorder.Event(r.dataUpload, false, datapath.EventReasonCancelled, "Data path for data upload %s canceled", duName) r.resultSignal <- dataPathResult{ diff --git a/pkg/datamover/restore_micro_service.go b/pkg/datamover/restore_micro_service.go index 5880dfc91..799fa0add 100644 --- a/pkg/datamover/restore_micro_service.go +++ b/pkg/datamover/restore_micro_service.go @@ -184,7 +184,7 @@ func (r *RestoreMicroService) RunCancelableDataPath(ctx context.Context) (string return "", errors.Wrap(err, "error starting data path restore") } - log.Info("Async fs restore data path started") + log.Info("Async restore data path started") r.eventRecorder.Event(dd, false, datapath.EventReasonStarted, "Data path for %s started", dd.Name) result := "" @@ -234,12 +234,12 @@ func (r *RestoreMicroService) OnDataDownloadCompleted(ctx context.Context, names } } - log.Info("Async fs restore data path completed") + log.Info("Async restore data path completed") } func (r *RestoreMicroService) OnDataDownloadFailed(ctx context.Context, namespace string, ddName string, err error) { log := r.logger.WithField("datadownload", ddName) - log.WithError(err).Error("Async fs restore data path failed") + log.WithError(err).Error("Async restore data path failed") r.eventRecorder.Event(r.dataDownload, false, datapath.EventReasonFailed, "Data path for data download %s failed, error %v", r.dataDownloadName, err) r.resultSignal <- dataPathResult{ @@ -249,7 +249,7 @@ func (r *RestoreMicroService) OnDataDownloadFailed(ctx context.Context, namespac func (r *RestoreMicroService) OnDataDownloadCancelled(ctx context.Context, namespace string, ddName string) { log := r.logger.WithField("datadownload", ddName) - log.Warn("Async fs restore data path canceled") + log.Warn("Async restore data path canceled") r.eventRecorder.Event(r.dataDownload, false, datapath.EventReasonCancelled, "Data path for data download %s canceled", ddName) r.resultSignal <- dataPathResult{ diff --git a/pkg/util/datamover/datamover.go b/pkg/util/datamover/datamover.go index b6d965d60..7815f7a4a 100644 --- a/pkg/util/datamover/datamover.go +++ b/pkg/util/datamover/datamover.go @@ -20,6 +20,7 @@ limitations under the License. package datamover const ( + DataMoverTypeEmpty = "" // DataMoverTypeVelero refers to the default built-in data mover. The default // data mover may change among releases; see GetDefaultBuiltInDataMover. DataMoverTypeVelero = "velero" @@ -35,6 +36,7 @@ func IsBuiltInDataMover(dataMover string) bool { return IsVeleroBlockDataMover(dataMover) || IsVeleroFSDataMover(dataMover) } +// IsVeleroFSDataMover checks whether the given data mover belongs to fs type. func IsVeleroFSDataMover(dataMover string) bool { if dataMover == "" || dataMover == DataMoverTypeVelero { dataMover = DataMoverTypeVeleroFs @@ -42,6 +44,7 @@ func IsVeleroFSDataMover(dataMover string) bool { return dataMover == DataMoverTypeVeleroFs } +// IsVeleroBlockDataMover checks whether the given data mover belongs to block type. func IsVeleroBlockDataMover(dataMover string) bool { return dataMover == DataMoverTypeVeleroBlock } From de32d93b8ee44d65cc6202385aa42830c62acbb5 Mon Sep 17 00:00:00 2001 From: Shashank Singh <63052147+Shashank1306s@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:58:01 +0530 Subject: [PATCH 154/194] Fix ResourceDeletionStatusTracker key Kind mismatch in terminating-namespace wait (#9987) EnsureNamespaceExistsAndIsReady wrote the tracker key with namespace.Kind (getNamespace() sets Kind=Namespace) but read it with clusterNS.Kind (client.Get strips TypeMeta -> Kind=empty). The keys never matched, so the skip-path never fired and every item in a terminating namespace paid the full --terminating-resource-timeout wait (per-resource instead of per-namespace). Use the passed-in namespace object for Contains so Add/Contains keys match. Add a regression test that reproduces the production Kind divergence. Signed-off-by: Shashank1306s Co-authored-by: Shashank1306s Co-authored-by: Priyansh Choudhary --- changelogs/unreleased/9987-Shashank1306s | 1 + pkg/util/kube/utils.go | 5 +++- pkg/util/kube/utils_test.go | 33 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/9987-Shashank1306s diff --git a/changelogs/unreleased/9987-Shashank1306s b/changelogs/unreleased/9987-Shashank1306s new file mode 100644 index 000000000..4a975b5da --- /dev/null +++ b/changelogs/unreleased/9987-Shashank1306s @@ -0,0 +1 @@ +Fix ResourceDeletionStatusTracker key mismatch so restore into a terminating namespace waits once per namespace instead of once per resource diff --git a/pkg/util/kube/utils.go b/pkg/util/kube/utils.go index d76dad4a3..c3d0b2046 100644 --- a/pkg/util/kube/utils.go +++ b/pkg/util/kube/utils.go @@ -103,7 +103,10 @@ func EnsureNamespaceExistsAndIsReady(namespace *corev1api.Namespace, client core return true, err } if clusterNS != nil && (clusterNS.GetDeletionTimestamp() != nil || clusterNS.Status.Phase == corev1api.NamespaceTerminating) { - if resourceDeletionStatusTracker.Contains(clusterNS.Kind, clusterNS.Name, clusterNS.Name) { + // Use namespace.Kind (not clusterNS.Kind) so this key matches the one Add() + // writes below: client.Get() strips TypeMeta (Kind=""), but getNamespace() + // sets Kind="Namespace". Mismatched keys made Contains never match. + if resourceDeletionStatusTracker.Contains(namespace.Kind, namespace.Name, namespace.Name) { namespaceAlreadyInDeletionTracker = true return true, errors.Errorf("namespace %s is already present in the polling set, skipping execution", namespace.Name) } diff --git a/pkg/util/kube/utils_test.go b/pkg/util/kube/utils_test.go index 23db12a41..cc53b31b5 100644 --- a/pkg/util/kube/utils_test.go +++ b/pkg/util/kube/utils_test.go @@ -154,6 +154,39 @@ func TestEnsureNamespaceExistsAndIsReady(t *testing.T) { } } +// TestEnsureNamespaceExistsAndIsReadyTerminatingTrackerKindMismatch verifies the +// tracker skip-path fires when Add and Contains see different Kind values, as they +// do in production: getNamespace() sets Kind="Namespace" but client.Get() strips it. +func TestEnsureNamespaceExistsAndIsReadyTerminatingTrackerKindMismatch(t *testing.T) { + // Passed-in namespace mirrors getNamespace(): Kind is set. + namespace := &corev1api.Namespace{ + TypeMeta: metav1.TypeMeta{Kind: "Namespace", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + } + + // clusterNS mirrors client.Get(): Kind stripped, phase Terminating. + clusterNS := &corev1api.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: corev1api.NamespaceStatus{Phase: corev1api.NamespaceTerminating}, + } + + nsClient := &velerotest.FakeNamespaceClient{} + defer nsClient.AssertExpectations(t) + nsClient.On("Get", "test", metav1.GetOptions{}).Return(clusterNS, nil) + + // Seed the tracker as production Add() does. + tracker := NewResourceDeletionStatusTracker() + tracker.Add(namespace.Kind, namespace.Name, namespace.Name) + + result, nsCreated, err := EnsureNamespaceExistsAndIsReady(namespace, nsClient, time.Millisecond, tracker) + + assert.False(t, result) + assert.False(t, nsCreated) + // Skip-path must fire, not the full terminating-resource-timeout wait. + require.ErrorContains(t, err, "skipping polling for terminating namespace") + assert.NotContains(t, err.Error(), "timed out waiting for terminating namespace") +} + // TestGetVolumeDirectorySuccess tests that the GetVolumeDirectory function // returns a volume's name or a volume's name plus '/mount' when a PVC is present. func TestGetVolumeDirectorySuccess(t *testing.T) { From 23a0cbe163f7b063649218451f673879b2a5dcf3 Mon Sep 17 00:00:00 2001 From: lyndon-li <98304688+Lyndon-Li@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:00:45 +0800 Subject: [PATCH 155/194] add incremental size for block uploader (#10151) Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader_test.go | 1 + pkg/uploader/provider/block.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index f2de0e8c2..1765eb045 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -320,6 +320,7 @@ func TestBlockUploaderBackup(t *testing.T) { iterMock.On("Count").Return(uint64(1)) iterMock.On("Next").Return(uint64(0), true).Maybe() + objWriter.On("WriteAt", mock.Anything, mock.Anything).Return(0, context.Canceled).Maybe() objWriter.On("Result").Return(udmrepo.ID(""), errors.New("write failed")).Maybe() } else if tc.shortWrite { iterMock.On("BlockSize").Return(uint(1048576)) diff --git a/pkg/uploader/provider/block.go b/pkg/uploader/provider/block.go index 9135bb67b..6a7ae2802 100644 --- a/pkg/uploader/provider/block.go +++ b/pkg/uploader/provider/block.go @@ -150,7 +150,7 @@ func (bp *blockProvider) RunBackup( }, ) - log.Infof("Block backup finished, snapshot ID %s, backup size %d", snapshotInfo.ID, snapshotInfo.Size) + log.Infof("Block backup finished, snapshot ID %s, backup size %v, incremental size %v", snapshotInfo.ID, snapshotInfo.Size, snapshotInfo.IncrementalSize) return snapshotInfo.ID, false, snapshotInfo.Size, snapshotInfo.IncrementalSize, nil } From b2dea8d169f55413bc5553d13b677271fa55d27b Mon Sep 17 00:00:00 2001 From: Jay2006sawant Date: Tue, 4 Aug 2026 14:49:08 +0530 Subject: [PATCH 156/194] chore: add one-line changelog for PR 10138 Signed-off-by: Jay2006sawant --- changelogs/unreleased/10138-Jay2006sawant | 1 + changelogs/unreleased/fix-error-handling-Jay2006sawant | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) create mode 100644 changelogs/unreleased/10138-Jay2006sawant delete mode 100644 changelogs/unreleased/fix-error-handling-Jay2006sawant diff --git a/changelogs/unreleased/10138-Jay2006sawant b/changelogs/unreleased/10138-Jay2006sawant new file mode 100644 index 000000000..cc5339217 --- /dev/null +++ b/changelogs/unreleased/10138-Jay2006sawant @@ -0,0 +1 @@ +Fix block uploader restore validation and BatchForget error handling diff --git a/changelogs/unreleased/fix-error-handling-Jay2006sawant b/changelogs/unreleased/fix-error-handling-Jay2006sawant deleted file mode 100644 index 82a05f6c3..000000000 --- a/changelogs/unreleased/fix-error-handling-Jay2006sawant +++ /dev/null @@ -1,9 +0,0 @@ -fix: return errors correctly in block restore validation and BatchForget - -Block uploader Restore used errors.Wrapf with a stale nil err after -successful getSourceSize, causing size validation failures to return -(0, nil). flushZeroBlocks had the same pattern on short writes. - -BatchForget dropped delete errors when flush also failed. - -Signed-off-by: Jay2006sawant From 5a615ad580af17706332a5494fb5664cd2f11d11 Mon Sep 17 00:00:00 2001 From: chlins Date: Tue, 4 Aug 2026 17:19:25 +0800 Subject: [PATCH 157/194] 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 b4b72a35624bb5f6aed722519dde334b5345e7f8 Mon Sep 17 00:00:00 2001 From: Chlins Zhang Date: Tue, 4 Aug 2026 21:50:37 +0800 Subject: [PATCH 158/194] Add regression test for additional item with invalid JSON (#10103) archive.Unmarshal returns (nil, err) when an item file contains malformed JSON, and restoreItem dereferences its obj argument on entry, so an additional item that fails to unmarshal must be skipped rather than passed on. The loop only records the error and continues today; nothing covers that, so removing the continue reintroduces a nil pointer dereference in the restore reconciler without failing any test. The item file is added to the tarball so the existing Stat check passes and the unmarshal is actually reached. Signed-off-by: chlins --- pkg/restore/restore_test.go | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 935586e63..46667b1f9 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -2299,6 +2299,62 @@ func TestRestoreActionAdditionalItems(t *testing.T) { } } +// TestRestoreActionAdditionalItemsInvalidJSON verifies that an additional item whose file +// exists in the backup but does not contain valid JSON is reported as an error and skipped, +// rather than being passed to restoreItem as a nil object. +// +// archive.Unmarshal returns (nil, err) for malformed JSON, and restoreItem dereferences its +// obj argument immediately, so failing to skip the item panics the restore reconciler. +func TestRestoreActionAdditionalItemsInvalidJSON(t *testing.T) { + h := newHarness(t) + + for _, r := range []*test.APIResource{test.Pods(), test.PVs()} { + h.AddItems(t, r) + } + + // pv-1.json exists so the Stat check passes, but its contents are not valid JSON. + tarball := test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + Add("resources/persistentvolumes/cluster/pv-1.json", []byte("not-json")). + Done() + + actions := []riav2.RestoreItemAction{ + &pluggableAction{ + executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) { + return &velero.RestoreItemActionExecuteOutput{ + UpdatedItem: input.Item, + AdditionalItems: []velero.ResourceIdentifier{ + {GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"}, + }, + }, nil + }, + }, + } + + data := &Request{ + Log: h.log, + Restore: defaultRestore().Result(), + Backup: defaultBackup().Result(), + BackupReader: tarball, + } + + // A nil additional item passed on to restoreItem panics here rather than failing. + warnings, errs := h.restorer.Restore(data, actions, nil) + + assertWantErrsOrWarnings(t, Result{}, warnings) + assertWantErrsOrWarnings(t, Result{ + Namespaces: map[string][]string{ + "ns-1": {"error restoring additional item persistentvolumes/pv-1"}, + }, + }, errs) + + // The item that triggered the action is still restored, so the loop continued. + assertAPIContents(t, h, map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1"}, + test.PVs(): {}, + }) +} + // TestRestoreMustIncludeAdditionalItems covers restore must-include edge cases beyond the // basic filter-bypass cases in TestRestoreActionAdditionalItems. func TestRestoreMustIncludeAdditionalItems(t *testing.T) { From e1cd2b826692166af3d94bdf23e8154f2fa6cfa4 Mon Sep 17 00:00:00 2001 From: PragatiVerma111 Date: Tue, 4 Aug 2026 19:22:15 +0530 Subject: [PATCH 159/194] docs: update community page backlog links away from classic projects (#10157) Signed-off-by: Pragati Co-authored-by: Pragati Co-authored-by: Cursor --- site/content/community/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/community/_index.md b/site/content/community/_index.md index 41755f9db..043941685 100644 --- a/site/content/community/_index.md +++ b/site/content/community/_index.md @@ -9,7 +9,7 @@ If you’re a newcomer, check out the “[Good first issue](https://github.com/v If you are ready to jump in and test, add code, or help with documentation, follow the instructions on our [Start contributing](https://velero.io/docs/main/start-contributing/) documentation for guidance on how to setup Velero for development. -You can follow the work we do, see our milestones, and our backlog on our [GitHub project boards](https://github.com/velero-io/velero/projects). +You can follow the work we do via our [GitHub milestones](https://github.com/velero-io/velero/milestones) and the project [Roadmap](https://github.com/velero-io/velero/wiki/Roadmap). * Follow us on Twitter at [@projectvelero](https://twitter.com/projectvelero) * Join our Kubernetes Slack channel and talk to over 800 other community members: [#velero-users](https://kubernetes.slack.com/messages/velero-users) From cf6202be46bb57424844e6ddc37868f0204f424f Mon Sep 17 00:00:00 2001 From: Uajjawal <118979788+wolf-06@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:17:21 +0530 Subject: [PATCH 160/194] docs: document ownership loss on mount-constant filesystems (#10044) (#10147) Signed-off-by: wolf-06 --- .../docs/main/csi-snapshot-data-movement.md | 1 + site/content/docs/main/file-system-backup.md | 93 ++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/site/content/docs/main/csi-snapshot-data-movement.md b/site/content/docs/main/csi-snapshot-data-movement.md index 378f99055..9c9bc6184 100644 --- a/site/content/docs/main/csi-snapshot-data-movement.md +++ b/site/content/docs/main/csi-snapshot-data-movement.md @@ -170,6 +170,7 @@ kubectl -n velero get datadownloads -l velero.io/restore-name=YOUR_RESTORE_NAME that anyone who has access to your backup storage can decrypt your backup data**. Make sure that you limit access to the backup storage appropriately. - [Velero built-in data mover] Even though the backup data could be incrementally preserved, for a single file data, Velero built-in data mover leverages on deduplication to find the difference to be saved. This means that large files (such as ones storing a database) will take a long time to scan for data deduplication, even if the actual difference is small. +- [Velero built-in data mover] On volumes where the underlying filesystem enforces mount-constant identity (Azure Files SMB/CIFS, Azure Blob via blobfuse, GCP Cloud Storage FUSE, and similar), data download's `chown`/`chmod` can report success while changing nothing, silently losing file ownership (and on FUSE mounts, permission bits) with no error surfaced anywhere. See [File Ownership and Permission Preservation](file-system-backup.md#file-ownership-and-permission-preservation) for details and remediation. ## Troubleshooting diff --git a/site/content/docs/main/file-system-backup.md b/site/content/docs/main/file-system-backup.md index 139b91438..907fc08e8 100644 --- a/site/content/docs/main/file-system-backup.md +++ b/site/content/docs/main/file-system-backup.md @@ -367,7 +367,98 @@ For this reason, FSB can only backup volumes that are mounted by a pod and not d (without running pods), some Velero users overcame this limitation running a staging pod (i.e. a busybox or alpine container with an infinite sleep) to mount these PVC/PV pairs prior taking a Velero backup. - Velero File System Backup expects volumes to be mounted under `/` (`hostPath` is configurable as mentioned in [Configure Node Agent DaemonSet spec](#configure-node-agent-daemonset-spec)). Some Kubernetes systems (i.e., [vCluster][11]) don't mount volumes under the `` sub-dir, Velero File System Backup is not working with them. -- File system restores of the same pod won't start until all the volumes of the pod get bound, even though some of the volumes have been bound and ready for restore. An a result, if a pod has multiple volumes, while only part of the volumes are restored by file system restore, these file system restores won't start until the other volumes are restored completely by other restore types (i.e., [CSI Snapshot Restore][12], [CSI Snapshot Data Movement][13]), the file system restores won't happen concurrently with those other types of restores. +- File system restores of the same pod won't start until all the volumes of the pod get bound, even though some of the volumes have been bound and ready for restore. An a result, if a pod has multiple volumes, while only part of the volumes are restored by file system restore, these file system restores won't start until the other volumes are restored completely by other restore types (i.e., [CSI Snapshot Restore][12], [CSI Snapshot Data Movement][13]), the file system restores won't happen concurrently with those other types of restores. +- On volumes where the underlying filesystem enforces mount-constant identity (Azure Files SMB/CIFS, Azure Blob via blobfuse, GCP Cloud Storage FUSE, and similar), FSB restore's `chown`/`chmod` can report success while changing nothing, silently losing file ownership (and on FUSE mounts, permission bits) with no error surfaced anywhere. See [File Ownership and Permission Preservation](#file-ownership-and-permission-preservation) below. + +## File Ownership and Permission Preservation + +[#file-ownership-and-permission-preservation](#file-ownership-and-permission-preservation) + +Some volume types enforce a **mount-constant identity**: file ownership and/or permission mode are determined +entirely by the mount configuration rather than being stored per-file on the underlying storage. On these +filesystems, when FSB restore runs `chown`/`chmod` as root, the system call **returns success while changing +nothing**: the restored files simply present whatever owner/mode the mount is configured to force. Because no +error is ever raised, this is a silent failure: the restore reports `Completed` with zero warnings, and nothing +in the node-agent or data mover pod logs indicates a problem. + +This is a distinct failure mode from cases where the storage backend actively rejects the ownership change +(for example, NFS server-side `root_squash`, which returns a real `EPERM`). That class of failure can, in +principle, be caught by inspecting the error path. The mount-constant-identity case cannot, because there is no +error to catch. + +**Affected volume types (verified or by design):** + +| Storage | Ownership storage | `chown` as root | `chmod` as root | +| ---------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------ | ------------------------------------------ | +| Azure Files SMB/CIFS, default or forced `uid=`/`gid=` mount options | Mount-constant | Silent no-op | Forced by `file_mode=`/`dir_mode=` | +| Azure Files SMB with `idsfromsid,modefromsid` mount options | Stored in NTFS security descriptors | Works, persists, survives remount | Works, persists | +| Azure Blob via blobfuse (`blobfuse2`) | Mount-constant | Silent no-op | Silent no-op | +| Azure Blob NFSv3 (Premium) | Real POSIX (server-side) | Works | Works | +| Azure Files NFS 4.1 (Premium) | Real POSIX (server-side) | Works | Works | +| Azure Files NFS 4.1 with `rootSquashType: RootSquash` | Real POSIX, but root is squashed | Real `EPERM` (see NFS ownership caveat below) | Works | +| GCP Cloud Storage FUSE (`gcsfuse.csi.storage.gke.io`) | Mount-time uid/gid/mode, not stored | Not supported - silent-loss class | Not supported - silent-loss class | +| AWS FSx for Windows (SMB, NTFS ACLs) | NTFS ACLs, don't map to POSIX ownership | Doesn't map | Doesn't map | +| AWS EFS Access Points with a `PosixUser` | Access Point overrides uid/gid for all operations | Neutralized server-side | N/A | + +Azure Disk (block storage) and plain Azure Files/EFS without the above configurations use real POSIX semantics +and are not affected. + +### Verified remediation for Azure Files SMB + +Add `idsfromsid,modefromsid` to the StorageClass `mountOptions`, and do **not** force `uid=`/`gid=`/`mode=` +alongside them. This stores real per-file ownership and mode in the share's NTFS security descriptors and gives +full fidelity across backup and restore. + +Caveats: + +- On a fresh share, the volume root receives a translated security descriptor on first mount (typically + `uid=0 gid= mode=1707`). Non-root workloads may need a one-time root init container to `chown`/`chmod` + the volume root before the main container starts; this operation itself works correctly on this mount. +- A restrictive owner/mode on the volume root can prevent Velero's FSB restore-wait init container from + accessing the volume if its identity doesn't match the workload's. If you hit a restore stuck at + `Init:0/1`, configure the restore helper's security context (`secCtxRunAsUser`, `secCtxRunAsGroup`, or `secCtx`) + to match your workload's UID/GID. See [Customize Restore Helper Container](#customize-restore-helper-container). + +As an alternative, Azure Files NFS 4.1 (Premium tier) or Azure Blob NFSv3 (Premium tier) also preserve ownership +and mode with full fidelity, **provided you avoid `rootSquashType: RootSquash`**. Root-squashed NFS mounts +reject root's `chown` with a real `EPERM`, which is a different (but related) failure. See the NFS ownership +note below. + +### No remediation exists for blobfuse or gcsfuse + +For Azure Blob via blobfuse and GCP Cloud Storage FUSE, there is currently no mount option or configuration +that preserves per-file ownership or mode. This is a limitation of the FUSE drivers themselves, not something +Velero or its restore path can work around. If your workload depends on stat-level ownership fidelity (for +example, databases like PostgreSQL or MySQL that refuse to start if the data directory's ownership doesn't +match the running user), avoid these volume types for that data. Use block storage, a real POSIX-backed +protocol (e.g. Azure Files NFS 4.1, Azure Blob NFSv3), or Azure Files SMB with `idsfromsid,modefromsid` instead. + +### Related: NFS root_squash ownership loss + +A related but mechanically distinct issue affects NFS mounts with server-side `root_squash` enabled: the +`chown` call receives a real `EPERM` from the server, but Velero's kopia integration currently sets +`IgnorePermissionErrors: true`, which silently discards that error. The end result looks the same to the user +(a `Completed` restore with lost ownership), but the underlying mechanism differs. Here an error genuinely +occurs, it is simply swallowed, whereas on mount-constant-identity filesystems no error is ever generated in +the first place. If you're troubleshooting ownership loss on NFS-backed volumes with root squashing enabled, +this is the more likely cause. + +### Diagnosing which case you're hitting + +Check the mount options inside the affected pod: + + `mount | grep -E 'cifs|fuse|nfs'` + +Look for `uid=`/`gid=` (CIFS) or a FUSE filesystem type. The typical symptom in all these cases is a restore +that reports `Completed` with no warnings, followed by an application failing immediately afterward with an +ownership-related error, for example: + + FATAL: data directory "/var/lib/postgresql/data/pgdata" has wrong ownership + HINT: The server must be started by the user that owns the data directory. + +This signature, a clean restore followed by an immediate ownership-related crash, is the indicator that +you're affected by one of the limitations described above rather than a genuine restore failure. + ## Customize Restore Helper Container From ca72c2e7e2cc448fb071d2df756fd44f0295ce4e Mon Sep 17 00:00:00 2001 From: AftAb-25 Date: Tue, 4 Aug 2026 22:36:45 +0530 Subject: [PATCH 161/194] Fix missing `gcFailureBSLUnavailable` label during garbage collection (#10154) * Fix gcFailureBSLUnavailable label not applied (Issue #10153) Signed-off-by: aftab * Fix linter error: use require.NoError before checking label Signed-off-by: aftab --------- Signed-off-by: aftab --- pkg/controller/gc_controller.go | 4 ++++ pkg/controller/gc_controller_test.go | 27 ++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/pkg/controller/gc_controller.go b/pkg/controller/gc_controller.go index 6b3ade484..f477ae9c6 100644 --- a/pkg/controller/gc_controller.go +++ b/pkg/controller/gc_controller.go @@ -156,6 +156,10 @@ func (c *gcReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Re if !veleroutil.BSLIsAvailable(*loc) { log.Infof("BSL %s is unavailable, cannot gc backup", loc.Name) + backup.Labels[garbageCollectionFailure] = gcFailureBSLUnavailable + if err := c.Update(ctx, backup); err != nil { + log.WithError(err).Error("error updating backup labels") + } return ctrl.Result{}, fmt.Errorf("bsl %s is unavailable, cannot gc backup", loc.Name) } diff --git a/pkg/controller/gc_controller_test.go b/pkg/controller/gc_controller_test.go index 754b46e0a..be7553888 100644 --- a/pkg/controller/gc_controller_test.go +++ b/pkg/controller/gc_controller_test.go @@ -21,6 +21,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -48,11 +49,12 @@ func TestGCReconcile(t *testing.T) { defaultBackupLocation := builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "default").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result() tests := []struct { - name string - backup *velerov1api.Backup - deleteBackupRequests []*velerov1api.DeleteBackupRequest - backupLocation *velerov1api.BackupStorageLocation - expectError bool + name string + backup *velerov1api.Backup + deleteBackupRequests []*velerov1api.DeleteBackupRequest + backupLocation *velerov1api.BackupStorageLocation + expectError bool + expectedGCFailureLabel string }{ { name: "can't find backup - no error", @@ -118,10 +120,11 @@ func TestGCReconcile(t *testing.T) { }, }, { - name: "BSL is unavailable", - backup: defaultBackup().Expiration(fakeClock.Now().Add(-time.Second)).StorageLocation("default").Result(), - backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "default").Phase(velerov1api.BackupStorageLocationPhaseUnavailable).Result(), - expectError: true, + name: "BSL is unavailable", + backup: defaultBackup().Expiration(fakeClock.Now().Add(-time.Second)).StorageLocation("default").Result(), + backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "default").Phase(velerov1api.BackupStorageLocationPhaseUnavailable).Result(), + expectError: true, + expectedGCFailureLabel: gcFailureBSLUnavailable, }, } @@ -147,6 +150,12 @@ func TestGCReconcile(t *testing.T) { _, err := reconciler.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.backup.Namespace, Name: test.backup.Name}}) gotErr := err != nil assert.Equal(t, test.expectError, gotErr) + + if test.expectedGCFailureLabel != "" { + updatedBackup := &velerov1api.Backup{} + require.NoError(t, fakeClient.Get(t.Context(), types.NamespacedName{Namespace: test.backup.Namespace, Name: test.backup.Name}, updatedBackup)) + assert.Equal(t, test.expectedGCFailureLabel, updatedBackup.Labels[garbageCollectionFailure]) + } }) } } From 3c49bbec752556e295f7f3f48573ea9c21564717 Mon Sep 17 00:00:00 2001 From: Joseph Date: Wed, 22 Jul 2026 09:17:38 -0400 Subject: [PATCH 162/194] Add dynamic resource autocompletion to Velero CLI Register cobra completion callbacks for all commands that accept existing Velero resource names. A centralized completeNames helper uses apimachinery's meta.ExtractList/Accessor to list resources with a 3-second timeout, filter by prefix, and deduplicate already-typed arguments. Wires ValidArgsFunction on 20 commands and RegisterFlagCompletionFunc on 9 flags across backup, restore, schedule, backuplocation, snapshotlocation, repo, and debug. Closes #9782 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joseph --- changelogs/unreleased/9720-Joeavaikath | 1 + pkg/cmd/cli/backup/create.go | 5 + pkg/cmd/cli/backup/delete.go | 1 + pkg/cmd/cli/backup/describe.go | 2 + pkg/cmd/cli/backup/download.go | 2 + pkg/cmd/cli/backup/get.go | 2 + pkg/cmd/cli/backup/logs.go | 2 + pkg/cmd/cli/backuplocation/delete.go | 1 + pkg/cmd/cli/backuplocation/get.go | 2 + pkg/cmd/cli/backuplocation/set.go | 2 + pkg/cmd/cli/completion_functions.go | 97 ++++++++ pkg/cmd/cli/completion_functions_test.go | 212 ++++++++++++++++++ pkg/cmd/cli/debug/debug.go | 5 + pkg/cmd/cli/repo/get.go | 2 + pkg/cmd/cli/restore/create.go | 4 + pkg/cmd/cli/restore/delete.go | 1 + pkg/cmd/cli/restore/describe.go | 2 + pkg/cmd/cli/restore/get.go | 2 + pkg/cmd/cli/restore/logs.go | 2 + pkg/cmd/cli/schedule/create.go | 4 + pkg/cmd/cli/schedule/delete.go | 1 + pkg/cmd/cli/schedule/describe.go | 2 + pkg/cmd/cli/schedule/get.go | 2 + pkg/cmd/cli/schedule/pause.go | 1 + pkg/cmd/cli/schedule/unpause.go | 1 + pkg/cmd/cli/snapshotlocation/get.go | 2 + pkg/cmd/cli/snapshotlocation/set.go | 2 + .../docs/main/customize-installation.md | 2 +- 28 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/9720-Joeavaikath create mode 100644 pkg/cmd/cli/completion_functions.go create mode 100644 pkg/cmd/cli/completion_functions_test.go diff --git a/changelogs/unreleased/9720-Joeavaikath b/changelogs/unreleased/9720-Joeavaikath new file mode 100644 index 000000000..cde7a017f --- /dev/null +++ b/changelogs/unreleased/9720-Joeavaikath @@ -0,0 +1 @@ +Add dynamic resource autocompletion to Velero CLI diff --git a/pkg/cmd/cli/backup/create.go b/pkg/cmd/cli/backup/create.go index 5e18f468f..ae9dd2fec 100644 --- a/pkg/cmd/cli/backup/create.go +++ b/pkg/cmd/cli/backup/create.go @@ -32,6 +32,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" "github.com/vmware-tanzu/velero/pkg/util/collections" @@ -75,6 +76,10 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command { output.BindFlags(c.Flags()) output.ClearOutputFlagDefault(c) + _ = c.RegisterFlagCompletionFunc("from-schedule", cli.CompleteScheduleNames(f)) + _ = c.RegisterFlagCompletionFunc("storage-location", cli.CompleteBackupStorageLocationNames(f)) + _ = c.RegisterFlagCompletionFunc("volume-snapshot-locations", cli.CompleteVolumeSnapshotLocationNames(f)) + return c } diff --git a/pkg/cmd/cli/backup/delete.go b/pkg/cmd/cli/backup/delete.go index f4eaf1b83..ba5a4954b 100644 --- a/pkg/cmd/cli/backup/delete.go +++ b/pkg/cmd/cli/backup/delete.go @@ -64,6 +64,7 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteBackupNames(f) o.BindFlags(c.Flags()) return c diff --git a/pkg/cmd/cli/backup/describe.go b/pkg/cmd/cli/backup/describe.go index b0ef4a93e..dd819edd1 100644 --- a/pkg/cmd/cli/backup/describe.go +++ b/pkg/cmd/cli/backup/describe.go @@ -29,6 +29,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" "github.com/vmware-tanzu/velero/pkg/label" ) @@ -112,6 +113,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteBackupNames(f) c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.") c.Flags().BoolVar(&details, "details", details, "Display additional detail in the command output.") c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") diff --git a/pkg/cmd/cli/backup/download.go b/pkg/cmd/cli/backup/download.go index e4afd216c..a8d692520 100644 --- a/pkg/cmd/cli/backup/download.go +++ b/pkg/cmd/cli/backup/download.go @@ -31,6 +31,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/cacert" "github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest" ) @@ -55,6 +56,7 @@ func NewDownloadCommand(f client.Factory) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteBackupNames(f) o.BindFlags(c.Flags()) return c diff --git a/pkg/cmd/cli/backup/get.go b/pkg/cmd/cli/backup/get.go index 159fac30d..1af80399b 100644 --- a/pkg/cmd/cli/backup/get.go +++ b/pkg/cmd/cli/backup/get.go @@ -27,6 +27,7 @@ import ( api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" ) @@ -66,6 +67,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteBackupNames(f) c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector") output.BindFlags(c.Flags()) diff --git a/pkg/cmd/cli/backup/logs.go b/pkg/cmd/cli/backup/logs.go index a0149acf1..6e60c30f1 100644 --- a/pkg/cmd/cli/backup/logs.go +++ b/pkg/cmd/cli/backup/logs.go @@ -30,6 +30,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/cacert" "github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest" ) @@ -119,6 +120,7 @@ func NewLogsCommand(f client.Factory) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteBackupNames(f) l.BindFlags(c.Flags()) return c diff --git a/pkg/cmd/cli/backuplocation/delete.go b/pkg/cmd/cli/backuplocation/delete.go index 9c1e60507..eabadef97 100644 --- a/pkg/cmd/cli/backuplocation/delete.go +++ b/pkg/cmd/cli/backuplocation/delete.go @@ -62,6 +62,7 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteBackupStorageLocationNames(f) o.BindFlags(c.Flags()) return c } diff --git a/pkg/cmd/cli/backuplocation/get.go b/pkg/cmd/cli/backuplocation/get.go index fd7c057c2..964ae5a7e 100644 --- a/pkg/cmd/cli/backuplocation/get.go +++ b/pkg/cmd/cli/backuplocation/get.go @@ -27,6 +27,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" ) @@ -89,6 +90,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteBackupStorageLocationNames(f) c.Flags().BoolVar(&showDefaultOnly, "default", false, "Displays the current default backup storage location.") c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.") diff --git a/pkg/cmd/cli/backuplocation/set.go b/pkg/cmd/cli/backuplocation/set.go index c1b52e536..2024f0761 100644 --- a/pkg/cmd/cli/backuplocation/set.go +++ b/pkg/cmd/cli/backuplocation/set.go @@ -33,6 +33,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" "github.com/vmware-tanzu/velero/pkg/util/boolptr" ) @@ -51,6 +52,7 @@ func NewSetCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteBackupStorageLocationNames(f) o.BindFlags(c.Flags()) return c diff --git a/pkg/cmd/cli/completion_functions.go b/pkg/cmd/cli/completion_functions.go new file mode 100644 index 000000000..3a7231484 --- /dev/null +++ b/pkg/cmd/cli/completion_functions.go @@ -0,0 +1,97 @@ +/* +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 cli + +import ( + "context" + "strings" + "time" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/meta" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/client" +) + +// completionFunc is the function signature for cobra's ValidArgsFunction. +type completionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) + +// completeNames builds a completion function for any Velero list type. +// It extracts resource names via apimachinery's meta helpers. +func completeNames(f client.Factory, list kbclient.ObjectList) completionFunc { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + kbClient, err := f.KubebuilderClient() + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + freshList := list.DeepCopyObject().(kbclient.ObjectList) + if err := kbClient.List(ctx, freshList, &kbclient.ListOptions{Namespace: f.Namespace()}); err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + items, err := meta.ExtractList(freshList) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + seen := make(map[string]bool, len(args)) + for _, a := range args { + seen[a] = true + } + var filtered []string + for _, item := range items { + accessor, err := meta.Accessor(item) + if err != nil { + continue + } + name := accessor.GetName() + if seen[name] { + continue + } + if strings.HasPrefix(name, toComplete) { + filtered = append(filtered, name) + } + } + return filtered, cobra.ShellCompDirectiveNoFileComp + } +} + +func CompleteBackupNames(f client.Factory) completionFunc { + return completeNames(f, &velerov1api.BackupList{}) +} + +func CompleteRestoreNames(f client.Factory) completionFunc { + return completeNames(f, &velerov1api.RestoreList{}) +} + +func CompleteScheduleNames(f client.Factory) completionFunc { + return completeNames(f, &velerov1api.ScheduleList{}) +} + +func CompleteBackupStorageLocationNames(f client.Factory) completionFunc { + return completeNames(f, &velerov1api.BackupStorageLocationList{}) +} + +func CompleteVolumeSnapshotLocationNames(f client.Factory) completionFunc { + return completeNames(f, &velerov1api.VolumeSnapshotLocationList{}) +} + +func CompleteBackupRepositoryNames(f client.Factory) completionFunc { + return completeNames(f, &velerov1api.BackupRepositoryList{}) +} diff --git a/pkg/cmd/cli/completion_functions_test.go b/pkg/cmd/cli/completion_functions_test.go new file mode 100644 index 000000000..b765ed54a --- /dev/null +++ b/pkg/cmd/cli/completion_functions_test.go @@ -0,0 +1,212 @@ +/* +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 cli + +import ( + "fmt" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks" + velerotest "github.com/vmware-tanzu/velero/pkg/test" +) + +// TestCompleteNames exercises the core completeNames helper with various list +// types, prefix filters, and edge cases (empty cluster, no match). +func TestCompleteNames(t *testing.T) { + tests := []struct { + name string + objects []runtime.Object + list kbclient.ObjectList + args []string + toComplete string + want []string + }{ + { + name: "no resources returns nil", + objects: nil, + list: &velerov1api.BackupList{}, + toComplete: "", + want: nil, + }, + { + name: "returns all matching names", + objects: []runtime.Object{ + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily", Namespace: "velero"}}, + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "weekly", Namespace: "velero"}}, + }, + list: &velerov1api.BackupList{}, + toComplete: "", + want: []string{"daily", "weekly"}, + }, + { + name: "filters by prefix", + objects: []runtime.Object{ + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily", Namespace: "velero"}}, + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "weekly", Namespace: "velero"}}, + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily-full", Namespace: "velero"}}, + }, + list: &velerov1api.BackupList{}, + toComplete: "dai", + want: []string{"daily", "daily-full"}, + }, + { + name: "no prefix match returns nil", + objects: []runtime.Object{ + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily", Namespace: "velero"}}, + }, + list: &velerov1api.BackupList{}, + toComplete: "xyz", + want: nil, + }, + { + name: "works with RestoreList", + objects: []runtime.Object{ + &velerov1api.Restore{ObjectMeta: metav1.ObjectMeta{Name: "restore-1", Namespace: "velero"}}, + &velerov1api.Restore{ObjectMeta: metav1.ObjectMeta{Name: "restore-2", Namespace: "velero"}}, + }, + list: &velerov1api.RestoreList{}, + toComplete: "restore-", + want: []string{"restore-1", "restore-2"}, + }, + { + name: "works with ScheduleList", + objects: []runtime.Object{ + &velerov1api.Schedule{ObjectMeta: metav1.ObjectMeta{Name: "nightly", Namespace: "velero"}}, + }, + list: &velerov1api.ScheduleList{}, + toComplete: "", + want: []string{"nightly"}, + }, + { + name: "works with BackupStorageLocationList", + objects: []runtime.Object{ + &velerov1api.BackupStorageLocation{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "velero"}}, + &velerov1api.BackupStorageLocation{ObjectMeta: metav1.ObjectMeta{Name: "secondary", Namespace: "velero"}}, + }, + list: &velerov1api.BackupStorageLocationList{}, + toComplete: "s", + want: []string{"secondary"}, + }, + { + name: "works with VolumeSnapshotLocationList", + objects: []runtime.Object{ + &velerov1api.VolumeSnapshotLocation{ObjectMeta: metav1.ObjectMeta{Name: "aws-snap", Namespace: "velero"}}, + }, + list: &velerov1api.VolumeSnapshotLocationList{}, + toComplete: "", + want: []string{"aws-snap"}, + }, + { + name: "works with BackupRepositoryList", + objects: []runtime.Object{ + &velerov1api.BackupRepository{ObjectMeta: metav1.ObjectMeta{Name: "repo-1", Namespace: "velero"}}, + }, + list: &velerov1api.BackupRepositoryList{}, + toComplete: "", + want: []string{"repo-1"}, + }, + { + name: "excludes already-typed args", + objects: []runtime.Object{ + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily", Namespace: "velero"}}, + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "weekly", Namespace: "velero"}}, + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "monthly", Namespace: "velero"}}, + }, + list: &velerov1api.BackupList{}, + args: []string{"daily", "monthly"}, + toComplete: "", + want: []string{"weekly"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + kbClient := velerotest.NewFakeControllerRuntimeClient(t, tc.objects...) + + f := new(factorymocks.Factory) + f.On("KubebuilderClient").Return(kbClient, nil) + f.On("Namespace").Return("velero") + + completionFn := completeNames(f, tc.list) + got, directive := completionFn(&cobra.Command{}, tc.args, tc.toComplete) + + assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) + assert.Equal(t, tc.want, got) + }) + } +} + +// TestCompleteNames_KubebuilderClientError verifies that a factory error +// (e.g. no kubeconfig) returns nil completions instead of panicking. +func TestCompleteNames_KubebuilderClientError(t *testing.T) { + f := new(factorymocks.Factory) + f.On("KubebuilderClient").Return(nil, fmt.Errorf("connection refused")) + + completionFn := completeNames(f, &velerov1api.BackupList{}) + got, directive := completionFn(&cobra.Command{}, nil, "") + + assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) + assert.Nil(t, got) +} + +// TestCompleteWrappers verifies each exported Complete*Names wrapper returns +// only its own resource type. A single fake client holds one object of every +// type, so each wrapper must filter correctly and not leak other kinds. +func TestCompleteWrappers(t *testing.T) { + objects := []runtime.Object{ + &velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "b1", Namespace: "velero"}}, + &velerov1api.Restore{ObjectMeta: metav1.ObjectMeta{Name: "r1", Namespace: "velero"}}, + &velerov1api.Schedule{ObjectMeta: metav1.ObjectMeta{Name: "s1", Namespace: "velero"}}, + &velerov1api.BackupStorageLocation{ObjectMeta: metav1.ObjectMeta{Name: "bsl1", Namespace: "velero"}}, + &velerov1api.VolumeSnapshotLocation{ObjectMeta: metav1.ObjectMeta{Name: "vsl1", Namespace: "velero"}}, + &velerov1api.BackupRepository{ObjectMeta: metav1.ObjectMeta{Name: "br1", Namespace: "velero"}}, + } + kbClient := velerotest.NewFakeControllerRuntimeClient(t, objects...) + + f := new(factorymocks.Factory) + f.On("KubebuilderClient").Return(kbClient, nil) + f.On("Namespace").Return("velero") + + tests := []struct { + name string + fn completionFunc + expected []string + }{ + {"CompleteBackupNames", CompleteBackupNames(f), []string{"b1"}}, + {"CompleteRestoreNames", CompleteRestoreNames(f), []string{"r1"}}, + {"CompleteScheduleNames", CompleteScheduleNames(f), []string{"s1"}}, + {"CompleteBackupStorageLocationNames", CompleteBackupStorageLocationNames(f), []string{"bsl1"}}, + {"CompleteVolumeSnapshotLocationNames", CompleteVolumeSnapshotLocationNames(f), []string{"vsl1"}}, + {"CompleteBackupRepositoryNames", CompleteBackupRepositoryNames(f), []string{"br1"}}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, directive := tc.fn(&cobra.Command{}, nil, "") + require.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) + assert.Equal(t, tc.expected, got) + }) + } +} diff --git a/pkg/cmd/cli/debug/debug.go b/pkg/cmd/cli/debug/debug.go index fac49d622..62f1d0823 100644 --- a/pkg/cmd/cli/debug/debug.go +++ b/pkg/cmd/cli/debug/debug.go @@ -38,6 +38,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" ) //go:embed cshd-scripts/velero.cshd @@ -171,6 +172,10 @@ specs of resources created by velero server, and optionally the logs of backup a }, } o.bindFlags(c.Flags()) + + _ = c.RegisterFlagCompletionFunc("backup", cli.CompleteBackupNames(f)) + _ = c.RegisterFlagCompletionFunc("restore", cli.CompleteRestoreNames(f)) + return c } diff --git a/pkg/cmd/cli/repo/get.go b/pkg/cmd/cli/repo/get.go index ec57b9845..b3b914ae3 100644 --- a/pkg/cmd/cli/repo/get.go +++ b/pkg/cmd/cli/repo/get.go @@ -27,6 +27,7 @@ import ( api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" ) @@ -66,6 +67,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteBackupRepositoryNames(f) c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.") output.BindFlags(c.Flags()) diff --git a/pkg/cmd/cli/restore/create.go b/pkg/cmd/cli/restore/create.go index c76097176..ac4284229 100644 --- a/pkg/cmd/cli/restore/create.go +++ b/pkg/cmd/cli/restore/create.go @@ -36,6 +36,7 @@ import ( api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" "github.com/vmware-tanzu/velero/pkg/util/boolptr" @@ -81,6 +82,9 @@ Notes: output.BindFlags(c.Flags()) output.ClearOutputFlagDefault(c) + _ = c.RegisterFlagCompletionFunc("from-backup", cli.CompleteBackupNames(f)) + _ = c.RegisterFlagCompletionFunc("from-schedule", cli.CompleteScheduleNames(f)) + return c } diff --git a/pkg/cmd/cli/restore/delete.go b/pkg/cmd/cli/restore/delete.go index 51c31e1da..b20186fb8 100644 --- a/pkg/cmd/cli/restore/delete.go +++ b/pkg/cmd/cli/restore/delete.go @@ -61,6 +61,7 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command { cmd.CheckError(Run(o)) }, } + c.ValidArgsFunction = cli.CompleteRestoreNames(f) o.BindFlags(c.Flags()) return c } diff --git a/pkg/cmd/cli/restore/describe.go b/pkg/cmd/cli/restore/describe.go index 6404ef21d..7fc58ce22 100644 --- a/pkg/cmd/cli/restore/describe.go +++ b/pkg/cmd/cli/restore/describe.go @@ -29,6 +29,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" "github.com/vmware-tanzu/velero/pkg/label" ) @@ -92,6 +93,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteRestoreNames(f) c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.") c.Flags().BoolVar(&details, "details", details, "Display additional detail in the command output.") c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") diff --git a/pkg/cmd/cli/restore/get.go b/pkg/cmd/cli/restore/get.go index 9a4014b25..568e31b8d 100644 --- a/pkg/cmd/cli/restore/get.go +++ b/pkg/cmd/cli/restore/get.go @@ -27,6 +27,7 @@ import ( api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" ) @@ -76,6 +77,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteRestoreNames(f) c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.") output.BindFlags(c.Flags()) diff --git a/pkg/cmd/cli/restore/logs.go b/pkg/cmd/cli/restore/logs.go index f4315c917..26d3123ac 100644 --- a/pkg/cmd/cli/restore/logs.go +++ b/pkg/cmd/cli/restore/logs.go @@ -29,6 +29,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/cacert" "github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest" ) @@ -82,6 +83,7 @@ func NewLogsCommand(f client.Factory) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteRestoreNames(f) c.Flags().DurationVar(&timeout, "timeout", timeout, "How long to wait to receive logs.") c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") c.Flags().StringVar(&caCertFile, "cacert", caCertFile, "Path to a certificate bundle to use when verifying TLS connections. If not specified, the CA certificate from the BackupStorageLocation will be used if available.") diff --git a/pkg/cmd/cli/schedule/create.go b/pkg/cmd/cli/schedule/create.go index 2e4a1e8e9..03f5626fd 100644 --- a/pkg/cmd/cli/schedule/create.go +++ b/pkg/cmd/cli/schedule/create.go @@ -30,6 +30,7 @@ import ( api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/cli/backup" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" ) @@ -77,6 +78,9 @@ example: "@every 2h30m".`, output.BindFlags(c.Flags()) output.ClearOutputFlagDefault(c) + _ = c.RegisterFlagCompletionFunc("storage-location", cli.CompleteBackupStorageLocationNames(f)) + _ = c.RegisterFlagCompletionFunc("volume-snapshot-locations", cli.CompleteVolumeSnapshotLocationNames(f)) + return c } diff --git a/pkg/cmd/cli/schedule/delete.go b/pkg/cmd/cli/schedule/delete.go index 78e8c9104..28418afbd 100644 --- a/pkg/cmd/cli/schedule/delete.go +++ b/pkg/cmd/cli/schedule/delete.go @@ -62,6 +62,7 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteScheduleNames(f) o.BindFlags(c.Flags()) return c } diff --git a/pkg/cmd/cli/schedule/describe.go b/pkg/cmd/cli/schedule/describe.go index 82c88dac7..b657245e9 100644 --- a/pkg/cmd/cli/schedule/describe.go +++ b/pkg/cmd/cli/schedule/describe.go @@ -28,6 +28,7 @@ import ( v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" ) @@ -73,6 +74,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteScheduleNames(f) c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.") return c diff --git a/pkg/cmd/cli/schedule/get.go b/pkg/cmd/cli/schedule/get.go index 88bd49fe0..ba8ddb122 100644 --- a/pkg/cmd/cli/schedule/get.go +++ b/pkg/cmd/cli/schedule/get.go @@ -27,6 +27,7 @@ import ( api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" ) @@ -71,6 +72,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteScheduleNames(f) c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.") output.BindFlags(c.Flags()) diff --git a/pkg/cmd/cli/schedule/pause.go b/pkg/cmd/cli/schedule/pause.go index 41a17f384..06fc43f5c 100644 --- a/pkg/cmd/cli/schedule/pause.go +++ b/pkg/cmd/cli/schedule/pause.go @@ -60,6 +60,7 @@ func NewPauseCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteScheduleNames(f) o.BindFlags(c.Flags()) pauseOpts.BindFlags(c.Flags()) diff --git a/pkg/cmd/cli/schedule/unpause.go b/pkg/cmd/cli/schedule/unpause.go index 72197a934..15107ba38 100644 --- a/pkg/cmd/cli/schedule/unpause.go +++ b/pkg/cmd/cli/schedule/unpause.go @@ -49,6 +49,7 @@ func NewUnpauseCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteScheduleNames(f) o.BindFlags(c.Flags()) pauseOpts.BindFlags(c.Flags()) diff --git a/pkg/cmd/cli/snapshotlocation/get.go b/pkg/cmd/cli/snapshotlocation/get.go index 2acddbf7f..79da478bf 100644 --- a/pkg/cmd/cli/snapshotlocation/get.go +++ b/pkg/cmd/cli/snapshotlocation/get.go @@ -26,6 +26,7 @@ import ( api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" ) @@ -56,6 +57,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command { cmd.CheckError(err) }, } + c.ValidArgsFunction = cli.CompleteVolumeSnapshotLocationNames(f) c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector") output.BindFlags(c.Flags()) return c diff --git a/pkg/cmd/cli/snapshotlocation/set.go b/pkg/cmd/cli/snapshotlocation/set.go index 0814bdfe7..c67ef4231 100644 --- a/pkg/cmd/cli/snapshotlocation/set.go +++ b/pkg/cmd/cli/snapshotlocation/set.go @@ -30,6 +30,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/cmd" + "github.com/vmware-tanzu/velero/pkg/cmd/cli" "github.com/vmware-tanzu/velero/pkg/cmd/util/flag" "github.com/vmware-tanzu/velero/pkg/cmd/util/output" ) @@ -48,6 +49,7 @@ func NewSetCommand(f client.Factory, use string) *cobra.Command { }, } + c.ValidArgsFunction = cli.CompleteVolumeSnapshotLocationNames(f) o.BindFlags(c.Flags()) return c } diff --git a/site/content/docs/main/customize-installation.md b/site/content/docs/main/customize-installation.md index e9561eea9..28cc24154 100644 --- a/site/content/docs/main/customize-installation.md +++ b/site/content/docs/main/customize-installation.md @@ -356,7 +356,7 @@ Run `velero install --help` or see the [Helm chart documentation](https://vmware ### Enabling shell autocompletion -**Velero CLI** provides autocompletion support for `Bash` and `Zsh`, which can save you a lot of typing. +**Velero CLI** provides autocompletion support for `Bash`, `Zsh`, and `Fish`, which can save you a lot of typing. In addition to command and flag names, the CLI dynamically completes resource names (backups, restores, schedules, etc.) by querying the cluster. Below are the procedures to set up autocompletion for `Bash` (including the difference between `Linux` and `macOS`) and `Zsh`. From 80440f5d5a26009ce22a1c0cb1b1193024a16060 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Tue, 4 Aug 2026 17:42:38 -0400 Subject: [PATCH 163/194] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Tiger Kaovilai --- pkg/cmd/cli/completion_functions.go | 12 ++++++++++-- pkg/cmd/cli/completion_functions_test.go | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/cli/completion_functions.go b/pkg/cmd/cli/completion_functions.go index 3a7231484..c2ef20d04 100644 --- a/pkg/cmd/cli/completion_functions.go +++ b/pkg/cmd/cli/completion_functions.go @@ -40,9 +40,17 @@ func completeNames(f client.Factory, list kbclient.ObjectList) completionFunc { if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + parentCtx := context.Background() + if cmd != nil && cmd.Context() != nil { + parentCtx = cmd.Context() + } + ctx, cancel := context.WithTimeout(parentCtx, 3*time.Second) defer cancel() - freshList := list.DeepCopyObject().(kbclient.ObjectList) + freshObject := list.DeepCopyObject() + freshList, ok := freshObject.(kbclient.ObjectList) + if !ok { + return nil, cobra.ShellCompDirectiveNoFileComp + } if err := kbClient.List(ctx, freshList, &kbclient.ListOptions{Namespace: f.Namespace()}); err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/pkg/cmd/cli/completion_functions_test.go b/pkg/cmd/cli/completion_functions_test.go index b765ed54a..3bc33402d 100644 --- a/pkg/cmd/cli/completion_functions_test.go +++ b/pkg/cmd/cli/completion_functions_test.go @@ -153,7 +153,7 @@ func TestCompleteNames(t *testing.T) { got, directive := completionFn(&cobra.Command{}, tc.args, tc.toComplete) assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) - assert.Equal(t, tc.want, got) + assert.ElementsMatch(t, tc.want, got) }) } } From 64079056b7b058c5998028379cdeed3b5ecd8ebb Mon Sep 17 00:00:00 2001 From: Joseph Date: Mon, 27 Jul 2026 10:47:57 -0400 Subject: [PATCH 164/194] Fast-fail backup when built-in data mover has no running node-agent Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Joseph --- changelogs/unreleased/9697-Joeavaikath | 1 + pkg/backup/actions/csi/pvc_action.go | 13 ++ pkg/backup/actions/csi/pvc_action_test.go | 55 +++++++-- pkg/nodeagent/node_agent.go | 30 +++++ pkg/nodeagent/node_agent_test.go | 141 ++++++++++++++++++++++ 5 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/9697-Joeavaikath diff --git a/changelogs/unreleased/9697-Joeavaikath b/changelogs/unreleased/9697-Joeavaikath new file mode 100644 index 000000000..ad8e5eb2e --- /dev/null +++ b/changelogs/unreleased/9697-Joeavaikath @@ -0,0 +1 @@ +Fail backup validation when built-in data mover is requested but no node-agent pods are running diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index 8df6d68de..6998d13ce 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -48,6 +48,7 @@ import ( veleroclient "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/kuberesource" "github.com/vmware-tanzu/velero/pkg/label" + "github.com/vmware-tanzu/velero/pkg/nodeagent" plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common" "github.com/vmware-tanzu/velero/pkg/plugin/utils/volumehelper" "github.com/vmware-tanzu/velero/pkg/plugin/velero" @@ -55,6 +56,7 @@ import ( uploaderUtil "github.com/vmware-tanzu/velero/pkg/uploader/util" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/csi" + datamover "github.com/vmware-tanzu/velero/pkg/util/datamover" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" podvolumeutil "github.com/vmware-tanzu/velero/pkg/util/podvolume" vhutil "github.com/vmware-tanzu/velero/pkg/util/volumehelper" @@ -340,6 +342,17 @@ func (p *pvcBackupItemAction) Execute( return nil, nil, "", nil, err } + // validate that the node-agent daemonset is ready when snapshot data movement with + // the built-in data mover is requested. Without this, the DataUpload CR will be + // created but never processed (the DataUpload controller runs inside node-agent), + // causing the backup to hang until itemOperationTimeout expires. + if boolptr.IsSetToTrue(backup.Spec.SnapshotMoveData) && datamover.IsBuiltInDataMover(backup.Spec.DataMover) { + if err := nodeagent.IsReady(context.TODO(), backup.Namespace, p.crClient, p.log); err != nil { + p.log.WithError(err).Error("cannot perform snapshot data movement without running node-agent pods") + return nil, nil, "", nil, errors.Wrap(err, "CSI PVC BIA cannot proceed: node-agent is not ready for snapshot data movement") + } + } + policySnapshotClass, scErr := vh.GetSnapshotClass(item, kuberesource.PersistentVolumeClaims) if scErr != nil { p.log.WithError(scErr).Warn("failed to get snapshotClass from volume policy, proceeding without it") diff --git a/pkg/backup/actions/csi/pvc_action_test.go b/pkg/backup/actions/csi/pvc_action_test.go index 316bf5868..e59591146 100644 --- a/pkg/backup/actions/csi/pvc_action_test.go +++ b/pkg/backup/actions/csi/pvc_action_test.go @@ -31,6 +31,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + appsv1api "k8s.io/api/apps/v1" corev1api "k8s.io/api/core/v1" storagev1api "k8s.io/api/storage/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -92,6 +93,7 @@ func TestExecute(t *testing.T) { expectedDataUpload *velerov2alpha1.DataUpload expectedPVC *corev1api.PersistentVolumeClaim resourcePolicy *corev1api.ConfigMap + extraObjects []runtime.Object failVSCreate bool skipVSReadyUpdate bool // New flag to control VS readiness expectedVSClassName string @@ -121,12 +123,21 @@ func TestExecute(t *testing.T) { expectErr: true, // Expect an error, but the exact message can vary }, { - name: "Test SnapshotMoveData", - backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), - pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(), - sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), - vsClass: builder.ForVolumeSnapshotClass("testVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(), + name: "Test SnapshotMoveData", + backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), + pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(), + sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), + vsClass: builder.ForVolumeSnapshotClass("testVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(), + extraObjects: []runtime.Object{ + &corev1api.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "linux-node", Labels: map[string]string{"kubernetes.io/os": "linux"}}, + }, + &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "velero", Name: "node-agent"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 3}, + }, + }, operationID: ".", expectedDataUpload: &velerov2alpha1.DataUpload{ TypeMeta: metav1.TypeMeta{ @@ -167,18 +178,37 @@ func TestExecute(t *testing.T) { }, }, { - name: "Verify PVC is modified as expected", - backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(), - pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), - pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(), - sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), - vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(), + name: "Verify PVC is modified as expected", + backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), + pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(), + sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), + vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(), + extraObjects: []runtime.Object{ + &corev1api.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "linux-node", Labels: map[string]string{"kubernetes.io/os": "linux"}}, + }, + &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "velero", Name: "node-agent"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 3}, + }, + }, operationID: ".", expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC"). ObjectMeta(builder.WithAnnotations(velerov1api.MustIncludeAdditionalItemAnnotation, "true", velerov1api.DataUploadNameAnnotation, "velero/"), builder.WithLabels(velerov1api.BackupNameLabel, "test")). VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), }, + { + name: "Test SnapshotMoveData without node-agent", + backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(), + pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(), + pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(), + sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(), + vsClass: builder.ForVolumeSnapshotClass("testVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(), + expectErr: true, + skipVSReadyUpdate: true, + }, { name: "Test ResourcePolicy", backup: builder.ForBackup("velero", "test").ResourcePolicies("resourcePolicy").SnapshotVolumes(false).CSISnapshotTimeout(time.Duration(3600) * time.Second).Result(), @@ -220,6 +250,7 @@ func TestExecute(t *testing.T) { if tc.resourcePolicy != nil { objects = append(objects, tc.resourcePolicy) } + objects = append(objects, tc.extraObjects...) var crClient crclient.Client if tc.failVSCreate { diff --git a/pkg/nodeagent/node_agent.go b/pkg/nodeagent/node_agent.go index 61720c99d..61dff9299 100644 --- a/pkg/nodeagent/node_agent.go +++ b/pkg/nodeagent/node_agent.go @@ -22,6 +22,8 @@ import ( "fmt" "github.com/cockroachdb/errors" + "github.com/sirupsen/logrus" + appsv1api "k8s.io/api/apps/v1" corev1api "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -80,6 +82,34 @@ func KbClientIsRunningInNode(ctx context.Context, namespace string, nodeName str return isRunningInNode(ctx, namespace, nodeName, nil, kubeClient) } +// IsReady checks whether the node-agent daemonset has at least one ready pod +// by inspecting the DaemonSet status. It only checks the daemonset for node +// OS types that are present in the cluster, following the same pattern as +// server.checkNodeAgent. +func IsReady(ctx context.Context, namespace string, crClient ctrlclient.Client, log logrus.FieldLogger) error { + if kube.WithLinuxNode(ctx, crClient, log) { + ds := new(appsv1api.DaemonSet) + if err := crClient.Get(ctx, ctrlclient.ObjectKey{Namespace: namespace, Name: daemonSet}, ds); err != nil { + return errors.Wrap(err, "failed to get linux node-agent daemonset") + } + if ds.Status.NumberReady > 0 { + return nil + } + } + + if kube.WithWindowsNode(ctx, crClient, log) { + ds := new(appsv1api.DaemonSet) + if err := crClient.Get(ctx, ctrlclient.ObjectKey{Namespace: namespace, Name: daemonsetWindows}, ds); err != nil { + return errors.Wrap(err, "failed to get windows node-agent daemonset") + } + if ds.Status.NumberReady > 0 { + return nil + } + } + + return errors.New("node-agent is not ready: no ready pods found") +} + // IsRunningInNode checks if the node agent pod is running properly in a specified node through controller client. If not, return the error found func IsRunningInNode(ctx context.Context, namespace string, nodeName string, crClient ctrlclient.Client) error { return isRunningInNode(ctx, namespace, nodeName, crClient, nil) diff --git a/pkg/nodeagent/node_agent_test.go b/pkg/nodeagent/node_agent_test.go index 36b154a75..a523bf15a 100644 --- a/pkg/nodeagent/node_agent_test.go +++ b/pkg/nodeagent/node_agent_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/cockroachdb/errors" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1api "k8s.io/api/apps/v1" @@ -213,6 +214,146 @@ func TestIsRunningInNode(t *testing.T) { } } +func TestIsReady(t *testing.T) { + scheme := runtime.NewScheme() + appsv1api.AddToScheme(scheme) + corev1api.AddToScheme(scheme) + + log := logrus.New() + + linuxNode := &corev1api.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "linux-node", + Labels: map[string]string{kube.NodeOSLabel: kube.NodeOSLinux}, + }, + } + windowsNode := &corev1api.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "windows-node", + Labels: map[string]string{kube.NodeOSLabel: kube.NodeOSWindows}, + }, + } + + dsLinuxNotReady := &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "fake-ns", Name: "node-agent"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 0}, + } + dsLinuxReady := &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "fake-ns", Name: "node-agent"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 3}, + } + dsWindowsNotReady := &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "fake-ns", Name: "node-agent-windows"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 0}, + } + dsWindowsReady := &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "fake-ns", Name: "node-agent-windows"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 2}, + } + + tests := []struct { + name string + kubeClientObj []runtime.Object + namespace string + expectErr string + }{ + { + name: "no nodes in cluster", + namespace: "fake-ns", + expectErr: "node-agent is not ready: no ready pods found", + }, + { + name: "linux node exists but daemonset not found", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + linuxNode, + }, + expectErr: "failed to get linux node-agent daemonset", + }, + { + name: "linux node and daemonset exist but no ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + linuxNode, + dsLinuxNotReady, + }, + expectErr: "node-agent is not ready: no ready pods found", + }, + { + name: "linux node and daemonset with ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + linuxNode, + dsLinuxReady, + }, + }, + { + name: "windows node and daemonset with ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + windowsNode, + dsWindowsReady, + }, + }, + { + name: "windows node and daemonset with no ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + windowsNode, + dsWindowsNotReady, + }, + expectErr: "node-agent is not ready: no ready pods found", + }, + { + name: "both node types with both daemonsets ready", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + linuxNode, + windowsNode, + dsLinuxReady, + dsWindowsReady, + }, + }, + { + name: "both node types but neither daemonset has ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + linuxNode, + windowsNode, + dsLinuxNotReady, + dsWindowsNotReady, + }, + expectErr: "node-agent is not ready: no ready pods found", + }, + { + name: "linux not ready but windows ready", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + linuxNode, + windowsNode, + dsLinuxNotReady, + dsWindowsReady, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClient := clientFake.NewClientBuilder(). + WithScheme(scheme). + WithRuntimeObjects(test.kubeClientObj...). + Build() + + err := IsReady(t.Context(), test.namespace, fakeClient, log) + if test.expectErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, test.expectErr) + } + }) + } +} + func TestGetPodSpec(t *testing.T) { podSpec := corev1api.PodSpec{ NodeName: "fake-node", From 66b637e398b886a764a7c249c28660baac59f8f8 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 6 Aug 2026 09:36:28 +0000 Subject: [PATCH 165/194] update protocol buffer code Signed-off-by: Lyndon-Li --- pkg/plugin/generated/BackupItemAction.pb.go | 201 ++---- pkg/plugin/generated/DeleteItemAction.pb.go | 160 ++--- pkg/plugin/generated/ObjectStore.pb.go | 603 +++++------------- pkg/plugin/generated/PluginLister.pb.go | 112 +--- pkg/plugin/generated/RestoreItemAction.pb.go | 219 ++----- pkg/plugin/generated/Shared.pb.go | 299 +++------ pkg/plugin/generated/VolumeSnapshotter.pb.go | 590 +++++------------ .../v2/BackupItemAction.pb.go | 344 +++------- .../itemblockaction/v1/ItemBlockAction.pb.go | 199 ++---- .../v2/RestoreItemAction.pb.go | 453 ++++--------- 10 files changed, 894 insertions(+), 2286 deletions(-) diff --git a/pkg/plugin/generated/BackupItemAction.pb.go b/pkg/plugin/generated/BackupItemAction.pb.go index 5d3d3cb7e..f9f363d1e 100644 --- a/pkg/plugin/generated/BackupItemAction.pb.go +++ b/pkg/plugin/generated/BackupItemAction.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: BackupItemAction.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,22 +22,19 @@ const ( ) type ExecuteRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` - Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ExecuteRequest) Reset() { *x = ExecuteRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_BackupItemAction_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_BackupItemAction_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ExecuteRequest) String() string { @@ -47,7 +45,7 @@ func (*ExecuteRequest) ProtoMessage() {} func (x *ExecuteRequest) ProtoReflect() protoreflect.Message { mi := &file_BackupItemAction_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -84,21 +82,18 @@ func (x *ExecuteRequest) GetBackup() []byte { } type ExecuteResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Item []byte `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - AdditionalItems []*ResourceIdentifier `protobuf:"bytes,2,rep,name=additionalItems,proto3" json:"additionalItems,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Item []byte `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + AdditionalItems []*ResourceIdentifier `protobuf:"bytes,2,rep,name=additionalItems,proto3" json:"additionalItems,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExecuteResponse) Reset() { *x = ExecuteResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_BackupItemAction_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_BackupItemAction_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ExecuteResponse) String() string { @@ -109,7 +104,7 @@ func (*ExecuteResponse) ProtoMessage() {} func (x *ExecuteResponse) ProtoReflect() protoreflect.Message { mi := &file_BackupItemAction_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -139,20 +134,17 @@ func (x *ExecuteResponse) GetAdditionalItems() []*ResourceIdentifier { } type BackupItemActionAppliesToRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + sizeCache protoimpl.SizeCache } func (x *BackupItemActionAppliesToRequest) Reset() { *x = BackupItemActionAppliesToRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_BackupItemAction_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_BackupItemAction_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *BackupItemActionAppliesToRequest) String() string { @@ -163,7 +155,7 @@ func (*BackupItemActionAppliesToRequest) ProtoMessage() {} func (x *BackupItemActionAppliesToRequest) ProtoReflect() protoreflect.Message { mi := &file_BackupItemAction_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -186,20 +178,17 @@ func (x *BackupItemActionAppliesToRequest) GetPlugin() string { } type BackupItemActionAppliesToResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ResourceSelector *ResourceSelector `protobuf:"bytes,1,opt,name=ResourceSelector,proto3" json:"ResourceSelector,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ResourceSelector *ResourceSelector `protobuf:"bytes,1,opt,name=ResourceSelector,proto3" json:"ResourceSelector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BackupItemActionAppliesToResponse) Reset() { *x = BackupItemActionAppliesToResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_BackupItemAction_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_BackupItemAction_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *BackupItemActionAppliesToResponse) String() string { @@ -210,7 +199,7 @@ func (*BackupItemActionAppliesToResponse) ProtoMessage() {} func (x *BackupItemActionAppliesToResponse) ProtoReflect() protoreflect.Message { mi := &file_BackupItemAction_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -234,65 +223,38 @@ func (x *BackupItemActionAppliesToResponse) GetResourceSelector() *ResourceSelec var File_BackupItemAction_proto protoreflect.FileDescriptor -var file_BackupItemAction_proto_rawDesc = []byte{ - 0x0a, 0x16, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x64, 0x1a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x54, 0x0a, 0x0e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, - 0x16, 0x0a, 0x06, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x06, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x22, 0x6e, 0x0a, 0x0f, 0x45, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, - 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x47, - 0x0a, 0x0f, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0f, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x3a, 0x0a, 0x20, 0x42, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, - 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x22, 0x6c, 0x0a, 0x21, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, - 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, - 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x32, 0xbc, 0x01, 0x0a, 0x10, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x66, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, - 0x73, 0x54, 0x6f, 0x12, 0x2b, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, - 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2c, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x42, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, - 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, - 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x67, 0x65, 0x6e, 0x65, - 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, - 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, - 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, 0x61, 0x6e, 0x7a, 0x75, 0x2f, 0x76, 0x65, 0x6c, 0x65, - 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x67, 0x65, - 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_BackupItemAction_proto_rawDesc = "" + + "\n" + + "\x16BackupItemAction.proto\x12\tgenerated\x1a\fShared.proto\"T\n" + + "\x0eExecuteRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x12\n" + + "\x04item\x18\x02 \x01(\fR\x04item\x12\x16\n" + + "\x06backup\x18\x03 \x01(\fR\x06backup\"n\n" + + "\x0fExecuteResponse\x12\x12\n" + + "\x04item\x18\x01 \x01(\fR\x04item\x12G\n" + + "\x0fadditionalItems\x18\x02 \x03(\v2\x1d.generated.ResourceIdentifierR\x0fadditionalItems\":\n" + + " BackupItemActionAppliesToRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\"l\n" + + "!BackupItemActionAppliesToResponse\x12G\n" + + "\x10ResourceSelector\x18\x01 \x01(\v2\x1b.generated.ResourceSelectorR\x10ResourceSelector2\xbc\x01\n" + + "\x10BackupItemAction\x12f\n" + + "\tAppliesTo\x12+.generated.BackupItemActionAppliesToRequest\x1a,.generated.BackupItemActionAppliesToResponse\x12@\n" + + "\aExecute\x12\x19.generated.ExecuteRequest\x1a\x1a.generated.ExecuteResponseB5Z3github.com/vmware-tanzu/velero/pkg/plugin/generatedb\x06proto3" var ( file_BackupItemAction_proto_rawDescOnce sync.Once - file_BackupItemAction_proto_rawDescData = file_BackupItemAction_proto_rawDesc + file_BackupItemAction_proto_rawDescData []byte ) func file_BackupItemAction_proto_rawDescGZIP() []byte { file_BackupItemAction_proto_rawDescOnce.Do(func() { - file_BackupItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(file_BackupItemAction_proto_rawDescData) + file_BackupItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_BackupItemAction_proto_rawDesc), len(file_BackupItemAction_proto_rawDesc))) }) return file_BackupItemAction_proto_rawDescData } var file_BackupItemAction_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_BackupItemAction_proto_goTypes = []interface{}{ +var file_BackupItemAction_proto_goTypes = []any{ (*ExecuteRequest)(nil), // 0: generated.ExecuteRequest (*ExecuteResponse)(nil), // 1: generated.ExecuteResponse (*BackupItemActionAppliesToRequest)(nil), // 2: generated.BackupItemActionAppliesToRequest @@ -320,61 +282,11 @@ func file_BackupItemAction_proto_init() { return } file_Shared_proto_init() - if !protoimpl.UnsafeEnabled { - file_BackupItemAction_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExecuteRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_BackupItemAction_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExecuteResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_BackupItemAction_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupItemActionAppliesToRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_BackupItemAction_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupItemActionAppliesToResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_BackupItemAction_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_BackupItemAction_proto_rawDesc), len(file_BackupItemAction_proto_rawDesc)), NumEnums: 0, NumMessages: 4, NumExtensions: 0, @@ -385,7 +297,6 @@ func file_BackupItemAction_proto_init() { MessageInfos: file_BackupItemAction_proto_msgTypes, }.Build() File_BackupItemAction_proto = out.File - file_BackupItemAction_proto_rawDesc = nil file_BackupItemAction_proto_goTypes = nil file_BackupItemAction_proto_depIdxs = nil } diff --git a/pkg/plugin/generated/DeleteItemAction.pb.go b/pkg/plugin/generated/DeleteItemAction.pb.go index 871b63889..3935cf216 100644 --- a/pkg/plugin/generated/DeleteItemAction.pb.go +++ b/pkg/plugin/generated/DeleteItemAction.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: DeleteItemAction.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,22 +22,19 @@ const ( ) type DeleteItemActionExecuteRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` - Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteItemActionExecuteRequest) Reset() { *x = DeleteItemActionExecuteRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_DeleteItemAction_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_DeleteItemAction_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteItemActionExecuteRequest) String() string { @@ -47,7 +45,7 @@ func (*DeleteItemActionExecuteRequest) ProtoMessage() {} func (x *DeleteItemActionExecuteRequest) ProtoReflect() protoreflect.Message { mi := &file_DeleteItemAction_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -84,20 +82,17 @@ func (x *DeleteItemActionExecuteRequest) GetBackup() []byte { } type DeleteItemActionAppliesToRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteItemActionAppliesToRequest) Reset() { *x = DeleteItemActionAppliesToRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_DeleteItemAction_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_DeleteItemAction_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteItemActionAppliesToRequest) String() string { @@ -108,7 +103,7 @@ func (*DeleteItemActionAppliesToRequest) ProtoMessage() {} func (x *DeleteItemActionAppliesToRequest) ProtoReflect() protoreflect.Message { mi := &file_DeleteItemAction_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -131,20 +126,17 @@ func (x *DeleteItemActionAppliesToRequest) GetPlugin() string { } type DeleteItemActionAppliesToResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ResourceSelector *ResourceSelector `protobuf:"bytes,1,opt,name=ResourceSelector,proto3" json:"ResourceSelector,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ResourceSelector *ResourceSelector `protobuf:"bytes,1,opt,name=ResourceSelector,proto3" json:"ResourceSelector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeleteItemActionAppliesToResponse) Reset() { *x = DeleteItemActionAppliesToResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_DeleteItemAction_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_DeleteItemAction_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteItemActionAppliesToResponse) String() string { @@ -155,7 +147,7 @@ func (*DeleteItemActionAppliesToResponse) ProtoMessage() {} func (x *DeleteItemActionAppliesToResponse) ProtoReflect() protoreflect.Message { mi := &file_DeleteItemAction_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -179,60 +171,35 @@ func (x *DeleteItemActionAppliesToResponse) GetResourceSelector() *ResourceSelec var File_DeleteItemAction_proto protoreflect.FileDescriptor -var file_DeleteItemAction_proto_rawDesc = []byte{ - 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x64, 0x1a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x64, 0x0a, 0x1e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, - 0x16, 0x0a, 0x06, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x06, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x22, 0x3a, 0x0a, 0x20, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, - 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x22, 0x6c, 0x0a, 0x21, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, - 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, - 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x32, 0xc2, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x66, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, - 0x73, 0x54, 0x6f, 0x12, 0x2b, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2c, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, - 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, - 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x29, 0x2e, 0x67, 0x65, 0x6e, 0x65, - 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, 0x61, 0x6e, 0x7a, - 0x75, 0x2f, 0x76, 0x65, 0x6c, 0x65, 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_DeleteItemAction_proto_rawDesc = "" + + "\n" + + "\x16DeleteItemAction.proto\x12\tgenerated\x1a\fShared.proto\"d\n" + + "\x1eDeleteItemActionExecuteRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x12\n" + + "\x04item\x18\x02 \x01(\fR\x04item\x12\x16\n" + + "\x06backup\x18\x03 \x01(\fR\x06backup\":\n" + + " DeleteItemActionAppliesToRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\"l\n" + + "!DeleteItemActionAppliesToResponse\x12G\n" + + "\x10ResourceSelector\x18\x01 \x01(\v2\x1b.generated.ResourceSelectorR\x10ResourceSelector2\xc2\x01\n" + + "\x10DeleteItemAction\x12f\n" + + "\tAppliesTo\x12+.generated.DeleteItemActionAppliesToRequest\x1a,.generated.DeleteItemActionAppliesToResponse\x12F\n" + + "\aExecute\x12).generated.DeleteItemActionExecuteRequest\x1a\x10.generated.EmptyB5Z3github.com/vmware-tanzu/velero/pkg/plugin/generatedb\x06proto3" var ( file_DeleteItemAction_proto_rawDescOnce sync.Once - file_DeleteItemAction_proto_rawDescData = file_DeleteItemAction_proto_rawDesc + file_DeleteItemAction_proto_rawDescData []byte ) func file_DeleteItemAction_proto_rawDescGZIP() []byte { file_DeleteItemAction_proto_rawDescOnce.Do(func() { - file_DeleteItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(file_DeleteItemAction_proto_rawDescData) + file_DeleteItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_DeleteItemAction_proto_rawDesc), len(file_DeleteItemAction_proto_rawDesc))) }) return file_DeleteItemAction_proto_rawDescData } var file_DeleteItemAction_proto_msgTypes = make([]protoimpl.MessageInfo, 3) -var file_DeleteItemAction_proto_goTypes = []interface{}{ +var file_DeleteItemAction_proto_goTypes = []any{ (*DeleteItemActionExecuteRequest)(nil), // 0: generated.DeleteItemActionExecuteRequest (*DeleteItemActionAppliesToRequest)(nil), // 1: generated.DeleteItemActionAppliesToRequest (*DeleteItemActionAppliesToResponse)(nil), // 2: generated.DeleteItemActionAppliesToResponse @@ -258,49 +225,11 @@ func file_DeleteItemAction_proto_init() { return } file_Shared_proto_init() - if !protoimpl.UnsafeEnabled { - file_DeleteItemAction_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteItemActionExecuteRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_DeleteItemAction_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteItemActionAppliesToRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_DeleteItemAction_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteItemActionAppliesToResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_DeleteItemAction_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_DeleteItemAction_proto_rawDesc), len(file_DeleteItemAction_proto_rawDesc)), NumEnums: 0, NumMessages: 3, NumExtensions: 0, @@ -311,7 +240,6 @@ func file_DeleteItemAction_proto_init() { MessageInfos: file_DeleteItemAction_proto_msgTypes, }.Build() File_DeleteItemAction_proto = out.File - file_DeleteItemAction_proto_rawDesc = nil file_DeleteItemAction_proto_goTypes = nil file_DeleteItemAction_proto_depIdxs = nil } diff --git a/pkg/plugin/generated/ObjectStore.pb.go b/pkg/plugin/generated/ObjectStore.pb.go index 563849355..c960f159c 100644 --- a/pkg/plugin/generated/ObjectStore.pb.go +++ b/pkg/plugin/generated/ObjectStore.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: ObjectStore.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,23 +22,20 @@ const ( ) type PutObjectRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` + Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` + Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` - Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` - Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` + sizeCache protoimpl.SizeCache } func (x *PutObjectRequest) Reset() { *x = PutObjectRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PutObjectRequest) String() string { @@ -48,7 +46,7 @@ func (*PutObjectRequest) ProtoMessage() {} func (x *PutObjectRequest) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -92,22 +90,19 @@ func (x *PutObjectRequest) GetBody() []byte { } type ObjectExistsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` + Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` - Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ObjectExistsRequest) Reset() { *x = ObjectExistsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ObjectExistsRequest) String() string { @@ -118,7 +113,7 @@ func (*ObjectExistsRequest) ProtoMessage() {} func (x *ObjectExistsRequest) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -155,20 +150,17 @@ func (x *ObjectExistsRequest) GetKey() string { } type ObjectExistsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` unknownFields protoimpl.UnknownFields - - Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ObjectExistsResponse) Reset() { *x = ObjectExistsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ObjectExistsResponse) String() string { @@ -179,7 +171,7 @@ func (*ObjectExistsResponse) ProtoMessage() {} func (x *ObjectExistsResponse) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -202,22 +194,19 @@ func (x *ObjectExistsResponse) GetExists() bool { } type GetObjectRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` + Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` - Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetObjectRequest) Reset() { *x = GetObjectRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetObjectRequest) String() string { @@ -228,7 +217,7 @@ func (*GetObjectRequest) ProtoMessage() {} func (x *GetObjectRequest) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -265,20 +254,17 @@ func (x *GetObjectRequest) GetKey() string { } type Bytes struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields - - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Bytes) Reset() { *x = Bytes{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Bytes) String() string { @@ -289,7 +275,7 @@ func (*Bytes) ProtoMessage() {} func (x *Bytes) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -312,23 +298,20 @@ func (x *Bytes) GetData() []byte { } type ListCommonPrefixesRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` + Delimiter string `protobuf:"bytes,3,opt,name=delimiter,proto3" json:"delimiter,omitempty"` + Prefix string `protobuf:"bytes,4,opt,name=prefix,proto3" json:"prefix,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` - Delimiter string `protobuf:"bytes,3,opt,name=delimiter,proto3" json:"delimiter,omitempty"` - Prefix string `protobuf:"bytes,4,opt,name=prefix,proto3" json:"prefix,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListCommonPrefixesRequest) Reset() { *x = ListCommonPrefixesRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListCommonPrefixesRequest) String() string { @@ -339,7 +322,7 @@ func (*ListCommonPrefixesRequest) ProtoMessage() {} func (x *ListCommonPrefixesRequest) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -383,20 +366,17 @@ func (x *ListCommonPrefixesRequest) GetPrefix() string { } type ListCommonPrefixesResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Prefixes []string `protobuf:"bytes,1,rep,name=prefixes,proto3" json:"prefixes,omitempty"` unknownFields protoimpl.UnknownFields - - Prefixes []string `protobuf:"bytes,1,rep,name=prefixes,proto3" json:"prefixes,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListCommonPrefixesResponse) Reset() { *x = ListCommonPrefixesResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListCommonPrefixesResponse) String() string { @@ -407,7 +387,7 @@ func (*ListCommonPrefixesResponse) ProtoMessage() {} func (x *ListCommonPrefixesResponse) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -430,22 +410,19 @@ func (x *ListCommonPrefixesResponse) GetPrefixes() []string { } type ListObjectsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` + Prefix string `protobuf:"bytes,3,opt,name=prefix,proto3" json:"prefix,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` - Prefix string `protobuf:"bytes,3,opt,name=prefix,proto3" json:"prefix,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListObjectsRequest) Reset() { *x = ListObjectsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListObjectsRequest) String() string { @@ -456,7 +433,7 @@ func (*ListObjectsRequest) ProtoMessage() {} func (x *ListObjectsRequest) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -493,20 +470,17 @@ func (x *ListObjectsRequest) GetPrefix() string { } type ListObjectsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Keys []string `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` unknownFields protoimpl.UnknownFields - - Keys []string `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListObjectsResponse) Reset() { *x = ListObjectsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListObjectsResponse) String() string { @@ -517,7 +491,7 @@ func (*ListObjectsResponse) ProtoMessage() {} func (x *ListObjectsResponse) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -540,22 +514,19 @@ func (x *ListObjectsResponse) GetKeys() []string { } type DeleteObjectRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` + Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` - Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteObjectRequest) Reset() { *x = DeleteObjectRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteObjectRequest) String() string { @@ -566,7 +537,7 @@ func (*DeleteObjectRequest) ProtoMessage() {} func (x *DeleteObjectRequest) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -603,23 +574,20 @@ func (x *DeleteObjectRequest) GetKey() string { } type CreateSignedURLRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` + Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` + Ttl int64 `protobuf:"varint,4,opt,name=ttl,proto3" json:"ttl,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` - Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` - Ttl int64 `protobuf:"varint,4,opt,name=ttl,proto3" json:"ttl,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreateSignedURLRequest) Reset() { *x = CreateSignedURLRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateSignedURLRequest) String() string { @@ -630,7 +598,7 @@ func (*CreateSignedURLRequest) ProtoMessage() {} func (x *CreateSignedURLRequest) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -674,20 +642,17 @@ func (x *CreateSignedURLRequest) GetTtl() int64 { } type CreateSignedURLResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` unknownFields protoimpl.UnknownFields - - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreateSignedURLResponse) Reset() { *x = CreateSignedURLResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateSignedURLResponse) String() string { @@ -698,7 +663,7 @@ func (*CreateSignedURLResponse) ProtoMessage() {} func (x *CreateSignedURLResponse) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -721,21 +686,18 @@ func (x *CreateSignedURLResponse) GetUrl() string { } type ObjectStoreInitRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Config map[string]string `protobuf:"bytes,2,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Config map[string]string `protobuf:"bytes,2,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + sizeCache protoimpl.SizeCache } func (x *ObjectStoreInitRequest) Reset() { *x = ObjectStoreInitRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ObjectStore_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ObjectStore_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ObjectStoreInitRequest) String() string { @@ -746,7 +708,7 @@ func (*ObjectStoreInitRequest) ProtoMessage() {} func (x *ObjectStoreInitRequest) ProtoReflect() protoreflect.Message { mi := &file_ObjectStore_proto_msgTypes[12] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -777,138 +739,80 @@ func (x *ObjectStoreInitRequest) GetConfig() map[string]string { var File_ObjectStore_proto protoreflect.FileDescriptor -var file_ObjectStore_proto_rawDesc = []byte{ - 0x0a, 0x11, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x1a, 0x0c, - 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x68, 0x0a, 0x10, - 0x50, 0x75, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x63, 0x6b, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x57, 0x0a, 0x13, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, - 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, - 0x2e, 0x0a, 0x14, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, - 0x54, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x62, - 0x75, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x62, 0x75, 0x63, - 0x6b, 0x65, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x1b, 0x0a, 0x05, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x12, - 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, - 0x74, 0x61, 0x22, 0x81, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x63, 0x6b, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, - 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72, 0x12, 0x16, - 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0x38, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, - 0x22, 0x5c, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x16, - 0x0a, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0x29, - 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x57, 0x0a, 0x13, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x63, 0x6b, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x22, 0x6c, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, - 0x65, 0x64, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x74, 0x74, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x74, 0x74, 0x6c, - 0x22, 0x2b, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, - 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, - 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0xb2, 0x01, - 0x0a, 0x16, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x6e, 0x69, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x12, 0x45, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x2d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x39, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x32, 0xe4, 0x04, 0x0a, 0x0b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x6f, - 0x72, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x21, 0x2e, 0x67, 0x65, 0x6e, - 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x6f, - 0x72, 0x65, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x3c, 0x0a, 0x09, 0x50, 0x75, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1b, 0x2e, 0x67, - 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x50, 0x75, 0x74, 0x4f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x67, 0x65, 0x6e, 0x65, - 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x4f, 0x0a, - 0x0c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x1e, 0x2e, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, - 0x0a, 0x09, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1b, 0x2e, 0x67, 0x65, - 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x64, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x30, 0x01, 0x12, 0x61, 0x0a, 0x12, - 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x65, 0x73, 0x12, 0x24, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x50, - 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x4c, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x1d, - 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, - 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1e, 0x2e, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x58, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, - 0x52, 0x4c, 0x12, 0x21, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x52, 0x4c, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x52, - 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, - 0x61, 0x6e, 0x7a, 0x75, 0x2f, 0x76, 0x65, 0x6c, 0x65, 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_ObjectStore_proto_rawDesc = "" + + "\n" + + "\x11ObjectStore.proto\x12\tgenerated\x1a\fShared.proto\"h\n" + + "\x10PutObjectRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x16\n" + + "\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x10\n" + + "\x03key\x18\x03 \x01(\tR\x03key\x12\x12\n" + + "\x04body\x18\x04 \x01(\fR\x04body\"W\n" + + "\x13ObjectExistsRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x16\n" + + "\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x10\n" + + "\x03key\x18\x03 \x01(\tR\x03key\".\n" + + "\x14ObjectExistsResponse\x12\x16\n" + + "\x06exists\x18\x01 \x01(\bR\x06exists\"T\n" + + "\x10GetObjectRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x16\n" + + "\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x10\n" + + "\x03key\x18\x03 \x01(\tR\x03key\"\x1b\n" + + "\x05Bytes\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"\x81\x01\n" + + "\x19ListCommonPrefixesRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x16\n" + + "\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x1c\n" + + "\tdelimiter\x18\x03 \x01(\tR\tdelimiter\x12\x16\n" + + "\x06prefix\x18\x04 \x01(\tR\x06prefix\"8\n" + + "\x1aListCommonPrefixesResponse\x12\x1a\n" + + "\bprefixes\x18\x01 \x03(\tR\bprefixes\"\\\n" + + "\x12ListObjectsRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x16\n" + + "\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x16\n" + + "\x06prefix\x18\x03 \x01(\tR\x06prefix\")\n" + + "\x13ListObjectsResponse\x12\x12\n" + + "\x04keys\x18\x01 \x03(\tR\x04keys\"W\n" + + "\x13DeleteObjectRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x16\n" + + "\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x10\n" + + "\x03key\x18\x03 \x01(\tR\x03key\"l\n" + + "\x16CreateSignedURLRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x16\n" + + "\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x10\n" + + "\x03key\x18\x03 \x01(\tR\x03key\x12\x10\n" + + "\x03ttl\x18\x04 \x01(\x03R\x03ttl\"+\n" + + "\x17CreateSignedURLResponse\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\"\xb2\x01\n" + + "\x16ObjectStoreInitRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12E\n" + + "\x06config\x18\x02 \x03(\v2-.generated.ObjectStoreInitRequest.ConfigEntryR\x06config\x1a9\n" + + "\vConfigEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xe4\x04\n" + + "\vObjectStore\x12;\n" + + "\x04Init\x12!.generated.ObjectStoreInitRequest\x1a\x10.generated.Empty\x12<\n" + + "\tPutObject\x12\x1b.generated.PutObjectRequest\x1a\x10.generated.Empty(\x01\x12O\n" + + "\fObjectExists\x12\x1e.generated.ObjectExistsRequest\x1a\x1f.generated.ObjectExistsResponse\x12<\n" + + "\tGetObject\x12\x1b.generated.GetObjectRequest\x1a\x10.generated.Bytes0\x01\x12a\n" + + "\x12ListCommonPrefixes\x12$.generated.ListCommonPrefixesRequest\x1a%.generated.ListCommonPrefixesResponse\x12L\n" + + "\vListObjects\x12\x1d.generated.ListObjectsRequest\x1a\x1e.generated.ListObjectsResponse\x12@\n" + + "\fDeleteObject\x12\x1e.generated.DeleteObjectRequest\x1a\x10.generated.Empty\x12X\n" + + "\x0fCreateSignedURL\x12!.generated.CreateSignedURLRequest\x1a\".generated.CreateSignedURLResponseB5Z3github.com/vmware-tanzu/velero/pkg/plugin/generatedb\x06proto3" var ( file_ObjectStore_proto_rawDescOnce sync.Once - file_ObjectStore_proto_rawDescData = file_ObjectStore_proto_rawDesc + file_ObjectStore_proto_rawDescData []byte ) func file_ObjectStore_proto_rawDescGZIP() []byte { file_ObjectStore_proto_rawDescOnce.Do(func() { - file_ObjectStore_proto_rawDescData = protoimpl.X.CompressGZIP(file_ObjectStore_proto_rawDescData) + file_ObjectStore_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ObjectStore_proto_rawDesc), len(file_ObjectStore_proto_rawDesc))) }) return file_ObjectStore_proto_rawDescData } var file_ObjectStore_proto_msgTypes = make([]protoimpl.MessageInfo, 14) -var file_ObjectStore_proto_goTypes = []interface{}{ +var file_ObjectStore_proto_goTypes = []any{ (*PutObjectRequest)(nil), // 0: generated.PutObjectRequest (*ObjectExistsRequest)(nil), // 1: generated.ObjectExistsRequest (*ObjectExistsResponse)(nil), // 2: generated.ObjectExistsResponse @@ -956,169 +860,11 @@ func file_ObjectStore_proto_init() { return } file_Shared_proto_init() - if !protoimpl.UnsafeEnabled { - file_ObjectStore_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PutObjectRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ObjectExistsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ObjectExistsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetObjectRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Bytes); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListCommonPrefixesRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListCommonPrefixesResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListObjectsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListObjectsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteObjectRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateSignedURLRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateSignedURLResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ObjectStore_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ObjectStoreInitRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_ObjectStore_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ObjectStore_proto_rawDesc), len(file_ObjectStore_proto_rawDesc)), NumEnums: 0, NumMessages: 14, NumExtensions: 0, @@ -1129,7 +875,6 @@ func file_ObjectStore_proto_init() { MessageInfos: file_ObjectStore_proto_msgTypes, }.Build() File_ObjectStore_proto = out.File - file_ObjectStore_proto_rawDesc = nil file_ObjectStore_proto_goTypes = nil file_ObjectStore_proto_depIdxs = nil } diff --git a/pkg/plugin/generated/PluginLister.pb.go b/pkg/plugin/generated/PluginLister.pb.go index 590265750..239e57266 100644 --- a/pkg/plugin/generated/PluginLister.pb.go +++ b/pkg/plugin/generated/PluginLister.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: PluginLister.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,22 +22,19 @@ const ( ) type PluginIdentifier struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` + Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields - - Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` - Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + sizeCache protoimpl.SizeCache } func (x *PluginIdentifier) Reset() { *x = PluginIdentifier{} - if protoimpl.UnsafeEnabled { - mi := &file_PluginLister_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_PluginLister_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PluginIdentifier) String() string { @@ -47,7 +45,7 @@ func (*PluginIdentifier) ProtoMessage() {} func (x *PluginIdentifier) ProtoReflect() protoreflect.Message { mi := &file_PluginLister_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -84,20 +82,17 @@ func (x *PluginIdentifier) GetName() string { } type ListPluginsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugins []*PluginIdentifier `protobuf:"bytes,1,rep,name=plugins,proto3" json:"plugins,omitempty"` unknownFields protoimpl.UnknownFields - - Plugins []*PluginIdentifier `protobuf:"bytes,1,rep,name=plugins,proto3" json:"plugins,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListPluginsResponse) Reset() { *x = ListPluginsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_PluginLister_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_PluginLister_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListPluginsResponse) String() string { @@ -108,7 +103,7 @@ func (*ListPluginsResponse) ProtoMessage() {} func (x *ListPluginsResponse) ProtoReflect() protoreflect.Message { mi := &file_PluginLister_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -132,46 +127,32 @@ func (x *ListPluginsResponse) GetPlugins() []*PluginIdentifier { var File_PluginLister_proto protoreflect.FileDescriptor -var file_PluginLister_proto_rawDesc = []byte{ - 0x0a, 0x12, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x72, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x1a, - 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x54, 0x0a, - 0x10, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6b, - 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x22, 0x4c, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x07, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x65, - 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x49, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x73, 0x32, 0x4f, 0x0a, 0x0c, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x12, 0x3f, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, - 0x12, 0x10, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x1e, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x76, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, 0x61, 0x6e, 0x7a, 0x75, 0x2f, 0x76, 0x65, - 0x6c, 0x65, 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, -} +const file_PluginLister_proto_rawDesc = "" + + "\n" + + "\x12PluginLister.proto\x12\tgenerated\x1a\fShared.proto\"T\n" + + "\x10PluginIdentifier\x12\x18\n" + + "\acommand\x18\x01 \x01(\tR\acommand\x12\x12\n" + + "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"L\n" + + "\x13ListPluginsResponse\x125\n" + + "\aplugins\x18\x01 \x03(\v2\x1b.generated.PluginIdentifierR\aplugins2O\n" + + "\fPluginLister\x12?\n" + + "\vListPlugins\x12\x10.generated.Empty\x1a\x1e.generated.ListPluginsResponseB5Z3github.com/vmware-tanzu/velero/pkg/plugin/generatedb\x06proto3" var ( file_PluginLister_proto_rawDescOnce sync.Once - file_PluginLister_proto_rawDescData = file_PluginLister_proto_rawDesc + file_PluginLister_proto_rawDescData []byte ) func file_PluginLister_proto_rawDescGZIP() []byte { file_PluginLister_proto_rawDescOnce.Do(func() { - file_PluginLister_proto_rawDescData = protoimpl.X.CompressGZIP(file_PluginLister_proto_rawDescData) + file_PluginLister_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_PluginLister_proto_rawDesc), len(file_PluginLister_proto_rawDesc))) }) return file_PluginLister_proto_rawDescData } var file_PluginLister_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_PluginLister_proto_goTypes = []interface{}{ +var file_PluginLister_proto_goTypes = []any{ (*PluginIdentifier)(nil), // 0: generated.PluginIdentifier (*ListPluginsResponse)(nil), // 1: generated.ListPluginsResponse (*Empty)(nil), // 2: generated.Empty @@ -193,37 +174,11 @@ func file_PluginLister_proto_init() { return } file_Shared_proto_init() - if !protoimpl.UnsafeEnabled { - file_PluginLister_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PluginIdentifier); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_PluginLister_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPluginsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_PluginLister_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_PluginLister_proto_rawDesc), len(file_PluginLister_proto_rawDesc)), NumEnums: 0, NumMessages: 2, NumExtensions: 0, @@ -234,7 +189,6 @@ func file_PluginLister_proto_init() { MessageInfos: file_PluginLister_proto_msgTypes, }.Build() File_PluginLister_proto = out.File - file_PluginLister_proto_rawDesc = nil file_PluginLister_proto_goTypes = nil file_PluginLister_proto_depIdxs = nil } diff --git a/pkg/plugin/generated/RestoreItemAction.pb.go b/pkg/plugin/generated/RestoreItemAction.pb.go index 9489af476..f0d6dd3b7 100644 --- a/pkg/plugin/generated/RestoreItemAction.pb.go +++ b/pkg/plugin/generated/RestoreItemAction.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: RestoreItemAction.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,23 +22,20 @@ const ( ) type RestoreItemActionExecuteRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` - Restore []byte `protobuf:"bytes,3,opt,name=restore,proto3" json:"restore,omitempty"` - ItemFromBackup []byte `protobuf:"bytes,4,opt,name=itemFromBackup,proto3" json:"itemFromBackup,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + Restore []byte `protobuf:"bytes,3,opt,name=restore,proto3" json:"restore,omitempty"` + ItemFromBackup []byte `protobuf:"bytes,4,opt,name=itemFromBackup,proto3" json:"itemFromBackup,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionExecuteRequest) Reset() { *x = RestoreItemActionExecuteRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_RestoreItemAction_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_RestoreItemAction_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionExecuteRequest) String() string { @@ -48,7 +46,7 @@ func (*RestoreItemActionExecuteRequest) ProtoMessage() {} func (x *RestoreItemActionExecuteRequest) ProtoReflect() protoreflect.Message { mi := &file_RestoreItemAction_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -92,22 +90,19 @@ func (x *RestoreItemActionExecuteRequest) GetItemFromBackup() []byte { } type RestoreItemActionExecuteResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Item []byte `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - AdditionalItems []*ResourceIdentifier `protobuf:"bytes,2,rep,name=additionalItems,proto3" json:"additionalItems,omitempty"` - SkipRestore bool `protobuf:"varint,3,opt,name=skipRestore,proto3" json:"skipRestore,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Item []byte `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + AdditionalItems []*ResourceIdentifier `protobuf:"bytes,2,rep,name=additionalItems,proto3" json:"additionalItems,omitempty"` + SkipRestore bool `protobuf:"varint,3,opt,name=skipRestore,proto3" json:"skipRestore,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionExecuteResponse) Reset() { *x = RestoreItemActionExecuteResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_RestoreItemAction_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_RestoreItemAction_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionExecuteResponse) String() string { @@ -118,7 +113,7 @@ func (*RestoreItemActionExecuteResponse) ProtoMessage() {} func (x *RestoreItemActionExecuteResponse) ProtoReflect() protoreflect.Message { mi := &file_RestoreItemAction_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -155,20 +150,17 @@ func (x *RestoreItemActionExecuteResponse) GetSkipRestore() bool { } type RestoreItemActionAppliesToRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionAppliesToRequest) Reset() { *x = RestoreItemActionAppliesToRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_RestoreItemAction_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_RestoreItemAction_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionAppliesToRequest) String() string { @@ -179,7 +171,7 @@ func (*RestoreItemActionAppliesToRequest) ProtoMessage() {} func (x *RestoreItemActionAppliesToRequest) ProtoReflect() protoreflect.Message { mi := &file_RestoreItemAction_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -202,20 +194,17 @@ func (x *RestoreItemActionAppliesToRequest) GetPlugin() string { } type RestoreItemActionAppliesToResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ResourceSelector *ResourceSelector `protobuf:"bytes,1,opt,name=ResourceSelector,proto3" json:"ResourceSelector,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ResourceSelector *ResourceSelector `protobuf:"bytes,1,opt,name=ResourceSelector,proto3" json:"ResourceSelector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionAppliesToResponse) Reset() { *x = RestoreItemActionAppliesToResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_RestoreItemAction_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_RestoreItemAction_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionAppliesToResponse) String() string { @@ -226,7 +215,7 @@ func (*RestoreItemActionAppliesToResponse) ProtoMessage() {} func (x *RestoreItemActionAppliesToResponse) ProtoReflect() protoreflect.Message { mi := &file_RestoreItemAction_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -250,75 +239,40 @@ func (x *RestoreItemActionAppliesToResponse) GetResourceSelector() *ResourceSele var File_RestoreItemAction_proto protoreflect.FileDescriptor -var file_RestoreItemAction_proto_rawDesc = []byte{ - 0x0a, 0x17, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x64, 0x1a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x22, 0x8f, 0x01, 0x0a, 0x1f, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, - 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x12, - 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, - 0x65, 0x6d, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x26, 0x0a, 0x0e, - 0x69, 0x74, 0x65, 0x6d, 0x46, 0x72, 0x6f, 0x6d, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x69, 0x74, 0x65, 0x6d, 0x46, 0x72, 0x6f, 0x6d, 0x42, 0x61, - 0x63, 0x6b, 0x75, 0x70, 0x22, 0xa1, 0x01, 0x0a, 0x20, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x47, 0x0a, - 0x0f, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, - 0x65, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0f, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x6b, 0x69, 0x70, 0x52, 0x65, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x6b, 0x69, - 0x70, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x22, 0x3b, 0x0a, 0x21, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, - 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, - 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x6d, 0x0a, 0x22, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, - 0x73, 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x52, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x32, 0xe1, 0x01, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x68, 0x0a, 0x09, 0x41, 0x70, - 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x12, 0x2c, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, - 0x2a, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x67, 0x65, - 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, - 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, 0x61, - 0x6e, 0x7a, 0x75, 0x2f, 0x76, 0x65, 0x6c, 0x65, 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_RestoreItemAction_proto_rawDesc = "" + + "\n" + + "\x17RestoreItemAction.proto\x12\tgenerated\x1a\fShared.proto\"\x8f\x01\n" + + "\x1fRestoreItemActionExecuteRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x12\n" + + "\x04item\x18\x02 \x01(\fR\x04item\x12\x18\n" + + "\arestore\x18\x03 \x01(\fR\arestore\x12&\n" + + "\x0eitemFromBackup\x18\x04 \x01(\fR\x0eitemFromBackup\"\xa1\x01\n" + + " RestoreItemActionExecuteResponse\x12\x12\n" + + "\x04item\x18\x01 \x01(\fR\x04item\x12G\n" + + "\x0fadditionalItems\x18\x02 \x03(\v2\x1d.generated.ResourceIdentifierR\x0fadditionalItems\x12 \n" + + "\vskipRestore\x18\x03 \x01(\bR\vskipRestore\";\n" + + "!RestoreItemActionAppliesToRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\"m\n" + + "\"RestoreItemActionAppliesToResponse\x12G\n" + + "\x10ResourceSelector\x18\x01 \x01(\v2\x1b.generated.ResourceSelectorR\x10ResourceSelector2\xe1\x01\n" + + "\x11RestoreItemAction\x12h\n" + + "\tAppliesTo\x12,.generated.RestoreItemActionAppliesToRequest\x1a-.generated.RestoreItemActionAppliesToResponse\x12b\n" + + "\aExecute\x12*.generated.RestoreItemActionExecuteRequest\x1a+.generated.RestoreItemActionExecuteResponseB5Z3github.com/vmware-tanzu/velero/pkg/plugin/generatedb\x06proto3" var ( file_RestoreItemAction_proto_rawDescOnce sync.Once - file_RestoreItemAction_proto_rawDescData = file_RestoreItemAction_proto_rawDesc + file_RestoreItemAction_proto_rawDescData []byte ) func file_RestoreItemAction_proto_rawDescGZIP() []byte { file_RestoreItemAction_proto_rawDescOnce.Do(func() { - file_RestoreItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(file_RestoreItemAction_proto_rawDescData) + file_RestoreItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_RestoreItemAction_proto_rawDesc), len(file_RestoreItemAction_proto_rawDesc))) }) return file_RestoreItemAction_proto_rawDescData } var file_RestoreItemAction_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_RestoreItemAction_proto_goTypes = []interface{}{ +var file_RestoreItemAction_proto_goTypes = []any{ (*RestoreItemActionExecuteRequest)(nil), // 0: generated.RestoreItemActionExecuteRequest (*RestoreItemActionExecuteResponse)(nil), // 1: generated.RestoreItemActionExecuteResponse (*RestoreItemActionAppliesToRequest)(nil), // 2: generated.RestoreItemActionAppliesToRequest @@ -346,61 +300,11 @@ func file_RestoreItemAction_proto_init() { return } file_Shared_proto_init() - if !protoimpl.UnsafeEnabled { - file_RestoreItemAction_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionExecuteRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_RestoreItemAction_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionExecuteResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_RestoreItemAction_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionAppliesToRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_RestoreItemAction_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionAppliesToResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_RestoreItemAction_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_RestoreItemAction_proto_rawDesc), len(file_RestoreItemAction_proto_rawDesc)), NumEnums: 0, NumMessages: 4, NumExtensions: 0, @@ -411,7 +315,6 @@ func file_RestoreItemAction_proto_init() { MessageInfos: file_RestoreItemAction_proto_msgTypes, }.Build() File_RestoreItemAction_proto = out.File - file_RestoreItemAction_proto_rawDesc = nil file_RestoreItemAction_proto_goTypes = nil file_RestoreItemAction_proto_depIdxs = nil } diff --git a/pkg/plugin/generated/Shared.pb.go b/pkg/plugin/generated/Shared.pb.go index 07af30089..7c458579b 100644 --- a/pkg/plugin/generated/Shared.pb.go +++ b/pkg/plugin/generated/Shared.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: Shared.proto @@ -12,6 +12,7 @@ import ( timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -22,18 +23,16 @@ const ( ) type Empty struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Empty) Reset() { *x = Empty{} - if protoimpl.UnsafeEnabled { - mi := &file_Shared_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_Shared_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Empty) String() string { @@ -44,7 +43,7 @@ func (*Empty) ProtoMessage() {} func (x *Empty) ProtoReflect() protoreflect.Message { mi := &file_Shared_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -60,20 +59,17 @@ func (*Empty) Descriptor() ([]byte, []int) { } type Stack struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Frames []*StackFrame `protobuf:"bytes,1,rep,name=frames,proto3" json:"frames,omitempty"` unknownFields protoimpl.UnknownFields - - Frames []*StackFrame `protobuf:"bytes,1,rep,name=frames,proto3" json:"frames,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Stack) Reset() { *x = Stack{} - if protoimpl.UnsafeEnabled { - mi := &file_Shared_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_Shared_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Stack) String() string { @@ -84,7 +80,7 @@ func (*Stack) ProtoMessage() {} func (x *Stack) ProtoReflect() protoreflect.Message { mi := &file_Shared_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -107,22 +103,19 @@ func (x *Stack) GetFrames() []*StackFrame { } type StackFrame struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + File string `protobuf:"bytes,1,opt,name=file,proto3" json:"file,omitempty"` + Line int32 `protobuf:"varint,2,opt,name=line,proto3" json:"line,omitempty"` + Function string `protobuf:"bytes,3,opt,name=function,proto3" json:"function,omitempty"` unknownFields protoimpl.UnknownFields - - File string `protobuf:"bytes,1,opt,name=file,proto3" json:"file,omitempty"` - Line int32 `protobuf:"varint,2,opt,name=line,proto3" json:"line,omitempty"` - Function string `protobuf:"bytes,3,opt,name=function,proto3" json:"function,omitempty"` + sizeCache protoimpl.SizeCache } func (x *StackFrame) Reset() { *x = StackFrame{} - if protoimpl.UnsafeEnabled { - mi := &file_Shared_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_Shared_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *StackFrame) String() string { @@ -133,7 +126,7 @@ func (*StackFrame) ProtoMessage() {} func (x *StackFrame) ProtoReflect() protoreflect.Message { mi := &file_Shared_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -170,23 +163,20 @@ func (x *StackFrame) GetFunction() string { } type ResourceIdentifier struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Group string `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` + Resource string `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + Namespace string `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields - - Group string `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` - Resource string `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` - Namespace string `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"` - Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ResourceIdentifier) Reset() { *x = ResourceIdentifier{} - if protoimpl.UnsafeEnabled { - mi := &file_Shared_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_Shared_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ResourceIdentifier) String() string { @@ -197,7 +187,7 @@ func (*ResourceIdentifier) ProtoMessage() {} func (x *ResourceIdentifier) ProtoReflect() protoreflect.Message { mi := &file_Shared_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -241,24 +231,21 @@ func (x *ResourceIdentifier) GetName() string { } type ResourceSelector struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - IncludedNamespaces []string `protobuf:"bytes,1,rep,name=includedNamespaces,proto3" json:"includedNamespaces,omitempty"` - ExcludedNamespaces []string `protobuf:"bytes,2,rep,name=excludedNamespaces,proto3" json:"excludedNamespaces,omitempty"` - IncludedResources []string `protobuf:"bytes,3,rep,name=includedResources,proto3" json:"includedResources,omitempty"` - ExcludedResources []string `protobuf:"bytes,4,rep,name=excludedResources,proto3" json:"excludedResources,omitempty"` - Selector string `protobuf:"bytes,5,opt,name=selector,proto3" json:"selector,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + IncludedNamespaces []string `protobuf:"bytes,1,rep,name=includedNamespaces,proto3" json:"includedNamespaces,omitempty"` + ExcludedNamespaces []string `protobuf:"bytes,2,rep,name=excludedNamespaces,proto3" json:"excludedNamespaces,omitempty"` + IncludedResources []string `protobuf:"bytes,3,rep,name=includedResources,proto3" json:"includedResources,omitempty"` + ExcludedResources []string `protobuf:"bytes,4,rep,name=excludedResources,proto3" json:"excludedResources,omitempty"` + Selector string `protobuf:"bytes,5,opt,name=selector,proto3" json:"selector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ResourceSelector) Reset() { *x = ResourceSelector{} - if protoimpl.UnsafeEnabled { - mi := &file_Shared_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_Shared_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ResourceSelector) String() string { @@ -269,7 +256,7 @@ func (*ResourceSelector) ProtoMessage() {} func (x *ResourceSelector) ProtoReflect() protoreflect.Message { mi := &file_Shared_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -320,10 +307,7 @@ func (x *ResourceSelector) GetSelector() string { } type OperationProgress struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` Completed bool `protobuf:"varint,1,opt,name=completed,proto3" json:"completed,omitempty"` Err string `protobuf:"bytes,2,opt,name=err,proto3" json:"err,omitempty"` NCompleted int64 `protobuf:"varint,3,opt,name=nCompleted,proto3" json:"nCompleted,omitempty"` @@ -332,15 +316,15 @@ type OperationProgress struct { Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` Started *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=started,proto3" json:"started,omitempty"` Updated *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=updated,proto3" json:"updated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *OperationProgress) Reset() { *x = OperationProgress{} - if protoimpl.UnsafeEnabled { - mi := &file_Shared_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_Shared_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *OperationProgress) String() string { @@ -351,7 +335,7 @@ func (*OperationProgress) ProtoMessage() {} func (x *OperationProgress) ProtoReflect() protoreflect.Message { mi := &file_Shared_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -424,82 +408,54 @@ func (x *OperationProgress) GetUpdated() *timestamppb.Timestamp { var File_Shared_proto protoreflect.FileDescriptor -var file_Shared_proto_rawDesc = []byte{ - 0x0a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x22, 0x36, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x12, 0x2d, 0x0a, 0x06, - 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, - 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x46, 0x72, - 0x61, 0x6d, 0x65, 0x52, 0x06, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x22, 0x50, 0x0a, 0x0a, 0x53, - 0x74, 0x61, 0x63, 0x6b, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x69, 0x6c, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x6c, 0x69, 0x6e, - 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x78, 0x0a, - 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xea, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x2e, 0x0a, 0x12, - 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, - 0x65, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x12, - 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, - 0x65, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x11, - 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, - 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x65, 0x78, - 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x22, 0xb1, 0x02, 0x0a, 0x11, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x72, 0x72, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x72, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, - 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x54, - 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x54, 0x6f, 0x74, - 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, - 0x6e, 0x69, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x07, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, 0x61, - 0x6e, 0x7a, 0x75, 0x2f, 0x76, 0x65, 0x6c, 0x65, 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_Shared_proto_rawDesc = "" + + "\n" + + "\fShared.proto\x12\tgenerated\x1a\x1fgoogle/protobuf/timestamp.proto\"\a\n" + + "\x05Empty\"6\n" + + "\x05Stack\x12-\n" + + "\x06frames\x18\x01 \x03(\v2\x15.generated.StackFrameR\x06frames\"P\n" + + "\n" + + "StackFrame\x12\x12\n" + + "\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" + + "\x04line\x18\x02 \x01(\x05R\x04line\x12\x1a\n" + + "\bfunction\x18\x03 \x01(\tR\bfunction\"x\n" + + "\x12ResourceIdentifier\x12\x14\n" + + "\x05group\x18\x01 \x01(\tR\x05group\x12\x1a\n" + + "\bresource\x18\x02 \x01(\tR\bresource\x12\x1c\n" + + "\tnamespace\x18\x03 \x01(\tR\tnamespace\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\"\xea\x01\n" + + "\x10ResourceSelector\x12.\n" + + "\x12includedNamespaces\x18\x01 \x03(\tR\x12includedNamespaces\x12.\n" + + "\x12excludedNamespaces\x18\x02 \x03(\tR\x12excludedNamespaces\x12,\n" + + "\x11includedResources\x18\x03 \x03(\tR\x11includedResources\x12,\n" + + "\x11excludedResources\x18\x04 \x03(\tR\x11excludedResources\x12\x1a\n" + + "\bselector\x18\x05 \x01(\tR\bselector\"\xb1\x02\n" + + "\x11OperationProgress\x12\x1c\n" + + "\tcompleted\x18\x01 \x01(\bR\tcompleted\x12\x10\n" + + "\x03err\x18\x02 \x01(\tR\x03err\x12\x1e\n" + + "\n" + + "nCompleted\x18\x03 \x01(\x03R\n" + + "nCompleted\x12\x16\n" + + "\x06nTotal\x18\x04 \x01(\x03R\x06nTotal\x12&\n" + + "\x0eoperationUnits\x18\x05 \x01(\tR\x0eoperationUnits\x12 \n" + + "\vdescription\x18\x06 \x01(\tR\vdescription\x124\n" + + "\astarted\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\astarted\x124\n" + + "\aupdated\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\aupdatedB5Z3github.com/vmware-tanzu/velero/pkg/plugin/generatedb\x06proto3" var ( file_Shared_proto_rawDescOnce sync.Once - file_Shared_proto_rawDescData = file_Shared_proto_rawDesc + file_Shared_proto_rawDescData []byte ) func file_Shared_proto_rawDescGZIP() []byte { file_Shared_proto_rawDescOnce.Do(func() { - file_Shared_proto_rawDescData = protoimpl.X.CompressGZIP(file_Shared_proto_rawDescData) + file_Shared_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_Shared_proto_rawDesc), len(file_Shared_proto_rawDesc))) }) return file_Shared_proto_rawDescData } var file_Shared_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_Shared_proto_goTypes = []interface{}{ +var file_Shared_proto_goTypes = []any{ (*Empty)(nil), // 0: generated.Empty (*Stack)(nil), // 1: generated.Stack (*StackFrame)(nil), // 2: generated.StackFrame @@ -524,85 +480,11 @@ func file_Shared_proto_init() { if File_Shared_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_Shared_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Empty); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_Shared_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Stack); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_Shared_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StackFrame); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_Shared_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ResourceIdentifier); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_Shared_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ResourceSelector); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_Shared_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OperationProgress); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_Shared_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_Shared_proto_rawDesc), len(file_Shared_proto_rawDesc)), NumEnums: 0, NumMessages: 6, NumExtensions: 0, @@ -613,7 +495,6 @@ func file_Shared_proto_init() { MessageInfos: file_Shared_proto_msgTypes, }.Build() File_Shared_proto = out.File - file_Shared_proto_rawDesc = nil file_Shared_proto_goTypes = nil file_Shared_proto_depIdxs = nil } diff --git a/pkg/plugin/generated/VolumeSnapshotter.pb.go b/pkg/plugin/generated/VolumeSnapshotter.pb.go index 673ad9739..2b5f9a86e 100644 --- a/pkg/plugin/generated/VolumeSnapshotter.pb.go +++ b/pkg/plugin/generated/VolumeSnapshotter.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: VolumeSnapshotter.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,24 +22,21 @@ const ( ) type CreateVolumeRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + SnapshotID string `protobuf:"bytes,2,opt,name=snapshotID,proto3" json:"snapshotID,omitempty"` + VolumeType string `protobuf:"bytes,3,opt,name=volumeType,proto3" json:"volumeType,omitempty"` + VolumeAZ string `protobuf:"bytes,4,opt,name=volumeAZ,proto3" json:"volumeAZ,omitempty"` + Iops int64 `protobuf:"varint,5,opt,name=iops,proto3" json:"iops,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - SnapshotID string `protobuf:"bytes,2,opt,name=snapshotID,proto3" json:"snapshotID,omitempty"` - VolumeType string `protobuf:"bytes,3,opt,name=volumeType,proto3" json:"volumeType,omitempty"` - VolumeAZ string `protobuf:"bytes,4,opt,name=volumeAZ,proto3" json:"volumeAZ,omitempty"` - Iops int64 `protobuf:"varint,5,opt,name=iops,proto3" json:"iops,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreateVolumeRequest) Reset() { *x = CreateVolumeRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateVolumeRequest) String() string { @@ -49,7 +47,7 @@ func (*CreateVolumeRequest) ProtoMessage() {} func (x *CreateVolumeRequest) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -100,20 +98,17 @@ func (x *CreateVolumeRequest) GetIops() int64 { } type CreateVolumeResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + VolumeID string `protobuf:"bytes,1,opt,name=volumeID,proto3" json:"volumeID,omitempty"` unknownFields protoimpl.UnknownFields - - VolumeID string `protobuf:"bytes,1,opt,name=volumeID,proto3" json:"volumeID,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreateVolumeResponse) Reset() { *x = CreateVolumeResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateVolumeResponse) String() string { @@ -124,7 +119,7 @@ func (*CreateVolumeResponse) ProtoMessage() {} func (x *CreateVolumeResponse) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -147,22 +142,19 @@ func (x *CreateVolumeResponse) GetVolumeID() string { } type GetVolumeInfoRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + VolumeID string `protobuf:"bytes,2,opt,name=volumeID,proto3" json:"volumeID,omitempty"` + VolumeAZ string `protobuf:"bytes,3,opt,name=volumeAZ,proto3" json:"volumeAZ,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - VolumeID string `protobuf:"bytes,2,opt,name=volumeID,proto3" json:"volumeID,omitempty"` - VolumeAZ string `protobuf:"bytes,3,opt,name=volumeAZ,proto3" json:"volumeAZ,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetVolumeInfoRequest) Reset() { *x = GetVolumeInfoRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetVolumeInfoRequest) String() string { @@ -173,7 +165,7 @@ func (*GetVolumeInfoRequest) ProtoMessage() {} func (x *GetVolumeInfoRequest) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -210,21 +202,18 @@ func (x *GetVolumeInfoRequest) GetVolumeAZ() string { } type GetVolumeInfoResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + VolumeType string `protobuf:"bytes,1,opt,name=volumeType,proto3" json:"volumeType,omitempty"` + Iops int64 `protobuf:"varint,2,opt,name=iops,proto3" json:"iops,omitempty"` unknownFields protoimpl.UnknownFields - - VolumeType string `protobuf:"bytes,1,opt,name=volumeType,proto3" json:"volumeType,omitempty"` - Iops int64 `protobuf:"varint,2,opt,name=iops,proto3" json:"iops,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetVolumeInfoResponse) Reset() { *x = GetVolumeInfoResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetVolumeInfoResponse) String() string { @@ -235,7 +224,7 @@ func (*GetVolumeInfoResponse) ProtoMessage() {} func (x *GetVolumeInfoResponse) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -265,23 +254,20 @@ func (x *GetVolumeInfoResponse) GetIops() int64 { } type CreateSnapshotRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + VolumeID string `protobuf:"bytes,2,opt,name=volumeID,proto3" json:"volumeID,omitempty"` + VolumeAZ string `protobuf:"bytes,3,opt,name=volumeAZ,proto3" json:"volumeAZ,omitempty"` + Tags map[string]string `protobuf:"bytes,4,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - VolumeID string `protobuf:"bytes,2,opt,name=volumeID,proto3" json:"volumeID,omitempty"` - VolumeAZ string `protobuf:"bytes,3,opt,name=volumeAZ,proto3" json:"volumeAZ,omitempty"` - Tags map[string]string `protobuf:"bytes,4,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + sizeCache protoimpl.SizeCache } func (x *CreateSnapshotRequest) Reset() { *x = CreateSnapshotRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateSnapshotRequest) String() string { @@ -292,7 +278,7 @@ func (*CreateSnapshotRequest) ProtoMessage() {} func (x *CreateSnapshotRequest) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -336,20 +322,17 @@ func (x *CreateSnapshotRequest) GetTags() map[string]string { } type CreateSnapshotResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + SnapshotID string `protobuf:"bytes,1,opt,name=snapshotID,proto3" json:"snapshotID,omitempty"` unknownFields protoimpl.UnknownFields - - SnapshotID string `protobuf:"bytes,1,opt,name=snapshotID,proto3" json:"snapshotID,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreateSnapshotResponse) Reset() { *x = CreateSnapshotResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateSnapshotResponse) String() string { @@ -360,7 +343,7 @@ func (*CreateSnapshotResponse) ProtoMessage() {} func (x *CreateSnapshotResponse) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -383,21 +366,18 @@ func (x *CreateSnapshotResponse) GetSnapshotID() string { } type DeleteSnapshotRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + SnapshotID string `protobuf:"bytes,2,opt,name=snapshotID,proto3" json:"snapshotID,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - SnapshotID string `protobuf:"bytes,2,opt,name=snapshotID,proto3" json:"snapshotID,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteSnapshotRequest) Reset() { *x = DeleteSnapshotRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteSnapshotRequest) String() string { @@ -408,7 +388,7 @@ func (*DeleteSnapshotRequest) ProtoMessage() {} func (x *DeleteSnapshotRequest) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -438,21 +418,18 @@ func (x *DeleteSnapshotRequest) GetSnapshotID() string { } type GetVolumeIDRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - PersistentVolume []byte `protobuf:"bytes,2,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + PersistentVolume []byte `protobuf:"bytes,2,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetVolumeIDRequest) Reset() { *x = GetVolumeIDRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetVolumeIDRequest) String() string { @@ -463,7 +440,7 @@ func (*GetVolumeIDRequest) ProtoMessage() {} func (x *GetVolumeIDRequest) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -493,20 +470,17 @@ func (x *GetVolumeIDRequest) GetPersistentVolume() []byte { } type GetVolumeIDResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + VolumeID string `protobuf:"bytes,1,opt,name=volumeID,proto3" json:"volumeID,omitempty"` unknownFields protoimpl.UnknownFields - - VolumeID string `protobuf:"bytes,1,opt,name=volumeID,proto3" json:"volumeID,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetVolumeIDResponse) Reset() { *x = GetVolumeIDResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetVolumeIDResponse) String() string { @@ -517,7 +491,7 @@ func (*GetVolumeIDResponse) ProtoMessage() {} func (x *GetVolumeIDResponse) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -540,22 +514,19 @@ func (x *GetVolumeIDResponse) GetVolumeID() string { } type SetVolumeIDRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - PersistentVolume []byte `protobuf:"bytes,2,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"` - VolumeID string `protobuf:"bytes,3,opt,name=volumeID,proto3" json:"volumeID,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + PersistentVolume []byte `protobuf:"bytes,2,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"` + VolumeID string `protobuf:"bytes,3,opt,name=volumeID,proto3" json:"volumeID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SetVolumeIDRequest) Reset() { *x = SetVolumeIDRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SetVolumeIDRequest) String() string { @@ -566,7 +537,7 @@ func (*SetVolumeIDRequest) ProtoMessage() {} func (x *SetVolumeIDRequest) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -603,20 +574,17 @@ func (x *SetVolumeIDRequest) GetVolumeID() string { } type SetVolumeIDResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - PersistentVolume []byte `protobuf:"bytes,1,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + PersistentVolume []byte `protobuf:"bytes,1,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SetVolumeIDResponse) Reset() { *x = SetVolumeIDResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SetVolumeIDResponse) String() string { @@ -627,7 +595,7 @@ func (*SetVolumeIDResponse) ProtoMessage() {} func (x *SetVolumeIDResponse) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -650,21 +618,18 @@ func (x *SetVolumeIDResponse) GetPersistentVolume() []byte { } type VolumeSnapshotterInitRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Config map[string]string `protobuf:"bytes,2,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Config map[string]string `protobuf:"bytes,2,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + sizeCache protoimpl.SizeCache } func (x *VolumeSnapshotterInitRequest) Reset() { *x = VolumeSnapshotterInitRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_VolumeSnapshotter_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_VolumeSnapshotter_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VolumeSnapshotterInitRequest) String() string { @@ -675,7 +640,7 @@ func (*VolumeSnapshotterInitRequest) ProtoMessage() {} func (x *VolumeSnapshotterInitRequest) ProtoReflect() protoreflect.Message { mi := &file_VolumeSnapshotter_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -706,147 +671,87 @@ func (x *VolumeSnapshotterInitRequest) GetConfig() map[string]string { var File_VolumeSnapshotter_proto protoreflect.FileDescriptor -var file_VolumeSnapshotter_proto_rawDesc = []byte{ - 0x0a, 0x17, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, - 0x74, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x64, 0x1a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x22, 0x9d, 0x01, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x44, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, - 0x49, 0x44, 0x12, 0x1e, 0x0a, 0x0a, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x41, 0x5a, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x41, 0x5a, 0x12, 0x12, - 0x0a, 0x04, 0x69, 0x6f, 0x70, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x69, 0x6f, - 0x70, 0x73, 0x22, 0x32, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, - 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x22, 0x66, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, - 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, - 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, - 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x41, 0x5a, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x41, 0x5a, 0x22, 0x4b, - 0x0a, 0x15, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x76, 0x6f, 0x6c, 0x75, 0x6d, - 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x76, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x6f, 0x70, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x69, 0x6f, 0x70, 0x73, 0x22, 0xe0, 0x01, 0x0a, 0x15, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, - 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x41, 0x5a, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x41, 0x5a, 0x12, 0x3e, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x04, 0x74, 0x61, 0x67, 0x73, 0x1a, 0x37, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x38, - 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, - 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x44, 0x22, 0x4f, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x44, 0x22, 0x58, 0x0a, 0x12, 0x47, 0x65, 0x74, - 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x65, 0x72, 0x73, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x10, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x22, 0x31, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, - 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x22, 0x74, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, - 0x6e, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, - 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x22, 0x41, 0x0a, 0x13, - 0x53, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x70, - 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x22, - 0xbe, 0x01, 0x0a, 0x1c, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, - 0x6f, 0x74, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x4b, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x64, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, - 0x68, 0x6f, 0x74, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x39, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x32, 0xc0, 0x04, 0x0a, 0x11, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, - 0x68, 0x6f, 0x74, 0x74, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x27, - 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, - 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x69, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x5b, 0x0a, 0x18, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x53, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x1e, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1f, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x6e, 0x66, - 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x20, 0x2e, 0x67, - 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, - 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, - 0x68, 0x6f, 0x74, 0x12, 0x20, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x56, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x1d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, - 0x65, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, - 0x6d, 0x65, 0x49, 0x44, 0x12, 0x1d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, - 0x2e, 0x53, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, - 0x53, 0x65, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x76, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, 0x61, 0x6e, 0x7a, 0x75, 0x2f, 0x76, - 0x65, 0x6c, 0x65, 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, -} +const file_VolumeSnapshotter_proto_rawDesc = "" + + "\n" + + "\x17VolumeSnapshotter.proto\x12\tgenerated\x1a\fShared.proto\"\x9d\x01\n" + + "\x13CreateVolumeRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x1e\n" + + "\n" + + "snapshotID\x18\x02 \x01(\tR\n" + + "snapshotID\x12\x1e\n" + + "\n" + + "volumeType\x18\x03 \x01(\tR\n" + + "volumeType\x12\x1a\n" + + "\bvolumeAZ\x18\x04 \x01(\tR\bvolumeAZ\x12\x12\n" + + "\x04iops\x18\x05 \x01(\x03R\x04iops\"2\n" + + "\x14CreateVolumeResponse\x12\x1a\n" + + "\bvolumeID\x18\x01 \x01(\tR\bvolumeID\"f\n" + + "\x14GetVolumeInfoRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x1a\n" + + "\bvolumeID\x18\x02 \x01(\tR\bvolumeID\x12\x1a\n" + + "\bvolumeAZ\x18\x03 \x01(\tR\bvolumeAZ\"K\n" + + "\x15GetVolumeInfoResponse\x12\x1e\n" + + "\n" + + "volumeType\x18\x01 \x01(\tR\n" + + "volumeType\x12\x12\n" + + "\x04iops\x18\x02 \x01(\x03R\x04iops\"\xe0\x01\n" + + "\x15CreateSnapshotRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x1a\n" + + "\bvolumeID\x18\x02 \x01(\tR\bvolumeID\x12\x1a\n" + + "\bvolumeAZ\x18\x03 \x01(\tR\bvolumeAZ\x12>\n" + + "\x04tags\x18\x04 \x03(\v2*.generated.CreateSnapshotRequest.TagsEntryR\x04tags\x1a7\n" + + "\tTagsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"8\n" + + "\x16CreateSnapshotResponse\x12\x1e\n" + + "\n" + + "snapshotID\x18\x01 \x01(\tR\n" + + "snapshotID\"O\n" + + "\x15DeleteSnapshotRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x1e\n" + + "\n" + + "snapshotID\x18\x02 \x01(\tR\n" + + "snapshotID\"X\n" + + "\x12GetVolumeIDRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12*\n" + + "\x10persistentVolume\x18\x02 \x01(\fR\x10persistentVolume\"1\n" + + "\x13GetVolumeIDResponse\x12\x1a\n" + + "\bvolumeID\x18\x01 \x01(\tR\bvolumeID\"t\n" + + "\x12SetVolumeIDRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12*\n" + + "\x10persistentVolume\x18\x02 \x01(\fR\x10persistentVolume\x12\x1a\n" + + "\bvolumeID\x18\x03 \x01(\tR\bvolumeID\"A\n" + + "\x13SetVolumeIDResponse\x12*\n" + + "\x10persistentVolume\x18\x01 \x01(\fR\x10persistentVolume\"\xbe\x01\n" + + "\x1cVolumeSnapshotterInitRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12K\n" + + "\x06config\x18\x02 \x03(\v23.generated.VolumeSnapshotterInitRequest.ConfigEntryR\x06config\x1a9\n" + + "\vConfigEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xc0\x04\n" + + "\x11VolumeSnapshotter\x12A\n" + + "\x04Init\x12'.generated.VolumeSnapshotterInitRequest\x1a\x10.generated.Empty\x12[\n" + + "\x18CreateVolumeFromSnapshot\x12\x1e.generated.CreateVolumeRequest\x1a\x1f.generated.CreateVolumeResponse\x12R\n" + + "\rGetVolumeInfo\x12\x1f.generated.GetVolumeInfoRequest\x1a .generated.GetVolumeInfoResponse\x12U\n" + + "\x0eCreateSnapshot\x12 .generated.CreateSnapshotRequest\x1a!.generated.CreateSnapshotResponse\x12D\n" + + "\x0eDeleteSnapshot\x12 .generated.DeleteSnapshotRequest\x1a\x10.generated.Empty\x12L\n" + + "\vGetVolumeID\x12\x1d.generated.GetVolumeIDRequest\x1a\x1e.generated.GetVolumeIDResponse\x12L\n" + + "\vSetVolumeID\x12\x1d.generated.SetVolumeIDRequest\x1a\x1e.generated.SetVolumeIDResponseB5Z3github.com/vmware-tanzu/velero/pkg/plugin/generatedb\x06proto3" var ( file_VolumeSnapshotter_proto_rawDescOnce sync.Once - file_VolumeSnapshotter_proto_rawDescData = file_VolumeSnapshotter_proto_rawDesc + file_VolumeSnapshotter_proto_rawDescData []byte ) func file_VolumeSnapshotter_proto_rawDescGZIP() []byte { file_VolumeSnapshotter_proto_rawDescOnce.Do(func() { - file_VolumeSnapshotter_proto_rawDescData = protoimpl.X.CompressGZIP(file_VolumeSnapshotter_proto_rawDescData) + file_VolumeSnapshotter_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_VolumeSnapshotter_proto_rawDesc), len(file_VolumeSnapshotter_proto_rawDesc))) }) return file_VolumeSnapshotter_proto_rawDescData } var file_VolumeSnapshotter_proto_msgTypes = make([]protoimpl.MessageInfo, 14) -var file_VolumeSnapshotter_proto_goTypes = []interface{}{ +var file_VolumeSnapshotter_proto_goTypes = []any{ (*CreateVolumeRequest)(nil), // 0: generated.CreateVolumeRequest (*CreateVolumeResponse)(nil), // 1: generated.CreateVolumeResponse (*GetVolumeInfoRequest)(nil), // 2: generated.GetVolumeInfoRequest @@ -893,157 +798,11 @@ func file_VolumeSnapshotter_proto_init() { return } file_Shared_proto_init() - if !protoimpl.UnsafeEnabled { - file_VolumeSnapshotter_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateVolumeRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateVolumeResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetVolumeInfoRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetVolumeInfoResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateSnapshotRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateSnapshotResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteSnapshotRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetVolumeIDRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetVolumeIDResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetVolumeIDRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetVolumeIDResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_VolumeSnapshotter_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VolumeSnapshotterInitRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_VolumeSnapshotter_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_VolumeSnapshotter_proto_rawDesc), len(file_VolumeSnapshotter_proto_rawDesc)), NumEnums: 0, NumMessages: 14, NumExtensions: 0, @@ -1054,7 +813,6 @@ func file_VolumeSnapshotter_proto_init() { MessageInfos: file_VolumeSnapshotter_proto_msgTypes, }.Build() File_VolumeSnapshotter_proto = out.File - file_VolumeSnapshotter_proto_rawDesc = nil file_VolumeSnapshotter_proto_goTypes = nil file_VolumeSnapshotter_proto_depIdxs = nil } diff --git a/pkg/plugin/generated/backupitemaction/v2/BackupItemAction.pb.go b/pkg/plugin/generated/backupitemaction/v2/BackupItemAction.pb.go index 5eb2c852b..097dfc721 100644 --- a/pkg/plugin/generated/backupitemaction/v2/BackupItemAction.pb.go +++ b/pkg/plugin/generated/backupitemaction/v2/BackupItemAction.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: backupitemaction/v2/BackupItemAction.proto @@ -13,6 +13,7 @@ import ( emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -23,22 +24,19 @@ const ( ) type ExecuteRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` - Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ExecuteRequest) Reset() { *x = ExecuteRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ExecuteRequest) String() string { @@ -49,7 +47,7 @@ func (*ExecuteRequest) ProtoMessage() {} func (x *ExecuteRequest) ProtoReflect() protoreflect.Message { mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -86,23 +84,20 @@ func (x *ExecuteRequest) GetBackup() []byte { } type ExecuteResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` Item []byte `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` AdditionalItems []*generated.ResourceIdentifier `protobuf:"bytes,2,rep,name=additionalItems,proto3" json:"additionalItems,omitempty"` OperationID string `protobuf:"bytes,3,opt,name=operationID,proto3" json:"operationID,omitempty"` PostOperationItems []*generated.ResourceIdentifier `protobuf:"bytes,4,rep,name=postOperationItems,proto3" json:"postOperationItems,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExecuteResponse) Reset() { *x = ExecuteResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ExecuteResponse) String() string { @@ -113,7 +108,7 @@ func (*ExecuteResponse) ProtoMessage() {} func (x *ExecuteResponse) ProtoReflect() protoreflect.Message { mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -157,20 +152,17 @@ func (x *ExecuteResponse) GetPostOperationItems() []*generated.ResourceIdentifie } type BackupItemActionAppliesToRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + sizeCache protoimpl.SizeCache } func (x *BackupItemActionAppliesToRequest) Reset() { *x = BackupItemActionAppliesToRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *BackupItemActionAppliesToRequest) String() string { @@ -181,7 +173,7 @@ func (*BackupItemActionAppliesToRequest) ProtoMessage() {} func (x *BackupItemActionAppliesToRequest) ProtoReflect() protoreflect.Message { mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -204,20 +196,17 @@ func (x *BackupItemActionAppliesToRequest) GetPlugin() string { } type BackupItemActionAppliesToResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` ResourceSelector *generated.ResourceSelector `protobuf:"bytes,1,opt,name=ResourceSelector,proto3" json:"ResourceSelector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BackupItemActionAppliesToResponse) Reset() { *x = BackupItemActionAppliesToResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *BackupItemActionAppliesToResponse) String() string { @@ -228,7 +217,7 @@ func (*BackupItemActionAppliesToResponse) ProtoMessage() {} func (x *BackupItemActionAppliesToResponse) ProtoReflect() protoreflect.Message { mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -251,22 +240,19 @@ func (x *BackupItemActionAppliesToResponse) GetResourceSelector() *generated.Res } type BackupItemActionProgressRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + OperationID string `protobuf:"bytes,2,opt,name=operationID,proto3" json:"operationID,omitempty"` + Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - OperationID string `protobuf:"bytes,2,opt,name=operationID,proto3" json:"operationID,omitempty"` - Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` + sizeCache protoimpl.SizeCache } func (x *BackupItemActionProgressRequest) Reset() { *x = BackupItemActionProgressRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *BackupItemActionProgressRequest) String() string { @@ -277,7 +263,7 @@ func (*BackupItemActionProgressRequest) ProtoMessage() {} func (x *BackupItemActionProgressRequest) ProtoReflect() protoreflect.Message { mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -314,20 +300,17 @@ func (x *BackupItemActionProgressRequest) GetBackup() []byte { } type BackupItemActionProgressResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Progress *generated.OperationProgress `protobuf:"bytes,1,opt,name=progress,proto3" json:"progress,omitempty"` unknownFields protoimpl.UnknownFields - - Progress *generated.OperationProgress `protobuf:"bytes,1,opt,name=progress,proto3" json:"progress,omitempty"` + sizeCache protoimpl.SizeCache } func (x *BackupItemActionProgressResponse) Reset() { *x = BackupItemActionProgressResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *BackupItemActionProgressResponse) String() string { @@ -338,7 +321,7 @@ func (*BackupItemActionProgressResponse) ProtoMessage() {} func (x *BackupItemActionProgressResponse) ProtoReflect() protoreflect.Message { mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -361,22 +344,19 @@ func (x *BackupItemActionProgressResponse) GetProgress() *generated.OperationPro } type BackupItemActionCancelRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + OperationID string `protobuf:"bytes,2,opt,name=operationID,proto3" json:"operationID,omitempty"` + Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - OperationID string `protobuf:"bytes,2,opt,name=operationID,proto3" json:"operationID,omitempty"` - Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` + sizeCache protoimpl.SizeCache } func (x *BackupItemActionCancelRequest) Reset() { *x = BackupItemActionCancelRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *BackupItemActionCancelRequest) String() string { @@ -387,7 +367,7 @@ func (*BackupItemActionCancelRequest) ProtoMessage() {} func (x *BackupItemActionCancelRequest) ProtoReflect() protoreflect.Message { mi := &file_backupitemaction_v2_BackupItemAction_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -425,105 +405,52 @@ func (x *BackupItemActionCancelRequest) GetBackup() []byte { var File_backupitemaction_v2_BackupItemAction_proto protoreflect.FileDescriptor -var file_backupitemaction_v2_BackupItemAction_proto_rawDesc = []byte{ - 0x0a, 0x2a, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x69, 0x74, 0x65, 0x6d, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x2f, 0x76, 0x32, 0x2f, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x32, - 0x1a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, - 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x54, 0x0a, 0x0e, 0x45, - 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, - 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x62, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x22, 0xdf, 0x01, 0x0a, 0x0f, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x47, 0x0a, 0x0f, 0x61, 0x64, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x52, 0x0f, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, - 0x6d, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x4d, 0x0a, 0x12, 0x70, 0x6f, 0x73, 0x74, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, - 0x12, 0x70, 0x6f, 0x73, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x74, - 0x65, 0x6d, 0x73, 0x22, 0x3a, 0x0a, 0x20, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, - 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, - 0x6c, 0x0a, 0x21, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x10, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x73, 0x0a, - 0x1f, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x61, - 0x63, 0x6b, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x62, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x22, 0x5c, 0x0a, 0x20, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, - 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, - 0x61, 0x74, 0x65, 0x64, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, - 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, - 0x22, 0x71, 0x0a, 0x1d, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x62, - 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x62, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x32, 0xbc, 0x02, 0x0a, 0x10, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, - 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x58, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x6c, - 0x69, 0x65, 0x73, 0x54, 0x6f, 0x12, 0x24, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, - 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x76, 0x32, - 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x32, 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x12, 0x2e, - 0x76, 0x32, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x13, 0x2e, 0x76, 0x32, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x23, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x49, 0x74, - 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, - 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, - 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x21, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, - 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x42, 0x49, 0x5a, 0x47, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x76, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, 0x61, 0x6e, 0x7a, 0x75, 0x2f, 0x76, 0x65, - 0x6c, 0x65, 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, - 0x69, 0x74, 0x65, 0x6d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x32, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_backupitemaction_v2_BackupItemAction_proto_rawDesc = "" + + "\n" + + "*backupitemaction/v2/BackupItemAction.proto\x12\x02v2\x1a\fShared.proto\x1a\x1bgoogle/protobuf/empty.proto\"T\n" + + "\x0eExecuteRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x12\n" + + "\x04item\x18\x02 \x01(\fR\x04item\x12\x16\n" + + "\x06backup\x18\x03 \x01(\fR\x06backup\"\xdf\x01\n" + + "\x0fExecuteResponse\x12\x12\n" + + "\x04item\x18\x01 \x01(\fR\x04item\x12G\n" + + "\x0fadditionalItems\x18\x02 \x03(\v2\x1d.generated.ResourceIdentifierR\x0fadditionalItems\x12 \n" + + "\voperationID\x18\x03 \x01(\tR\voperationID\x12M\n" + + "\x12postOperationItems\x18\x04 \x03(\v2\x1d.generated.ResourceIdentifierR\x12postOperationItems\":\n" + + " BackupItemActionAppliesToRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\"l\n" + + "!BackupItemActionAppliesToResponse\x12G\n" + + "\x10ResourceSelector\x18\x01 \x01(\v2\x1b.generated.ResourceSelectorR\x10ResourceSelector\"s\n" + + "\x1fBackupItemActionProgressRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12 \n" + + "\voperationID\x18\x02 \x01(\tR\voperationID\x12\x16\n" + + "\x06backup\x18\x03 \x01(\fR\x06backup\"\\\n" + + " BackupItemActionProgressResponse\x128\n" + + "\bprogress\x18\x01 \x01(\v2\x1c.generated.OperationProgressR\bprogress\"q\n" + + "\x1dBackupItemActionCancelRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12 \n" + + "\voperationID\x18\x02 \x01(\tR\voperationID\x12\x16\n" + + "\x06backup\x18\x03 \x01(\fR\x06backup2\xbc\x02\n" + + "\x10BackupItemAction\x12X\n" + + "\tAppliesTo\x12$.v2.BackupItemActionAppliesToRequest\x1a%.v2.BackupItemActionAppliesToResponse\x122\n" + + "\aExecute\x12\x12.v2.ExecuteRequest\x1a\x13.v2.ExecuteResponse\x12U\n" + + "\bProgress\x12#.v2.BackupItemActionProgressRequest\x1a$.v2.BackupItemActionProgressResponse\x12C\n" + + "\x06Cancel\x12!.v2.BackupItemActionCancelRequest\x1a\x16.google.protobuf.EmptyBIZGgithub.com/vmware-tanzu/velero/pkg/plugin/generated/backupitemaction/v2b\x06proto3" var ( file_backupitemaction_v2_BackupItemAction_proto_rawDescOnce sync.Once - file_backupitemaction_v2_BackupItemAction_proto_rawDescData = file_backupitemaction_v2_BackupItemAction_proto_rawDesc + file_backupitemaction_v2_BackupItemAction_proto_rawDescData []byte ) func file_backupitemaction_v2_BackupItemAction_proto_rawDescGZIP() []byte { file_backupitemaction_v2_BackupItemAction_proto_rawDescOnce.Do(func() { - file_backupitemaction_v2_BackupItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(file_backupitemaction_v2_BackupItemAction_proto_rawDescData) + file_backupitemaction_v2_BackupItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_backupitemaction_v2_BackupItemAction_proto_rawDesc), len(file_backupitemaction_v2_BackupItemAction_proto_rawDesc))) }) return file_backupitemaction_v2_BackupItemAction_proto_rawDescData } var file_backupitemaction_v2_BackupItemAction_proto_msgTypes = make([]protoimpl.MessageInfo, 7) -var file_backupitemaction_v2_BackupItemAction_proto_goTypes = []interface{}{ +var file_backupitemaction_v2_BackupItemAction_proto_goTypes = []any{ (*ExecuteRequest)(nil), // 0: v2.ExecuteRequest (*ExecuteResponse)(nil), // 1: v2.ExecuteResponse (*BackupItemActionAppliesToRequest)(nil), // 2: v2.BackupItemActionAppliesToRequest @@ -561,97 +488,11 @@ func file_backupitemaction_v2_BackupItemAction_proto_init() { if File_backupitemaction_v2_BackupItemAction_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_backupitemaction_v2_BackupItemAction_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExecuteRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_backupitemaction_v2_BackupItemAction_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExecuteResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_backupitemaction_v2_BackupItemAction_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupItemActionAppliesToRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_backupitemaction_v2_BackupItemAction_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupItemActionAppliesToResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_backupitemaction_v2_BackupItemAction_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupItemActionProgressRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_backupitemaction_v2_BackupItemAction_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupItemActionProgressResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_backupitemaction_v2_BackupItemAction_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BackupItemActionCancelRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_backupitemaction_v2_BackupItemAction_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_backupitemaction_v2_BackupItemAction_proto_rawDesc), len(file_backupitemaction_v2_BackupItemAction_proto_rawDesc)), NumEnums: 0, NumMessages: 7, NumExtensions: 0, @@ -662,7 +503,6 @@ func file_backupitemaction_v2_BackupItemAction_proto_init() { MessageInfos: file_backupitemaction_v2_BackupItemAction_proto_msgTypes, }.Build() File_backupitemaction_v2_BackupItemAction_proto = out.File - file_backupitemaction_v2_BackupItemAction_proto_rawDesc = nil file_backupitemaction_v2_BackupItemAction_proto_goTypes = nil file_backupitemaction_v2_BackupItemAction_proto_depIdxs = nil } diff --git a/pkg/plugin/generated/itemblockaction/v1/ItemBlockAction.pb.go b/pkg/plugin/generated/itemblockaction/v1/ItemBlockAction.pb.go index cec604477..6d73eb826 100644 --- a/pkg/plugin/generated/itemblockaction/v1/ItemBlockAction.pb.go +++ b/pkg/plugin/generated/itemblockaction/v1/ItemBlockAction.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: itemblockaction/v1/ItemBlockAction.proto @@ -12,6 +12,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -22,20 +23,17 @@ const ( ) type ItemBlockActionAppliesToRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ItemBlockActionAppliesToRequest) Reset() { *x = ItemBlockActionAppliesToRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ItemBlockActionAppliesToRequest) String() string { @@ -46,7 +44,7 @@ func (*ItemBlockActionAppliesToRequest) ProtoMessage() {} func (x *ItemBlockActionAppliesToRequest) ProtoReflect() protoreflect.Message { mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -69,20 +67,17 @@ func (x *ItemBlockActionAppliesToRequest) GetPlugin() string { } type ItemBlockActionAppliesToResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` ResourceSelector *generated.ResourceSelector `protobuf:"bytes,1,opt,name=ResourceSelector,proto3" json:"ResourceSelector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ItemBlockActionAppliesToResponse) Reset() { *x = ItemBlockActionAppliesToResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ItemBlockActionAppliesToResponse) String() string { @@ -93,7 +88,7 @@ func (*ItemBlockActionAppliesToResponse) ProtoMessage() {} func (x *ItemBlockActionAppliesToResponse) ProtoReflect() protoreflect.Message { mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -116,22 +111,19 @@ func (x *ItemBlockActionAppliesToResponse) GetResourceSelector() *generated.Reso } type ItemBlockActionGetRelatedItemsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` - Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ItemBlockActionGetRelatedItemsRequest) Reset() { *x = ItemBlockActionGetRelatedItemsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ItemBlockActionGetRelatedItemsRequest) String() string { @@ -142,7 +134,7 @@ func (*ItemBlockActionGetRelatedItemsRequest) ProtoMessage() {} func (x *ItemBlockActionGetRelatedItemsRequest) ProtoReflect() protoreflect.Message { mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -179,20 +171,17 @@ func (x *ItemBlockActionGetRelatedItemsRequest) GetBackup() []byte { } type ItemBlockActionGetRelatedItemsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + RelatedItems []*generated.ResourceIdentifier `protobuf:"bytes,1,rep,name=relatedItems,proto3" json:"relatedItems,omitempty"` unknownFields protoimpl.UnknownFields - - RelatedItems []*generated.ResourceIdentifier `protobuf:"bytes,1,rep,name=relatedItems,proto3" json:"relatedItems,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ItemBlockActionGetRelatedItemsResponse) Reset() { *x = ItemBlockActionGetRelatedItemsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ItemBlockActionGetRelatedItemsResponse) String() string { @@ -203,7 +192,7 @@ func (*ItemBlockActionGetRelatedItemsResponse) ProtoMessage() {} func (x *ItemBlockActionGetRelatedItemsResponse) ProtoReflect() protoreflect.Message { mi := &file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -227,70 +216,37 @@ func (x *ItemBlockActionGetRelatedItemsResponse) GetRelatedItems() []*generated. var File_itemblockaction_v1_ItemBlockAction_proto protoreflect.FileDescriptor -var file_itemblockaction_v1_ItemBlockAction_proto_rawDesc = []byte{ - 0x0a, 0x28, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x2f, 0x76, 0x31, 0x2f, 0x49, 0x74, 0x65, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x31, 0x1a, 0x0c, - 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x39, 0x0a, 0x1f, - 0x49, 0x74, 0x65, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, - 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x6b, 0x0a, 0x20, 0x49, 0x74, 0x65, 0x6d, 0x42, - 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, - 0x73, 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x52, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x22, 0x6b, 0x0a, 0x25, 0x49, 0x74, 0x65, 0x6d, 0x42, 0x6c, 0x6f, 0x63, - 0x6b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x65, - 0x64, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, - 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x61, 0x63, - 0x6b, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x62, 0x61, 0x63, 0x6b, 0x75, - 0x70, 0x22, 0x6b, 0x0a, 0x26, 0x49, 0x74, 0x65, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x49, 0x74, - 0x65, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x72, - 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x32, 0xd3, - 0x01, 0x0a, 0x0f, 0x49, 0x74, 0x65, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x56, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x12, - 0x23, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x42, 0x6c, - 0x6f, 0x63, 0x6b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, - 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x0f, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x29, 0x2e, - 0x76, 0x31, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x49, 0x74, 0x65, 0x6d, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x74, - 0x65, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x65, 0x74, - 0x52, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x48, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x76, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, 0x61, 0x6e, 0x7a, 0x75, 0x2f, - 0x76, 0x65, 0x6c, 0x65, 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x69, 0x74, 0x65, 0x6d, - 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_itemblockaction_v1_ItemBlockAction_proto_rawDesc = "" + + "\n" + + "(itemblockaction/v1/ItemBlockAction.proto\x12\x02v1\x1a\fShared.proto\"9\n" + + "\x1fItemBlockActionAppliesToRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\"k\n" + + " ItemBlockActionAppliesToResponse\x12G\n" + + "\x10ResourceSelector\x18\x01 \x01(\v2\x1b.generated.ResourceSelectorR\x10ResourceSelector\"k\n" + + "%ItemBlockActionGetRelatedItemsRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x12\n" + + "\x04item\x18\x02 \x01(\fR\x04item\x12\x16\n" + + "\x06backup\x18\x03 \x01(\fR\x06backup\"k\n" + + "&ItemBlockActionGetRelatedItemsResponse\x12A\n" + + "\frelatedItems\x18\x01 \x03(\v2\x1d.generated.ResourceIdentifierR\frelatedItems2\xd3\x01\n" + + "\x0fItemBlockAction\x12V\n" + + "\tAppliesTo\x12#.v1.ItemBlockActionAppliesToRequest\x1a$.v1.ItemBlockActionAppliesToResponse\x12h\n" + + "\x0fGetRelatedItems\x12).v1.ItemBlockActionGetRelatedItemsRequest\x1a*.v1.ItemBlockActionGetRelatedItemsResponseBHZFgithub.com/vmware-tanzu/velero/pkg/plugin/generated/itemblockaction/v1b\x06proto3" var ( file_itemblockaction_v1_ItemBlockAction_proto_rawDescOnce sync.Once - file_itemblockaction_v1_ItemBlockAction_proto_rawDescData = file_itemblockaction_v1_ItemBlockAction_proto_rawDesc + file_itemblockaction_v1_ItemBlockAction_proto_rawDescData []byte ) func file_itemblockaction_v1_ItemBlockAction_proto_rawDescGZIP() []byte { file_itemblockaction_v1_ItemBlockAction_proto_rawDescOnce.Do(func() { - file_itemblockaction_v1_ItemBlockAction_proto_rawDescData = protoimpl.X.CompressGZIP(file_itemblockaction_v1_ItemBlockAction_proto_rawDescData) + file_itemblockaction_v1_ItemBlockAction_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_itemblockaction_v1_ItemBlockAction_proto_rawDesc), len(file_itemblockaction_v1_ItemBlockAction_proto_rawDesc))) }) return file_itemblockaction_v1_ItemBlockAction_proto_rawDescData } var file_itemblockaction_v1_ItemBlockAction_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_itemblockaction_v1_ItemBlockAction_proto_goTypes = []interface{}{ +var file_itemblockaction_v1_ItemBlockAction_proto_goTypes = []any{ (*ItemBlockActionAppliesToRequest)(nil), // 0: v1.ItemBlockActionAppliesToRequest (*ItemBlockActionAppliesToResponse)(nil), // 1: v1.ItemBlockActionAppliesToResponse (*ItemBlockActionGetRelatedItemsRequest)(nil), // 2: v1.ItemBlockActionGetRelatedItemsRequest @@ -317,61 +273,11 @@ func file_itemblockaction_v1_ItemBlockAction_proto_init() { if File_itemblockaction_v1_ItemBlockAction_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ItemBlockActionAppliesToRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ItemBlockActionAppliesToResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ItemBlockActionGetRelatedItemsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_itemblockaction_v1_ItemBlockAction_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ItemBlockActionGetRelatedItemsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_itemblockaction_v1_ItemBlockAction_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_itemblockaction_v1_ItemBlockAction_proto_rawDesc), len(file_itemblockaction_v1_ItemBlockAction_proto_rawDesc)), NumEnums: 0, NumMessages: 4, NumExtensions: 0, @@ -382,7 +288,6 @@ func file_itemblockaction_v1_ItemBlockAction_proto_init() { MessageInfos: file_itemblockaction_v1_ItemBlockAction_proto_msgTypes, }.Build() File_itemblockaction_v1_ItemBlockAction_proto = out.File - file_itemblockaction_v1_ItemBlockAction_proto_rawDesc = nil file_itemblockaction_v1_ItemBlockAction_proto_goTypes = nil file_itemblockaction_v1_ItemBlockAction_proto_depIdxs = nil } diff --git a/pkg/plugin/generated/restoreitemaction/v2/RestoreItemAction.pb.go b/pkg/plugin/generated/restoreitemaction/v2/RestoreItemAction.pb.go index a7bbc421e..48444045d 100644 --- a/pkg/plugin/generated/restoreitemaction/v2/RestoreItemAction.pb.go +++ b/pkg/plugin/generated/restoreitemaction/v2/RestoreItemAction.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.33.0 +// protoc-gen-go v1.36.11-devel // protoc v4.25.2 // source: restoreitemaction/v2/RestoreItemAction.proto @@ -14,6 +14,7 @@ import ( emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -24,23 +25,20 @@ const ( ) type RestoreItemActionExecuteRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` - Restore []byte `protobuf:"bytes,3,opt,name=restore,proto3" json:"restore,omitempty"` - ItemFromBackup []byte `protobuf:"bytes,4,opt,name=itemFromBackup,proto3" json:"itemFromBackup,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + Restore []byte `protobuf:"bytes,3,opt,name=restore,proto3" json:"restore,omitempty"` + ItemFromBackup []byte `protobuf:"bytes,4,opt,name=itemFromBackup,proto3" json:"itemFromBackup,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionExecuteRequest) Reset() { *x = RestoreItemActionExecuteRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionExecuteRequest) String() string { @@ -51,7 +49,7 @@ func (*RestoreItemActionExecuteRequest) ProtoMessage() {} func (x *RestoreItemActionExecuteRequest) ProtoReflect() protoreflect.Message { mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -95,25 +93,22 @@ func (x *RestoreItemActionExecuteRequest) GetItemFromBackup() []byte { } type RestoreItemActionExecuteResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` Item []byte `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` AdditionalItems []*generated.ResourceIdentifier `protobuf:"bytes,2,rep,name=additionalItems,proto3" json:"additionalItems,omitempty"` SkipRestore bool `protobuf:"varint,3,opt,name=skipRestore,proto3" json:"skipRestore,omitempty"` OperationID string `protobuf:"bytes,4,opt,name=operationID,proto3" json:"operationID,omitempty"` WaitForAdditionalItems bool `protobuf:"varint,5,opt,name=waitForAdditionalItems,proto3" json:"waitForAdditionalItems,omitempty"` AdditionalItemsReadyTimeout *durationpb.Duration `protobuf:"bytes,6,opt,name=additionalItemsReadyTimeout,proto3" json:"additionalItemsReadyTimeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionExecuteResponse) Reset() { *x = RestoreItemActionExecuteResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionExecuteResponse) String() string { @@ -124,7 +119,7 @@ func (*RestoreItemActionExecuteResponse) ProtoMessage() {} func (x *RestoreItemActionExecuteResponse) ProtoReflect() protoreflect.Message { mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -182,20 +177,17 @@ func (x *RestoreItemActionExecuteResponse) GetAdditionalItemsReadyTimeout() *dur } type RestoreItemActionAppliesToRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionAppliesToRequest) Reset() { *x = RestoreItemActionAppliesToRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionAppliesToRequest) String() string { @@ -206,7 +198,7 @@ func (*RestoreItemActionAppliesToRequest) ProtoMessage() {} func (x *RestoreItemActionAppliesToRequest) ProtoReflect() protoreflect.Message { mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -229,20 +221,17 @@ func (x *RestoreItemActionAppliesToRequest) GetPlugin() string { } type RestoreItemActionAppliesToResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` ResourceSelector *generated.ResourceSelector `protobuf:"bytes,1,opt,name=ResourceSelector,proto3" json:"ResourceSelector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionAppliesToResponse) Reset() { *x = RestoreItemActionAppliesToResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionAppliesToResponse) String() string { @@ -253,7 +242,7 @@ func (*RestoreItemActionAppliesToResponse) ProtoMessage() {} func (x *RestoreItemActionAppliesToResponse) ProtoReflect() protoreflect.Message { mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -276,22 +265,19 @@ func (x *RestoreItemActionAppliesToResponse) GetResourceSelector() *generated.Re } type RestoreItemActionProgressRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + OperationID string `protobuf:"bytes,2,opt,name=operationID,proto3" json:"operationID,omitempty"` + Restore []byte `protobuf:"bytes,3,opt,name=restore,proto3" json:"restore,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - OperationID string `protobuf:"bytes,2,opt,name=operationID,proto3" json:"operationID,omitempty"` - Restore []byte `protobuf:"bytes,3,opt,name=restore,proto3" json:"restore,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionProgressRequest) Reset() { *x = RestoreItemActionProgressRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionProgressRequest) String() string { @@ -302,7 +288,7 @@ func (*RestoreItemActionProgressRequest) ProtoMessage() {} func (x *RestoreItemActionProgressRequest) ProtoReflect() protoreflect.Message { mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -339,20 +325,17 @@ func (x *RestoreItemActionProgressRequest) GetRestore() []byte { } type RestoreItemActionProgressResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Progress *generated.OperationProgress `protobuf:"bytes,1,opt,name=progress,proto3" json:"progress,omitempty"` unknownFields protoimpl.UnknownFields - - Progress *generated.OperationProgress `protobuf:"bytes,1,opt,name=progress,proto3" json:"progress,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionProgressResponse) Reset() { *x = RestoreItemActionProgressResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionProgressResponse) String() string { @@ -363,7 +346,7 @@ func (*RestoreItemActionProgressResponse) ProtoMessage() {} func (x *RestoreItemActionProgressResponse) ProtoReflect() protoreflect.Message { mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -386,22 +369,19 @@ func (x *RestoreItemActionProgressResponse) GetProgress() *generated.OperationPr } type RestoreItemActionCancelRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + OperationID string `protobuf:"bytes,2,opt,name=operationID,proto3" json:"operationID,omitempty"` + Restore []byte `protobuf:"bytes,3,opt,name=restore,proto3" json:"restore,omitempty"` unknownFields protoimpl.UnknownFields - - Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` - OperationID string `protobuf:"bytes,2,opt,name=operationID,proto3" json:"operationID,omitempty"` - Restore []byte `protobuf:"bytes,3,opt,name=restore,proto3" json:"restore,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionCancelRequest) Reset() { *x = RestoreItemActionCancelRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionCancelRequest) String() string { @@ -412,7 +392,7 @@ func (*RestoreItemActionCancelRequest) ProtoMessage() {} func (x *RestoreItemActionCancelRequest) ProtoReflect() protoreflect.Message { mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -449,22 +429,19 @@ func (x *RestoreItemActionCancelRequest) GetRestore() []byte { } type RestoreItemActionItemsReadyRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` Restore []byte `protobuf:"bytes,2,opt,name=restore,proto3" json:"restore,omitempty"` AdditionalItems []*generated.ResourceIdentifier `protobuf:"bytes,3,rep,name=additionalItems,proto3" json:"additionalItems,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionItemsReadyRequest) Reset() { *x = RestoreItemActionItemsReadyRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionItemsReadyRequest) String() string { @@ -475,7 +452,7 @@ func (*RestoreItemActionItemsReadyRequest) ProtoMessage() {} func (x *RestoreItemActionItemsReadyRequest) ProtoReflect() protoreflect.Message { mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -512,20 +489,17 @@ func (x *RestoreItemActionItemsReadyRequest) GetAdditionalItems() []*generated.R } type RestoreItemActionItemsReadyResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ready bool `protobuf:"varint,1,opt,name=ready,proto3" json:"ready,omitempty"` unknownFields protoimpl.UnknownFields - - Ready bool `protobuf:"varint,1,opt,name=ready,proto3" json:"ready,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RestoreItemActionItemsReadyResponse) Reset() { *x = RestoreItemActionItemsReadyResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RestoreItemActionItemsReadyResponse) String() string { @@ -536,7 +510,7 @@ func (*RestoreItemActionItemsReadyResponse) ProtoMessage() {} func (x *RestoreItemActionItemsReadyResponse) ProtoReflect() protoreflect.Message { mi := &file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -560,142 +534,62 @@ func (x *RestoreItemActionItemsReadyResponse) GetReady() bool { var File_restoreitemaction_v2_RestoreItemAction_proto protoreflect.FileDescriptor -var file_restoreitemaction_v2_RestoreItemAction_proto_rawDesc = []byte{ - 0x0a, 0x2c, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x69, 0x74, 0x65, 0x6d, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x32, 0x2f, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, - 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, - 0x76, 0x32, 0x1a, 0x0c, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8f, 0x01, - 0x0a, 0x1f, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x18, 0x0a, - 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, - 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x74, 0x65, 0x6d, 0x46, - 0x72, 0x6f, 0x6d, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x0e, 0x69, 0x74, 0x65, 0x6d, 0x46, 0x72, 0x6f, 0x6d, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x22, - 0xd8, 0x02, 0x0a, 0x20, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x47, 0x0a, 0x0f, 0x61, 0x64, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x52, 0x0f, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, - 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x6b, 0x69, 0x70, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x6b, 0x69, 0x70, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x36, 0x0a, 0x16, 0x77, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, - 0x41, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x77, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x41, 0x64, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x5b, 0x0a, - 0x1b, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, - 0x52, 0x65, 0x61, 0x64, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x1b, 0x61, - 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x52, 0x65, - 0x61, 0x64, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x22, 0x3b, 0x0a, 0x21, 0x52, 0x65, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, - 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x6d, 0x0a, 0x22, 0x52, 0x65, 0x73, 0x74, 0x6f, - 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, - 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, - 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, - 0x74, 0x65, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x52, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x76, 0x0a, 0x20, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x22, 0x5d, - 0x0a, 0x21, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x22, 0x74, 0x0a, - 0x1e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x72, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x22, 0x9f, 0x01, 0x0a, 0x22, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, - 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x52, 0x65, - 0x61, 0x64, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x07, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x47, 0x0a, 0x0f, - 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, - 0x64, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x52, 0x0f, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x49, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x3b, 0x0a, 0x23, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x52, - 0x65, 0x61, 0x64, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x72, 0x65, 0x61, 0x64, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, 0x61, - 0x64, 0x79, 0x32, 0xd0, 0x03, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, - 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x6c, - 0x69, 0x65, 0x73, 0x54, 0x6f, 0x12, 0x25, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, - 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, - 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x76, - 0x32, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, - 0x23, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x08, 0x50, 0x72, - 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x24, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, - 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x76, - 0x32, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x22, 0x2e, - 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x6a, 0x0a, 0x17, 0x41, 0x72, 0x65, - 0x41, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x52, - 0x65, 0x61, 0x64, 0x79, 0x12, 0x26, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x73, - 0x52, 0x65, 0x61, 0x64, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x76, - 0x32, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x52, 0x65, 0x61, 0x64, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x4a, 0x5a, 0x48, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2d, 0x74, 0x61, 0x6e, 0x7a, 0x75, - 0x2f, 0x76, 0x65, 0x6c, 0x65, 0x72, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x69, 0x74, 0x65, 0x6d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, - 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_restoreitemaction_v2_RestoreItemAction_proto_rawDesc = "" + + "\n" + + ",restoreitemaction/v2/RestoreItemAction.proto\x12\x02v2\x1a\fShared.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1egoogle/protobuf/duration.proto\"\x8f\x01\n" + + "\x1fRestoreItemActionExecuteRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x12\n" + + "\x04item\x18\x02 \x01(\fR\x04item\x12\x18\n" + + "\arestore\x18\x03 \x01(\fR\arestore\x12&\n" + + "\x0eitemFromBackup\x18\x04 \x01(\fR\x0eitemFromBackup\"\xd8\x02\n" + + " RestoreItemActionExecuteResponse\x12\x12\n" + + "\x04item\x18\x01 \x01(\fR\x04item\x12G\n" + + "\x0fadditionalItems\x18\x02 \x03(\v2\x1d.generated.ResourceIdentifierR\x0fadditionalItems\x12 \n" + + "\vskipRestore\x18\x03 \x01(\bR\vskipRestore\x12 \n" + + "\voperationID\x18\x04 \x01(\tR\voperationID\x126\n" + + "\x16waitForAdditionalItems\x18\x05 \x01(\bR\x16waitForAdditionalItems\x12[\n" + + "\x1badditionalItemsReadyTimeout\x18\x06 \x01(\v2\x19.google.protobuf.DurationR\x1badditionalItemsReadyTimeout\";\n" + + "!RestoreItemActionAppliesToRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\"m\n" + + "\"RestoreItemActionAppliesToResponse\x12G\n" + + "\x10ResourceSelector\x18\x01 \x01(\v2\x1b.generated.ResourceSelectorR\x10ResourceSelector\"v\n" + + " RestoreItemActionProgressRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12 \n" + + "\voperationID\x18\x02 \x01(\tR\voperationID\x12\x18\n" + + "\arestore\x18\x03 \x01(\fR\arestore\"]\n" + + "!RestoreItemActionProgressResponse\x128\n" + + "\bprogress\x18\x01 \x01(\v2\x1c.generated.OperationProgressR\bprogress\"t\n" + + "\x1eRestoreItemActionCancelRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12 \n" + + "\voperationID\x18\x02 \x01(\tR\voperationID\x12\x18\n" + + "\arestore\x18\x03 \x01(\fR\arestore\"\x9f\x01\n" + + "\"RestoreItemActionItemsReadyRequest\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x18\n" + + "\arestore\x18\x02 \x01(\fR\arestore\x12G\n" + + "\x0fadditionalItems\x18\x03 \x03(\v2\x1d.generated.ResourceIdentifierR\x0fadditionalItems\";\n" + + "#RestoreItemActionItemsReadyResponse\x12\x14\n" + + "\x05ready\x18\x01 \x01(\bR\x05ready2\xd0\x03\n" + + "\x11RestoreItemAction\x12Z\n" + + "\tAppliesTo\x12%.v2.RestoreItemActionAppliesToRequest\x1a&.v2.RestoreItemActionAppliesToResponse\x12T\n" + + "\aExecute\x12#.v2.RestoreItemActionExecuteRequest\x1a$.v2.RestoreItemActionExecuteResponse\x12W\n" + + "\bProgress\x12$.v2.RestoreItemActionProgressRequest\x1a%.v2.RestoreItemActionProgressResponse\x12D\n" + + "\x06Cancel\x12\".v2.RestoreItemActionCancelRequest\x1a\x16.google.protobuf.Empty\x12j\n" + + "\x17AreAdditionalItemsReady\x12&.v2.RestoreItemActionItemsReadyRequest\x1a'.v2.RestoreItemActionItemsReadyResponseBJZHgithub.com/vmware-tanzu/velero/pkg/plugin/generated/restoreitemaction/v2b\x06proto3" var ( file_restoreitemaction_v2_RestoreItemAction_proto_rawDescOnce sync.Once - file_restoreitemaction_v2_RestoreItemAction_proto_rawDescData = file_restoreitemaction_v2_RestoreItemAction_proto_rawDesc + file_restoreitemaction_v2_RestoreItemAction_proto_rawDescData []byte ) func file_restoreitemaction_v2_RestoreItemAction_proto_rawDescGZIP() []byte { file_restoreitemaction_v2_RestoreItemAction_proto_rawDescOnce.Do(func() { - file_restoreitemaction_v2_RestoreItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(file_restoreitemaction_v2_RestoreItemAction_proto_rawDescData) + file_restoreitemaction_v2_RestoreItemAction_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_restoreitemaction_v2_RestoreItemAction_proto_rawDesc), len(file_restoreitemaction_v2_RestoreItemAction_proto_rawDesc))) }) return file_restoreitemaction_v2_RestoreItemAction_proto_rawDescData } var file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes = make([]protoimpl.MessageInfo, 9) -var file_restoreitemaction_v2_RestoreItemAction_proto_goTypes = []interface{}{ +var file_restoreitemaction_v2_RestoreItemAction_proto_goTypes = []any{ (*RestoreItemActionExecuteRequest)(nil), // 0: v2.RestoreItemActionExecuteRequest (*RestoreItemActionExecuteResponse)(nil), // 1: v2.RestoreItemActionExecuteResponse (*RestoreItemActionAppliesToRequest)(nil), // 2: v2.RestoreItemActionAppliesToRequest @@ -739,121 +633,11 @@ func file_restoreitemaction_v2_RestoreItemAction_proto_init() { if File_restoreitemaction_v2_RestoreItemAction_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionExecuteRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionExecuteResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionAppliesToRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionAppliesToResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionProgressRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionProgressResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionCancelRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionItemsReadyRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RestoreItemActionItemsReadyResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_restoreitemaction_v2_RestoreItemAction_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_restoreitemaction_v2_RestoreItemAction_proto_rawDesc), len(file_restoreitemaction_v2_RestoreItemAction_proto_rawDesc)), NumEnums: 0, NumMessages: 9, NumExtensions: 0, @@ -864,7 +648,6 @@ func file_restoreitemaction_v2_RestoreItemAction_proto_init() { MessageInfos: file_restoreitemaction_v2_RestoreItemAction_proto_msgTypes, }.Build() File_restoreitemaction_v2_RestoreItemAction_proto = out.File - file_restoreitemaction_v2_RestoreItemAction_proto_rawDesc = nil file_restoreitemaction_v2_RestoreItemAction_proto_goTypes = nil file_restoreitemaction_v2_RestoreItemAction_proto_depIdxs = nil } From 22ae12575ca4a5ae412d5f2a7f9698ea6e96cae2 Mon Sep 17 00:00:00 2001 From: PragatiVerma111 Date: Sun, 9 Aug 2026 20:45:29 +0530 Subject: [PATCH 166/194] =?UTF-8?q?Fix=20excluded=20namespace=20objects=20?= =?UTF-8?q?leaking=20into=20backup=20with=20cross-namespa=E2=80=A6=20(#101?= =?UTF-8?q?59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix excluded namespace objects leaking into backup with cross-namespace listing Signed-off-by: Pragati * Add changelog for PR 10159 Signed-off-by: Pragati --------- Signed-off-by: Pragati Co-authored-by: Pragati --- changelogs/unreleased/10159-Pragati5-DEBUG | 1 + pkg/backup/backup_test.go | 23 ++++++++++++++++++++++ pkg/backup/item_collector.go | 3 ++- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/10159-Pragati5-DEBUG diff --git a/changelogs/unreleased/10159-Pragati5-DEBUG b/changelogs/unreleased/10159-Pragati5-DEBUG new file mode 100644 index 000000000..e8f33122d --- /dev/null +++ b/changelogs/unreleased/10159-Pragati5-DEBUG @@ -0,0 +1 @@ +Fix excluded namespace objects leaking into backups when using cross-namespace listing diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index 3baae0131..b116d5376 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -5429,6 +5429,29 @@ func TestBackupNamespaces(t *testing.T) { "resources/namespaces/v1-preferredversion/cluster/ns-3.json", }, }, + { + name: "Wildcard star with excluded namespaces test", + backup: defaultBackup().IncludedNamespaces("*").ExcludedNamespaces("ns-2").Result(), + apiResources: []*test.APIResource{ + test.Namespaces( + builder.ForNamespace("ns-1").Phase(corev1api.NamespaceActive).Result(), + builder.ForNamespace("ns-2").Phase(corev1api.NamespaceActive).Result(), + builder.ForNamespace("ns-3").Phase(corev1api.NamespaceActive).Result(), + ), + test.Deployments( + builder.ForDeployment("ns-1", "deploy-1").Result(), + builder.ForDeployment("ns-2", "deploy-2").Result(), + ), + }, + want: []string{ + "resources/namespaces/cluster/ns-1.json", + "resources/namespaces/v1-preferredversion/cluster/ns-1.json", + "resources/namespaces/cluster/ns-3.json", + "resources/namespaces/v1-preferredversion/cluster/ns-3.json", + "resources/deployments.apps/namespaces/ns-1/deploy-1.json", + "resources/deployments.apps/v1-preferredversion/namespaces/ns-1/deploy-1.json", + }, + }, { name: "Empty namespace test", backup: defaultBackup().IncludedNamespaces("invalid*").Result(), diff --git a/pkg/backup/item_collector.go b/pkg/backup/item_collector.go index f4c712921..3aade5fad 100644 --- a/pkg/backup/item_collector.go +++ b/pkg/backup/item_collector.go @@ -508,7 +508,8 @@ func (r *itemCollector) getResourceItems( kind: resource.Kind, }) - if item.GetNamespace() != "" { + if item.GetNamespace() != "" && + r.backupRequest.NamespaceIncludesExcludes.ShouldInclude(item.GetNamespace()) { log.Debugf("Track namespace %s in nsTracker", item.GetNamespace()) r.nsTracker.track(item.GetNamespace()) } From cc4161b7edd3cd4c92cfb38bf11a5848a7a61d63 Mon Sep 17 00:00:00 2001 From: PragatiVerma111 Date: Sun, 9 Aug 2026 21:31:32 +0530 Subject: [PATCH 167/194] ci: add backport/cherry-pick GitHub Action for release branches (#10158) * ci: add backport/cherry-pick GitHub Action for release branches Signed-off-by: Pragati * ci: add unreleased changelog for backport Action Signed-off-by: Pragati * ci: pin backport-action to commit SHA for write-permission safety Signed-off-by: Pragati * ci: address review nits on backport workflow Move permissions to the job (least privilege), document the backport-action bot user id guard, and fix a garbled comment. Signed-off-by: Pragati --------- Signed-off-by: Pragati Co-authored-by: Pragati --- .github/workflows/backport.yml | 78 ++++++++++++++++++++++ changelogs/unreleased/10158-Pragati5-DEBUG | 1 + 2 files changed, 79 insertions(+) create mode 100644 .github/workflows/backport.yml create mode 100644 changelogs/unreleased/10158-Pragati5-DEBUG diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 000000000..d0e13129e --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,78 @@ +name: Backport merged pull request + +# Automates cherry-picking merged PRs onto release branches. +# - Label a merged PR with e.g. `backport release-1.17` to backport on merge. +# - Or comment `/backport release-1.17` or `/cherrypick release-1.17` on a merged PR. +# See: https://github.com/velero-io/velero/issues/9603 + +on: + pull_request_target: + types: [closed] + issue_comment: + types: [created] + +permissions: {} + +jobs: + backport: + name: Backport pull request + # Exclude comments from the backport-action bot (user id 97796249) to prevent + # recursive triggers. The bot does not post /backport commands, so startsWith + # already blocks recursion; the id check is defense in depth. + if: > + github.repository == 'velero-io/velero' && + ( + ( + github.event_name == 'pull_request_target' && + github.event.pull_request.merged && + contains(toJSON(github.event.pull_request.labels.*.name), '"backport ') + ) || ( + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.comment.user.id != 97796249 && + contains( + fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), + github.event.comment.author_association + ) && + ( + startsWith(github.event.comment.body, '/backport') || + startsWith(github.event.comment.body, '/cherrypick') + ) + ) + ) + runs-on: ubuntu-latest + permissions: + contents: write # push backport branches and comment + pull-requests: write # open backport PRs + steps: + - name: Parse target branches from comment + id: parse + if: github.event_name == 'issue_comment' + env: + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + # First line only; strip /backport or /cherrypick prefix. + # Remaining text is a space-delimited list of target branches + # (may be empty, falls back to labels). + line=$(printf '%s' "$COMMENT_BODY" | head -n1 | tr -d '\r') + branches=$(printf '%s' "$line" | sed -E 's|^/(backport|cherrypick)[[:space:]]*||') + echo "branches=${branches}" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Create backport pull requests + # Pin to commit SHA: workflow has contents/pull-requests write. + uses: korthout/backport-action@2e830a1d0b8269505846ddd407a70876913ad1f8 # v4.6.0 + with: + # Labels like `backport release-1.17` select the target branch. + label_pattern: '^backport ([^ ]+)$' + # Prefer draft PRs with conflict markers over failing the job silently. + experimental: | + { + "conflict_resolution": "draft_commit_conflicts" + } + # Empty when triggered by merge labels; set when `/backport` or `/cherrypick` includes branches. + target_branches: ${{ steps.parse.outputs.branches }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/changelogs/unreleased/10158-Pragati5-DEBUG b/changelogs/unreleased/10158-Pragati5-DEBUG new file mode 100644 index 000000000..060fa5c38 --- /dev/null +++ b/changelogs/unreleased/10158-Pragati5-DEBUG @@ -0,0 +1 @@ +Add GitHub Action to automate backport/cherry-pick onto release branches From c9d4501d3699537cf24aa1c5a90da26994ca2357 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, 10 Aug 2026 14:20:59 +0800 Subject: [PATCH 168/194] Design for supporting volume data in-place restore (#10014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Design for supporting volume data in-place restore Design for supporting volume data in-place restore Signed-off-by: Wenkai Yin(尹文开) * Update the in-place restore design according the comments from internal and community Update the in-place restore design according the comments from internal and community Signed-off-by: Wenkai Yin(尹文开) * Add namespace-mapping section to clarify how to handle the namespace mapping Signed-off-by: Wenkai Yin(尹文开) --------- Signed-off-by: Wenkai Yin(尹文开) --- .../volume-data-inplace-restore.md | 370 ++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 design/volume-data-inplace-restore/volume-data-inplace-restore.md diff --git a/design/volume-data-inplace-restore/volume-data-inplace-restore.md b/design/volume-data-inplace-restore/volume-data-inplace-restore.md new file mode 100644 index 000000000..664f5a654 --- /dev/null +++ b/design/volume-data-inplace-restore/volume-data-inplace-restore.md @@ -0,0 +1,370 @@ +# Volume Data In-place Full/Incremental Restore + +## Table of Contents + +- [Background](#background) +- [Goals](#goals) +- [Non-Goals](#non-goals) +- [Overview](#overview) +- [Detailed Design](#detailed-design) + - [CRD Changes](#crd-changes) + - [CLI](#cli) + - [Workload Management](#workload-management) + - [Handling Cross-Zone Scheduling (WaitForFirstConsumer)](#handling-cross-zone-scheduling-waitforfirstconsumer) + - [Namespace Mapping](#namespace-mapping) + - [Pre-flight Checks](#pre-flight-checks) + - [1. PVC is Not Actively Used by a Running Pod](#1-pvc-is-not-actively-used-by-a-running-pod) + - [2. PVC is Bound to the Original PV](#2-pvc-is-bound-to-the-original-pv) + - [3. Volume Size Validation](#3-volume-size-validation) + - [Error Handling](#error-handling) + - [Restore Workflow Update](#restore-workflow-update) + - [In-place Incremental Restore for CSI Snapshot with Block Data Move for Block Volumes](#in-place-incremental-restore-for-csi-snapshot-with-block-data-move-for-block-volumes) + - [In-place Full Restore for CSI Snapshot with Block Data Move for Block Volumes](#in-place-full-restore-for-csi-snapshot-with-block-data-move-for-block-volumes) + - [In-place Incremental Restore for CSI Snapshot with File System Data Move for File System Volumes](#in-place-incremental-restore-for-csi-snapshot-with-file-system-data-move-for-file-system-volumes) + - [In-place Full Restore for CSI Snapshot with File System Data Move for File System Volumes](#in-place-full-restore-for-csi-snapshot-with-file-system-data-move-for-file-system-volumes) + - [In-place Incremental Restore for CSI Snapshot with Block Data Move for File System Volumes](#in-place-incremental-restore-for-csi-snapshot-with-block-data-move-for-file-system-volumes) + - [In-place Full Restore for CSI Snapshot with Block Data Move for File System Volumes](#in-place-full-restore-for-csi-snapshot-with-block-data-move-for-file-system-volumes) + - [In-place Incremental Restore for File System Backup for File System Volumes](#in-place-incremental-restore-for-file-system-backup-for-file-system-volumes) + - [In-place Full Restore for File System Backup for File System Volumes](#in-place-full-restore-for-file-system-backup-for-file-system-volumes) +- [Installation](#installation) +- [Upgrade](#upgrade) + +## Background + +Currently, Velero only supports restoring volume data to a newly provisioned PVC. If the target PVC already exists in the cluster, Velero skips the data restoration entirely and leaves the existing volume untouched. + +This design introduces the "in-place restore" capability, allowing Velero to restore volume data directly into an existing, bound PVC. When performing an in-place restore, users can choose to either overwrite the volume entirely (in-place full restore) or only restore the modified data to optimize performance (in-place incremental restore). + +To ensure data consistency and allow Velero to safely recreate the PVC during the process, users must manually delete any pods consuming the target volume before initiating an in-place restore. + +## Goals + +- Enable Velero to restore volume data directly into an existing, bound PVC without requiring the user to manually delete the PVC and PV. +- Support both Full (overwrite all) and Incremental (overwrite only changed data) in-place restores. +- Support in-place restores for Windows workloads. +- Ensure data consistency and correct Kubernetes scheduling constraints (e.g., handling `WaitForFirstConsumer` and zonal topologies) are respected during and after the restore. + +## Non-Goals + +- Automating the deletion of workloads before the restore. It remains the user's responsibility to ensure the volume is not actively consumed and the Pods are completely removed before triggering the restore to prevent data corruption and allow PVC recreation. +- In-place restore for CSI snapshot without data move. +- In-place restore for Native Snapshots (cloud provider snapshots without CSI). +- Fine-grained, per-volume control over in-place restores. The newly introduced in-place restore policies apply globally to all volumes within a single restore operation. Allowing users to specify different restore strategies for individual volumes is deferred to a future enhancement. + +## Overview + +This design focuses exclusively on volume data restoration. To support this, we are introducing a new field, `ExistingVolumeDataPolicy`, to the `Restore` spec. This feature operates independently of Kubernetes resource restoration, which remains controlled by the existing `ExistingResourcePolicy` field. + +Depending on how unchanged data is handled during the restoration process, in-place volume data restores are categorized into two types: + +- **In-place full restore**: Overwrites the volume with the backup data, regardless of whether the existing data has changed. +- **In-place incremental restore**: Optimizes the process by restoring only the data that has changed since the backup, leaving unmodified data intact. This is achieved by leveraging Changed Block Tracking (CBT) for block data and file metadata comparisons for file system data. + +Support for in-place full and incremental restores varies depending on the underlying backup method, as detailed in the following table: + +| Backup Method | In-place Full Restore | In-place Incremental Restore | +| --------------------------------------- | --------------------- | ---------------------------- | +| CSI Snapshot with Block Data Move | Yes | Yes | +| CSI Snapshot with File System Data Move | Yes | Yes | +| CSI Snapshot without Data Move | No | No | +| File System Backup | Yes | Yes | +| Native Snapshot | No | No | + +Additionally, a new boolean field `DeleteExtraFiles` is added to the `UploaderConfig` within the `Restore` spec. When performing a file system restore (either via PodVolumeBackup or CSI File System Data Move), this flag controls whether files present in the target volume but absent in the backup should be deleted. Setting this to `true` ensures the target volume's file system exactly mirrors the backup state. Note that this setting is ignored for block data mover restores, as block-level operations inherently overwrite the entire file system structure. + +Because Velero must create a temporary restore Pod in the Velero namespace to mount the volume and restore the data, it cannot directly use the existing PVC, which resides in the workload namespace. Velero must delete the existing PVC, recreate a temporary restore PVC in the Velero namespace, and bind it to the existing PV. The core strategy for implementing an in-place restore involves the following sequence: + +```mermaid +flowchart TD + subgraph PVC CSI RIA + A[Patch existing PV's reclaim policy to Retain] --> B[Delete existing PVC] + end + subgraph Exposer + B --> C[Create temporary restore PVC in Velero namespace
and bind it to existing PV] + C --> D[Create temporary restore Pod
that mounts temporary restore PVC] + end + subgraph Block/File System Uploader + D --> E[Restore data directly into the volume] + end + subgraph Exposer Post-Restore + E --> F[Delete temporary restore Pod and PVC] + end + F --> G[Target workload Pod mounts target PVC
once it is recreated] +``` + +When restoring a file system volume using the block data mover, the PV must temporarily have its `volumeMode` set to `Block` so the restore Pod can mount it as a raw block device. Because the `volumeMode` field in a PV spec is immutable, reusing the existing PV directly is not possible. Instead, Velero must delete the existing PV and create a temporary one. The sequence for this scenario is as follows: + +```mermaid +flowchart TD + subgraph PVC CSI RIA + A[Patch existing PV's reclaim policy to Retain] --> B[Delete existing PVC] + end + subgraph Exposer + B --> C[Delete existing PV] + C --> D[Create temporary restore PV with volumeMode: Block
using same volume handle] + D --> E[Create temporary restore PVC in Velero namespace
with volumeMode: Block and bind to temporary PV] + E --> F[Create temporary restore Pod
that mounts temporary restore PVC] + end + subgraph Block Uploader + F --> G[Restore data directly into the volume] + end + subgraph Exposer Post-Restore + G --> H[Delete temporary restore Pod, PVC, and PV] + H --> I[Recreate original PV with volumeMode: Filesystem] + end + I --> J[Recreate original PVC in workload namespace
and allow it to bind to recreated PV] +``` + + +## Detailed Design + +### CRD Changes + +To support the new in-place restore policies and incremental data transfer, several Custom Resource Definitions (CRDs) will be updated. + +**Restore CRD** +A new field `existingVolumeDataPolicy` is added to the `Restore` spec to allow users to define how existing volume data should be handled. Additionally, a new field `deleteExtraFiles` is added to the `uploaderConfig` to control file deletion during file system restores. + +```yaml +spec: + existingVolumeDataPolicy: "" # Valid values: "", none, full, incremental + uploaderConfig: + deleteExtraFiles: false +``` + +- `existingVolumeDataPolicy`: + - `""` (default) or `none`: Do not restore volume data if the target PVC already exists. + - `full`: Perform an in-place full restore, overwriting all existing data on the volume. + - `incremental`: Perform an in-place incremental restore, only overwriting data that has changed since the backup. +- `uploaderConfig.deleteExtraFiles`: A boolean flag that controls whether files present in the target volume but absent from the backup should be deleted. **Note:** This setting is *only* applicable to File System restores (PodVolumeBackup or CSI File System Data Move) and has no effect on Block Data Move restores. Furthermore, it is ignored for non-in-place restores (where `existingVolumeDataPolicy` is not set to `full` or `incremental`). + +If the target PVC does not exist, Velero will fall back to its default behavior and provision a new PVC for the restore, regardless of whether `existingVolumeDataPolicy` is set to `full` or `incremental`. Furthermore, if `existingVolumeDataPolicy` is set to `incremental` but the underlying storage does not support incremental restores, Velero will automatically fall back to a `full` restore. + +The following table summarizes the expected behavior for different combinations of `existingResourcePolicy` and `existingVolumeDataPolicy` when the target PVC already exists: + +| `existingResourcePolicy` | `existingVolumeDataPolicy` | PVC Resource Action | Volume Data Restore | +| ------------------------ | -------------------------- | ------------------- | ------------------- | +| `none` | `none` | Untouched | Untouched | +| `none` | `full` | Untouched | Full | +| `none` | `incremental` | Untouched | Incremental | +| `update` | `none` | Patched | Untouched | +| `update` | `full` | Patched | Full | +| `update` | `incremental` | Patched | Incremental | + +**DataDownload CRD** +To support incremental restores, the `DataDownload` spec is extended with a new `restoreType` string flag (valid values are `full` and `incremental`) to instruct the data mover to perform an incremental restore. It also introduces a new `csiSnapshot` field, which captures the metadata of a snapshot taken from the existing PVC, acting as the baseline for Changed Block Tracking (CBT) delta calculations during an in-place incremental block restore. Additionally, the `deleteExtraFiles` configuration is passed to the underlying data mover via the existing `dataMoverConfig` map. + +```yaml +spec: + restoreType: "incremental" + csiSnapshot: + volumeSnapshot: "" + storageClass: "" + snapshotClass: "" + driver: "" +``` + +- `restoreType`: A string flag indicating whether the data mover should perform a `full` or `incremental` restore. +- `csiSnapshot`: + - `volumeSnapshot`: the name of the volume snapshot + - `storageClass`: the name of the storage class of the PVC that the volume snapshot is created from + - `snapshotClass`: the name of the snapshot class that the volume snapshot is created with + - `driver`: the driver used by the VolumeSnapshotContent + +**PodVolumeRestore CRD** +A new `restoreType` string flag (valid values are `full` and `incremental`) is added to the `PodVolumeRestore` spec to instruct the file system data mover (e.g., Kopia) to perform an incremental restore. Additionally, the `deleteExtraFiles` configuration is passed to the underlying uploader via the existing `uploaderSettings` map. + +```yaml +spec: + restoreType: "incremental" +``` + +- `restoreType`: A string flag indicating whether the data mover should perform a `full` or `incremental` restore. + +### CLI + +New flags will be added to the `velero restore create` command to support the new policy: + +- `--existing-volume-data-policy`: Accepts the values `none`, `full`, or `incremental`, mapping to `existingVolumeDataPolicy`. +- `--delete-extra-files`: A boolean flag mapping to `uploaderConfig.deleteExtraFiles`. + +### Workload Management + +To ensure data consistency and allow for necessary configuration changes, users must delete any Pods actively using the target volume before initiating an in-place restore. This is required for three primary reasons: + +1. **Preventing Data Corruption:** It is critical to prevent the active workload Pods and the temporary restore Pods from writing to the volume simultaneously, which would lead to data corruption. +2. **PVC Recreation:** Velero creates a temporary restore Pod in the Velero namespace to mount the volume and restore the data. Since it cannot directly use the existing PVC located in the workload namespace, Velero must delete the existing PVC, create a temporary restore PVC in the Velero namespace, and bind it to the existing PV. However, Kubernetes' `pvc-protection` finalizer prevents the deletion of any PVC actively used by a running Pod. Consequently, simply pausing the workload is insufficient; the Pods must be completely removed to allow the PVC deletion to proceed. +3. **ReadWriteOncePod Access Mode:** If the volume is configured with the `ReadWriteOncePod` access mode, Kubernetes strictly enforces that the volume can only be mounted by a single Pod at a time. The existing workload Pod must be completely deleted to release the volume, allowing Velero's temporary restore Pod to successfully mount it and perform the data transfer. + +Users must manage the lifecycle of their workloads before starting the restore. This applies to various workload types: + +- **Standard Controllers (Deployments, StatefulSets, Jobs, CronJobs):** The required action depends on the restore method: + - **For CSI Snapshot Restores:** Users can scale these controllers down to zero replicas to terminate the underlying Pods. + - **For File System Restores (PodVolumeRestore):** Users must completely delete the controllers. Simply scaling down to zero is insufficient because file system restores rely on an init container injected into the restored target Pod to process the data transfer. If the controller is only scaled down, it will immediately terminate the Pod restored by Velero to maintain its zero-replica count. Although the controller may subsequently spawn a new Pod, that new Pod will lack the required restore init container, causing the restore to fail. +- **DaemonSets:** Since Kubernetes lacks a mechanism to scale DaemonSets to zero, users must either delete the DaemonSet entirely or use node selectors/cordoning to evict the Pods. +- **Operator-Managed Pods:** Custom controllers (like ArgoCD) may have fast reconciliation loops that aggressively recreate Pods. These operators must be paused or suspended, and their managed Pods deleted. +- **Out-of-Cluster Clients:** External consumers accessing the storage directly (e.g., via NFS or storage APIs) are invisible to Kubernetes and must be manually disconnected to ensure no external writes occur during the restore. + +**Note:** Automating the deletion of these workloads is explicitly out of scope for this feature. It remains the user's responsibility to ensure the volume is not actively consumed and the Pods are removed before triggering the restore. + +### Handling Cross-Zone Scheduling (WaitForFirstConsumer) + +When performing an in-place restore, Velero deletes the existing target PVC and recreates it. For StorageClasses using the `WaitForFirstConsumer` volume binding mode, this recreation resets the scheduling lifecycle. Even though Velero adds a selector to the PVC spec to ensure it binds exclusively to the original PV, a scheduling issue can still occur. If the target PVC loses its node affinity, the Kubernetes Scheduler might schedule the recreated business Pod to a different availability zone. Because the original PV is physically constrained to its original zone, the Pod will fail to mount the volume and remain stuck in the `ContainerCreating` state with an attachment error. + +**Solution**: +During the PVC Restore Item Action (RIA), Velero must extract the `volume.kubernetes.io/selected-node` annotation from the original PVC. When Velero recreates the target PVC, it must inject this annotation back into the PVC spec. +By preserving the `selected-node` annotation, the Kubernetes Scheduler is forced to schedule the recreated business Pod to the original node/zone, ensuring it successfully mounts the restored PV. + +### Namespace Mapping +When namespace mapping is configured, in-place restores work normally in most scenarios. However, in-place incremental restores using CSI snapshots with a block data mover are not natively supported across different namespaces. Velero cannot use Changed Block Tracking (CBT) to calculate data deltas when the target volume is in a different namespace, as the volumes may belong to different lineages. + +Despite this, users can achieve a fast cross-namespace "clone and restore" workflow. For example, to quickly clone a large production workload into a test namespace ((e.g., for debugging, testing, or auditing)), a standard full restore would be too slow. Instead, users can manually take a CSI snapshot of the source PVC and provision a new PVC in the destination namespace from that snapshot. + +When a Velero restore is triggered against this new PVC, Velero detects the snapshot and uses CBT to write only the blocks that changed since the backup. This effectively "rolls back" the clone to the backup's state, drastically reducing data transfer and speeding up the restore. + +The key requirements for this approach are: +1. **Manual Cloning:** Users must manually snapshot the source PVC and clone it to the destination namespace before the restore. *(Note: Users must manually recreate the `VolumeSnapshotContent` and `VolumeSnapshot` in the destination namespace, or use `CrossNamespaceVolumeDataSource` if supported).* +2. **Workload Management:** Ensure no Pods are mounting the destination PVC during the restore to prevent data corruption. +3. **Snapshot Detection:** Velero inspects the destination PVC's `dataSource`. If it is a `VolumeSnapshot`, Velero uses its `SnapshotHandle` along with the backup's handle to calculate CBT. +4. **One-Shot Operation:** This is a one-time process. To restore a different backup later, users must clean up the destination namespace and repeat the workflow. +5. **Snapshot Cleanup:** Users must manually delete the temporary snapshot after the restore completes. + +### Pre-flight Checks + +Before initiating an in-place restore for a volume, Velero performs the following pre-flight checks to ensure the operation is safe and valid: + +#### 1. PVC is Not Actively Used by a Running Pod +Velero verifies that the target PVC is not currently mounted or consumed by any running Pods in the cluster. If the PVC is in use, Velero will skip the in-place restore for that volume and log an error. This enforces the prerequisite that users must completely delete consuming workloads prior to the restore, which prevents data corruption and avoids deadlocks caused by the Kubernetes `pvc-protection` finalizer during PVC recreation. + +#### 2. PVC is Bound to the Original PV +Velero checks whether the existing PVC in the cluster is still bound to the same PersistentVolume (PV) it was bound to at the time of the backup. If the PVC is bound to a different PV, performing an in-place restore (especially an incremental one that relies on Changed Block Tracking) may be unsafe or result in unpredictable behavior. If this check fails, Velero will log an error and skip the in-place restore for that volume. + + +#### 3. Volume Size Validation + +For in-place restores, the target volume must be large enough to accommodate the backed-up data. While the data path performs size checks during the actual restoration (only for block data mover), Velero will fail early to prevent unnecessary operations (such as taking a temporary snapshot). + +Before initiating an in-place restore, Velero compares the existing PV's size (`pv.spec.capacity.storage`) against the backup's data size (retrieved from the backup volume info). If the target PV is smaller than the backup data size, Velero will log an error and skip the volume data restoration. + +### Error Handling + +It is highly recommended that users create a backup (e.g., a CSI snapshot backup without data movement, if possible) before initiating an in-place restore. This ensures that the original state can be recovered in the event of a restore failure. + +If an in-place restore fails, Velero will intentionally leave certain temporary resources intact, such as the temporary PVC bound to the existing PV. Velero does not automatically clean up these resources because doing so could inadvertently trigger the deletion of the underlying storage volume. In such failure scenarios, users must manually clean up these temporary resources and, if necessary, use their pre-restore backup to recover the system's state. + +### Restore Workflow Update + +This section outlines the step-by-step control path and data path workflows for in-place restores. The exact sequence of operations depends on the backup method (CSI snapshot vs. file system backup), the chosen data mover (block vs. file system), and the target volume mode (block vs. file system). The following subsections detail the mechanisms for each supported scenario. + +#### In-place Incremental Restore for CSI Snapshot with Block Data Move for Block Volumes + +**Control Path** + +PVC RIA: +- Preserve the `volume.kubernetes.io/selected-node` annotation to ensure correct scheduling during target PVC recreation. + +PVC CSI RIA: +- Create a snapshot of the existing `PVC` to serve as the baseline for CBT delta calculations. +- Patch the existing PV's reclaim policy to `Retain`. +- Delete the existing PVC. +- Create a `DataDownload` resource referencing this snapshot and the existing `PV`, with `restoreType` set to `incremental`. + +Restore Exposer: +- Create a temporary restore PVC and bind it to the existing PV. +- Create a temporary restore Pod that mounts the temporary restore PVC. + +**Data Path** + +Block Uploader: +- The block uploader leverages Changed Block Tracking (CBT) to calculate the delta between the volume's current state and the backup snapshot. By skipping unchanged blocks and exclusively overwriting the modified ones, it significantly reduces I/O operations and accelerates the overall restore process. If the underlying storage system lacks CBT support, Velero will automatically fall back to performing an in-place full restore. + +#### In-place Full Restore for CSI Snapshot with Block Data Move for Block Volumes + +The workflow is identical to the **In-place Incremental Restore for CSI Snapshot with Block Data Move for Block Volumes**, with the following exceptions: +- No baseline snapshot is taken. +- The uploader does not use CBT to calculate deltas; instead, it overwrites all data on the volume. + +#### In-place Incremental Restore for CSI Snapshot with File System Data Move for File System Volumes + +**Control Path** + +The control path workflow is identical to the **In-place Incremental Restore for CSI Snapshot with Block Data Move for Block Volumes**, with the following exceptions: +- No baseline snapshot is taken. + +**Data Path** + +Kopia Uploader: +- Set the `incremental` flag to `true` when initiating the restore with the Kopia uploader. +- Pass the `deleteExtraFiles` configuration to the Kopia uploader based on the user's settings. +- Kopia evaluates file metadata (e.g., modification times and sizes) to identify changed files. It skips downloading and overwriting files that are identical to the backup, only restoring those that are modified, missing, or corrupted. + +#### In-place Full Restore for CSI Snapshot with File System Data Move for File System Volumes + +The workflow is identical to the **In-place Incremental Restore for CSI Snapshot with File System Data Move for File System Volumes**, with the following exceptions: +- The `restoreType` flag set to `full`. +- The Kopia uploader does not evaluate file metadata to skip unchanged files; instead, it overwrites all data on the target volume. + +#### In-place Incremental Restore for CSI Snapshot with Block Data Move for File System Volumes + +**Control Path** + +PVC RIA: +- Preserve the `volume.kubernetes.io/selected-node` annotation to ensure correct scheduling during target PVC recreation. + +PVC CSI RIA: +- Create a snapshot of the existing `PVC` to serve as the baseline for CBT delta calculations. +- Patch the existing `PV` to set its `persistentVolumeReclaimPolicy` to `Retain`. +- Delete the existing `PVC`. +- Create a `DataDownload` resource referencing the snapshot and the existing `PV`, with `restoreType` set to `incremental`. + +Restore Exposer: +- Delete the existing `PV`. +- Create a temporary restore `PV` with `volumeMode` set to `Block`, using the same volume handle as the original `PV`. +- Reset the bind information of the temporary restore `PV` to ensure it only binds to the temporary restore `PVC`. +- Create a temporary restore `PVC` with `volumeMode` set to `Block`. +- Create a temporary restore Pod that mounts the temporary restore `PVC`. + +**Data Path** + +Block Uploader: +- The block uploader leverages Changed Block Tracking (CBT) to calculate the delta between the volume's current state and the backup snapshot. By skipping unchanged blocks and exclusively overwriting the modified ones, it significantly reduces I/O operations and accelerates the overall restore process. If the underlying storage system lacks CBT support, Velero will automatically fall back to performing an in-place full restore. + +**Control Path (Post-Restore)** + +Restore Exposer: +- Delete the temporary restore Pod, `PVC`, and `PV`. +- Recreate the original `PV` with its `volumeMode` set back to `Filesystem`. +- Proceed with the standard process to allow the target `PVC` to bind to the recreated `PV`. + +#### In-place Full Restore for CSI Snapshot with Block Data Move for File System Volumes + +The workflow is identical to the **In-place Incremental Restore for CSI Snapshot with Block Data Move for File System Volumes**, with the following exceptions: +- No baseline snapshot is taken. +- The uploader does not use CBT to calculate deltas; instead, it overwrites all data on the volume. + +#### In-place Incremental Restore for File System Backup for File System Volumes + +**Control Path** + +- Create a `PodVolumeRestore` resource with `restoreType` set to `incremental`. + +**Data Path** + +Kopia Uploader: +- Set the `incremental` flag to `true` when initiating the restore with the Kopia uploader. +- Pass the `deleteExtraFiles` configuration to the Kopia uploader based on the user's settings. +- Similar to the CSI File System Data Move, Kopia evaluates file metadata to skip unchanged files and only restores those that are modified or missing. + +#### In-place Full Restore for File System Backup for File System Volumes + +The workflow is identical to the **In-place Incremental Restore for File System Backup for File System Volumes**, with the following exceptions: +- The `restoreType` flag is set to `full`. +- The Kopia uploader does not evaluate file metadata to skip unchanged files; instead, it overwrites all data on the target volume. + +## Installation + +No change to Installation. + +## Upgrade + +No impacts to Upgrade. The new fields in the CRDs are all optional fields and have backwards compatible values. \ No newline at end of file From fb86290def985b2c27285e2faf5dc19347b98e0d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:44:01 -0700 Subject: [PATCH 169/194] Merge pull request #10208 from velero-io/copilot/edit-autoassign-workflow Re-request maintainer review when only one CODEOWNERS approval exists --- .github/workflows/auto_assign_prs.yml | 68 ++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto_assign_prs.yml b/.github/workflows/auto_assign_prs.yml index 8966b235e..b51fde199 100644 --- a/.github/workflows/auto_assign_prs.yml +++ b/.github/workflows/auto_assign_prs.yml @@ -6,6 +6,10 @@ name: "Auto Assign Author" on: pull_request_target: types: [opened, reopened, ready_for_review] + # Watch for submitted reviews so we can re-request a second CODEOWNERS + # review once only one maintainer has approved. + pull_request_review: + types: [submitted] permissions: contents: read @@ -14,10 +18,72 @@ permissions: jobs: # Automatically assigns reviewers and owner add-reviews: - if: github.repository == 'velero-io/velero' + if: github.repository == 'velero-io/velero' && github.event_name == 'pull_request_target' runs-on: ubuntu-latest steps: - name: Set the author of a PR as the assignee uses: kentaro-m/auto-assign-action@v2.0.0 with: configuration-path: ".github/auto-assignees.yml" + + # `.github/CODEOWNERS` automatically requests review from the + # velero-io/maintainer team, but that request is cleared as soon as a + # single member of the team submits a review. Since we require a minimum + # of 2 reviewers (see `number_of_reviewers` in auto-assignees.yml), this + # re-requests a review from the maintainer team whenever a PR still has + # fewer than the required number of approvals, so a second CODEOWNERS + # reviewer gets pinged. + re-request-review: + if: github.repository == 'velero-io/velero' && github.event_name == 'pull_request_review' && github.event.review.state == 'approved' + runs-on: ubuntu-latest + steps: + - name: Re-request review from maintainers if more approvals are needed + uses: actions/github-script@v7 + with: + script: | + const requiredApprovals = 2; + const maintainerTeam = 'maintainer'; + const { owner, repo } = context.repo; + const pull_number = context.payload.pull_request.number; + + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number, + }); + + // Count distinct users whose most recent review is an approval. + // The Reviews API does not guarantee chronological order, so + // sort by submission time before folding into the map. + const sortedReviews = [...reviews].sort( + (a, b) => new Date(a.submitted_at) - new Date(b.submitted_at) + ); + const latestReviewByUser = new Map(); + for (const review of sortedReviews) { + latestReviewByUser.set(review.user.login, review.state); + } + const approvedReviewers = [...latestReviewByUser.entries()].filter( + ([, state]) => state === 'APPROVED' + ); + + if (approvedReviewers.length >= requiredApprovals) { + console.log( + `PR already has ${approvedReviewers.length} approvals, no need to re-request review.` + ); + return; + } + + console.log( + `PR has ${approvedReviewers.length}/${requiredApprovals} approvals, re-requesting review from @${owner}/${maintainerTeam}.` + ); + + try { + await github.rest.pulls.requestReviewers({ + owner, + repo, + pull_number, + team_reviewers: [maintainerTeam], + }); + } catch (error) { + core.warning(`Failed to re-request review from maintainers: ${error.message}`); + } From ced051b72f3abb467fa57eb2b85a0e7aedcf4936 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Mon, 10 Aug 2026 15:36:14 -0400 Subject: [PATCH 170/194] Fix restore-wait init container ignoring pod-level securityContext (#10047) restore-wait's securityContext fallback chain checked the fs-restore ConfigMap, then the first container's SecurityContext, then hardcoded runAsUser 1000. It never consulted pod.Spec.SecurityContext, so pods that set identity only at the pod level got a helper running as uid 1000 regardless of the workload's actual uid. On volumes where restored content is owner-only-visible to a non-1000 uid, the helper's stat on the done-file returns EACCES forever and the pod deadlocks at Init:0/1. Add pod-level spec.securityContext.runAsUser/runAsGroup as a fallback between the container-level check and the hardcoded default, since the workload's own identity is the one that can read what it restored. Defer to the pod's own RunAsNonRoot setting when runAsUser is 0, since the hardcoded RunAsNonRoot: true would otherwise contradict a root uid. Also add a test case covering both container-level and pod-level SecurityContext set together, confirming container-level still wins. Fixes #10046 Signed-off-by: Tiger Kaovilai --- changelogs/unreleased/10047-kaovilai | 1 + .../actions/pod_volume_restore_action.go | 19 ++ .../actions/pod_volume_restore_action_test.go | 205 ++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 changelogs/unreleased/10047-kaovilai diff --git a/changelogs/unreleased/10047-kaovilai b/changelogs/unreleased/10047-kaovilai new file mode 100644 index 000000000..6d96bdede --- /dev/null +++ b/changelogs/unreleased/10047-kaovilai @@ -0,0 +1 @@ +Fix restore-wait init container ignoring pod-level securityContext, falling back to hardcoded runAsUser 1000 instead of the workload's own uid/gid, causing fs-backup restores to deadlock at Init:0/1 on owner-restricted volumes diff --git a/pkg/restore/actions/pod_volume_restore_action.go b/pkg/restore/actions/pod_volume_restore_action.go index 5f2b3db3e..cbfcbfb35 100644 --- a/pkg/restore/actions/pod_volume_restore_action.go +++ b/pkg/restore/actions/pod_volume_restore_action.go @@ -198,6 +198,25 @@ func (a *PodVolumeRestoreAction) Execute(input *velero.RestoreItemActionExecuteI securityContext = *pod.Spec.Containers[0].SecurityContext.DeepCopy() securityContextSet = true } + // if no configmap or container-level securityContext is set, fall back to the pod-level + // spec.securityContext runAsUser/runAsGroup: the workload's own identity is the one that + // wrote the restored files, so it's the one that can read them back + if !securityContextSet && pod.Spec.SecurityContext != nil && + (pod.Spec.SecurityContext.RunAsUser != nil || pod.Spec.SecurityContext.RunAsGroup != nil) { + securityContext = defaultSecurityCtx() + if pod.Spec.SecurityContext.RunAsUser != nil { + securityContext.RunAsUser = pod.Spec.SecurityContext.RunAsUser + // defaultSecurityCtx() hardcodes RunAsNonRoot: true, which contradicts a pod-level + // RunAsUser of 0 (root); defer to the pod's own RunAsNonRoot setting in that case + if *pod.Spec.SecurityContext.RunAsUser == 0 { + securityContext.RunAsNonRoot = pod.Spec.SecurityContext.RunAsNonRoot + } + } + if pod.Spec.SecurityContext.RunAsGroup != nil { + securityContext.RunAsGroup = pod.Spec.SecurityContext.RunAsGroup + } + securityContextSet = true + } if !securityContextSet { securityContext = defaultSecurityCtx() } diff --git a/pkg/restore/actions/pod_volume_restore_action_test.go b/pkg/restore/actions/pod_volume_restore_action_test.go index bc9662ab7..614a5d1be 100644 --- a/pkg/restore/actions/pod_volume_restore_action_test.go +++ b/pkg/restore/actions/pod_volume_restore_action_test.go @@ -156,6 +156,155 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) { defaultRestoreHelperImage := "velero/velero:v1.0" + podLevelUID := int64(999) + podLevelGID := int64(999) + podLevelSecurityContext := corev1api.SecurityContext{ + AllowPrivilegeEscalation: boolptr.False(), + Capabilities: &corev1api.Capabilities{ + Drop: []corev1api.Capability{"ALL"}, + }, + SeccompProfile: &corev1api.SeccompProfile{ + Type: corev1api.SeccompProfileTypeRuntimeDefault, + }, + RunAsUser: &podLevelUID, + RunAsGroup: &podLevelGID, + RunAsNonRoot: boolptr.True(), + } + + podWithPodLevelSecurityContext := builder.ForPod("ns-1", "my-pod"). + ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + Result() + podWithPodLevelSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &podLevelUID, RunAsGroup: &podLevelGID} + + wantPodWithPodLevelSecurityContext := builder.ForPod("ns-1", "my-pod"). + ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + InitContainers( + newRestoreInitContainerBuilder(defaultRestoreHelperImage, ""). + Resources(&resourceReqs). + SecurityContext(&podLevelSecurityContext). + VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()). + Command([]string{"/velero-restore-helper"}).Result()). + Result() + wantPodWithPodLevelSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &podLevelUID, RunAsGroup: &podLevelGID} + + podLevelRootUID := int64(0) + podLevelRootSecurityContext := corev1api.SecurityContext{ + AllowPrivilegeEscalation: boolptr.False(), + Capabilities: &corev1api.Capabilities{ + Drop: []corev1api.Capability{"ALL"}, + }, + SeccompProfile: &corev1api.SeccompProfile{ + Type: corev1api.SeccompProfileTypeRuntimeDefault, + }, + RunAsUser: &podLevelRootUID, + } + + podWithPodLevelRootSecurityContext := builder.ForPod("ns-1", "my-pod"). + ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + Result() + podWithPodLevelRootSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &podLevelRootUID} + + wantPodWithPodLevelRootSecurityContext := builder.ForPod("ns-1", "my-pod"). + ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + InitContainers( + newRestoreInitContainerBuilder(defaultRestoreHelperImage, ""). + Resources(&resourceReqs). + SecurityContext(&podLevelRootSecurityContext). + VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()). + Command([]string{"/velero-restore-helper"}).Result()). + Result() + wantPodWithPodLevelRootSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &podLevelRootUID} + + podLevelGroupOnlyGID := int64(777) + podLevelGroupOnlySecurityContext := corev1api.SecurityContext{ + AllowPrivilegeEscalation: boolptr.False(), + Capabilities: &corev1api.Capabilities{ + Drop: []corev1api.Capability{"ALL"}, + }, + SeccompProfile: &corev1api.SeccompProfile{ + Type: corev1api.SeccompProfileTypeRuntimeDefault, + }, + RunAsUser: &id, + RunAsGroup: &podLevelGroupOnlyGID, + RunAsNonRoot: boolptr.True(), + } + + podWithPodLevelGroupOnlySecurityContext := builder.ForPod("ns-1", "my-pod"). + ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + Result() + podWithPodLevelGroupOnlySecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsGroup: &podLevelGroupOnlyGID} + + wantPodWithPodLevelGroupOnlySecurityContext := builder.ForPod("ns-1", "my-pod"). + ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + InitContainers( + newRestoreInitContainerBuilder(defaultRestoreHelperImage, ""). + Resources(&resourceReqs). + SecurityContext(&podLevelGroupOnlySecurityContext). + VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()). + Command([]string{"/velero-restore-helper"}).Result()). + Result() + wantPodWithPodLevelGroupOnlySecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsGroup: &podLevelGroupOnlyGID} + + bothLevelsPodUID := int64(500) + bothLevelsContainerUID := int64(999) + bothLevelsContainerSecurityContext := corev1api.SecurityContext{ + AllowPrivilegeEscalation: boolptr.False(), + Capabilities: &corev1api.Capabilities{ + Drop: []corev1api.Capability{"ALL"}, + }, + SeccompProfile: &corev1api.SeccompProfile{ + Type: corev1api.SeccompProfileTypeRuntimeDefault, + }, + RunAsUser: &bothLevelsContainerUID, + RunAsNonRoot: boolptr.True(), + } + + podWithBothLevelsSecurityContext := builder.ForPod("ns-1", "my-pod"). + ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + Containers( + builder.ForContainer("app-container", "app-image"). + SecurityContext(&bothLevelsContainerSecurityContext).Result()). + Result() + podWithBothLevelsSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &bothLevelsPodUID} + + wantPodWithBothLevelsSecurityContext := builder.ForPod("ns-1", "my-pod"). + ObjectMeta(builder.WithAnnotations("snapshot.velero.io/myvol", "")). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + Containers( + builder.ForContainer("app-container", "app-image"). + SecurityContext(&bothLevelsContainerSecurityContext).Result()). + InitContainers( + newRestoreInitContainerBuilder(defaultRestoreHelperImage, ""). + Resources(&resourceReqs). + SecurityContext(&bothLevelsContainerSecurityContext). + VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()). + Command([]string{"/velero-restore-helper"}).Result()). + Result() + wantPodWithBothLevelsSecurityContext.Spec.SecurityContext = &corev1api.PodSecurityContext{RunAsUser: &bothLevelsPodUID} + tests := []struct { name string pod *corev1api.Pod @@ -350,6 +499,62 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) { VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()). Command([]string{"/velero-restore-helper"}).Result()).Result(), }, + { + name: "Restoring pod with pod-level securityContext (no container-level SecurityContext) uses pod-level runAsUser/runAsGroup for the restore initContainer", + pod: podWithPodLevelSecurityContext, + podVolumeBackups: []runtime.Object{ + builder.ForPodVolumeBackup(veleroNs, "pvb-1"). + PodName("my-pod"). + PodNamespace("ns-1"). + Volume("myvol"). + ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)). + SnapshotID("foo"). + Result(), + }, + want: wantPodWithPodLevelSecurityContext, + }, + { + name: "Restoring pod with pod-level securityContext.runAsUser=0 does not force RunAsNonRoot on the restore initContainer", + pod: podWithPodLevelRootSecurityContext, + podVolumeBackups: []runtime.Object{ + builder.ForPodVolumeBackup(veleroNs, "pvb-1"). + PodName("my-pod"). + PodNamespace("ns-1"). + Volume("myvol"). + ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)). + SnapshotID("foo"). + Result(), + }, + want: wantPodWithPodLevelRootSecurityContext, + }, + { + name: "Restoring pod with pod-level securityContext.runAsGroup only (no runAsUser) still applies the group to the restore initContainer", + pod: podWithPodLevelGroupOnlySecurityContext, + podVolumeBackups: []runtime.Object{ + builder.ForPodVolumeBackup(veleroNs, "pvb-1"). + PodName("my-pod"). + PodNamespace("ns-1"). + Volume("myvol"). + ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)). + SnapshotID("foo"). + Result(), + }, + want: wantPodWithPodLevelGroupOnlySecurityContext, + }, + { + name: "Restoring pod with both container-level and pod-level SecurityContext set uses the container-level SecurityContext for the restore initContainer (container-level takes priority)", + pod: podWithBothLevelsSecurityContext, + podVolumeBackups: []runtime.Object{ + builder.ForPodVolumeBackup(veleroNs, "pvb-1"). + PodName("my-pod"). + PodNamespace("ns-1"). + Volume("myvol"). + ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)). + SnapshotID("foo"). + Result(), + }, + want: wantPodWithBothLevelsSecurityContext, + }, { name: "pod volume backups in a different namespace are ignored when looking for matches due to namespace scoping", pod: builder.ForPod("ns-1", "my-pod"). From 513e93ff4bb7bf57abc0093ac853a7dcd795d0e6 Mon Sep 17 00:00:00 2001 From: Shelly Chahar Date: Tue, 11 Aug 2026 01:10:02 +0530 Subject: [PATCH 171/194] fix: correct typos in log messages and status strings (#10192) - Fix 'dataudownload' typo in DataDownload warning log message (data_download_controller.go:696) - Fix 'datadownlad' misspelled structured log field key to 'datadownload' (data_download_controller.go:700) - this caused the log field to be unqueryable by the correct key name - Fix 'retrieveable' -> 'retrievable' in BackupRepository maintenance status messages (maintenance.go:354, 417) - Update corresponding test assertion to match corrected string (maintenance_test.go:792) Signed-off-by: shellyco-code Co-authored-by: shellyco-code --- pkg/controller/data_download_controller.go | 4 ++-- pkg/repository/maintenance/maintenance.go | 4 ++-- pkg/repository/maintenance/maintenance_test.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/controller/data_download_controller.go b/pkg/controller/data_download_controller.go index 337d10936..422879d6e 100644 --- a/pkg/controller/data_download_controller.go +++ b/pkg/controller/data_download_controller.go @@ -693,11 +693,11 @@ func (r *DataDownloadReconciler) findSnapshotRestoreForPod(ctx context.Context, r.prepareDataDownload(dd) return true }); err != nil { - log.WithError(err).Warn("failed to update dataudownload, prepare will halt for this dataudownload") + log.WithError(err).Warn("failed to update datadownload, prepare will halt for this datadownload") return []reconcile.Request{} } } else if unrecoverable, reason := kube.IsPodUnrecoverable(pod, log); unrecoverable { - err := UpdateDataDownloadWithRetry(context.Background(), r.client, types.NamespacedName{Namespace: dd.Namespace, Name: dd.Name}, r.logger.WithField("datadownlad", dd.Name), + err := UpdateDataDownloadWithRetry(context.Background(), r.client, types.NamespacedName{Namespace: dd.Namespace, Name: dd.Name}, r.logger.WithField("datadownload", dd.Name), func(dataDownload *velerov2alpha1api.DataDownload) bool { if dataDownload.Spec.Cancel { return false diff --git a/pkg/repository/maintenance/maintenance.go b/pkg/repository/maintenance/maintenance.go index 33c3fb1f8..86525d54f 100644 --- a/pkg/repository/maintenance/maintenance.go +++ b/pkg/repository/maintenance/maintenance.go @@ -351,7 +351,7 @@ func WaitJobComplete(cli client.Client, ctx context.Context, jobName, ns string, if maintenanceJob.Status.Failed > 0 { if r, err := getResultFromJob(cli, maintenanceJob); err != nil { log.WithError(err).Warn("Failed to get maintenance job result") - result = "Repo maintenance failed but result is not retrieveable" + result = "Repo maintenance failed but result is not retrievable" } else { result = r } @@ -414,7 +414,7 @@ func WaitAllJobsComplete(ctx context.Context, cli client.Client, repo *velerov1a if job.Status.Failed > 0 { if msg, err := getResultFromJob(cli, job); err != nil { log.WithError(err).Warnf("Failed to get result of maintenance job %s", job.Name) - message = fmt.Sprintf("Repo maintenance failed but result is not retrieveable, err: %v", err) + message = fmt.Sprintf("Repo maintenance failed but result is not retrievable, err: %v", err) } else { message = msg } diff --git a/pkg/repository/maintenance/maintenance_test.go b/pkg/repository/maintenance/maintenance_test.go index 05fce89e9..ee34241ce 100644 --- a/pkg/repository/maintenance/maintenance_test.go +++ b/pkg/repository/maintenance/maintenance_test.go @@ -789,7 +789,7 @@ func TestWaitAllJobsComplete(t *testing.T) { { Result: velerov1api.BackupRepositoryMaintenanceFailed, StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)}, - Message: "Repo maintenance failed but result is not retrieveable, err: no pod found for job job2", + Message: "Repo maintenance failed but result is not retrievable, err: no pod found for job job2", }, }, }, From 943a4d6bb414c61cd1008a57ec3e9a2baee20382 Mon Sep 17 00:00:00 2001 From: harshit saini <123226128+harshitsaini17@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:30:54 +0530 Subject: [PATCH 172/194] docs: fix restore logs command name in self-signed-certificates (#10213) The list of commands supporting --insecure-skip-tls-verify referred to `velero restore log`, but the registered command is `velero restore logs` (pkg/cmd/cli/restore/logs.go). `velero restore log` silently falls through to the parent command's help text and exits 0, so a user following the docs gets no logs and no error. Fixes #10183 Signed-off-by: Harshit saini --- site/content/docs/main/self-signed-certificates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/docs/main/self-signed-certificates.md b/site/content/docs/main/self-signed-certificates.md index 41eb8b247..87576dae2 100644 --- a/site/content/docs/main/self-signed-certificates.md +++ b/site/content/docs/main/self-signed-certificates.md @@ -150,7 +150,7 @@ Velero provides a way for you to skip TLS verification on the object store when * velero backup download * velero backup logs * velero restore describe -* velero restore log +* velero restore logs If true, the object store's TLS certificate will not be checked for validity before Velero or backup repository connects to the object storage. You can permanently skip TLS verification for an object store by setting `Spec.Config.InsecureSkipTLSVerify` to true in the [BackupStorageLocation](api-types/backupstoragelocation.md) CRD. From ccbb7d1cc7e13cbe536a4c04cea3d08bff5508a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:01:13 -0400 Subject: [PATCH 173/194] Bump actions/setup-go from 6 to 7 (#10202) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6 to 7. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/e2e-test-kind.yaml | 4 ++-- .github/workflows/pr-ci-check.yml | 2 +- .github/workflows/pr-linter-check.yml | 2 +- .github/workflows/push.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index 88cc3b641..34a98203c 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@v7 - name: Set up Go version - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: ${{ needs.get-go-version.outputs.version }} @@ -122,7 +122,7 @@ jobs: uses: actions/checkout@v7 - name: Set up Go version - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: ${{ needs.get-go-version.outputs.version }} diff --git a/.github/workflows/pr-ci-check.yml b/.github/workflows/pr-ci-check.yml index 01e86dc08..fd5948f7b 100644 --- a/.github/workflows/pr-ci-check.yml +++ b/.github/workflows/pr-ci-check.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v7 - name: Set up Go version - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: ${{ needs.get-go-version.outputs.version }} diff --git a/.github/workflows/pr-linter-check.yml b/.github/workflows/pr-linter-check.yml index 6f8057be6..1a25569f1 100644 --- a/.github/workflows/pr-linter-check.yml +++ b/.github/workflows/pr-linter-check.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v7 - name: Set up Go version - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: ${{ needs.get-go-version.outputs.version }} diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index f4f4d9c6e..f5ce8c456 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Go version - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ needs.get-go-version.outputs.version }} From 476a7ca160a8cd9a6ea941fd09ecf4d7b281cec5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:01:37 -0400 Subject: [PATCH 174/194] Bump github/codeql-action from 4.37.3 to 4.37.6 (#10203) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.3 to 4.37.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.3...v4.37.6) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.37.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nightly-trivy-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly-trivy-scan.yml b/.github/workflows/nightly-trivy-scan.yml index 3acb1d15b..4c1381a5b 100644 --- a/.github/workflows/nightly-trivy-scan.yml +++ b/.github/workflows/nightly-trivy-scan.yml @@ -31,6 +31,6 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v4.37.3 + uses: github/codeql-action/upload-sarif@v4.37.6 with: sarif_file: 'trivy-results.sarif' \ No newline at end of file From 9a549f781f650e78fbf370a3db10dbb5bcb68f0e Mon Sep 17 00:00:00 2001 From: Jay Sawant Date: Tue, 11 Aug 2026 01:40:20 +0530 Subject: [PATCH 175/194] docs: fix grammar and typos in customize-installation (#10190) Signed-off-by: Jay2006sawant --- site/content/docs/main/customize-installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/content/docs/main/customize-installation.md b/site/content/docs/main/customize-installation.md index 28cc24154..194d947eb 100644 --- a/site/content/docs/main/customize-installation.md +++ b/site/content/docs/main/customize-installation.md @@ -40,7 +40,7 @@ When installing with the `--use-node-agent` flag, the node-agent will mount the By default, `velero install` does not enable the use of File System Backup (FSB) to take backups of all pod volumes. You must apply an [annotation](file-system-backup.md/#using-opt-in-pod-volume-backup) to every pod which contains volumes for Velero to use FSB for the backup. -If you are planning to only use FSB for volume backups, you can run the `velero install` command with the `--default-volumes-to-fs-backup` flag. This will default all pod volumes backups to use FSB without having to apply annotations to pods. Note that when this flag is set during install, Velero will always try to use FSB to perform the backup, even want an individual backup to use volume snapshots, by setting the `--snapshot-volumes` flag in the `backup create` command. Alternatively, you can set the `--default-volumes-to-fs-backup` on an individual backup to to make sure Velero uses FSB for each volume being backed up. +If you are planning to only use FSB for volume backups, you can run the `velero install` command with the `--default-volumes-to-fs-backup` flag. This will default all pod volume backups to use FSB without having to apply annotations to pods. Note that when this flag is set during install, Velero will always try to use FSB to perform the backup. If you want an individual backup to use volume snapshots instead, set the `--snapshot-volumes` flag in the `backup create` command. Alternatively, you can set the `--default-volumes-to-fs-backup` flag on an individual backup to make sure Velero uses FSB for each volume being backed up. ## Update an existing installation @@ -219,7 +219,7 @@ kubectl patch daemonset node-agent -n velero --patch \ '{"spec":{"template":{"spec":{"containers":[{"name": "node-agent", "resources": {"limits":{"cpu": "1", "memory": "1024Mi"}, "requests": {"cpu": "1", "memory": "512Mi"}}}]}}}}' ``` -Additionally, you may want to update the the default File System Backup operation timeout (default 240 minutes) to allow larger backups more time to complete. You can adjust this timeout by adding the `- --fs-backup-timeout` argument to the Velero Deployment spec. +Additionally, you may want to update the default File System Backup operation timeout (default 240 minutes) to allow larger backups more time to complete. You can adjust this timeout by adding the `- --fs-backup-timeout` argument to the Velero Deployment spec. **NOTE:** Changes made to this timeout value will revert back to the default value if you re-run the Velero install command. From 40de7f9f97f3004129653d0de9c90e6a63ef1c6a Mon Sep 17 00:00:00 2001 From: Xun Jiang/Bruce Jiang <59276555+blackpiglet@users.noreply.github.com> Date: Tue, 11 Aug 2026 04:29:13 +0800 Subject: [PATCH 176/194] Make backupType case insensitive in the CLI. (#10189) Signed-off-by: Xun Jiang --- pkg/cmd/cli/backup/create.go | 10 ++++++++-- pkg/cmd/cli/backup/create_test.go | 16 ++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/cli/backup/create.go b/pkg/cmd/cli/backup/create.go index ae9dd2fec..5082eb239 100644 --- a/pkg/cmd/cli/backup/create.go +++ b/pkg/cmd/cli/backup/create.go @@ -242,11 +242,17 @@ func (o *CreateOptions) validateFromScheduleFlag(c *cobra.Command) error { return nil } +// validateBackupType check the backupType value and return the valid value. func (o *CreateOptions) validateBackupType() error { - backupType := strings.TrimSpace(o.BackupType) + // Allow full, and incremental from the CLI, and ignore case of the input string's case. + backupType := strings.ToLower(strings.TrimSpace(o.BackupType)) switch backupType { - case "", "Incremental", "Full": + case "": + case "incremental": + o.BackupType = string(velerov1api.BackupTypeIncremental) + case "full": + o.BackupType = string(velerov1api.BackupTypeFull) default: return fmt.Errorf("invalid backup type %s - valid values are 'Incremental', and 'Full'", backupType) } diff --git a/pkg/cmd/cli/backup/create_test.go b/pkg/cmd/cli/backup/create_test.go index 718ab0e96..46885b7c9 100644 --- a/pkg/cmd/cli/backup/create_test.go +++ b/pkg/cmd/cli/backup/create_test.go @@ -129,30 +129,34 @@ func TestCreateOptions_ValidateBackupType(t *testing.T) { o.BackupType = "" err := o.validateBackupType() require.NoError(t, err) + require.Empty(t, o.BackupType) o.BackupType = "Incremental" err = o.validateBackupType() require.NoError(t, err) + require.EqualValues(t, velerov1api.BackupTypeIncremental, o.BackupType) o.BackupType = "Full" err = o.validateBackupType() require.NoError(t, err) + require.EqualValues(t, velerov1api.BackupTypeFull, o.BackupType) o.BackupType = " Incremental " err = o.validateBackupType() require.NoError(t, err) + require.EqualValues(t, velerov1api.BackupTypeIncremental, o.BackupType) + + o.BackupType = "iNcReMeNtAl" + err = o.validateBackupType() + require.NoError(t, err) + require.EqualValues(t, velerov1api.BackupTypeIncremental, o.BackupType) }) t.Run("invalid backup type", func(t *testing.T) { o := NewCreateOptions() - o.BackupType = "incremental" - err := o.validateBackupType() - require.Error(t, err) - require.Equal(t, "invalid backup type incremental - valid values are 'Incremental', and 'Full'", err.Error()) - o.BackupType = "invalid" - err = o.validateBackupType() + err := o.validateBackupType() require.Error(t, err) require.Equal(t, "invalid backup type invalid - valid values are 'Incremental', and 'Full'", err.Error()) }) From a41cb1190250a531f1948d37755a37013055b83f Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 11 Aug 2026 12:56:23 +0800 Subject: [PATCH 177/194] add prefetch options for repo interface Signed-off-by: Lyndon-Li --- pkg/repository/udmrepo/kopialib/lib_repo.go | 4 ++-- pkg/repository/udmrepo/repo.go | 7 ++++++- pkg/uploader/block/uploader.go | 2 +- pkg/uploader/kopia/shim.go | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo.go b/pkg/repository/udmrepo/kopialib/lib_repo.go index c7bb65a43..b26edb76f 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo.go @@ -338,7 +338,7 @@ func (km *kopiaMaintenance) maintainProgress(uploaded int64) { } } -func (kr *kopiaRepository) OpenObject(ctx context.Context, id udmrepo.ID) (udmrepo.ObjectReader, error) { +func (kr *kopiaRepository) OpenObject(ctx context.Context, id udmrepo.ID, opt udmrepo.ObjectReadOptions) (udmrepo.ObjectReader, error) { if kr.rawRepo == nil { return nil, errors.New("repo is closed or not open") } @@ -550,7 +550,7 @@ func (kr *kopiaRepository) WriteMetadata(ctx context.Context, meta *udmrepo.Meta } func (kr *kopiaRepository) ReadMetadata(ctx context.Context, id udmrepo.ID) (*udmrepo.Metadata, error) { - reader, err := kr.OpenObject(ctx, id) + reader, err := kr.OpenObject(ctx, id, udmrepo.ObjectReadOptions{}) if err != nil { return nil, errors.Wrapf(err, "error to open metadata object %v", id) } diff --git a/pkg/repository/udmrepo/repo.go b/pkg/repository/udmrepo/repo.go index 76cdc5f1d..5873db743 100644 --- a/pkg/repository/udmrepo/repo.go +++ b/pkg/repository/udmrepo/repo.go @@ -72,6 +72,11 @@ type ObjectWriteOptions struct { ParentObject ID // The object in the previous snapshot, for incremental backup } +type ObjectReadOptions struct { + Prefetch bool + PrefetchBudgetMB int +} + type AdvancedFeatureInfo struct { MultiPartBackup bool // if set to true, it means the repo supports multiple-part backup } @@ -136,7 +141,7 @@ type BackupRepoService interface { type BackupRepo interface { // OpenObject opens an existing object for read. // id: the object's unified identifier. - OpenObject(ctx context.Context, id ID) (ObjectReader, error) + OpenObject(ctx context.Context, id ID, opt ObjectReadOptions) (ObjectReader, error) // GetManifest gets a manifest data from the backup repository. GetManifest(ctx context.Context, id ID, mani *RepoManifest) error diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 824c0ae9f..4d8d86e84 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -176,7 +176,7 @@ func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bi return 0, errors.Errorf("dest dev(%s) size is too small (%v vs. %v)", dest.path, dest.size, sourceSize) } - reader, err := blkup.repoWriter.OpenObject(blkup.ctx, meta.SubObjects[0].ID) + reader, err := blkup.repoWriter.OpenObject(blkup.ctx, meta.SubObjects[0].ID, udmrepo.ObjectReadOptions{}) if err != nil { return 0, errors.Wrapf(err, "error opening bdev object %v", meta.SubObjects[0].Name) } diff --git a/pkg/uploader/kopia/shim.go b/pkg/uploader/kopia/shim.go index 4a3908185..465b76c04 100644 --- a/pkg/uploader/kopia/shim.go +++ b/pkg/uploader/kopia/shim.go @@ -56,7 +56,7 @@ func NewShimRepo(repo udmrepo.BackupRepo) repo.RepositoryWriter { // OpenObject open specific object func (sr *shimRepository) OpenObject(ctx context.Context, id object.ID) (object.Reader, error) { - reader, err := sr.udmRepo.OpenObject(ctx, udmrepo.ID(id.String())) + reader, err := sr.udmRepo.OpenObject(ctx, udmrepo.ID(id.String()), udmrepo.ObjectReadOptions{}) if err != nil { return nil, errors.Wrapf(err, "failed to open object with id %v", id) } From b74824f9c0b7ad39260dbb5dda382490bd276746 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 11 Aug 2026 13:00:44 +0800 Subject: [PATCH 178/194] extend object reader for prefetch Signed-off-by: Lyndon-Li --- pkg/repository/udmrepo/kopialib/lib_repo.go | 138 +++++++++++++++++++- 1 file changed, 134 insertions(+), 4 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo.go b/pkg/repository/udmrepo/kopialib/lib_repo.go index b26edb76f..0efde34a7 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo.go @@ -73,8 +73,22 @@ type logThrottle struct { interval time.Duration } +type objectPrefetch struct { + ctx context.Context + cancel context.CancelFunc + entries []object.IndirectObjectEntry + curOffset int64 + cond *sync.Cond + mu sync.Mutex + nextEntry int + budget int64 +} + type kopiaObjectReader struct { rawReader object.Reader + rawRepo repo.Repository + prefetch *objectPrefetch + logger logrus.FieldLogger } type kopiaObjectWriter struct { @@ -353,9 +367,43 @@ func (kr *kopiaRepository) OpenObject(ctx context.Context, id udmrepo.ID, opt ud return nil, errors.Wrap(err, "error to open object") } - return &kopiaObjectReader{ + var prefetch *objectPrefetch + if opt.Prefetch { + if e, err := kr.getFlattenedEntries(ctx, objID); err != nil { + kr.logger.WithError(err).Warnf("Failed to load entries for object %v, skip prefetch", id) + } else { + pCtx, pCancel := context.WithCancel(ctx) + prefetch = &objectPrefetch{ + ctx: pCtx, + cancel: pCancel, + budget: int64(opt.PrefetchBudgetMB) << 20, + entries: e, + } + + prefetch.cond = sync.NewCond(&prefetch.mu) + } + + } + + rd := &kopiaObjectReader{ rawReader: reader, - }, nil + rawRepo: kr.rawRepo, + prefetch: prefetch, + logger: kr.logger, + } + + if rd.prefetch != nil { + go rd.prefetchProc() + + go func() { + <-rd.prefetch.ctx.Done() + prefetch.mu.Lock() + prefetch.cond.Broadcast() + prefetch.mu.Unlock() + }() + } + + return rd, nil } func (kr *kopiaRepository) GetManifest(ctx context.Context, id udmrepo.ID, mani *udmrepo.RepoManifest) error { @@ -792,7 +840,16 @@ func (kor *kopiaObjectReader) Read(p []byte) (int, error) { return 0, errors.New("object reader is closed or not open") } - return kor.rawReader.Read(p) + n, err := kor.rawReader.Read(p) + if n > 0 { + if kor.prefetch != nil { + kor.prefetch.mu.Lock() + kor.prefetch.curOffset += int64(n) + kor.prefetch.cond.Signal() + kor.prefetch.mu.Unlock() + } + } + return n, err } func (kor *kopiaObjectReader) Seek(offset int64, whence int) (int64, error) { @@ -800,10 +857,83 @@ func (kor *kopiaObjectReader) Seek(offset int64, whence int) (int64, error) { return -1, errors.New("object reader is closed or not open") } - return kor.rawReader.Seek(offset, whence) + off, err := kor.rawReader.Seek(offset, whence) + if err == nil { + if kor.prefetch != nil { + kor.prefetch.mu.Lock() + kor.prefetch.curOffset = off + kor.prefetch.cond.Signal() + kor.prefetch.mu.Unlock() + } + } + + return off, err +} + +func (kor *kopiaObjectReader) prefetchProc() { + prefetch := kor.prefetch + if prefetch == nil { + return + } + + for { + prefetch.mu.Lock() + + select { + case <-prefetch.ctx.Done(): + prefetch.mu.Unlock() + return + default: + } + + curOffset := prefetch.curOffset + + for prefetch.nextEntry < len(prefetch.entries) { + entry := prefetch.entries[prefetch.nextEntry] + if entry.Start+entry.Length <= curOffset { + prefetch.nextEntry++ + } else { + break + } + } + + if prefetch.nextEntry >= len(prefetch.entries) { + prefetch.mu.Unlock() + return + } + + var toFetch []object.ID + for prefetch.nextEntry < len(prefetch.entries) { + entry := prefetch.entries[prefetch.nextEntry] + + if entry.Start > curOffset+prefetch.budget { + break + } + + toFetch = append(toFetch, entry.Object) + prefetch.nextEntry++ + } + + if len(toFetch) == 0 { + prefetch.cond.Wait() + prefetch.mu.Unlock() + continue + } + + prefetch.mu.Unlock() + + _, err := kor.rawRepo.PrefetchObjects(prefetch.ctx, toFetch, "") + if err != nil && err != context.Canceled { + kor.logger.WithError(err).Warnf("Failed to prefetch contents for offset %v", curOffset) + } + } } func (kor *kopiaObjectReader) Close() error { + if kor.prefetch != nil && kor.prefetch.cancel != nil { + kor.prefetch.cancel() + } + if kor.rawReader == nil { return nil } From de87def9d902df85d48c6add91c278bb754698ea Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 11 Aug 2026 13:02:11 +0800 Subject: [PATCH 179/194] enable object reader prefetch for block uploader Signed-off-by: Lyndon-Li --- pkg/uploader/block/uploader.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 4d8d86e84..275fd18ed 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -176,7 +176,10 @@ func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bi return 0, errors.Errorf("dest dev(%s) size is too small (%v vs. %v)", dest.path, dest.size, sourceSize) } - reader, err := blkup.repoWriter.OpenObject(blkup.ctx, meta.SubObjects[0].ID, udmrepo.ObjectReadOptions{}) + reader, err := blkup.repoWriter.OpenObject(blkup.ctx, meta.SubObjects[0].ID, udmrepo.ObjectReadOptions{ + Prefetch: true, + PrefetchBudgetMB: 256, + }) if err != nil { return 0, errors.Wrapf(err, "error opening bdev object %v", meta.SubObjects[0].Name) } From c27343fc4e256647310d754e3c52e01afb2d45a3 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 11 Aug 2026 13:07:27 +0800 Subject: [PATCH 180/194] fix UT errors Signed-off-by: Lyndon-Li --- .../udmrepo/kopialib/lib_repo_test.go | 2 +- pkg/repository/udmrepo/mocks/BackupRepo.go | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_test.go index 370b82b9e..0cc52261a 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_test.go @@ -318,7 +318,7 @@ func TestOpenObject(t *testing.T) { kr.rawRepo = tc.rawRepo } - _, err := kr.OpenObject(t.Context(), udmrepo.ID(tc.objectID)) + _, err := kr.OpenObject(t.Context(), udmrepo.ID(tc.objectID), udmrepo.ObjectReadOptions{}) if tc.expectedErr == "" { assert.NoError(t, err) diff --git a/pkg/repository/udmrepo/mocks/BackupRepo.go b/pkg/repository/udmrepo/mocks/BackupRepo.go index 623c4d70d..3206422b4 100644 --- a/pkg/repository/udmrepo/mocks/BackupRepo.go +++ b/pkg/repository/udmrepo/mocks/BackupRepo.go @@ -699,8 +699,8 @@ func (_c *BackupRepo_NewObjectWriter_Call) RunAndReturn(run func(ctx context.Con } // OpenObject provides a mock function for the type BackupRepo -func (_mock *BackupRepo) OpenObject(ctx context.Context, id udmrepo.ID) (udmrepo.ObjectReader, error) { - ret := _mock.Called(ctx, id) +func (_mock *BackupRepo) OpenObject(ctx context.Context, id udmrepo.ID, opt udmrepo.ObjectReadOptions) (udmrepo.ObjectReader, error) { + ret := _mock.Called(ctx, id, opt) if len(ret) == 0 { panic("no return value specified for OpenObject") @@ -708,18 +708,18 @@ func (_mock *BackupRepo) OpenObject(ctx context.Context, id udmrepo.ID) (udmrepo var r0 udmrepo.ObjectReader var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) (udmrepo.ObjectReader, error)); ok { - return returnFunc(ctx, id) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID, udmrepo.ObjectReadOptions) (udmrepo.ObjectReader, error)); ok { + return returnFunc(ctx, id, opt) } - if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) udmrepo.ObjectReader); ok { - r0 = returnFunc(ctx, id) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID, udmrepo.ObjectReadOptions) udmrepo.ObjectReader); ok { + r0 = returnFunc(ctx, id, opt) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(udmrepo.ObjectReader) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, udmrepo.ID) error); ok { - r1 = returnFunc(ctx, id) + if returnFunc, ok := ret.Get(1).(func(context.Context, udmrepo.ID, udmrepo.ObjectReadOptions) error); ok { + r1 = returnFunc(ctx, id, opt) } else { r1 = ret.Error(1) } @@ -734,11 +734,12 @@ type BackupRepo_OpenObject_Call struct { // OpenObject is a helper method to define mock.On call // - ctx context.Context // - id udmrepo.ID -func (_e *BackupRepo_Expecter) OpenObject(ctx interface{}, id interface{}) *BackupRepo_OpenObject_Call { - return &BackupRepo_OpenObject_Call{Call: _e.mock.On("OpenObject", ctx, id)} +// - opt udmrepo.ObjectReadOptions +func (_e *BackupRepo_Expecter) OpenObject(ctx interface{}, id interface{}, opt interface{}) *BackupRepo_OpenObject_Call { + return &BackupRepo_OpenObject_Call{Call: _e.mock.On("OpenObject", ctx, id, opt)} } -func (_c *BackupRepo_OpenObject_Call) Run(run func(ctx context.Context, id udmrepo.ID)) *BackupRepo_OpenObject_Call { +func (_c *BackupRepo_OpenObject_Call) Run(run func(ctx context.Context, id udmrepo.ID, opt udmrepo.ObjectReadOptions)) *BackupRepo_OpenObject_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -748,9 +749,14 @@ func (_c *BackupRepo_OpenObject_Call) Run(run func(ctx context.Context, id udmre if args[1] != nil { arg1 = args[1].(udmrepo.ID) } + var arg2 udmrepo.ObjectReadOptions + if args[2] != nil { + arg2 = args[2].(udmrepo.ObjectReadOptions) + } run( arg0, arg1, + arg2, ) }) return _c @@ -761,7 +767,7 @@ func (_c *BackupRepo_OpenObject_Call) Return(objectReader udmrepo.ObjectReader, return _c } -func (_c *BackupRepo_OpenObject_Call) RunAndReturn(run func(ctx context.Context, id udmrepo.ID) (udmrepo.ObjectReader, error)) *BackupRepo_OpenObject_Call { +func (_c *BackupRepo_OpenObject_Call) RunAndReturn(run func(ctx context.Context, id udmrepo.ID, opt udmrepo.ObjectReadOptions) (udmrepo.ObjectReader, error)) *BackupRepo_OpenObject_Call { _c.Call.Return(run) return _c } From 92bcf5a3b33dc27649e12e81eb755c4520a7e33d Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 11 Aug 2026 13:36:26 +0800 Subject: [PATCH 181/194] add UT for prefetch Signed-off-by: Lyndon-Li --- .../udmrepo/kopialib/lib_repo_test.go | 216 +++++++++++++++++- 1 file changed, 212 insertions(+), 4 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_test.go index 0cc52261a..6bd64009c 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_test.go @@ -22,12 +22,14 @@ import ( "encoding/json" "math" "os" + "sync" "testing" "time" "github.com/cockroachdb/errors" "github.com/kopia/kopia/fs" "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/content" "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/repo/object" "github.com/kopia/kopia/snapshot" @@ -285,6 +287,7 @@ func TestOpenObject(t *testing.T) { name string rawRepo *repomocks.MockRepository objectID string + opt udmrepo.ObjectReadOptions retErr error expectedErr string }{ @@ -304,21 +307,38 @@ func TestOpenObject(t *testing.T) { retErr: errors.New("fake-open-error"), expectedErr: "error to open object: fake-open-error", }, + { + name: "raw open success, without prefetch", + rawRepo: repomocks.NewMockRepository(t), + objectID: "D0123456789abcdef0123456789abcdef", + }, + { + name: "raw open success, with prefetch", + rawRepo: repomocks.NewMockRepository(t), + objectID: "D0123456789abcdef0123456789abcdef", + opt: udmrepo.ObjectReadOptions{Prefetch: true, PrefetchBudgetMB: 10}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - kr := &kopiaRepository{} + kr := &kopiaRepository{ + logger: velerotest.NewLogger(), + } if tc.rawRepo != nil { - if tc.retErr != nil { - tc.rawRepo.On("OpenObject", mock.Anything, mock.Anything).Return(nil, tc.retErr) + if tc.name != "objectID is invalid" { + if tc.retErr != nil { + tc.rawRepo.On("OpenObject", mock.Anything, mock.Anything).Return(nil, tc.retErr) + } else { + tc.rawRepo.On("OpenObject", mock.Anything, mock.Anything).Return(nil, nil) + } } kr.rawRepo = tc.rawRepo } - _, err := kr.OpenObject(t.Context(), udmrepo.ID(tc.objectID), udmrepo.ObjectReadOptions{}) + _, err := kr.OpenObject(t.Context(), udmrepo.ID(tc.objectID), tc.opt) if tc.expectedErr == "" { assert.NoError(t, err) @@ -845,6 +865,7 @@ func TestReaderClose(t *testing.T) { name string rawObjReader *repomocks.Reader rawReaderRetErr error + withPrefetch bool expectedErr string }{ { @@ -860,6 +881,11 @@ func TestReaderClose(t *testing.T) { name: "succeed", rawObjReader: repomocks.NewReader(t), }, + { + name: "succeed with prefetch", + rawObjReader: repomocks.NewReader(t), + withPrefetch: true, + }, } for _, tc := range testCases { @@ -871,8 +897,20 @@ func TestReaderClose(t *testing.T) { kr.rawReader = tc.rawObjReader } + if tc.withPrefetch { + ctx, cancel := context.WithCancel(t.Context()) + kr.prefetch = &objectPrefetch{ + ctx: ctx, + cancel: cancel, + } + } + err := kr.Close() + if tc.withPrefetch { + assert.ErrorIs(t, kr.prefetch.ctx.Err(), context.Canceled) + } + if tc.expectedErr == "" { assert.NoError(t, err) } else { @@ -1832,3 +1870,173 @@ func TestListSnapshot(t *testing.T) { }) } } + +func mustParseID(s string) object.ID { + id, _ := object.ParseID(s) + return id +} + +func TestPrefetchProc(t *testing.T) { + testCases := []struct { + name string + setupPrefetch func(ctx context.Context, cancel context.CancelFunc) *objectPrefetch + mockRepo func(mockRepo *repomocks.MockRepository) + runConcurrently bool + trigger func(prefetch *objectPrefetch) + }{ + { + name: "nil prefetch", + setupPrefetch: func(ctx context.Context, cancel context.CancelFunc) *objectPrefetch { + return nil + }, + }, + { + name: "context canceled", + setupPrefetch: func(ctx context.Context, cancel context.CancelFunc) *objectPrefetch { + cancel() + p := &objectPrefetch{ + ctx: ctx, + } + p.cond = sync.NewCond(&p.mu) + return p + }, + }, + { + name: "fetch all entries and exit", + setupPrefetch: func(ctx context.Context, cancel context.CancelFunc) *objectPrefetch { + p := &objectPrefetch{ + ctx: ctx, + entries: []object.IndirectObjectEntry{ + {Start: 0, Length: 100, Object: mustParseID("D0123456789abcdef0123456789abcdef")}, + {Start: 100, Length: 100, Object: mustParseID("D0123456789abcdef0123456789abcdeg")}, + }, + budget: 200, + curOffset: 0, + } + p.cond = sync.NewCond(&p.mu) + return p + }, + mockRepo: func(mockRepo *repomocks.MockRepository) { + mockRepo.On("PrefetchObjects", mock.Anything, []object.ID{mustParseID("D0123456789abcdef0123456789abcdef"), mustParseID("D0123456789abcdef0123456789abcdeg")}, "").Return(([]content.ID)(nil), nil).Once() + }, + }, + { + name: "fetch partial, wait, and fetch rest", + runConcurrently: true, + setupPrefetch: func(ctx context.Context, cancel context.CancelFunc) *objectPrefetch { + p := &objectPrefetch{ + ctx: ctx, + entries: []object.IndirectObjectEntry{ + {Start: 0, Length: 100, Object: mustParseID("D0123456789abcdef0123456789abcdef")}, + {Start: 100, Length: 100, Object: mustParseID("D0123456789abcdef0123456789abcdeg")}, + }, + budget: 50, + curOffset: 0, + } + p.cond = sync.NewCond(&p.mu) + return p + }, + mockRepo: func(mockRepo *repomocks.MockRepository) { + mockRepo.On("PrefetchObjects", mock.Anything, []object.ID{mustParseID("D0123456789abcdef0123456789abcdef")}, "").Return(([]content.ID)(nil), nil).Once() + mockRepo.On("PrefetchObjects", mock.Anything, []object.ID{mustParseID("D0123456789abcdef0123456789abcdeg")}, "").Return(([]content.ID)(nil), nil).Once() + }, + trigger: func(prefetch *objectPrefetch) { + // Wait a bit for the first fetch and wait to happen + time.Sleep(50 * time.Millisecond) + prefetch.mu.Lock() + prefetch.curOffset = 100 + prefetch.cond.Signal() + prefetch.mu.Unlock() + }, + }, + { + name: "cancel while waiting on cond", + runConcurrently: true, + setupPrefetch: func(ctx context.Context, cancel context.CancelFunc) *objectPrefetch { + p := &objectPrefetch{ + ctx: ctx, + cancel: cancel, + entries: []object.IndirectObjectEntry{ + {Start: 0, Length: 100, Object: mustParseID("D0123456789abcdef0123456789abcdef")}, + {Start: 100, Length: 100, Object: mustParseID("D0123456789abcdef0123456789abcdeg")}, + }, + budget: 50, + curOffset: 0, + } + p.cond = sync.NewCond(&p.mu) + // Simulate the watcher goroutine spawned in OpenObject + go func() { + <-ctx.Done() + p.mu.Lock() + p.cond.Broadcast() + p.mu.Unlock() + }() + return p + }, + mockRepo: func(mockRepo *repomocks.MockRepository) { + mockRepo.On("PrefetchObjects", mock.Anything, []object.ID{mustParseID("D0123456789abcdef0123456789abcdef")}, "").Return(([]content.ID)(nil), nil).Once() + }, + trigger: func(prefetch *objectPrefetch) { + // Wait a bit for the first fetch and wait to happen + time.Sleep(50 * time.Millisecond) + prefetch.cancel() // This triggers the watcher, broadcasts, and exits prefetchProc + }, + }, + { + name: "prefetch error should not panic and continue", + setupPrefetch: func(ctx context.Context, cancel context.CancelFunc) *objectPrefetch { + p := &objectPrefetch{ + ctx: ctx, + entries: []object.IndirectObjectEntry{ + {Start: 0, Length: 100, Object: mustParseID("D0123456789abcdef0123456789abcdef")}, + }, + budget: 200, + curOffset: 0, + } + p.cond = sync.NewCond(&p.mu) + return p + }, + mockRepo: func(mockRepo *repomocks.MockRepository) { + mockRepo.On("PrefetchObjects", mock.Anything, []object.ID{mustParseID("D0123456789abcdef0123456789abcdef")}, "").Return(([]content.ID)(nil), errors.New("fake-error")).Once() + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + mockRepo := repomocks.NewMockRepository(t) + if tc.mockRepo != nil { + tc.mockRepo(mockRepo) + } + + kor := &kopiaObjectReader{ + rawRepo: mockRepo, + logger: velerotest.NewLogger(), + prefetch: tc.setupPrefetch(ctx, cancel), + } + + if tc.runConcurrently { + done := make(chan struct{}) + go func() { + kor.prefetchProc() + close(done) + }() + if tc.trigger != nil { + tc.trigger(kor.prefetch) + } + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("prefetchProc did not finish in time") + } + } else { + kor.prefetchProc() + } + + mockRepo.AssertExpectations(t) + }) + } +} From c8127e243b78b8dc67b8fda6858088195cafd04e Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 11 Aug 2026 13:38:41 +0800 Subject: [PATCH 182/194] object reader throughput improvement Signed-off-by: Lyndon-Li --- changelogs/unreleased/10225-Lyndon-Li | 1 + pkg/repository/udmrepo/kopialib/lib_repo.go | 1 - pkg/repository/udmrepo/kopialib/lib_repo_test.go | 2 +- pkg/uploader/block/uploader_test.go | 2 +- pkg/uploader/kopia/shim_test.go | 6 +++--- 5 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/10225-Lyndon-Li diff --git a/changelogs/unreleased/10225-Lyndon-Li b/changelogs/unreleased/10225-Lyndon-Li new file mode 100644 index 000000000..435da13d1 --- /dev/null +++ b/changelogs/unreleased/10225-Lyndon-Li @@ -0,0 +1 @@ +Add prefetch mechanism to object reader so as to improve the restore throughput of block data mover \ No newline at end of file diff --git a/pkg/repository/udmrepo/kopialib/lib_repo.go b/pkg/repository/udmrepo/kopialib/lib_repo.go index 0efde34a7..e60128358 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo.go @@ -382,7 +382,6 @@ func (kr *kopiaRepository) OpenObject(ctx context.Context, id udmrepo.ID, opt ud prefetch.cond = sync.NewCond(&prefetch.mu) } - } rd := &kopiaObjectReader{ diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_test.go index 6bd64009c..b4d487c43 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_test.go @@ -908,7 +908,7 @@ func TestReaderClose(t *testing.T) { err := kr.Close() if tc.withPrefetch { - assert.ErrorIs(t, kr.prefetch.ctx.Err(), context.Canceled) + require.ErrorIs(t, kr.prefetch.ctx.Err(), context.Canceled) } if tc.expectedErr == "" { diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index 3b8930476..1b1eddbce 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -663,7 +663,7 @@ func TestBlockUploaderRestore(t *testing.T) { objReader.On("Read", mock.Anything).Return(0, io.EOF) objReader.On("Close").Return(nil) - repoWriter.On("OpenObject", mock.Anything, udmrepo.ID("data-id")).Return(objReader, nil) + repoWriter.On("OpenObject", mock.Anything, udmrepo.ID("data-id"), mock.Anything).Return(objReader, nil) snap := udmrepo.Snapshot{ Description: "test snapshot", diff --git a/pkg/uploader/kopia/shim_test.go b/pkg/uploader/kopia/shim_test.go index 7933ec6b8..3c7941405 100644 --- a/pkg/uploader/kopia/shim_test.go +++ b/pkg/uploader/kopia/shim_test.go @@ -81,7 +81,7 @@ func TestOpenObject(t *testing.T) { name: "Success", backupRepo: func() *mocks.BackupRepo { backupRepo := &mocks.BackupRepo{} - backupRepo.On("OpenObject", mock.Anything, mock.Anything).Return(&shimObjectReader{}, nil) + backupRepo.On("OpenObject", mock.Anything, mock.Anything, mock.Anything).Return(&shimObjectReader{}, nil) return backupRepo }(), }, @@ -89,7 +89,7 @@ func TestOpenObject(t *testing.T) { name: "Open object error", backupRepo: func() *mocks.BackupRepo { backupRepo := &mocks.BackupRepo{} - backupRepo.On("OpenObject", mock.Anything, mock.Anything).Return(&shimObjectReader{}, errors.New("Error open object")) + backupRepo.On("OpenObject", mock.Anything, mock.Anything, mock.Anything).Return(&shimObjectReader{}, errors.New("Error open object")) return backupRepo }(), isOpenObjectError: true, @@ -98,7 +98,7 @@ func TestOpenObject(t *testing.T) { name: "Get nil reader", backupRepo: func() *mocks.BackupRepo { backupRepo := &mocks.BackupRepo{} - backupRepo.On("OpenObject", mock.Anything, mock.Anything).Return(nil, nil) + backupRepo.On("OpenObject", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) return backupRepo }(), isReaderNil: true, From 8b7951426b5d282f041b3708f70db0f661c0e80d 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: Tue, 11 Aug 2026 15:48:31 +0800 Subject: [PATCH 183/194] Add "SnapshotClass" to DataUploadResult (#10227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "SnapshotClass" to DataUploadResult Signed-off-by: Wenkai Yin(尹文开) --- changelogs/unreleased/10227-ywk253100 | 1 + pkg/apis/velero/v2alpha1/data_upload_types.go | 4 ++++ .../actions/dataupload_retrieve_action.go | 3 +++ .../dataupload_retrieve_action_test.go | 23 +++++++++++++++++++ 4 files changed, 31 insertions(+) create mode 100644 changelogs/unreleased/10227-ywk253100 diff --git a/changelogs/unreleased/10227-ywk253100 b/changelogs/unreleased/10227-ywk253100 new file mode 100644 index 000000000..dcbbd6ac5 --- /dev/null +++ b/changelogs/unreleased/10227-ywk253100 @@ -0,0 +1 @@ +Add "SnapshotClass" to DataUploadResult \ No newline at end of file diff --git a/pkg/apis/velero/v2alpha1/data_upload_types.go b/pkg/apis/velero/v2alpha1/data_upload_types.go index 606502254..37e273b2b 100644 --- a/pkg/apis/velero/v2alpha1/data_upload_types.go +++ b/pkg/apis/velero/v2alpha1/data_upload_types.go @@ -268,4 +268,8 @@ type DataUploadResult struct { // FSType is the file system type of the volume. // +optional FSType string `json:"fsType,omitempty"` + + // SnapshotClass is the name of the snapshot class that the volume snapshot is created with + // +optional + SnapshotClass string `json:"snapshotClass,omitempty"` } diff --git a/pkg/restore/actions/dataupload_retrieve_action.go b/pkg/restore/actions/dataupload_retrieve_action.go index 77e4766f5..27db07471 100644 --- a/pkg/restore/actions/dataupload_retrieve_action.go +++ b/pkg/restore/actions/dataupload_retrieve_action.go @@ -82,6 +82,9 @@ func (d *DataUploadRetrieveAction) Execute(input *velero.RestoreItemActionExecut NodeOS: dataUpload.Status.NodeOS, FSType: dataUpload.Spec.SourceFSType, } + if dataUpload.Spec.CSISnapshot != nil { + dataUploadResult.SnapshotClass = dataUpload.Spec.CSISnapshot.SnapshotClass + } jsonBytes, err := json.Marshal(dataUploadResult) if err != nil { diff --git a/pkg/restore/actions/dataupload_retrieve_action_test.go b/pkg/restore/actions/dataupload_retrieve_action_test.go index 64be241bf..33a46a0a3 100644 --- a/pkg/restore/actions/dataupload_retrieve_action_test.go +++ b/pkg/restore/actions/dataupload_retrieve_action_test.go @@ -66,6 +66,29 @@ func TestDataUploadRetrieveActionExectue(t *testing.T) { }, expectedDataUploadResult: builder.ForConfigMap("velero", "").ObjectMeta(builder.WithGenerateName("testDU-"), builder.WithLabels(velerov1.PVCNamespaceNameLabel, "testNamespace.testPVC", velerov1.RestoreUIDLabel, "testingUID", velerov1.ResourceUsageLabel, string(velerov1.VeleroResourceUsageDataUploadResult))).Data("testingUID", `{"backupStorageLocation":"testLocation","snapshotID":"fake-id","sourceNamespace":"testNamespace","snapshotSize":1000}`).Result(), }, + { + name: "DataUploadRetrieve Action test with optional fields", + dataUpload: func() *velerov2alpha1.DataUpload { + du := builder.ForDataUpload("velero", "testDU"). + SourceNamespace("testNamespace"). + SourcePVC("testPVC"). + SnapshotID("fake-id"). + TotalBytes(1000). + DataMover("velero"). + NodeOS("linux"). + CSISnapshot(&velerov2alpha1.CSISnapshotSpec{SnapshotClass: "testClass"}). + Result() + du.Status.DataMoverResult = &map[string]string{"key": "value"} + du.Spec.SourceFSType = "ext4" + return du + }(), + restore: builder.ForRestore("velero", "testRestore").ObjectMeta(builder.WithUID("testingUID")).Backup("testBackup").Result(), + runtimeScheme: scheme, + veleroObjs: []runtime.Object{ + builder.ForBackup("velero", "testBackup").StorageLocation("testLocation").Result(), + }, + expectedDataUploadResult: builder.ForConfigMap("velero", "").ObjectMeta(builder.WithGenerateName("testDU-"), builder.WithLabels(velerov1.PVCNamespaceNameLabel, "testNamespace.testPVC", velerov1.RestoreUIDLabel, "testingUID", velerov1.ResourceUsageLabel, string(velerov1.VeleroResourceUsageDataUploadResult))).Data("testingUID", `{"backupStorageLocation":"testLocation","datamover":"velero","snapshotID":"fake-id","sourceNamespace":"testNamespace","dataMoverResult":{"key":"value"},"nodeOS":"linux","snapshotSize":1000,"fsType":"ext4","snapshotClass":"testClass"}`).Result(), + }, { name: "Long source namespace and PVC name should also work", dataUpload: builder.ForDataUpload("velero", "testDU").SourceNamespace("migre209d0da-49c7-45ba-8d5a-3e59fd591ec1").SourcePVC("kibishii-data-kibishii-deployment-0").Result(), From bbb0f11f3315d656f53281f479956e4565b50700 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 11 Aug 2026 15:37:02 +0800 Subject: [PATCH 184/194] use source size in progress for block uploader restore Signed-off-by: Lyndon-Li --- pkg/uploader/block/snapshot.go | 4 ++-- pkg/uploader/block/snapshot_test.go | 8 ++++---- pkg/uploader/block/uploader.go | 24 +++++++++++++----------- pkg/uploader/block/uploader_test.go | 8 ++++---- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/pkg/uploader/block/snapshot.go b/pkg/uploader/block/snapshot.go index 53e7e7f14..adec352ef 100644 --- a/pkg/uploader/block/snapshot.go +++ b/pkg/uploader/block/snapshot.go @@ -235,12 +235,12 @@ func Restore(ctx context.Context, blkUp Uploader, rep udmrepo.BackupRepo, snapsh return 0, errors.Wrapf(err, "error reset pos of block device %s", dest) } - size, err := blkUp.Restore(snapshot, destInfo{dev: destDev, path: destPath, size: destSize}, bitmap.Iterator(), uploaderCfg) + _, totalSize, err := blkUp.Restore(snapshot, destInfo{dev: destDev, path: destPath, size: destSize}, bitmap.Iterator(), uploaderCfg) if err != nil { return 0, errors.Wrapf(err, "error restoring to block dev %s", destPath) } - return size, nil + return totalSize, nil } func findPreviousSnapshot(ctx context.Context, rep udmrepo.BackupRepo, path string, snapshotTags map[string]string, noLaterThan *time.Time, log logrus.FieldLogger) (udmrepo.Snapshot, error) { diff --git a/pkg/uploader/block/snapshot_test.go b/pkg/uploader/block/snapshot_test.go index 8f6338311..fa77b2d10 100644 --- a/pkg/uploader/block/snapshot_test.go +++ b/pkg/uploader/block/snapshot_test.go @@ -46,9 +46,9 @@ func (m *mockUploader) Backup(src sourceInfo, parent udmrepo.ID, iter cbttypes.I return args.Get(0).(udmrepo.Snapshot), args.Get(1).(int64), args.Error(2) } -func (m *mockUploader) Restore(snap udmrepo.Snapshot, dest destInfo, iter cbttypes.Iterator, cfg map[string]string) (int64, error) { +func (m *mockUploader) Restore(snap udmrepo.Snapshot, dest destInfo, iter cbttypes.Iterator, cfg map[string]string) (int64, int64, error) { args := m.Called(snap, dest, iter, cfg) - return args.Get(0).(int64), args.Error(1) + return args.Get(0).(int64), args.Get(1).(int64), args.Error(2) } func testLog() logrus.FieldLogger { @@ -574,7 +574,7 @@ func TestRestore(t *testing.T) { repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-001")). Return(storedSnap, nil) blkup.On("Restore", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(int64(0), errors.New("restore I/O error")) + Return(int64(0), int64(0), errors.New("restore I/O error")) }, setupOpenDev: func(t *testing.T) *os.File { t.Helper() @@ -588,7 +588,7 @@ func TestRestore(t *testing.T) { repo.On("GetSnapshot", mock.Anything, udmrepo.ID("snap-001")). Return(storedSnap, nil) blkup.On("Restore", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(int64(4096), nil) + Return(int64(4096), int64(4096), nil) }, setupOpenDev: func(t *testing.T) *os.File { t.Helper() diff --git a/pkg/uploader/block/uploader.go b/pkg/uploader/block/uploader.go index 824c0ae9f..b2d879a48 100644 --- a/pkg/uploader/block/uploader.go +++ b/pkg/uploader/block/uploader.go @@ -59,7 +59,7 @@ type destInfo struct { type Uploader interface { Backup(sourceInfo, udmrepo.ID, cbt.Iterator, map[string]string) (udmrepo.Snapshot, int64, error) - Restore(udmrepo.Snapshot, destInfo, cbt.Iterator, map[string]string) (int64, error) + Restore(udmrepo.Snapshot, destInfo, cbt.Iterator, map[string]string) (int64, int64, error) } type blockUploader struct { @@ -148,18 +148,18 @@ func (blkup *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, b }, backupSize, nil } -func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bitmap cbt.Iterator, configs map[string]string) (int64, error) { +func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bitmap cbt.Iterator, configs map[string]string) (int64, int64, error) { if bitmap == nil { - return 0, errors.New("bitmap is not available") + return 0, 0, errors.New("bitmap is not available") } meta, err := blkup.repoWriter.ReadMetadata(blkup.ctx, snapshot.RootObject.ID) if err != nil { - return 0, errors.Wrapf(err, "error reading snapshot metadata for %s", snapshot.Description) + return 0, 0, errors.Wrapf(err, "error reading snapshot metadata for %s", snapshot.Description) } if len(meta.SubObjects) != 1 { - return 0, errors.Errorf("unexpected number of bdev object (%d) for snapshot %s", len(meta.SubObjects), snapshot.Description) + return 0, 0, errors.Errorf("unexpected number of bdev object (%d) for snapshot %s", len(meta.SubObjects), snapshot.Description) } sourceSize, err := getSourceSize(snapshot) @@ -169,25 +169,25 @@ func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bi } if sourceSize > meta.SubObjects[0].Size { - return 0, errors.Errorf("unexpected size (%v vs. %v) for bdev object %s", meta.SubObjects[0].Size, sourceSize, meta.SubObjects[0].Name) + return 0, 0, errors.Errorf("unexpected size (%v vs. %v) for bdev object %s", meta.SubObjects[0].Size, sourceSize, meta.SubObjects[0].Name) } if sourceSize > dest.size { - return 0, errors.Errorf("dest dev(%s) size is too small (%v vs. %v)", dest.path, dest.size, sourceSize) + return 0, 0, errors.Errorf("dest dev(%s) size is too small (%v vs. %v)", dest.path, dest.size, sourceSize) } reader, err := blkup.repoWriter.OpenObject(blkup.ctx, meta.SubObjects[0].ID) if err != nil { - return 0, errors.Wrapf(err, "error opening bdev object %v", meta.SubObjects[0].Name) + return 0, 0, errors.Wrapf(err, "error opening bdev object %v", meta.SubObjects[0].Name) } defer reader.Close() size, err := blkup.restoreData(reader, dest.dev, bitmap, sourceSize, dest.path) if err != nil { - return 0, errors.Wrapf(err, "error restoring bdev object %s to volume %s", meta.SubObjects[0].Name, dest.path) + return 0, 0, errors.Wrapf(err, "error restoring bdev object %s to volume %s", meta.SubObjects[0].Name, dest.path) } - return size, nil + return size, sourceSize, nil } func (blkup *blockUploader) backupObject(dev *os.File, dest udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (udmrepo.ID, int64, int64, error) { @@ -441,6 +441,8 @@ func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bit return written, errors.Wrap(writeErr, "error writing data") } + blkup.progress.UpdateProgress(&uploader.Progress{BytesDone: totalLength, TotalBytes: totalLength}) + return written, nil } @@ -576,7 +578,7 @@ func restoreWriteProc(ctx context.Context, dest *os.File, resultChan chan readRe result.resetBuffer(list) - progress.UpdateProgress(&uploader.Progress{BytesDone: written, TotalBytes: totalLength}) + progress.UpdateProgress(&uploader.Progress{BytesDone: result.offset + length, TotalBytes: totalLength}) } result.resetBuffer(list) diff --git a/pkg/uploader/block/uploader_test.go b/pkg/uploader/block/uploader_test.go index 3b8930476..340ee710f 100644 --- a/pkg/uploader/block/uploader_test.go +++ b/pkg/uploader/block/uploader_test.go @@ -623,7 +623,7 @@ func TestBlockUploaderRestore(t *testing.T) { repoWriter.On("ReadMetadata", mock.Anything, udmrepo.ID("root-id")).Return(nil, errors.New("meta not found")) iterMock := cbtmocks.NewIterator(t) - _, err := blkup.Restore(udmrepo.Snapshot{RootObject: udmrepo.ObjectMetadata{ID: "root-id"}}, destInfo{}, iterMock, nil) + _, _, err := blkup.Restore(udmrepo.Snapshot{RootObject: udmrepo.ObjectMetadata{ID: "root-id"}}, destInfo{}, iterMock, nil) require.Error(t, err) assert.Contains(t, err.Error(), "meta not found") }) @@ -685,7 +685,7 @@ func TestBlockUploaderRestore(t *testing.T) { iterMock.On("Next").Return(uint64(0), false) iterMock.On("BlockSize").Return(uint(1048576)) - written, err := blkup.Restore(snap, dest, iterMock, nil) + written, _, err := blkup.Restore(snap, dest, iterMock, nil) require.NoError(t, err) assert.Equal(t, int64(1048576), written) }) @@ -709,7 +709,7 @@ func TestBlockUploaderRestore(t *testing.T) { dest := destInfo{size: 4194304, path: "/dev/target"} iterMock := cbtmocks.NewIterator(t) - _, err := blkup.Restore(snap, dest, iterMock, nil) + _, _, err := blkup.Restore(snap, dest, iterMock, nil) require.Error(t, err) assert.Contains(t, err.Error(), "unexpected size (1048576 vs. 2097152) for bdev object bdev") }) @@ -733,7 +733,7 @@ func TestBlockUploaderRestore(t *testing.T) { dest := destInfo{size: 512, path: "/dev/small"} iterMock := cbtmocks.NewIterator(t) - _, err := blkup.Restore(snap, dest, iterMock, nil) + _, _, err := blkup.Restore(snap, dest, iterMock, nil) require.Error(t, err) assert.Contains(t, err.Error(), "dest dev(/dev/small) size is too small") }) 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 185/194] 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 { From 57da5e5f051ab3a3b341895d43ec36ed8b2b8259 Mon Sep 17 00:00:00 2001 From: Jay Sawant Date: Tue, 11 Aug 2026 17:32:47 +0530 Subject: [PATCH 186/194] docs: fix grammar and typos in backup-restore-windows (#10228) Signed-off-by: Jay2006sawant --- .../docs/main/backup-restore-windows.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/site/content/docs/main/backup-restore-windows.md b/site/content/docs/main/backup-restore-windows.md index 9d700f472..b0b8ef1ae 100644 --- a/site/content/docs/main/backup-restore-windows.md +++ b/site/content/docs/main/backup-restore-windows.md @@ -17,38 +17,38 @@ For volume backups, CSI and CSI snapshot should be supported by the storage. As mentioned in [Image building][2], a hybrid image is provided for all platforms, so you don't need to set different images for linux and Windows clusters, you can always use the all-in-one image, e.g., `velero/velero:v1.16.0` or `velero/velero:main`. In order to backup/restore volumes for stateful workloads, Velero node-agent needs to run in the Windows nodes. Velero provides a dedicated daemonset for Windows nodes, called `node-agent-windows`. -Therefore, in a typical cluster with linux and Windows nodes, there are two daemonsets for Velero node-agent, the existing `node-agent` deamonset for linux nodes, and the `node-agent-windows` daemonset for Windows nodes. -If you want to install `node-agent` deamonset, specify `--use-node-agent` parameter in `velero install` command; and if you want to install `node-agent-windows` daemonset, specify `--use-node-agent-windows` parameter. +Therefore, in a typical cluster with linux and Windows nodes, there are two daemonsets for Velero node-agent, the existing `node-agent` daemonset for linux nodes, and the `node-agent-windows` daemonset for Windows nodes. +If you want to install `node-agent` daemonset, specify `--use-node-agent` parameter in `velero install` command; and if you want to install `node-agent-windows` daemonset, specify `--use-node-agent-windows` parameter. ## Resource backup restore -Resource backup/restore for Windows workloads are done by Velero server as same as linux workloads. +Resource backup/restore for Windows workloads is done by the Velero server the same as for linux workloads. -Since Velero server is running in linux nodes only, all the existing plugins, i.e., BIA, RIA, BackupStore plugins, could be started by Velero in a cluster with Windows nodes. However, whether or how the plugins are functional to Windows workloads are decided by the plugins themselves. -It is recommended that plugin providers do a well round test with Velero in Windows cluster environments, and: +Since Velero server is running in linux nodes only, all the existing plugins, i.e., BIA, RIA, BackupStore plugins, could be started by Velero in a cluster with Windows nodes. However, whether or how the plugins are functional for Windows workloads is decided by the plugins themselves. +It is recommended that plugin providers do a thorough test with Velero in Windows cluster environments, and: - If they need to support Windows workloads, make the necessary modification to ensure their plugins work well with Windows workloads - If they don't want to support Windows workloads, or part of the Windows workloads, they need to ensure the plugins won't cause any failure or crash when they process the undesired Windows workload items ## Volume backup restore -Below are the status of supportive of Windows workload volumes for different backup methods: -- CSI snapshot data movement: block volumes (i.e., vSphere CNS Block Volume, Azure Disk, AWS EBS, GCP Persistent Disk, etc.) are full supported; file volumes (i.e., vSphere CNS File Volume, Azure File, AWS EFS, GCP Filestore, etc.) are not tested or officially supported. This is the same with linux workloads -- CSI snapshot backup: block volumes (i.e., vSphere CNS Block Volume, Azure Disk, AWS EBS, GCP Persistent Disk, etc.) are full supported; file volumes (i.e., vSphere CNS File Volume, Azure File, AWS EFS, GCP Filestore, etc.) are not tested or officially supported. This is the same with linux workloads -- native snapshot backup: supported as same as linux workloads +Below is the support status for Windows workload volumes for different backup methods: +- CSI snapshot data movement: block volumes (i.e., vSphere CNS Block Volume, Azure Disk, AWS EBS, GCP Persistent Disk, etc.) are fully supported; file volumes (i.e., vSphere CNS File Volume, Azure File, AWS EFS, GCP Filestore, etc.) are not tested or officially supported. This is the same for linux workloads +- CSI snapshot backup: block volumes (i.e., vSphere CNS Block Volume, Azure Disk, AWS EBS, GCP Persistent Disk, etc.) are fully supported; file volumes (i.e., vSphere CNS File Volume, Azure File, AWS EFS, GCP Filestore, etc.) are not tested or officially supported. This is the same for linux workloads +- native snapshot backup: supported the same as for linux workloads - file system backup: at present, NOT supported -For volume backups/restores conducted through Velero plugins, the supportive status is decided by the plugin themselves. +For volume backups/restores conducted through Velero plugins, the support status is decided by the plugins themselves. ### CSI snapshot data movement During backup, Velero automatically identifies the OS type of the workload and schedules data mover pods to the right nodes. Specifically, for a linux workload, linux nodes in the cluster will be used; for a Windows workload, Windows nodes in the cluster will be used. You could view the OS type that a data mover pod is running with from the DataUpload status's `nodeOS` field. -Velero takes several measures to deduce the OS type for volumes of workloads, from PVCs, VolumeAttach CRs, nodes and storage classes. If Velero fails to deduce the OS type, it fallbacks to linux, then the data mover pods will be scheduled to linux nodes. As a result, the data mover pods may not be able to start and the corresponding DataUploads will be cancelled because of timeout, so the backup will be partially failed. +Velero takes several measures to deduce the OS type for volumes of workloads, from PVCs, VolumeAttach CRs, nodes and storage classes. If Velero fails to deduce the OS type, it falls back to linux, then the data mover pods will be scheduled to linux nodes. As a result, the data mover pods may not be able to start and the corresponding DataUploads will be cancelled because of timeout, so the backup will be partially failed. Therefore, it is highly recommended you provide a dedicated storage class for Windows workloads volumes, and set `csi.storage.k8s.io/fstype` correctly. E.g., for linux workload volumes, set `csi.storage.k8s.io/fstype=ext4`; for Windows workload volumes set `csi.storage.k8s.io/fstype=ntfs`. Specifically, if you have X number of storage classes for linux workloads, you need to create another X number of storage classes for Windows workloads. -This is helpful for Velero to deduce the right OS type successfully all the time, especially when you are backing up below kind of volumes belonging to a Windows workload: +This is helpful for Velero to deduce the right OS type successfully all the time, especially when you are backing up the following kinds of volumes belonging to a Windows workload: - The PVC is with Immediate mode - There is no pod mounting the PVC at the time of backup From 48f2095dc9f704edf05d2798d34c743838e03402 Mon Sep 17 00:00:00 2001 From: Ralthos <161431341+Ralthos@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:14:02 +0530 Subject: [PATCH 187/194] Site: document artifact download failures with an in-cluster s3Url (#10231) troubleshooting.md covers SignatureDoesNotMatch but not the other way a log or results download fails: the pre-signed URL carries the s3Url host, which for an in-cluster Service name does not resolve on the client. The backup or restore itself is unaffected, which makes the error easy to misread. The fix, publicUrl, is documented only under exposing Minio, so this links there instead of duplicating it. Signed-off-by: saral --- changelogs/unreleased/10231-Ralthos | 1 + site/content/docs/main/troubleshooting.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 changelogs/unreleased/10231-Ralthos diff --git a/changelogs/unreleased/10231-Ralthos b/changelogs/unreleased/10231-Ralthos new file mode 100644 index 000000000..415cc3d27 --- /dev/null +++ b/changelogs/unreleased/10231-Ralthos @@ -0,0 +1 @@ +Add a troubleshooting entry for artifact downloads failing when the BackupStorageLocation s3Url is only resolvable inside the cluster diff --git a/site/content/docs/main/troubleshooting.md b/site/content/docs/main/troubleshooting.md index dc692771c..df5d71753 100644 --- a/site/content/docs/main/troubleshooting.md +++ b/site/content/docs/main/troubleshooting.md @@ -77,6 +77,19 @@ Here are some things to verify if you receive `SignatureDoesNotMatch` errors: * Make sure your S3-compatible layer is using [signature version 4][5] (such as Ceph RADOS v12.2.7) * For Ceph, try using a native Ceph account for credentials instead of external providers such as OpenStack Keystone +### `velero backup logs` or `velero describe` fails with `no such host` + +Downloading artifacts uses a pre-signed URL built from the `s3Url` in your `BackupStorageLocation`. If that address is only resolvable inside the cluster, such as a Kubernetes Service name, the Velero client cannot fetch the artifact even though the backup or restore itself succeeded: + +``` +Warnings: +``` + +The backup or restore is unaffected. Only the download of its log or results file fails. + +To fix this, give the location a `publicUrl` that your client can reach. See [Expose Minio outside your cluster][26] for the Minio case; the same applies to any object store addressed by an in-cluster name. + ## Velero (or a pod it was backing up) restarted during a backup and the backup is stuck InProgress Velero cannot resume backups that were interrupted. Backups stuck in the `InProgress` phase can be deleted with `kubectl delete backup -n `. @@ -250,3 +263,4 @@ Please refer to [Issue 9007](https://github.com/velero-io/velero/issues/9007) fo [11]: /plugins [12]: https://kubernetes.io/docs/concepts/configuration/secret/#editing-a-secret [25]: https://kubernetes.slack.com/messages/velero +[26]: contributions/minio.md From 3da77b9469381d8caf08b96ffb04b6c3f872d111 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 11 Aug 2026 08:55:33 -0700 Subject: [PATCH 188/194] Site: add conference talks to resources and LinkedIn to community page (#10180) * site: add conference talks to resources page and LinkedIn to community page Add a Conference Talks section to the resources page with Velero-related talks from KubeCon EU 2026, KubeCon India 2026, KubeCon China 2024, KubeCon EU 2023, and DevConf.IN 2025. Includes YouTube embeds where recordings are available and sched.com links for all talks. Add LinkedIn page link to the community page alongside existing Twitter and Slack links. Signed-off-by: Shubham Pampattiwar * site: add Open Source Summit NA 2022 Velero talk to resources Add the Velero talk by Orlin Vasilev and Scott Seago from Open Source Summit North America 2022 with YouTube embed and sched.com link. Signed-off-by: Shubham Pampattiwar --------- Signed-off-by: Shubham Pampattiwar --- site/content/community/_index.md | 1 + site/content/resources/_index.md | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/site/content/community/_index.md b/site/content/community/_index.md index 043941685..cc1b63818 100644 --- a/site/content/community/_index.md +++ b/site/content/community/_index.md @@ -12,6 +12,7 @@ If you are ready to jump in and test, add code, or help with documentation, foll You can follow the work we do via our [GitHub milestones](https://github.com/velero-io/velero/milestones) and the project [Roadmap](https://github.com/velero-io/velero/wiki/Roadmap). * Follow us on Twitter at [@projectvelero](https://twitter.com/projectvelero) +* Follow us on LinkedIn at [Project Velero](https://www.linkedin.com/company/project-velero) * Join our Kubernetes Slack channel and talk to over 800 other community members: [#velero-users](https://kubernetes.slack.com/messages/velero-users) * Join the Velero community meetings Bi-weekly community meeting alternating every week between Beijing Friendly timezone and EST/Europe Friendly Timezone diff --git a/site/content/resources/_index.md b/site/content/resources/_index.md index 5f05a811a..2a3e6217a 100644 --- a/site/content/resources/_index.md +++ b/site/content/resources/_index.md @@ -3,7 +3,35 @@ title: Resources description: Velero Resources id: resources --- -Here you will find external resources about Velero, such as videos, podcasts, and community articles. +Here you will find external resources about Velero, including conference talks, videos, podcasts, and community articles. + +## Conference Talks + +### KubeCon + CloudNativeCon + +* **KubeCon EU 2026 (Amsterdam)** - [Snapshots Gone Wild: Taming Multi-PVC Chaos with VolumeGroupSnapshot](https://kccnceu2026.sched.com/event/2CW53/snapshots-gone-wild-taming-multi-pvc-chaos-with-volumegroupsnapshot-shubham-pampattiwar-scott-seago-red-hat) - Shubham Pampattiwar & Scott Seago, Red Hat + + {{< youtube pLmRkRO6O6E >}} + +* **KubeCon India 2026 (Mumbai)** - [Sponsored Demo: Cloud Native AI: Model Management with Harbor & Velero](https://kccncind2026.sched.com/event/2OdTx/sponsored-demo-cloud-native-ai-model-management-with-harbor-velero-dhruv-tyagi-broadcom) - Dhruv Tyagi, Broadcom + +* **KubeCon China 2024 (Hong Kong)** - [The Challenges of Kubernetes Data Protection - Real Examples and Solutions with Velero](https://kccncossaidevchn2024.sched.com/event/1eYb8/the-challenges-of-kubernetes-data-protection-real-examples-and-solutions-with-velero-kuberneteszha-velerozha-kang-reji-wenkai-yin-broadcom-bruce-zou-shanghai-jibu-tech) - Wenkai Yin, Broadcom & Bruce Zou, Shanghai Jibu Tech + +* **KubeCon EU 2023 (Amsterdam)** - [Disaster Recovery: Bringing Back Production from Scratch in Under 1 Hour Using KOps, ArgoCD and Velero](https://kccnceu2023.sched.com/event/1Hye8/disaster-recovery-bringing-back-production-from-scratch-in-under-1-hour-using-kops-argocd-and-velero-andre-jay-marcelo-tanner-ada-support) - Andre Jay Marcelo-Tanner, Ada Support + + {{< youtube oPQW99NiV_0 >}} + +### Open Source Summit + +* **Open Source Summit NA 2022 (Austin)** - [Velero - The Cloud Native Backup for Kubernetes](https://ossna2022.sched.com/event/11Nu9) - Orlin Vasilev, VMware & Scott Seago, Red Hat + + {{< youtube DKMW69OSI7c >}} + +### DevConf + +* **DevConf.IN 2025** - From Chaos to Control: Mastering Kubernetes Backups and Restore with Velero - Aziza Karol & Prasad Joshi + + {{< youtube Bo4lSle0J7k >}} ## All community meetings From ae1d869c776b86c5e38fe03e9f35ad9ff48a984e Mon Sep 17 00:00:00 2001 From: Ralthos <161431341+Ralthos@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:28:19 +0530 Subject: [PATCH 189/194] Add printer columns for Backup and Restore CRDs (#10200) * Add printer columns for Backup and Restore CRDs kubectl get backup and kubectl get restore fall back to the default NAME/AGE table because neither type declares printer columns, while Schedule and BackupStorageLocation do. Anything reading the API without the velero binary cannot see a backup's phase, error count or timing. Printer columns were added in #2881 and reverted in #3652 as a workaround for #3600, a CRD install error that was never root-caused. Schedule regained columns in 2022 and BackupStorageLocation has them today, with no recurrence. Only fields expressible as plain JSONPath are included. Expiration is deliberately omitted: kubectl renders a date column as time elapsed, so a future expiration prints , which covers every backup that has not yet expired. Fixes #10199 Signed-off-by: saral * Rename changelog name to pass changelog check Signed-off-by: Tiger Kaovilai --------- Signed-off-by: saral Signed-off-by: Tiger Kaovilai Co-authored-by: Tiger Kaovilai --- changelogs/unreleased/10200-Ralthos | 1 + config/crd/v1/bases/velero.io_backups.yaml | 23 ++++++++++++++++++++- config/crd/v1/bases/velero.io_restores.yaml | 23 ++++++++++++++++++++- config/crd/v1/crds/crds.go | 4 ++-- pkg/apis/velero/v1/backup_types.go | 5 +++++ pkg/apis/velero/v1/restore_types.go | 5 +++++ 6 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/10200-Ralthos diff --git a/changelogs/unreleased/10200-Ralthos b/changelogs/unreleased/10200-Ralthos new file mode 100644 index 000000000..b54e0c7f8 --- /dev/null +++ b/changelogs/unreleased/10200-Ralthos @@ -0,0 +1 @@ +Add printer columns for Backup and Restore CRDs so kubectl shows status, errors, warnings and timing diff --git a/config/crd/v1/bases/velero.io_backups.yaml b/config/crd/v1/bases/velero.io_backups.yaml index 9695d3001..c20418c90 100644 --- a/config/crd/v1/bases/velero.io_backups.yaml +++ b/config/crd/v1/bases/velero.io_backups.yaml @@ -16,7 +16,27 @@ spec: singular: backup scope: Namespaced versions: - - name: v1 + - additionalPrinterColumns: + - description: Backup status such as New/InProgress + jsonPath: .status.phase + name: Status + type: string + - description: Total number of errors logged during the backup + jsonPath: .status.errors + name: Errors + type: integer + - description: Total number of warnings logged during the backup + jsonPath: .status.warnings + name: Warnings + type: integer + - description: The time the backup was started + jsonPath: .status.startTimestamp + name: Started + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: description: |- @@ -688,3 +708,4 @@ spec: type: object served: true storage: true + subresources: {} diff --git a/config/crd/v1/bases/velero.io_restores.yaml b/config/crd/v1/bases/velero.io_restores.yaml index aa4e167af..e12ea9b4f 100644 --- a/config/crd/v1/bases/velero.io_restores.yaml +++ b/config/crd/v1/bases/velero.io_restores.yaml @@ -16,7 +16,27 @@ spec: singular: restore scope: Namespaced versions: - - name: v1 + - additionalPrinterColumns: + - description: The name of the backup this restore is from + jsonPath: .spec.backupName + name: Backup + type: string + - description: Restore status such as New/InProgress + jsonPath: .status.phase + name: Status + type: string + - description: Total number of errors logged during the restore + jsonPath: .status.errors + name: Errors + type: integer + - description: Total number of warnings logged during the restore + jsonPath: .status.warnings + name: Warnings + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: description: |- @@ -597,3 +617,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 0f645d6c1..7887493a6 100644 --- a/config/crd/v1/crds/crds.go +++ b/config/crd/v1/crds/crds.go @@ -30,13 +30,13 @@ import ( var rawCRDs = [][]byte{ []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccW͎\xdb6\x10\xbe\xfb)\x06鵲\x1b\x14-\n\xdf\x12\xa7\x01\x82&\xc1\xc2\x0e\xf6NSc\x99Y\x8aTɡS\xf7\xe7\u074b!%[\x96hk\xbd\x87\xa2\xbc\x89\x9c\xffo\xe6#U\x14\xc5L4\xea\x11\x9dW\xd6,A4\n\xff 4\xfc\xe5\xe7O\xbf\xf8\xb9\xb2\x8b\xc3\xebٓ2\xe5\x12V\xc1\x93\xad\xd7\xe8mp\x12\xdf\xe1N\x19EʚY\x8d$JAb9\x03\x10\xc6X\x12\xbc\xed\xf9\x13@ZC\xcej\x8d\xae\xa8\xd0̟\xc2\x16\xb7A\xe9\x12]4\u07b9>\xfc0\x7f\xfd\xf3\xfc\xa7\x19\x80\x115.a+\xe4Sh\x1c6\xd6+\xb2N\xa1\x9f\x1fP\xa3\xb3seg\xbeA\xc9\xd6+gC\xb3\x84\xf3A\xd2n=\xa7\xa8\xdfFC\xeb\xce\xd01\x1ei\xe5\xe9\xb7\xec\xf1G\xe5)\x8a4:8\xa1s\x81\xc4c\xbf\xb7\x8e>\x9f\x9d\x15\xc0\"\xe9H\x99*h\xe1F\xba\xec\xdbK\xdb\xe0\x12\xa2f#$\x963\x80\xb6\b\xd1R\x01\xa2,cY\x85~p\xca\x10\xba\x95ա6'?_\xbd5\x0f\x82\xf6K\x98w\x85\x9fK\x87\xb1\xe6_T\x8d\x9eD\xddDٮ\x96o*l\xbf\xe9\xc8\xceKA86\xc6E\x9d\x9fc\xfdrl\xf0\xc2ʹF\xd0;K\x16=9e\xaa\xd9Y\xf8\xf0:\x95B\xee\xb1\x16\xcbV\xd66h\xde<|x\xfcqs\xb1\r\xd08۠#\xd5\x153\xad^g\xf6v\x01J\xf4ҩ\x86b\xdf\xfc]\\\x9c\x01\xb0\x83\xa4\x05%\xb7(z\xa0=v5Ʋ\x8d\t\xec\x0eh\xaf<\xc3\xe6УIM\xcb\xdb\u0080\xdd~EI\xf3\x81\xe9\r:6\xc3\xd8\a]rg\x1f\xd0\x118\x94\xb62\xeaϓm\x0fd\xa3S-\b=AD\xd1\b\r\a\xa1\x03~\x0f\u0094\x03˵8\x82C\xf6\t\xc1\xf4\xecE\x05?\x8c\xe3\x93u\b\xca\xec\xec\x12\xf6D\x8d_.\x16\x95\xa2n^\xa5\xad\xeb`\x14\x1d\x17q\xf4\xd46\x90u~Q\xe2\x01\xf5«\xaa\x10N\xee\x15\xa1\xa4\xe0p!\x1aU\xc4DL\x9c\xd9y]~\xe7\xda\t\xf7\x17nG@\xa7\x15\x87\xec\x0exx\xea@y\x10\xad\xa9\x94\xe2\x19\x05\xde\xe2ҭ\x7f\xdd|\x81.\x92\x84T\x02\xe5,:\xaaK\x87\x0fWS\x99\x1d\xba\xa4\xb7s\xb6\x8e6є\x8dU\x86\xe2\x87\xd4\n\r\x81\x0f\xdbZ\x11\xb7\xc1\xef\x01=1tC\xb3\xab\xc8i\xb0E\b\r\x8fN9\x14\xf8``%j\xd4+\xe1\xf1?ƊQ\xf1\x05\x83\xf0,\xb4\xfaL=\x14N\xe5\xed\x1dt,{\x05\xda!sn\x1a\x94\x8c,\x17\x97U\xd5N\xc94S;\xeb@\x8c\xe4/+\x95\xa7\x00^\x89D7d\x9d\xa8\xf0\xa3M6\x87BSm\xc7\xebm\xceP\x171\xd3V\xe2\x04\xcc\vf\f\xd2^P\x8f\fH(s\xe2\x94l\x927\x90\x89\xe8\bf\n#\x8c\xc4\xf7\xb1\x1f\x8d\x8a\x9f\xfd\x99\n?\xfc\xcb#з\xa0,\x18\xcb\xef\x8a\xe7VF\xf7\xa0,\x013\xb9\x15\xec\xef5lM\x8c\xc4N95\xa0-e\f(A9\xd9S^\xc1\xc2\x12\xe5\brA\x0fD\x81\xed\x93T\xa2\x05\x0f\x1b\xe8\xe3q\xfc,\x15\x10&6\xf2\x8a\xec\x8c)\xf5\xd5\xeb\xd7[f\x82\xe6e\xb2(*\xc1\xcc\xe15*\x11[WF*\xfd:\x87=\xf0ךm\x97Te;f \xb3l~MK\xb6DD\x04j\xdfe\x91\xff[\x10\x0f\xdd\xe9\xd6\x1c\xac\xd8j\xa3\x98ض>\xa0\xe6\xcc`\x8fU*'\x8c\x0e\x94C\xb1\xe1\x82\xfdɒ\xee\xf3\xfbۻ\xb6\xa02\xed\x99Ғ\xd7!\xfeXj2\xb1\x01\xe5\xdam\x94,\x10&\x88܉*\xca9g \f\xd1պ`Ɗ\xc1o\x15h\xab\x03\xf2\x18\xec5Z'\xb2\x06R\x95\xb9\x15\xe3\xe3\n+A\xaei\x01\xfc\x9ajxa^Y\xae\xe8\xa5eB\x12\xb7\xda6\xf7\xb8\xb2#o\xebC0\x9d\x03\xacu\x86嶄\xac\xa3h\xb6\x15۰̩\xd3F\xaa\xc6\xee8\x1bإP\\\xf5mq\xb5\xef\xec؎\xbeD\ab+\x86\xceA\x93\x9d|\f\xd6\xc6\"lE\xce\x02\x84\x9cT\xe5\x82<2\xb3\xeb\x01%\xa4\x94Z\xb35\a\xafw\x84\x89\x8cW\xb9\x15\xc9\x0f\x15\xe7h\xccV\"SPXs\xc1\x8fYM\b\x88\xaa\xe8\x0fv\x89\xad#?\xb7`\xf5\xbe\x0e0ЖL\xb3[AK\xbd\x93\xe6\x8e\x15 +3A\xa0\x9e\x12\xdar}\xbb:\x82Ң\x9ee$\x1a\xf3JCn\xad\xd9#e\x06\x99y}\xbb\"\xf7H\xd7\xd0\x1a\xady\xa5\x89\xa9\x94\xb0\xea\x13\xe9\xeb3\xd0\xfcp'\x7f\xd1@\xf2\n\xb5>S\x80\x02\xb2 k\xd8X\x15Q`\xdb\xdbO\xa0\x94\x15\x1a\x8d\x03\x90U\xcf\n\xdbr\xb7\x03+t\xb4\xe2\xc6\x1b\x10\xa6ɛ?\x93\x82\x89\xca\xf4tp\x94\x9aV:\n\xb9\au\n\x11\xdfQC\x7f\xb6\x8d\x8fh\x87\"\x87P-\xf1֞\x8e\xeb\x03~\x8c\xa9\x81+\xabM\v\"\xd3\xe4\xe2\x82HE.\x9c\xd3r\xb1p\xa0=\xda\xd6\x1d2K&\xda}=2\xceCo\xf3\x88\xe0\x80:\xc6\xea;\xf9A;\xa5:\x89&\x03\xb0Z$z܁ف\"\xa5\xac]\x82\r\xe3@\xf4A\x1b(<\x81\xc24\xeb\xf1\x89\xf4\x84ƅs\x0fB[\xfazD\xfaȋ\x8as\xba\xe6pE\x8c\xaa`\x806k)9P1A\x9cϠ\r\xcb\xceA\x1a\a)B\x18\xe5?t(\x80^\x05}\x00B#\xa0=ͬ\xfb\xc2y\x8b\xb0]\xaaD\xc7T*\xc8\xec\xb4v\xe5\xa7K\x06\x1c\xa7h!\t\x97b\v\xca\xf5n\xad_\x100\x05V\xe0rbg\"\x05\xdcN\xb7dS\xd9I\xea\x92X-\x1f\x94\x01&\xb4\x01\x1a\x11\xce'\xf0\a\xbeX+\r\xf9\xb5\xf3Lo\xad\x83\x9d\x87\x05GoZI\xe1\xd3\xfbQ\x88\xde}\xe1,C/\xd9;\xc4Kt\xeccb\xdax1v\x8a\xc2U\x87e\xa5\x1fv㞌\xda\x05\r\xc66\xba\xf8\xd3\xc5\x029\xdc\xed\xb5ۇ&TAM\x96d\xfb\tEi\x0e\xfd\xda\xcc@\x11\xa1\xe2\xa8=I\xe4'U\x8a\x1e\x06\xb8Y/\x90\xce\xc8\xcf!\x98G\x1c\x15\xa1\xda\v\xf3\xf4\xb8\xdf\x7ff\xae\x9e\x87\x8f\x1a\x03\x05\x94\t\xcb?\xbbf\xef\xb0O\xbb\x05\xae%\x9b\x90&\x02\xcf\xf9w\x90\xe3\xdau\x84[\xbf\x13\xb1\xce\"\xf3CB^˖\x17\xde\x7fHJ\xed\xa4|\x98\xa2\xce\x0f\xb6N\xb3j$\x19\x06\xa4\xc8\x1avtϤ\xf2\xa87S-|\x81\xac2Q\xad\xa7\x86\xe4l\xb3\x01e\xe1\x94;\xaaA\xbb8\xc20A\x86\xd77\xa4eF\xa2\x1f\x8f\xf0h\x18iل\x98\x0f\r\xdd\xfa\x11dzd(v\xa0\xd6\xcd\xc6\xc98g{\x96W\x94\xe3\xbcLE\xe6\xf0\xa1\xf5\xb8bVf\x84ɽ1G%\xd3\x15\xe7\x10\x04\xa4,\x93:KI)\xc0\xfa\xbe\x85]\x1b\xf4\xab\x0ec\xbe\xa6\xd6W\x91C\xd8\x13d\x96\xaa8h\xdfU\x8endc3\x16\rS0RC8]\x03'\x1a8dF\xaa8E\xa6\xf8\xecJ\x8a\x11\x1c d\xc4\xf2uW\x1c\r\x02# \t.\xe5v,\xdb9W\xcf\n\x11\xc2!\xb9\x04\xeb\xf0\x19B˒G\xa6\x8b\xa6\x8c2\xdfw2\xa6\xebM\x99\xd0\xfacx1\xfdoJ\x82\xcdlJ\x94\xb4\x8d~u)[\x8bC|m۔\x7fN\xc2\x06\xcb\x7f\x82Ўh?\xc1\xb0Y\xb2L\x0fʭ\xa5*\x03}i\xdd)\xf4t\x16\x84\x99\xf0\xeb\x94&t|\xae^4\xb1C\x84\xaf\x9b7\xf3\x85>\x915):\xf1L\x8c\xa9\xbb\xf8\a\xe4\vN\x19\xb7~\xc6H\xe6\xc9O\xedV\v\xc265\xd1\xf3\x05\xd90n@\x1dQ\xff$S\x1f8s\x0eb\xa4\xccz\x04\xf77L\xb6{\xffź`\xba\xd9\xe4K\xa4\xcbqc\xe7\xc8\x06o\xbf;=O\xc0%\x18\xe7g.\xea\xaa/q\xc5\xd4\xfe\x05]\xab\xb7\x1f\xdf\xc5\xd7W\xed\x92 y=D&\x94Ε\xb7G\x18\xb5\xc7\xe7]\xf8\xf0\x05}\xa0z\x01\xe4b\xd6\vB\xc9\x03\x1c\x9c\xebB\x05\xb1\xfc\xa1\xa1rB\xf7\np\xd3\n\xe5\xec\x01\x0e\b&\xbe\v\xd5/\xa9\xd2\xe0\xca\x03\x1cR\xaa\x1d\xd1Ў\x89i\xbf\xbbf\xe9d\x7f@B\xe0\xe6C\xaa\x18\xb8\xe2U!\xb2\xe7\x13/\x89\xb6$\x94@\xfb\x13\xd0L\x12\x95v\x1f\xedm\\\x94\x80\xef\xb4\xe3\xa5\u0558\x1d+Ѭb\xc4An\x92\x19\xea\xca=\xe5,\xaf;r:\xb2\x12\v\xf2Q\x1a\xfb\xcf\xfb/L\xfb\x9d\xdew\x12\xf4Gi\xf0\x97g\xa1\xa8\x1b\xf8s\xd23\xec\xfcX\x84\x9c\x95\xb7\x04k\xefU\xba9\xcdJ[M{\xa6\xc9J\xd8\xe5\x8a#IbW\xb8-\xed\xbas\x1d\x15\x95\xc6mF!\xc5҅mb=yzK\xd5!\xf7\x93;\xf5\x1d\xde\xd9\xc9\xc2}q\x9b\xe3\x9cf\x90\x87m\x1bܵ\xa5\x06\xb6,K\xec\xaf\x00\xb5\x05RZ\x13\x9e&\x11\x89\x86\xd5c3O|\xd2f\xefv\xf9\xb2|\xa8\x93 \x96v\xcaYz\bF\x16\t4\xf0\xb6;\x9f\xc6giu6\xa1V\x90\x84ɪ\x03\x9b\xba\xc3US\x88\xf2\x04r\xe0,\x8e.\xce$wi\x9ec\x8a\x10\xe573f\x94\x19\xb20\xd74\xb4\xc6\xee\xa6\xe0\x82\xe2V\xcb\xffؙ\x16\xb5\xe9\xffHI\x99җ\xe4-\xe6\xfcp\xe8|\xf3A\xb3\x16\x98\x84.1g\xc7\xcaϞr;\xf7[\x03.\bp\xe7\t\xc8M\xcf/Z\x90ǝ\xd4nڮ7q.\x1e\xe0\xe0v\x0e'\xbbl\x1b\x99\x8b\x95\xb8p>D\xcf`\xd4\x0e\x87\x14\xfc@.\xf0\xdb\xc5S\\\xa9DIM\xac\xd6\x11т\x96i\x12\x8a9W\xa9\x8e\xba]\xb0\x06'\xc46\xacs\x89\xac\x93=\x86m\x92\x88\x96RG6\xf4\a\x862!\xbc7R\x1b\x17/\xeb\xf8\xccр\x9a\fA4B7.\xc1K\xaa\x90\x8dc\x8d\xf2T\xe8\xb7]\xeev\xa0\xc1\xefW\xf8\xc0\x9c\x03jWv\x17\x8d~;k\x7f\xe1\xf6K\xb0\x13\x9a\xa1ǂmK%3\xd0ѽ\xec\xa6$\xcc\x17\x91l\x916\xeeȗ\xbaU\x92\xcbY\x19\x0f\x81\x86\x92\xee\xf2ZB\xcc\\/\xbc\xff\xd2\n\x88Zݷ\x7fO\xc9\xd8\xdcq\x11̶,\nz\x9cǕ4\xc4k\xd72h\x83\a\xe4\x16\x1fj[\xa1%H\x9d\xcbk\x01\xfc\x1a\x1c\x85\x82\x89\x15v@\xde<\x83c\xe1mh,\xe9$VNse\xafC'\rw\xea\x1f\x9c*\x97\x12\xb7\n\x14t\x98\u05cf\xaa\xa3\x1f*\xa4i\x05$f\xb8\x9b\xa5̿\xd3dÔ6\xed!\xe8\x814\x95(\x98\x99\v/\xf1^\xa9\x93\xd6]\x9f\\ˣD2\x9f\xbf\xe6\b\x93\x889\xee/\x01a\x1b\xc2\f\x01\x91\xc9J`\x00\xc7\xea1v\xe1\x88\xeb,,KU\x924\xed'\x83\xb9h\xb1\xb2DIab4\xd2Ӯ\xfe\x81\xb2~\xc2Z\xac\xccd\x9b\x19\xcaf\x8b\x95\xd3t\"\xa4\xba\xb53\x16\v\xfa\x85\x15UAhay\x84\x939+\xa0\xcb\xf4&\x01ζ\xc0i\xc2H\xab1%\a\x03>\x89-q\f\x99\x14\x9a\xe5PO\xae^\x10\xa4 \x94l(\xe3\x95J\xb4\x80\xb3\xc8;g)\xe2-\xc1\xf9\xd6\x18i\x9d/\x91\x14\t\xd1\xdcD_q\xdc\x1a\x97*\xdd\xe3\x9br\xb3\x14\xcc\xf7\xb2J\xc5$\xa6\a\x9e\xd9\xd1\xf2\t\x95T\x1c\xbeyZ\xa9C\xfd\xe6i\x8d\x95o\x9e\xd6D\xf9\xe6i}\xf3\xb4Rj~\xf3\xb4\xbeyZ\xed\xf2/\xe1iM\x8d\xc8\x1dx\x1c\xf889\x8a\x84\xad\xea\xb1!\x8e\xc0\xf7\xc9\x15>\a\xfcI\xb9\x98\xab8\xa8H\xe2\xff@Zw\xcch5\x93G\x9d\x9ci\xb5&ȼ;\x7f5\xe1J>!\xeb>tz\xbe\xac\xfb\xd5(\xc43e\xdd\xfbaO\xfb\xd8'\xe5\xdc\a\xa2\xcc\xcb\xce^\xf8D\x8d\x02h\b\xab\xbbm\xf8\x18^C\x122\xd1\xff\v'\xe6\xf6\xb2\xc6\xce(\x1fϞş,#Q\x96^\xfc\xe9\xe2\xeb#\xffy\b>H\xe2>\xed\xfc\x01\xf0\bT\xbb\x02m\xa7\x85u\xb3\xf0\xbeN1>\x8bܦf\xe2\xd7D\x8c\xc0\xea\x8a\xe4\x11\x15\xbfV[`\xa0\xf8T\xfa\x19\xe9\t'VW\x118IgV\xa9>\x88l\xa7\xa4\x90\x95\xf6Q\t\v\xebm\xe6N\xfc\a\x901a\x8dj\xf8\x7f\x90\x9d\xac\"\x99\xe0#\xe4\x9b\xc8\b\x9cF\xbe\x93\x1c\xe87\xa1\xc1\xd0\xfd\x9b\xcb\xee\x17#}\xaa\xe0\xd0\x19\xe7\xc7\x1d\b\xdca\x17\xdb\xf6\x01\x80pa\x83\xbf\xb9\xe0X\xc0\"\x80\xa4\"\x82q'y\xf5u\x0fm\xb9#\x9fJ\x17{\x9a\xedw\x8c\xc7TҒ\tON!\xec\xa6\b\x0e\xf8\xa5sw\xbb\xcfrd\xe2wI\r\x9c\x9f\x10\x98\x12\x11\x9bH\xfe;!\xe5/1\xb7\xf8\xc9\xdb\xf3)I}sV\xccϖ\xc0w\xfe\xb4\xbd$\xfaL\xa7\xe8͡γ\xa7\xe3\xbd`\x12\xdeˤ\xde%&ܝ/s>-\x1e{R\xe6\xd8t\xe8`8in2Un2\xb40\x85\xd8l\x94&S\xe0\xe6$\xbeMr'M\xcd^,\xb5\xed\xc5\x12\xda^6\x8dmT\x8aF?\xceIT\x8b\xdf\xdbC&'\xdb\u07bdj\xbd\n甸\xe4Xܠ\xd2\xf1\x97R\x8eS\xd9&U\xc7\xdd>i=\xf8\xe9\b\x86\x15\xd4\xe0\x8a\xbe\x90O_Tܰ\x92\xe3\xc6\xef\x9e\xe5\xd1\xe0\x88\xd9\xc1\xa1\xbe\xf0\xe3W\x89Ge\xfd\r6\x9f>\xd7Zvy\xb42\xa1\x9a<\x02\xe7\x84\xc6\xec@\x0f\xf3\xcc]\xad\x95\xc9%\xd8\xf9\xd3Z\x13\x7f\x91\x89\xbf\x8fk\xe1\xd4\x13O\x03\xe3,\\\xc4BbT\f\xdfz38ѥ\xd8Ǟ\xc7\xed\xd6\r\xf8\xdbo\x15\xa8\x03\xc1{wj\xbf\xac9\xb4\xe6\r\x89\xb6\v\xc7`ڼ\x99\x1d\x8a\xf7\xf7\x16)\x8d\xe9!o\x85\xf3\x12\x8eǃm\xacMk\x16a\xd6P\x8b\u0605S$(X\xbf\xb9\x90u\xebH\xb3)\x87>\xf5t\xd7\xf3.\xc9\xe6/\xca&\xbd\xa0tO\xf5w:\xb5u\xcai\xad\xb4\x84\x85\xc9\xd3YϵD\x9bZ\xa4%\xfb\xa5i\xa7\xaf\xe6mn>\xe3i\xab\xe78e\x95H\xa9\x94SU\xf3\xe8\xf4\x02\xa7\xa8^\xf4\xf4\xd4K\x9d\x9aJ>-\x95\x94\x92\x93\xbck\x9d\x9aRs\xe2\xf1\x9f\xe9=\xe9\xf1\xd3O\t\xa7\x9e\x12v\xab\xa7\x91<\x01\xbd\x84SM\xf3N3%\xf0,U\x15_\xf0\xd4\xd2\v\x9eVz\xe9SJ\x13\x925\xf1y\xdei\xa4\x93\xb7X\xa4\xcaA\x8dnS\xa5J\xe1\xa8\xfc\xa5\xacm\xba\x039ڟ\t\xb7\x14\xdaZ\x1d\x7f\x19\xa7\a\x7fs,\xde\x11<\xb4\xddj%\xad\xe5mt\xf6\xce\x1a\xf7\xa7\xebL\xfa\x8b\x83\xdd\xf6\x9a\x86\x92*\xbc\x8cz}p\xe97ѩ\xf9=\xcdvG\xd0wT\x93\x8dT\x055\xe4\xa2ް|\xed\x80ۿ/.\t\xf9 \xeb\x1c\x8e\xf6=B\x9a\x15%?\xd8\x15\n\xb9h78M\x02\xa2\xd2\x16z\xbb\x91\x9ce\x11\xdf-z\x97\x94\xabܻ\xdc\x03o\xb8\xca\xda)\x0e\xa5\xad\x18w\xdd\xd0\xcd\xeb^ٹ\x91\x9c\xcbǹ\xb1\x8a\x92\xfd\x05/i\x7fB4\xeb\xed\xcd\na\x04\xf1\xc0[\xdf\xebd\xb2\x1a\x9b5\xd8i\xb9\xc1sH\xf7W\x9b\x0e\xc4n^f\xfb\xb6c\xc8\xdd\xc5\xd6\xc1-\xf0\xa63\x93ֺܬ\xdc8\x86z\xb12CŁH\xcc\x002;\xa6\xf2eI\x959\xb8ĒEg\fa.\x1d\x8bF\r\xce\x1e\xfd˺\xa3\xe4\rwt\xe3\x8e\xea\xa1\xecnR\x1f\xd3\xee\x94q\f\x9f\xb6\x9c\xe1\x8d\xfe\ars\x8fk\xb4ڴy\x15\xf5k\xb4\x10*\v\x9b\xd7\x118\xbe\xc1\xf7\xe7O\xa5\xd3F*\xba\x85\x9f\xa4\xbb4}\x8a\xed\xddڝ\xcb\xf4\xbd\xd7\x13\xf2]\x83\xd2\xc4.\f\xf6\u05f7\x1f\x01kr\xd4{\x970\xdbQμV\xda\x18~\n\xdf\xef\xee~rX\x19V\xc0\xe5\xbbʥgX\x9b\xa8\xc1\x928`\xeb \xad\xed\x7fw\xf2\x11/+\x8e\xc71\xc3#\x18\r2\n09\x1eS&g\xa1T\x95\\\xd2\x1cԵ\x14\x1b\xb6\x9d\xc0\xee\x97N\xe5\xa3i6\xc3\x1f=r\xf5\x1c\x15\xe0\x9f9g\xc2\xfa<\x9c\x03\xff\xc08h7\xac\x04\x03|\xd3oU\xdb\xe3\xaaX;\x1fnc?\xd6\x1d\f\xccq\x0e-\fE\x97\xa0\xac\x17\xe5\x82֕\x0e\xb2:\x8cx\xc3\x11&\fl\xa1\xbf\n\x1c\xb1\xc0\xee\x16l\x9c>\x839\xc1\xb5̏\xb1\xf8V\a\xf9\xfb\xe1\x96G\x9cl\x85\xbcb7\x04:'\xe4\xe6\xfeZ\x93J\xe4\x18.\xbe\xff\xcb\xed,\xa9\xdbwn\xdc\x0f\xda:eT\xef\xe3\xadZ\xceq\xcb^8\xefXn\"\b\f\xc1i=\xec\xf2Ȍ\xbfh\xec\xbc7\xc3\x0e-y\x86\x9e\xac\xc0\xa7\b\xa6\x1f\xadp/\x16\xf8\xa7n\xbc:V\n\xafu\xf5\xaf\x19\xe05\xa8Ox\xb7\xa2\x93\xac\xa6\xdf\x1a\x03Eib\xbeƴ9\xfc~\f`\xed\xa7ICyK+i\xa8\x10\xf3\xb4\xf5Adc\x89p\xde\x1a\x8dpsL\x1fc\x04\xb8\xf6\xe77\xceF\x80\x1a\xe0\x10\x01t\x95e\xa0\xf5\xa6\xe2\xfcP\x1f\x1f\xf9J\xa8\xf1\x812~>R8h\x83\x82`\xd1\x1b\x854\x89\xb0OO\a\x91\aM\x0fG\xab\xe6\x91\xc2s\xc1gojC\x8b\x93\x1e\x98\xb8\xee\x83\xc17\x98T\xdeJ\x02\xa5\xf5ةn\xd8\x1f\x9b\\\x1ap\xae%.\xb2,4\xc8\t\xecA\x10;;;\x12\x87\xe7\xc5fB\xf1'r\xdd\f\x17\xe6\xbb\x10\n\x89\xbe4E|\xb4C\xe3\x8bF\xdf\xe9\x1a&\xe6\xb6\xe2;,}\"\xf4\x9d_\x17\xad\xb8\xb2\xde?,-\x88Ӽ֡Wh\xba\xf3\xc2ӌ\xdc\xf5\xedj\b\xdc)&\xae\xffL\xcd\x13ո\x8f\xee\x93LZ\x1f\xddY\x06-\x02\xb1\x96\xf1\xf3㎪~\xda%\xf4\xd8\xd29\x1cY8\xf3G9\xf7\a3\vКn\xc3\xed\xf3\x8fv\xe9\xb1\x05\x01.<\xe76O\"@\x9bS|ݻם\xca\xd0\xccT\xd4w\x10\x12\x92[\xb5\xbeӄ\xcb\x18T|\x80\x86\x85\xa7\xdf\u009al&\xa1\xbe\x94L\xa5\xac\xe1\xde\xd7\x15-m\xd0\x13F\xee4\x8f\xf5\x01g[|\x8a\xcarnK՚na\x99I\xce\x01\xadu\x7f\\ϩ\xeb\xfe\xac\xe4g\xa0z\x12\xb5\x0f\xed\xba~\a\xd0q\xdbm|S\x97\x9e\x8fϱ\x19\xa6\xa0y\x19\xb17 \x89\x1d\xcfr\x94\x1d\x15\xa2\xcf\x06\xf6Gڮ\x1b\xb4Λe\x1f\xe7\xf5\xaf\x06.\x9a\x97\xc0\"\xe3,\xe8\xafR-H\xc1\x84\xfd\x87\x8a\xdcm\xe0\x85Ƴƿ\x93\xf2\xe16\xe2\xc4\xf6\x06\xffC]\xb1\xd9\xea`\xc2\r\x1b\x0f\xb8\xaee\xe5w\xdfk\x876\xbe\xad\x82/\t\x9cy\xb9\x890G\xe6\x83\x1e:\x83\x11\xdd\x1f:\x90&\xa7\x02\xd7\xf3\x00\xac\xdb\xf04\x1d\xe7\x87\xc51\xe4\xa3g0\x1bح\x97\x16\xbc\x1b\xd0ܟ0\xd0Qؑ\x8a\x02\xa9/\xeah\x1b\xf4SV\xbd\x9e\xccC\xced\x8f\xc6?4\xb5\x87\xe8\xe8\x86\xd9r\xf7\x06\x10\xec8\x81\xe7]\xb0\xe3\xb3\x1a\x13\xc2\x7fc\xeb\xd4w-\xb4\x16n!Kl0J7\xf4B\xdfG\xe8oW,\xc9_+\xa8\"4X\x86\a\xedn\rU\xfd\x90\xaf;\xb6\x0f9ft\xa06F\xaa\xacč\x92[\x05\xba/\xacK\xf27\xca\f\x13\xdb\x0fR\xdd\xf0j\xcbħ\xe1#Jc\x95o\xa82\xcc\n\xbb\x1bOl\xa0LP\xce\xfe\x1e\xb3k\xed\x8fӀ\xae\a\x17XK\x920\x8c\xa1\x0f\xef\xc0\xfa\xb8\x83q\x81\xa8\t-=]O\xf1W\x02O\xa6lj\xedK4\xbeH\xe8\xf6\x92|\x94Q\xc3\xe0ӡX\x17\xa6u\xc9@\x9b%l6R\x19\xb7[\xbd\\\x12\xb6\t\xc1\aks0n\xe6\x1e\x1f%,\xb6\xcd\\'\x9a4\xd3\x17\x06\xbd\x15\xce\xc2x\xf5~A\x0fng\x8afYe=\xac\xd7\xdaP\x1eqp\x9ed\xf81\xca\xf3=>\xb4\xf9˓v\xf2Vm@\xfd\xa0#\xf6\xe3H\x8a\x97\x7f8\xaf\x8f[\x14A\x90GŌ\xb1>\x95\x1cI%\xf0\xa42ַ\xe2\x9chKꓢ\x8fę\xd1\xd5pJN\x1a\xcaw5\x94!\xf3\xec\xb1\xc6\x17%\xeb\xd7L}\xf6\x91\xafeٜ\xed\xa8\xd8\x0eި\xb0S\xb2\xda\xee\x82$\x0f8\xd3$\xaf\x00\x83\xb5hRtx)\xdaTJ\xb4R\tF\x8e\xa9\x93 \f8\\\x9a=\u0eeb\xee%f\xff\x04\xf7k\xfff\xcbr\xa3d\xb1\xf4\xfdb,u\xe1w\xf2\x15\x93\xd6s1\xbb(Չ\xf3\xda\xfd\xb3\b(\te\t\x82P\xed{N\xb8\xd9\xea\xe4i\xea7;5\xdcH\xcd\x12\xbc\xfd(\xc7\xff\xda\x06\x10\x18^\x86\xbf\xbb\xcc\xf0+\x18\xec3\x86\xc7'\x7fe\x00\xec\xa90n9QO\x91\x17n\x12\xbb\x98\xb5\x90\xd1vb{R\x90\xe6\xb6\x03a\">\x83\xdd\xc5Yt\xeb\xd35\xdc\xc5e\xd7\xfe\xd9\xd8\x1a\xf0\x82h&\xc2K\xe6.\xf5\xc3I\x7ft'P\xe0ÚRų1\xc7\x03.]\x84^6ֲ\xaf=\x89\xf7'/\xc5\xef\x8f`\x1c\x1dB\xc7wT\xeb*a\xf9\xfc\a\x16\xdb\x0f\xc04\xde̢\xf2\xc7\xdf\xfdp\xf9>i\xa9\x17\xa7\xc8\xd8\xca\x0f\x17u\xc3K\xb8\ueee97\x1c\xac\xb6i\x80\xee\xa2r\x96\xce\xed\xcf\x18M;g(-\xbc\xd9\x7f\x9eX\xd2\xfe\x8cA\xb4g\x8b\xa0\x9d\x17\xe5G\x8a\x0f[\x9f\xa4\xb5\x7f\xf3m#!4\x0f\xf6\xdcA\xb4V\f-\f\xfcE\xa3h\xd19\xb7\xf7#\xda\xe9\xbce-|O\xfe\x97\xff\x0f\x00\x00\xff\xff\x93\xf6\x83\\\xa3\x84\x00\x00"), + []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xec}_s#)\x92\xf8{\x7f\n¿\x87\xd9ݐ\xe4\xed\xf8\xdd]\\\xf8\xcd\xe3\xee\xdeQ\xecL\xb7\xb7\xed\xf1<\xa3\xaa\x94Ę\x82\x1a\xa0\xe4\xd6\xee\xedw\xbf \x81\xfaK\xa9\x90,{z\xf7\x9a\x97n\xab !\xff\x90\x99$\t\xcc\xe7\xf37\xb4d\x0f\xa04\x93\xe2\x8aВ\xc1\x17\x03\xc2\xfe\xa5\x17\x8f\xff\xad\x17L^\xee\u07beyd\"\xbf\"7\x956\xb2\xf8\fZV*\x83w\xb0f\x82\x19&ś\x02\fͩ\xa1Wo\b\xa1BHC\xed\xcf\xda\xfeIH&\x85Q\x92sP\xf3\r\x88\xc5c\xb5\x82U\xc5x\x0e\n\x81\x87\xaew\x7f^\xbc\xfd\xaf\xc5\x7f\xbe!D\xd0\x02\xaeȊf\x8fU\xa9\x17;\xe0\xa0\xe4\x82\xc97\xba\x84̂\xdc(Y\x95W\xa4\xf9\xe0\x9a\xf8\xee\xdcP\xbf\xc7\xd6\xf8\x03g\xda\xfc\xb5\xf5\xe3\x8fL\x1b\xfcP\xf2JQ^\xf7\x84\xbf\xe9\xadT\xe6c\x03mNV\xf4\xd1}abSq\xaaB\xfd7\x84\xe8L\x96pE\xb0zI3\xc8\xdf\x10\xe2\xf1\xc1\xe6sB\xf3\x1c)D\xf9\xadb\u0080\xba\x91\xbc*D\r<\a\x9d)V\x1a\xa4\x80\x1b\x1eц\x9aJ\x13]e[B5\xf9\bO\x97Kq\xab\xe4F\x81v\x83$\xe4W-\xc5-5\xdb+\xb2p\xd5\x17\xe5\x96j\xf0_\x1d\x01\xef\xf0\x83\xff\xc9\xec\xedH\xb5QLlb}\xdfKC9\x11U\xb1\x02E䚀RRi\xc2\xe5f\x039\xc9+ێ\x98-4\xc8LJ\xe1\xdau\xc6\xf1\xbe\xfd\x93\x1b\x87%\xc5\x06T\xca@\x9e\xa8\x12LlN\x18Jh\xd9\x19\xcc/\xdd\x1f\xa7\x87\xb3\x05bX\x01\xad\x0e\xc9\x13ՖI\xca \xc3\xe3\x9d\xe3\xf7{V\x806\xb4(\xfb|i5u#ȩ\x01\xdf}\vV\x98V\x8bL\x01Ψ8\xc0\xeb\rā\xb9ϻ\xb7N~\xb3-\x14\xf4\xcaה%\x88\xeb\xdb\xe5\xc3\xff\xbf\xeb\xfcL\xba\xd8\xffϼ\xfe\x9d\x04\xf1d\x9aP\xf2\x80s\x8f(\xaf\n\x88\xd9RC\x14\x94\n4\b\xa3\x91Z\x19-M\xa5\xc02\xf1\xaf\xd5\n\x94\x00\x03\xba\x05/\xe3\x956\xa0PށPC()%\x13\x860\xe1H\xfe\x87\xeb\xdb%\x91\xab_!3\x9aP\x91\x13\xaa\xb5\xcc\x185\x90\x93\x9d\x9dG\xe0\xda\xfeqQC-\x95,A\x19\x16\xa6\xaf+-\r\xd7\xfa\xf5\x10\xae\xb6X\xf2\xb8V$\xb7\xaa\x0e\x1cZ~\x82C\xee)j\xf13[\xa6\x1b\xf4\x91U\xf6g*\xfc\xf0\x17=\xd0w\xa0,\x18\xabm*\x9e[\r\xb9\x03e\t\x98ɍ`\x7f\xafakb$vʩ\x01mPP\x95\xa0\x9c\xec(\xaf`f\x89҃\\\xd0=Q`\xfb$\x95h\xc1\xc3\x06\xba?\x8e\x9f\xa4\x02\xc2\xc4Z^\x91\xad1\xa5\xbe\xba\xbc\xdc0\x13\xf4~&\x8b\xa2\x12\xcc\xec/Q\x85\xb3Ue\xa4җ9\xec\x80_j\xb6\x99S\x95m\x99\x81̲\xf9\x92\x96l\x8e\x88\b\xd4\xfd\x8b\"\xff\x7fA\xd8\x1b\xb4\x8dd\x05\xa4*\xed$\xcd\xfb\x15\x96\x82\xdc\xd0\x02\xf8\r\xd5\xf0ʼ\xb2\\\xd1s˄$n\xb5-~\xbf\xb2#o\xebC0\xdc#\xacu\x8a宄\xac3\xd1l+\xb6f\x99\x9bNk\xa9\x1a\xbd\xe3\x14q\x97B\xf1\xa9o\x8b\xab}o\xc7\xd6\xfb\x12\x1d\x88\xad\x18:\aM\xb6\xf2)h\x1b\x8b\xb0\x159\v\x10rR\x953\xf2\xc4\xccv\x00\x94\x90Rj\xcdV\x1c\xfc\xbc#Ld\xbcʭH~\xa88Ge\xb6\x14\x99\x82ª\v\xdeg5! \xaab8\xd89\xb6\x8e\xfc܂5\xf8:\xc2@[2\xcd\xee\x04-\xf5V\xa2\xa9\x92\x95\x99 \xd0`\x12\xdars\xb7\xecAiQ\xcf\x04\xfbYiȭ6{\xa2\xcc 3o\xee\x96\xe4\x01\xe9\x1aZ\a\xcf\xc7TJ\xd8\xe9\x13\xe9\xeb3\xd0|\x7f/\x7f\xd6\x10\x1c\x81`\x1agd\x05k;E\x14\xd8\xf6\xf6\x13\xfa\"օ2nXC2\x13\xb4\xef9\xaciōW L\x93\xb7\x7f&\x05\x13\x95\x19\xcc\xc1\x83Դ\xd2Q\xc8\x1d\xa8S\x88\xf8\x8e\x1a\xfa\x93mܣ\x1d\x8a\x1cB\xb5\xc4[y:\xae\xf6-\x7f$\x86\xd6r݂\xc84\xb9\xb8 R\x91\v\xe72_\xcc\x1ch\x8f\xb6u\xc6͜\x89v_O\x8c\xf3\xd0\xdbqDp@\x1dc\xf5\xbd\xfc\xa0ݤ:\x89&#\xb0Z$zڂق\"\xa5\xac]\x825\xe3@\xf4^\x1b(\x82\xc3\xe6ͬ\xc7'\xd2\x13*\x17\xce=\bm\xe9\xeb\x11\x19\"/*\xce\xe9\x8a\xc3\x151\xaa\x82\x11ڬ\xa4\xe4@\xc5\x04q>\x836,;\ai\x1c\xa4\ba\x94\xffС\x00z\x15\xf4\x11\b\x8d\x80\xf64\xb3\xee\v\xe7-\xc2v\xa9\x12\x1dS\xa9 \xb3f\xedʛK\x06\x1cM\xb4\x90\x84K\xb1\x01\xe5z\xb7\xda/\b\x98\x02+p9\xb1\x96H\x01\xb7斬+k\xa4\x16\xc4\xce\xf2Q\x19`B\x1b\xa0\x11\xe1|\x06\x7f\xe0\x8b\xd5Ґ\xdf8\xcf\xf4\xce.\xef\xf2\xb0\xdc\x1d\x98\x95\x14>\xbd?\bѻ/\x9ce\xe8%{\x87x\x8e\xcbʘ\x986^\x8c5Q\xb8浬\xf4\xc3nܓ\x83zA\x83\xb1\x8d.\xfet1C\x0ew{\xed\xf6\xa1\tUP\x93%Y\x7fBQ\x9a\xfd\xb063PD\xa8xP\x9f$\xf2\x93*E\xf7#ܬ\x97\xe7g\xe4\xe7\x18\xcc\x1eGE\xa8\xf6\xca<\xed\xf7\xfb\xef\xcc\xd5\xf3\xf0Qc\x98\x8a2a\xf9Ǚ6\x1d\xf6i\xb7\xc0\xb5d\x13\xd2D\xe09\xff\x0er\\\xbb\x1e\xe0\xd6\xefD\xac\xb3\xc8\xfc\x98\x90ײ\xe5\x85\xf7_\x92R[)\x1f\xa7\xa8\xf3\x83\xadӬ\x1aI\x86\xe1P\xb2\x82-\xdd1\xa9<ꍩ\x85/\x90U&:\xeb\xa9!9[\xafAY8\x18\xba\xd3.\x8e0N\x90\xf1\xf5\ri\xa9\x91\xe8\xc7\x1e\x1e\r#-\x9b\x10\xf3\xb1\xa1[?\xa2o%C\xb1\x03\xb5n6\x1a\xe3\x9c\xedX^Q\x8ev\x99\x8a\xcc\xe1C\xebqŴ\xcc\x01&\x0f\xc6\x1c\x95LW\x9cC\x10\x90\xb2L\xea,%\xa5\x00\xeb\xfb\x16vm0\xac:\x8e\xf9\x8aZ_E\x8eaO\x90Y\xaa\xe2\xa0}W9\xba\x91\x8dΘ5L\xc1H\r\xe1t\x05\x9ch\xe0\x90\x19\xa9\xe2\x14\x99\xe2\xb3+)Jp\x84\x90\x11\xcd\xd7]q4\b\x1c\x00Ip)\xb7e\xd9ֹzV\x88\x10\x0e\xc9%X\x87\xcf\x10Z\x960\xe7\x80ڕ\xddE3\xbf\x9d\xb6\xbfp\xfb%\xd8\t\xcd\xd0c\xc1\xb6\xa5\x92\x19\xe8\xe8^vS\x12\xecE$[\xa4\x8d{\x1ds\xa4n\x95\xe4rV\x0e\x87@CIwy-!\x8e\\/\xbc\xff\xd2\n\x88ڹo\xff\x9e\x92\xb1c\xc7E0\u05f7(h?\x8f+i\x887\xaee\x98\r\x1e\x90[|\xa8M\x85\x9a Ֆ\xd7\x02\xf858\n\x05\x13K쀼}\x01\xc7\xc2\xeb\xd0X\xd2I\xac\x9c\xe6\xcaބN\x1a\xee\xd4?\xb8\xa9\\J\xdc*P\xd0a\xde0\xaa\x8e~\xa8\x90\xa6\x15\x908\xc2\xdd,e\xfe\x9d&k\xa6\xb4i\x0fA\x8f\xa4\xa9D\xc1\x1c\xb9\xf0\x12\x98\xbd|\x02q?\xb9\x96\xbdD2\x9f\xbf\xe6\b\x93\x889\xee/\x01ak\xc2\f\x01\x91\xc9J`\x00\xc7\xcec\xec\xc2\x11\xd7iX\x96:I\xd2f?\x19\xcdE\x8b\x959J\n\x13\a#=\xed\xea\x1f(\x1b&\xac\xc5ʑl3c\xd9l\xb1rڜ\b\xa9n\xed\x8cł~aEU\x10ZX\x1e\xa11g\x05t\x99\xde$\xc0\xd9\x16h&\x8c\xb43\xa6\xe4`\xc0'\xb1%\x8e!\x93B\xb3\x1cj\xe3\xea\x05A\nBɚ2^\xa9D\rx\x14y\x8fY\x8axMp\xbe5FZ\xe7s$EB47\xd1W<\xac\x8dK\x95\xee\xf1M\xb9Y\n\x8e\xf7\xb2J\xc5$\xa6\a\x9e\xd9\xd1\xf2\t\x95T\xec\xbfyZ\xa9C\xfd\xe6i\x1d*\xdf<\xad\x89\xf2\xcd\xd3\xfa\xe6i\xa5\xd4\xfc\xe6i}\xf3\xb4\xda\xe5\xff\x84\xa755\xa29\xc6\xd0F>N\x8e\"a\xab\xfa\xd0\x10\x0f\xc0\xf7\xc9\x15>\a\xfcY\xb9\x98\xcb8\xa8H\xe2\xffHZwLi5ƣNδ\xb3&ȼ;\x7f5\xe1J>#\xeb>tz\xbe\xac\xfb\xe5A\x88gʺ\xf7Þ\xf6\xb1Oʹ\x0fD9.;{\xe6\x135\n\xa0!\xac\xee\xb6\xe1cx\x8dI\xc8D\xff\xaf\x9c\x98;\xc8\x1a;\xa3|\xbcx\x16\x7f\xb2\x8cDYz\U000672ef\x8f\xfc\xe7!\xf8(\x89\x87\xb4\xf3\a\xc0#P\xed\n\xb4\x9d\x16\xd6\xcd\xc2\xfb:\xc5\xf8,r\x9b\x9a\x89_\x131\x02\xab+\x92=*~\xad\xba\xc0@\xf1\xa9\xf4\x16\xe9\x19'V\x97\x118IgV\xa9ދl\xab\xa4\x90\x95\xf6Q\t\v\xeb:s'\xfe\x03Ș\xb0Fg\xf8\x7f\x90\xad\xac\"\x99\xe0\a\xc87\x91\x118\x8d|'9\xd0oB\x83\xa1\xbb\xb7\x8b\xee\x17#}\xaa\xe0\xd8\x19\xe7\xa7-\b\xdca\x17\x9b\xf6\x01\x80pa\x83\xbf\xb9\xa0/`\x11@R\x11\xc1\xb8\x93\xbc\xfa\xba\x87\xb6ܑO\xa5\x8b=\x1d\xedw\x1c\x8e\xa9\xa4%\x13\x9e\x9cB\xd8M\x11\x1c\xf1K\x8f\xdd\xed>ˑ\x89\xdf%5\xf0\xf8\x84\xc0\x94\x88\xd8D\xf2\xdf\t)\x7f\x89\xb9\xc5\xcfޞOI\xea;f\xc5\xfcb\t|\xe7O\xdbK\xa2\xcft\x8a\xde1\xd4y\xf1t\xbcWL\xc2{\x9dԻĄ\xbb\xf3eΧ\xc5cO\xca\x1c\x9b\x0e\x1d\x8c'\xcdM\xa6\xcaM\x86\x16\xa6\x10;\x1a\xa5\xc9\x14\xb8c\x12\xdf&\xb9\x936\xcd^-\xb5\xed\xd5\x12\xda^7\x8d\xed\xa0\x14\x1d\xfcxL\xa2Z\xfc\xde\x1e2il\a\xb7\xfa\r*\x9cS\xe2\x92cq\xa3\x93\x8e\xbf\xd6\xe48\x95mRu\xdc\xed\x93փ\x9fz0\xac\xa0\x06W\xf4\x95|\xfa\xa2↕\x1c7~w,\x8f\x06G\xcc\x16\xf6\xf5\x85\x1f\xbfJ<*\xebo\xb0\xf9\xf4\xb9\x9ee\x8b\xdeʄj\xf2\x04\x9c\x13\x1a\xd3\x03\x03\xcc3w\xb5V&\xe7`\xed\xa7\xd5&\xfe\"\x13\x7f\x1f\xd7\xccMO<\r\x8cV\xb8\x88\x85Ĩ\x18\xbf\xf5f\xd4Х\xe8ǁ\xc7\xed\xd6\r\xf8\xdbo\x15\xa8=\xc1{wj\xbf\xac9\xb4\xe6\x15\x89\xb6\vǠڼ\x9a\x1d\x8b\xf7\x0f\x16)\x8d\xea!\xd7\xc2y\t\xfd\xf1`\x1b\xabӚE\x98U\xd4\"v\xe1\x14\t\x13l\xd8\\Ⱥu\xa4ٔC\x9fz\xba\xebe\x97d\xc7/\xca&\xbd\xa0tO\xf5w:\xb5u\xcai\xad\xb4\x84\x85\xc9\xd3Y/\xb5D\x9bZ\xa4%\xfb\xa5i\xa7\xaf\x8e\xdb\xdc|\xc1\xd3V/q\xca*\x91R)\xa7\xaa\x8e\xa3\xd3+\x9c\xa2z\xd5\xd3S\xafuj*\xf9\xb4TRJN\xf2\xaeujJ͉\xc7\x7f\xa6\xf7\xa4\x0f\x9f~J8\xf5\x94\xb0[=\x8d\xe4\t\xe8%\x9cj:\xee4S\x02\xcfR\xa7\xe2+\x9eZz\xc5\xd3J\xaf}JiB\xb2&>\x1fw\x1a\xe9\xe4-\x16\xa9rP\a\xb7\xa9R\xa5\xf0\xa0\xfc\xa5\xacm\xba\x03\xe9\xedτ[\nm\xad\x8e\xbf\x8c\xe6\xc1\xdf\x1c\x8bw\x04\x8fm\xb7ZIky\x1b\x9d\xbd\xb3\xc6\xfd\xe9:\x93\xfe\xe2`\xb7\xbd\xa6\xa1\xa4\n/\xa3^\xed]\xfaM\xd44\xbf\xa7ٶ\a}K5YKUPC.\xea\r\xcbK\a\xdc\xfe}\xb1 䃬s8\xda\xf7\biV\x94|oW(\xe4\xa2\xdd\xe04\t\x88J[\xe8\xedVr\x96E|\xb7\xe8]R\xae\xf2\xe0r\x0f\xbc\xe1*k\xa78\x94\xb6b\xdcuC7\xaf{e\xe7Zr.\x9f\x8e\x8dU\x94\xec/\xf8D\xc03\xa2Y\u05f7K\x84\x11\xc4\x03\xdf\x1c\xa8\x93\xc9jlV`\xcdr\x83\xe7\xd8\xdc_\xae;\x10\xbby\x99\xedێ!w\x17[\a\xb7\xc0\xab\xceLZ\xedr\xbbt\xe3\x18\xeb\xc5\xca\f\x15{\"1\x03\xc8l\x99\xca\xe7%Uf\xef\x12Kf\x9d1\x04[z(\x1a5j=\x86\x97uG\xc9\x1b\xee\xe8\xc6\x1d\xd5}\xd9ݤ\xee\xd3\xee\x94q\x8c\x9f\xb6\x9c\xc4[\xb5\x9c㖾pޱ\\G\x10\x18\x83\xd3z\xd8\xe5\x89\x19\x7f\xd1\xd8yo\x86\x1d[\xf2\x8c=Y\x81O\x11L?Z\xe1^,\xf0O\xdd\xf8\xe9X)\xbc\xd6տf\x80נ>\xe3݊N\xb2\x9a\xbe6\x06\x8a\xd2\xc4|\x8diu\xf8\xfd!\x80\xb5\x9f\xd6{\x80\x89\x86\n1O[\xefEv(\x11\xcek\xa3\x03\xdc<4\x1fc\x04\xb8\xf1\xe77\xceF\x80\x1a\xe0\x18\x01t\x95e\xa0\xf5\xba\xe2|_\x1f\x1f\xf9J\xa8\xf1\x812~>R8h\xa3\x82`\xd1;\bi\x12a\x9f\x9e\x0e\"\x0f3=\x1c\xad:\x8e\x14\x9e\v\xed\x17\xb1N\xa1\xc1\xcd\x10\f\xbe\xc1\xa4\xf2V\x12(m?\xfbU\xb3?f\\\x1ap\xae%.\xb2,4\xc8\t\xec@\x10k\x9d\x1d\x89\xc3\xe3vGB\xf1'r\x9d\x85\xeb>\x836\xf2\xd2\x14\xf1\xd1\x0e\x8d/\x1a}\xa7k\x98\x98ۊ\xef\xb0\f\x890t~]\xb4\xc2=-6\xb7 N\xf3Z\xc7^\xa1\xe9څ\xe7)\xb9\x9b\xbb\xe5\x18\xb8ST\xdc\xf0\x99\x9agN\xe3!\xba\xcfRiCt\x8fRh\x11\x88\xb5\x8c\x9f\x1fw\xf7:\xe0I\x97л\xf7\b\xd1\xe1\xc8\u0099?ʹ?\x98Y\x80\xd6t\x13n\x9f\x7f\xb2K\x8f\r\bp\xe19\xb7y\x12\x01ڜ\xe2\xeb\u07bd\xee\xa6\f\xcdLEyx\t\xd1%$\xb7j}\x87O\x12F\xa0\xe2\x034,<\xfd\x16\xd6dG\x12\xeaK\xc9T\xca\x1a\xee}]\xd1\xd2\x06=a\xe4N\xf3X\x1fp\xb6\xc1\xa7\xa8,\xe76T\xad\xe8\x06\xe6\x99\xe4\x1cP[\x0f\xc7\xf5\x92sݟ\x95\xfc\fTO\xa2\xf6\xa1]\xd7\xef\x00:n\xbb\x8do\xea\xd2\xf3\xf196\xc3T\xef=\xc8\u0380$v|\x94\xa3\xec\xa8\x10}6p8\xd2v\xdd0\xeb\xbcZ\xf6q^\xffj\xe0\xacy\t,2\u0382\xfe*Ռ\x14L\xd8\x7f\xa8\xc8\xdd\x06^h|\xd4\xf8\xb7R>\xdeE\x9c\xd8\xc1\xe0\x7f\xa8+6[\x1dL\xb8a\xe3\x01ו\xac\xfc\xee{\xed\xd0ƷU\xf0%\x813/7\x11\xe6\x01{0@g4\xa2\xfbC\aҤ)p=\x8f\xc0\xba\vO\xd3q\xbe\x9f\xf5!\xf7\x9e\xc1l`\xb7^Z\xf0n@s\x7f\xc2HGaG*\n\xa4\xbe\xa8\xa3\xad\xd0OY\xf5z2\x8f9\x93\x03\x1a\xff\xd0\xd4\x1e\xa3\xa3\x1bf\xcb\xdd\x1bA\xb0\xe3\x04\x9ew\xc1\x8e\xcfjL\b\xff\xad\xadSߵ\xd0Z\xb8\x85,\xb1\xd1(\xdd\xd8\v}\x1fa\xb8]1'\x7f\xab\xa0\x8a\xd0`\x1e\x1e\xb4\xc37a#\x9f\x1d\x911\xa3\x03gc\xa4\xca\xe0m\xe0\xf6\xc7_(3Ll>Hu˫\r\x13\x9fƏ(\x1d\xaa|K\x95aV\xd8\xddxb\x03e\x82r\xf6\xf7\x98^k\x7f\x9c\x06t3\xba\xc0\x9a\x93\x84a\x8c}x\a\xd6\xc7\x1d\x8d\vDUh\xe9\xe9z\x8a\xbf\x12x2\xa5Sk_\xa2\xf1EB\xb7\v\xf2QF\x15\x83O\x87b]\x98\xd6%\x03m\xe6\xb0^Ke\xdcn\xf5|N\xd8:\x04\x1f\xac\xce\xc1\xb8\x99{|\x94\xb0\xd86s\x9dhҘ/\fz+\xb4\xc2x\xf5~A\xf7ng\x8afYe=\xacKm(\x8f88\xcfR\xfc\x18\xe5\xf9\x1e\x1f\xda\xfc\xf9Y;y\xcb6\xa0a\xd0\x11\xfbq$\xc5\xcb?\x9c\xd7\xc7-\x8a ȓb\xc6X\x9fJ\x1eH%\xf0\xa42ַ\xe2\x9chKꓢ\x8fĩ\xd1\xe5xJN\x1a\xca\xf75\x941\xf5\xec\xb1\xc6\x17%\xeb\xd7L}\xf6\x91\xafeٜm\xa9،ި\xb0U\xb2\xdal\x83$\x8f8\xd3$\xaf\x00\x83\xb5\xa8Rtx)\xdaTJ\xb4R\t\x0e\x1cS'A\x18p\xb84{\xc4wW\xddK\xcc\xfe\x01\xf8K\xfff\xcb|\xadd1\xf7\xfdb,u\xe6w\xf2\x15\x93\xd6s1\xdb(Չ\xf3\xda\xfd\xb3\b(\te\t\x82P\xed{N\xb8\xd9\xead3\xf5\x9b5\r\xb7R\xb3\x04o?\xca\xf1\xbf\xb5\x01\x04\x86\x97\xe1\xef.3\xfc\n\x06\xfb\x8c\xe1\xf1\xc9_\x19\x00;*\x8c[N\xd4&\xf2\xc2\x19\xb1\x8b\xa3\x162\xddw\xd0Oڻ\xeb@\x98\x88\xcf\xf8g\xd9c\xa8\xdd\xf9t\rwq\xd9M\xffE\xf5\x19\xd1L\x84\x97\xcc]ꇓ\xfe\xe8N\xa0\xc0\x875\xa5\x8agc\x1e\x0e\xb8t\x11z\xddXˮ\xf6$ޟ\xbc\x14\x7f\xe8\xc1\xe8\x1dB\xc7wT\xeb*a\xf9\xfc\a\x16\xdb\x0f\xc04\xde̢\xf2\xc7\xdf\xfdp\xf9.i\xa9\x17\xa7ȡ\x95\x1f.\xeaƗp\xddwSo9\xd8٦\x01\xba\x8bʣ\xe6\xdc\xee\x8cѴs\x86\xd2\u009b\xfd\xe7\x89%\xed\xce\x18D{\xb1\b\xdayQ~\xa2\xf8\xb0\xf5I\xb3\xf6\x17\xdf6\x12B\xf3`\xcf\x1dDk\xc5\xd0\xc2\xc0_5\x8a\x16\xb5\xb9\x83\x1fQO\xe7-m\xe1{j\xffR\xad\x9a\xe7\x15\xc9?\xfe\xf9\xe6\x7f\x03\x00\x00\xff\xff\xc3!Ko6\x87\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xccZK\x93۸\x11\xbe\xebWt\xed\x1e\xf6\xb2\x94줒J\xe96\x96\x93*W\xc6\xf1\xd4hvr]\bhJ\xb0@\x80\x01@\xc9\xca㿧\x1a\x0f\x89\xe2C\x0f;q\u008b-\x12h\xf4\xf3\xeb\x0f\xc0\x14E1a\xb5|E\xeb\xa4\xd1s`\xb5\xc4/\x1e5\xfdr\xd3\xed\x1f\xdcT\x9a\xd9\xee\xedd+\xb5\x98âq\xdeT\xcf\xe8Lc9\xbe\xc7Rj\xe9\xa5ѓ\n=\x13̳\xf9\x04\x80im<\xa3\u05ce~\x02p\xa3\xbd5J\xa1-֨\xa7\xdbf\x85\xabF*\x816\b\xcfK\xef\xdeL\xdf\xfe~\xfa\xbb\t\x80f\x15\xcea\xc5\xf8\xb6\xa9\x9d7\x96\xadQ\x19\x1eENw\xa8К\xa94\x13W#\xa7\x15\xd6\xd64\xf5\x1cN\x1f\xa2\x84\xb4z\xd4\xfc]\x10\xb6\x8c\xc2\x1e\x93\xb0\xf0]I\xe7\xff<>\xe6Q:\x1f\xc6ժ\xb1L\x8d\xa9\x15\x86\xb8\x8d\xb1\xfe/\xa7\xa5\vX9\x15\xbfH\xbdn\x14\xb3#\xd3'\x00\x8e\x9b\x1a\xe7\x10f\u05cc\xa3\x98\x00$\xd7\x04i\x050!\x82\xb3\x99z\xb2R{\xb4\v\xa3\x9aJ\x1f\xd7\x12踕\xb5\x0fΌ\xb6@2\x06\xb25\xe0<\xf3\x8d\x03\xd7\xf0\r0\a\x0f;&\x15[)\x9c\xfd\xa2Y\xfe\x7f\x90\a\xf0\xd9\x19\xfd\xc4\xfcf\x0e\xd38kZo\x98\xcb_c\x8c\x9eZo\xfc\x81\fp\xdeJ\xbd\x1eR\xe9\x919\xffʔ\x14A\x93\x17Y!H\a~\x83\xa0\x98\xf3\xe0\xe9\x05\xfd\x8a\x1e\x02r\x11B\xf6\x10\xec\x99K\xeb\x00좔\xe0\xa3aMUo\xad3\xb5I\x15x\xedH\x89\xfaӛ\xa4}Kl\xce\xef)\xb7x\x14\xe9<\xab\xea3\xb9\x0fk\x1c\x13v\xe6\x8a\xf7X\xb2F\xf9\xb6\xa9\x14%\xd5\xce\xcbs\xb3j\xe4S\x11g\x9d\xad\xf8\xfe\xec]\\ue\x8cB\x16\xa5\xc4Q\xbb\xb71\v\xf9\x06+6O\x83M\x8d\xfa\xe1\xe9\xc3\xebo\x97g\xafa(\x91:EA\x81c\xad\xd8l\xd0\"\xbc\x86\xfa\x8bqsɴ\xa3L\x00\xb3\xfa\x8cܟ\x82X[S\xa3\xf52\x17K|ZX\xd4z\xdb\xd1\xe9\x9f\xc5\xd97\x002#\xce\x02A\xa0\x841\xafR\xfd\xa0H\x96\x83)\xc1o\xa4\x03\x8b\xb5E\x87:\xc2\x14\xbdf:)8\xed\x88^\xa2%1Tۍ\x12\x84e;\xb4\x1e,r\xb3\xd6\xf2\xefG\xd9\x0e\xbcI\xc9\xec\xd1y\b\x15\xaa\x99\xa2dm\xf0g`Zt$W\xec\x00\x16iMhtK^\x98\xe0\xbaz|\xa4j\x90\xba4s\xd8x_\xbb\xf9l\xb6\x96>#47U\xd5h\xe9\x0f\xb3\x00\xb6r\xd5xc\xddL\xe0\x0e\xd5\xcc\xc9u\xc1,\xdfH\x8f\xdc7\x16g\xac\x96E0DGH\xadď6a\xba;[\xb6W\xd2\xf1\t\x90zGx\b^c\xcaDQ\xd1\xc4S\x14\xe8\x15\xb9\xee\xf9\x8f\xcb\x17Ț\xc4HŠ\x9c\x86\xf6\xfc\x92\xe3Cޔ\xbaD\x1b\xe7\x95\xd6TA&jQ\x1b\xa9}\xf8\xc1\x95D\xed\xc15\xabJzJ\x83\xbf5\xe8<\x85\xae+v\x11\xba\x18\xac\x10\x9a:\x80Dw\xc0\a\r\vV\xa1Z0\x87\xdf9V\x14\x15WP\x10n\x8aV\xbb7w\aG\xf7\xb6>\xe4\x9e:\x12\xdaA4X\xd6\xc8\xcf\xeaN\xa0\x93\x96*\xc33\x8f\xa1\xba:\x0eJP1ޔ\xf33\f\x12\xf40\xceѹ\x8fF`\xf7KG\xe5\x87\xe3\xc03\x1dk\xb4\x95t\xa1\xbdBil\xb7\xf3\xb0#\x92\xb7\x9f\x8cx݀\x03\xa0n\xaa\xbe\"\x05<#\x13\x9f\xb4:\x8c|\xfa\xab\x95\xbe\xbf\xd0H \xe9\x89*.\x0f\x9a?\xa1\x95F\\1\xfe]g\xf8\xd1\x05\x1b\xb3\x872\xe4\xbf\xf6\xea@\xd8\xe5\x0e\x9a\xf7Q;?\x0fO\x1f2\x82\xc7\xdaJ\x85\x99|5\x85\x87TԦ\x847 \xa4#\"\xe1\x82о\xb3t\xa3\x02ј\x83\xb7\xcd]\xe6s\xa3K\xb9\xee\x1b\xdd\xe6Fc\x19sEt\xc7s\x8b\xb0\x12\xa1\x16eGm\xcdN\n\xb4\x05Շ,%O\x9a46v\x90R\xa2\x12=l\x1a\xad\xb2`\x8aEAE\xcdԕ\x18.\x8e\x03\x03\x93fR\xc7\f>\t\bXc\xabԚ\xb5G-\xb0\xdbm\x826&\x00\x9aC\x01{\xe97\x11)\xd5P\xdd\xc1\xc5ڣg\x8b\x87\xa1\xd7\x1d\xdd_6H#c\xe3Ep\xc8-\xfa\x90m\xa8(}(\x95\xa6\x00\x1f\x1b\x17\xb0\xb6\x8b\x13\xf9\t\x84/\xcf\xde\xe2\xa1\xefh\xb8\x16\xdcD\x85FT\x0e$j\x0e?\xfcpݤ^w\xcb\x0fQ\xf7l\xa8\xc5\x12-\xea\x1e\x9b\xc8\xcfK\xe8Q\x944\x94aX\x96Ƚܡ:\x84\x9eD\xe0\xf93\xac\x1a\x0f\xa2\xc1\x105Ʒ{f\x85\x03n\xaa\x9ay\xb9\x92J\xfa\x03H7\"\x9f)e\xf6(Rı\xaa\xfda\n\x1f\xb4\xf3LstG\x1eD\x1e\x8b\xa9\xc0t\x1c\x95\xaa8\x10:f\x8700\x8a\xaf\x8c\xf3\xc0\xd1R:\xaa\x03\xec\xad\xd1\xeb1c\a\xda!\xed\x01\xadF\x8f\xa1#\n\xc3\x1d5C\x8e\xb5w3\xb3C\xbb\x93\xb8\x9f\xed\x8d\xddJ\xbd.H\xc1\"\x81\xcf,\xec\xecf?\x86\x7f\xbe&\vL\x1dq\xe2\x86\xe4]\x86Z?\x10\xbd\xf5\x1b\x8c-b\x19s\xd0X \x02A\xa9]\xa5܍\xc8:TvC\xbc\xbc\xfd\xe4\x90\x0f\xf5\x8f-\xf6[\xc7\x05P\x01\xf8R\x9c|[T\xac.\xe2h\xe6M%\xf9\xa4km\xcc\xfb\xcb\xf8\x937+R\vɉܞ\xe3F\xdeĉ\xb3=̀\x1b\xba\xbb\x9c1\xb4\x1cvS47q\x85+\x1a\x7fj\x8f=m}#t\xa7\xfe\xef\xd0\x13\xeft\xa0\x91\xf8\x01\xb3}?\a\xc0\xe4FkB*o\x80\x1d\xdb\xc0O\xae\xdb\xff\xeeD\xcfU÷8\xe0\xf8\x9e)\xef\xc2\xc0\xec\xe38\x8dti\x1c\x86\xc6tM\r\xb8^\x11\x9c-\xd0ޢ\xcb(\xf2-\x1eH\u0091[0X<\xc0\xaa\xd1BaVu\xbfAM\xdb1Y\x1e\x88\xec\xbf<.\xb3c\x03\x01K[\xa7\xec\xde1 yO\xbb\x00JA1\x87_\x1c\xa6u\x9f\xb1\x04\xa9\x9dG\xd6#\xe9\xf1\x89\xbdq\x0e\xab\xc3\x00\u05fa\xd9A\xcfX~\xbb\x8f\x82\xae\xe4\xa1\xd4 8\xc6\xc4J\xb0\x92\xfa{\xde\x0f-\x1e\x02\xc4\x12\xdf \"}\xe6ґe\xeetth\xd0i\xf1\fdR\xc7\x02a\xd5\xd8\":\x1fR\x01\x8by9H䇃q\xb9.\xe0\x12\xb3\xe89\xfb>v1*\x13\x80\xdd\xc80\xe0z\xb2\xc0E\xa6\x017\xb0\x8d\x9e\x99\xa39\x05w\xb2\x0e\xf8\x0e\xcc\x03\xfe\xfb\xec\x03\xeef \xf0\xddY\bܖ)\x97\xd9\b|\x13#\xb9\xe0\x8bK\\\x05\xae\xf2\x15\xb8\xc8Y`\x94\xb7\xc05\xee\x02w\xf2\x17\bx\x82\xa5\xfcr\x032?\x85\x81\xb9\x93\xd6\xcco\xa8kH\x81\xc0\x06\xfaj<\xa1\x18q\xd0q\xd3\xfb)\x85\xef+\xfa\xee%\xd2\x17չ\x87\xf7e@\xbfB\x8c\x9eҰ\xa3\x17\xf2\xef\x04 \xe7\a c\x04mТ\xdd\xf1\xb4\xfdO\xf1X\x81\x0f\xa0\xf8\x992\xaf\xfd\x19\x17\x8e'\xf2\x99\xff\x10K\xa3Ͱ\xb1\x16]m\xb4\xa0\xb6w\xdb\xe1\xc4I\xe5\xff\xdc\x11\xc5pX\x8bs\xfa\xda\xf9\x96\xa3p\xd3\xf9\\\xb8߸\xfb\x84.\xde\xfa\xb4Ͽ\xccʡݵ\x0e\xe9:6~\x97\xb3\xb9\xc1\xce\xd6:\xb0#\xaa\xa4\xa1\xd1\xe1\xc8\"4\xad\xe9d`F\x9b\x17\xfa\xd0<\xa4\x03m\xf64\xb9%-v=\x13\xe9M8\xb4dZ\xa4\xe3b\xfa4 y/\x95\xa2\x1ef\xb12\xe4,\xd4^Zj\x96,\xb4\xb1\xddo\xa6o\xfewg\x81\x8a9\xbf\xc6Qq\xff\x99\xa6\x00[\x99\xc6\x0f\xf4\xfeV\xc2\x0f\xd6t\xb8e\xbfG\xc7\xf0\xb7\x03\xd7\xe8\t\x8d\xc9\x11፵\xe1\xb2._\"ݱ\xd1\x1cC\xe0\x87Ο8\xb4\xbf\xf5\xff\x00\xe2\x06\xbb\x06\xbbt\xefe촭\xb8&'\xb7\xdf4\xab\xe3\x15\xec\x1c\xfe\xf1\xafɿ\x03\x00\x00\xff\xff%\xff\\)\x99#\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcVK\x8f\xdb6\x10\xbe\xfbW\f\xd0k%wQ\xb4(tk69,\xda\x06\xc6n\x90;M\x8e-f)\x92\x9d\x19z\xeb>\xfe{AR\xf2C\xb67\xc9%\xba\x99\xf3\xe4\xf7\xcd|t\xd34\v\x15\xedG$\xb6\xc1w\xa0\xa2ſ\x04}\xfe\xc5\xed\xf3/\xdcڰ\xdc\xdd-\x9e\xad7\x1d\xdc'\x960<\"\x87D\x1a\xdf\xe2\xc6z+6\xf8ŀ\xa2\x8c\x12\xd5-\x00\x94\xf7AT>\xe6\xfc\x13@\a/\x14\x9cCj\xb6\xe8\xdb\xe7\xb4\xc6u\xb2\xce \x95\xe4S\xe9\xdd\x0f\xed\xdd\xcf\xedO\v\x00\xaf\x06\xec\xc0\xa0C\xc1\xb5\xd2\xcf)\x12\xfe\x99\x90\x85\xdb\x1d:\xa4\xd0ڰ\xe0\x88:\xe7\xdfRH\xb1\x83\xa3\xa1Ə\xb5k\xdfoK\xaa7%\xd5cMU\xacβ\xfcv\xcb\xe3w;zE\x97H\xb9\xeb\r\x15\a\xee\x03\xc9\xfbc\xd1\x06̚\xaa\xc5\xfamr\x8a\xae\x06/\x00X\x87\x88\x1d\x94ب4\x9a\x05\xc0\bH\xc9Հ2\xa6@\xac܊\xac\x17\xa4\xfb\xe0\xd2\xe0\x8f\x95\x905\xd9(\x05\xc2\x0f=\x96\xcbC\u0600\xf4\b\xb5\x1cH\x805\x8e\x1d\x98\x12\a\xf0\x89\x83_)\xe9;h3\x92mu͍\x8c\x0e\x95\x847\xf3c\xd9\xe7\x86Y\xc8\xfa\xed\xad\x16X\x94$\x9e\x9a(um\xf0@'ȟ7P\xfc\xdb\xd8+>\xaf\xfeT\f\xb7*W\x9f\xdd]EZ\xf78\xa8n\xf4\r\x11\xfd\xaf\xab\x87\x8f?>\x9d\x1d\xc3y\xafWH\aˠ\xa6N3p\x155\b\x1e!\x10\f\x81&T\xb9=$\x8d\x14\"\x92؉\xff\xfa\x9d\xac\xd5\xc9鬅\x7f\x9b3\x1b@\xee\xbaF\x81\xc9\xfb\x85\\@\x1c\x87\x02\xcdx\xd1\n\xaee \x8c\x84\x8c\xben\\>V\x1e\xc2\xfa\x13jig\xa9\x9f\x90r\x9a<\xaeə\xbc\x96;$\x01B\x1d\xb6\xde\xfe}\xc8\xcd\xf9\u07b9\xa8SR \xc9c畃\x9dr\t\xbf\a\xe5\xcd,\xf3\xa0\xf6@\x98kB\xf2'\xf9J\x00\xcf\xfb\xf8#\x83h\xfd&tЋD\xee\x96˭\x95Ilt\x18\x86\xe4\xad\xec\x97E7\xec:I ^\x1aܡ[\xb2\xdd6\x8ato\x05\xb5$¥\x8a\xb6)\x17\xf1Ep\xda\xc1|G\xa3<\xf1Yً\xe9\xa9_ч\xaf\xa0'\xabE\x9d\x91\x9a\xaa^\xf1\xc8B>\xca\xd0=\xbe{\xfa\x00S'\x95\xa9J\xca\xd1\xf5\x02\x97\x89\x9f\x8c\xa6\xf5\x1b\xa4\x1a\xb7\xa10\x94\x9c\xe8M\f\xd6K\xf9\xa1\x9dE/\xc0i=X\xe1ib3u\xf3\xb4\xf7E\x90\xb3\x02\xa4h\x94\xa0\x99;b\xa3\x13Q\x19\xbe\x83ΫkA_\x8a\x05\x12\x05\xba8\x9d5\xf5\xae8\x95\xff\x12\xcaz\x06\xe5\xf7c H\xaf\x04^\x90\xf2\x86萲Z\xa1\x01\x93.\xf0\x1ba9}\x93\"\x05\x8d|\xb1\x8a\x00Vp\xb8\xd2\xd3+\xec\xe4\xcf'\xe7\xd4\xdaa\aB\to0\xab\x88\xd4~f+o\xdfg Xe\x9fk\x1c\x1c\xde\xfbϒP\xe0\xf6i\xb8\xac\xd4\xc0{|\xb9r\xfa\xe0W\x14\xb6\x84<\x1f\xf9l\\U\xf4p\xfe\x1e\xbc\x82\xd2ա\xbc8\xe4,\x85\xe6\x04E\x96@j{\x8a+\xa7\xf5A\xe9;\xf8\xe7\xbf\xc5\xff\x01\x00\x00\xff\xffߙ6&\xcb\n\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xbcW\xcdn\xe36\x10\xbe\xfb)\x06\xe8\xa5\x05Vr\x83\xa2E\xe1[\xeb\xec!\xd8t\x1b\xc4\xdb\xdc)j$\xb1\xa1H\x96\x1c:\x9b\xa2\x0f_\f)\xf9G\x96\x1d粺y8\x9c\x9fof\xbe\xa1\x8b\xa2X\b\xa7\x9e\xd0\ae\xcd\n\x84S\xf8\x95\xd0\xf0\xafP>\xff\x1aJe\x97ۛų2\xf5\n\xd61\x90\xed\x1f1\xd8\xe8%\xdeb\xa3\x8c\"e͢G\x12\xb5 \xb1Z\x00\bc,\t\x16\a\xfe\t \xad!o\xb5F_\xb4h\xca\xe7Xa\x15\x95\xae\xd1'\xe3\xa3\xeb\xed\x8f\xe5\xcd/\xe5\xcf\v\x00#z\\Am_\x8c\xb6\xa2\xf6\xf8O\xc4@\xa1ܢFoKe\x17\xc1\xa1dۭ\xb7ѭ`\x7f\x90\xef\x0e~s̷\x83\x99\xc7l&\x9dh\x15\xe8\xd3\xdc\xe9\xbd\x1a4\x9c\x8e^\xe8\xd3 \xd2a謧\xcf{G\x05\xf0y>R\xa6\x8dZ\xf8\x93\x9b\v\x80 \xad\xc3\x15\xa4\x8bNH\xac\x17\x00C\xf6\xc9P1$\xbe\xbdɦd\x87\xbd\xc8\x1e\x00\xacC\xf3\xdb\xc3\xdd\xd3O\x9b#1@\x8dAz\xe5(a\xf8_\xb1\x93\xc347P\x01\x04\f\xe1\x00\xd9]\x84 \f\bO\xaa\x11\x92\xa0\xf1\xb6\x87J\xc8\xe7\xe8\xc0V\x7f\xa3$\bd\xbdh\xf1\x03\x84(;\x10l%+\x1c\xf8Ҷ\x85Fi,w2\xe7\xadCOj\x04)\x7f\a\xbdv \xbd\x94\x05\x7f\x9cx\xbe\x0557\x1d\x06\xa0\x0eG\xf0\xb0\x1e\xb0\x02\xdb\x00u*\x80G\xe71\xa0\xc9m\xc8ba\x86lʉ\xe9\rz6\xc35\x8d\xba\xe6^ݢ'\xf0(mkԿ;ہ\x11c\xa7ZP\x02\xd3\x10z#4l\x85\x8e\xf8\x01\x84\xa9'\x96{\xf1\n\x1e\x13\x82\xd1\x1c\xd8K\x17\xc24\x8e?\xacGP\xa6\xb1+\xe8\x88\\X-\x97\xad\xa2q\x02\xa5\xed\xfbh\x14\xbd.\xd30\xa9*\x92\xf5aY\xe3\x16\xf52\xa8\xb6\x10^v\x8aPR\xf4\xb8\x14N\x15)\x11\x93\xa6\xb0\xec\xeb\xef\xfc0\xb3\xe1\xc8-\xbdrC\x06\xf2ʴ\a\aip\xdeQ\x1e\x1e\xa5\xdc]\xd9TNq_\x05\x161t\x8f\x1f7_`\x8c$Wjh\xb1\x9d\xea\t.c}\x18Me\x1a\xf4\xf9^jS\xb6\x89\xa6vV\x19J?\xa4Vh\bB\xaczEa\xecu.\xdd\xd4\xec:\xb1\x14T\b\xd1Ղ\xb0\x9e*\xdc\x19X\x8b\x1e\xf5Z\x04\xfcƵ⪄\x82\x8bpU\xb5\x0e\xb9w\xaa\x9c\xe1=8\x18\x99\xf3Li'\x94\xb1q(\xb9\xb0\x8c-\xdfT\x8d\x92y\xa4\x1a\xebA\xec\x19d@\xfa\x18\xa8y\x06H\xc1\t\xdf\"M\xa5\x93X\xbe$%v\xff҉c\xc2\xfa\x1e˶d\xce\tC \x99\x8f~\x98\x16\xeaR\f0\xdb賑\x8c\xfd\xcd00\xaeL(Lv\x871\x9d\xba\xe6\x0fM\xec\xe7\x1d\x14\xf0{\x8a\xf9\u07b6\x17\xcf\xd7\xd6\x10\xcf\xc5E\xa5'\xabc\x8f\x1b#\\\xe8\xec\x1b\xbaw\x84\xfd\x9f\x0e}\xde\xd0\x17U\xc7E\xbfۊ\x17\x14\xa3>\xeb\xf7\x11y\x83\xe0\xf9L\a\x85\xab\xac\\\x11ӠyU\xa2\xeb\xcd\xdd{ <\xa3\xfe\x8e\"ݙƾ\x91\xe2^qV\xef\f\r\x8c_zC\xbc\xdd\xd3\xfc\n\x19{\x9a\xaf\xe4݉\xf0)V\xe8\r\x12\x86=S\xbf(\xeaf-\x02\xbctJv\xe9b\x1a\b^\x02!X\xa9\xe6(\xf5\x8a\xf0\x99G\x94Ǚ\xa1,ҰΈ9\xf8\x13\xf1\x19\xf6;\xe7\xa0\x18\x18\xe9*\x06%A1\xbc\x83C\x93\xfe\b\xb5\x8cާ\x15\x95\xa5\xfc2\x99^\xb8\x96DG\xe6\xf9\xeb\xf1\xfe\r&\xbd\xddk\xa6ǸP&G\xe3<\x16A\xb5\xfc\x82\xe23\xe6\xd2\xc4q\xa7`\xe4\xef\xf8\x85w\f\xd4lE\xf1\xabSy\x00\xdf\b\xf1\xe3N1\x13>\x9a\xbc\xe7\xa7o\xd8d\x10\x03?\xb7@\n3\x13c\x85P\xa3F\xc2\x1a\xaa\u05fc\xb9^\x03a\x7f\x1awc}/h\x05\xbc\xff\vR3md\xa2֢Ҹ\x02\xf2\xf1\\\x97\xcd&\xee:\x11f\xc6\xf0(\xe7\a֙k\x8c\xdd0^\xec\f8\xbb_\n\xf8\x8c/3\xd2\ao%\x86\x80\xa7ct6\x93\xd9!8\x11\x06~\xa4\xd5\a(\r\x7f\x19\x06\xc9\xff\x01\x00\x00\xff\xff\xe5\x0fY\x99e\x0e\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4:K\x93\x1b\xb7\xd1\xf7\xfd\x15]\xeb\x83\xed*\ri\xf9\xfb\xe2J\xf1&\xad\xe2\x14\x13[\xd9\x12W\xba\xb8|\xc0\f\x9a\x1cxg\x00\x04\xc0\x90b\x1c\xff\xf7T\x03\x987H\xeeR\xb6\x85\xcb.\xf1h\xf4\xfb\x85ɲ\xec\x86i\xf1\x01\x8d\x15J\xae\x80i\x81\x1f\x1dJ\xfae\x17\x8f\x7f\xb5\v\xa1\x96\xfb\x977\x8fB\xf2\x15\xdc5֩\xfa\x1dZ\u0558\x02\xdf\xe0VHᄒ75:ƙc\xab\x1b\x00&\xa5r\x8c\xa6-\xfd\x04(\x94tFU\x15\x9al\x87r\xf1\xd8\xe4\x987\xa2\xe2h<\xf0\xf6\xea\xfd7\x8b\x97\xdf-\xfer\x03 Y\x8d+Њ\xefU\xd5Ԙ\xb3\xe2\xb1\xd1v\xb1\xc7\n\x8dZ\buc5\x16\x04{gT\xa3W\xd0/\x84\xb3\xf1ހ\xf3\xbd\xe2\x1f<\x98\xd7\x1e\x8c_\xa9\x84u\xffL\xad\xfe \xac\xf3;t\xd5\x18V͑\xf0\x8b\xb6Tƽ\xed/\xca@\xef\xf3\xb0\"䮩\x98\x99\x1d\xbc\x01\xb0\x85Ҹ\x02\x7fN\xb3\x02\xf9\r@$\xde\xc3ɀq\xee\xd9ɪ{#\xa4CsG\x10dw\vG[\x18\xa1\x9dg\xd7\x04s\xb0\x8e\xb9Ƃm\x8a\x12\x98\x85\xb7xX\xae\xe5\xbdQ;\x836\xa0\r\xf0\x8bU\xf2\x9e\xb9r\x05\x8b\xb0}\xa1Kf1\xae\x06\xb6o\xfcB\x9crGB\xd9:#\xe4.\x85ă\xa8\x11xc\xbc\xb8\x89\xfa\x02\xc1\x95\xc2ΰ;0K\x18\x1a\xe7\xc9N\xe3\xe2\xd7\t\xa2u\xac\xd6S\xa4\x06G\x03V\x9c9L\xe1t\xa7j]\xa1C\x0e\xf9\xd1aK\xc9V\x99\x9a\xb9\x15\b\xe9\xbe\xfb\xff\xd3\xec\x88\xfcZ\xf8\xa3o\x94\x1c\xf3\xe65\xcd\xc2`:`B\xb2ڡI2H9V}\n\"\x8e\x00\xbc\x1e\x9c\x0f\x98\x04\xb8\xc3\xf9\x8b\xa8\xacea\xb0Fy\x1dB\xa2?=\xc7f\bz\xb8\xaa\x8dPF\xb8\xe3\n^~\xf3T4\xc9>@m\xc1\x95\bQy6N\x19\xb6C\xf8A\x15A\xd1\x0e%\x9a\xa8hy\xd4\xfeR5\x15\x87\xbc\x15\f\x80u\xca$\x95Mc\xb1\b\xa7\"\xdc\x16\xecD\xe3\xc6w\xfe\x11\x06Q\x18dI\x83h\xdd\xe9\xc2\xef\x10J\xa6\xad\xe2\xd5\x0e\x9fd\x11C\x96Jű\xe3\x1f\xce\xd0\x12\x16\xb4Q\x05Z{\xc6P\t\xc6\b\x91\xb7\xfd\xc4E\x06\x95\xe8\xf7\xb4\xf84\xbaR\x8c\xa3\x01\xa7\xa0d\x92WHd0p\x86I\xbb\x8d*2\x17`{\xec\xe1\xa8Ǩ\xbc\x8f\v\xa7\xd0\t\xbb\xf6/\x83\xbb.J\xac\xd9*\xeeU\x1a\xe5\xab\xfb\xf5\x87\xffی\xa6I\x8d\x95F\xe3D\xeb\xef\xc3\x18\x84\xcd\xc1,\x8c\xc9\xfdo6Z\x03\xa0\v\xc2)\xe0\x14?\xd1z6\xc4@\x80<\xe2\x14\xd8#,\x18\xd4\x06-\x99\x96\xd7(\xb5\x05&A\xe5\xbf`\xe1\x16\x13\xd0\x1b4\x04\xa6\xb5\x85B\xc9=\x1a\a\x06\v\xb5\x93\xe2?\x1dlK\xbc\xa6K+\xe6\xd0:o\x8cF\xb2\n\xf6\xacj\xf0\x050\xc9'\x90kv\x04\x83t'4r\x00\xcf\x1f\xb0S<~T\x06AȭZA霶\xab\xe5r'\\\x9bL\x14\xaa\xae\x1b)\xdcq\xe9\xf3\x02\x917N\x19\xbb\xe4\xb8\xc7ji\xc5.c\xa6(\x85\xc3\xc25\x06\x97L\x8b\xcc\x13\"}B\xb1\xa8\xf9\x17&\xa6\x1fvt\xedL\xd0a\xf8\x1c\xe0\x19⡬\x80\x8c\x80EP\x81\xc4^\n4E\xac{\xf7\xb7\xcd\x03\xb4\x98\x04I\x05\xa1\xf4[g|i\xe5C\xdc\x14rK:O\xe7\xb6F\xd5\x1e&J\xae\x95\x90\xce\xff(*\x81ҁm\xf2Z8R\x83\x7f7h\x1d\x89n\n\xf6\xce'\\\x90\x93-\x91\a\xe0\xd3\rk\tw\xac\xc6\xea\x8eY\xfc\x93eER\xb1\x19\t\xe1I\xd2\x1a\xa6\x91\xd3́\xbd\x83\x856\t\x13\x86a\xc6;%:W\xaaB6\xe5%\x85\xbb\v4S\x00L\t\xcbG[W2\xd7\xe2F\x9bL#圷4\x94|\x968\xb4\xe2\x17\xf0\x8a720\xb8E\x83>\x1b\t\xbe_+\x1f!\x1c\x13\xb2\xf5i\xa1^\x01\xa7\x12\x98\xe5A\x89\x90\xc3\xd46\xe0\xac}\xc0\x99@\x99\xc4\xf8\xd5\xfd\xba\r\x86-\x13#\xee\xb3xw\x91?4\xb6\x02+\xee3\x87\xcbw'5\x97\xc6z\x1b\x90\xf0\x11\xc1)`\xa0\x05\x168\x8a\xc6 \xa4u\xc8x\x9c$'h0\xae\xbd\b\x9e\xfe$\x92\x10\x8a\xca\x18\xb5I&\xc0(\xf2\b\x0e\xff\xd8\xfc\xeb\xed\xf2\xef*\xd0\x01\xac\xa0\xd4\xcc\xd7z>\xdf~\xd1\xd5{\x1c\xad0ȩz\xc3Eͤآu\x8b\b\r\x8d\xfd\xe9۟\xd3\xfc\x03\xf8^\x19\xc0\x8f\x8c\xaa\xa6\x17 \x02ϻ`֪\x8d\xb0\x81\xf0\x0e\"\x1c\x84+=\xa2Z\xf1H\xe0\xc1\x93\xe0\xd8#Yr \xa1A\xa8\xc4c\xc2~¸\xf5\xd9\\\x8f\xe6\xafd=\xbf\xdd\xc2W\xc1y\xdd\xd2\xcfۀF\x97\xb6\f\r\xacG'X\x99\x11\xbb\x1d\xf6y\xffLY(\xccR\x80\xfa\x1a\x94!Z\xa5\x1a\x80\xf0\x80IN!> \x9f\xa1\xf7ӷ?\xdf\xc2Wc\x1e\x9c\xb8JH\x8e\x1f\xe1[\xf2>\x9e7Z\xf1\xaf\x17\xf0\xe0\xf5\xe0(\x1d\xfbH7\x15\xa5\xb2(A\xc9\xea\x18\x12\xe0=\x82U5\xc2\x01\xab*\v\t\"\x87\x03;\x82ڞ\xb8\xa7\x15\x11\xa9&\x03͌;\x9b$F>\x9c7\x9ay\xd6Ԏ\xa7ًϢ\x9ed\xbd\x9f-\x03y\"'|\xb9\xf0\t\x9c\x18\x96^Wp\xe2\xb1\xc9\xd1Ht\xe8\x99\xc1Ua\x89\x0f\x05jg\x97j\x8ff/\xf0\xb0<(\xf3(\xe4.#ê\xd4\xed\xd2\xf7\xc1\x96_\xf8?\xd7\x12\xee\xdbT\x9fJ\xbd\a\xf2\xf9X@\xb7\xdb\xe55\x1ch\xb3\xfb\xa7Ǯ\x93|\xd8Ąs\n\x93l\xfeP\x8a\xa2lk\xbd\x81\xb7\xad\x19\x0f\xee\x98\xc9\xe3g\xb2\x1d\xe2sc\b\xa3c\x16\x9b\xb8\x19\x93\x9c\xfe\xb7\xc2:\x9a\xbf\x86\xb1\x8d\xf8$\xe7\xf2~\xfd\xe6sZT#\xae\xf1$'j\x980>f=VY\xcdt\x16v3\xa7jQLvS\x0e\xbf\xe6$\xa4\xad@s!\xfd{7\xda\xdc&\xa8\x89j\xa0\xdb\xf3\xac\xfcӱ]\"\xe1\x1bv\xb1ϥ\x85g\xf9uY\x15\x1e\xd8\xce\x023\b\fj\xa6I#\x1e\U0005814cC3A\xe9\x02e\x04]c\x10\x98\xd6\x15\xc5\xf4\x90E$ \xc6\xfc7\xb2\x87YO\xdf)\x86$E\xd9v\xa56蜐\x9f\x919\xef'\x88\xfc\xbe\x8c\xeazv\x85\x92[\xb1\x8b\xdd\xce9\xa7dSU,\xafp\x05\xce4\xa7j\xae\xb3\x8c|\xa0-\xe7\xe9\x7f?\xd8\xdaj\xf8\x85\x06c\x9a\xaaQ\xdbqN\fʦ\x9e\xa3\x92\xc1\xa3҂%\xe6\rZ7\xb3^Z\xb8\xbd}\x8e\x8d\x05\xa5\xbc\xa6\xe4\x0eep\xaa*\x8d\x8a\x1e\x13\xf8\xb62u\xaa\xaf\xf2\x92B\x7f\x86o\xa0\xea\x9eʑ1\xdeY\xba]2\xd93\xe8.\xb7SZ\xf1\xc9\xcc\xd8\rN\x16\x03}O\xea!\xf9\x86\xf63\xbaH\xe1\x91-\xf24\x04G\xd7>\xbdQ\xda}m\x1f\x89\n;\xed\x90w\x8d\xfek$\xfej\n\xc4\xf7~\r\x8fF!j\xecJ\xff\xb1\xaf\v\xc5]\x8e\xa0\rj\x96\xec\n\x81\xef\xdc[\xdf\xc2\xfc\xd2\x06`\xc2Bc\x91\xfb\x0e\xda\xec\xee\x19\x84\xf6\x9d\x893\x87\x19\x9d\xbf\xce_\xa4\x1bS\xe1\xcdo\xf8RrU\x97j\x0ef\xceB\xd6r\xcd?ᴏ\x8d)\x8e\xf5\xe0:~\x05h\xc8}\x15JE\xf2\x96\x89\n9\xb4o\xdcτ\x92\xe3\x96R\x9c\xe0\xe3\xda>ND\xeft\xfdw^\x92\t&\xcc\x13\x9e?R\x98ӧ\xc6\v\x92\\O\xb6C\xa9\xaa(/\xd9\xd49\x1a2L\xff\xe0\t\x12\x0fT\xf7\x17%\x93\xbb\xa4\x93k\x1f\xec\x10*f\x1d\xe4\xfd'\x03)\xe2\x87/\xa6Sʆ/\x9c\xfd\xa8\xd1Z\xb6\xbb\xe4\xce\x7f\f\xbbB\xe7.\x1e\x01\x96\xabƥ\xed\xf7K\x1b]\xd0\xf3\xba\x87ɦ\xd8\xd8\xfb1W\xb6\xcen\xdbT\x95?3\x8c\x1b\xfd\xa7\x1d\x1e\xab\x1c\xd3\x19\xff\x99\xd6\xe19\x04Kf/\xb1\xea\x9e\xf6\xa4\xfcq\x17\xec\xce:d8\x13\xd8\xdf\xe2!1\xdb\xfa\xb9\xc4\xd2}t\x9e\x89\xa5ٗ\x18\xc3\xc5\xd0\x1bOq\xae]K\xc2\xec\xbesH\xac}\xef\xbdʳ\x98\x1d\xf1\xbb\xc6mv\xbd\xf5\xde\xf2\xfcg\v3\xfb\x1b\xe7\x1fL\xf2\xa1\xd8RM\x88\xfe|\xabA\x01Rl\xa4\xc5'\x01ﺜ\x02.\xac\xaeر\xa3ŗ~d\xaa\xe9\xf7\x91ޢZ\x8f\xa9\xf1T*{\xbe\xc3\xdd}-\x92\xaek\xcf\xfb\v\xb8\xe03\xfc\xba:\xed\f\x7f\x8f\x1bΤ\xe2V2mK\xe5\xd6o.\xa8Ʀ\xdb\xd8\xdac_V\xfa\xc0\xe2\x9f\xde⦨\n\tT{\xef\xf6,g1\xfex\xe8\x1a-ތ \\\x88\xfb\xf1[\xa6Ttݐ\x17 \a\xe4\x1fv\xef\xa6_p\xbc\xe8\x82\fs\xb1A\x1e\xe2Q\xaa\xab\xa0\xa4\xaf#\x94\x99\xbf\xb2\xc3\xc5@>&\xe8ό\xe1Iu\x9aMz\xcc\xf9\x00v|\xd3\x1c\xce4y\xf7ܿ\x82_\x7f\xbb\xf9_\x00\x00\x00\xff\xff\xfc¬w\xb0(\x00\x00"), []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x00\xff\xc4Z͒\x1b\xb7\x11\xbe\xefSt\xad\x0f\xb6\xab4d\xa4$\xae\x14o\xd2*Nmbo\xb6DI\x17\x97\x0f\xe0\xa09\x03s\x06\x80\x01\f\xb9\xb4\xe3wO5\x80\x19\xce\x0fH.\xa9\x925\x17i\xf1\xd3\xf8\xf0u\xa3\xbb\xd1`\x96e7L\x8b\x8fh\xacPr\x01L\v|r(\xe9/;\xdb\xfc\xc3΄\x9ao_\xdel\x84\xe4\v\xb8k\xacS\xf5;\xb4\xaa19\xbeŵ\x90\xc2\t%ojt\x8c3\xc7\x167\x00LJ\xe5\x185[\xfa\x13 W\xd2\x19UUh\xb2\x02\xe5lӬpՈ\x8a\xa3\xf1\xc2ۥ\xb7\x7f\x99\xbd\xfcn\xf6\xf7\x1b\x00\xc9j\\\x80V|\xab\xaa\xa6F\x83\xd6)\x83v\xb6\xc5\n\x8d\x9a\tuc5\xe6$\xbc0\xaa\xd1\v8t\x84\xc9q\xe1\x00\xfaQ\xf1\x8f^λ \xc7wUº\xff$\xbb\x7f\x10\xd6\xf9!\xbaj\f\xab\x128|\xaf-\x95q\x0f\x87\xb52\xd0[\x13z\x84,\x9a\x8a\x99\xe9\xcc\x1b\x00\x9b+\x8d\v\xf0\x135ˑ\xdf\x00D\x06\xbc\xa0\f\x18\xe7\x9eSV=\x1a!\x1d\x9a;\x12!\xbbe8\xda\xdc\b\xed\xd3%\xb3\x18{\x03\xf9K\xdf\x11\x9bܞ0[g\x84,R(ދ\x1a\x817\xc6+\x9d\xf6\x9f#\xb8R\xd8)\xbc\x1d\xb3\x04\xd18\xbf\xf14\x18\xdfO\"\xadc\xb5\x1e\xa3\xeaM\r\xb08s\x98\x02u\xa7j]\xa1C\x0e\xab\xbd\xc3v+kej\xe6\x16 \xa4\xfb\xeeo\xc7\xf9\x88\x84\xcd\xfcԷJ\x0e\xc9yC\xad\xd0k\x0eHH[\x05\x9a$Cʱ\xeaS\x808\x12\xf0\xa67? \tr\xfb\xedg\xa1\x90\xe9\x81Z\x83+\x11ް|\xd3hX:eX\x81\xf0\x83ʃ\nw%\x1a\xf4#Va\x04\x9dm\x10\xa4;e\x92\xaaӘ\xcf\xc2\xd8(\xac\x955\xd2\xdfp\xa1\xcfb_\xb9A\x96\xb4\xaf\xd6I\xcd\xfc\b\xa1d\xda\xc8^\x17\xf8,\x03\xeb\x13)\x15\xc7\x1ek\x13\\\u00826*GkO\x18>\t\x19 y84\x9c\xa5\xa8D?\xa6\x05\xd4\xe8J1\x8e\x06\x9c\x82\x92I^aС3L\xdau\xb4\x8c\xa9\n\xdbi\xef\xf7z\b\xe5C+\xaf\xd73\xc1\x14\x86n_\x067\x98\x97X\xb3E\x1c\xab4\xca\u05cf\xf7\x1f\xff\xba\x1c4\x03Ѣ\xd18\xd1\xfa\xd1\xf0\xf5BR\xaf\x15\x86{\xfe_6\xe8\x03\xa0\x05\xc2,\xe0\x14\x9b\xd0z.\xa2\x7fE\x1e1\x05\x8e\x84\x05\x83ڠE\x19\xa2\x1553\tj\xf5\v\xe6n6\x12\xbdDCb\xc8\xed7\x15\xa7\x90\xb6E\xe3\xc0`\xae\n)~\xebd[\"\x9c\x16\xad\x98C\xeb\xfcA4\x92U\xb0eU\x83/\x80I>\x92\\\xb3=\x18\xa45\xa1\x91=y~\x82\x1d\xe3\xf8\xd1[\x93\\\xab\x05\x94\xcei\xbb\x98\xcf\v\xe1\xda@\x9d\xab\xban\xa4p\xfb\xb9\x8f\xb9b\xd58e\xec\x9c\xe3\x16\xab\xb9\x15E\xc6L^\n\x87\xb9k\fΙ\x16\x99߈\xf4\xc1zV\xf3\xafL\f\xedv\xb0\xecD\xd1\xe1\xf3\xe1\xf5\x02\xf5P\xbc\xa5\x93\xc0\xa2\xa8\xb0Ń\x16\xa8\x89\xa8{\xf7\xcf\xe5{h\x91\x04M\x05\xa5\x1c\x86Nxi\xf5Cl\n\xb9&çyk\xa3j/\x13%\xd7JH\xe7\xff\xc8+\x81ҁmV\xb5pd\x06\xbf6h\x1d\xa9n,\xf6\xce'3\xb0\xa2\x03E~\x80\x8f\a\xdcK\xb8c5Vw\xcc⟬+Ҋ\xcdH\t\xcf\xd2V?E\x1b\x0f\x0e\xf4\xf6:\xda\xfc\xea\x88j\xc7\xfem\xa91'\xcd\x12\xb94U\xacE\x8c$ke\x80M\xc6\x0f\x99J\xbb\x00\xfa\x92\x11e<\xe8\x9c\xd9\xd1\xf7&%\xa8E,{\x8e<\xc6;\x1b\x03U5\fT\xfdo\x12#\rje\x85Sf\x7f\x88\x94c\x938\xaa\x1d\xfar&s\xac\xae\xd9ޝ\x9f\tBr\xe2\x1d;\x93&g\x14\xa4z\xa0J\x16\x8a\x0e\xd9D\x1dp\xefh\x1cٹE\x97ެ<\x1aل\x84C\x8e\t\xfd\\r\xbc\xed\x95R\x15\xb21\x9bZ\xf13\x9b~T\xd1q\x18\\\xa3A\x1f\xff\x83\x9b\xd5\xca;cDŽl\xddGH\xb9\xc1\xa9\xc4>V\xe4n\x8e\xa9\xe6\xb8\x1d\u0089\x90\x94\x04\xfc\xfa\xf1\xbe\r;\xadeE\xe8\x93\xc8\xd2\xe7'i\x16\xf4\xad\x05V\xdc\a\xea\xf3k'-\x84\xbe\xfbu\x00\xe1}\xafS\xc0@\v\xccq\x10\xf7@H\xeb\x90\xf1\xd8H\xee\xc6`\xec{\x11|\xeaQ\x90\x10\xaeE1>\x92J\x80\x91\x8f\x17\x1c\xfe\xbd\xfc\xef\xc3\xfc_*\xec\x03XN\x99\x90\xbf\xab`\x8dҽ\xe8\xee+\x1c\xad0\xc8\xe9\xf6\x81\xb3\x9aI\xb1F\xebfQ\x1a\x1a\xfbӫ\x9f\xd3\xfc\x01|\xaf\f\xe0\x13\xa3\xa4\xff\x05\x88\xc0y\x176Z\xab\x116l\xbc\x93\b;\xe1J\x0fT+\x1e7\xb8\xf3[plC'&l\xa1A\xa8\xc4\x06\xd3\xec\x03\xdc\xfa\xe4\xe9\x00\xf3wr)\x7f\xdc\xc27\xc1I\xdcҟ\xb7\x01F\x97 \xf4\xbd\xce\x01\x8e+\x99\x03gDQ\xe0!ў\x18\v\x054\n\x05߂2\xb4W\xa9z\"\xbc`\xd2Sp\xc4\xc8'\xf0~z\xf5\xf3-|3\xe4\xe0\xc8RBr|\x82Wt\xc6=7Z\xf1og\xf0\xde\xdb\xc1^:\xf6D+奲(A\xc9j\x1f\xf2\xcd-\x82U5\xc2\x0e\xab*\v\xa9\x18\x87\x1dۃZ\x1fY\xa7U\x11\x99&\x03͌;\x99\x8eE\x1eN\x1f\x9ai~\xd2~\xcf;/>_y\xd6\xe9\xfdb\xb1\xfe\x99L\xf8\xc4\xfc\x13\x98\xe8_u\xae`bӬ\xd0Ht\xe8\xc9\xe0*\xb7\xc4C\x8e\xdaٹڢ\xd9\n\xdc\xcdw\xcal\x84,22\xc6,h\xdd\xce}1g\xfe\x95\xff\xe7ڍ\xfb:˧\xee\xde\v\xf9r\x14\xd0\xeav~\r\x03m\x1e\xfd\xfc\xd8u\x94\x87e\xcc\xec\xc62\xe9\xcc\xefJ\x91\x97\xed\xad\xaa\xe7mkƃ;fr\xff\x85\xce\x0e\xf1\xdc\x18B\xb4\xcfb)2c\x92\xd3\xff\xad\xb0\x8eگ!\xb6\x11\x9f\xe4\\>ܿ\xfd\x92'\xaa\x11\xd7x\x92#\xb7\x85\xf0=e\aTY\xcdt\x16F3\xa7j\x91\x8fFS\xae|\xcfIIk\x81\xe6L\xf6\xf7n0\xb8\xcd\xda\x13Yw7梴\xdbJ\xa6m\xa9\xdc\xfd\xdb38\x96\xdd\xc0\x16\xc3A\x871\xe9leё8\x99k>\x03\xcfR\xfc\x96p[ID4\xb4\xc5T\xa9B\xe4\xac\x02\xeb\xdbd,VF\x98\xad\xec)\xa0T=r\f\xb7_U\xec\xe1\xf5\xbe\xe0ḧ\x1dB\x1e\x8enQ+#\n!Yu\xf0\xd8\xfe\xea(Y\xcd\xfc_\t[\xad\x99\xd6B\x16\x17q\xdbַ\x96蜐E\"\xd1\xef\x97\xdfO]\aN\x9e\x93\xf3.\xe0\xc3\b\b0\x83\xc0hO\xa4\xaa\r\uecd0uj&(e\xa4\xac0\xa6\xd6+\x04\xa6uEy]\xc8$S\xbe\xa9\xad\xd6\xe5J\xaeE\x11+\xa7S\xa6dSUlU\xe1\x02\x9ci\x8e]ڒǽ_(<\xa3\xf1\x0f\xbd\xa1\xad\xbaϔ*ӻ\x1a\x140\xa7\x9bA\xd9\xd4S(\x19l\x94\x16,\xd1N\x87s☨\xe3\xf6\xf6\x12\x93\n'\xff\f\a\xe1Μ*8D\xc7\x11\xaf!\xf1\x8a\x1d\xdcG:\x9a_\xeaP\f\xfe\xdaНj\x880K\xd7VFc\xb4\xe27c\xd2\xfa\xbex\xd4y\xf0\xa4\xe3\x8e\xe1\xa1\x1f\xf5\x06\n\x9eU\x96\xf2\x85\xf2K\nS\xe19,\xf2\x1e\xd2\x00\xd7>\x92\xd1\x05\xe3\xea\xd2\x14\xdda\xb5C\u07bd!\\S\xb7y=\x16\xe2\vʆ\xc7C\"j\xec\x8a\x1c\xd1N̡\xec\x12B\x8c6\xa8Y\xd2\"\xc0?\nX_\x18\xfd\xda\x06i\xc2Bc\x91{\xdf:Y\xfchL\xe0\xccaF\xf3\xafs \xe9bWx\x9e\xeb\xbf\xc2\\U\xf9\x9a\x8a\x99r\xc8:\xda\xfc\xfbP\xfb0\x98\xa2\xec \xaf#,\x88C\xee\xafܠ$\xac\x99\xa8\x90C\xf7,}1\xf3\t\xd0\xd3d\xecs\x92_\xa3\xb5\xac8\xe7\xb4~\f\xa3B\xe5-N\x01\xb6R\x8d;b\x95_\xdbx\xb4.\x8a\xc9R\xf1sH\x1e\x14\xf70\xe4\xf1'\xb7)\x9a\x84Z\xfa\xcfp\x17a\xf4E\xcdsEJ\x1a\x93r5\x1d\xe4Ӿ\x06Nİ\a\xdc%Z\xdb\x13\x9c\xe8z\x8cn!\xd15\xf9=@\xbf3T\x92S9Mۗ\x94\xd9=\xb6'\xfa\xbe\xf7\xc7\xe5\"\xb6#\xbek\x1cBW\x87.U\xd5\xfa\x00\xffH.\x9bz\x85\x86T\xb1Je\xc4\xc0$\xefk.UL\xe8$\xb4a8\x88\x8a\xf5\xb0X@\xf7\xa7\xdc)\xe0\xc2\xea\x8a\xed\xbb\xcd\xf8\x1b\x1c\x1d\xe9\xf4s\xc2\xe1\\\xb5\xbe\x8a\"ϑ\xbc\xedt\xa5\xba\xfb\xd1B\xfa~z:Ӈ3پ\xef\xef~\x8c\xf0yV8\x91w\x0e\x7f\x1cr\x8d\x81,\a\x12\xce\x05\x8b\xf8c\x95\xcb}\xfcp\x99?ӽ'ٛ4z\xe4\xbc';>y\xf5[\x9aU\xf7\x1e\xbc\x80\xdf\xff\xb8\xf9\x7f\x00\x00\x00\xff\xffϡa\xa5-&\x00\x00"), - []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=]\x93\xdc(\x92\xef\xfe\x15\x84\xefa\xee\"\xba\xca7q\x1fq\xd1o\u07b6}\xee\u06ddv\x87\xdbg?SRV\x89i\x04\x1a@]\xae\xdd\xdb\xff~A\x02\x12R!\x89\xaa\xfe\x98\x99\x8d\xd1KG\xab \x81\xfc\xce$A\xab\xd5\xea\x15m\xd8WP\x9aIqIh\xc3\xe0\xbb\x01a\xff\xd3\xeb\xfb\xff\xd2k&\xdf<\xfc\xf8꞉\xf2\x92\\\xb5\xda\xc8\xfa3h٪\x02\xde\xc1\x96\tf\x98\x14\xafj0\xb4\xa4\x86^\xbe\"\x84\n!\r\xb5\xaf\xb5\xfd\x97\x90B\n\xa3$\xe7\xa0V;\x10\xeb\xfbv\x03\x9b\x96\xf1\x12\x14\x02\x0fC?\xfc\xeb\xfa\xc7\xff\\\xff\xc7+B\x04\xad\xe1\x92(\xd0F*\xd0\xeb\a\xe0\xa0\xe4\x9a\xc9W\xba\x81\xc2\xc2\xdc)\xd96\x97\xa4\xff\xc1\xf5\xf1㹹~v\xdd\xf1\rg\xda\xfc9~\xfb\x17\xa6\r\xfe\xd2\xf0VQ\xde\x0f\x86/u%\x95\xb9\xe9\x01\xae\x88\xf2\xcd5\x13\xbb\x96S\xd5uxE\x88.d\x03\x97\x04\xdb7\xb4\x80\xf2\x15!~Q\xd8\x7fEhY\"\x9a(\xbfUL\x18PW\x92\xb7\xb5蠗\xa0\v\xc5\x1a\x83h\xf8R\x01.\x86\xc8-1\x15\x90\r-\xeeۆ\x98\x8a\xe90(a\x9al\x95\xac\xb1;!?k)n\xa9\xa9.\xc9\xda\"h\xedz\xd8\xf9\xf8\x06\x0e\x9f\x7f\xc2\xd7\xfe\x959\xd89k\xa3\x98إf\xe1\xf1D\xb4\xa1\xa6\xd5D\xb7EE\xa8&7\xb0\x7fs-n\x95\xdc)\xd0:1>6_7\x15\xd5\xc3\xc1\xef\xf0\x87\xcc\xc1\xbfHC9\x11m\xbd\x01e\xd1\x00JI\xa5\t\x97\xbb\x1d\x94\xa4lm?č\x8ah\x9c\x9a\x87\xeb8\x98\xc8\xfb\xf8\x95\x9b\x88%\xc9\x0eT\xceL\xf6T\t&v\xe7\xcc%t\x1d\xcc\xe6\xdb\xf0ej>\x11\xa4 e\xebB\x01\n\xd8\x17V\x836\xb4n\x06@\xdf\xee`\x00\xaf\xa4ƽp??\xfc\xe8X\xb9\xa8\xa0\xa6\x97\xbe\xa5l@\xbc\xbd\xbd\xfe\xfaow\x83\xd7d\x88\x8e\xff[u\xefI\xc7\"L\x13J\xbe\xa2(Z$\xa0j \xa6\xa2\x86(h\x14h\x10F#\x86h\xd3pV\xe0ĉ\xdcF\x90B/\xc7\xd5=\xb4\xc0\xfa\x92Pb\xa8ځ!\x7fn7\xa0\x04\x18Ф\xe0\xad6\xa0\xd6\x1d\xa0F\xc9\x06\x94aAl\xdd\x13i\xb7\xe8\xed\xdc\xc2\xeccq\xe1z\x91Ҫ9pK\xf0r\r\xa5G\x9f\x13R\x94L\xbf\u0530\xd9)H\xab\x97\xdd\xe0\xa6n\xa0\xa2\x0fL\xaa\x94\x18H\x85M#{ޫii\xb5\xa4\a2\xb6q\x99\vN\"\xab\x92\xf2~\x89!>\xda6\xbdu \x05\x86<\xddR<\xb5\xbd\xed\xde\x00\x81\xefP\xb4&1M\x12\x9cC\xa9H#\xb5\x99\xa6\xfb\xb4\xea\"\xb1s\x94\xfaq\x86i\x8eV\x96du\xf7x%\x1c\x88jq0P\xc8R\x80]Fm\x89ڷU\xb2um'\x91B6TCI\xa4\x98\x1c\x19٥\xe5\xa0\xfdX%rF\xaf\x87.\xfa\xf5\xa3\xc7C8\xdd\x00'\x1a8\x14F\xaacd\xe6\xa0\xd4=9\x8au\x02\x95\tm:\x94\x80~\x013 \x89\xe5\xf4}Ŋ\xcay\x18\x96=\x11\x0e)%h\xabM\xd0e>L-\x92,\x91\xdf\x0f2\xa7=\xfagA\xac\xc6\xf0R\x1a\xa5\x7f2\xd4p\xff$Q\xdb\xeb\xde#\xdd\xe2\xdf\x1b9\xbb\xec\x7fL\xc4\x06cr\x06\xd3\xce\xc8?A\xf73\x9b\xa7'\xf9\x16#<\xd0kr\xbd%P7\xe6pA\x98\to\x97$\x81r\x1e\x8d\xf1;\xa6\xcd\xe9L\x9fI\x9a\x1c\x99x&\xc2tC\xfc\x0e\xe9\x82&\xe3\xce[\x8cl\x9a\xfc%\xeeuAضCzyA\xb6\x8c\x1bP#쟥\xea\x03e\x9e\x02\x199V\x8f`\x9e\xc0\x14\xd5\xfb\xef\xd6\xc5\xd1}\x9a6\x13/\xe3\xce\xce7\x0e\x11\xc4\xd0y\xd6;~\xbe\xaf\xee\xbb|\xc1ʚ\x9c\x95\x87`d\x9d\x81\x03\xaf\xbb\xcb\xe5\xf5\xac\xac\xccf\xb4\n\x9c\xb0\xd8t\"9:\xdd4\a)\x8f@\aZqtq\x16\xa9\x1b\xef^\xe6[\x94\x13x\xe1T\xd5\x10\xcdݙ\xe0\x9a6V-\xfc\xcdZZ\x94\xa6\xbf\x93\x862\xa5\xd7\xe4-\xee\xd8r\x18\xfc\xe6\xf3p\x11\x98\x8c!\x1b;\x94\xe5\x9f\aʭ\xed\xb7\n\\\x10\xe0\xce\x13\x90\xdb#\xbf\xe8\x82\xec+\xa9\x9d\xd9\xde2\xe0\xb8_\xf1\xfa\x1e\x0e\xaf/\xec\xf0\x8bC\xc6J\xe6\xf5\xb5x\xed|\x88#\x85\xd19\x1cR\xf0\x03y\x8d\xbf\xbd~\x8c+\x95ɩ\x99\xcd\x06,Z\xd3&\x8fCE2Y\xdf?\x03\x8e\x89s\xf3}R\xde;\xd9s\xab\xcdb\xd1Fj\xf31\x9d7\x9c\x98\xcfm\xe81\xf4\x8c\x139\xb6ň\xc1\xe7\xd1:}o\x9dȭ\x01\xe5s\x89\xce\x06\x84\xf8㑑YjW&\x9el\x97\f\xa4]~\xd7\"x\x81\x9b\xdc\xc6M\xce\x14OqX-^N\xf4\xf6\xdf\x7f\x8f\xf2\x99Vr\xed\xff\xf1B\x9eڡ.d]\xd3\xf1\xaef\xd6T\xaf\\\xcf\xc0\xd3\x1e\x90\xa3\xbeڵ(Ϲ\x16\xb9\xe7!ܿ\xdc3S1AhP\x1b\xa0\x83\xebѡ\xeb9\x9dݫ\x8e&\x1d\xe5\xbb\x17\xced5\xb2$\xfb\n\x14\f\x18\xe38\uf39e\xaa\x90&JY\x9c\xe0\x906\xb2\xfcA\x93-S\xda\xc4SФչ\xb4>\x91|v\xde_X\r\xb25ω\xe0\xf7\xfd0\x83\xbd\xe6\x9a~gu[\x13Z\xcb\xd6\x19s\xc3\xeanWףwO\x99鶭0\x7fc\xa4%A\xc3\xc1\x00\xd9\xc06\xbdߛz\n)4+\xa1+\x1drdc\xd2\n\xe6\x962ަv\x89Rϩ\x11\xb0\xc0\xea\xa73P\xfc\xc9\xf5\x8c\xf2\x8e\x95\xdc\x0f\x11\x94\xb9v\xdcH\x03¶\x84\x19\x02\xa2\xb0\x18\a\xe5T2\x0eᑁ\xa8a\xb9z.O\x81\xdb\aD[\xe7!`\x85\x02\xc9\xc4l\xca-n\xfe\x812\xfe\x1cd\xb3\x9c\xf7A\xaa\xcf@\xcbsr4ߢ\xee\x04\x84n\x15n\xfe;ݱg}\xeedq=\nb\xa8&{\xe0\x9c\xd0\x14w\x1c-\xbfp'\x93\v\xb9\xc2#\x81\x96\xbc\x81I\xfcy\xe6\v'\xc5x\f\f\xa9W'\xe0\x16T\xe0\xe9f\x9dXȤ9\xccѢG~\xb9\x8b.\xf0\xdd/-\xa8\x03\x91\x0fX\xc2ཷ\xfe\xac\x82W7\xdaƘA\x01ze<\xb5\xa9p\x14\xca\xf4\n\x8a\xbc\x15Η\x18\xcf\a\xfbX\xcdׇjV\x9d\xdb(,9\xc6Dw!\xbbމnKn\x7fnQ\xff\xf3\x06n\xa7\x87n\x8b\xbeR\xbe?\xfb+\x15\xeb\x9fS\xa4\x9f\xb7\x1d\xb4X\x94\xff\\\x81\xdcR(\x97\xed\xbd\xe6\x15ݟ\xb6\x89\xfa\x8cE\xf6\xcfQ\\\x9f\x89\xa9\x9cb\xfa\xd3\xf0\xf4\x02\xc5\xf3/Z4\xffR\xc5\xf2\xd9E\xf2Y\xfb\x98ٛV\xb9یgV}/\xef\xba\xcf\x17\xbdg\x14\xbbg\xec\xa4-/\xf2\x8c\xe5e\x14\xb3\x9fVĞA\xb3\\Q|\xc1b\xf5\x17,R\x7f\xe9\xe2\xf4\x05\xceZ\xf8\xf9\xb4\"\xf4\xb3w`\xc2V\xff\x8d,\xe1V*\xb3\x14\x9c\u070e\xdb'vR\xa3\x80M\xf2\x92\x88\xd04\xb1J\f1|xqޢқ\x9e\xc1\x9d\xfeI\x96vnK{,\x9fG͏\xce*oA\x81p\xd7|\xfc\xcfݧ\x9b\x0e~\xca\xe7\xf5\x9e\xf1\xe8z\t\xe7\xc1\x94\x1e9~k\xce\x1739l\xa1\x0f\xf0\xc4\xfb\"\xb4a\xff\x8d\xf7\x0e>\"\x1d\xf4\xf6\xf6\x1aa\x04?\r/2\xec\xaa(\xba\x1d\xcb\rX\x8bաjR,\xae\xb7\x03\x88Ê\xdf\xf8\x1a%(ݕY\xc1b\xb2P\xe3e\x05\xef\xf6\xda\xcdcj\x94\x0f\xd6i\x14\a\"\x1dGVL\x95\xab\x86*s@\xb6\xd1\x17\x839\x0433\x97ΙT\xac\xc7׀%\xd1\x1bn\xff½\xc8C3\xdc\xed\x1d\xe3\xee\x9cyL\x9f?YfX6Jk|sq\xfc`\xbc\xe4j\x11\x19\xbf\x88\xb2\xb7/S\xa6\x93y\xc5\xd6ٗk9\xf4L\xa8\x1fܑ\xb0\xaa\xed\x18Sg\x14\xe8,\x86\xdb\x19\a?\xe6\x13\v\x99W3\xe5\x19\x8c3\xaecB|\xe5\xe2\x8a$oiʼ\x89\xe9WE\xf4\x8cV\xd3E\x05e\xcb\xe1\xdc{X\xef\xa2\xfe\xcb7\xb1\x86\xd12\xeeb\xb5Ȏ\f\xb4\xf5\xb0\x86w\xbezJx\xc81%\xa7\x82pLظ+\x1f\vw;pQ\x80\xd6ۖ\x87\xcaQ\xbc\xc0\x1b\xcaМ\xe9n\xc6'\xd5>\xea{ּs5\x94\xe3\xb0\xfb,\x1cO\x83\v\x97\xf8G\xd6\x01\xf7\x14\xd4\x03\xa8U\x81\x1ea\xab\xa0\f\x15\x9d3^$\xa9\x03H\xa6\xe3H~\xe0\xaa'\xfa\x7f\xab@ W:'*\x94\x8e\xc6\xd0,:\x1a(\t<\x80 lK\xa2yI\x11M8\x05\xfe#\xc5\r8\xd8n\xa10nC\x97\xa2\xc3\x1c\xa4\xf6\b#\xac\x97\xfb\x84g\xf5\b\x83\xd76\\\xd2\x12\x94s\xb4\x17\b\xf9\xbf\x83\xc6#M\x14\x10\xd0_\xa2<{\x01\xed\xa3\xecQC\x15\xe5\x1c\xf8\a\xc6A\xbf\x93{a畡foS\xfd\xa2\x13\xd0E\xab\xac\xb3v\b\xb7\xf0k0f:-\xbb\x95j\xfe,\xd2\xf1\x15\xfb\xc3g\xaf\x98\x81\xbb\x86*\r8\xa3\x8c\x15|\x1buqy\xde-\xa7;Wt^\xb2\x82\x1a\xe8\x04\aG\x98\x9a>\xf6\xd7\b\x8b\x1f\xb0\x06XNl/e\xab\xea\xa9Ï\x93\xcaz\xea\"\xef\x84\x03\x96\xbc\xca\xdb\xf9Y\x05m\f\x1e5E:\"\x11M\xf8\x9c\x84\xdc\x1e\xdd\xe6=\x00;\xcdi\xfe\xc0P\xfc\xed\x83s4\xdd\xd51\x18\xbc\x80_\x95Q\x85{|\x95qW\xcaN\xf6Twǖ\x92\x11U\x0fہA\xb5fA\a\xcddE\x912\x0e\xe5\x1c\xa7~\xe9\xb4\xd5\x0f\xba\x83\x835\xf7\x96\xc5\xef\fU\xa6\x9b\xfa\xb1w\xea\"s\xf7釕\xed}\x9e~J_H\x8e_\xd08\xeb^m\xf7\x1d\x0f\x14\x8f\"\x1cp\xb5>\x8d;\xf9]\x83\xd6t\x17ҽ{P@v ,\u07bb]\xbc\xa4\x1f\x1c\x0e\xcf{\x17`\x90\ue845i)\x0f_\x10\xa1\xf8E\x13_\xa7\x14\xbe\x04\x80\xf9\xe2ݤ\xe1M\xab\n\x7fL\xff3P=\xfe\xb0\xc4\x11.>\xc4m\xfdv\xac[\xb1\xabB\xa0\xee(\x05~Z\xc005\xfe\x94\xc8`J\x12G>\xc9I\xa8\xa4\xbc\xcf\n\x9e>v\r\xfb\x8d\x1b&\x1c+\xe1\xe5\x04\x1bٚ\xc8{\xf5\bOL\x13/\xda~b\xfb\x820ߺ\xa3\xcaS\xbb\x98y\xfe\xfb\xc7\x01\xa4.i1\xfa\xd4\v\xed\x1aT3W\xf5܅\xcf\x14p~\xb8\x18C\x1e}\xff\xa4\x87]\xf5\x97f{M\xd0_\xd421P\xd8_K\x02\xe9\xee\xdb\xee=ͩۍ\x97\xec\x1fB\xfd\x80\x93\xca\xc0\xf1Ǿ\xf5\x14\x1e\xdd4]\x18\x04\"\x9d? \x18R\x9a\xaa\x93\x8c3\xa6>\x13{\xe0爖6\xe3l\x9b\xce\xed\x88\xccU\x17Z|\x9e\x90\xca\xf4\x8d\x12+r\x03\xfb\xc4[\x87,\xac3A\xa9J49\xfa\xc0R\xfc\xe37ʬ\xfb\xf3A\xaa[\xde\xee\x98\xf84}\xc6j\xae\xf1-U\x86Y\xa6u\xf3I\xf4\xbd\n6.\xf1\xdbr\xef\xe9\x1f\x98\xa0\x9c\xfd5\xa5\xcb\xe3\x1f\x97F\x98\xd1w\x8dG\xde9\x16* ~I\x01z\r\xfd\x83\x8e\xccO\x18wMndR\x8c})\x16\x1b\x02e\x9al@\x9b\x15l\xb7R\x19\xb7S\xbeZ\xd9\xf0\xc5;HVC`\xf4\xef\xbe\x1bCX*\xba\xea\x8a\\\x82ò\xf5\tb\x85V\a\x13\t5=\xb8<3-\n\x1b\x13\xc0\x1bmh*\xe2|\x94\x9e\xc6\x04\x84\x97\x95\x1c\x15r\x1d\xb7\xef2\xb7\x9d\xfa@p\x0euxq\x8c3\xe8|\xaazdp/\x15\xd1\x16{\xe7(\x13\xe2\xd4\xd8\xf5t2%\xcf\xd4|\xe9\xa0L\xa9G\xbf\xbe\xc1'/|)\x93od\xc9VTT\xec&\x8f\x8eWJ\xb6\xbb*\xf0\xe6\x94CD\xca\x16\xf3!\r\xaa\x02\x1d>\xd1eZ%\xa2\xf2\x18_\xcd8\xa5\xa5\xbb\xe9N\xfb(\x8fPԪ?Bګ\xaa\x19\x9b\x9f\x9d\xfb\x9d\x80\xb8h\xfb\x13\x10\xa9>\x88b\xf6\xb0\xeb\xf1\xce\xe3I\xaee\x12\t\x9d6~2$t\x10\xa7\x90\x10\xfb\x12}\xc4\xf3\x9b\xc1Ȕ\x8fr&:\xe6\x9d\x18\\\xe2<\xa8\xe5E\xc7N\xd0\xd0\xdd9\r\x1dz\x10\xfc\x9d\x95\xe8\x1b@8%\xf2ű\xd3q\xefo7b}輭\xf7gǮ_G0F\x97\r\xd8(\xb6\x1f&ě\xff̶)yq\xdfA\xdcp\xf8\x97\xa3__\xf8Ҁ\xf0Q\xcas0\x12\xbe]\x99\x88\xe7=\xd8\xe7\x8c\xe8\xbb/q>UL\x9f4KG/\x91\xc1\xcb\b\xcf~\xa4\xf8M\xbb\xe9\xbf\xd9D\xfe\xf6\xf7W\xff\x1f\x00\x00\xff\xff\x95Pn\x17dw\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\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"), diff --git a/pkg/apis/velero/v1/backup_types.go b/pkg/apis/velero/v1/backup_types.go index dd3125fa7..a53b61a04 100644 --- a/pkg/apis/velero/v1/backup_types.go +++ b/pkg/apis/velero/v1/backup_types.go @@ -520,6 +520,11 @@ type HookStatus struct { // +kubebuilder:rbac:groups=velero.io,resources=backups,verbs=create;delete;get;list;patch;update;watch // +kubebuilder:rbac:groups=velero.io,resources=backups/status,verbs=get;update;patch // +kubebuilder:resource:shortName=bak +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Backup status such as New/InProgress" +// +kubebuilder:printcolumn:name="Errors",type="integer",JSONPath=".status.errors",description="Total number of errors logged during the backup" +// +kubebuilder:printcolumn:name="Warnings",type="integer",JSONPath=".status.warnings",description="Total number of warnings logged during the backup" +// +kubebuilder:printcolumn:name="Started",type="date",JSONPath=".status.startTimestamp",description="The time the backup was started" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // Backup is a Velero resource that represents the capture of Kubernetes // cluster state at a point in time (API objects and associated volume state). diff --git a/pkg/apis/velero/v1/restore_types.go b/pkg/apis/velero/v1/restore_types.go index 2ef791270..416a2b8ca 100644 --- a/pkg/apis/velero/v1/restore_types.go +++ b/pkg/apis/velero/v1/restore_types.go @@ -420,6 +420,11 @@ type RestoreProgress struct { // +kubebuilder:rbac:groups=velero.io,resources=restores,verbs=create;delete;get;list;patch;update;watch // +kubebuilder:rbac:groups=velero.io,resources=restores/status,verbs=get;update;patch // +kubebuilder:resource:shortName=rst +// +kubebuilder:printcolumn:name="Backup",type="string",JSONPath=".spec.backupName",description="The name of the backup this restore is from" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Restore status such as New/InProgress" +// +kubebuilder:printcolumn:name="Errors",type="integer",JSONPath=".status.errors",description="Total number of errors logged during the restore" +// +kubebuilder:printcolumn:name="Warnings",type="integer",JSONPath=".status.warnings",description="Total number of warnings logged during the restore" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // Restore is a Velero resource that represents the application of // resources from a Velero backup to a target Kubernetes cluster. From 105350b78b56c0a18d395baa46bb6344d89f753a Mon Sep 17 00:00:00 2001 From: Ralthos <161431341+Ralthos@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:03:02 +0530 Subject: [PATCH 190/194] Make restore logs testable by returning errors (#10234) pkg/cmd/cli/restore/logs.go was the last command in the CLI still calling cmd.Exit, which calls os.Exit directly. Two of its own tests were skipped because of it, and said so: t.Skip("Cannot test restore not complete case due to cmd.Exit() call") This gives restore logs the LogsOptions shape that backup logs already uses: Complete, BindFlags and Run returning an error, with the cobra command passing that to cmd.CheckError. Both skipped tests now run and assert on the returned errors. Exit status is unchanged; cmd.CheckError also exits 1. The two refusal messages now carry the standard "An error occurred:" prefix and match the wording backup logs uses. Signed-off-by: saral --- changelogs/unreleased/10234-Ralthos | 1 + pkg/cmd/cli/restore/logs.go | 108 ++++++++++++++++++---------- pkg/cmd/cli/restore/logs_test.go | 30 +++++--- 3 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 changelogs/unreleased/10234-Ralthos diff --git a/changelogs/unreleased/10234-Ralthos b/changelogs/unreleased/10234-Ralthos new file mode 100644 index 000000000..70ac3000a --- /dev/null +++ b/changelogs/unreleased/10234-Ralthos @@ -0,0 +1 @@ +Make velero restore logs return errors instead of calling os.Exit directly, matching velero backup logs, and enable the two previously skipped tests diff --git a/pkg/cmd/cli/restore/logs.go b/pkg/cmd/cli/restore/logs.go index 26d3123ac..366fd511e 100644 --- a/pkg/cmd/cli/restore/logs.go +++ b/pkg/cmd/cli/restore/logs.go @@ -23,8 +23,9 @@ import ( "time" "github.com/spf13/cobra" + "github.com/spf13/pflag" apierrors "k8s.io/apimachinery/pkg/api/errors" - ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/client" @@ -34,59 +35,94 @@ import ( "github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest" ) -func NewLogsCommand(f client.Factory) *cobra.Command { +// LogsOptions holds the state for the restore logs command, mirroring +// pkg/cmd/cli/backup.LogsOptions so both commands are shaped the same way. +type LogsOptions struct { + Timeout time.Duration + InsecureSkipTLSVerify bool + CaCertFile string + Client kbclient.Client + RestoreName string +} + +func NewLogsOptions() LogsOptions { config, err := client.LoadConfig() if err != nil { fmt.Fprintf(os.Stderr, "WARNING: Error reading config file: %v\n", err) } - timeout := time.Minute - insecureSkipTLSVerify := false - caCertFile := config.CACertFile() + return LogsOptions{ + Timeout: time.Minute, + InsecureSkipTLSVerify: false, + CaCertFile: config.CACertFile(), + } +} + +func (l *LogsOptions) BindFlags(flags *pflag.FlagSet) { + flags.DurationVar(&l.Timeout, "timeout", l.Timeout, "How long to wait to receive logs.") + flags.BoolVar(&l.InsecureSkipTLSVerify, "insecure-skip-tls-verify", l.InsecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") + flags.StringVar(&l.CaCertFile, "cacert", l.CaCertFile, "Path to a certificate bundle to use when verifying TLS connections. If not specified, the CA certificate from the BackupStorageLocation will be used if available.") +} + +func (l *LogsOptions) Run(c *cobra.Command, f client.Factory) error { + restore := new(velerov1api.Restore) + err := l.Client.Get(context.Background(), kbclient.ObjectKey{Namespace: f.Namespace(), Name: l.RestoreName}, restore) + if apierrors.IsNotFound(err) { + return fmt.Errorf("restore %q does not exist", l.RestoreName) + } else if err != nil { + return fmt.Errorf("error checking for restore %q: %v", l.RestoreName, err) + } + + switch restore.Status.Phase { + case velerov1api.RestorePhaseCompleted, velerov1api.RestorePhaseFailed, velerov1api.RestorePhasePartiallyFailed, velerov1api.RestorePhaseWaitingForPluginOperations, velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed: + // terminal and waiting for plugin operations phases, do nothing. + default: + return fmt.Errorf("logs for restore %q are not available until it's finished processing, please wait "+ + "until the restore has a phase of Completed or Failed and try again", l.RestoreName) + } + + // Get BSL cacert if available + bslCACert, err := cacert.GetCACertFromRestore(context.Background(), l.Client, f.Namespace(), restore) + if err != nil { + // Log the error but don't fail - we can still try to download without the BSL cacert + fmt.Fprintf(os.Stderr, "WARNING: Error getting cacert from BSL: %v\n", err) + bslCACert = "" + } + + return downloadrequest.StreamWithBSLCACert(context.Background(), l.Client, f.Namespace(), l.RestoreName, velerov1api.DownloadTargetKindRestoreLog, os.Stdout, l.Timeout, l.InsecureSkipTLSVerify, l.CaCertFile, bslCACert) +} + +func (l *LogsOptions) Complete(args []string, f client.Factory) error { + if len(args) > 0 { + l.RestoreName = args[0] + } + + kbClient, err := f.KubebuilderClient() + if err != nil { + return err + } + l.Client = kbClient + return nil +} + +func NewLogsCommand(f client.Factory) *cobra.Command { + l := NewLogsOptions() c := &cobra.Command{ Use: "logs RESTORE", Short: "Get restore logs", Args: cobra.ExactArgs(1), Run: func(c *cobra.Command, args []string) { - restoreName := args[0] - - kbClient, err := f.KubebuilderClient() + err := l.Complete(args, f) cmd.CheckError(err) - restore := new(velerov1api.Restore) - err = kbClient.Get(context.Background(), ctrlclient.ObjectKey{Namespace: f.Namespace(), Name: restoreName}, restore) - if apierrors.IsNotFound(err) { - cmd.Exit("Restore %q does not exist.", restoreName) - } else if err != nil { - cmd.Exit("Error checking for restore %q: %v", restoreName, err) - } - - switch restore.Status.Phase { - case velerov1api.RestorePhaseCompleted, velerov1api.RestorePhaseFailed, velerov1api.RestorePhasePartiallyFailed, velerov1api.RestorePhaseWaitingForPluginOperations, velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed: - // terminal and waiting for plugin operations phases, don't exit. - default: - cmd.Exit("Logs for restore %q are not available until it's finished processing. Please wait "+ - "until the restore has a phase of Completed or Failed and try again.", restoreName) - } - - // Get BSL cacert if available - bslCACert, err := cacert.GetCACertFromRestore(context.Background(), kbClient, f.Namespace(), restore) - if err != nil { - // Log the error but don't fail - we can still try to download without the BSL cacert - fmt.Fprintf(os.Stderr, "WARNING: Error getting cacert from BSL: %v\n", err) - bslCACert = "" - } - - err = downloadrequest.StreamWithBSLCACert(context.Background(), kbClient, f.Namespace(), restoreName, velerov1api.DownloadTargetKindRestoreLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile, bslCACert) + err = l.Run(c, f) cmd.CheckError(err) }, } c.ValidArgsFunction = cli.CompleteRestoreNames(f) - c.Flags().DurationVar(&timeout, "timeout", timeout, "How long to wait to receive logs.") - c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") - c.Flags().StringVar(&caCertFile, "cacert", caCertFile, "Path to a certificate bundle to use when verifying TLS connections. If not specified, the CA certificate from the BackupStorageLocation will be used if available.") + l.BindFlags(c.Flags()) return c } diff --git a/pkg/cmd/cli/restore/logs_test.go b/pkg/cmd/cli/restore/logs_test.go index 61c2392b6..5e020bf43 100644 --- a/pkg/cmd/cli/restore/logs_test.go +++ b/pkg/cmd/cli/restore/logs_test.go @@ -17,10 +17,12 @@ limitations under the License. package restore import ( + "fmt" "os" "testing" "time" + flag "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" kbclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -77,13 +79,20 @@ func TestNewLogsCommand(t *testing.T) { c := NewLogsCommand(f) assert.Equal(t, "Get restore logs", c.Short) - // The restore command exits with an error message when restore is not complete - // We can't easily test this since it calls cmd.Exit, which exits the process - // So we'll skip this test case - t.Skip("Cannot test restore not complete case due to cmd.Exit() call") + l := NewLogsOptions() + flags := new(flag.FlagSet) + l.BindFlags(flags) + err = l.Complete([]string{restoreName}, f) + require.NoError(t, err) + + err = l.Run(c, f) + require.Error(t, err) + require.ErrorContains(t, err, fmt.Sprintf("logs for restore %q are not available until it's finished processing", restoreName)) }) t.Run("Restore not exist test", func(t *testing.T) { + restoreName := "not-exist" + // create a factory f := &factorymocks.Factory{} @@ -95,10 +104,15 @@ func TestNewLogsCommand(t *testing.T) { c := NewLogsCommand(f) assert.Equal(t, "Get restore logs", c.Short) - // The restore command exits with an error message when restore doesn't exist - // We can't easily test this since it calls cmd.Exit, which exits the process - // So we'll skip this test case - t.Skip("Cannot test restore not exist case due to cmd.Exit() call") + l := NewLogsOptions() + flags := new(flag.FlagSet) + l.BindFlags(flags) + err := l.Complete([]string{restoreName}, f) + require.NoError(t, err) + + err = l.Run(c, f) + require.Error(t, err) + require.Equal(t, fmt.Sprintf("restore %q does not exist", restoreName), err.Error()) }) t.Run("Restore with BSL cacert test", func(t *testing.T) { From 1832f099f296366276668b484653e926da9c8ad4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:23:56 -0400 Subject: [PATCH 191/194] backport: add pre-merge label-on-comment support; add copilot-instructions and auto-labeler for changelog exemptions (#10211) * Initial plan * chore: commit backport.yml pre-merge labeling enhancement Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> * chore: auto-label kind/changelog-not-required for non-shipping PRs; add copilot-instructions.md Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> * chore: add changelog naming convention to copilot-instructions; tighten labeler exclusions Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> * fix: use # as sed delimiter to avoid conflict with | in alternation group Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> --- .github/copilot-instructions.md | 70 +++++++++++++++++++++++++ .github/labeler.yml | 22 ++++++++ .github/workflows/backport.yml | 93 +++++++++++++++++++++++++++++++-- 3 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..c4e7d9923 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,70 @@ +# GitHub Copilot instructions for velero-io/velero + +## Changelog requirement + +Velero uses per-PR changelog fragments that are assembled into release notes. + +### File naming convention + +Every shipping PR must add exactly **one** file at: + +``` +changelogs/unreleased/- +``` + +- `` is the pull request number (e.g. `10200`). +- `` is the GitHub login of the PR author (e.g. `jdoe`). +- The file has **no extension**. +- The file content is a single line describing the change (the PR title is a + sensible default). + +**Example:** PR #10200 by `jdoe` → `changelogs/unreleased/10200-jdoe` + +The easiest way to create this file is: + +```bash +make new-changelog CHANGELOG_BODY="Brief description of the change" +``` + +`make new-changelog` reads the PR number and author from `gh pr view`; the file +is written automatically to the correct path with the correct name. + +The CI check (`hack/changelog-check.sh`) looks for +`changelogs/unreleased/-*` and fails if no file is found. + +### When a changelog is NOT required + +A changelog entry is **not** required when a PR exclusively changes non-shipping +content, i.e. the only files touched belong to one or more of these categories: + +| Category | Paths | +|---|---| +| GitHub Actions / CI workflows | `.github/**` | +| Documentation | `site/content/docs/**`, `site/**`, `docs/**`, `*.md` | +| Website (non-docs) | `site/**` (excluding `site/content/docs/**`) | + +When you open or review a PR that falls into one of the above categories (and does +**not** modify `pkg/`, `internal/`, `cmd/`, `vendor/`, `hack/`, `Makefile`, +`go.mod`, `go.sum`, or `changelogs/**`), apply the label +**`kind/changelog-not-required`** instead of requesting a changelog entry. The +`labeler.yml` auto-labeler handles this automatically for most cases; apply the +label manually if the auto-labeler did not. + +## Backport / cherry-pick workflow + +Velero uses `.github/workflows/backport.yml` to automate cherry-picks onto release +branches. + +- **Before merge:** comment `/backport release-1.17` (or `/cherrypick release-1.17`) + to add the label `backport release-1.17` to the PR. Multiple branches can be + space-delimited: `/backport release-1.17 release-1.18`. The label causes the + backport to run automatically when the PR merges. +- **After merge:** the same comment immediately creates the backport PR. +- Only repository **owners, members, and collaborators** may trigger these commands. + +## General coding guidelines + +- Follow the existing code style of the file being edited. +- Add unit tests for new exported functions in `pkg/`. +- Do not commit secrets, credentials, or API tokens. +- Keep PRs focused; prefer small, reviewable changes over large omnibus PRs. diff --git a/.github/labeler.yml b/.github/labeler.yml index 183f8365f..880977caf 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -31,3 +31,25 @@ has-e2e-2tests: has-unit-tests: - changed-files: - any-glob-to-any-file: pkg/**/*_test.go +# PRs that only touch non-shipping files (.github/ config, workflows, or docs) +# do not need a changelog entry; auto-apply the label so the changelog check passes. +kind/changelog-not-required: + - all: + - changed-files: + - any-glob-to-any-file: + - .github/**/* + - site/content/docs/**/* + - site/**/* + - '*.md' + - docs/**/* + - all-globs-to-all-files: + - '!pkg/**' + - '!internal/**' + - '!cmd/**' + - '!vendor/**' + - '!hack/**' + - '!Makefile' + - '!go.mod' + - '!go.sum' + - '!changelogs/**' + - '!**/*.go' diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index d0e13129e..670e16103 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,8 +1,21 @@ name: Backport merged pull request # Automates cherry-picking merged PRs onto release branches. -# - Label a merged PR with e.g. `backport release-1.17` to backport on merge. -# - Or comment `/backport release-1.17` or `/cherrypick release-1.17` on a merged PR. +# +# Pre-merge (open PR): +# An authorized /backport or /cherrypick comment adds one `backport ` +# label per requested branch. These labels are then picked up automatically +# when the PR is merged (see the pull_request_target: closed trigger below). +# +# Post-merge (merged PR): +# - Label a PR with e.g. `backport release-1.17` before merging; the label +# triggers the backport automatically when the PR closes as merged. +# - Comment `/backport release-1.17` or `/cherrypick release-1.17` on an +# already-merged PR to create the backport PR immediately. +# +# In both cases multiple target branches can be space-delimited in a comment: +# /backport release-1.17 release-1.18 +# # See: https://github.com/velero-io/velero/issues/9603 on: @@ -13,7 +26,78 @@ on: permissions: {} +# Shared condition for authorized /backport or /cherrypick comments. +# Used by both jobs below to avoid duplicating the gate logic. +env: + AUTHORIZED_COMMENT: >- + ${{ + github.event_name == 'issue_comment' && + github.event.issue.pull_request != '' && + github.event.comment.user.id != 97796249 && + contains( + fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), + github.event.comment.author_association + ) && + ( + startsWith(github.event.comment.body, '/backport') || + startsWith(github.event.comment.body, '/cherrypick') + ) + }} + jobs: + # ── Pre-merge: convert a /backport or /cherrypick comment into labels ─────── + # When the PR is still open the backport-action cannot run (it requires a + # merged commit). Instead, add one `backport ` label per requested + # branch so that the post-merge job picks them up automatically on close. + label-for-backport: + name: Label PR for deferred backport + # Run only when an authorized command is posted on an *open* (unmerged) PR. + if: > + github.repository == 'velero-io/velero' && + github.event_name == 'issue_comment' && + github.event.issue.pull_request != '' && + github.event.issue.state == 'open' && + github.event.comment.user.id != 97796249 && + contains( + fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), + github.event.comment.author_association + ) && + ( + startsWith(github.event.comment.body, '/backport') || + startsWith(github.event.comment.body, '/cherrypick') + ) + runs-on: ubuntu-latest + permissions: + issues: write # apply labels to the PR (PRs share the issues API) + steps: + - name: Parse branches and apply labels + env: + COMMENT_BODY: ${{ github.event.comment.body }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.issue.number }} + run: | + # Extract branch names from the first line of the comment. + # Strip the /backport or /cherrypick prefix; what remains is a + # space-delimited list of target branch names. + line=$(printf '%s' "$COMMENT_BODY" | head -n1 | tr -d '\r') + branches=$(printf '%s' "$line" | sed -E 's#^/(backport|cherrypick)[[:space:]]*##') + + if [ -z "$branches" ]; then + echo "No target branches specified in comment; nothing to label." + exit 0 + fi + + for branch in $branches; do + label="backport ${branch}" + echo "Applying label: '${label}'" + # Create the label if it does not exist yet (idempotent). + gh label create "${label}" --repo "${REPO}" --color "0075ca" \ + --description "Backport to ${branch}" 2>/dev/null || true + gh issue edit "${PR_NUMBER}" --repo "${REPO}" --add-label "${label}" + done + + # ── Post-merge: create backport PRs ───────────────────────────────────────── backport: name: Backport pull request # Exclude comments from the backport-action bot (user id 97796249) to prevent @@ -28,7 +112,8 @@ jobs: contains(toJSON(github.event.pull_request.labels.*.name), '"backport ') ) || ( github.event_name == 'issue_comment' && - github.event.issue.pull_request && + github.event.issue.pull_request != '' && + github.event.issue.state == 'closed' && github.event.comment.user.id != 97796249 && contains( fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), @@ -55,7 +140,7 @@ jobs: # Remaining text is a space-delimited list of target branches # (may be empty, falls back to labels). line=$(printf '%s' "$COMMENT_BODY" | head -n1 | tr -d '\r') - branches=$(printf '%s' "$line" | sed -E 's|^/(backport|cherrypick)[[:space:]]*||') + branches=$(printf '%s' "$line" | sed -E 's#^/(backport|cherrypick)[[:space:]]*##') echo "branches=${branches}" >> "$GITHUB_OUTPUT" - uses: actions/checkout@v7 From 9cb2c25eb8dd126ee0a8344806a0301898467bd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:28:38 -0400 Subject: [PATCH 192/194] Bump kentaro-m/auto-assign-action from 2.0.0 to 2.0.2 (#10201) Bumps [kentaro-m/auto-assign-action](https://github.com/kentaro-m/auto-assign-action) from 2.0.0 to 2.0.2. - [Release notes](https://github.com/kentaro-m/auto-assign-action/releases) - [Commits](https://github.com/kentaro-m/auto-assign-action/compare/v2.0.0...v2.0.2) --- updated-dependencies: - dependency-name: kentaro-m/auto-assign-action dependency-version: 2.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto_assign_prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto_assign_prs.yml b/.github/workflows/auto_assign_prs.yml index b51fde199..a1ea2fb79 100644 --- a/.github/workflows/auto_assign_prs.yml +++ b/.github/workflows/auto_assign_prs.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Set the author of a PR as the assignee - uses: kentaro-m/auto-assign-action@v2.0.0 + uses: kentaro-m/auto-assign-action@v2.0.2 with: configuration-path: ".github/auto-assignees.yml" From 0ba74dbf510bf63b3eb125aaadeab9a386f9451c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:32:17 -0400 Subject: [PATCH 193/194] Group Dependabot GitHub Actions updates (#10220) * Initial plan * Group Dependabot GitHub Actions updates Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 682c01231..a26f3eedf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,10 @@ updates: directory: "/" schedule: interval: "weekly" + groups: + github-actions: + patterns: + - "*" labels: - "Dependencies" - "github_actions" From c303809857fec756531379d7a045e8167c2595af Mon Sep 17 00:00:00 2001 From: Krishna Awasthi <140143710+opbot-xd@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:12:48 +0530 Subject: [PATCH 194/194] Enhancement: Add missing test assertions for PVCBackupSummary in podvolume backupper (#10218) Signed-off-by: opbot_xd --- changelogs/unreleased/10218-opbot-xd | 1 + pkg/podvolume/backupper_test.go | 36 ++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/10218-opbot-xd diff --git a/changelogs/unreleased/10218-opbot-xd b/changelogs/unreleased/10218-opbot-xd new file mode 100644 index 000000000..d7b291f3c --- /dev/null +++ b/changelogs/unreleased/10218-opbot-xd @@ -0,0 +1 @@ +Add missing test assertions for PVCBackupSummary in podvolume backupper diff --git a/pkg/podvolume/backupper_test.go b/pkg/podvolume/backupper_test.go index 59466e02a..1ef4297af 100644 --- a/pkg/podvolume/backupper_test.go +++ b/pkg/podvolume/backupper_test.go @@ -380,6 +380,8 @@ func TestBackupPodVolumes(t *testing.T) { pvbs int mockGetRepositoryType bool errs []string + expectedBackedup []string + expectedSkipped map[string]string }{ { name: "empty volume list", @@ -573,6 +575,10 @@ func TestBackupPodVolumes(t *testing.T) { uploaderType: "kopia", bsl: "fake-bsl", errs: []string{}, + expectedSkipped: map[string]string{ + "fake-volume-1": "volume fake-volume-1 is declared in pod fake-ns/fake-pod but not mounted by any container, skipping", + "fake-volume-2": "volume fake-volume-2 is declared in pod fake-ns/fake-pod but not mounted by any container, skipping", + }, }, { name: "return completed pvbs", @@ -589,14 +595,14 @@ func TestBackupPodVolumes(t *testing.T) { ctlClientObj: []runtime.Object{ createBackupRepoObj(), }, - runtimeScheme: scheme, - uploaderType: "kopia", - bsl: "fake-bsl", - pvbs: 1, - errs: []string{}, + runtimeScheme: scheme, + uploaderType: "kopia", + bsl: "fake-bsl", + pvbs: 1, + errs: []string{}, + expectedBackedup: []string{"fake-volume-1"}, }, } - // TODO add more verification around PVCBackupSummary returned by "BackupPodVolumes" for _, test := range tests { t.Run(test.name, func(t *testing.T) { ctx := t.Context() @@ -627,7 +633,7 @@ func TestBackupPodVolumes(t *testing.T) { funcGetRepositoryType = getRepositoryType } - pvbs, _, errs := bp.BackupPodVolumes(backupObj, test.sourcePod, test.volumes, nil, velerotest.NewLogger()) + pvbs, summary, errs := bp.BackupPodVolumes(backupObj, test.sourcePod, test.volumes, nil, velerotest.NewLogger()) if test.errs != nil { for i := 0; i < len(errs); i++ { @@ -636,6 +642,22 @@ func TestBackupPodVolumes(t *testing.T) { } assert.Len(t, pvbs, test.pvbs) + + if summary != nil { + assert.Len(t, summary.Backedup, len(test.expectedBackedup)) + for _, vol := range test.expectedBackedup { + assert.Contains(t, summary.Backedup, vol) + } + + assert.Len(t, summary.Skipped, len(test.expectedSkipped)) + for vol, reason := range test.expectedSkipped { + require.Contains(t, summary.Skipped, vol) + assert.Equal(t, reason, summary.Skipped[vol].Reason) + } + } else { + assert.Empty(t, test.expectedBackedup) + assert.Empty(t, test.expectedSkipped) + } }) } }