From f439918530f9559f7ecb78c715134de21b88f882 Mon Sep 17 00:00:00 2001 From: Greg Fichtenholtz <74032303+gfichtenholt@users.noreply.github.com> Date: Sun, 6 Mar 2022 23:09:31 -0800 Subject: [PATCH] fix for kubeapps-apis CrashLoopBackoff #4329 and [fluxv2] non-FQDN chart url fails on chart view #4381 (#4382) * attempt #2 * fix for #4329 kubeapps-apis CrashLoopBackoff * fix for [fluxv2] non-FQDN chart url fails on chart view #4381 * forgot two files * added integration test for flux helm release auto-update * moved test index yamls into a separate subdirectory not to crowd testdata * fixed chart_cache.go to be consistent with latest helm code * introduce retries+exponential backoff into NewRedisClientFromEnv * fix retries in NewRedisClientFromEnv --- .../packages/v1alpha1/cache/chart_cache.go | 29 +- .../packages/v1alpha1/cache/watcher_cache.go | 48 ++- .../v1alpha1/chart_integration_test.go | 20 +- .../fluxv2/packages/v1alpha1/chart_test.go | 285 ++++++++++++------ .../fluxv2/packages/v1alpha1/common/utils.go | 34 ++- .../v1alpha1/integration_utils_test.go | 122 ++++++-- .../v1alpha1/release_integration_test.go | 180 ++++++++++- .../fluxv2/packages/v1alpha1/release_test.go | 44 +-- .../plugins/fluxv2/packages/v1alpha1/repo.go | 2 +- .../v1alpha1/repo_integration_test.go | 32 +- .../fluxv2/packages/v1alpha1/repo_test.go | 58 ++-- .../fluxv2/packages/v1alpha1/server.go | 2 +- .../fluxv2/packages/v1alpha1/server_test.go | 7 +- .../packages/v1alpha1/test_util_test.go | 52 ++-- .../packages/v1alpha1/testdata/Dockerfile | 6 +- .../testdata/charts/airflow-1.0.0.tgz | Bin 0 -> 69180 bytes .../{ => charts}/airflow-many-versions.yaml | 0 .../charts/chart-with-relative-url.yaml | 32 ++ .../{ => charts}/index-after-update.yaml | 0 .../{ => charts}/index-before-update.yaml | 0 .../{ => charts}/index-with-categories.yaml | 0 .../testdata/{ => charts}/jetstack-index.yaml | 0 .../testdata/charts/podinfo-6.0.3.tgz | Bin 0 -> 13524 bytes .../podinfo-basic-auth-index.yaml | 0 .../charts/podinfo-index-updated.yaml | 83 +++++ .../testdata/{ => charts}/podinfo-index.yaml | 0 .../{ => charts}/podinfo-tls-index.yaml | 0 .../{ => charts}/redis-many-versions.yaml | 2 +- .../{ => charts}/redis-two-versions.yaml | 4 +- .../{ => charts}/single-package-template.yaml | 2 +- .../testdata/{ => charts}/valid-index.yaml | 6 +- go.mod | 3 + go.sum | 3 + 33 files changed, 796 insertions(+), 260 deletions(-) create mode 100644 cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/airflow-1.0.0.tgz rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/airflow-many-versions.yaml (100%) create mode 100644 cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/chart-with-relative-url.yaml rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/index-after-update.yaml (100%) rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/index-before-update.yaml (100%) rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/index-with-categories.yaml (100%) rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/jetstack-index.yaml (100%) create mode 100644 cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-6.0.3.tgz rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/podinfo-basic-auth-index.yaml (100%) create mode 100644 cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-index-updated.yaml rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/podinfo-index.yaml (100%) rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/podinfo-tls-index.yaml (100%) rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/redis-many-versions.yaml (99%) rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/redis-two-versions.yaml (96%) rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/single-package-template.yaml (95%) rename cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/{ => charts}/valid-index.yaml (93%) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/chart_cache.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/chart_cache.go index 226e264c22d..e87196eb70a 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/chart_cache.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/chart_cache.go @@ -8,6 +8,7 @@ import ( "encoding/gob" "fmt" "io/ioutil" + "net/url" "os" "reflect" "strings" @@ -139,11 +140,33 @@ func (c *ChartCache) SyncCharts(charts []models.Chart, clientOptions *common.Cli // The tarball URL will always be the first URL in the repo.chartVersions. // So says the helm plugin :-) + // however, not everybody agrees: + // ref https://github.com/helm/helm/blob/65d8e72504652e624948f74acbba71c51ac2e342/pkg/downloader/chart_downloader.go#L296 + u, err := url.Parse(chart.ChartVersions[0].URLs[0]) + if err != nil { + return fmt.Errorf("invalid URL format for chart [%s]: %v", chart.ID, err) + } + + // If the URL is relative (no scheme), prepend the chart repo's base URL + // ref https://github.com/kubeapps/kubeapps/issues/4381 + // ref https://github.com/helm/helm/blob/65d8e72504652e624948f74acbba71c51ac2e342/pkg/downloader/chart_downloader.go#L303 + if !u.IsAbs() { + repoURL, err := url.Parse(chart.Repo.URL) + if err != nil { + return fmt.Errorf("invalid URL format for chart repo [%s]: %v", chart.ID, err) + } + q := repoURL.Query() + // We need a trailing slash for ResolveReference to work, but make sure there isn't already one + repoURL.Path = strings.TrimSuffix(repoURL.Path, "/") + "/" + u = repoURL.ResolveReference(u) + u.RawQuery = q.Encode() + } + entry := chartCacheStoreEntry{ namespace: chart.Repo.Namespace, id: chart.ID, version: chart.ChartVersions[0].Version, - url: chart.ChartVersions[0].URLs[0], + url: u.String(), clientOptions: clientOptions, deleted: false, } @@ -387,7 +410,11 @@ func (c *ChartCache) syncHandler(workerName, key string) error { // this is effectively a cache GET operation func (c *ChartCache) FetchForOne(key string) ([]byte, error) { + c.resyncCond.L.(*sync.RWMutex).RLock() + defer c.resyncCond.L.(*sync.RWMutex).RUnlock() + log.Infof("+FetchForOne(%s)", key) + // read back from cache: should be either: // - what we previously wrote OR // - redis.Nil if the key does not exist or has been evicted due to memory pressure/TTL expiry diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/watcher_cache.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/watcher_cache.go index 3fd58deb8f4..0deca02c9e2 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/watcher_cache.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/cache/watcher_cache.go @@ -139,7 +139,8 @@ type NamespacedResourceWatcherCacheConfig struct { ListItemsFunc GetListItemsFunc } -func NewNamespacedResourceWatcherCache(name string, config NamespacedResourceWatcherCacheConfig, redisCli *redis.Client, stopCh <-chan struct{}) (*NamespacedResourceWatcherCache, error) { +// invokeExpectResync arg is only set to true for by unit tests only +func NewNamespacedResourceWatcherCache(name string, config NamespacedResourceWatcherCacheConfig, redisCli *redis.Client, stopCh <-chan struct{}, invokeExpectResync bool) (*NamespacedResourceWatcherCache, error) { log.Infof("+NewNamespacedResourceWatcherCache(%s, %v, %v)", name, config.Gvr, redisCli) if redisCli == nil { @@ -160,29 +161,30 @@ func NewNamespacedResourceWatcherCache(name string, config NamespacedResourceWat resyncCond: sync.NewCond(&sync.RWMutex{}), } - // confidence test that the specified GVR is a valid registered CRD + // sanity check that the specified GVR is a valid registered CRD if err := c.isGvrValid(); err != nil { return nil, err } - // this will launch a single worker that processes items on the work queue as they come in - // runWorker will loop until "something bad" happens. The .Until() func will + // this will launch a single worker that processes items on the work queue as they + // come in runWorker will loop until "something bad" happens. The .Until() func will // then rekick the worker after one second go wait.Until(c.runWorker, time.Second, stopCh) - // let's do the initial sync and creating a new RetryWatcher here so - // bootstrap errors, if any, are flagged early synchronously and the - // caller does not end up with a partially initialized cache - - // RetryWatcher will take care of re-starting the watcher if the underlying channel - // happens to close for some reason, as well as recover from other failures - // at the same time ensuring not to replay events that have been processed - watcher, err := c.resyncAndNewRetryWatcher(true) - if err != nil { - return nil, err + // this is needed by unit tests only. Since the potential lengthy bootstrap is done + // asynchronously (see below), the this func will set a condition before returning and + // the unit test will wait for for this condition to complete WaitUntilResyncComplete(). + // That's how it knows when the bootstrap is done + if invokeExpectResync { + if _, err := c.ExpectResync(); err != nil { + return nil, err + } } - go c.watchLoop(watcher, stopCh) + // per https://github.com/kubeapps/kubeapps/issues/4329 + // we want to do this asynchronously, so that having to parse existing large repos in the cluster + // doesn't block the kubeapps apis pod start-up + go c.syncAndStartWatchLoop(stopCh) return &c, nil } @@ -211,6 +213,22 @@ func (c *NamespacedResourceWatcherCache) isGvrValid() error { return fmt.Errorf("CRD [%s] is not valid", c.config.Gvr) } +func (c *NamespacedResourceWatcherCache) syncAndStartWatchLoop(stopCh <-chan struct{}) { + // RetryWatcher will take care of re-starting the watcher if the underlying channel + // happens to close for some reason, as well as recover from other failures + // at the same time ensuring not to replay events that have been processed + watcher, err := c.resyncAndNewRetryWatcher(true) + if err != nil { + err = fmt.Errorf( + "[%s]: Initial resync failed after [%d] retries were exhausted, last error: %v", + c.queue.Name(), maxWatcherCacheRetries, err) + // yes, I really want this to panic. Something is seriously wrong and + // possibly restarting kubeapps-apis server is needed... + runtime.Must(err) + } + c.watchLoop(watcher, stopCh) +} + // runWorker is a long-running function that will continually call the // processNextWorkItem function in order to read and process a message on the // workqueue. diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_integration_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_integration_test.go index ce9fe3f93b5..d554e1322ce 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_integration_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_integration_test.go @@ -38,7 +38,10 @@ import ( // -rw-rw-rw-@ 1 gfichtenholt staff 10394218 Nov 7 19:41 bitnami_index.yaml // Also now we are caching helmcharts themselves for each repo so that will affect how many will fit too func TestKindClusterGetAvailablePackageSummariesForLargeReposAndTinyRedis(t *testing.T) { - fluxPlugin, _ := checkEnv(t) + fluxPlugin, _, err := checkEnv(t) + if err != nil { + t.Fatal(err) + } redisCli, err := newRedisClientForIntegrationTest(t) if err != nil { @@ -54,12 +57,6 @@ func TestKindClusterGetAvailablePackageSummariesForLargeReposAndTinyRedis(t *tes if err = redisCli.ConfigSet(redisCli.Context(), "notify-keyspace-events", "EA").Err(); err != nil { t.Fatalf("%+v", err) } - t.Cleanup(func() { - t.Logf("Resetting notify-keyspace-events") - if err = redisCli.ConfigSet(redisCli.Context(), "notify-keyspace-events", "").Err(); err != nil { - t.Logf("%v", err) - } - }) if err = initNumberOfChartsInBitnamiCatalog(t); err != nil { t.Errorf("Failed to get number of charts in bitnami catalog due to: %v", err) @@ -91,7 +88,7 @@ func TestKindClusterGetAvailablePackageSummariesForLargeReposAndTinyRedis(t *tes for ; totalRepos < MAX_REPOS_NEVER && evictedRepos.Len() == 0; totalRepos++ { repo := fmt.Sprintf("bitnami-%d", totalRepos) // this is to make sure we allow enough time for repository to be created and come to ready state - if err = kubeAddHelmRepository(t, repo, "https://charts.bitnami.com/bitnami", "default", ""); err != nil { + if err = kubeAddHelmRepository(t, repo, "https://charts.bitnami.com/bitnami", "default", "", 0); err != nil { t.Fatalf("%v", err) } t.Cleanup(func() { @@ -126,7 +123,10 @@ func TestKindClusterGetAvailablePackageSummariesForLargeReposAndTinyRedis(t *tes // one particular code path I'd like to test: // make sure that GetAvailablePackageVersions() works w.r.t. a cache entry that's been evicted - grpcContext := newGrpcAdminContext(t, "test-create-admin") + grpcContext, err := newGrpcAdminContext(t, "test-create-admin") + if err != nil { + t.Fatal(err) + } // copy the evicted list because before ForEach loop below will modify it in a goroutine evictedCopy := sets.StringKeySet(evictedRepos) @@ -179,7 +179,7 @@ func TestKindClusterGetAvailablePackageSummariesForLargeReposAndTinyRedis(t *tes for ; totalRepos < MAX_REPOS_NEVER && evictedRepos.Len() == evictedCopy.Len(); totalRepos++ { repo := fmt.Sprintf("bitnami-%d", totalRepos) // this is to make sure we allow enough time for repository to be created and come to ready state - if err = kubeAddHelmRepository(t, repo, "https://charts.bitnami.com/bitnami", "default", ""); err != nil { + if err = kubeAddHelmRepository(t, repo, "https://charts.bitnami.com/bitnami", "default", "", 0); err != nil { t.Fatalf("%v", err) } t.Cleanup(func() { diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_test.go index d4408a0f5e6..0066752c2e3 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/chart_test.go @@ -12,6 +12,7 @@ import ( "os" "strings" "testing" + "time" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" redismock "github.com/go-redis/redismock/v8" @@ -173,7 +174,7 @@ func TestGetAvailablePackageDetail(t *testing.T) { } ts2, repo, err := newRepoWithIndex( - "testdata/redis-two-versions.yaml", repoName, repoNamespace, replaceUrls, secretRef) + testYaml("redis-two-versions.yaml"), repoName, repoNamespace, replaceUrls, secretRef) if err != nil { t.Fatalf("%+v", err) } @@ -268,7 +269,7 @@ func TestTransientHttpFailuresAreRetriedForChartCache(t *testing.T) { } ts2, repo, err := newRepoWithIndex( - "testdata/redis-two-versions.yaml", repoName, repoNamespace, replaceUrls, "") + testYaml("redis-two-versions.yaml"), repoName, repoNamespace, replaceUrls, "") if err != nil { t.Fatalf("%+v", err) } @@ -439,7 +440,7 @@ func TestNonExistingRepoOrInvalidPkgVersionGetAvailablePackageDetail(t *testing. } ts2, repo, err := newRepoWithIndex( - "testdata/redis-two-versions.yaml", tc.repoName, tc.repoNamespace, replaceUrls, "") + testYaml("redis-two-versions.yaml"), tc.repoName, tc.repoNamespace, replaceUrls, "") if err != nil { t.Fatalf("%+v", err) } @@ -571,7 +572,7 @@ func TestGetAvailablePackageVersions(t *testing.T) { }{ { name: "it returns the package version summary for redis chart in bitnami repo", - repoIndex: "testdata/redis-many-versions.yaml", + repoIndex: testYaml("redis-many-versions.yaml"), repoNamespace: "kubeapps", repoName: "bitnami", request: &corev1.GetAvailablePackageVersionsRequest{ @@ -582,7 +583,7 @@ func TestGetAvailablePackageVersions(t *testing.T) { }, { name: "it returns error for non-existent chart", - repoIndex: "testdata/redis-many-versions.yaml", + repoIndex: testYaml("redis-many-versions.yaml"), repoNamespace: "kubeapps", repoName: "bitnami", request: &corev1.GetAvailablePackageVersionsRequest{ @@ -680,7 +681,7 @@ func TestChartCacheResyncNotIdle(t *testing.T) { } // what I need is a single repo with a whole bunch of unique charts (packages) - tarGzBytes, err := ioutil.ReadFile("./testdata/charts/redis-14.4.0.tgz") + tarGzBytes, err := ioutil.ReadFile(testTgz("redis-14.4.0.tgz")) if err != nil { t.Fatalf("%+v", err) } @@ -700,7 +701,7 @@ func TestChartCacheResyncNotIdle(t *testing.T) { } defer os.Remove(tmpFile.Name()) - templateYAMLBytes, err := ioutil.ReadFile("testdata/single-package-template.yaml") + templateYAMLBytes, err := ioutil.ReadFile(testTgz("single-package-template.yaml")) if err != nil { t.Fatalf("%+v", err) } @@ -837,6 +838,101 @@ func TestChartCacheResyncNotIdle(t *testing.T) { }) } +// ref https://github.com/kubeapps/kubeapps/issues/4381 +// [fluxv2] non-FQDN chart url fails on chart view #4381 +func TestChartWithRelativeURL(t *testing.T) { + repoName := "testRepo" + repoNamespace := "default" + + tarGzBytes, err := ioutil.ReadFile(testTgz("airflow-1.0.0.tgz")) + if err != nil { + t.Fatal(err) + } + + indexYAMLBytes, err := ioutil.ReadFile(testYaml("chart-with-relative-url.yaml")) + if err != nil { + t.Fatal(err) + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/index.yaml" { + fmt.Fprintln(w, string(indexYAMLBytes)) + } else if r.RequestURI == "/charts/airflow-1.0.0.tgz" { + w.WriteHeader(200) + w.Write(tarGzBytes) + } else { + w.WriteHeader(404) + } + })) + + repoSpec := &sourcev1.HelmRepositorySpec{ + URL: ts.URL, + Interval: metav1.Duration{Duration: 1 * time.Minute}, + } + + lastUpdateTime, err := time.Parse(time.RFC3339, "2021-07-01T05:09:45Z") + if err != nil { + t.Fatal(err) + } + + repoStatus := &sourcev1.HelmRepositoryStatus{ + Artifact: &sourcev1.Artifact{ + Checksum: "651f952130ea96823711d08345b85e82be011dc6", + LastUpdateTime: metav1.Time{Time: lastUpdateTime}, + Revision: "651f952130ea96823711d08345b85e82be011dc6", + }, + Conditions: []metav1.Condition{ + { + Type: "Ready", + Status: "True", + Reason: sourcev1.IndexationSucceededReason, + }, + }, + URL: ts.URL + "/index.yaml", + } + repo := newRepo(repoName, repoNamespace, repoSpec, repoStatus) + defer ts.Close() + + s, mock, err := newServerWithRepos(t, + []sourcev1.HelmRepository{repo}, + []testSpecChartWithUrl{ + { + chartID: fmt.Sprintf("%s/airflow", repoName), + chartRevision: "1.0.0", + chartUrl: ts.URL + "/charts/airflow-1.0.0.tgz", + repoNamespace: repoNamespace, + }, + }, nil) + if err != nil { + t.Fatal(err) + } + + key, bytes, err := s.redisKeyValueForRepo(repo) + if err != nil { + t.Fatal(err) + } + + mock.ExpectGet(key).SetVal(string(bytes)) + + response, err := s.GetAvailablePackageVersions( + context.Background(), &corev1.GetAvailablePackageVersionsRequest{ + AvailablePackageRef: availableRef(repoName+"/airflow", repoNamespace), + }) + if err != nil { + t.Fatal(err) + } + opts := cmpopts.IgnoreUnexported( + corev1.GetAvailablePackageVersionsResponse{}, + corev1.PackageAppVersion{}) + if got, want := response, expected_versions_airflow; !cmp.Equal(want, got, opts) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(want, got, opts)) + } + + if err = mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + func newChart(name, namespace string, spec *sourcev1.HelmChartSpec, status *sourcev1.HelmChartStatus) sourcev1.HelmChart { helmChart := sourcev1.HelmChart{ TypeMeta: metav1.TypeMeta{ @@ -959,95 +1055,102 @@ func compareActualVsExpectedAvailablePackageDetail(t *testing.T, actual *corev1. } // global vars - -var redis_charts_spec = []testSpecChartWithFile{ - { - name: "redis", - tgzFile: "testdata/charts/redis-14.4.0.tgz", - revision: "14.4.0", - }, - { - name: "redis", - tgzFile: "testdata/charts/redis-14.3.4.tgz", - revision: "14.3.4", - }, -} - -var expected_detail_redis_1 = &corev1.AvailablePackageDetail{ - AvailablePackageRef: availableRef("bitnami-1/redis", "default"), - Name: "redis", - Version: &corev1.PackageAppVersion{ - PkgVersion: "14.4.0", - AppVersion: "6.2.4", - }, - RepoUrl: "https://example.repo.com/charts", - HomeUrl: "https://github.com/bitnami/charts/tree/master/bitnami/redis", - IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", - DisplayName: "redis", - Categories: []string{"Database"}, - ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", - Readme: "RedisTM Chart packaged by Bitnami\n\n[RedisTM](http://redis.io/) is an advanced key-value cache", - DefaultValues: "## @param global.imageRegistry Global Docker image registry", - ValuesSchema: "\"$schema\": \"http://json-schema.org/schema#\"", - SourceUrls: []string{"https://github.com/bitnami/bitnami-docker-redis", "http://redis.io/"}, - Maintainers: []*corev1.Maintainer{ +var ( + redis_charts_spec = []testSpecChartWithFile{ { - Name: "Bitnami", - Email: "containers@bitnami.com", + name: "redis", + tgzFile: testTgz("redis-14.4.0.tgz"), + revision: "14.4.0", }, { - Name: "desaintmartin", - Email: "cedric@desaintmartin.fr", + name: "redis", + tgzFile: testTgz("redis-14.3.4.tgz"), + revision: "14.3.4", }, - }, -} + } -var expected_detail_redis_2 = &corev1.AvailablePackageDetail{ - AvailablePackageRef: availableRef("bitnami-1/redis", "default"), - Name: "redis", - Version: &corev1.PackageAppVersion{ - PkgVersion: "14.3.4", - AppVersion: "6.2.4", - }, - RepoUrl: "https://example.repo.com/charts", - IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", - HomeUrl: "https://github.com/bitnami/charts/tree/master/bitnami/redis", - DisplayName: "redis", - Categories: []string{"Database"}, - ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", - Readme: "RedisTM Chart packaged by Bitnami\n\n[RedisTM](http://redis.io/) is an advanced key-value cache", - DefaultValues: "## @param global.imageRegistry Global Docker image registry", - ValuesSchema: "\"$schema\": \"http://json-schema.org/schema#\"", - SourceUrls: []string{"https://github.com/bitnami/bitnami-docker-redis", "http://redis.io/"}, - Maintainers: []*corev1.Maintainer{ - { - Name: "Bitnami", - Email: "containers@bitnami.com", + expected_detail_redis_1 = &corev1.AvailablePackageDetail{ + AvailablePackageRef: availableRef("bitnami-1/redis", "default"), + Name: "redis", + Version: &corev1.PackageAppVersion{ + PkgVersion: "14.4.0", + AppVersion: "6.2.4", }, - { - Name: "desaintmartin", - Email: "cedric@desaintmartin.fr", + RepoUrl: "https://example.repo.com/charts", + HomeUrl: "https://github.com/bitnami/charts/tree/master/bitnami/redis", + IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", + DisplayName: "redis", + Categories: []string{"Database"}, + ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", + Readme: "RedisTM Chart packaged by Bitnami\n\n[RedisTM](http://redis.io/) is an advanced key-value cache", + DefaultValues: "## @param global.imageRegistry Global Docker image registry", + ValuesSchema: "\"$schema\": \"http://json-schema.org/schema#\"", + SourceUrls: []string{"https://github.com/bitnami/bitnami-docker-redis", "http://redis.io/"}, + Maintainers: []*corev1.Maintainer{ + { + Name: "Bitnami", + Email: "containers@bitnami.com", + }, + { + Name: "desaintmartin", + Email: "cedric@desaintmartin.fr", + }, }, - }, -} + } -var expected_versions_redis = &corev1.GetAvailablePackageVersionsResponse{ - PackageAppVersions: []*corev1.PackageAppVersion{ - {PkgVersion: "14.4.0", AppVersion: "6.2.4"}, - {PkgVersion: "14.3.4", AppVersion: "6.2.4"}, - {PkgVersion: "14.3.3", AppVersion: "6.2.4"}, - {PkgVersion: "14.3.2", AppVersion: "6.2.3"}, - {PkgVersion: "14.2.1", AppVersion: "6.2.3"}, - {PkgVersion: "14.2.0", AppVersion: "6.2.3"}, - {PkgVersion: "13.0.1", AppVersion: "6.2.1"}, - {PkgVersion: "13.0.0", AppVersion: "6.2.1"}, - {PkgVersion: "12.10.1", AppVersion: "6.0.12"}, - {PkgVersion: "12.10.0", AppVersion: "6.0.12"}, - {PkgVersion: "12.9.2", AppVersion: "6.0.12"}, - {PkgVersion: "12.9.1", AppVersion: "6.0.12"}, - {PkgVersion: "12.9.0", AppVersion: "6.0.12"}, - {PkgVersion: "12.8.3", AppVersion: "6.0.12"}, - {PkgVersion: "12.8.2", AppVersion: "6.0.12"}, - {PkgVersion: "12.8.1", AppVersion: "6.0.12"}, - }, -} + expected_detail_redis_2 = &corev1.AvailablePackageDetail{ + AvailablePackageRef: availableRef("bitnami-1/redis", "default"), + Name: "redis", + Version: &corev1.PackageAppVersion{ + PkgVersion: "14.3.4", + AppVersion: "6.2.4", + }, + RepoUrl: "https://example.repo.com/charts", + IconUrl: "https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png", + HomeUrl: "https://github.com/bitnami/charts/tree/master/bitnami/redis", + DisplayName: "redis", + Categories: []string{"Database"}, + ShortDescription: "Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.", + Readme: "RedisTM Chart packaged by Bitnami\n\n[RedisTM](http://redis.io/) is an advanced key-value cache", + DefaultValues: "## @param global.imageRegistry Global Docker image registry", + ValuesSchema: "\"$schema\": \"http://json-schema.org/schema#\"", + SourceUrls: []string{"https://github.com/bitnami/bitnami-docker-redis", "http://redis.io/"}, + Maintainers: []*corev1.Maintainer{ + { + Name: "Bitnami", + Email: "containers@bitnami.com", + }, + { + Name: "desaintmartin", + Email: "cedric@desaintmartin.fr", + }, + }, + } + + expected_versions_redis = &corev1.GetAvailablePackageVersionsResponse{ + PackageAppVersions: []*corev1.PackageAppVersion{ + {PkgVersion: "14.4.0", AppVersion: "6.2.4"}, + {PkgVersion: "14.3.4", AppVersion: "6.2.4"}, + {PkgVersion: "14.3.3", AppVersion: "6.2.4"}, + {PkgVersion: "14.3.2", AppVersion: "6.2.3"}, + {PkgVersion: "14.2.1", AppVersion: "6.2.3"}, + {PkgVersion: "14.2.0", AppVersion: "6.2.3"}, + {PkgVersion: "13.0.1", AppVersion: "6.2.1"}, + {PkgVersion: "13.0.0", AppVersion: "6.2.1"}, + {PkgVersion: "12.10.1", AppVersion: "6.0.12"}, + {PkgVersion: "12.10.0", AppVersion: "6.0.12"}, + {PkgVersion: "12.9.2", AppVersion: "6.0.12"}, + {PkgVersion: "12.9.1", AppVersion: "6.0.12"}, + {PkgVersion: "12.9.0", AppVersion: "6.0.12"}, + {PkgVersion: "12.8.3", AppVersion: "6.0.12"}, + {PkgVersion: "12.8.2", AppVersion: "6.0.12"}, + {PkgVersion: "12.8.1", AppVersion: "6.0.12"}, + }, + } + + expected_versions_airflow = &corev1.GetAvailablePackageVersionsResponse{ + PackageAppVersions: []*corev1.PackageAppVersion{ + {PkgVersion: "1.0.0", AppVersion: "2.1.4"}, + }, + } +) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/common/utils.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/common/utils.go index 590d5dd99a5..c4bd794e966 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/common/utils.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/common/utils.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" "sync" + "time" helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" @@ -32,6 +33,9 @@ import ( const ( // copied from helm plug-in UserAgentPrefix = "kubeapps-apis/plugins" + // max number of attempts to initialize redis client before giving up + maxRedisInitClientRetries = 10 + redisInitClientRetryWait = 1 * time.Second ) // Set the pluginDetail once during a module init function so the single struct @@ -132,17 +136,27 @@ func NewRedisClientFromEnv() (*redis.Client, error) { return nil, err } - redisCli := redis.NewClient(&redis.Options{ - Addr: REDIS_ADDR, - Password: REDIS_PASSWORD, - DB: REDIS_DB_NUM, - }) + // ref https://github.com/kubeapps/kubeapps/pull/4382#discussion_r820386531 + var redisCli *redis.Client + for i := 0; i < maxRedisInitClientRetries; i++ { + redisCli = redis.NewClient(&redis.Options{ + Addr: REDIS_ADDR, + Password: REDIS_PASSWORD, + DB: REDIS_DB_NUM, + }) - // confidence test that the redis client is connected - if pong, err := redisCli.Ping(redisCli.Context()).Result(); err != nil { - return nil, err - } else { - log.Infof("Redis [PING]: %s", pong) + // confidence test that the redis client is connected + var pong string + if pong, err = redisCli.Ping(redisCli.Context()).Result(); err == nil { + log.Infof("Redis [PING]: %s", pong) + break + } + log.Infof("Waiting %ds before retrying to due to %v...", redisInitClientRetryWait, err) + time.Sleep(redisInitClientRetryWait) + } + + if err != nil { + return nil, fmt.Errorf("initializing redis client failed after [%d] retries were exhausted, last error: %v", maxRedisInitClientRetries, err) } if maxmemory, err := redisCli.ConfigGet(redisCli.Context(), "maxmemory").Result(); err != nil { diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go index 90a5960423f..b6b2969da72 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/integration_utils_test.go @@ -22,6 +22,7 @@ import ( "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" "github.com/go-redis/redis/v8" + "github.com/kubeapps/kubeapps/cmd/apprepository-controller/pkg/client/clientset/versioned/scheme" plugins "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/core/plugins/v1alpha1" fluxplugin "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/plugins/fluxv2/packages/v1alpha1" "github.com/kubeapps/kubeapps/pkg/chart/models" @@ -32,18 +33,21 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" apiv1 "k8s.io/api/core/v1" - kubecorev1 "k8s.io/api/core/v1" - kuberbacv1 "k8s.io/api/rbac/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/watch" + "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/portforward" "k8s.io/client-go/transport/spdy" + "k8s.io/kubectl/pkg/cmd/cp" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -66,28 +70,29 @@ const ( podinfo_tls_repo_url = "https://fluxv2plugin-testdata-ssl-svc.default.svc.cluster.local:443" ) -func checkEnv(t *testing.T) (fluxplugin.FluxV2PackagesServiceClient, fluxplugin.FluxV2RepositoriesServiceClient) { +func checkEnv(t *testing.T) (fluxplugin.FluxV2PackagesServiceClient, fluxplugin.FluxV2RepositoriesServiceClient, error) { enableEnvVar := os.Getenv(envVarFluxIntegrationTests) runTests := false if enableEnvVar != "" { var err error runTests, err = strconv.ParseBool(enableEnvVar) if err != nil { - t.Fatalf("%+v", err) + return nil, nil, err } } if !runTests { t.Skipf("skipping flux plugin integration tests because environment variable %q not set to be true", envVarFluxIntegrationTests) + return nil, nil, nil } else { if up, err := isLocalKindClusterUp(t); err != nil || !up { - t.Fatalf("Failed to find local kind cluster due to: [%v]", err) + return nil, nil, fmt.Errorf("Failed to find local kind cluster due to: [%v]", err) } var fluxPluginPackagesClient fluxplugin.FluxV2PackagesServiceClient var fluxPluginReposClient fluxplugin.FluxV2RepositoriesServiceClient var err error if fluxPluginPackagesClient, fluxPluginReposClient, err = getFluxPluginClients(t); err != nil { - t.Fatalf("Failed to get fluxv2 plugin due to: [%v]", err) + return nil, nil, fmt.Errorf("Failed to get fluxv2 plugin due to: [%v]", err) } // check the fluxv2plugin-testdata-svc is deployed - without it, @@ -95,19 +100,18 @@ func checkEnv(t *testing.T) (fluxplugin.FluxV2PackagesServiceClient, fluxplugin. // long time typedClient, err := kubeGetTypedClient() if err != nil { - t.Fatalf("%+v", err) + return nil, nil, err } ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() _, err = typedClient.CoreV1().Services("default").Get(ctx, "fluxv2plugin-testdata-svc", metav1.GetOptions{}) if err != nil { - t.Fatalf("Failed to get service [default/fluxv2plugin-testdata-svc] due to: [%v]", err) + return nil, nil, fmt.Errorf("Failed to get service [default/fluxv2plugin-testdata-svc] due to: [%v]", err) } rand.Seed(time.Now().UnixNano()) - return fluxPluginPackagesClient, fluxPluginReposClient + return fluxPluginPackagesClient, fluxPluginReposClient, nil } - return nil, nil } func isLocalKindClusterUp(t *testing.T) (up bool, err error) { @@ -153,7 +157,7 @@ func getFluxPluginClients(t *testing.T) (fluxplugin.FluxV2PackagesServiceClient, target := "localhost:8080" conn, err := grpc.Dial(target, opts...) if err != nil { - t.Fatalf("failed to dial [%s] due to: %v", target, err) + return nil, nil, fmt.Errorf("failed to dial [%s] due to: %v", target, err) } t.Cleanup(func() { conn.Close() }) pluginsCli := plugins.NewPluginsServiceClient(conn) @@ -162,7 +166,7 @@ func getFluxPluginClients(t *testing.T) (fluxplugin.FluxV2PackagesServiceClient, response, err := pluginsCli.GetConfiguredPlugins(ctx, &plugins.GetConfiguredPluginsRequest{}) if err != nil { - t.Fatalf("failed to GetConfiguredPlugins due to: %v", err) + return nil, nil, fmt.Errorf("failed to GetConfiguredPlugins due to: %v", err) } found := false for _, p := range response.Plugins { @@ -179,8 +183,11 @@ func getFluxPluginClients(t *testing.T) (fluxplugin.FluxV2PackagesServiceClient, // This creates a flux helm repository CRD. The usage of this func should be minimized as much as // possible in favor of flux Plugin's AddPackageRepository() call -func kubeAddHelmRepository(t *testing.T, name, url, namespace, secretName string) error { +func kubeAddHelmRepository(t *testing.T, name, url, namespace, secretName string, interval time.Duration) error { t.Logf("+kubeCreateHelmRepository(%s,%s)", name, namespace) + if interval <= 0 { + interval = time.Duration(10 * time.Minute) + } repo := sourcev1.HelmRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.HelmRepositoryKind, @@ -191,7 +198,8 @@ func kubeAddHelmRepository(t *testing.T, name, url, namespace, secretName string Namespace: namespace, }, Spec: sourcev1.HelmRepositorySpec{ - URL: url, + URL: url, + Interval: metav1.Duration{Duration: interval}, }, } @@ -216,7 +224,7 @@ func kubeWaitUntilHelmRepositoryIsReady(t *testing.T, name, namespace string) er if ifc, err := kubeGetCtrlClient(); err != nil { return err } else { - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() var repoList sourcev1.HelmRepositoryList if watcher, err := ifc.Watch(ctx, &repoList); err != nil { @@ -330,7 +338,7 @@ func kubeCreateServiceAccountWithClusterRole(t *testing.T, name, namespace, role defer cancel() _, err = typedClient.CoreV1().ServiceAccounts(namespace).Create( ctx, - &kubecorev1.ServiceAccount{ + &apiv1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, @@ -375,18 +383,18 @@ func kubeCreateServiceAccountWithClusterRole(t *testing.T, name, namespace, role } _, err = typedClient.RbacV1().ClusterRoleBindings().Create( ctx, - &kuberbacv1.ClusterRoleBinding{ + &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: name + "-binding", }, - Subjects: []kuberbacv1.Subject{ + Subjects: []rbacv1.Subject{ { - Kind: kuberbacv1.ServiceAccountKind, + Kind: rbacv1.ServiceAccountKind, Name: name, Namespace: namespace, }, }, - RoleRef: kuberbacv1.RoleRef{ + RoleRef: rbacv1.RoleRef{ Kind: "ClusterRole", Name: role, }, @@ -446,7 +454,7 @@ func kubeCreateNamespace(t *testing.T, namespace string) error { defer cancel() _, err = typedClient.CoreV1().Namespaces().Create( ctx, - &kubecorev1.Namespace{ + &apiv1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: namespace, }, @@ -573,6 +581,33 @@ func kubePortForwardToRedis(t *testing.T) error { } } +// ref https://stackoverflow.com/questions/51686986/how-to-copy-file-to-container-with-kubernetes-client-go +// example kubectl cp /tmp/foo.txt default/fluxv2plugin-testdata-app-7f7dd58796-w2qbg:/ +func kubeCopyFileToPod(t *testing.T, srcFile string, podName types.NamespacedName, destFile string) error { + t.Logf("+kubeCopyFileToPod(%s, %s, %s)", srcFile, podName, destFile) + ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() + copyOptions := cp.NewCopyOptions(ioStreams) + restcfg, err := restConfig() + if err != nil { + return err + } + restcfg.APIPath = "/api" // Make sure we target /api and not just / + restcfg.GroupVersion = &schema.GroupVersion{Version: "v1"} // this targets the core api groups so the url path will be /api/v1 + restcfg.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: scheme.Codecs} + copyOptions.ClientConfig = restcfg + typedcli, err := kubeGetTypedClient() + if err != nil { + return err + } + copyOptions.Clientset = typedcli + destSpec := fmt.Sprintf("%s/%s:%s", podName.Namespace, podName.Name, destFile) + err = copyOptions.Run([]string{srcFile, destSpec}) + if err != nil { + return fmt.Errorf("Could not run copy operation: %v", err) + } + return nil +} + func kubeGetCtrlClient() (ctrlclient.WithWatch, error) { if ctrlClient != nil { return ctrlClient, nil @@ -581,8 +616,8 @@ func kubeGetCtrlClient() (ctrlclient.WithWatch, error) { return nil, err } else { scheme := runtime.NewScheme() - _ = sourcev1.AddToScheme(scheme) - _ = helmv2.AddToScheme(scheme) + sourcev1.AddToScheme(scheme) + helmv2.AddToScheme(scheme) return ctrlclient.NewWithWatch(config, ctrlclient.Options{Scheme: scheme}) } @@ -621,30 +656,30 @@ func newGrpcContext(t *testing.T, token string) context.Context { metadata.Pairs("Authorization", "Bearer "+token)) } -func newGrpcAdminContext(t *testing.T, name string) context.Context { +func newGrpcAdminContext(t *testing.T, name string) (context.Context, error) { token, err := kubeCreateAdminServiceAccount(t, name, "default") if err != nil { - t.Fatalf("Failed to create service account due to: %+v", err) + return nil, fmt.Errorf("Failed to create service account due to: %+v", err) } t.Cleanup(func() { if err := kubeDeleteServiceAccount(t, name, "default"); err != nil { t.Logf("Failed to delete service account due to: %+v", err) } }) - return newGrpcContext(t, token) + return newGrpcContext(t, token), nil } -func newGrpcFluxPluginContext(t *testing.T, name string) context.Context { +func newGrpcFluxPluginContext(t *testing.T, name string) (context.Context, error) { token, err := kubeCreateFluxPluginServiceAccount(t, name, "default") if err != nil { - t.Fatalf("Failed to create service account due to: %+v", err) + return nil, fmt.Errorf("Failed to create service account due to: %+v", err) } t.Cleanup(func() { if err := kubeDeleteServiceAccount(t, name, "default"); err != nil { t.Logf("Failed to delete service account due to: %+v", err) } }) - return newGrpcContext(t, token) + return newGrpcContext(t, token), nil } func redisCheckTinyMaxMemory(t *testing.T, redisCli *redis.Client, expectedMaxMemory string) error { @@ -655,7 +690,7 @@ func redisCheckTinyMaxMemory(t *testing.T, redisCli *redis.Client, expectedMaxMe currentMaxMemory := fmt.Sprintf("%v", maxmemory[1]) t.Logf("Current redis maxmemory = [%s]", currentMaxMemory) if currentMaxMemory != expectedMaxMemory { - t.Fatalf("This test requires redis config maxmemory to be set to %s", expectedMaxMemory) + return fmt.Errorf("This test requires redis config maxmemory to be set to %s", expectedMaxMemory) } } maxmemoryPolicy, err := redisCli.ConfigGet(redisCli.Context(), "maxmemory-policy").Result() @@ -665,7 +700,7 @@ func redisCheckTinyMaxMemory(t *testing.T, redisCli *redis.Client, expectedMaxMe currentMaxMemoryPolicy := fmt.Sprintf("%v", maxmemoryPolicy[1]) t.Logf("Current maxmemory policy = [%s]", currentMaxMemoryPolicy) if currentMaxMemoryPolicy != "allkeys-lfu" { - t.Fatalf("This test requires redis config maxmemory-policy to be set to allkeys-lfu") + return fmt.Errorf("This test requires redis config maxmemory-policy to be set to allkeys-lfu") } } return nil @@ -713,10 +748,10 @@ func newRedisClientForIntegrationTest(t *testing.T) (*redis.Client, error) { // and you should be able to clean up manually // $ kubectl delete helmrepositories --all if keys, err := redisCli.Keys(redisCli.Context(), "*").Result(); err != nil { - return nil, fmt.Errorf("%v", err) + return nil, err } else { if len(keys) != 0 { - t.Fatalf("Failing due to unexpected state of the cache. Current keys: %s", keys) + return nil, fmt.Errorf("Failing due to unexpected state of the cache. Current keys: %s", keys) } } return redisCli, nil @@ -800,6 +835,27 @@ func initNumberOfChartsInBitnamiCatalog(t *testing.T) error { return nil } +func getFluxPluginTestdataPodName() (*types.NamespacedName, error) { + cli, err := kubeGetTypedClient() + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() + podList, err := cli.CoreV1().Pods("default").List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + for _, p := range podList.Items { + if strings.HasPrefix(p.Name, "fluxv2plugin-testdata-app-") { + return &types.NamespacedName{ + Name: p.Name, + Namespace: p.Namespace}, nil + } + } + return nil, fmt.Errorf("fluxplugin testdata pod not found") +} + // global vars var ( typedClient kubernetes.Interface diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_integration_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_integration_test.go index cb206c809cc..3e5cbecaec1 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_integration_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_integration_test.go @@ -29,6 +29,7 @@ import ( type integrationTestCreatePackageSpec struct { testName string repoUrl string + repoInterval time.Duration // 0 for default (10m) request *corev1.CreateInstalledPackageRequest expectedDetail *corev1.InstalledPackageDetail expectedPodPrefix string @@ -42,7 +43,10 @@ type integrationTestCreatePackageSpec struct { } func TestKindClusterCreateInstalledPackage(t *testing.T) { - fluxPluginClient, _ := checkEnv(t) + fluxPluginClient, _, err := checkEnv(t) + if err != nil { + t.Fatal(err) + } testCases := []integrationTestCreatePackageSpec{ { @@ -110,7 +114,10 @@ func TestKindClusterCreateInstalledPackage(t *testing.T) { }, } - grpcContext := newGrpcAdminContext(t, "test-create-admin") + grpcContext, err := newGrpcAdminContext(t, "test-create-admin") + if err != nil { + t.Fatal(err) + } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { @@ -129,7 +136,10 @@ type integrationTestUpdatePackageSpec struct { } func TestKindClusterUpdateInstalledPackage(t *testing.T) { - fluxPluginClient, _ := checkEnv(t) + fluxPluginClient, _, err := checkEnv(t) + if err != nil { + t.Fatal(err) + } testCases := []integrationTestUpdatePackageSpec{ { @@ -209,10 +219,12 @@ func TestKindClusterUpdateInstalledPackage(t *testing.T) { request: update_request_6, unauthorized: true, }, - // TODO (gfichtenholt) test automatic upgrade to new version when it becomes available } - grpcContext := newGrpcAdminContext(t, "test-create-admin") + grpcContext, err := newGrpcAdminContext(t, "test-create-admin") + if err != nil { + t.Fatal(err) + } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { @@ -275,13 +287,91 @@ func TestKindClusterUpdateInstalledPackage(t *testing.T) { } } +func TestKindClusterAutoUpdateInstalledPackage(t *testing.T) { + fluxPluginClient, _, err := checkEnv(t) + if err != nil { + t.Fatal(err) + } + + spec := integrationTestCreatePackageSpec{ + testName: "create test (auto update)", + repoUrl: podinfo_repo_url, + repoInterval: 30 * time.Second, + request: create_request_auto_update, + expectedDetail: expected_detail_auto_update, + expectedPodPrefix: "my-podinfo-16", + expectedStatusCode: codes.OK, + expectedResourceRefs: expected_resource_refs_auto_update, + } + + grpcContext, err := newGrpcAdminContext(t, "test-auto-update") + if err != nil { + t.Fatal(err) + } + + // this will also make sure that response looks like expected_detail_auto_update + installedRef := createAndWaitForHelmRelease(t, spec, fluxPluginClient, grpcContext) + podName, err := getFluxPluginTestdataPodName() + if err != nil { + t.Fatal(err) + } + t.Logf("podName = [%s]", podName) + + if err = kubeCopyFileToPod( + t, + testTgz("podinfo-6.0.3.tgz"), + *podName, + "/usr/share/nginx/html/podinfo/podinfo-6.0.3.tgz"); err != nil { + t.Fatal(err) + } + if err = kubeCopyFileToPod( + t, + testYaml("podinfo-index-updated.yaml"), + *podName, + "/usr/share/nginx/html/podinfo/index.yaml"); err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + if err = kubeCopyFileToPod( + t, + testYaml("podinfo-index.yaml"), + *podName, + "/usr/share/nginx/html/podinfo/index.yaml"); err != nil { + t.Logf("Error reverting to previos podinfo index: %v", err) + } + }) + t.Logf("Waiting 45 seconds...") + time.Sleep(45 * time.Second) + + resp, err := fluxPluginClient.GetInstalledPackageDetail( + grpcContext, &corev1.GetInstalledPackageDetailRequest{ + InstalledPackageRef: installedRef, + }) + if err != nil { + t.Fatal(err) + } + expected_detail_auto_update_2.InstalledPackageRef = installedRef + expected_detail_auto_update_2.PostInstallationNotes = strings.ReplaceAll( + expected_detail_auto_update_2.PostInstallationNotes, + "@TARGET_NS@", + spec.request.TargetContext.Namespace) + compareActualVsExpectedGetInstalledPackageDetailResponse( + t, resp, &corev1.GetInstalledPackageDetailResponse{ + InstalledPackageDetail: expected_detail_auto_update_2, + }) +} + type integrationTestDeletePackageSpec struct { integrationTestCreatePackageSpec unauthorized bool } func TestKindClusterDeleteInstalledPackage(t *testing.T) { - fluxPluginClient, _ := checkEnv(t) + fluxPluginClient, _, err := checkEnv(t) + if err != nil { + t.Fatal(err) + } testCases := []integrationTestDeletePackageSpec{ { @@ -309,7 +399,10 @@ func TestKindClusterDeleteInstalledPackage(t *testing.T) { }, } - grpcContext := newGrpcAdminContext(t, "test-delete-admin") + grpcContext, err := newGrpcAdminContext(t, "test-delete-admin") + if err != nil { + t.Fatal(err) + } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { @@ -395,7 +488,7 @@ func TestKindClusterDeleteInstalledPackage(t *testing.T) { func createAndWaitForHelmRelease(t *testing.T, tc integrationTestCreatePackageSpec, fluxPluginClient fluxplugin.FluxV2PackagesServiceClient, grpcContext context.Context) *corev1.InstalledPackageReference { availablePackageRef := tc.request.AvailablePackageRef idParts := strings.Split(availablePackageRef.Identifier, "/") - err := kubeAddHelmRepository(t, idParts[0], tc.repoUrl, availablePackageRef.Context.Namespace, "") + err := kubeAddHelmRepository(t, idParts[0], tc.repoUrl, availablePackageRef.Context.Namespace, "", tc.repoInterval) if err != nil { t.Fatalf("%+v", err) } @@ -1252,4 +1345,75 @@ var ( Cluster: KubeappsCluster, }, } + + create_request_auto_update = &corev1.CreateInstalledPackageRequest{ + AvailablePackageRef: availableRef("podinfo-16/podinfo", "default"), + Name: "my-podinfo-16", + TargetContext: &corev1.Context{ + Namespace: "test-16", + Cluster: KubeappsCluster, + }, + PkgVersionReference: &corev1.VersionReference{ + Version: ">= 6", + }, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 30, + }, + } + + expected_detail_auto_update = &corev1.InstalledPackageDetail{ + PkgVersionReference: &corev1.VersionReference{ + Version: ">= 6", + }, + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.0.0", + AppVersion: "6.0.0", + }, + Status: statusInstalled, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 30, + }, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/my-podinfo-16 8080:9898\n", + } + + expected_detail_auto_update_2 = &corev1.InstalledPackageDetail{ + PkgVersionReference: &corev1.VersionReference{ + Version: ">= 6", + }, + CurrentVersion: &corev1.PackageAppVersion{ + PkgVersion: "6.0.3", + AppVersion: "6.0.3", + }, + Name: "my-podinfo-16", + Status: statusInstalled, + ReconciliationOptions: &corev1.ReconciliationOptions{ + Interval: 30, + }, + AvailablePackageRef: &corev1.AvailablePackageReference{ + Context: &corev1.Context{ + Cluster: KubeappsCluster, + Namespace: "default", + }, + Identifier: "podinfo-16/podinfo", + Plugin: fluxPlugin, + }, + PostInstallationNotes: "1. Get the application URL by running these commands:\n " + + "echo \"Visit http://127.0.0.1:8080 to use your application\"\n " + + "kubectl -n @TARGET_NS@ port-forward deploy/my-podinfo-16 8080:9898\n", + } + + expected_resource_refs_auto_update = []*corev1.ResourceRef{ + { + ApiVersion: "v1", + Kind: "Service", + Name: "my-podinfo-16", + }, + { + ApiVersion: "apps/v1", + Kind: "Deployment", + Name: "my-podinfo-16", + }, + } ) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go index c99a467fa4d..7af8c09223c 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/release_test.go @@ -529,7 +529,7 @@ func TestCreateInstalledPackage(t *testing.T) { existingObjs: testSpecCreateInstalledPackage{ repoName: "podinfo", repoNamespace: "namespace-1", - repoIndex: "testdata/podinfo-index.yaml", + repoIndex: testYaml("podinfo-index.yaml"), }, expectedStatusCode: codes.OK, expectedResponse: create_installed_package_resp_my_podinfo, @@ -550,7 +550,7 @@ func TestCreateInstalledPackage(t *testing.T) { existingObjs: testSpecCreateInstalledPackage{ repoName: "podinfo", repoNamespace: "namespace-1", - repoIndex: "testdata/podinfo-index.yaml", + repoIndex: testYaml("podinfo-index.yaml"), }, expectedStatusCode: codes.OK, expectedResponse: create_installed_package_resp_my_podinfo, @@ -573,7 +573,7 @@ func TestCreateInstalledPackage(t *testing.T) { existingObjs: testSpecCreateInstalledPackage{ repoName: "podinfo", repoNamespace: "namespace-1", - repoIndex: "testdata/podinfo-index.yaml", + repoIndex: testYaml("podinfo-index.yaml"), }, expectedStatusCode: codes.OK, expectedResponse: create_installed_package_resp_my_podinfo, @@ -592,7 +592,7 @@ func TestCreateInstalledPackage(t *testing.T) { existingObjs: testSpecCreateInstalledPackage{ repoName: "podinfo", repoNamespace: "namespace-1", - repoIndex: "testdata/podinfo-index.yaml", + repoIndex: testYaml("podinfo-index.yaml"), }, expectedStatusCode: codes.OK, expectedResponse: create_installed_package_resp_my_podinfo, @@ -611,7 +611,7 @@ func TestCreateInstalledPackage(t *testing.T) { existingObjs: testSpecCreateInstalledPackage{ repoName: "podinfo", repoNamespace: "namespace-1", - repoIndex: "testdata/podinfo-index.yaml", + repoIndex: testYaml("podinfo-index.yaml"), }, expectedStatusCode: codes.OK, expectedResponse: create_installed_package_resp_my_podinfo, @@ -1413,9 +1413,9 @@ var ( redis_existing_spec_completed = testSpecGetInstalledPackages{ repoName: "bitnami-1", repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", + repoIndex: testYaml("redis-many-versions.yaml"), chartName: "redis", - chartTarGz: "testdata/charts/redis-14.4.0.tgz", + chartTarGz: testTgz("redis-14.4.0.tgz"), chartSpecVersion: "14.4.0", chartArtifactVersion: "14.4.0", releaseName: "my-redis", @@ -1461,9 +1461,9 @@ var ( redis_existing_spec_completed_with_values_and_reconciliation_options = testSpecGetInstalledPackages{ repoName: "bitnami-1", repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", + repoIndex: testYaml("redis-many-versions.yaml"), chartName: "redis", - chartTarGz: "testdata/charts/redis-14.4.0.tgz", + chartTarGz: testTgz("redis-14.4.0.tgz"), chartSpecVersion: "14.4.0", chartArtifactVersion: "14.4.0", releaseName: "my-redis", @@ -1497,9 +1497,9 @@ var ( redis_existing_spec_failed = testSpecGetInstalledPackages{ repoName: "bitnami-1", repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", + repoIndex: testYaml("redis-many-versions.yaml"), chartName: "redis", - chartTarGz: "testdata/charts/redis-14.4.0.tgz", + chartTarGz: testTgz("redis-14.4.0.tgz"), chartSpecVersion: "14.4.0", chartArtifactVersion: "14.4.0", releaseName: "my-redis", @@ -1539,9 +1539,9 @@ var ( airflow_existing_spec_completed = testSpecGetInstalledPackages{ repoName: "bitnami-2", repoNamespace: "default", - repoIndex: "testdata/airflow-many-versions.yaml", + repoIndex: testYaml("airflow-many-versions.yaml"), chartName: "airflow", - chartTarGz: "testdata/charts/airflow-6.7.1.tgz", + chartTarGz: testTgz("airflow-6.7.1.tgz"), chartSpecVersion: "6.7.1", chartArtifactVersion: "6.7.1", releaseName: "my-airflow", @@ -1572,9 +1572,9 @@ var ( airflow_existing_spec_semver = testSpecGetInstalledPackages{ repoName: "bitnami-2", repoNamespace: "default", - repoIndex: "testdata/airflow-many-versions.yaml", + repoIndex: testYaml("airflow-many-versions.yaml"), chartName: "airflow", - chartTarGz: "testdata/charts/airflow-6.7.1.tgz", + chartTarGz: testTgz("airflow-6.7.1.tgz"), chartSpecVersion: "<=6.7.1", chartArtifactVersion: "6.7.1", releaseName: "my-airflow", @@ -1605,9 +1605,9 @@ var ( redis_existing_spec_pending = testSpecGetInstalledPackages{ repoName: "bitnami-1", repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", + repoIndex: testYaml("redis-many-versions.yaml"), chartName: "redis", - chartTarGz: "testdata/charts/redis-14.4.0.tgz", + chartTarGz: testTgz("redis-14.4.0.tgz"), chartSpecVersion: "14.4.0", chartArtifactVersion: "14.4.0", releaseName: "my-redis", @@ -1630,9 +1630,9 @@ var ( redis_existing_spec_pending_2 = testSpecGetInstalledPackages{ repoName: "bitnami-1", repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", + repoIndex: testYaml("redis-many-versions.yaml"), chartName: "redis", - chartTarGz: "testdata/charts/redis-14.4.0.tgz", + chartTarGz: testTgz("redis-14.4.0.tgz"), chartSpecVersion: "14.4.0", chartArtifactVersion: "14.4.0", releaseName: "my-redis", @@ -1664,9 +1664,9 @@ var ( redis_existing_spec_latest = testSpecGetInstalledPackages{ repoName: "bitnami-1", repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", + repoIndex: testYaml("redis-many-versions.yaml"), chartName: "redis", - chartTarGz: "testdata/charts/redis-14.4.0.tgz", + chartTarGz: testTgz("redis-14.4.0.tgz"), chartSpecVersion: "*", chartArtifactVersion: "14.4.0", releaseName: "my-redis", @@ -1948,7 +1948,7 @@ var ( redis_existing_spec_target_ns_is_set = testSpecGetInstalledPackages{ repoName: "bitnami-1", repoNamespace: "default", - repoIndex: "testdata/redis-many-versions.yaml", + repoIndex: "testdata/charts/redis-many-versions.yaml", chartName: "redis", chartTarGz: "testdata/charts/redis-14.4.0.tgz", chartSpecVersion: "14.4.0", diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go index 6200d8b163c..b0d9a85f266 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo.go @@ -427,7 +427,7 @@ func (s *repoEventSink) indexOneRepo(repo sourcev1.HelmRepository) ([]models.Cha modelRepo := &models.Repo{ Namespace: repo.Namespace, Name: repo.Name, - URL: indexUrl, + URL: repo.Spec.URL, Type: "helm", } diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go index e539070b03d..df5ffb0fcb8 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_integration_test.go @@ -40,7 +40,7 @@ func TestKindClusterAddThenDeleteRepo(t *testing.T) { // now load some large repos (bitnami) // I didn't want to store a large (10MB) copy of bitnami repo in our git, // so for now let it fetch from bitnami website - if err = kubeAddHelmRepository(t, "bitnami-1", "https://charts.bitnami.com/bitnami", "default", ""); err != nil { + if err = kubeAddHelmRepository(t, "bitnami-1", "https://charts.bitnami.com/bitnami", "default", "", 0); err != nil { t.Fatalf("%v", err) } // wait until this repo reaches 'Ready' state so that long indexation process kicks in @@ -65,7 +65,10 @@ func TestKindClusterAddThenDeleteRepo(t *testing.T) { } func TestKindClusterRepoWithBasicAuth(t *testing.T) { - fluxPluginClient, _ := checkEnv(t) + fluxPluginClient, _, err := checkEnv(t) + if err != nil { + t.Fatal(err) + } secretName := "podinfo-basic-auth-secret" repoName := "podinfo-basic-auth" @@ -80,7 +83,7 @@ func TestKindClusterRepoWithBasicAuth(t *testing.T) { } }) - if err := kubeAddHelmRepository(t, repoName, podinfo_basic_auth_repo_url, "default", secretName); err != nil { + if err := kubeAddHelmRepository(t, repoName, podinfo_basic_auth_repo_url, "default", secretName, 0); err != nil { t.Fatalf("%v", err) } t.Cleanup(func() { @@ -95,7 +98,10 @@ func TestKindClusterRepoWithBasicAuth(t *testing.T) { t.Fatalf("%v", err) } - grpcContext := newGrpcAdminContext(t, "test-create-admin-basic-auth") + grpcContext, err := newGrpcAdminContext(t, "test-create-admin-basic-auth") + if err != nil { + t.Fatal(err) + } const maxWait = 25 for i := 0; i <= maxWait; i++ { @@ -134,9 +140,13 @@ func TestKindClusterRepoWithBasicAuth(t *testing.T) { // first try the negative case, no auth - should fail due to not being able to // read secrets in all namespaces fluxPluginServiceAccount := "test-repo-with-basic-auth" - ctx, cancel := context.WithTimeout(newGrpcFluxPluginContext(t, fluxPluginServiceAccount), defaultContextTimeout) + grpcCtx, err := newGrpcFluxPluginContext(t, fluxPluginServiceAccount) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(grpcCtx, defaultContextTimeout) defer cancel() - _, err := fluxPluginClient.GetAvailablePackageDetail( + _, err = fluxPluginClient.GetAvailablePackageDetail( ctx, &corev1.GetAvailablePackageDetailRequest{AvailablePackageRef: availablePackageRef}) if err == nil { @@ -168,7 +178,10 @@ type integrationTestAddRepoSpec struct { } func TestKindClusterAddPackageRepository(t *testing.T) { - _, fluxPluginReposClient := checkEnv(t) + _, fluxPluginReposClient, err := checkEnv(t) + if err != nil { + t.Fatal(err) + } // these will be used further on for TLS-related scenarios. Init // byte arrays up front so they can be re-used in multiple places later @@ -314,7 +327,10 @@ func TestKindClusterAddPackageRepository(t *testing.T) { }, } - grpcContext := newGrpcAdminContext(t, "test-add-repo-admin") + grpcContext, err := newGrpcAdminContext(t, "test-add-repo-admin") + if err != nil { + t.Fatal(err) + } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go index 1b2722cb1de..0d2c66cb431 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/repo_test.go @@ -58,7 +58,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "bitnami-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/valid-index.yaml", + index: testYaml("valid-index.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{Context: &corev1.Context{}}, @@ -73,7 +73,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "bitnami-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/valid-index.yaml", + index: testYaml("valid-index.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{Context: &corev1.Context{Namespace: "default"}}, @@ -88,7 +88,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "bitnami-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/valid-index.yaml", + index: testYaml("valid-index.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{Context: &corev1.Context{ @@ -106,13 +106,13 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "bitnami-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/valid-index.yaml", + index: testYaml("valid-index.yaml"), }, { name: "jetstack-1", namespace: "ns1", url: "https://charts.jetstack.io", - index: "testdata/jetstack-index.yaml", + index: testYaml("jetstack-index.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{Context: &corev1.Context{Namespace: "non-default"}}, @@ -127,13 +127,13 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "bitnami-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/valid-index.yaml", + index: testYaml("valid-index.yaml"), }, { name: "jetstack-1", namespace: "ns1", url: "https://charts.jetstack.io", - index: "testdata/jetstack-index.yaml", + index: testYaml("jetstack-index.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -155,13 +155,13 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "bitnami-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/valid-index.yaml", + index: testYaml("valid-index.yaml"), }, { name: "jetstack-1", namespace: "ns1", url: "https://charts.jetstack.io", - index: "testdata/jetstack-index.yaml", + index: testYaml("jetstack-index.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -181,7 +181,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -203,7 +203,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -223,7 +223,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -243,7 +243,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -265,7 +265,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -285,7 +285,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -307,7 +307,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -327,7 +327,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -349,7 +349,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -371,7 +371,7 @@ func TestGetAvailablePackageSummariesWithoutPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, }, request: &corev1.GetAvailablePackageSummariesRequest{ @@ -451,7 +451,7 @@ func TestGetAvailablePackageSummariesWithPagination(t *testing.T) { name: "index-with-categories-1", namespace: "default", url: "https://example.repo.com/charts", - index: "testdata/index-with-categories.yaml", + index: testYaml("index-with-categories.yaml"), }, } repos := []sourcev1.HelmRepository{} @@ -574,12 +574,12 @@ func TestGetAvailablePackageSummariesWithPagination(t *testing.T) { func TestGetAvailablePackageSummaryAfterRepoIndexUpdate(t *testing.T) { t.Run("test get available package summaries after repo index is updated", func(t *testing.T) { - indexYamlBeforeUpdateBytes, err := ioutil.ReadFile("testdata/index-before-update.yaml") + indexYamlBeforeUpdateBytes, err := ioutil.ReadFile(testYaml("index-before-update.yaml")) if err != nil { t.Fatalf("%+v", err) } - indexYamlAfterUpdateBytes, err := ioutil.ReadFile("testdata/index-after-update.yaml") + indexYamlAfterUpdateBytes, err := ioutil.ReadFile(testYaml("index-after-update.yaml")) if err != nil { t.Fatalf("%+v", err) } @@ -741,7 +741,7 @@ func TestGetAvailablePackageSummaryAfterFluxHelmRepoDelete(t *testing.T) { charts = append(charts, c) } ts, repo, err := newRepoWithIndex( - "testdata/valid-index.yaml", repoName.Name, repoName.Namespace, replaceUrls, "") + testYaml("valid-index.yaml"), repoName.Name, repoName.Namespace, replaceUrls, "") if err != nil { t.Fatalf("%+v", err) } @@ -847,7 +847,7 @@ func TestGetAvailablePackageSummaryAfterFluxHelmRepoDelete(t *testing.T) { // test that causes RetryWatcher to stop and the cache needs to resync func TestGetAvailablePackageSummaryAfterCacheResync(t *testing.T) { t.Run("test that causes RetryWatcher to stop and the cache needs to resync", func(t *testing.T) { - ts2, repo, err := newRepoWithIndex("testdata/valid-index.yaml", "bitnami-1", "default", nil, "") + ts2, repo, err := newRepoWithIndex(testYaml("valid-index.yaml"), "bitnami-1", "default", nil, "") if err != nil { t.Fatalf("%+v", err) } @@ -958,7 +958,7 @@ func TestGetAvailablePackageSummariesAfterCacheResyncQueueNotIdle(t *testing.T) for i := 0; i < MAX_REPOS; i++ { repoName := fmt.Sprintf("bitnami-%d", i) - ts, repo, err := newRepoWithIndex("testdata/valid-index.yaml", repoName, "default", nil, "") + ts, repo, err := newRepoWithIndex(testYaml("valid-index.yaml"), repoName, "default", nil, "") if err != nil { t.Fatalf("%+v", err) } @@ -1095,7 +1095,7 @@ func TestGetAvailablePackageSummariesAfterCacheResyncQueueIdle(t *testing.T) { repoName := "bitnami-0" repoNamespace := "default" - ts, repo, err := newRepoWithIndex("testdata/valid-index.yaml", repoName, repoNamespace, nil, "") + ts, repo, err := newRepoWithIndex(testYaml("valid-index.yaml"), repoName, repoNamespace, nil, "") if err != nil { t.Fatalf("%+v", err) } @@ -1784,17 +1784,17 @@ var ( valid_index_charts_spec = []testSpecChartWithFile{ { name: "acs-engine-autoscaler", - tgzFile: "testdata/charts/acs-engine-autoscaler-2.1.1.tgz", + tgzFile: testTgz("acs-engine-autoscaler-2.1.1.tgz"), revision: "2.1.1", }, { name: "wordpress", - tgzFile: "testdata/charts/wordpress-0.7.5.tgz", + tgzFile: testTgz("wordpress-0.7.5.tgz"), revision: "0.7.5", }, { name: "wordpress", - tgzFile: "testdata/charts/wordpress-0.7.4.tgz", + tgzFile: testTgz("wordpress-0.7.4.tgz"), revision: "0.7.4", }, } diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go index 470185ddbc2..9407147f054 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server.go @@ -120,7 +120,7 @@ func NewServer(configGetter core.KubernetesConfigGetter, kubeappsCluster string, }, } if repoCache, err := cache.NewNamespacedResourceWatcherCache( - "repoCache", repoCacheConfig, redisCli, stopCh); err != nil { + "repoCache", repoCacheConfig, redisCli, stopCh, false); err != nil { return nil, err } else { return &Server{ diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server_test.go index fe11a5c9bac..dbffcb07e97 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/server_test.go @@ -463,13 +463,16 @@ func newServer(t *testing.T, } repoCache, err := cache.NewNamespacedResourceWatcherCache( - "repoCacheTest", cacheConfig, redisCli, stopCh) + "repoCacheTest", cacheConfig, redisCli, stopCh, true) if err != nil { return nil, mock, err } t.Cleanup(func() { repoCache.Shutdown() }) - // need to wait until ChartCache has finished syncing + // need to wait until repoCache has finished syncing + repoCache.WaitUntilResyncComplete() + + // need to wait until chartCache has finished syncing for key := range cachedChartKeys { chartCache.WaitUntilForgotten(key) } diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/test_util_test.go b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/test_util_test.go index ec48b6a9d20..5e4b690966f 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/test_util_test.go +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/test_util_test.go @@ -149,11 +149,11 @@ func compareJSONStrings(t *testing.T, expectedJSONString, actualJSONString strin // generate-cert.sh script in testdata directory is used to generate these files func getCertsForTesting(t *testing.T) (ca, pub, priv []byte) { var err error - if ca, err = ioutil.ReadFile("testdata/cert/ca.pem"); err != nil { + if ca, err = ioutil.ReadFile(testCert("ca.pem")); err != nil { t.Fatalf("%+v", err) - } else if pub, err = ioutil.ReadFile("testdata/cert/server.pem"); err != nil { + } else if pub, err = ioutil.ReadFile(testCert("server.pem")); err != nil { t.Fatalf("%+v", err) - } else if priv, err = ioutil.ReadFile("testdata/cert/server-key.pem"); err != nil { + } else if priv, err = ioutil.ReadFile(testCert("server-key.pem")); err != nil { t.Fatalf("%+v", err) } return ca, pub, priv @@ -334,22 +334,36 @@ func ctrlClientAndWatcher(t *testing.T, s *Server) (client.WithWatch, *watch.Rac } } +func testTgz(name string) string { + return "./testdata/charts/" + name +} + +func testYaml(name string) string { + return "./testdata/charts/" + name +} + +func testCert(name string) string { + return "./testdata/cert/" + name +} + // misc global vars that get re-used in multiple tests -var fluxPlugin = &plugins.Plugin{Name: "fluxv2.packages", Version: "v1alpha1"} -var fluxHelmRepositoryCRD = &apiextv1.CustomResourceDefinition{ - TypeMeta: metav1.TypeMeta{ - Kind: "CustomResourceDefinition", - APIVersion: "apiextensions.k8s.io/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "helmrepositories.source.toolkit.fluxcd.io", - }, - Status: apiextv1.CustomResourceDefinitionStatus{ - Conditions: []apiextv1.CustomResourceDefinitionCondition{ - { - Type: "Established", - Status: "True", +var ( + fluxPlugin = &plugins.Plugin{Name: "fluxv2.packages", Version: "v1alpha1"} + fluxHelmRepositoryCRD = &apiextv1.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "helmrepositories.source.toolkit.fluxcd.io", + }, + Status: apiextv1.CustomResourceDefinitionStatus{ + Conditions: []apiextv1.CustomResourceDefinitionCondition{ + { + Type: "Established", + Status: "True", + }, }, }, - }, -} + } +) diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/Dockerfile b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/Dockerfile index fd5d08bfe8f..a12adb1d4ee 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/Dockerfile +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/Dockerfile @@ -9,16 +9,16 @@ COPY ./nginx.conf /etc/nginx/nginx.conf # only has a single user: foo, password: bar COPY ./.htpasswd /etc/apache2/.htpasswd -COPY ./podinfo-index.yaml /usr/share/nginx/html/podinfo/index.yaml +COPY ./charts/podinfo-index.yaml /usr/share/nginx/html/podinfo/index.yaml COPY ./charts/podinfo-6.0.0.tgz /usr/share/nginx/html/podinfo/ COPY ./charts/podinfo-5.2.1.tgz /usr/share/nginx/html/podinfo/ -COPY ./podinfo-basic-auth-index.yaml /usr/share/nginx/html/podinfo-basic-auth/index.yaml +COPY ./charts/podinfo-basic-auth-index.yaml /usr/share/nginx/html/podinfo-basic-auth/index.yaml COPY ./charts/podinfo-6.0.0.tgz /usr/share/nginx/html/podinfo-basic-auth/ COPY ./charts/podinfo-5.2.1.tgz /usr/share/nginx/html/podinfo-basic-auth/ COPY ./cert/ssl-bundle.pem /etc/ssl/certs/ COPY ./cert/server-key.pem /etc/ssl/certs/ -COPY ./podinfo-tls-index.yaml /usr/share/nginx/html/podinfo-tls/index.yaml +COPY ./charts/podinfo-tls-index.yaml /usr/share/nginx/html/podinfo-tls/index.yaml COPY ./charts/podinfo-6.0.0.tgz /usr/share/nginx/html/podinfo-tls/ COPY ./charts/podinfo-5.2.1.tgz /usr/share/nginx/html/podinfo-tls/ diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/airflow-1.0.0.tgz b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/airflow-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..df26ea056f1b4deb508bb3e20b8bd8a72c166ea6 GIT binary patch literal 69180 zcmZ^qQ*@<4yKdKtZQHi7V%uiNww-ir+ji2i(VcW`Cmq{%I@$f7v-izGUDQ>L@8+vf z?-MaFYb`TXhl8)EGaRR z6|xavrI+Hf>>YRSb1V}8&%H&PlzhQcwc3goGi2`wz<&?#CAAPM*t#N!Cq<}`xXfTStPI?^^my8Ss_DfJT=$>ba%?xcy1Z=vTHM<7ef4`*D(rnLIw(klAN^c3 z>}p_eG8zR3|6n&EP@4yGcO&9J%*_Z?TwZX3h77{Niwt|kJ|Q|>ssJLv5nu(1gCc-U zRxoHEN_%h^Fyd%Zb95aVW4EClGcZVxqH84}!f);eUvkQ5PIS$oE4-2tc#<8b$H1E9+&FS7jmP7$gZ5f%B)yz%&r6$79*P0uM7 z96E>|R_eNr5N_o%TM-gc4G)h64FzA^4qOaK353uyVL0cQ8aZnIQc0tBf`h>%IiPXC zFXYFD1TzNH=Y#b_h@z**A1x*#00gkw&jwQv0_b=Egj6v=8a(_sL;wcl$;HK=t@6qo zNQdKtmqy>^^R}nlw(6Vwmq^!vSw~WfQSk4yK_WY*TB?FOzw~(RZ9EoBO~D*+u`*-Z z@bQB-yp^UEV(Iz0Ce!HAKVn31RJZ5V!Vd>G zfKIPSgB>w8xLufF)LxO;(MMDgu0ef8YOUkg1ZwR{^C5hE?~Yu_nk5^%Aaah-BIY2f z7#~(Vo2^xVmdkHeXp|2ZC!hIwdbMJadN3&sPyeW2_@j;Dd>LLuBq+j6o_JI~h-@UY z;za3@{BggbI*cEnd|`y$Wk>ly1Wv&cG-O_bED!#Vc@vL66M1Rz?t1V>boGS&DsEMpeBG1{*clwObUV87Jh7u27@Zl9GDA>@?Z`~mW_{FC~L4il0o!tB-rO= zyg~ZcWO@_xg}~$*ZO_OaODqx8l=PDuZIQLv0S~-^c->D}w9&JmuS#xFB<(NU(7r$s z{|iO_9WO7*VMs9o)Hwi2q2YG@{Go2Lsfb-CS4U8Y?<*IZ8aAhuVT)TYRu?=CdSvU< zx^K7Qdsy?c|9Sc4MW4(1y^qD{FVOZ6AHwJ7CQg`6VLK*GDR4Zrb{N8Y;izh=;8k}I zqsa);>UB;p?eb?b{kFWnuO&F&=5o-ux1S%M|KN5`4yxaEF$z*TJV=3(bB?F)z4JJ@ zgpprIF$W~%^BO~atofL*xjg^<3MN?{XumGUfn5}IhVrwBLP3+amXn~skLtG8hvfGmNiEg!br}daLp*s_$7zobY60vq$ z7Q;S>N#(fB8(ZZA3Lx!&%V!MZACFi4P-GJ+mAdI+9Qn?O7er*jty(gnoFai65zEEtL8uB*M0E@Bn9a zbr}hEAte+ z?o1BZa|xbh!Do}R12~s~P`BuV5$|9?xhMZ`o5bFixwYLkp$}?u^gun!d z>?k3wDY5t@pXhyFg!n+Ox${@F{)WmNO&RB^_?9&=&{pNCC(=x;5ZtF^aPrKHrkc4= z+^P&$mPw6GM%`pQlbT|uMXuYP;fRDZm_95-X)M|asBc)sqm?F)4}z}v(D{y7 zSd+BAkju5V;}M$*Q;{7Nias#H@gP78mWBBTaMXZ=!XPdaj=M?9jgiX9HQ;Rw&;Y;6 zM&8&#d5u)q-xdl3AA&)Pw46BT=dgPxoRx1c={zYCtYg&zSL>!ZaEU1U-I(I&RZ%-) z&lsm2w_BbH>ZFPWbk(B;I{di)&f25mcwmU4{sWg(C5L;NbO6w9W=O!Atqsr4vs&HT2hSJF^$CW`Ebz4&~yZBVx&cy8>#fpJS zM3p#@q}e>QcMrAt78X7bHgpm>D?US-jOadwSjh19&RBx}ss|f(bfdl0cM_ zRql>_c9JbjO5N!lSFY>1k*b$K%IR~H#|LOLy9URg#qF`QF)$nQM^wY3=$271h1yQU zq2e}-(-+2Ind}_`H)KM$PaUJx};k$gFpE4s^uQ5_p-N z&OD8~7@$fk)oT5rgVaE`IN!XL3|}<-YZqnP{`ZvSAXNPRP>yhc zSOv>V*OQ3bR=pLyeu?!VWP0P5_$ z33*s@U79^fRhWF@M;uN>Cb%0miPj!}f>LYxbiy?Mz^`AVCXpR52#2B{k)u;1U(`hx zadbK!aQZBKLEwQlulRbRD;gEdV(304aS)2XnB2x(%8ulF7kJt&^CT?4Za$het_yOB z2_Cz@e4Tup=Pz+{bA~&-hecbmM2psC=tcHLG&rcZol*VXdc#x}3gkF0#LuO^8)Ql5 zgzI;&pSNK)d{EreYW0^+?0i`*ApYc$c-<=0eI8Cmz3O)=+(M)T_zH~X#o$jyplvmwUXgOt>A$d#iGTF; z_jKa~Aw57m^!Lu)Je|3XR)QqV0GgN~h)SDKeBq{sg&`nBi9J(sn`NBrj5lzeD|)s~ zCB7pL>c0?k9`OhTd>}&G%{9fVd)`iq(~`At5$;8Eayo`8b3mrbj(0 zK8a+fC@$-hh(Fl6M1eBzs7#ln8W4K9#Ru~~<;;rH%vrijoy5pGg|Sm)2#t_&f#$r_ zr%drW^Z;f_r(Rd?!eY2%bn_!4=6-8wWB=>#VA4Q`OIfBQI{5-xtoS9$Hm-*V`1*X= z0qJrH0J&8o{#4OSW-S3aJH>jU$1a_9_D5t6&E~kL+3Z6KQNDPEevGrd7x)iHj=t)@ zR*mu2VpHzbu2T5X?9(88fWGOTdu${{pe#pF(mk|}hCKKp@EG{P zDBfiBdZqnM5xx=RHc~b2@#T$S{ENjIG$?4+b?43_Tz|t3ZyyR{vZ^rMWojw#U1Qc} z485Du*EcV?=Eq#4BcC@RGN2xQla((`^d(Fc`|6Ky8@hX;kksLp3Gpc+Mz7ZK_p0d7 zZr3TO_jtbz02F*w!g8eDZ>lOtDkh{t;wPHJ$gBmJ@18hkObomV=*GA$a(xBI;?cgu=2z=hV(?CRVl)( z4;RjFEM+~c`UKcZX}we&GR+YH7sfBCpAp+w$k>K19tw`s151?&4+Uv1%U?hKX*OX( zW)8iQEVod1O3EA~(7NmMu@G0r^q?>G| zVa-&ZAK=cTD;It{c}{FIt!Ym89H_V1_1}@Xcs@82FR20rMpf12`o$gxYm!&wz-egc zEwFL5;(?;uqS&r-(jSJDw@1uMbDEFRV0+HP>_YhpJ2xBxH)4(YspkgQfK&dj3LBLV zMJBPz(7Ky+{BpgGTva_}_!iYz)lKZ<7pv>32k2~ zXi(<%VJoj~FXK1Sq#Vt(5M;vfQQY7UoRaPm#6aoXTLuI;MUsp?rerAHNznHJO%<>d z$#WjGw;fl?GRy5K@V12PtBIGN_^dV+FR^f?3B#V)(8jfwY&1bGT)}cx*JG|!!U$@Y zIA!5$$&RfY<`eprWP~?8n~~Fn{2*BBAn}8Nt3X>80(r~;e2aE{8#<@TWx-VqM9ABo zwM%0=(K&3OLkks?fI^f+1E+h?exNOUjgFc1PvD~Gn&Z0^fg_-|kkm)<_1ec&l zjM$~Gj?$nN76(ta0G^0@B3Sj^I?h^Oh1C`d&wMt~j}z-hvRD+4X_QYvya_TV^)J0n z(iG7WA;{DhHA({OZKN?1M{s72bKH0ax)T(d@A`6CRZGrHI<7-*3u1nrsQlDaemobi zYrlwGNGB#GCM{2A%<-v`3l}x#6s)eL9P}Ut5JfrRQ%4-ur=syo)~1;h?Msf8!W!vjlhy% ztyZ-Lb^0jH<2M7`=)$QmzyU!cK)TZynCSj9xi^*;8(Q$+{9GP4g-EemaAA>VCiIOU z%g2RQ^A{Q_8qf$aqdLN_!&R<`{L{+Uk|vN4N@Uy_)s zk^Ys%rY#qOC;bMOg+s!~IgYytjV>?RIo|HEu`S?uZ@GhKyREmR_$R1Iojj>=LGF%( zB*nLc6!sS)S>!#uBy_kweJ{sDLLzd-T=|AQzC%EW6;1KVdv>AdMFfID9PXa!nXk$% zNv~@`J14y9&5z@j;3i$yvIDjAs4xh5Q6pnoSt;Y2%kW*W#B?WCXfX(hQ4GuSX~=MW zzATbG+~cDK>+5Uko%b(InvTdcUl}gHsfy_XwSDszl$!OJYUo99q9X=%LrP;5A96GO z+=Sv4$Hfg%!Nu6Sy%UAsbJFaVveas1i5CIC(QmDZl;>)hoFxx8v^QCYQzk41Q|Lt8 zrzDdwpgv-dOxA0JdX6Li#v$?9ssSD5s%u$Yb& z(TUZKnx!m1!9t8pXUI-FN$c7$`8#=Xf+oW3!BI0Pe}T=0KC?mjXULG0`$q$9H^I;S zr!4hPiAtSjtVea@IXXR2cnhZdu_QD1f;-_?<9BHro;u1e62Y}5q%nX?iFqUSn$GWn zH3ADrJBjdy(Pr4N9*OwQOe5ybzR3`+MIExKll{ZFW!gDxR=O;5bK)p{^htr2le|vl z2E&h~)k-pfR}^*!hIMB|;&sn6T{4#RH@$I?UTrmo{jmeC2;YjNIhBO7Ly~XahZS=| zbZ8QTuCZ=eeXC`5ShLYvHotpF6?iTG7Qj%>p}sJd4U_|4ea2N}gMiM2^6*ecYH&Ea zO8S*@b;{4RD*`^G)z>TLIzt)CnA?`pwQ{Y&xm77%bc0L&#kvaqjo|4 zUPv)wo71O8&(~$YjKHUEOK3;ME~y>P4mP`bQVbtw+nSf6rxc;SSu^V0W5R_5+~RTl zO&N|QIo2}A$JUrt1B$K;^eWs(`jIp6dJb5odn#s6Lamw=2sqYu0b>-NI1r~ckkyZ` z#mRrUe)*63p8o-rz@XQbO8vQO3*rq6pZ3G0yekgPHbnf8$M1XbxtgDNaR;cBRfc}` zAC^oKm2JaQR5~_+&x`iQp#jWYoOxSr=a}zK^iMGr=2fk=;aS~2T&^PL677?s(>u;! z--Vfu&CZ|De1`P$vPIh9E3kdx@^&78O2L_pGh4#XiZ){Ko$VOP*-v<&wsOFyTckBCqat`k>gBE%XQCIV|Wy@+p36INbI z@u*5=G*CuX-aB{?cb++mT>zm*4%uJ+WUPu#fDmpng-q+RDN>RcaCfw6r_&Y{js4Z> z{5YULCsqbpoW3NL^4Xwsfs+6su!gUH=_W`aVnwai>yMS^W)Behi`8Q)$kH!!Uz{vR z)o}C8-L**YC-{9@b|##C1Pza0Ue=*A^NdSEVNYwz{|j)qBN1t&2ua3%C`n0WQae10 zHy|%u<3-VK8hXAiyrYgg z(>1W~;i8?p`rB8qg5^iF;741>4q-)W>q+xF2}^=-hl>PJjC?W-?!!`mP7D*yxw6=+ z%ALSEQlmw%j&qvZ`0FIK(hp6_N>nJdZv@+5P!0R#Gv%f%IuN(}S0Kj>wgEjylD^+4 zqz7-vBq6vfH2JUkBD1Vix2(5f0t|a}3GP%Z)xawAakS?}TsSwUmY?JFLpgoR+M#XX zO_^0-7EjP>u3c50c{JjS@B2ur+uPd--*Lvdx7+(O^n=S2sMX%0V zi%E`cp9p(Py}%^102bSAu@fs4Bc*#&~us9b$Ko`KsC&(As4 zI~a-w-{>yZV_AEhHkBnw?f|dYCB4gs1)2|@d-nn~JgY;DNM$3@MuEh+%gMlp1 zqsn$g$wMSgwsO%{LTb+3QH2sCtvl*ydEvjnVi7_?qYpQM+xw?fd*Q2((R#XcC!oU=FRJEXrV)y72- zb~m%3?8Cu}7vOW4_SAOM(gyOyZE+;ZYpw8g?EBr4|5u9H{i9KK<~P|lA`IyOlZC#% zF3;^r534^elSlvVzo6Kepe!^<=?zU%G4$@fg}66PG9xy`m>j({%8ttACnUx`%swB6 zNFdG#8BJ|gls1ZnQYbG?Ew+DxOf+1hvMLC@1^8$CIu*N_6<>aVwRbuxQNpg0l%Mp?PRqyyqQx>VDF5D@#sVjAb z2>eOBfG66wm%s!vl+`BlxhL{Sm)>fUZT2p7Cyw?4dY))L;jcGj|)$E|p;7Ubkg!o4+-@|D7ZLMA}cBP`hz^rM|DxwbSjQz)dVm;C6ys~Dj6_7#5hG%q1Hx@{-KxNX_<69Ro!m;iMu*T#XQdcL*i0V>*5Ss)qL2O-Fa(7O`S+jV|?zO#~ zhb@Ov_#@C~M=hz;@-!5j0(_#W`fakgDI80(_xNKp@(P`UB_^}-^$-dTOS2G1D$kU! z@o3KS^v)!eL3Gyf;FXSfm8}_0(0V`5>X*#xH0j^?Ekjot*gd>z_!1Oum%}TSl|)JSjx18#=G@ZSuGXBcJ=~FN=Ixa!9=KE4S zZD$=l)%>sVarV0-#$=3{C$e&2@|3M!xxeqqT_t@M*xEbqJ&LWzfY2iM<6q@6Yqqi_ zd~zZxqtjaJF>dVCs6}{RWF8Hx2T48H6y_3T@Ue`!$J#|X!hG6SS)*~8zD{u>Ma;Q? zzhm5iCEwMfUf>krB=d|YHD*twIY!=V2CW9Hf-(mpx%pM3`2pE$e?qDnB+iYqz-fvII8*s%AMNj&YG^5~d=C^m%#XjO4sl zvWm9-5W_p>V!{~`_d}w<3`ub!s79F6(67M5No3_=jMyXYXMzt?gy9QpV9N4(u+^KO zB0kga!L-n>i;5!{^^V7z+$kuzmMl|!`C)ZIS!L8*G9$uTxG0sFiWx)Ao#n_DeB;-9 z#E7R(yzt}t&t!;%c4mr7JHm<205x9%x1IG^ zNK_nN;)j(zj%C?r9fC2b5exW$g)1pwBsC@skegQhQU#=iQ%v?|Icm418oP|nu=)EJ zPou8GvE=R^#-U!66jPZQgy5mL+tW%G1y>qr=aZlTm4w$JqyEqp76OAsAofpjs=WJOg~dF1561)IyBQxF zWPNf*kvfd$X^NAdBfEX`r@#9Zq3lF(`gw>htbcXlaWSM21x`h!xtctti!F|2~YrR{Re#%myfC5eF)gpZpK{=MiliC3`Ul(fE zswcRiBaA2J&145cU4Z(8-KjbJi$uA1F6USC5H>_hb3O9r#VN?Jg+a08#fM;EwebWJ za}dc&m^NnDyjpF4HsCnpu3EKp*a>fW^-z+WRC}-&9T=+0;U?|0(?&eq#?R>g1 z)sS)&L?uEqUa}*;kS!E8g~P2!(cPTTl#7cc-}rhyCcz86B+~eAQm2({hRY^P*ZZC8 z)6|Wc^p+X2-OC~9k3hBEGbU#&ISzujR#q~XS{fQy^qw`@(7!Z1MrEXGqH|pXOpjzW zv9{rmeq7w!nqvvL`CC@AwUzjW^45o@tNDY<+*Gny=LcK; zgWXsf=*DrELoNVLF25)zx7q63;QlTyCN`hTay3=0Ny&sZV)AoO26_5edLKn}7V&eo z9g_$b{A_1=Ud+NYm|H#Ui5gjV{oH{JT~>S)cy2tkB1;t(93-Bl|`LaqSX1!Er~aQnQ#y942lMIFpw& zK_KsG$1--3FhZ$qy-J)ay9x5l52ON14R=v+7sOw(#`u!;)=tiWQ~bf5sluj$s#;33|Lq+th+Y$g31cT9FgzuQc}=j%>WEM4_%oQ=XfESjmj zqad?y6!KIj$~!~zBZ_kb>CRBvEZqa(zNqg?p)+e9B~$O??_Rz^P7ja&)UnDQmqN`xw-WM98r?c$NU@j*z5O4`x-A6izj%75K8BY1 zd-X+>Ynbci`$YjbajGILoWESm>~D`%tez=i=*=G)K2o{v{Ra_8U+0^%C$PY;uJtrE zWe=LR7(!~z9VT;s7E0F4eQVM2&ca#Ob`~M z)OoJnI4>Z~_3*`pj@Z<(e$ni_hhGt26~srG?T_(nxyNM zm`Y&DN@TC)r>Jx?zEnG4%P1eDPS$T0T>VO4|)|--XCp@=FS%x*_qfjFAPP?Q8FKOEk#)aB#*lI87eeOvJC+H`g^# z6#kA_BbYtA&+uCOxKe75w5m0tGgXFYya+Uiz~m&G!T!B<`V7}^=Fd*N9Q$`$C}3?g z1=Bd*5XEg#*7xCW?`*3fVY_1lA~FIOL>B^ zg8BRMM>_$dV&LqNc5LS`IHLn8IhV!Eq34l=o0pbd==(IvG|Y}?%^t;u%ol{k2_&lY zmQv!Zaf^(@Kb?`-V#_4w_hujiD3Sl+(FXhVEpOqCk3Q=x^0Xvxo$gXXSp~VF5Z+ma zg97sBtaDM%TJ$TUN!j_ti4cTgZSw_nSff`QwgXDFDM^5bVEC|Ll z>L;is!VMKUG1VPTZMa9hixOR%Pj((`K1@ANcar5`naglWs@rVDC&D#ZupYOq@U)P& z@1fo3k2;9IZ8VR@bs;pM!0A-}b+?LT@| zlad%`cJKr(y{5qHiII^Wz|e$K0KMjP4GBx3M*I5&gglXElxjrAP^h$f(){Sj*qN9P zL?0LQgd@OE8^fYNh_tsa%{1Aj5LI{9b9u%m`+?^KP>-}rc2x|);m6Dymv}iH(*k5g zV0ZK1di=Gxy#+#BzL-4Aom2v$MP;i&s(?3jdnDp5ms|GK%R=!!p6~QZCFlRxVU|B*c-TS|Lqz zdyJIJ&<Y280etP8ZnS)(#q5p&N4YkaKvws`| z--QQ<-5k2WSJD_{+;tnsUN&iajW&sjYD|eXwy(>X0C_$TIcOLtd~TzYKdA0N$Oo3-PLiU=f^Bu0Xrxi|5|WRTz4EWev zM1_Lrm)2_2zFP}4so>vj;wCaX$3n0=*VczbpZi@#M9-Z_X`@L}8w$i+@Be5e`05ta zdvDgfo#;A)VX;$%6cK>7wqNPk^mv!*DTFR8M2Md)LgsBqjo``LVAVBsHLx z?loSH)PZNp6h_5uhJd=O1f=}LKnVp+P5kpS?Ej%s5CH)#ufm~>T~+Qru!LJXzBN2% zq*|xDti!+V?87BCgA3B;T+@~egOVJ%uog3Pw7nSjmbrvJZF*4eyj+ENr z^WB#xUI|OX$4S9vV8Z&tEd;iBOs38VqNwYgJYC{?EsbnSr}X$l|U zcb=RqsuE6eXPHHYwur;IXw6SA>|GL^tlz-H}iT;$r7vF@X3 za7BtQL+%<6)^{;jL7-C&;^I+H+BsX_s|e~gJqVNaK2`k*`8_i-Lfo*`HT*4diZwV* z#0_($4)M3m%3Y?5^N#7LtuhY$aCe6TSTv^SP0$}aB#xB3ach2iOUHa1%DGYo<|PaX zFz4M>J|s%^`t*>s9?#bf8{o@_2oR&)?Hi!R#V2mhsOqLMTMGw#ynBV6^xCCB@SGv?bCZw zl>I>-ObG>Xc)W<~QrC4?tvMWt^hqqd{x`xc>*KjCmK>0q5&T5v`2DZTb)j&2%ePL!L`B|U>vG969?@)O#HFsPTjH-zFpK71^>1x`KLq>Ce zHmI-1jN@`@77`iJg0IA#x$raP(IPf$ zqTzQDYvC5^Kb$+;58JCE!WBMbB^!Sjh3>EIUx?5f>*uDx$IJBr;9MQRekc z^0&Z;B@VA}e_SCtfQ#W_< zL*{eEg4>tnLnFj0t-Luqk>N)r{&0UX77U|b|9_bAk!e?2Rn?~i^mvV?+?DcawWdYl z3qB#}fbgT0&ZaI}x29MgUP%+v+(Hx;OUAxWK1>}F3^YjIS#JlG3R&4`O$sM4wvyUG zH>weVm!_w-$4SXFG`S)d)qHSxPBqfLOlFV;C&j5L=|Nue0zPkOZq`@01xsi^b~0ve zxTbkoF{tUeS_EC+e?0ECvw-S3;2R%a)UYJ2h6k3AtN{Fd164-$=gkeGqwp|;h*&wN zJ@gTr%7(Cl8-bgmM$5NO^RC3wpsxQXXmg zqgPCY*X3>7lUK7+YpZ4X``=oKthFUiqD(|HI;qLrCK?mAQoCZSxs)`c47a^DCwi|X z6|Irh&b?qG#s-bS$c!U&oVuNzT-Oi9J@K|1Clwgvf5uge1ZirCUL>Myu%(_i56bQy zNipouBlubQj+%lrRS2n}0hIy5Xn*#?kn^b-Yfwby?Vg!^Ymnv?XievFm(Dn~4#W^i z7jbBUhn2$Z#0x_VD!Bb4qo4l6KQmGyUE<@;ps@fzQxm8AkhK%_CQzGkE4%5bd^-d! zee9+NG>GfSS~;(pVf-(TvauWQd4q^(@H zgaKQp;h$x9Q26#{hvoz|2XSl%azA>rfDAT4jkDtxL|2KR2QelT{yyzkZ0$5U1~}qe z$S*lH?>e5*KjieWeQob-$l?96D0Li*`U28KPkphV4muv_K8<0`Bh<bJavc)k3?4&F5K6>cxbl+(AZoc@+*XTh;pc)o<=Z&gUA<=2U8Tv-m2wqtoCAR_zA_{nJ7K*~Z2SAvErqdWjF`~?PrzwY)9xw(aU`#IPD4m03Ow)8Bf>lijcps-4vtSG60K5!DD@v0E1zVqFub$em0l2Tfw~v&&Imzdv|r1_u@G%0Gz_;$}`= z973^za@+K^i)Ni*=YGexWgkR*cg};qR^3rM5hY@AZy$t6_zi}DvPvMz#MM4XRb&VD zS3wx}h84T*U&AJ7ovGAg)56kq>{ThK?!6X@>-aF3FM7F-UMYvvqevLnkjvTs`Nw2V z;}fd~4l*%8sEIr-mRerh7=-u8R&6UhUDSoj81p0rHFbMmt2Q}P)EcLtJNioHpU9LG z>M0X8KXPZUxIL1577SDIP|5mYY`SGj6|`Xedm}EF4CC|VPdwYexYqt~>)o+8+gidm z%U2Mn&EeeE)z|BPPq%zzPEwsZ&bD}TRnY9re#9cpXWE0O@#wEt+%ZCvC8FweCi>;fx+cPh}jT9ElUSF#MJs&SnR}(@UXup zBEY==&Kjg>$t<^qJysL$?FVc6c}Y=njEj%Hq@WDkRA_->SP|9L3QZ&#Ns{4(fX#2f z9c2vGA!nq%-&+TFHjifdjSK$`= zLCq_}a|#>4Jyqn;J6XF$00@Z}9?%eu+x;YQy-94j8e#!PNAsQB?L=%E8&2H^#GxOE z!PuB#<)SCiH=7J5CN@0!7Jx?muY}`W-OXUpr;Pdy7RwRy$j}u@h1pk8QlXZy7jlMp zbKbTjn_*|&Tr)S$rMP6@5AsvreZPkQ*_47tP{K}6B037E^o5V14S2^JtxgMAj};3- zn}T@z%t#PIia3e(aBS2w=&S@=!CSE=NxZ%BtooH5&z^xM4O?z9$9h<7 zqLOnXon~MdP$$fmh}=2AB$e2?-|1G8!+_|=hlgv}*jWY$N9>^RTZn*gl4_pL`eh{_}HDmWX7L6a@UJcP> zjgU|Shs|NA(?fYZmQUGt|JRSDf|@*Cy2?FP+e! z$VXU2N(Cd#2Dxnb>ZzeNb^@V@f^7V0_~%UgTr^AqEfJ0043?z4GY__BBg+`fp^z9) zqqpm4ZK~N=%0p^ zUYgcV)sQ3WH?3TLb%;h1nXf*GM(M)ZOouhCL6E62sA3=OANB{Y0k7CyF8_qfpos5j zR^jo5LExj!T3W;mi#vWWE{U~UHPrc`G3&eoCV-=|{)U;_HD?XeMH8B>m8pyLpr00D zz8Exgc8SR_2HxRn5r?S|X5#y~CtNaJbJx6AEc^#u{G}}6=4e|)qLMCd_wr;pnq}Pc~v_tGGpUg7T33 zBdO)Ag%{(@HgP&$UT3R?`mTCaHKdt$G$pkk%7^L#7I5JQDux&jsf(kH-hj5G{?Q{e zU9Nv;#fWDIAR?>b_mkq$ae%Ni!1gvy0Pg^paXm*kA9qk+iJRLE`3Y%wQNmZ9J9{Sh z_v4C4>Tob!d~P)ACbE%`qr5V?!Rjfi*f1C`OH0epVIBe=ro4**K$mHYDdh_)R?OT2 z@O$Zic?;Y@`I#8d|3At?hq4GAcJ6!*q@_A^t5?VCKd+NV7llgpeB_|JR2_iKZeHg% zVBXbyL-%0ejg}Vd9(u5AkJ;bHEfn;&S4ex;NMaRS+27AZUJa%Wo<>@Oh1RD^^P;1y zu(PQb`8QDmrr2mSl-54e?3cbQkBeBaBMMG7Rf%#nI7cZ0kf%_7(#hJ82;O`9C!O}r z?hY6eOLwmmiZH7tcPT5YM|044N(w1u3)m>iO_konhqkW<{1D`}N_J7$PJdX(XPW>O z*bn@Z@4$cUQB;w@VL76>#%Xs*lujwjcuc`%?8sXjcwaj(^ze6+qSLq<#~TEzU!L5X0ADB&YXSr zUiZ4!T05^$cCKvqU4y$?TjKtdjY>vi_V=}o=g+NC2K8^TSe7vSrKyoz1vay$vwF=*w|8{PpmWZ{Y3y zawgR1VaN~a1gItrmSD~KNwH(!$~s&PwKqFs4`xcTF7T)p#d~*E;?bV{S&IUQ2*Y&; zKYPlGZCf==&x>4~v3*8EVG)M2Rw{N$kd2co;?W3ORdWH0#t~JC-a!TvDBRi2$ucA) zc27#&2z4f3VRtZ&Y`LbIn`$#IBBc}?rnH7d3YLiVdS2IMHlr;f=%5I_VnHSV`y$VC z*wOfRvsHaAFV{#$jO`Vw@DupTriE9cD#U#z=OUWPtYy05m<3Q!uCS@5eJ>Wh?GE1R zqD%rt*9&DN3tP!H7bc!z$qt5tfa>b*ZtvhgXt(4} zjen+<>aGxt(q_?NZ!=-Vg?AAa!wsVm3;u7nejSxyJ2YB`Gv_SEKW_bGSNFD_GXkmu zvi=xC^!+l3L@+75u|Oj3bB>;jz;!1Fd>`)4?;h{(@IKtl?fl$ryyy4bPZZqzLVUcP zyn;d}{x?mVo2M!LiUSssD`4OD)T7QJCvOW=tUCkT`3n?IgJmUq%nMxB>*s5T3~}baNNeQ~9XR!vi0d z?Ql2NGXrgSrKZ=-KQ`j36tA15QDd*B+kLh$9(3mZKp+0xSTDS7T#}Pmc8)yGq+xI_ zXqDQNPj@+jk`4Pjc%5lhNF8%}12^Z>Lr`eyGWP!XqjjML-&le@g~ReQ!Ob9q^AO8B z@wg~kYrqSd20I0M=40!12l=}lRWp03an|qhSo%|`o8NGx#TDv1S{)P>0h%;Hu{z(X2qTn{mo(H z2Iud?LlpTNrvZx*9Wv+^F>^AGpVTBmN7AY0o>rR*&dEvhcPT+urS-+MdMRFz`9yar zI7=sCiXIOZvkQ21$#$uFz=w(`jRbs;*oo2tdP>f6n3ncwMx#(pt>R0QulYH{r!Si5 zXM~Nv$YoQtQ=>wKqhi4v)NwD-(~oUz}12rtKy*D&jO(Erv9 zPc!gJ>V6-rvSEj2fd9}+#?9Wl-6Tjzygqfp`N2RAPK{va&RxgK3f$5M#Z8PQ2<<1N z0M917Fri1fVRT0(zh57=)nDceB;Hp*@5g&O@7|FXko2Ulc0Y=GyO3&A!J5I7KZA1# z`;t0-y#2}k2L)8mN9sc<1;Q}AY-k|95w8Eo2h?cxKl+0HAk~&?HOGa>hMDkfaL5s0 zTblU#e>;3pz0`&EH%v}Vnk7(8jAR~AePbR&NDg3=5b62? z|8{8qTRY&;mSVA1@vgXLhyfI*wtol)cqLvcd4A_i-l8G##YymP`xLb8$d7s7)lB`W zIbruV(bl9OkhW(wpB$E)eR+`vHg_-eA8u3Lx1N@k;&m#otr}|#~dJcD$p6>^y zGA7u|C8tU-ZjjYWm>a9>HVf|t7^rS`tL`l?HyjnX)h9xPtJCWo#Ss9whFPd=w5=e`(rup-{ z-MDqM#x6$K3im6=3-tkVU7=~cwD800-_EPfTFnhIrcGqSh zL~QsWh;W~%eikCuBO-3+Il6y-+sl)U`*|pNRf%(lGEFeF+QQs7QVbcF(Quk-L+`nQ z$W#J?L$lGFV{P;koq^aScALiTHJQRYazkNi(>zjF{%IPN`Z(ytfV&T2o19A3N|T0qCD-W-)Wt%({E z?OY0Gy3+s$SiFrFh47K=sSiDNqnp8qKl7jbiIUzk7eNiZcE-i_E{8=969@H1Zen~Q zgNdqj{i|7v*;(@QRBCvI>*FAOWb=qD6%@Db^@oiV@Yqg$b!1^RN}l_A@S+cb3$<+3 zD2@G1gjf# z!}(;xCu~Du8oXsiiv!|u451l1I{_FOy%Kn-Fgltm%PNiBXU{}$b2i0gcBh1~q;Di1 zSCx*@@m&4!`cx=w^*hL;xG)%>lNNvKs?r(7Zz`7AStHhl1pFtnhlAVMWTc?H z4BLa2{&hW~Ov28J`Rr&KmZqVR^FFUR@K3JxE|O~z72%L-O;-mo7q747+mF;^@`gQ+ z-!U_8KId$#_Dsd;w9J);RbFjvHgAL3lV%J3m}D;H2n3Ma&acK9i`6+ z&`=Wn3+AxZ6=!izm8~tIVaf$>GQ*#=y-l}j^NUxEMghYWB z%u1(!4{vnA)8UuH)1vn-t)hd&Eh9H8tU|Q0tYUSdr?~XCC8$$6-jKiwgDm8yvs37v zT6M49Jk&PO2MYHMdQeW*r+}W?{CPwXpQ`G|w#PTn19cxx4Rg zErq#Bo+&fzJ9F;xG_bsbAC3SJ@7ib|WdE?u7dqLe@>X{~~% zgH|i__?Mhu5bL7}T0VkCfFKK5FBFlnCPXr!=6C}eOX=6fr!4Aag9118PM?`0xy!lV z8H{`;1xFCLkYdsceG*N$ZzV1{DM(~?sa5P~f92Ikzn7cx^vuYK6e-mFD^a1a2?BXv zp_E}`dhdYqq;-<8zz?jid6QsCI-C4=biqlcPF-1L_lLmc*!#~Z_X%23K4TqXgfI{sr8oUHU16}?e6wN1rp`{-ALW{A!`g>_xpw1Lv@rWb8v>J;hPQ_tbp zYZAz(5Q}(%D?ZM=%XcDANE-XD>dnjxBvY@UcQqP2y(bajvWhk+; z-3T6Zrp^8tzQ1|%t&figbV7cE8}IXBY=us=U2^*N`XSY^ zWh0Gzs&Y!CLE&E@-}015N*BK8Om@;5Hf4m5*}VB|sP~A3^N*;5rF~5$rNUJFVtiem zq`>QtQWsJmW_efzg;Rybt;W2WpNDN-g-}a9a%Rds1jT5PS`}V}sI!Kgjp?6p=J9^2 zF5dH6rdCkNpX4H=nI>blTZRR!0?_N+yB)Gf7K6h$?Iz}V(nU{melJ(lbma0DM3 zg7DTEW~#B?c?g_8v(7g{G?4X2z-#vYF4rNP2@+rk-g}at>#K$1RKo0w+b>3oMv&F~`r)Hc?{juC zjL2&d)pfhFoxWQTc2g^${XIgf_SI9ZgLhokTMzMUF1;ek%WS72+T9MVGW;fgS{=3e zc)r{Hn;}B}cfWtW_(S7*vgb;(+lzrM zZ#kE?hX$o*@2+>nXe9?nasAl6fr;P#d?mMZaaJ;+Uq@QHPM zb`p<#R<)OipsIRxk13YNZ;}Y@yt>xXXv{A4x~$0>bumxvpm6G#a?OG7RxuG<9Xc+J zF$lyWXrJa{;PgAM!IibzkEe3!7P!&Ey>%~{OD7izq6QdxX~$FZa#GROS*N!0@g?~- zz};d7*Ugq)mKAgdave#*A$OteiTr!nxvmJY{HH|@d)VKb-%eN3>4rqfR-DL4 zY>QN1{;amqR9FASUf~tEI~g=b8$}*Bn#cJ4J*HU=Y|VyS)E9;A5>Kg`42svWd#_9P zz9~eVzmA~|>O}^$aUtZ5#euQMD#w1q6h5SpA7WRFtGH^ew~%|@*t0wz`HTgZx;bXC zghdej6$HU@q>8!WWuv+yW97RWF?QyeiAf9Wi)|1V49&u&b+r(Nj7^E&t4w(0Y8CV<)$Elj$O6or6SX_+`QizIOJ)80Ql~iww7YLjFgmaFxvN^&1 zF;wd~)J7*<(%yo;>1H3t!}(QkIh;;NRvl|GX|?-APNs8(Qh%2e(B_KQ%mbzOr)??AXJyg zvMpBeS?c{;4JMBh&Z6i~zwvbg{URwxqUWX4XDE)DBAwISWJZ{y!17e#NuGT&J|F&@ ze8d_q^xkC#oWgGL%~=%TFjv1Px91ZC>Vviu8@!*uRqN0nl}|BPpw?stT;F#>*k`V4 z#CmIC+^DC6XuT#sjjP=qA#1D)35SuS-XEFh0MKXf;zjwsGqQHTG=D`*yrVq;()-s- zZUAKK|G_pn!#N$3fBs3IyIRV?3={w5_^6-2sHjHiiz-LKkooOWQLlNh3Qb;V`0_{g zjcds}PO9zMs+CWZNs#B<-nn`CZ$rxbAGr1{ zn<|z^c8OJ{g~#Lj+{uvq#W2e~l~o+2{q;Vm35rfhvB&RRQp zTv0}_TORdF4ZRB6Tm|&cBl3Z_cl*$es(QH?*jPNYJR~CvPW)KX5P#%PTpNZh3X(m41m(Iy5P!N(be&=+c!FU^AE36LcGBs3B5x2s2COC?r@tiI&L_kyA zq1gl)@0Z?JoN+0D{6LDY{yeY1$XW*?(6zAc-8c^1QBR};hBtuCyU-9YvER)^`V{OV z_X-jvafok8ZeU!5TwovBflIG^5ajdHOL%+sg;1!d9lzCe!S8;hDLcIqPA0w;s&q7N zb2)`K6~hG=^s91ns~bh)U~!rnspVVHuYYPNgN?!a>~hd6(HI6f-#$qFZEre7Nq2$= zQg}Yrwlm%TqIvaKb6D?raw_d3+#&S8!1l2?!7w6|*P&6b;! zJD-~#_If~QY$g{9)3(P`Xf4x%V>vN?MMO|;ZK2*SPL^g>U8D`S$ZDH2Wd8H@l`LK;jj^7<3k-t?)S>}OnLond-*nr$4;cUW>=%JLPWz}vj*?)TIp9b z*)lqX!tK+wrQM;&$Ah;Lne3iSA%}}{0v4@~s0i&pfPPs8FSIIR{M9U4QxJSO=2v6c z0Hu0v1j%Z{>^~$ZSCKzqraICazg>>_?vDV$vG@<|;g_JGFMuFBU9S(dgr4lwDtH`|as+*Sw%9*f{uj9(7!5qn3x{4ciP-qj@Sp{@d& zp1(5TL~9WDZJf5OOGaicX-m+8lPEI|Bzi zl_}G%*sB+;j@D2_qx=bGV{$(prv`CJSvRN0r{RPo&Gs|n>=gcFjr69*d}Y)Yl| zM+kX3wHjx@yPE3LFq`Kc>_@t8-4Y6G+N~x>=Y<4)!)brKycjmv=@%SvKJH7z@Y*w( z$97J!{BccXM}0JsP5PK%m(dXurW#<+$vSD$N-9j_6-p7_49X9tLzxL(?CA4W=xxpJ zzp6_!E4^}LFp|d2<{DX=sUJk2v#@rjRdc7WlDJDz`Riqm5gSxwrnPyuEb?7t+k?d- zC|7z4H)=IJ=$2WGv4mkN-!{V1Dd(GY+-5c{O0GVk!c8W2lv58f@Wm$MNmZ)aN5u(Q z4f%8A>@A%coAa?fQ=8h*avX8spH}!Lr@ADWGhiR8U-X6hg6!>?fYDt*y?IFE)mVhZ z2pIr;9@OUn3y+7rWdKml<^a0(frVEP;d_@H<>Zqzd2n{%hroi~TY{*$hj~l0TEGhX zq%0Ik-qAabM$x-)pli<8B)%1;e9+st`-67XsAx>hMSD!^M1PV(KWdA=mQg`w2|5TP z|E;A!D-BAe$uw<~p}}w=B9R~Z#_iM2R(#v~k)ib?FZp`${>|>m11A#aHWERq+;v1u z5UmuGny_oHecbN#r zIQM&LHZf4Q1tm}t__Hxt9de=(INyKjdTV@t_~-JP_JR4=jr`}0>@TsBjkDd*%QWZ_ zEE$i}oymI3G>8xt{JKAdMp_h3`T=gdU8q1dmS)%DI6;Ija|*n<;TnQz=#%?hE7J3J z`>n&)X$Tg~<+s-~mtt%a0Cn+=Gktga4Ipj++&EVOUYQoY?=}AnUjXNR!xm?r+Q9F2 z;hX7#?htBD(K=}GZzsuvxb3u+dTNw&+xP+H8lBhmK^Kfe?Fb;HRp8a#EiRV|t9N^?e2p<@NJxr4lV?);v$WUfJsqjTrUdX|8QfE+eUK;27 ziMhL9eu7_2^B(nr8{_TmfYr*l{hIha)FaVCl}2SJ9u8rq2FYztLp(34Qm!i~bRgxa zIhP(!T6>28z%N45%-P@NWg8=Uj=jOrEuP1EMq<|;J8qQMD>ywtw&cV=a+C=QG7m^5 zwro(|gzx&j@JgD^50tnNr-5{c;C*O#8+_XgK(*LC%wO=4;TQXnLE%s6>2G zut>MZ@Z)jjwVa1eNJO-?PZ@0k#03?hI%no-p-Wk@eax}t-6sA(s~4XGi6pJKQ^PB3 z-qpgsNplkrACge`xjU9jLoeiFOWB72(+CpY!zVs9Azzy&Bc+8c0U13J33a!mmEpX( z{e&lLA*%~a>9r(~`k zjiJ#mHB{z0i7=4C?^A%;#YWr8@5WcB$0ywy2=W8DQ=){{$N6KvG~5AqRYmj_6RNY! zxsN1-FJJ14VX?sMP71vYUL3Y00=VMhZ7CFnTr{lXy8HX>E>Ji*Ib%@{xUK_A^gusD zJr|?8hziZoTsw+$x}N>U`{Gvg+H9VFxt@e4kZ+*#(>347Up|tj8x}ZCxmiJf*SA`Z z-wb(f5x)V)5jo}=K+V|KpLE9q?k zff8!!jxnG-9GE`4tH0l{2i4R8_S;scT~TjMbwIThAdn*Zk+9N5IXFqCRkG~IJ94Qo z|HLdDIU&K6y4bX#vhp|0^Q?7$1V_D5L3_Q~diT|b<$zVc)LRn26Kx%-iqbHehq7Hi(;&sddmm*$-`OzksPUc>zEJX@s#&N0B8u^zTH8YUO!JuP zt=bl-2y7Sko+=!u?=sv3Rvaz?%Ir@7*JD=?V6zP%(0tjmpE0R|Fc|bL%ra~l`_ zZ9weVy&n-Jn10QTjKn!4`6Zb^=@*(HX^ynITD_EWMXYJ*oD>ZszK%T$|5&47!c&y) zmxPMZb)&xbbwhky%`Krujqfrq7X}VY9)LyD!Xse6YjtTD2;jXoQNGX60$)#7EOtI+ ze&~PZ;ePe2a`u?n5*0_~TEU29fHXsXsa&4L=?n;qm?0~QH7Y_BhFV!C*}0enPL(F7=e7#5!iW99?Flb%6cIa9z}pq~qH zB9Scta6NZT0d(s?EIc$d$01BO>?1M14ieHXiHOf$O73_VfI_6Mhnmq_?=poWA-J&D`n0n6hzc{tSy7`iB?h=MxBSL0YSvJ;EFuH2tQP6q$XEN7P2gsSqp~nOa^%x!8Jo+BnEk;vD^dfI6NkK>r>QWMj7s#6v~6Y&L3M?+^WaV^q?3EP!nUU zn(biE@*fKkpz|jjys7i#zD|R2`ZvU#BJ;#)X#z4ZQ%TjnBzqV-wW?W(FCwM6{6b!?C=8DrPJUPl1BAh z@JqXO3JdEQbA9Lzl{vj=Wp(XJLcaPJp;!-*eYN;eY5-_aO6Jr` z?AyPd<)^@hI|4Z9QSNF1yvP+B-W}$Fv1icSozOrN`%mNvp_@gHl=KlkfB0-?ghS6G z+t})+0IXYj(mx0(V^mpT8C0NMro8!-g zRmZ)o(dK9G7N>oG2li{Fs#2tiWa>}(9`1*~%D^(;jYIKu_sWnPcZ=Hbv-ErY>Z8n= z`sYpK*M*gn^S#Rr<)$lOb#P|0>l487=%D^Gk(3;}$(|fLvH~*ZX&dd?_%gGcP4@9< z@{6O3#T4xCyns$X z$;0O}17i+E;X>WYyo=`|hVXq!HZ-wi?2*a6c8ts;4Vluii(#;Bk9kc!&Mc`-g~b2ood2diFje)}W_?F@d(0&B z_gxRRcsVE5zfpxb)#CfxiRJSOcS_PEm)wS}UpCsrC{hmDsM>qR8H|@s2xQ3;QW9$q zv)?6Y^OsL3=^{p6ErwgM3bS0TkztkgJsR*_whOz)h_vkbhn-DRX`x#apMOK%P>s3x z;KLV1dIzvH5US)n#Unzuh=K;%ueDbR18wS zL|sJ1v3a~k62QW>e&ZgM0l$sijNV;>7!Wj^v-&IeNG{qdhQZw#0wM8atWAH4Dh9#u z9sKaFO)qVydyG>&pS7FmYfmMa!QajAO3+H@@&Z6EjZqpr!W)lkMKKRW zoyf!bzG-X&*H_BM9}vv1S}4O|)*r&U^?v*Kc6G9o(XbVA(v;TUx?^TWD6mJa+#6%* z7uEk%6XZ=U-9Zx=i3N^&(<1J&m0J2kQNXRN)mYukYv(gg{xWTYWj7WXTC4=c-+;J$ zba~Xny>t)SRnnv_#6>TnGOwXYL!MynW+iAIDK~J zq#e7h!Ab@vO@@`+*v&N6pr0t*yRDN#dw|tU>D}QH z6bNVz0Gf*eQ4~8iq`1W1m`11a2MBcBdP?1KEjqEK^)3teq@PmU>+3~5ZZ+M0Etg8p zl09m&BNvMBlzr_YS}iXD?t=C!prJ~6fu;?C!DrAz^gZ5NSJ;2n?&tT7k4TUB8;@@U zVESKousUpO+haFzja4z=cBnDuL%}WC(8^x;rs1s9*&@+@?g?_pIKc;wIq{A%O{kS` z%d(ko16bV{oCl43SLDN=dAf$dQwH$CX&O}Ou(!VV7oQ4xE+6Hyh*tshL4Zcvd)xH8 zi)V9A8}O351g!mcu6lYQ#>(&Bv-j4rMbhzE@F4xJ2ONj!9B5@@mP0A}42fBV`1ZFE z9`k#>DN0Np{akD$_{c4XX9#$^6BW@o0s>E;fc2@Z55)8Le~4!TU=aV(-2aYNOsqQP zVwItOEOWW9-b&rCwoAqpxrl*&VVJKG45Kx+(tt!`ST>teA|Cp+^FB8q?;&`TI?TDtpbq33s}DRpubBfD z|5;6dfk|MqF)RV5rp4h%*vtVb}pXx6Y74Z*jsp5h+##R7wTmRUEHDJhWM&!uyD>V;~2v>Y} zHe`Plu~dKbZCdAyPMj*np&=HsQs25amE=YfHjRzLZ^OdRCSkyuz}<(RW?#jr4o&&? zM`pU4VI7X^z}*sA#w1$<_*@_81m_3s<;J@KllOa9Qxj-%v+JL6Yx56OCPD%PI#Ek8 zU^;WhFR*jJH-BS?DG zr7-w0po|t?ZDfC*f|kHJ61dvc`Ksyb7rgn4$V#p!X9J^s@)}?Fdrog{ z#&)j1L(WTC44}~jrn%MCq4&D5{()M0KpCERuM7XIzXE9Q-aaQ6Gn?HttByLQO`RH! zkC}2MX+hof<8eIsSEruz4uf6msy~3(lQPgk1tY6C!cm;MGs{pWww=mRb2 z$q4onf{CuqM+03^aElV4-KOJu?<+op1Weohi&N9Bb#g;SlDpw+Yy{J50nO(7&=j~( ztTJ@z+Z+8aMvin3eo+odZx4s`y?h5bNeJ&|JXjh?>r9^pk52k(Jy_m#na(Tx$}bH) zx^kj`x{Y4Y#^`@AgurW)>*<2??!)XLBcb$f1-V*ds3vyT7kK6s9Vc}gE>RIV^|A=@ zRo_~@MO8=$>K_n8P<8PmRyN?l`%mTBI*$&0%{9+u-JrLEg37m=g)^?J4bE%!$C@4x z(#Ltu|t@UVfZBZX8pcCF!Ez$U$NS6Ni`L&YNV?O7I1oy`& zrnFu#y{H`m1qNLBKvi4Q#v|yXbovL&;=gqjze!3DWvE}guT&yQWga2b;&0R=eyZ_Q zEoqUa2<7@RmXeM^5Yy3ZawE!mHSoya;Fs6Hc1oV_s*4JJ)AnxMDSH_0+<2qEK}XLFdSK9aQ8ZKQSfT=P6iV2G4K!+H zYfzG%41d&0!#Q^Ud)=N+DvWeS5>KtXluL7LcPgzTrmYZNG0VJl2=|?!bGNK`pmLjBGxcOD z?=ha1y06J~Mk>v>k4u35FSmU9k3b8y!M&6J!wRm;?;MT3gv3z8hT60=1JaQdk1t1} zBxyH z_kbcU$W(g2H)Q>^!(i}OAhB{DtdAn~x=zV9<1QBwoqG#f#hTVrg!+boqcoW`2^}3w zct9kv^DiS>!`}Y^bXQ#N!Bp6mzh2jS#Hjg8;1%ID#?vpF=w4eDKH;ZA&X=#K-##te zzQ!rey-&QafBR@EI0B_hz|!E%BIsYWnfBa!LMB72hRO}7WmfMU%aYXAYW{)u{+EJk zjm==J7rjW^WIpQ7&vyC0li-H_b|&y)y1xXK#|+W|7ukan(Dk#BdxFAyy&lH1=eXGu z?GK~6_lt*zhc;~>=5N6V+wkAztl}8JXCUpdq9^4Wv4d&+-OVDhN#q~u987vy2kD#J zIRq)mBT^A z!6AjYt~-NnJ;ZgNsvg9vfCi(9GBqlA+$;rOw2yz1|o;r4}Y$7>h!4Ll92lRNOs~5EkuzN%l5m#C6wW`pcS)0dAq? zK3GT8I!nIsr%Y572egoo->w#i)rX!ua{Td88Gk#Tr+Z_MT#~*C)q-fDyaP8yaOW;l zq(X-zHOjyufzj$m;!`XMDPT1`o`MMdrj1>{{>kt@cg>vZ>3ZOmvvx|e-Iq&zoFU4b zP@hUxXI2$odxi80;*-JRvZGyBFowPeC-hmgoFY2J(0F1##iggWq@z?xEyz)-AkB(3 zI*lY$La;$%vPIm#jg=0U)rJ|vFl{_6vSVnNKIwD$L>Qhtt+($d(`Q^nt80rll_^N_ zNg2yZHT&ej`o0QIz0N7sB|6!$RK7@(dGU-bn{yv*`z%v#C4Il3wVg@_vv@%h2I}#I zM_s>+F(9&O18)1oxR@&{=FOXXVP!JXLwIFfUc)9lWl>U@+if+CqQ~#1~f!kD$ z{@&?r+kJU8Y|TfMQp5`H&(sh_f>6fZ8;=Y+{KyD3PYREXi8CTAv+W)B>D2J&^?VyN ztqNapvp;xzCC39)0!L5Ye+Z0U`s~4R6T&Y!;9y;^%LZDj@Zt=WBH^FRm>o%-Qu0-H zdFqSI>2e5-c{l?Nj4GG6`Q7bdF@vNYeIP%*P7{gn+Fd~GVgJEy$9tw|FXk6c7a=A7 zD67EpZ^JiHWgMPdc(z3Vj3-ATgtI?pBH^L>u}mj8f7)^Xpy~!&^U5Y~;(Yh}Fr3kd zxQ6>SVGY?9(@!y1)mSUZ?G4876tJEc6=W5&)-Ze?U&4TCS(u+#|I!V$C&b;N3EKB@ zDqQQP>RwXgY#1u18(*^Q=x^eV$_VVQ<=|buD^@tO6}~5# zX`aTZz=7Xw1axoFlD{QGVign42(7@5&rFLJ7k_=s2B~KBwtPorzbK+pNjG=k_R@-` z#u0>#{V|{}P#v8vvnh=%xYzZ7x{KMyT`0=%cOONUVF_3E}?wvoT_DqKOpo!CI6;;jg zf@9?o2S;RPEo@> zVaHZ@)QMgjg{1N@*6`6g^i8*i&h{JP+|&*xi(3NoO1^Q?PgV3|HFxRX)FKy?kV;I^ zS(MS93C$iwk3l-(cMPtBvenEGiV_cD#t$_cfQm=WB-u1FOk*!SAcJAl@z6p)kk$R} z`cYU2ABN7#6fDE}2q-sr9)1Z1%W}UOWSO==)My!}zZ)74gL~R0M4Y)}VsP98=D|?a;p|Z=WH8d`&Z3~yf@`7)xyN`DouLR3jEJCr zmpQtXWKa22c5J11gtjk_;D&lnjM z{QHczC&JSAT`}ZOchE2d;hZ~~g4?wObrbG`d9h=!EFlwTId`*)0zlW@cDH69uzF(l z20XvKb@T%c=!!eQ2bIn74mx=Os&F7XX{TRiKA9*&yK z{lGHc=?A{!`Y*mC2K2u@eDF^-K|l3~peg5TnH9%-f1+JuCRyc+htcwSbf6kLS~Ypo zVbrl?FWw2lfN>$*aULIc4!&y7ZcS@ttC6*h-QZkzWxpF<5Ty=3ZoGsn-Q;xq{VM@D zZ2t%Lb-o)uBd4azK1(*&?lJkrG78E$`*DC_>)A56IgO|wDLZ;01m4cOGV3m#=Uoow zBk0K>QS4oVXx_TE1xV(e0qQourFNqd`1s-fFy&G2Aoe%Im;0FOBG45kaf~y9WEG^x z1U%Ufs?Ml_U%KV?yRpAu5h-EYo5gSRFGYK;F-0h*&zZJ=o#FffBw#ijelf%c+F?EO zjv!o8J(B-HJi}1W&4rh>jlnns`F1bdR&d3rF;$t+-hsw}f~KYgyW5XO$iI2|kz=4? zHjVs9N7wMcA;SYyNREJOG2Aq!w0xvCRCas&rWX*EWD7;}jQjaADC2ssu`-49m0XlT zf2NR7yk$japgirCi}w-#&Ed+St)A*MluRW{=4VNb97kWG|CzOMD)S|k(PJ4X<{& z-2gt6aEPDri4Sg*n8Tly-_{>JzZ3}fAIQzZvxz{pJ%_+1qt~ioh>aJSU(b{zn`!r5 z6=Nn+_aYmAv+nJi(!>2;Mj%w$tNsZhi{64t3Z!MpIEa;0kGHFFaz=A)!I|Y=jI;3@ zCYKP+)C=5f^1 zouClc5DB@@335!t=*D2z(9MA|2(v`>sz%g6ZMw(*C{)SZn%evaJb_F3AXOI@OTu^q zrEFA=<(6tR&`-9!%#dDV_VNVGEdxVaU4iQ&fdJ>#e@YfN;J}-dyeG!8^B3yiuX8D7 zeBx=&S7<{yr5~`SgM`Ch|LT6(7Ii?!X&&(74#J*7JiRk{=_behX=*jkP%emZ8?+LV z4CSo|a|w^6F3l$^mV_*zxq8(C-tb%B(*OP8s>AV3OYv!;1vsHT0j3r~oOk~ol!h7r zh{<%v8egc)mypJE3@PihB$Ny-^(th}+xP0&?Xmbnv4_sCQ8#K5r^T#>pwT-{Vbin8@MM2Nt zGqd&zv>Vd>eI#(k_NBrn4zpM~%ta$Bq{X(PpWpsY_@HTKocBidK2%yNSjWz5W2t7- z21Hhvuufnl*#|t11}Y@9#mIUpI==tr=(*(yN3^2s)gb0rVc*~EqQ0|O6t9sJR?|qAX@gw0tF_D2rnO^sYp38S zhtiD!Q`w1NP&+=bPuCfCJ91z@Yo2|=ubT093Q?o@*mOKeB`~r4h}J@zb$KrK<(c$G z^Dc1*w4=fB6=$L5^@djV2Q5%@E`kMI+}&rqb^TLw|BG`==>s%be(+11?o3C3k;nQ5 z>{8#^!6f?%2RSB!TE%aY=>4U&c2d_TF&>vHhO(P{nO%+<7f!G1)gsf*Ock{(68f1f zzWqE7c#mLm7B4Kv_|o2;!X$|l+>F!Fp_Tnzga*m}B|Jyxk;$Xt*x5|&3?OoEcgkxtz2tJC>s_NZ4WI+?^FfZ_?3H!9`tA^0M{znUpCC?%pK|}>|1)<$_ z@uw}_kgPIVJ5)0c$*eg_C4GTZ?AQavj|$%B9M(Y%*4NS!6@b|6OsL~;|NH-%cgN2_ z@JN5r9Po!>RC4M-)Eo$!^IBrmsi(ueD874^bYD1M?&#S-tiryCZzbB(SM8Y5072Td z+46lyr+S}JY2 z5Z$whDMfg8>?U%2ymR)ayN7B}(v(ONd0jj8bineO%JE6%)L_8aVy64Fe zziC6#k-cN{_z?5(9;EOkvyHU=n#E{OsgnFUEmA_h_FO23M{rK)yKwYdGaJ3ETCX-X zv<8dufyXvQE^T;Ex*_b9K3n{x3l7HfBm^iG1iWig^no)%j+ZWcuk<^^hIgBJ&`32< zon))$u%RitI9*!f6fl7DSS~Fj8PmDq`DX3iykmTTh1$aQ!q+F#-d@{aIGxeZ3LVU< zJ+ywl5Ur1O1j2h7#S=NefWx$S2(pinuUq?Z~JDqGj!B$ zt@B#o(31|m6K~70<8_3s@S_e)9Kr(BZIyzx9ZIaJj7}vwX=v?9 z6-{oaFmW(&6M6d&?Dx5D*F5{aiXK=jN`75)(~|llmn)QZVf@RSJuz(22xnU1?WIyY zPj*eM%!U6UqY@5nCKXwj$s^(pk=5dP-ZDO4g2pC!IC$z&ILXWv1N5p^*9V<~t*`u8 zAN6pyoiP3VkJHy@~M;eQHFJPc`7r{PR`R1VK?U7AQ{|Eo5I0!ign zQ+t$h%kQH%a}>S-o3RoNnb;^`-=QuCGIt>hkfue+*NDp9t*uL(`))d@Ls+6z(?*Z< z6PZ9gw)?a?O9%+(c}Z%S9~B^@J(QP`bWAKYcQG+Fu~~neyEyyx>Q5*Ug3UU*ts54w zfp>vS)t&8GSAyUG!jzyD4kwoXBZue=$xJS_#G{|D_Qm?egbr@V4&=L)A;_TB*wCd} z;%Ve5@xm3$@>McVC|U8;d?HpjBGHRzgjrd*6IL4;C`!)KGEJ-pyN*fr54C>~J@TT) zMazhtJ<0K$_hyBVZ|BY~j@-IuQsRbm_@#^N=)7;=H?$iCP|FrE*Bk?m-;* z%$w@&-zOwz$4%#;Ir$(E`y{XbN7XxUSJp*ayRj>%I2C8dM#Z*m+qP}nHY!QQ zww+XL+jjQNd%pAC+wL#etL?Vh9BcOe^gft_zqefSipRj7cO z_^#e}95$)s+Q+Uj4pNpGl2*luF&++NNz4KRO)(Pec84BdnNM-AWklr}QZfO;BJWQ) zoHDf)!j|`cHMHb$0q57xS&#j`CCaC~;jeFzXQ>gWElxyP=iq}He+rL*1>jC*k4wvn zkOq$&zl216f704TN7p;dII_WpkGf0J+nP-@_P%;bjVEcH!qzbE=*kQGDXy`@oqiKB zyBDwYL8=!An>)5w%^gK*om!_kwgjf{nSkGzaa5gP(AQV8P&q}V9YrgAby|`pvGXZG z3kLfD%-l6a|5Ad=B<})#=I7l5udiHweY8ZB{imT?-E3#R$PvYjtbEMt$|UYc+RFI# zebxo~Wc{y;!%k!K2Yl8Ppy<|GhPn^{hMia9&1Y#s`FGl(KVe!SI$F}nWO@K&N9pw3 zWlt=b$k8`t9o4&7SB|L8vEwxp>jwzbx1>u-a~4H&Y6a50w!!Tet&R#5XcK6`LHNT7 z^k~hC`nHyArv0q{Uxo$Yt4c6nJgtB(2TV&J3LRqrQYD(UUo{`sc|m(YAYO{ygquJd z7N6}&JXYDku9xLxV)tK&h;J<3bmv!(7>S3K@ZEBz?oQOBU(fc;s$b>{Fv}C?+o1n; zJZde0uNqRLhdRBO9_}N7Za(DK!3&(eTd*lp?Vcltbl^Ob(ld+sDQ+*UG{_QmCo}bQ z$ta_<#=POXQ3i#QOsB?V(_`wTa6i*Pi`hffwTVRt^{x5NgD8Pml-?GosdH?v3K%rUw+S&o2_-WQe?F1^RC%OAm1BCi6v08bM5{P zPM?n;kVgHBUf(=0yqM4?Vma|CteADsyT~ocWndx*<}5~G5%dHD5k&b@orCQ-z^HRhOj|l~k=tKn3B$!00b+vl8l+~-HX3hmxPSpPO z%GRkMKV=^O%Pv=l)H8KN&(W;p0vMKWkCGeW4!h@ziru8~s3dpEhW2Ol5wb@sO_ZJl z%p52q+1)2Wl6zehjQn9F8>@ca5^ieUZC7#L5HnH#?2IFpfA5Vmzjep5>PGdYg`P2G zuwO3)OA44mC8lV7=Y6uG&OiT;q?1=egmlC21J%<<7=f4p*x=EY{x5?=Ph%TWh_OT{ z62yen8s93Vz$oX59lCo7sGJT)a&=`!$_6}5xN^qM2Cor~Ed3`qi`FAB0OLYhxg%g| ztypeX;-|sQu$)$Fpa8?})4K&T9G-pjmng;aPju#5h(JspLa~9i;N_=UO13e`vbjkS zjl+P($C)cM){OQ+`e5r!D|UVSojI9<=mrsFh|P)KJR}ejl<_wXKuR=F5tA?#*I{BW znUosMtX8kd8iOlaO|9;w`0w7{e^!~QbK{>vYL%Jeg!rRB=`vdU?KX@cXE(~&q))C!9%Wp4;d9MM7OJl>(GgYXx%czI=J%)cH2 zdvXTa^1xH+|IGXer5H}L_NK;%4eg`L8}L9|xH}WCBj{|ds7V5Y9+Yep76GJXiid)r zSRaB$Xy8ZL`!lA^U!5KUSL0xwS=%(k+{C1skkV1*?RCgk1m`8L(bOa!B(0)wXN;gX zT{bBn1Rg%(lh0&A&Y^CtC|}kj1wlSF!J|gIA9oeFP?ys4f4tY)GL&ZgWJ|b!7Gx@= z(Ia^(8DRK6nkFa9*=l~5i${jaiRz@1xM>sfW`7LZuy5QAz{XYiupzp}!AghsOil@* zN*dvUg+q@y?b{(FN7~XMV##?QOf6FJzRn6eRUrSRZnC0Tz~iW2+$=Pz(AWi+my=|M zSo@wOfWD+#_3uFvyy98s>j*lUDsYTLb+W=^%#X8oVdm3Mvfoa!{R|Tg+p%94v7s{)z~^(uZzN8Omd+JAgZuKxd zybP`gsSBmRIL)5F;e=u5-mhgig z!9LhaH^1_D zHyK7}+3)O--(M6)sHHeldf*+s>ZC-O(4y%=Dx71FE&n8mvivq>rroOCS0(>Ncv+*4 zg{1H!P?}}XrNut!srCYtL{+M?Py^9quscf`WcZ;2;A-UB(wsg>%%8C*1rfoRu`Us7 zX|cxHUC8O05=XAlujCOwipCa)r$2L%Pu8Am97y0;@H^MV_(sw z!Qa8^8ZmRrO}aaCIcO5Wh{h^Kd_bwQGm<8zvk5dZX0#^CWp;5>vTf2;|2jr^dNvt3 zp*}6AEWKEHCKO@06s^|9@I*mBs75Y|HoD_ycnal*)=9~`TfVVXK$K@b(HD6TQBYPn z86tBgqWGbsjtP+wl~wm_Z}MjrPL^k4e=M~wmU&~6SL%ZueUjmq@>s^+JK_Cw`*f%I zG1Lel8^&4&x5`{;Vq-}WGqYgFaM+o=^Z8Ie6}F&E_5@SAp4$P6M4txQGs};2KBeVa z^ocDjo*m41Se6PSY0iGQqRMrs9#c6kY=wENIcm~m9`t7RXp_#=8U({OipJ~>E{b?u zm4rj%=@#~17L2r|k~dPD>7FHH=fVqK+I{JzFCoI8EPN>}kF>-j!VF!v+6VlK=yG+U zL(Z_7^^x_Y7=YoaDpHpY(jxGLBHt+%yLSEwhkRxcDJIIh&iROOj%xG09({;^<1eD{ zMM<^{spf)}bDK~pO(Fep%e6?MUL=}nc1m~0J$8^(#yIv z1SX{^xBJIn^+qR34@3DywUBDwurPK4J7P|k+ISSr0GCUKx<&ZUH>;0vpNQ<Q^WtpeW^DDs>S}~ zj}+_^N^ToqMv(UZ4!oKfG&*6jkFS$uP=%GaM|ltnHpkei+N)Z9_zK}iCEv5ilLP{_ z4g?2;j8d3ERBrve!Zxix!h(i@e$3x8{Pqiw?F(r21_ZjhdLq~c`~M+$o9OhTWh`oA zX!Emyv)JzEen=kWl zfMMv#7IrWuyLIJ3BP4L0l+lt_2xLmY^7{Nto7D?d)Lq|EUUPububoiqVz=jbaAA1T z@g@+5?iPfh7%iKZI5Rnrd1I!ivA@H04=nOe!Hkz^^#|&?EJT*i_6t9uN}gfZQiyI`&-}lten1Hkyy4M-jnp|{HvP8E%Hj4m(1-5 z7=eHl>=i}|KR=&WLjvp5)$QJ>Y` zC;I1#LEw4F7rLCvM@!e|C+^qv)$bQS)c-q^et*xTKEEk+ehrYF%QCKuEG)sX;07l&f-Qj1dq;7#@YA2VZ= z7IUS=l^RX>aVjrU6ZAer-+|u?~q~t#(@8r0C^u_EKF-=10`ZF z(r6wi=s<5+uaQ5|I2zK2+!{zSxqIUmUr(!t-^bHsQnEq;utV(g&(}5HB_7M0AL`5! zhP)0E=gwo;eM?y{JOxPt$$qFKWz|EG?n5Ny+Rs9y03*qw4ExMOE7AhY6*Q&-K-mo< zC`_^ug}7?++VIxePo@kZIR&Z*pM${fG>IxnAwTSc9V%XBLvwSl3!4A-^S?<04S#?M6+W-Og)B!&ClC z3^U^4Yk%3-&I}@~aE)Q)3<=|t9sU~D;@PMdN&ozKeYNra&D3y^YqHifHEhM5yMDlA zw}7Pe6G+xiT$$1xu81=Z-C!q-KVU@BRANAZ0WN*y2t-sGHyb6yfmmcLr1gd( z9U04BJ};o9(3~ZJmmO8uX-EvTHcc~O9VVfcgo-xebm7E~eL6*t>nFbqQ&*h~yQvIBRf4rfFk zV5IdV_`$s$MXYDtduy_OAcEc!dACyA8!w8i`|!bR<*^?Ije6#W*83jMY9iT#T z*m1P4F9)sDu!PF&0HF;Ztt`n|#c%k4m2E5}B7~U=07oD-cJl-_)rGrJU#+ZSIxF%& zddqO|2AF4>lrZlgZuu~L^1U{E1qkzuKwYxpIe%K(xCw-jHqqcP#WeX$&kykY-Bd9F z@td47M+tb@aIj@B5R!xacRZDQCE-r4w!5Pk{`J+|qlKvRYO1NRQBNyhv3N-*=sbNo zdjB}e8wX#$$LxySjnPLeGViya&c_kD>%XU-IzV3SgCo8|BCZsiG8+d) z#l(?CKv8Xe^nIee8}@=rcSd0H_vCxsgQ5 zUm#U9H)$k^Wv)#a8n3U;P%7F^AtL@whG61RHO>oC+7-gksfw(63&+i(y&3tBkd7AS z%0gYXvKqq6SRSg^>@gy5DMLW#HcVvsK=?4~Uct@)q{|v83!4jRxfDyH!R@sl)@AsGa1YrC3W{0_QF|#qlu+>!O>O|d!uNrFq zn_?OYpe)u8mIwLn1&<#t@~A}q-AD08A_#i#5r9ZWlv4O!GCtST4)1Q!EwTtFwr5YUUR^OE*{-rBb( z>EC7a&VuL1-Onh{8x6w3bCH z@{7mLt2*D>p7%(Hb{Rj}BN}P76+XSp)pQ5qZbneMqU6T4&>YZ9nyx6-hk41${bAhSKg<&TKP=~|JJ8rlSjjBh`EF^mD6{=$PoJP9Jr6?S7gxSBYNHVcO8`*_*0SdwubzN)xLPw8hi!07y{< z;3Oq(%V1%e7kEv>!&lnO)Tc=LlPf{tc+9BH8WTGPAmuj3EKPP}%_lm_c5yqK^i7U%E9P%^6{C1Pr$Kge|oh^Wu0{WHW%ElDCSC&rw z?)^pnGpMRXqVVtCAmi#33-!Y21Op7Eu8TeE{7@*omO?JBQe*8m!8}njZ*8}tGG|fXg>J8_VP6dMV+DEDx0`MfeSX zPpE(0(01985RIgBG>qXC|96f>{{ic9Vr{pQ6d!ZNh%NQzWVQ01~oWtWG{Z;FC3`wzjVNG3K^jo?a)1mz(RYFJ>oK z2k-0d%iedU?Y`ys8b}m6{(O8*KW%%E`@~3Y@8bRW^>Hw;zHep!xchc?cO-2o+V%}3 z=hcRphl8KNl60(i$6L`EsrNDf*a7%q!-a`P$gt}LSjG_xEEi$_5X3B4P$$5N?0BdS zn1*68>A6e9RXc+ESuAXkjxZx!s~)R=Yz^CtI(0tojNT7jU2!ZYf^6Q-jd8?7fFXDr z0F9J_$FzS@K3pLT1ZTisgUEQ>5nt7B$G920DT&Hh&{p71uPf##?GSJOf{iC#$-GhN zYMeXo$MQ!k;wqtQl!Pe_<|ESwaE?9;CswMJ*cEn(A{yT zgHHeFI6W8AXzMwZN}hzFi(o>5qi?0_`D3NV6`o?i0EXOL&HO6-o+~t$0Hs1CpD#8L!W(8P#YNQq z#(FrcY!#|rT-eQZNFP&x(XgukMkiuQFA_1gD8lk+1Mk8aLm zs_Egt1fN`T5$m2$j>A!QW?&(9xqpfz1~!Ls_#Xqsh2NO%Uj}|Km|^kJFddXO9yFp~ zq}K(F2vi)bm_hJUU~yv+7x!6fo+S%gAQL@a zVa4G)3mUUJ^SC-|B+M9vo$&*uiw;OtBXLu~dh_64E!mmHC`4mharL=^k|?`MLfGhT z_eb+{+5%uIaU8WS|H2tlyV*js1NIH+E>zDi_L8gmh{GyX&tJ~YF7_TifKZ(+gX3HX zpB{Y)FfQLa1}mu2D6Xwaw^X#=PE*=Inpf^o5}w&d(dTXJ8SuSE-6;73_Km1Sp2GQ$ zF&N?2zUge$bFl7iwKt0zvFP|{OA`|sIh(&ekwIyS3S_R8{YbbeUOhwEXVN7_s_Z zhE*Mbm##UH_h+nd`*(qzcV(%HW4UPt$#2?yahbV*%$dvGHSQ?o9K+1v-8qb+AW;`C zG9jg7MD5{qJf1n^isG2y^8jmSV&%4TR+*n+$IH`!?_ptL8L!*dc)yQ(SnobYdNfq6 za+r6{@j&%%AznD4d)3&1UDLPMF0y_tsSsu7*&n*+JI|HJ-OKDxU3V4N#L;jx=X_e# zwD&NMUpq!sk5X?#wSqcx0%b*ZzN{`7c9bwLZ>rWv%#$t$!M)f71F;*<=((*O{+vn& zqT?TY0|6t)$a&z|6ithB{214PlsVLLFk4>TNTOKG<1h%XhP!DvAD38Zo8ht$J%D3o zzeUd%R$mh@_p*{{U8*7`yFNYlDAGge1ipq-XR*9^wXa%X)m=?QDIz3I+6ivtwFa+u zzqlD!&3ckI*V<<{UYx;No{snDRLC0N#4N~8r=jU)2CLJ=g1Y)mUPefLY%S+L=?>c19XDVWgarXqYbfR9=)_Mig zW8WGVp1^6#wx?_DyIV$&-F=TDw)W@_j3NN>tf=>pqh>*cg$?+wdgR51>DSy1I&$Vq z>W~SU6sQT{sx4}3ovM8N!2lEK;PKldrl?oopB)@OLtymlsA0HcuA-TZ=C;3^BbHa6 zFa+QYYJ*|)#Ns&ee*&xu3_{WFv7AQPgKlq7$;A;I@fF?Aw>-p^zJ`jVKbY^y&K8hx z;n&$>^j-5!Ry*MZ0&Lp+?6Hp~m(kkyw{At8;`B}@- zSdr>IC@x4nqsM6R)znMCNE07JyS}+#8Bw@+9)(=T93>sGnktkOS+mO518y#voCwPj z-(hNRbh?QcIbL=K5E44QB7Xiy$oi=vM06*e)~@>4DafD8^nnDBFCvS-9l8a>MZ3ky z1kDgMdnjN@Dl|C{_{yy3dCu`;%17d3h$jRZ5xCkKK8I)=eLAI=gJFjd!ll*goC-ZzXf=zeLlP=rWe0&>8I_#XzTxhv-~1}+wkNn zTM)S$ysXoVglZh;H{N4ReYQ>0;BCz1?Q$lEMZs1Jb(pRBw2i&UP*)7mCkv(Xtnt6N z>tSv+9LIa8*;Z~zxs$opo_$}k*yQNCEiIh<*r9Zgry+t zpEhVC9&b2f=Fk=?#>@!D=~ z_V=w0bm~?lsNvIwsZT}MpFmN%p}sPl&jEKof*_zwD~V6H)<#Q-i`Ca|Dz{ThaIxMj?1_WQlUXB0a!i2O&mA^Um!H z`?p1kTf+>qn>~KzK=+0?x5Jk+_`#BtIxCi~J(yv#uF!_^X7}Zj^v8Xli5uf&9UQ06 zh&|NiOpgI<8RcCju=^~94=GWr`g<~j=9tOJ{6PowCK82snA_02p|*E6FMCF|qpqC5 zMy>%l3F0ckIr1z4L%FX-C?FagC-CbUghL5o%~X?Pg%4CdX(7-5{~&tI`W(HI=FH;$ zk2kd)brMu!5-65UzTKT)w+|mU&KM;aF;;GS>Y;@(b1Po%ixCu{sfV6>$W4cZ3M{+Y z;R%(%Dw2;O?rB?{IDbS#Sw-u+Mc9CxFH^68G>I)n%V-j_%+JYywrd5`2Iy44;Y#nF ze+Ylp*Xe$Qyi09wp4RZTP5!?sF)~CRHd*Tro$M9NuVSH-c8E@}C$R0d6nxNggU>#x-dLtimEw^$Tv`f-*AHF_*0^KpPTt)?7rwgh3l7w zvM8N_(+vS#`}=k0;Va)dRpwvBZOT%(m~cS@cK!(HNJX)J)ftP}E@p2RL9f-Vz3F$zV3{$#5oIIkf+r9CX72SGQ&4NS* z1~ilSt9G&%sL7JDzOy{6DgQ{L3p~D1rA|>W0rE|50v9*!KKxkktv|K6=j837MdM|) zVr25`U|Co}2gvwe;b29uP8L0>ZC-pI_l(|#c2b!7TllagYG-Qblq6o^bCt2r_?jHW z&s93io}VslJ5=C?b(`KdeP^z&)<5mmt0C;WJnMLFu?SD5;fM-gKI~n++vhDg=*)Xo zplZYhUt^x4;%ejUe6hB)eOq}s-b>!k%UD+|g7|vpv4=vx(vj;Y)lxHH-p7-nIpi`Y-+-iSmbhZLhCy&ujr#FWKIKzdV-a9yRA% znpOWja>&d)+W*keD>!`|LJO(n{k3RaP-tg!C|gQfagtHKe%a9fqH^&=l7DX*EgRCsc zt<1B5Ca=2-eoe`s$`ZIXt{yJw__3CVMoH|pcYM55r+24>3amDPspS0b)`-6aUOaOE ztZ%lVa{mYK#Ce}lTJwVmtg+7&C)XIKQ|1pUvuGMmxvp|u=Kb4o3AhGp)zH@&pJ$$u zW}L%OZ+3a|Q!^vKo{yTn}I?dnhh zVZzwQFH#k$_Vx6g64-+jq*IaY@(zYQB}>Js12ST0nSmPK25@pi+0khkL(won9e()C z+e-L>Qg?z6!F)Y@yeV}oIuh@JpW76)j9eF)EAw|274|z{VKfKsx)FU)e?8`Vnu4;8 zGYnhVXmqB+WOugV=IY?~_u}U&oBjE-0ROOgoTl~#k?LS!9tT5BmW~emz8>iu{Nx3Q#R^l;+ ze9{|gnAj=*){M90{WA9{kVE+@Ltr}NO}uRx{<{A9GH?aD*?lL6z6;a-`UXAz`jf{9 zw%y6+>*tiIuOdK{j*ucEyB;JIXr^Jn;eN}Qqt%8`s#iMMd!|(I!(F4uE;I{LReqe% zMW)Cy^b$u@Q^2qwpt;yGZ;_LlCW9F(RF|RP$@@sEr~S`AW+m^Tor2gey=C}6|A61x zSJgHbH@D?~QL5puW&J=XRBy_!@2zJJ%S~QlrO#>+f}eNZy-b{gmBSFKpb#2M$cYv` z+(RNzarMpBpk|SyyhwR}Ip4N}1*?)3nt*Qh3sW_~IM1oq>lp5s8arp7qVpd*b{eWu z@SOti{QBVflks(MV#WWwanP3_+6_i2k}x?e>C&y{-!3V3?588$Zpe zFYGStf*+1O-9Q@82*wWv0I%`@%xBzkMW{2FB6xD489e`|sN7aduJ7)agm4SC;)HP6 zpcDJ`FVT%;>ZHh`1aRJnSs@f=4=@q$6J-~61+}(6li|}n!~~q(WZ1GO0PA4Fsu^K{ zBVtwny34g45hh*s%~gvI9>d>!2B_0ZdGhiL9~GtSXbg6tl2NQo2vZ0q2=*oQ z-0-X-h=|&p!Kj^R)Iuctku)J#@7`EXmJ_341Q${~Ai z_mQlKnFbM&5>!>R=Qf8tUzRVmT)&kjtjq{04;lwK{hy#KKOGp3?O%WQ8h&h_KOw$r zyz70xOg2Xr|5f4q`pD}8GCcVHt82aXtk$Bphu(qfsKz#&4MA`Wpi}YxHD&4)1>6MN z*%yO%H_`_}e8MPnmAHJTunBwLcE-x&XhHPv4D*?oK9Jb&shNPVU~%-wSSxA4?y_jD zAJBH80#diV=amv|_JWrYBeq8YMkEe%f8d*6Z)8efJW?oy0)m_?@if9Q-7kbU-(<_F zgmiGXbKR}Qj{}$&p8JznS6q3*zTeiZr6K?CkZ!i>uSV<$c`xV>*vT`j6wuq>_reu& z@cc{#;5QSsl3A7F>%+g$Hhko6q^s7^vhGwj3zk+WTJK>HTitPDVRM@TetbdD;S7wL z{ofur@((=8Qf0~@_U!1A;Q&??L^-z1t~dgbhAhJAt9foKai%ADdIAa!0cIal=*II19qrA$xwB|R5*8myHC4vK zWF$5!piBb{-;gVHm?%jG^HvXjRxxbgmC4Qwv_H~|YMno_&>j>cU0{WTuSu+rtb;h| zm}hj^y*yVoeNAX)Sq4st@RW`Jbr1oANroi$MpX54#0X6!ZlJoDF}AEnuC`gE@qKTd zFfnx&MNPa!5m{NXte^1xuRb9gUCNx)d-2nS?^qV`mwPw2UOG+cvPxll@?{(WDu)3L z$fbx!3(v_xX!uE@Mf!V}C8nU5fXz5kE${^=$fC|8;A#*Z z<86gt`Oitfn4+@u!S<*S^G} zJKC%XxLV@WMTfrp-0XmaUciZO9%$t|J~_x0i?3FwNfvf?Rm=z)n`=K{9+Ot!U4(uBE?ArLa{(byk zAovM5{I$ITWPSoRDoUqPQ8uM7JN*++q?mARYEJGLL_|E`XN#JxDk)pk)%=1#XF-oe zVK%PHT z3-{*e;^9vKTCYl+AtUo+~=i{y|$O777kSc?o{`$UB7v0rxH zt1Qk-;vv*65>*g$*-a{$Cyu9Tt74umS}lLSBM!ys0>?-#$u)dk4?c(E4B-CM10UZs z-nVykoxbdt<%SngLJd9|mK%^RY$xKMz&%cz7`n8_5*+V)>|QnD#pU%;4unkVoQQ(< zGKa7cc7(COKy#ErP4(aJ-X0!1vhXahgFyKk?BTw5A0W(v3xPh0=3dvesSodhm@l(i zlBWLd_&vm&d?B&1y}cg#8ceULl{?V3L@5o9P>jatu(a4F6XRva%IK>}9I$xqLg3j& z!i7Lqt;NzaGKjuT8Y)8ZI#<6p-aLRpwa@cin%@&2$2 z+Fv<*Xj(Iu_(ziFKJC{L?&!!Yo{qUzFDB$!#jci!H@yq)>_x_tUtTNhPV z)7q2Lxo<*(nu=pD4)Vqvzk_TO336j%yFHU^PSEd1erW~ zyZ%0|B_fl9@>|Qn)kXJKTBxQ)u*vKLWxd5I$-Rs|Y;V`7`fSTpRRl;4Ja7VsBY5|j zG{=pIDkW(C#8^!31iQNDl#H~j%q*)YdGKVf9?m3LfcEYP8-Y!TkRU-VxnXczG=r?H zfw37PnIh^h$!PoXrko2nn7$p8VG~Ot~xy#&P zXmG6mK+=GEum-&-1hF>aPd56`YeXjF)9$O2HQR_coY-nD_>tda^oAy>FPV;c>$=GjdZ~i#udpO5~vMD&cCcBZ)B1|8!i9z#*9i+o_{4XMYtZB1tH(zFl(%K-O+!>5xI;3h& zV%xApc^`%F*6%qM!)-~zVbn|pwozXkSv9+koP?s(&Vr*JVm7L}P7ja3dv4m+rdUed zlI^Ou9)KSv_3#dic^zKb2}rchOLhRuT9-=is%Z(QU3cm(%meeepilaWq9=4jKq(Sg z<;$u8vFP@w<>xOI>EsNC=~j&7St_y=NYH{t6OgV|CV#HrSEGc%@05kU$i?vD|9MGk zKdJ%`gx12`PBecOgU?DSGvq)|C+~oWp^Y+B=#z0zI{bydaqonO$utNJW%#z9KF)L9 z5hRW=FZg>vEYQ?th2wn82wauJ;iR78T#$Uc#!bJLvXID^W=8C)Tg>=!x(=<$Cv#x7 zpz>oz_M2(0yuOJjkccVSExDc&s<&UrtQH4g?EP-$YH$b2P^?4Bw0yxB7+x=?S?R18 z900oQydUg$_JbZ;bXY*<&2HLW_*RKix0OBX@3?9q&xu8Kh}UNCDqgW&{v1fze= z_|pEVU}SoP2Zr0IXTABvNUW1bLi;(HmbZ%h;<${7j!8DyPBLZ(`wC`IA*1cQT)aMR zrZXF~E>|;#O2#PB86{!>F=eATdD8?6b5iG4Y4NPYdV~?9xBEJ3V_U%sNx60=RRLj- z8EnTOYCx`fPa>6Ne$S2E-rr0K8DH>tFp5vGE`KHn9o}#ciNf%MaJtr@!F{lzrlAfs z=GU(9#zg-m)PgZkY3nGt*&4-mNK|;u_Vf^1&&r>Nn#spdHP2{9DfjV5{MpBpnjemI zfG<^jNqR-@qu4=@JMz65>mnzGHO7otfk;f4gE||c)DZjG7EPSXQ5Xb@nRMps436tb+!Ga+b;K;Cwz8c!6y-1 z2cP9Vaoz5+_1n{nL-)SFLSDA@MnCHX2}arXLv^AYYGcUeGO_=~i#s$n;!%S>ftF8l zDMw6PdTi`mFdMYG#Km}?XT@9BvP;ai9Xp~--K{VPvJ6hVEAM4;PiiJm6G`@3+DiH& zwLZl%N=0G`|3~!bN`BK=?C}y#OW^)Fj7G@cMvF`0%-VuZV6KAo8N3ex_1=o5kRDdJlC zCubNyaI(N#^k>+NJp`JT2Q(I51a^voM|nJ{yyj-O0Z3$D@{IW4Z@22rTtY~(e~##K z^rl7^l|+v=3LDaWC0bkQ<9f84-mk|ozmSIJ{h}Ql((c(MZsmKTc|Jnq-I%Z|^jEfSVz0x8)-5~z~T>Qi?q*t5>c%j(rz zLQ_?NWc0%O9=!v`s43J+E{0X4LW>GEk9qxk{xGr zX-d0>v-YUF$f_Vg;#?a3guPLTHs9Szmj~k+V?M<=Zax0QWcby#2~#2GYP3o7^BzJK zE`UW?m^*W1J2V<2C%HFBm=DONrMH^Ml!lRs!ZWiD&}d$Xi+QpYlNI zY1?r8r-=mxJ%tdl4GjDyEc@_6yvcUXkT_zaMkIa1vsrx0!_F z2zWNm(PTjEt%`U#3SiaTtY}v{3s?MNg>XIgFwkF5V!o!c!K^BN%*0pVFq0=@69t-A zZa+2)$Hk_{rQ6p)Ei>a*uI2$A6s=uxX(wArNEA@pGaSK|`yh0N_d(`hQDO#b)(EMbXL<{A>N5FblV;PG<|2lp(`m-dsJkE!)RgNO|VNX7wi76G* z;348X=)C#`5@}x3kZWLo0fbf)>gS0p%GcKf#ZasNtk7aT9?*W-9aZ&IU8i`qojcpe zv~TJ(wK0(<_aOgF3j?91$yY#@QPB%r6;~N+UXhYaUqf-0`+-_w&>BUoi4B)LP60v@ zADwh#n#@!IH}N(uMj8<{`^2&(#3J~9J?+XYhYNs&2-f|&w^#cJ{`!SC0^rswqpA73_xm43y@lf+;~|jrY5oh?3r~M&HzkGh!-$on!s6V{Q~U%o5|h zI{z9?afikrJlK;D-db*sI!R~fj5+#U!E@{&?#ld|6W7CH0b|rbE(I@~Y!s5M!duPq zFksv~0g1o}9a}wzp6Kn?M8BZb(5tjAj)P+#_Q<%Mf}^H@IkpW%Jl@0} zeMhFerQ3h>O+`p(`n-;<^IU8I^yvnZzm>tX|*O3wbebRlzt%Nv! zvNga?b>ln}{-c5-ik7NFD|YN}MLE|g-je$9f}b-&sa}_o4<9;zL4V3eIug%qA$zpb z_4oN@?41Aq0i8f%zeaP%sra)$?{t(9u5A4h>O+u&GCQJRw{OhPGGKCUS+XbQd|S1c zQ=f!_Q-k5pMel^HpkzU4vI}gr+=u`M86SDyVTjO9-3M&>x~F zAeF%~8*c}lPB!D!rt|oAW>Z+uIT6=GhIFMWkbdMlyO%(IwiHD(R3bLLMG$^@-^c&l#q;2SitZ8IGR7Qk0avpily2g%ErfhO zCMt#}qzmGs2M;Gu^If3{~rh9+(JtTVnz8!`fDLvK>ZIt6cjzgl$XWf&R`N~ult81nQ-_G*C zn~H|K$V;IXO!1q-ty-0XL5?FecF$^emladb;EH4<3`dTm76Ca9irMcWav;fd%N0P* zTQd!Q)j1V?{#-h{Ro6x6$G7)md}Gc!Zb8>Noll=`1^;g-Cqr}=L>?(Ajsm8|!iv5} z>UGvxEp3tlr4=t6FmRqP_+&|%Wzwa_GYJHqrkv^cQPl$?P#=fb4=fvYG)2z5M!MRO zAS*Vciw3x0Ru?ldXxp3PgTsrXlM+P({cy|UTBK8uCFXtqHe`8r+6d<1{!XDEIS!!Wy9^g!=0JBkW|f9~PnljCjr%H#GnnHA;`jqW7){6(ng>zJ z3A4Mu9Gxh~s4TeI&9c_K!`zd4Pr0Gs5D(G!1^#>KwcIRMEtmiM!WlU`JpJY9h2@yE z9=I(Lck-`3eG)7sfiT7GAo9saq|cP8G;@P!3K?SiJFt?j_#d^J-Ou`@wcg0#zO~-(9PO z|J@xBTsrUePv0E9d6~I2Kq#(HIPo~~(RR6%F1{` zqiMK62@#Tm^dsgJ;Y%9mNS`F12}6*GJ5H#d%6JPyo8@+X2%-ti)S5o^ius)LIPBhx zUOAlhz$X~Ir5rViBSvwIFFRkpE=n8y&&7;{F-e%qT%8{F%E z?&A6MsrLX~2J<1?sP7!* zomZeplEypv2jFBVz2w|NO*S*pfgmqQ8hPl+L-=PfKT9W*;3Mifd1vDAIFh>c9(3{; zIW>mNw(xt3!(b8+AK^Hb5u2TNMD`Bfb1{YhgrPCtGBKHN3F{Dlm2y?{-RK2(_ z1Fk0~`s=DRi!@@r%fXm%Jm?ng2&Iw!zm*5b0}$K~KZ5o5QV&s!RCNNS)d3bsa2%EX z3Sm__K%Eu&6==4aF$*7dYe|ZEG~~ZE9M=%KFD^3OAG)Y}(Q@n6?Gu^)x0>Dys)siL zpp?Ja8BL6YWa6Bb#b%ZL%=lgl(seB|K|51t@UHLSz4ZGKiv*M4|wxYp*^M*QYB+7)-M zp?g|s=Vt9{VQ&V* z9dm&ZY_WMhM-{DSl)G!hV=P+H4x9BwrdMJs zF{79q5Lv;htVw4b8ec z_(p=T_G+|5rWHE?M*EFc%zu|BVUlKpMo0?@vR+$(wI|WC>bbXh94>43-Of^@OQGdv z_J>B5J-63NI5V?=EyJBPW7u-k*_*Y%oNjoVC{!^&!5t zYTC6xo@nVPaP9uiam}-O5<`hHl%B<=Wi{jovkliV#!zpQkvi z@3;|Jbv;xuwjSP^v9~G?SA^F@XDy1Gv_h+*`o>7U`)%18u13UJ{@u?1w~ha|yui+J zyk;@bCGr1z{r;fj|2^32Ke_k+zKh2QQl$ync7F~+!rYf39iy(yWnNNIb$9Gv<@xWs zI~J>FF-v^*dnHqRDHztSxB@Y(RJr;?5c$Jg^U;|Tj)g!q%*r6QjeafU%tk6Xq0G91 zK@MeCu63!||Ba`KIhZ0pyEwW)ARUrGaVMq7RSMs#s z|0PYfv#LC>g#Qor9+l(29_>B8&;N5L&nF`&v_=3>)q)<>Gk3F6PlcWIV=vwu`&hSp}weO`&tIYmxatQaQy;q+g49)k6wp_@M$S87B5m6gz8m)=?$z@_xR zBL5E_5BBcq|6M#KExR@d8)+@A1X&TL^~tds=v$`5rR4Ai1t}p@hsc<<ydj zfVG~hN}j08Pwa;wy+T0*r4h}VJn*}dQ|!$;W~( zZ7yFS&uyu6%j%=8wni(hl+ggV6Vy~IcmRC1g2d8Dtd; zhIMYYW;R-z3a#P@S6h9;7}fY@`3&q?T$*NM@z(y*`}qD@?`cQ>VcTQRqiMufX9G*< zfB*55hvoa<-qU;fe<#l;OO+6{-ur63?PV%TAITh$OVowx;{v1jW-0%QJT9l3UwX+4 z&kE;Xm6hE1`QO{yE6aa-4+r=DzjyK&=fBPdP^0cuwMy$u1$hxt*P{uc4AP{RDT3<6 zE;=rW`n5_&HIs~tk_2i9RV=k$pIy_oaP$6#saIRnm21wHtfeb9F4{dxnhdQOBashQ zUxiq$t}v|yZD+4b-{$(1ycq8CrLVRw>0R1Rqfl)(Z(J(EyL>`5{S;=E?N^I-5n{!2 zHBepEj*6w_7FCmfPP_WI^0ear9CI>BLq@m*8pst@LCySsZ|}*&vi$#WaL@nm=4q!~ zFc~|JV`lJiwKvYJ`k%J-0by5lzh!=^S(l>MZ&vYzm=WxUgfS@f4ESdSUSKW?VCqzT zb;p#tVwxx#C3N>{9>!)6&x#5`@q{H~%c_Q!6*tAocA1rgtunqUE@|-Ejj3-HvvR@d z$7Q~4Sx5VZzezTWI-iyL0YFeu+uI`XFmFcuG$e`XOd&)YiWlOgNx&B`XvE1!UasRTsA}PZ zoC;0Y%Y>$JVN&JZ)Hkb4$EI>KXJh`JahI$>%d)HTPLtkl=`2m{KsC)vSx;a_v{wKT zJNZRc0WH6YNcI3u+TLtVeMj()4Cip{>nV9{HsbfIoXf-i5c_@j7e9XM%m*aHY@z;c!fW&3*G?p4}OrSY%Dgk&&ArFNO$dC0h2Ji zKrnjR@2}T<5=4HRmdKe4=wm)ZedNsXNBQpoaw5u+BWwD&EW$CHQQj@Q4k_b5OlTVU zU386pEsT%QN1RL<@@DiZLe6Q^K}^zUpQ&W;!&MG!0|7uN>$N}xs#~aOHd%MF*=vC~ zL)OtJqJ;enzr!{wU^HauxUre~y@6tRwWoF^p>FA6Uis+W;z3=NX{@npTJ4d_Vjx7K z%j#}3xY0TDNW`m!6nxGA|GI&-bmU zw5Rqz2of9SEGt-s_u};M|XqQ^vVWmUGgvGjnrG>?oeF%a*m!>Gb>N(XTvE`K5{PqB#Tor z8J3t-Gi@|;Ojo=E5&A$D!(|Q~EA6cpuil=WAD&+9pPXF0*?(<8c#XmV$JxJ&7d*=# zulHe$e;l8k*ASofT%xY?Mv_qHW1|SdW;gQ&H}IO9_AA_|Z8m4Y7c~bf@5LA+lR8dT zaXYrGY#3aUEdQg8a~D1^aYR1mx(_|6iu_)Y;43mbS}n(<*^ijPH;{C)1gkAC+9qJR z`NWFb5$T-J2PDJ`o6e!QcPw8QyNhd;7sZZZyXrzbTw3^+_fh+Q8tCu?uEQa?A?ZKa-8+-~-}_UbY~6T>T9(wE5qE1Zo+wc1ypFo3Wd zI|v&|EVQH|L-Q(?vI5mCmSv&D_j;l&7VE3Xp}4HDF{!O#lZSeg~cN1otcjMG?d zVKqinTjiAY$Ht>C4?e3gVbz}aHs@U)Ws}L|BJ;{(b)5$OTi91kSSxKRPq#7A)_f1C zl*d^gYqvvpR-ig-fL~kbrUe>Ix9vtQV&x%Z9`nUPkPOkM)r^kDX)PJ%mK|>MTtBgc zm&QKkqx3p_nQ9l*mY@Zd1eTT9(i-45H~<_+*HpGz>e)V!dv` zxg}M45#nHeuDz|a%VXJp0#(MCH|l`G_m{(rZS|L5Vu$B!PB^Zz{AyZ8UQlV@GezB)c7^QZiY0z5bF*f;pvx!0HX zC{#&!?p+Gox)d5&iwXjPI*l7}D$IMj_bM#mWIeY+TjO;c>{LIiITn^1-C*E#TnjBt z?|lo=pVGH*`84mH3-6r^^XJ~V@ZPzwFvol6!vA3B!p-^r-KyteS>KmUZ_QJ?&t(s||Ic${9&+?J8sMs~#uW!Rew#~RJ zo#)#-!}sp2Yr3BR^Fj| zPu_b^-g{5pdrw~LA-(tHz4zq(VxGLWSja4Q{IdRRECZN34j8 zVlKy}dVAepEB(DCjbcTOU%fWfKyVvs`MSO>>iK2LdGY&qTJlH3XW+vaB*aJEqX^N& zCkf&d$ysZxqXkV-M2IhbVlnZ8$pT@Nb+c{Pn8I%~$-tO~bc{o{AgPzX-|4P*F+SKo z-@iCJetY`j@Z#<1k@a#MCqcv~sGD6PhJW>Ye`Wse|C1~T9xtyzA>eWbsy9z%B+(zFP*ocOqg*VzbpoRAQVdqUVLXsUW;1X zAT*WO*qznh;d=K8dtYl`E9~9f;C5l}@XOt4?`*eh?%r9`%C)Us(9;Zj{N@8cWnAWqFcnPs=%gdqmiqS) zy`d557#FD;515r|W^3{#}lf8TTe-}?X-}c+#5@Jt%%N(L2tE>3Os+_(&Jo-^dW-M}_LP(DsBTollx%=sl zLW=$8IK+`B*j2%0ywFegmGNO$-DHZ5t&=GApG_wd%-b=>8*|3Gm{$m|7T@b0^xsx! z${#o6MA$gc-q! z58gD$8E9=zaWvKHpSoX%FuHwxntE)tI_p50T?;KBX4gd7pK)unAiebQ8cg(_hoW*m zoG(pWzk#Qo|GP61&Vy-06LP~bmhk_6MgKQ=@_6v*p8wy)<4yzKIez!%@bv6UPv!pk z0#E(V_uAMOpFl+OR-CyyWBpZ~jfw$O=O^0WVa)#<#?|M+ct z#(B(!z24Q;mFeS4lWA`U1q|T`(eb~D$DM?PSXTMZeTe5c!c#IAf)-maPUdL)`1IiU z>$4pxzTpUwc|5}`V2DNt{5x1M4#I#hKr-y?sz`0HPfDVxmEBGX<|s^;YmP6N^gF`@*~5wzmp@?;u9t9HB5^oIz(X zPVk(FZGrd+WRfP$N;^DqMRh@s?3 zej5e0wGfMu>YsW(2?;0gvx#|Qqgxk2HOpI6QJs)Ey(A3jMK=*l=P`{)#2LbjQ7^z; zAH#fm$mI&5fw4>EE-pB_$#Ew%4CxiXDz*VLke;o|I4rDyIyA;+$cN@L5+vE$U&?!2 zr*n-?bbHk1xkd*>x?72TaEFf=Sk8T|)}3qQtn#20zu(w%vw;E7kqXs;FsM^94H!=r zqk6=?gwN3d@HBKJTA@TWyUCI*TN5;jg0Oa8%UTvx3Q0;anT`@<+@jxj=CrE#!JUoRMk}?7!j)4p9=F1Am zC=JjC5x2ae+HcvuqS&7YQ3gxRx@KM6Y+Y{!P^CRQp$RYdTmTe=G!s0nb`(EsWW$;R z-vXe9eZ9P-*XT6=5e}(+MNJcZyooj49H8Yq+V(!n>?LC?>ZA(dBg7_gMZ`&hH^ywpE#kRgo<=z+Uu>S^;YUDjKc_h?1wI+C0&XLqR?gZU&<(TKYcyg!A>5m- zvwa%@wQ|<>fNq$*UZc@|c!d{@%x_gdYZLQ~&^3YtJx2#1#|v5+6al}j=NgUr>yjb` zXej`?xwmWd4}2;0=9)u8+5zIj-3(2Fkbn-KU>_7N(IvXX$<2j{4e(Km8++3=?NDY% zGFshW`YTLzvw)_sKO4uZXc+G1Q4_sbIzxNwGN5r;8CD&yUX^mIj|Oz<4DGGUfPyQy zPHS%K>l(dD=VOwH^9LWZtkB@bV6smYjRrR)MveL7t6IP_!jbVA7~ukr=F}%H+7Cl| zMSLab57CH5qg^q?oN{u3<2YOxe_tg5C*tprdN>?`F>FL96FY|Mepw4pd&c?#AI(=Z zTS~B_f{Z>BB-uoR**SVcIT@lC;Fv6~Ia4SMDYz7lns%1E8TQhU6W=BoDc@OSd8 zD>&>V4S~U8{}M+Y@e9P65zd2Xy51hYMx*)S!WeWVr~%Lh1B7k7bxjgCd^T`@`D6TXz1Va{=G&cOY*J}+Gc`g$W$DdA!~-+ zoid<#QK$yV$N*I4wLy?)2H|9y&cR{jwgBDSikfAmYPu`{D)>Aw4!E)_sNx~dhD??EOsv@K=aW1ELV3Be%NTQI*PJsTx)4HzQgUpUiiX!WsMx9~~?v?Rl; zLyr|u-NO=y+fKG&MQ>&ZHj|$%>**RD1VCAFvJkshaTt}2ZOv)4n(BrDU2E%>psjAz zA}(9+!`lRO*%d}yyy3C}P?jK+{=HK3i5Q7kz=KPHhY3yQat|vcRGiUk(M^-J@nqlz z05wB3TUgT!_gx(q3_+f^{9L0^7Dc+Ia;)G5wS2s?L)9HU*J$+k(Zjtp_ih1D9c8*i z-Dz3TEI#!XpADQ*OVD}K8QnKx(@W|E)zjL*G)@(L`h0`dCZNTj^IED~icJqD8GLY5 z1BOOs;Q9Irt@6MdThY2O^^zfY-}e*36p6(NL0G9cuVy4d?9vmknxJk9(D%RHpfv#M z&4{0dB(dxL%HwI}m|&nEeq<IB_R6+uNFa5?LZ4LyY6ku-zBfif77M_eJKcYj9Hq^@g7fZ(SpsZk^6Z0J;yKA@SYafGIt&PYL$lB>^YLkw<=8 z+5M)RrFWTkr?H}^G8OP$0DFx_KfVk$x1!6$z^Vucx|CbxENbAko@aF;mMU!#5X-5mqF zIY2oLNwVzng09hd_T8O=s=`6mZEXM=Q-6Q?MeQ2B#t_DCW)kBR+@U#@vD4INn}>9B zD;iV(6|B$_s;dB1t9chNs)08JXr%;Vjli()`+>@Qq1eb=R!qEIn6J@I_HGTJjc2q1 z=v{5y8v_ciE-kssYU~Ai-q4kF16x-D+K#)d&R#YJ?8X3X$6eN8FPi~%1MEct+K#)d z#a=c7>Bd&H9d}uky=?B3ZVb?N+~o%B<%TeCjOw=IE^D!u4Q<^U1IkG<4@9#J@^{QL5FCx_xQ={V6-b{0)O?b`pRN!sj0VCe6BmAO0TP1fjyLbymhPE2K2gR zKqu^FLemup9x`!V*Ng{07D%a?MM&EN?)*pFcAg(JQ`T~LS zXcWmDe4FK`8(7h$h08~U@!%IUCKH;FY~P|7Hg3RF0c^h6eG3CW?BAeu70|dz0))6q zdWGnOlL#XBNiw0yTndB$>uo*PXs}s8i^aREev;_jj6?=t;(LZ@dfT$mHF|uL)>S~u zHNZ#Z@m>c}Fi+>=FbY`%yx>%$+!_-bBv^bH_#`1dd`EnQC!8c`hA)9VrJhHKPyA-n z_ceO5SwPF>(^~(EtB0j;Yr}JM7sKG2IwDy+)%?#&K~2+GAdVV=_ctAxd<2OTWr$ zQQe;gKR)e#9@#-{^v1S%VJE~7SO(`U`DAZ?O<&U};54bBPBQ5Pp9P?xSArR*Ixz06 z(1-LYGpZKEB*EhSdVp@29BDvtwn+0fi*8G4@Ewul_hzU}hhnAVOEOz?;=C?aiOt+qyx-Npe|M9bKa{AOcAL z?4qNIR>4ZIYh#^sfGjJv5l0u6{;L2~PVQXFzM~4DN+@0hPSs=`FgFTlt6aO^a*o|H zppCh9zos0!j^@})<5;R-F3Gg}jb_+Ic3_ue+x?ca>{bD7$hP|}XW7l^ZVUT&z1`as zcy9;L&B;bB%)(9P;9CWC1Lj~2Xe&d^w`_p116rdH{FXI=c0g+sg5Rrs*FkH#wkOCzxY$J){cHdAE14J{)aMKqD^TTKRTY|yTI5*^ zm6|=4i8Fk3HPgBEjoP+Qxb-HhW#IOp#tU zEqhw_A?larRrlQ>LrJ~=f;o1{DMfVN$>}aI1no4VW2bmMD3zUkT8yX zdKHBf%Q{Z)?fX)n+VcAyMZBmuZwchh4OJRsFs4K+)+=A_Xnco0q+{aoP{GDH=46tF zg0ajOMrnNqb|i8fz5LG}>gxVm=)GdB=$A8eG@qw14B~id)V=qq$6TfChuZg0AGbl_!P$6v6C_~Ir zRrE>3Kz@c5Fskuqid}DpqiHz;Bam5nAZ;|qQ?O})WZ~;FSYqpuMD!#z*`3aFi2!+Q z^par|kt;=`p$90{&RMSZWMVM+*(FJsfI{U2f`q%-ce{uM(^*)F09@{$1N zjV{wLA_*P`VZa5Og!l|suA@=f3zFaV%uKW@8!Tpu$iK$1;x4cwRgv?MQC->}6yILT zIGqQ7NOpELqam@RTosJn_a**ii|kZ%Vc1@2YF)9a@c9)@J_vke*^<z+7qadUbszWpub+26yPeKagtTLVLIfj-f~B72 zvzNnnbz*69>bm5kcdHE+ZjN_3(V zekJ4&av4x?0D<|d-3%2<^1>DJk|@ZuSk**rF@}tkcc#QOD($#Yr*kv`#P|>$a)qTx zuOvugdp9N*hCK!mU3zDDB!OhE`9?7CuMb6*xp*WeqvoEU5AoLZdrjw}b3UVtsFtgq z4Bro;X-}a3N>CEDgw{0Q<~~*;=Y=06BQ=%aQmrV2;jqmuQ2ilXWgE+@`0AUvCOh>i zd%3iaVH-n@w*ZHNf?_mD2%AafA9+OH&faUDlJC2m>rMkcOUJ-(wV_@8*O49u28-WO zwoA1x0(pv|nshl!L`;>Q)H*d69Yu&HzF0Vl$VcL(TopEx?LoF#pn1x;Ty;-IGlB{y zzkKO}5F@$E)tpJhlSNE}h&wWGvFnczqq0kNvl9YSI@cJprV&0NUKT_DAR z5Z?+}&5*3>@W|1ZY{t4w&o`#5NYcQhW?D-!Q?t~Y0i{WZPa@G?TqN`IJ2{5*{nws3 zI|oB4C{&_WID@kAAx;PyxokGt7618Nmz)y+bfpO^S#~DSz??fq@pf?awlRw z1v0}BmMJamxh7-GXIkpZD6`w?$dcx3NLK!n8U1L9e zi0BpUUBN&EH+lKua2M^rJKKdB&x0rsR|dCpM&uH`FGwiAZEuxoJ}?<73BsWIg=DGn zD9{H#VC6)ua)DmUyO~@ZNf2n9>5bsjs@q@1-&e?O@AErhzKjLeT$SiNYaRJOR?s$N z>HcZ(#g9l(2||*%7CR2ArS1V zE`U69-F7>jpJ`%F_Hy7pM*l7T^F&g6kN|)o@-&GMjY{y8SlfY={tigAF#L&g?|)s9 zFK22*Fd&bZn7_bbcp>xKsAH9NRo1p>^@tk5_%ahjyAk2;m~iYCv0cRDvroTvgGlT& z_G@=2{$MnODgW9X{@OL*`nCJ{v(mf06Oup?9}3C4h$nb53A{{_hXgN@+W%YN6e!r^ zM-TVZBEV*i6XFF-BV*ULiiPO-XVURYv4=bz1$-ninAPH#l87WA#T6pvCEbsR2iK*1 z)4Ni6AU`tv+T#!}a(YKo9;Z;22b)$sbzbD5I{-eW6(~SZ4}c+~V}YRnhZSGQPUmMq zh-4G3QhDU|-Ttm3?lEO77)u?5ltwZ=dIoXBC`#e;oG_?EqJ&Gmiz9NS8|SXMaI83< z>KO!cv4rZHIj6o5PHlojo+ny#U?#0Xz|36q95bo2+O6wA@KRO568Ko&T6Hhl1{}?> zzNd-h7#o*M*swO;q$aS+&XhgUdR6vMe%9u5j;GHCefNp|n@SnFXFUOCFP;Ll_BOUO zh{MU8<$Y(%B+^9Vy@!mkL4P*tMT*R04#?@K!s?;tz$dZwhP;u54nuRFxsuGSU z%ZgZ+B#pAPWwM4($$?g0Yoyv)WjHgczZdhy%WGvM?^G}J4PxY?ee{7W(3nJI5^&il zU}aMt%cWc~n+et08|8~UCW7ZA*y9jX2vRZ={3gbU$V16^4 z_yq@N#uFR}sRz6tj$;y?&4P*4SfjTLatuvFI>uqXW^fM&e&!yCKV_X+0o$4?ja!HGZmtT8}OIr)e%gfFW7PS0O#Z~21$F>6HIbB;`iG~JY$Qqud zW5?5b;JWl`Pc>X`#PU`})$Ln=G242X4+x)$!vS*)I>e?^ylv?okZo66oTfHa{K;DvR&u*JdwpTTmpBMP!w^KE zGgdr5J0FTs#?u7*q|-U#-!l{uagV~u0&t`B-qQG?lJ(^k(WPUQ@*NgG;RvO&H&E?^ z>0Az}!kvV`Bm~ZaF+7MaaTxeW7Sj$F#%(bh%SESTPGGCl%}Y6SDIl#i#6iz%3b~ov znQ3cW|wi%+>U`MNZ@V{xI-Ah;z zZ@X%ll+_8Y#Hvgal9n29uSv)MRt#ApO*E7KW@@8GQ++ZFvkfCedJ2*xp^1{&i>MZ? zSK&XTQ)ZmEcO(S*Ubh6rBgaR^mg;nl-u!%w(6eWMM-LuI zm$%Y6VT7kKdhno=efWzYxLD<0RW|k@LIxdpAg;WE_c7NsM#xOgr6IH*u3PxJ>@);k z4ubysgoFLVMK2|-mu^j^)+Gm{_AL)Gq0N{g5G<|C@nOcrRxb=!9#h$ovj*eiS5$I` zqDi>eA$XO_-sY4OGdZa7)fuO8T%2pMn+Ff7i#Z9RfX#^iUt(F8?|=snItaZV{Dyws zKi_{fM7b-q^4v;5cB1nbbI@!8vZMo(YI$&YzJDZ|o@-NKu3Wsr%&_>&ydy?MFQr2r z^mkQi4`nthk7rVUG#I!KU0`UMFL>a;BbjvQMntZV zOmn1(4UQ0wwmRZ)|W6X=Dwf8W%GF3LxU9h2eX(Dbry-a{`XM7%Ry*+t(x_@x= z=B4w?(Y|x?^5E_3ll@n(c8ob|eOJLmRnX8XyP4q$@2B%P(}!!IchQ_)O7@lzFf)VN zI5IJz9HAab)_ZPhu)`3v+PMy@POp`RUNgPt02;l7qe0(&>h_)F$&cDhpnnn*WHu*; zkWK?nb`w#bxc`<)Wz1%(2aBVloOq@pE4VvU)Pf_82<45fZj!s=b#{}C6B?5wT$Iex zBZDMKKPwmF(Ut8obBR#%W5o$V-B|xJPMkVst5&Gmx_V^(&_+v zP~R!WA&x$n*9YyxVEP1uQd(Z}FG)yaF^k-D`Jr(%GIavZj;YVw&XF>FPJ)jhW=MfUDS8dEqv}RY(lC@s*JmV*1xi$0 z7hs%@2{1$bm9Bv=X}}hGo)8R1WhDf6Iu07atodRDcAYUH5lV=RyW!MQJP)o`rJ6^s z=%-K^sHv~Qhemhid+4!PJgTY7;+ICBg&!0t+{=s;w*B|FrC1d~$8&$j?WLzT>eqzY${At`Osz8z3~Di zX%q#~w9`RW`?#PW?9zi@#nw7f3X^TA2>=xDWV}gHCtGAgLSm{U`Q^b5wXr@ga)qKE zGp0V;))y56FVJTWAR3tIheKn{caWjQLajLMGw$FnWu zCp6AyQo!;5nBS?B*#66ukp#UxJ3Os^0qSOy(2(RGs4J*O|M$<2&fo06J_2iYwu8Sf zbTdr^mIZ~2li)H4$&^5Rw|$NReNEG;X+^J;6lJs(JMAy`&mBKFNG2h@>Ph{)!Mu9X z$^GB{@d^O%m3{U_8$Ia@TmW|-+!{V_qhL;{@}^eN00x4 z`Wu9^_DO{l|Cj!a$0`H&jXW=Cyg)j_I;rT-aOC%BVu`%d(T!Y&G{{>kn&a5LKg8eC zv+Vq7#z}{O|P#dxQJ)e-}^7Fv6S04thm0V~-#df;6cF1qRW-W#mGM z&`4Vd_WdjnkbIS4fUiMTk0vyWny59%AbHHzi}ERky2!wQ<-~Z|`L7RhYflrFo&QJu zz0&!AG#EU&KmT{}{L6kom653!v__LWW=+xn9p)&mgDS4lM}8igE^k(2O1ah_>D&V= zoQgG%Ltq#V9SjjPIv{k%aCCRS^a_00r~drg({`*gM)Uc9*xwse`2W*~5AV+IWpr+V-r7PZpy1pE7n;T}-E!kymF5Xz{|!gx-Q3LO=P@lLcYq61A_jgT zO`C>e*|c%8L;2=ZC&mZIZ}H|NPutzu+Um$&h8<*Fc!ucH=gt;F#V&?uzR>@EgXbjw z!sv1cZxM2gkN-&)^4F}Da%_1KV#Y+bXU5;)l~n7fuQk+LTPPuuA*zYXNGzecJ&vcW zCt!xI1jtDST9(%z-fD_CMAdFO2yr|eqQSs@?Did>i~}4wKRL;heg~ngtt{<1&XJ?y z4&i4NQ3e9l_xs@$Ua;>`Fj4TSgf-uXm=ngo-$f*3MEXfm^u3wD{d?J6h6Q_6VaiD2 zOw+(8Js5!Xw&H|1aVqop$am0_*a6v8hsYdCj&z(y_-Z*9M+hegUI>;}-U+V4hVnkt z83pDzN{fY(OObwV39iO)$?WVnG4=C=L_GnC1kQ<*;1YA<=(X-?Xtz*o?pyJqY2Rva zq-sA&iCDC15=cgLt_m~4UK-aWqR|nDm`aA8@1Okx4(6Nv*GI@F<8(R&mzoITxqUJ= zaM9$~q`r1wVAk6b-8(dL#+c2VtltijW9S@P;t-wjM5(P~oCqNyT;z1j5aG4?GB<8+ z1I?S`Aj(WO+WaFv+z|(D3z=Qq_dgVl1IO_ramYA2E9*Yi8QDojHl1XiaX2du8M$yF zDzf7;jK@_iuRr`g%V_hnzii5g*@JlrK(Z8$2#=%n>j%MOmEnFanH;l_9k7^=y>BmtKZ{u#Y(Y~W(?5zaz9Ro?FoGqe0!b3rYMfm>^ z7JJg9jY$Xb!xR`)?E&IK)$mpXUA{R@CfR!mtM^k%o^hG^a72lMhrr;N(u%nO0fKK- zM+IBW%LR{@(Q2_e^R5dwI9k(Kq#ol6K1Uu|_V~8S^2lK&`luG{sw}V9t|hPx=gkTP zFf>J8JxnN!a!5OTdlK(k)CG9|m{IHf1vRJgOsiiGk9VOJ95Op!tvY_a(#<$_9?|4=f9L%_F{82m~(zqc5 zJx73N#p<`j8jH5{e%Yb!MmP1Gq-4s;vRJUhaA>_AtlknkfXTpw`1|9GucD)LR*~Pg zHyD>cok%W^f0l`#mcKC6;U`J&SsnW)$97xSPYA3j)$%cB2C{Q(wq_74{r^ z{{wW9YX70{iS#Yy=@Ca?f+o5QB-=qKkr1iYxk!PdI_<~YY{D~b}qqt!e@6km-sg_z1XJ4aBJAKCbB5Tv$GfrDszHA@S1HGChFzl$lE_+ZoOBX|LWeSkQ(sRiXl zkle6)7YwOo4Xfx7QFrZpH%H z+uU(YOBMCg?5=Y+(u)-Axm6C4=;EB2v|P*8vcjyPt!EzOAn41J+X+uwXppLT{J0r^ zW!&jdDh?f_@%mpXl70CSBdxE?lidBkh^Yc+pZLuUb#4FCYKkyv4125F&d_w(5_xw3 z`&_yc%&KbF^AdT#$yGa$jux?#)jM-B(E794k~AyDz`laL$?#H%PPc9d_Fjj>IBLB1 zO?Dx;vN3~W(B8nrN(6_qr5w&{x3(qaw0JA&P8zl)G@O)OWq!qK(M`?dPRCccUVCxh3QWFQswaz> z6z`K~2hE>T&KBMQ>;gw=9?&R;l*8%x6V=%KC)jH_15)-}Bu@V)AMyPoAN3#E>g-lI z7GaNiZPl)+)KWwT$rDxI)(A){2zf7>-oEd8>fpL$1r=Fhn8 zAmVVl5zv!k`+0bzfAM&c&fCzdv&jom#FC~@i%i4{T-k765Ol0JMy*SND^uUa+J_ZP zaG(1yXmd-j8&+NV_P20!bF|Zff>*g7Vkt^-k8klFF1C-hx!w8Wv>|W- zHitF{RLG|{LJrwW#;>zx6OBwDtvATmcokP#wohqv>Shq7hhAxE%rJ#d6C5g;9&RLG zVu9G*Z;uolelNr`V>>~!TGEW zpSV*uiG6gfx>pbH8T^Y4np?S=#TTP9p}|FZm#ZMkt15BHfDRnt2cDc9HG9O97<1NH z3K|?%I)TSH<@OGIYIGc457wM}D7W5_;dl<(LXpXPJtoxfrGFxEJ)gibHl4^?+gRx4aBy}6lN~+5Non9DWya#( zh4-mTuFQWjK6sYf$~>3IiJu=jX&n_WsIhX_8vi+JrAzdEDqdepl=nr*V}6T8j;IFO zmR!j`7D)ta=DMNAPnQVqXQB~{eKQQ#YW#OH2iav1XovYL;#_Lf(b}x!g2sUfj#tT< zQLOuzr{d^#R>ITfjBI_k`DYw!5s;QCX%mh^-5ciQ^z^Nq3$Q5N&xXBHU)f5It?P9j zlU+#-ZZm9_^fu|=d`SD#wsm`&#!9h5bgMqxiJu3I@ut9;H7kqD!pydS$oW?&C&3x; zQ#i};0<-pF$e_+HO+k&zw*?4KfC`EFeL4QWAkn=vre7A(mqZ~A+xR?g9#9tfHPoWi zwIV8lPaU!D5=(GNsMupKfv%n?e*ac-oEJJUxb!VkKRj=0%aYVV#T^nyT|VIptZ@Nk z_Bl`x7vB==PtAGbW5O2Q3*rN^`$()eJ0|W=j#x8@*~Rpue>+6AzZ{|id`kCX8#|mt z-@7L8%OO&a+}-G3viRi?P5$i=rTlV;Rzam7{*pNK5DdVWyNh80DHYLk5V*+S4p9(? zF2a)UL01U!4+nad5juZ;wvpk&)o^Uss&ed)<&)+|Uvp3EmQPs#fYVdPX+>9nHxTe2 DVsWL4 literal 0 HcmV?d00001 diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/airflow-many-versions.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/airflow-many-versions.yaml similarity index 100% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/airflow-many-versions.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/airflow-many-versions.yaml diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/chart-with-relative-url.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/chart-with-relative-url.yaml new file mode 100644 index 00000000000..b345edc1b2e --- /dev/null +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/chart-with-relative-url.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +entries: + airflow: + - apiVersion: v2 + appVersion: 2.1.4 + created: "2022-02-18T11:33:37.336580189Z" + dependencies: + - condition: postgresql.enabled + name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 10.9.4 + description: Helm chart to deploy Apache Airflow, a platform to programmatically + author, schedule, and monitor workflows + digest: 0bebbb733539200082bc73baaaf44287c90874ff1ce57ef306baa13b59082cf5 + home: https://airflow.apache.org/ + icon: https://airflow.apache.org/docs/apache-airflow/stable/_images/pin_large.png + keywords: + - apache + - airflow + - workflow + - scheduler + maintainers: + - email: dev@airflow.apache.org + name: Apache Airflow PMC + name: airflow + sources: + - https://github.com/apache/airflow + urls: + - charts/airflow-1.0.0.tgz + version: 1.0.0 +generated: "2022-02-18T11:33:37Z" +serverInfo: {} \ No newline at end of file diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/index-after-update.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/index-after-update.yaml similarity index 100% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/index-after-update.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/index-after-update.yaml diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/index-before-update.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/index-before-update.yaml similarity index 100% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/index-before-update.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/index-before-update.yaml diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/index-with-categories.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/index-with-categories.yaml similarity index 100% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/index-with-categories.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/index-with-categories.yaml diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/jetstack-index.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/jetstack-index.yaml similarity index 100% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/jetstack-index.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/jetstack-index.yaml diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-6.0.3.tgz b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-6.0.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..436942542df28e7d46331d2a624bd4c9e4493a4b GIT binary patch literal 13524 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 O0RR6-{t>qT1_1!f`~+A4 literal 0 HcmV?d00001 diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/podinfo-basic-auth-index.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-basic-auth-index.yaml similarity index 100% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/podinfo-basic-auth-index.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-basic-auth-index.yaml diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-index-updated.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-index-updated.yaml new file mode 100644 index 00000000000..53163de9b72 --- /dev/null +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-index-updated.yaml @@ -0,0 +1,83 @@ +apiVersion: v1 +entries: + podinfo: + - apiVersion: v1 + appVersion: 6.0.3 + created: "2021-10-21T14:56:40.169819105Z" + description: Podinfo Helm chart for Kubernetes + digest: e30b95a08787de69ffdad3c232d65cfb131b5b50c6fd44295f48a078fceaa44e + home: https://github.com/stefanprodan/podinfo + kubeVersion: '>=1.19.0-0' + maintainers: + - email: stefanprodan@users.noreply.github.com + name: stefanprodan + name: podinfo + sources: + - https://github.com/stefanprodan/podinfo + urls: + - http://fluxv2plugin-testdata-svc.default.svc.cluster.local:80/podinfo/podinfo-6.0.3.tgz + version: 6.0.3 + - apiVersion: v1 + appVersion: 6.0.2 + created: "2021-10-21T14:09:34.137081426Z" + description: Podinfo Helm chart for Kubernetes + digest: 0ebea59ca36fe7502c8ede1ce4fc28600057f96842844b2f2c1dd207f1126222 + home: https://github.com/stefanprodan/podinfo + kubeVersion: '>=1.19.0-0' + maintainers: + - email: stefanprodan@users.noreply.github.com + name: stefanprodan + name: podinfo + sources: + - https://github.com/stefanprodan/podinfo + urls: + - http://fluxv2plugin-testdata-svc.default.svc.cluster.local:80/podinfo/podinfo-6.0.2.tgz + version: 6.0.2 + - apiVersion: v1 + appVersion: 6.0.1 + created: "2021-10-20T10:40:13.599806084Z" + description: Podinfo Helm chart for Kubernetes + digest: 9d366c9afcef90b37684bbf2afd2749633783f5c489a4145728498a54cec8d3d + home: https://github.com/stefanprodan/podinfo + kubeVersion: '>=1.19.0-0' + maintainers: + - email: stefanprodan@users.noreply.github.com + name: stefanprodan + name: podinfo + sources: + - https://github.com/stefanprodan/podinfo + urls: + - http://fluxv2plugin-testdata-svc.default.svc.cluster.local:80/podinfo/podinfo-6.0.1.tgz + version: 6.0.1 + - apiVersion: v1 + appVersion: 6.0.0 + created: "2021-06-16T12:51:07.137321429Z" + description: Podinfo Helm chart for Kubernetes + digest: aed86105891a9b1db588128878b79425cf6447e69b1012119e82c1d37c68ae67 + home: https://github.com/stefanprodan/podinfo + kubeVersion: '>=1.19.0-0' + maintainers: + - email: stefanprodan@users.noreply.github.com + name: stefanprodan + name: podinfo + sources: + - https://github.com/stefanprodan/podinfo + urls: + - http://fluxv2plugin-testdata-svc.default.svc.cluster.local:80/podinfo/podinfo-6.0.0.tgz + version: 6.0.0 + - apiVersion: v1 + appVersion: 5.2.1 + created: "2021-05-13T12:57:54.003168505Z" + description: Podinfo Helm chart for Kubernetes + digest: 6c3cc3b955bce1686036ae6822ee2ca0ef6ecb994e3f2d19eaf3ec03dcba84b3 + home: https://github.com/stefanprodan/podinfo + maintainers: + - email: stefanprodan@users.noreply.github.com + name: stefanprodan + name: podinfo + sources: + - https://github.com/stefanprodan/podinfo + urls: + - http://fluxv2plugin-testdata-svc.default.svc.cluster.local:80/podinfo/podinfo-5.2.1.tgz + version: 5.2.1 +generated: "2021-10-21T14:56:40.168156466Z" diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/podinfo-index.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-index.yaml similarity index 100% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/podinfo-index.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-index.yaml diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/podinfo-tls-index.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-tls-index.yaml similarity index 100% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/podinfo-tls-index.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/podinfo-tls-index.yaml diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/redis-many-versions.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/redis-many-versions.yaml similarity index 99% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/redis-many-versions.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/redis-many-versions.yaml index 07fb1314e10..6fc40700ab1 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/redis-many-versions.yaml +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/redis-many-versions.yaml @@ -37,7 +37,7 @@ entries: - http://redis.io/ urls: #- https://charts.bitnami.com/bitnami/redis-14.4.0.tgz - - "{{testdata/charts/redis-14.4.0.tgz}}" + - "{{./testdata/charts/redis-14.4.0.tgz}}" version: 14.4.0 - annotations: category: Database diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/redis-two-versions.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/redis-two-versions.yaml similarity index 96% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/redis-two-versions.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/redis-two-versions.yaml index 5c9828864e1..bcd4c99b305 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/redis-two-versions.yaml +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/redis-two-versions.yaml @@ -37,7 +37,7 @@ entries: - http://redis.io/ urls: #- https://charts.bitnami.com/bitnami/redis-14.4.0.tgz - - "{{testdata/charts/redis-14.4.0.tgz}}" + - "{{./testdata/charts/redis-14.4.0.tgz}}" version: 14.4.0 - annotations: category: Database @@ -72,5 +72,5 @@ entries: - http://redis.io/ urls: #- https://charts.bitnami.com/bitnami/redis-14.3.4.tgz - - "{{testdata/charts/redis-14.3.4.tgz}}" + - "{{./testdata/charts/redis-14.3.4.tgz}}" version: 14.3.4 diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/single-package-template.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/single-package-template.yaml similarity index 95% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/single-package-template.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/single-package-template.yaml index 37c0736110d..28aba8e222d 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/single-package-template.yaml +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/single-package-template.yaml @@ -28,5 +28,5 @@ - https://github.com/bitnami/bitnami-docker-redis - http://redis.io/ urls: - - "{{testdata/charts/redis-14.4.0.tgz}}" + - "{{./testdata/charts/redis-14.4.0.tgz}}" version: 14.4.0 diff --git a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/valid-index.yaml b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/valid-index.yaml similarity index 93% rename from cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/valid-index.yaml rename to cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/valid-index.yaml index f0ecba48a64..6a857034c49 100644 --- a/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/valid-index.yaml +++ b/cmd/kubeapps-apis/plugins/fluxv2/packages/v1alpha1/testdata/charts/valid-index.yaml @@ -22,7 +22,7 @@ entries: - https://github.com/wbuchwalter/Kubernetes-acs-engine-autoscaler urls: #- https://kubernetes-charts.storage.googleapis.com/acs-engine-autoscaler-2.1.1.tgz - - "{{testdata/charts/acs-engine-autoscaler-2.1.1.tgz}}" + - "{{./testdata/charts/acs-engine-autoscaler-2.1.1.tgz}}" version: 2.1.1 wordpress: - appVersion: 4.9.1 @@ -48,7 +48,7 @@ entries: - https://github.com/bitnami/bitnami-docker-wordpress urls: #- https://kubernetes-charts.storage.googleapis.com/wordpress-0.7.5.tgz - - "{{testdata/charts/wordpress-0.7.5.tgz}}" + - "{{./testdata/charts/wordpress-0.7.5.tgz}}" version: 0.7.5 - appVersion: 4.9.0 created: 2017-12-01T11:49:00.136950565Z @@ -74,5 +74,5 @@ entries: - https://github.com/bitnami/bitnami-docker-wordpress urls: # - https://kubernetes-charts.storage.googleapis.com/wordpress-0.7.4.tgz - - "{{testdata/charts/wordpress-0.7.4.tgz}}" + - "{{./testdata/charts/wordpress-0.7.4.tgz}}" version: 0.7.4 diff --git a/go.mod b/go.mod index e9aeed85d7a..598144dd133 100644 --- a/go.mod +++ b/go.mod @@ -110,6 +110,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect github.com/cppforlife/cobrautil v0.0.0-20200514214827-bb86e6965d72 // indirect github.com/cppforlife/color v1.9.1-0.20200716202919-6706ac40b835 // indirect github.com/cppforlife/go-patch v0.2.0 // indirect @@ -126,11 +127,13 @@ require ( github.com/docker/go-units v0.4.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/camelcase v1.0.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fluxcd/pkg/apis/acl v0.0.3 // indirect github.com/fluxcd/pkg/apis/kustomize v0.3.1 // indirect github.com/fluxcd/pkg/runtime v0.12.5 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/fvbommel/sortorder v1.0.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.0.2 // indirect diff --git a/go.sum b/go.sum index 8d3eb1dead1..2887ed7f8c6 100644 --- a/go.sum +++ b/go.sum @@ -292,6 +292,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 h1:7aWHqerlJ41y6FOsEUvknqgXnGmJyJSbjhAWq5pO4F8= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= @@ -563,6 +564,7 @@ github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= 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/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= @@ -597,6 +599,7 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=