From cbf07f07d9cd2661f249b27ef1f71934a515455e Mon Sep 17 00:00:00 2001 From: Adam Martin Date: Sat, 4 Jul 2026 14:11:03 -0400 Subject: [PATCH] updates for helm v4 (#648) Signed-off-by: Adam Martin --- cmd/hauler/cli/store.go | 2 +- cmd/hauler/cli/store/add.go | 26 +++--- cmd/hauler/cli/store/add_test.go | 8 +- cmd/hauler/cli/store/sync.go | 2 +- cmd/hauler/cli/store/testhelpers_test.go | 2 +- go.mod | 20 +++-- go.sum | 41 ++++++--- internal/flags/add.go | 4 +- pkg/content/chart/chart.go | 104 +++++++++++++++++++---- pkg/content/chart/chart_test.go | 26 +++++- testdata/mismatched-filename-chart.tgz | Bin 0 -> 14970 bytes 11 files changed, 181 insertions(+), 54 deletions(-) create mode 100644 testdata/mismatched-filename-chart.tgz diff --git a/cmd/hauler/cli/store.go b/cmd/hauler/cli/store.go index 87da7a5..17cccfd 100644 --- a/cmd/hauler/cli/store.go +++ b/cmd/hauler/cli/store.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/spf13/cobra" - "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v4/pkg/action" "hauler.dev/go/hauler/v2/cmd/hauler/cli/store" "hauler.dev/go/hauler/v2/internal/flags" diff --git a/cmd/hauler/cli/store/add.go b/cmd/hauler/cli/store/add.go index 8cb9762..a281131 100644 --- a/cmd/hauler/cli/store/add.go +++ b/cmd/hauler/cli/store/add.go @@ -11,9 +11,11 @@ import ( "github.com/google/go-containerregistry/pkg/name" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - helmchart "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chartutil" - "helm.sh/helm/v3/pkg/engine" + "helm.sh/helm/v4/pkg/chart/common" + commonutil "helm.sh/helm/v4/pkg/chart/common/util" + helmchart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/engine" "k8s.io/apimachinery/pkg/util/yaml" "hauler.dev/go/hauler/v2/internal/flags" @@ -462,9 +464,13 @@ func storeChart(ctx context.Context, s *store.Layout, cfg v1.Chart, opts *flags. defer os.RemoveAll(tempDir) chartPath := chrt.Path() - if strings.HasSuffix(chartPath, ".tgz") { + chartPathInfo, err := os.Stat(chartPath) + if err != nil { + return fmt.Errorf("failed to stat chart path '%s': %w", chartPath, err) + } + if !chartPathInfo.IsDir() { l.Debugf("%sextracting chart archive [%s]", prefix, filepath.Base(chartPath)) - if err := chartutil.ExpandFile(tempDir, chartPath); err != nil { + if err := util.ExpandFile(tempDir, chartPath); err != nil { return fmt.Errorf("failed to extract chart: %w", err) } @@ -478,20 +484,20 @@ func storeChart(ctx context.Context, s *store.Layout, cfg v1.Chart, opts *flags. // add-images if opts.AddImages { - userValues := chartutil.Values{} + userValues := common.Values{} if opts.HelmValues != "" { - userValues, err = chartutil.ReadValuesFile(opts.HelmValues) + userValues, err = common.ReadValuesFile(opts.HelmValues) if err != nil { return fmt.Errorf("failed to read helm values file [%s]: %w", opts.HelmValues, err) } } // set helm default capabilities - caps := chartutil.DefaultCapabilities.Copy() + caps := common.DefaultCapabilities.Copy() // only parse and override if provided kube version if opts.KubeVersion != "" { - kubeVersion, err := chartutil.ParseKubeVersion(opts.KubeVersion) + kubeVersion, err := common.ParseKubeVersion(opts.KubeVersion) if err != nil { l.Warnf("%sinvalid kube-version [%s], using default kubernetes version", prefix, opts.KubeVersion) } else { @@ -499,7 +505,7 @@ func storeChart(ctx context.Context, s *store.Layout, cfg v1.Chart, opts *flags. } } - values, err := chartutil.ToRenderValues(c, userValues, chartutil.ReleaseOptions{Namespace: "hauler"}, caps) + values, err := commonutil.ToRenderValues(c, userValues, common.ReleaseOptions{Namespace: "hauler"}, caps) if err != nil { return err } diff --git a/cmd/hauler/cli/store/add_test.go b/cmd/hauler/cli/store/add_test.go index cfed0c6..f7a98f0 100644 --- a/cmd/hauler/cli/store/add_test.go +++ b/cmd/hauler/cli/store/add_test.go @@ -13,8 +13,8 @@ import ( "github.com/google/go-containerregistry/pkg/registry" "github.com/google/go-containerregistry/pkg/v1/remote" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - helmchart "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chartutil" + helmchart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/util" "hauler.dev/go/hauler/v2/internal/flags" v1 "hauler.dev/go/hauler/v2/pkg/apis/hauler.cattle.io/v1" @@ -767,9 +767,9 @@ func seedChartWithImages(t *testing.T, dir string, images []string) string { }, } - saved, err := chartutil.Save(c, dir) + saved, err := util.Save(c, dir) if err != nil { - t.Fatalf("seedChartWithImages: chartutil.Save: %v", err) + t.Fatalf("seedChartWithImages: util.Save: %v", err) } return saved } diff --git a/cmd/hauler/cli/store/sync.go b/cmd/hauler/cli/store/sync.go index f70845b..c4e73c8 100644 --- a/cmd/hauler/cli/store/sync.go +++ b/cmd/hauler/cli/store/sync.go @@ -16,7 +16,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/mitchellh/go-homedir" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v4/pkg/action" "k8s.io/apimachinery/pkg/util/yaml" "hauler.dev/go/hauler/v2/internal/flags" diff --git a/cmd/hauler/cli/store/testhelpers_test.go b/cmd/hauler/cli/store/testhelpers_test.go index fb78795..b44f59d 100644 --- a/cmd/hauler/cli/store/testhelpers_test.go +++ b/cmd/hauler/cli/store/testhelpers_test.go @@ -25,7 +25,7 @@ import ( digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/rs/zerolog" - "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v4/pkg/action" "hauler.dev/go/hauler/v2/internal/flags" "hauler.dev/go/hauler/v2/pkg/consts" diff --git a/go.mod b/go.mod index 93a14c9..b2487c0 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/spf13/cobra v1.10.2 golang.org/x/sync v0.21.0 gopkg.in/yaml.v3 v3.0.1 - helm.sh/helm/v3 v3.21.2 + helm.sh/helm/v4 v4.2.2 k8s.io/apimachinery v0.36.2 ) @@ -56,6 +56,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/STARRY-S/zip v0.2.3 // indirect github.com/ThalesIgnite/crypto11 v1.2.5 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect @@ -107,6 +108,7 @@ require ( github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/uax29/v2 v2.6.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -129,16 +131,19 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emicklei/proto v1.14.3 // indirect - github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.18.0 // indirect + github.com/extism/go-sdk v1.7.1 // indirect + github.com/fatih/color v1.19.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fluxcd/cli-utils v1.2.1 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-chi/chi/v5 v5.3.0 // indirect - github.com/go-errors/errors v1.4.2 // indirect + github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -169,6 +174,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.6 // indirect + github.com/gofrs/flock v0.13.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.3 // indirect @@ -183,13 +189,12 @@ require ( github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect github.com/in-toto/attestation v1.2.0 // indirect github.com/in-toto/in-toto-golang v0.11.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -279,6 +284,8 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tchap/go-patricia/v2 v2.3.3 // indirect + github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect + github.com/tetratelabs/wazero v1.12.0 // indirect github.com/thales-e-security/pool v0.0.2 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect @@ -353,6 +360,7 @@ require ( k8s.io/kubectl v0.36.2 // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect oras.land/oras-go/v2 v2.6.1 // indirect + sigs.k8s.io/controller-runtime v0.24.1 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect diff --git a/go.sum b/go.sum index 0604b85..9b94aff 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E= @@ -260,6 +262,8 @@ github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+Urai github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -340,6 +344,8 @@ github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj6 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.14.3 h1:zEhlzNkpP8kN6utonKMzlPfIvy82t5Kb9mufaJxSe1Q= @@ -348,14 +354,18 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= -github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= +github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/cli-utils v1.2.1 h1:ug9CicKW7H9QXnvNDapTSKuryZvWcu4Nw7pRvQa6jDY= +github.com/fluxcd/cli-utils v1.2.1/go.mod h1:cky6M6eHvTQkoPtsuFYLIgAMYdpTCSLoor4IA6vueSw= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= @@ -371,8 +381,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= @@ -387,6 +397,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw= github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= @@ -461,6 +473,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -559,7 +573,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDa github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -595,6 +608,8 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw= +github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpahjQk= github.com/in-toto/attestation v1.2.0/go.mod h1:r79G45gOmzPismgObLSL+rZTFxUgZLOQJI6LofTZgXk= github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA= @@ -774,8 +789,6 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -925,6 +938,10 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= +github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= +github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= @@ -1353,8 +1370,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -helm.sh/helm/v3 v3.21.2 h1:O8ktw30zQZm4tNRavKRN44rotcHICdbSU3e6gS7CCbQ= -helm.sh/helm/v3 v3.21.2/go.mod h1:Da+9m67Mzg42rQNrWAj0RvUd4tDT4dL8mrxUwvRgYWo= +helm.sh/helm/v4 v4.2.2 h1:E2zSCA2uUm9PNiZsSC/BioDVGsYk7nF2jNJFg/i+Dng= +helm.sh/helm/v4 v4.2.2/go.mod h1:dp3ihfy1AhCLKANDaPETmVWhqPkOmwvJtpK/biHfopE= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1389,6 +1406,8 @@ pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= 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/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= diff --git a/internal/flags/add.go b/internal/flags/add.go index 7613df0..9c8f45b 100644 --- a/internal/flags/add.go +++ b/internal/flags/add.go @@ -2,7 +2,7 @@ package flags import ( "github.com/spf13/cobra" - "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v4/pkg/action" ) type AddImageOpts struct { @@ -70,7 +70,7 @@ func (o *AddChartOpts) AddFlags(cmd *cobra.Command) { f.StringVar(&o.ChartOpts.Password, "password", "", "(Optional) Password to use for authentication") f.StringVar(&o.ChartOpts.CertFile, "cert-file", "", "(Optional) Location of the TLS Certificate to use for client authentication") f.StringVar(&o.ChartOpts.KeyFile, "key-file", "", "(Optional) Location of the TLS Key to use for client authentication") - f.BoolVar(&o.ChartOpts.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "(Optional) Skip TLS certificate verification") + f.BoolVar(&o.ChartOpts.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "(Optional) Skip TLS certificate verification") f.StringVar(&o.ChartOpts.CaFile, "ca-file", "", "(Optional) Location of CA Bundle to enable certification verification") f.StringVar(&o.Rewrite, "rewrite", "", "(Optional) Rewrite artifact path to specified string") diff --git a/pkg/content/chart/chart.go b/pkg/content/chart/chart.go index b83741c..1ab2712 100644 --- a/pkg/content/chart/chart.go +++ b/pkg/content/chart/chart.go @@ -4,10 +4,13 @@ import ( "archive/tar" "bytes" "compress/gzip" + "crypto/tls" + "crypto/x509" "encoding/json" "fmt" "io" "io/fs" + "net/http" "net/url" "os" "path/filepath" @@ -17,12 +20,11 @@ import ( gtypes "github.com/google/go-containerregistry/pkg/v1/types" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "hauler.dev/go/hauler/v2/pkg/artifacts" - "hauler.dev/go/hauler/v2/pkg/log" - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/registry" + "helm.sh/helm/v4/pkg/action" + "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/loader" + "helm.sh/helm/v4/pkg/cli" + "helm.sh/helm/v4/pkg/registry" "hauler.dev/go/hauler/v2/pkg/consts" "hauler.dev/go/hauler/v2/pkg/layer" @@ -43,7 +45,7 @@ type Chart struct { func NewChart(name string, opts *action.ChartPathOptions) (*Chart, error) { chartRef := name actionConfig := new(action.Configuration) - if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), log.NewLogger(os.Stdout).Debugf); err != nil { + if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER")); err != nil { return nil, err } @@ -51,7 +53,7 @@ func NewChart(name string, opts *action.ChartPathOptions) (*Chart, error) { client.ChartPathOptions.Version = opts.Version registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, - client.InsecureSkipTLSverify, client.PlainHTTP) + client.InsecureSkipTLSVerify, client.PlainHTTP) if err != nil { return nil, fmt.Errorf("missing registry client: %w", err) } @@ -148,7 +150,7 @@ func (h *Chart) configDescriptor() (gv1.Descriptor, error) { }, nil } -func (h *Chart) Load() (*chart.Chart, error) { +func (h *Chart) Load() (*v2.Chart, error) { return loader.Load(h.path) } @@ -234,8 +236,25 @@ func (h *Chart) chartData() (gv1.Layer, error) { chartdata = data } + // title defaults to the downloaded file's basename. Helm v4's + // ChartPathOptions.LocateChart downloads any non-local chart (HTTP repo or + // OCI) into a content-addressed cache and returns a hash-named path (e.g. + // ".chart", using the literal extension defined by + // downloader.CacheChart) rather than a human-readable filename, so + // filepath.Base(h.path) is meaningless for those sources. In that specific + // case only, prefer the canonical "-.tgz" form derived from + // the chart's own Chart.yaml metadata. Genuinely local archives (".tgz" or + // any other extension a user's file might carry) keep their real filename, + // even if it doesn't follow the "-.tgz" convention. + title := filepath.Base(h.path) + if !info.IsDir() && filepath.Ext(h.path) == ".chart" { + if ch, err := loader.Load(h.path); err == nil && ch.Metadata != nil && ch.Metadata.Name != "" && ch.Metadata.Version != "" { + title = fmt.Sprintf("%s-%s.tgz", ch.Metadata.Name, ch.Metadata.Version) + } + } + annotations := make(map[string]string) - annotations[ocispec.AnnotationTitle] = filepath.Base(h.path) + annotations[ocispec.AnnotationTitle] = title opener := func() layer.Opener { return func() (io.ReadCloser, error) { @@ -287,14 +306,27 @@ func newDefaultRegistryClient(plainHTTP bool) (*registry.Client, error) { return registryClient, nil } +// newRegistryClientWithTLS builds a registry client backed by an HTTP client with a custom +// TLS config. Helm v4 removed the registry.NewRegistryClientWithTLS convenience wrapper (it +// delegated to helm's internal/tlsutil package, which is not importable outside the helm +// module), so the TLS config construction is inlined here to match its prior behavior. func newRegistryClientWithTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*registry.Client, error) { - // create a new registry client - registryClient, err := registry.NewRegistryClientWithTLS( - io.Discard, - certFile, keyFile, caFile, - insecureSkipTLSverify, - settings.RegistryConfig, - settings.Debug, + tlsConf, err := newTLSConfig(certFile, keyFile, caFile, insecureSkipTLSverify) + if err != nil { + return nil, fmt.Errorf("can't create TLS config for client: %w", err) + } + + registryClient, err := registry.NewClient( + registry.ClientOptDebug(settings.Debug), + registry.ClientOptEnableCache(true), + registry.ClientOptWriter(io.Discard), + registry.ClientOptCredentialsFile(settings.RegistryConfig), + registry.ClientOptHTTPClient(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConf, + Proxy: http.ProxyFromEnvironment, + }, + }), ) if err != nil { return nil, err @@ -302,6 +334,44 @@ func newRegistryClientWithTLS(certFile, keyFile, caFile string, insecureSkipTLSv return registryClient, nil } +// newTLSConfig constructs a *tls.Config from the given cert/key/CA files, mirroring the +// behavior of helm's internal tlsutil.NewTLSConfig. +func newTLSConfig(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*tls.Config, error) { + config := &tls.Config{ + InsecureSkipVerify: insecureSkipTLSverify, + } + + if certFile != "" && keyFile != "" { + certPEMBlock, err := os.ReadFile(certFile) + if err != nil { + return nil, fmt.Errorf("unable to read cert file: %q: %w", certFile, err) + } + keyPEMBlock, err := os.ReadFile(keyFile) + if err != nil { + return nil, fmt.Errorf("unable to read key file: %q: %w", keyFile, err) + } + cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) + if err != nil { + return nil, fmt.Errorf("unable to load cert from key pair: %w", err) + } + config.Certificates = []tls.Certificate{cert} + } + + if caFile != "" { + caPEMBlock, err := os.ReadFile(caFile) + if err != nil { + return nil, fmt.Errorf("can't read CA file: %q: %w", caFile, err) + } + cp := x509.NewCertPool() + if !cp.AppendCertsFromPEM(caPEMBlock) { + return nil, fmt.Errorf("failed to append certificates from pem block") + } + config.RootCAs = cp + } + + return config, nil +} + // path returns the local filesystem path to the chart archive or directory func (h *Chart) Path() string { return h.path diff --git a/pkg/content/chart/chart_test.go b/pkg/content/chart/chart_test.go index b6a65bc..eff9304 100644 --- a/pkg/content/chart/chart_test.go +++ b/pkg/content/chart/chart_test.go @@ -7,7 +7,7 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v4/pkg/action" "hauler.dev/go/hauler/v2/pkg/consts" "hauler.dev/go/hauler/v2/pkg/content/chart" @@ -49,6 +49,30 @@ func TestNewChart(t *testing.T) { }, wantErr: false, }, + { + // Local chart archives keep the caller-provided filename as the OCI + // title annotation even when it doesn't follow the "-.tgz" + // convention (e.g. a user-renamed or repackaged chart file). Only + // Helm v4's content-addressed cache paths (".chart" extension) should + // fall back to a metadata-derived title. + name: "should preserve real filename for local archive with mismatched name", + args: args{ + name: "mismatched-filename-chart.tgz", + opts: &action.ChartPathOptions{RepoURL: "../../../testdata"}, + }, + want: v1.Descriptor{ + MediaType: consts.ChartLayerMediaType, + Size: 14970, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "0905de044a6e57cf3cd27bfc8482753049920050b10347ae2315599bd982a0e3", + }, + Annotations: map[string]string{ + ocispec.AnnotationTitle: "mismatched-filename-chart.tgz", + }, + }, + wantErr: false, + }, // TODO: This isn't matching digests b/c of file timestamps not being respected // { // name: "should create from a chart directory", diff --git a/testdata/mismatched-filename-chart.tgz b/testdata/mismatched-filename-chart.tgz new file mode 100644 index 0000000000000000000000000000000000000000..d596e49454644b4a1d57f39cbed9a011b66478cf GIT binary patch literal 14970 zcmZvj18gV2x9@A)+P1&iwr#uJ+O}=mwzXSrZMR$7w(b7j{$K9RyDzzu%*mWH$t06W z2H*1;q8JzqkpC1QH3+SdlnRrHlst!=7dN{xs|J&)D!Y}ADmRCsh6abcrmeM+y{VU) zvID=AnXMhjW%q_V0cRuC>dX`7YN?O3GwX$jN^0lw{NGu!tL4??Huc6XZ||4#emOPF zY;7StAuRQDwqB;22ety{9T)%I&jIZ=Gcd2Ji4J9F-V0%4upl7_h9Ds^5W~b!)uCV} zg?1r26wCA>CF1SQOc9C>c&->=^U<%Jo0k~{#U z@Cn*tbOtH_?8}bU{$92{@KW)s?t+Hph4)QZq%sgpbs*vghzOOT9OV`Y9BK(eIX{?1 zBqfuWC{sKtnsDd{Q=B;YZ!I!Qm}rwKaP&_k=6Gt{;5R2|3hC#y8-XiA50W9#aP*?& zZz%0hnCL!yevbaI5$Nsp23UJ+?c*2Mc$_Q%Nnhkh)qZJ+Xp|+5*g!-#VaUrCEY`v! z$5CWhDvX#IXK7PD?w1J$0O~*=W{o~x31u$v#T+GtFvAZ0`PMesY{XTh(^ueZEFCv^ z9d>ap4m5>qq{w=2dh_Jn<)!;`X$Ht0t2*QX07;(#_TR0oIXQg`@}IRn00I6HyI7$4 zED#WoOwk~*CXRixzo^ZrctFfCTs!lo0fDTq_~cZ40${hTsYRqHd$8#u#A1mALDZ%$ zWD#?ynGdGXWMY}l!WMZ<Ztp%pSI)H|`1 zYRE+U*!Z{9RU~2Iz==}cW*loRNYuff7@8J%sLm1bR}iVqDl8-DhyfYfBoc{2eAtB{ z*LO|VyRVFmIQW5sa?@70CvboAL(iBoNXJk$aZrO=nZ3h?_{}_4*hcn@VIWiwK~NS@ zMkKcLD~!Fxi$je`*x3KLvO$>OxR`1MD~Fna_q~#ndB(Qwirz9awS#!>2PBg37@;Bd zv&S{2;-WDHgCR9xY>1Uh24__fgYUw^={Ztw0VpD!IN5LKxVS-fadQu~2ZdXAu6#gX z%?1W3?#!NaRVl)E@qhlCKB}haHJQKDpGXyH)4a$Xa_6lg`I6Jv$a0GEE#K^7^FaSaX=OB?V2xXKj??;d}UQ8-c z*5;)w_?jZ5P2#!tmM~R2#v@WvEMgkkL!hW}`z$|zJme!T1wmjy7s-K0e@0G#q z^V26htXyugF$Xtp7Ugl~-48s;*_CtyBH|P(93UUn=A;pbRt6Tff2ADIvl_PfkEn{VF8H+;#I1h8>2qA+rmQy08it|de zm`E1ZS2eVzna!O_z*A}Z^i$4MS0YBB&{90b8)*FWLrwqZ%8du%6{~Icz^IJ*85@&T zQ%5DOqT@27DgI5ifLd`wfW2R76z$tX0Ifd6f==|I)rS(3?gDw?ieEyq2P~N|PnAPU zkr@kFnl4t05O~OEjvblEw;u}r z;aa|uYh@%r`g}lkbkZU^i2#HjwAN`C0Eb}X&^B7VrEJ>vr;{|TJ5m}GXrkD8Lg@S} zbAa(n3x_m&Zl0BSB$Ui9WPM)@lcI6k>;i{eZkK_o6a-cbVAi^HQTzu5qD^Il z8&cF+Bsei=`5kGTY>`G&MDT>qW3_C#cx#k@qC$fL7<`WV@9oo1T0pnkugT4jE$LU^5A7C5Terf10Db--5dCSOF!>q37%WrVj5eROmyzJ*JCb zJq>Kj&)`sd#^~}@*=32+w+;9Txbt(EQ6p`#Q*bW(t%tU*hl$L$)IWK_}4?y$i zeF)}l%yEhOKFkN;;iXkS&|_23<^(wSDd2nRx7F@lFxE~MQH+=CT1%N14RYqY_3>Es zuqO&a!ZrdC6td1-68;PdlZ29+S1EXBn;Jj* z%FhLZ6xbgoReBs-{1ObHLJ8DH)RFnh9>7Uxz)+Pg<~notm%wWc#%_cf$AoUBN|hnl zbq()B&RUXw5FDY3=L$p!!kPIkl*BP^h(x%<@R;hyp8Q({>K-CYu_F)L#E`%aFX|4( zX)Bfo5#H{&U0ldI5yTk;^DcayJzFRJJ_kRcX}Kj!8y-J#{gZ+pkRb}{_svJ5RGb9rw1=9~l8?sr&GtvY|*oSj(#n=66*_|10)$oDQLF4deI-EV~@ zqHce&st>}?Feq|=(C8$N8^N;-Zbl#r&Grsz>q38_8lsKs?Nl+GB7Cn$=ibsHo(!8T zX`ZG3)N!ETx%lIRSIQ!`3`My#Ti@-fhtFb)OC^S~^q@r3s}H^$x?pwsZpn)-i*2cB zUI-Pu!Y6y@O=gzlcCy0gJZhCsgF6|QflZT)0xb8ob8F&={5`M61~*)d19GvvV9NEu zwG#bupQ+8NVoUCUuhfXydGy-M^IT1`wro^UPm%CBQ8@mtL(_K^s0n7=8Kf5tyVOQ2 zB7Q+#+poN2ak|1o=GI>v4wT{+`iu9Qt)7iFX$8HjkwG01ZBS8zU+GXQa7I>U&9YX1SsRHo~BI3o(VkcRum{Y#eHn7n%| zctfsN3XHqAg07m*nDvXKM|8zClJKs~5gw$Juz2onf+zR0D_aHMsUnWpAn{gEbqMRm zGIzR5;a9TOABvFLpL7LL)d_3qvoK@17l{Wke7h?@JP3M)34q5H9Hc{{>r>22=&9;K z7S3eFQZBdY6l~>Qp{lB2)eUTw`N=rwr7?Ql*pXrv8hRtMg*yGWropR_qdR3zbe&FH z){{7RYCoB&Iq{YZ4sHMR2ks0zs%XdV>>WxBeVW&cK<2rtsCG*P5}qfbp4^}Y!4{|# z*(s`XJD^~vnrZjU+#=#3>&@vld3HGEremSo-}w!LMjwxL0Q$Wqs}9hgbfo^ zgOKX~#;hM;A)ZlT?L=ez@I9F|j<;l^7$JUzf$#&r0V*ntD^O2{EXuM$O~1uIRo01g z!0W_N%1n^ixQ+3TM$ULd{ z?Wi~+g6zOk z0-!6YbiKD4KQ!C~X3qrD?52uACy`J%uBMMpTk)-jOKh(zRlS0I{?^@r)Jk?Dwx>~E zILp!Sjx~Y%-TpfgXcHYoU!ouUxu;W0`aqzdm~N4vI1bC4!hXt3Rfk07W2AuuLBZUo z#7vc)-^2YW>e(MEE%AJJPtsu=TaWaRayqrzA8(^0pm$pvIs{|?Fmwd@AMFHa* zCLOv>6!goM14rN&U9l2WJ2VgN?31{)f8+>w<9K|aYdn5WkrQf-&+5p|FEx-!Q^`A} z6DOL!zqrc?v&~u8lYf`N@UjM)E-a9A7n0-UxmvxjdmsfL`f4rAW>9-`wnW3_zRkxb z8R7}Pc5JuJE4My8Xnq^;tmUW|UcXgYJ7o3X%3ze>l7Nj6>Tsfvhujt`*(U{tfZ;Me zIeH5pN5V{CaU@T_y96l`MCIaXPSf73W<#GGa=B0N@R^(P4#!mV=kiRBj(uid#4Jmn zkcUlCH<2${oXn=HDvLB{XJ#MRk}E5FY||&iXveX_$G(}#AoC8GNA{&0Rj;p(U4VSjQ0o`J0?bn&RPlq7AzsO?oa zPin*+ECY6~4lhb*=H@N;rV`B~^}+&cu<;wM{`CBp<{*vrlxxiFgGz0}xCc+|qh_sy zlJz~JB@GX8wkH!_k#BQ?aFe%)@+i2{Yb2mY;6f zM0*{~t@#8l-hH!+c34^21q#`_h2jzgK`7bzY!$wET=6HP8!>erqHRFY_Z~-8=y-}y z;c()V_d=UJ47O1SVA6|2$d`+upbF)u*csy45!olB>$h$Q8|7nJX;FNc&9jyn9(-CS zUrFsEu(H@u93&s@>zFPIx8tv;=oo91Y$X@Wm($g${*+j;!po9J=S9&HeXMJw9Syf$ z4)ANKEY}!r0NQvj#_=LsNxfgNQl4fVB)j3}8q~7BF z9atOT&wKu2^JJ;Jq@P}^r^BNG*d_w!ZM3fy>EeWPH<{B4a{smHU_M+a-lF*d*(s{v zn%CZ(R|TRlYCcDHSVq&foP75qUisNw$GtZ-z(IU@F;UFTF16PejBv$3Dp}M^Gm)?1 zFl|S14zk@rdCsV#=7K5gs|S?hb;j=UoKv05Q9V8eg=?lp%JbuBP_4U}q-gTYzkOhX zwlOlJ*>#n^fLMVy%R@I8^r!EtS)K-?k&6z0Z|R)i?;Am5t!h>u&Jl+nAYfq~nK}Tv z`(>t^iPx;7QFA`9o(|C%(%ZC-BR$9vO%l-E-9czbcmY2ioO4FKR0 zB+wEtIM<-eJ`)0wBt`o?m$WMif1#N3!dHFxJLmIp|`sn$#dTiE7 zrjDB#wapN#?3S0` zGe|U!x9v-*2C(bGdg7%=a91^=yfv5AWc8W?hRq=znu~jD6}e-(b~Tu3fT8Ip@&dp2 z3^D)V5}m0FddzK+*mR=XYSh9w{db*=h%dOQXr5;ZhMM#BqQY9CURqS`AaCS>hT?R; z$l-PCXBre*>#bi@wUxhvP(Be<+w@z$Ha(aErg!jO0$6FF6BN3J$p%d4FJ*2qZ+a1+ zzAC~#dKG6B3R+$Q1cwx_0k~x%cDzCDPCbqaUgh@8?uyhosC&8&mxMcu&+E75uVwFU;KT)qCPnlvnz8f-k^R7(PaQb`VEzQfqQL&mPf#lPq-28 zFDwhrfJpRIB6aHiwhoOHw?4ux%@P^@W>r9p%cJmEv0*1P&ff#kHE>3O`w%1$1ah^> zu1`!H_HDQFFVV+s2p4o~25F%U&RbHgQBux1+0`kZci)^R{s@wn& z+nB_&JuF6if)hViyFbD+ZorsHDKV7{S>ABB6sWrw1lS+A!ax^IDvjUG({TF&oMh^$ zmsc2ann50~*{!!PUPe$xsYT!IBJ>i0NF!YD!8ndkN>aGRy4?~yFHsy0+1-o3Ya-d| z?V$h0vi~qFl2~zmKf1g!o8}?c5m*#h(#)58J~~-K@zI{6yZ=#d!7#xNso8LfmXy{a z10Iw%SWj9XUABy=fy?TXu3To7Z}_NBJdid{owMx)5xE^wZ8Nw{te}cjEc@>T$J?|X zk1R%!Nz505e%K6Cj;pm)`Gk2WdmC9f_Sf38h@rOF!l zAdN#7)WHO(zJ+00v|Z)$iLamZMrUsIkC)H#?(OGS9+o4QjmGtz_g;!;gR0Uj7oJ;x zaPV8WkDG;*l$7Ex56Af);0JMUHB}yY#k%tpOZoTkSF@-g=J!KwJ1_(I;>V<#%(*Z+{&jVd80eM6=3>8>3l?;EH zNnFHs=|e&(Q>is%qlT@k;p!^IQJ>tCh*gE3^+v9O7({&{uwaM&BB`D<1vZ1UVVwM( z_WaUBU8s!`y8U7KrU|OOHlq=dND=u@VioW^UlokVXA=N3sEYr06{0G81#jud%GHii^i2AQiymxk70G!Cq`17)Meye#msaEBO2nYE3Ebs|>- zTVKD#xuL5C8>4N9{8Qjmr5P4;1>qtn-46q`LrAva?hM(|NjOUAm@lP3W#}iELC_~< zy@M8WK>D0^V43Iq&)^|={23^he*=U`{KIb8f3(lUhA_Rn>R0YkQ%t93`b9JfqW1aL>|L zGFXmU;r?v2x#O?cAFSJLT5OG|1K-;9YedryJ<{66dN z0Yv*RMrA-wACSkv!B_vh2yx(;2I@m1NqmD=tddiT=2#l!01l~&evn-wn6gv#+7>2~ zQhE^ra^}vz>jsB*Q0kwVq-{=8meJT`-0I(8rrc;sPc!6s!g36jI>8L)HF0MM>D!5F zRxGUL0E{F4D0z*a{6GBMqu%tAyg6lq(Y*7vN4R{GFh+IL8-R6&R}<#$_KeBakR_RQ zt%5vU+O*fh@`))GsqM}uE(>?Gm=X+yeMa&ZZ&`S`EL>G8oVmHW{k3Y050^IKBd>ns ziPJ2Fe5P7k=*nfkd9q{S;c6Qg!tU7@{kvkJ#2BDN%eVO4f$ZC z=**}eais^@$yOflhhB%4_$TNZ+?a-t{~6f*j?c$qC$O#^DB8fYfq3BdCy4er;HyG0 z=0s3Y;eGV#`v%}o`x2m?aAW$AkwBzbHn(E5MhljD{1E&X6*ZOqQdZ>o&-@Qd?&oX} z-hn4SS`N%pO#%2{RMI1~B6(~$|1<>DfSu22)9vhs0Jt@RstXWz4j}ZGMToG~cR}-W z0rXp4E>Jr`u<-80;m`D!oh?Sj+r#;Eg_wTex^x;@$mfLNetTsCD3!&UW66 zb*%h;;{e%I?7{O52?zYVnoS&2?+1ta54-Q$TP@r`bx-|7=9b9A9n4I+F&ee31lutD zI_ey2K`YV4ri5yFixMvP=C)^(gI_lPYR<@r#m!!}U-E#i9G&(3kxG_Kuk}lexw99? zRc-*LM=!V5!XFB6S{qKlv_+CPwsT0Ir0Qlo^+^+zk#8Rqm%%*DFp|oXUK~5PUEnej z#7PsLRSK$!P~D-UK#dazDzozf{C^~iMqZN#?FL?V5F+miyRHDiu{ZK^tdnr#&%F_^bk^Fz$@GtXopC<_^W_5kc9Rp=pM%#qZ>8yxzejmPA8AoD2(py^9U z;?7A!ZlaIHKCeEo`Tmi#$1D-Etg@iJ{=N>W$3&)DEr1f<6!3Z?!sMG1jn-UH(`-Uz z7#T@LFYgr&soHv}DOBZ(JjN%l1#Q!~qCP#!7B3^^R{S?#h2ar$7BZ2~1f6}!Qef#l z5h8JCxDT_*#G$^XHr{6?Kx25NQ(H( z6lEs35NyDjO{}3olS>D$<8c;~Q4vTRaluDab37BB?%~|E477#Hb{Dq94*f6l+K3gp zqns)GQS-KR1MF1An7IvA3jb4UqW-yaX0`X7(7$%3-3bgx+yT69EG01Z$J`in`@2Bu z8}$0!y)Rj=azm^r!3@Db2ZQm$^?hNWI_B(i4VZ#`hwtM`8G2T1*IW<;D`!g?4T2UF zb3vtKZU^IHsC4EKfQB1W-4X;rL|3ICNkhP7pFZ>_pt}iSi0=}{P`@Tn<|({5yRs%v z8dUK17gu~QJ-T+Z_BQ0duTaQQ^1mf~9lFe4_--A&NS|C>(|7%AVzRz*nt9H@y#udZ z(ItRkB6DZ^F#Y+{7LNvb*SQgFBHaN#RvLo99o;;Gp04xh;<^hhGaxT7Z5NQ=)7zcJ z@&qXQiPVqJ4;HA-Xje$T!m8W5wDbl80wSZA-kZRKn;A#&3NxKT20ZtL80`BF@B5;>@S#9*H+b4SG7M*gV$X-CH?gx&AyJaZ z70jm`z6%MJEU&MNwrb9Rw0NlAk17)_>T7GV03d28pyG3&0HVL2sH`w5va?qtrO~=J z+(1WPKK5KGR_(oN8p0jEizq`i8r&#e@i58QyD1`YCB-k)oeyP*5tLjAq6$D0xO|GG z!YzSe#*yXbmv=al=+PLlT(n{_ViFs ze~r~p5*6jUe48+S7IQRP z@fjn*APv$WRW{kkn>|;94n~q`Dn#B;qK%wcwm{!Jx z>&pb-&l>50N3j+;Lf3hI7!r#KLneE91cHAC#u!9 zI3>MC#{?gI3|lYDcbu^6RI^zdcI12oJ1)l0GG&7@k z)G}D;TU8h(U%`^%|9@dJLtXO0eiZCf`ndxFL-`E5$hFOLQ$y z{U&Ni&@oF5ULlwIXPM3Nm1++LY0qv;qE;*pnm$K#wTAm0{C~uCm0lSnGKc8?X2YNs zCFY$V&yto7)#^&DMZp7Hv#L;8Arde!MwT{p)U#|*_*m(HuplxkDoenA9Mw^AFdIbJ zMMr(k^yFb_&BeCsa{Qt0r9Sy@Lgo_bM^k??#}`12(;?eQNzQ%dt? z<>9E2=D+@f&t*ZAO;b)3Cr8^WKf21#s68eUBJy(?e=w8u7p8yeQ4k&MbecaHUAQEs zoljX}uzHv)`nC$*hO%*cCF5Yl<9B#Pt47DxZo|T%LiomYO^(s0jaVZ-D8I7GaeY4h z?N)$(k9R6&Q(T3RjiD>P95r?pm;EvdA};(Mq;c@dgNFPeB!XFkgG{e)H@~M4qtu(#1LnF9LX`w8ao#mRJdU&0d#bRX&>69wHGbz&R zYeS-0`j&IJ&d}uewYC2AQ-<4ybGmy^?Vi6p09A|b)2hq1J-tj=N;LqnU+hyrXY@)t zBd$dUG3qz*D~mniH$pL8XHV_3R?Lmh#C0@}OOgxp@lNzhB3i#??oguVXZrjk{X?g1 zgs4M|@(RtnIJkqkRn0qTFTDyO@t8PMWqNB*B{$FuqMji>D8>rBztNv)(wUGEh)rJE z2df(q()AfKn=rL;?tl$Tb15)Ya=}v3m&g^gg&X~QO_F{0uSSU>Q6j%Y70%v{@A9@1 zO&|HOhmbj@Cw{K{@)?6RXe1d!{@yx2{&|{vLWIW(cK4j(PEUvT&qAPboWF|vHy><& z%wzD;Nq zE~N>)rg`;424i&N8wWXwUPPRCX{UvZ54;4G94;2TTj{3BEy)7~6-Znc)&9AraY27U zh1A%O%ED#YyO@@J*=&DmwGs|n>lvTYT+(oZD>&gBW^CsJRq^%XE48NaDUatB42;Lq9#WwtuUcI;y99j(%2kT3&`HYA)$9e>g&GLMp>*u zJpUCEc*D$GoGmb@)LBXEsNZv|drRC2-eUwv2zLE#Y&~3Pw-arM%91id_?yOpJW=0K zT$27Xtusl@U3*D0+!r-hpNpYTC!A5&VSL$l1EytWqPN{>_@Gmv!N+w|@Td34f3X$2a>vV-|h$8loID# zB0Lu%rw$RawRSpPxwM>7D2ZbPmM*e#uoA=obvI>#lB=nWhDSi94(Pc;2jLHBAFG1% zpQ)c;>+E9>K~P#8L##VC<8yfOgJd5{t?{M?!NLQ~EYJINKwZsT?$k|+nexA%e9}Ug zX?iD@fzM1JW3x(gPNlS56E+&C%Kmi;&ugd8NY%zB8;?`#uftEDi(gze^UiB-S}NO- z)W^gVVEY$%${MBFavF_hqCdQuVR)4x6+Yz2md5Ck+;Ou+u&mTU&~Lw5WzFWXG)x}u zg|d(ET#|D3-Dg6(!De?n41h!MLqwcsFv-*Qs402?=1Qlw z!m|l=2l)FHjXs5(+dGz(mhJAv5E#DjXHPUwmv86xi55uF>92Q048{k^nN+yLj^)2d zzzMOeDHAD{ID`~mxSN>%OH}v*e$i?8te=CWG`LJs#cnu@@UNnd$GZGC8`S2+la=Om zbDIPv0lwoR($A#A*l<=%Wubvz%*@w-MNldo9UA^{2 zDO)?|S^58=hd+OMizpIA_P@i3vlIqzdCp&L>9~vD?pBL7mWT( z3FIZX5Iv8Cq3=~e-tzA#Y9BsoOS-3n|HW2hEAO&!l&ichMQ_!W$yxHr1n>6iY15jW zU{s4vV$1>PLWvx!)Iih>7}?I<3FURe1)(ZV!_z{eRj(cJj2egYqeDL4DM9Q`in^H( z7X`7UH+VlR-xd)3{s+M9oZ3#nQD0^o#BsQEhotTrs6sFp#IdoMdA9$J6!Vs9N|m`i)GJ8Bn%N*d zB3G-4ljc7YP|#!?VGi~btq4)z`snWsb`Fv8+AfKZZ4?j%T%Pn+Rt6jl>-4u4uQ{c{w3Y zZw zhPHI&EMo(?qDM4c;DI!j)isdrCPoLb8?a`2y7n3h* zN+hRWqsdOA1<6yf+7i-B=mBl92me%a<_kv*cNBydBS4XnNsLeD9w`` zTkZrQ!Xvv6>G`k)iSy}pJ0*YzkSC#1%JcqCnb^`7YaY@8xe$n<4gM( zI#ge}^Zq#by_NLGp3qBSvSgFh&y;j8;nM8KwiFT1=rd1*H`d`xsweh+(Tihx<>1#d zO&ew2I;}ndRn$tXUDJ~(v6<6oDI#y*W>i_6Ds@c6E>jG#`#C#g2SpM7{2ID^bG!sc z#QfBWQjY%Q>Q#t;&E;``QAw)fr1!jJ!-nR^Z^J zv4#t}jg-Z6_WXY_)>Bd5SASU=uh;pUG7EC*+PMd^pZ|)(UmH79MWCn0fgEHc?lSKO z*Sv_jF@Rjl{(cZnuO;&9E>}+97kI8#wPnrHhczz`MhK*&a_twDff#~dzAlFnz@vn= z#8*vcCy=rm16NAOL+mak{w3L4@)}1EMj07N%Mt*sZH~+j5p?do@jwG7#He3hwKI$3 zC%M+rbM#7{y(I%xE0Qd1urQV%%$=}Ek%nfIK6jIgS5;3rp1j|2K->Sc<%B&=3?bBu z?47lvHV*kyE3G}oN?+0)J)0UAtyV6K2`b_pNQyFm6Y&TW$~AI!Db+rq;&1BQ#E57r zh*>La4W^d&|6|k11qjLffU^JQrmcwed-97?0T&RrEKFHrS3iX)GP@WpW)cm#4nZ|X zlx2JBkHaHWG0L2h&~xgBcH+3__29|LhvWxNKf($U#BjC>Uto_;N0j!!$=(->ZJE~C zGoeGS*OJ~2UWcCfqM4<4KE$8wygs3l66s@Ej*fR&>DbJ4(zpIvhK3ggEZ^Wqrk3EZ zQE-WID%~JE!w0#;8{>z_N&_|K*0E{p_=;H{AZ_^BML zlwor{jzuL^+85$P@wdpu4xu-lX6?EC5DorZxQhlbQl&m`qaDos-T83296p z)de7co@&FVB?0DRxGEQF@dO0HA;3S`4+qyPib&zRLMevZ1!MIReGLJYV=hb=d@tEaG05DJr@&3*)XvMhXq+}cIN@Y-O^IsYwe{US`S%>y{ zCm1l_9PSY2)M87^QSJbzynP}ki2b{(6c(zr)%0%W8_}{l4K9^8e^q`lz-QSQz+zjPuRdTlTf!PM@YZqlTd1V6!x~edd!yb zH}4}?zvY7#-r3MY5jfkBkQvA5 z*OJ5jWtngrn%N6^e?Sv2OFkXr9ywvGvNB*=e6_;8iO>U#GPMxD^s}L`NNto( zcZX>lg$j3{B7qTKx00!30%%YEI_Cdtpu37D`>i#I6}gMBq7}M}03C9WvQS|$)ALrd zM3XvsFmKyxV4Mt>I@Jm@{uP^#*bOt~!xs#0;;M{aR`c^ae&h_{&E(R)JZF*4*B#M? z<5q>cqgp}Z^W;zPJw4atl^$q#Q{Q$GMkfCrI*RYNo~q&o61%2Eg6AahUQ}b-A>ye% zoBH78rPa%;AHo_0|M()6jCEksML(PEa316JF!**Exyps-;|%+AE;<KjsDKj&A-=fUHnSin}1I2iLoDq}(& zCya!%yBBun?Z51y(YC`_M1lKzccgW;h6T38l0)jE8^I#V+ZtF#_$H#ch|%WXp-C@2 zVgrq^HXL%b)wBH>>3k)pDaX_7If&fDT$a)$$6K|4?2EqeK7s+}IaJZWfdjr%qx$k* z!m1RpUvcMf^?2VW(pt84sUl-$w0Sqez9w~7)7Y)CZ9e`)ZXWsJ>jng3>a#0${%N)G`M>65b~Dkz zCxVp3kevDN-+@GW-Hm0_5v2{re>@2~{WB)?(68{ysgSQ@j zRxF=7#>GxC4!^=bn6Bmv<0>Kzg(*AOz>TAHDQ z*QK`<&IQ*??YkrDUX*|u??9zm1)P4*2T~B)&wvn%uVUb@fOp&3cOc>ZOV($=TIG7( z!_CX}*<|+c^k{H|gX+q}HGfFm&5f{cfX{7Y+LzH}w;=&z zdLjDHfC-D|3Rj^E>9+%1J?`ih`bp&r=NdWb5>``V7D+LsKfj0T%qR`Esx2ATK59mE zZO$B^w-lha?62l1KUyV`WBEIxT0B{O9{W~Ha(@2~`}((NoK0s}Q{O)B>w@&_0R@DD zHdnpKcOP=kI|$CB5rQmTz;H^zdk&ifzfVxPUW3N7X8z|n!Vk*?zvfYabrYhihJ@!; zNuSFSH`Qi)xC+UioWnQmm>VWUbz7b>Fis_J=ND6){>S}>4KE)Kv6MT8nU8Uy3Aunbhd=Tf|==26S-8biSz6kGDf~2BZW@I`$j6C!k6E&q<3yQV1v9| z!kkt4?)8P7+A^ANm{aqH$s`P5W+D@h80pE5SZil^II40~#!aO`#Kez;7?U1T*B$(a z3(>b4p#jV!pI`f}8osUjK8SZ)geTSsE-k&!E8puo*}Xu_f8R`j*51)uAfKQh{|CJn BRl5KH literal 0 HcmV?d00001